perf(receive-pack): drop the throttled-SHA cheat — feed absorb-only, flush verifies #28
receive-pack-no-cheat
into main
Honest replacement for the 4 MB throttled SHA-1 check landed in #27. Per docs/PERFORMANCE.md, that throttle was the scheduling-around-the-cost cheat — same big-O as the original O(N²) bug, just a bigger denominator.
What changed
feed/2 is now absorb-only: appends bytes to an iolist body, maintains a 20-byte lookahead, never verifies, never decides completeness, never transitions out of :pack. Per-chunk cost: O(1) amortized. Total streaming cost: O(N).
flush/1 is the explicit end-of-stream signal. It hands the materialized buffer to Pack.Reader.parse/2 once. That parse pass already runs the SHA-1 trailer verification — so the previous ReceivePack-level check was both redundant AND quadratic. Total verify work: one O(N) hash + one parse, period.
Errors (corrupted trailer, truncated body, malformed entries) now flow through Pack.Reader’s {:error, reason} and surface as a proper unpack <reason> line in report-status. State transitions to :done so the channel doesn’t hang on a closed connection.
Caller contract change
feed/2 no longer auto-finalizes. Every transport must call flush at end-of-stream:
- SSH (
Anvil.SSH.CLI) — already wired in anvil#127’s{:eof, channel_id}handler. - HTTP (
AnvilWeb.GitHttpController.receive_pack) — needs flush afterread_full_body. Bundled with the mix.lock bump in the companion anvil PR. - Test transports —
git_daemon.exflushes on socket close; the LFS HTTP adapter flushes after one-shot body read. Both updated here.
Test plan
- 928/0 in the full ex_git_objectstore suite.
- All test files using direct
ReceivePack.feedupdated to follow the new feed-then-flush contract. - New assertions in
protocol_interop_testpin the corrupted-trailer + truncated-pack behavior: state transitions to:donewith{:error, _}and reports a structuredunpackfailure, rather than sitting in:packuntil the channel closes. -
mix format --check-formattedclean. -
mix dialyzerclean.
Memory note
Pack body is still fully buffered in memory until flush. Tracked in anvil#153 — the path forward is a streaming Pack.Reader that consumes bytes and emits parsed objects incrementally. For ovs (~106 MB pack) full-buffer fits comfortably under the 3.82 GiB container cap.