Stream patch blob and diff downloads to disk (#571) - #607
Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
apply, get, repair and rollback used to hold every patch blob and diff archive fully in memory before writing it to the .socket cache. Each body now streams chunk by chunk into its .socket-dl-* stage file. A blob is hashed from the staged file, then renamed into place. ApiClient::fetch_blob and fetch_diff return a BinaryBody stream. The two near-identical blob and diff download loops in blob_fetcher are now one. Error messages, counts and the no-litter guarantees are unchanged. A failed download still leaves no stage file and no cache directory it created. Fixes #571. Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
Assisted-by: Claude Code:claude-opus-5-5
create_dir_all can create some ancestors of the cache directory and then fail. Those empty directories were left behind, because the creation ran before the failure cleanup. It now runs inside that cleanup, so a download that lands nothing never leaves a .socket/ or .socket/blobs/ husk. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
The streaming test read the stage size from DirEntry::metadata. On Windows that is the directory entry's cached size, which NTFS leaves at 0 while the downloader's handle is open, so the test saw nothing streamed. std::fs::metadata asks the file itself. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit ae928ae. Configure here.
Assisted-by: Claude Code:claude-opus-5-5
|
[burn-down agent] Labeled Ready for review at
Slack announcement: not sent (Slack send tool unavailable this run). Generated by Claude Code |
|
Reviewed Checked streamed hashing and atomic publication, body-error/timeout cleanup, cache-directory ownership, and production callers. The lack of a size cap matches the maintainer’s direction in #571. Verification: 206 tests passed across the focused API integration suites (41), blob/hash unit tests (45), repair (116), rollback API overrides (2), and two additional reviewer tests. The reviewer controls cover unknown-length chunked and empty bodies, truncated framing, idle timeouts, and preservation of existing cache content on both authenticated/public clients and blob/diff paths. Production files remained unchanged. Merge check against current main is clean. CI on this commit is complete: 332 successful checks, 7 skipped, all eight workflows finished successfully or skipped, and Bugbot is clean with no unresolved threads. Local tests ran on macOS; platform coverage also comes from CI. |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #571
Summary
Patch blob and diff downloads now stream chunk by chunk into their
.socket-dl-*stage file. They are no longer buffered whole in memory first.ApiClient::fetch_blobandfetch_diffreturn aBinaryBodystream, and the two near-identical download loops inapi/blob_fetcher.rsare merged into one. As the maintainer asked on #571, there is no size cap.Why
register/20-audit-core.md. The living document covers the HTTP client in Part 7 (doc/07-infra-agent.md).What changed
api/client.rs:fetch_binaryreturns the unread 200 response as aBinaryBody.BinaryBody::chunk()yields the next chunk. A failed or stalled read maps to the sameApiError::Network("Error reading {kind} body for {id}: …")as before.None, 401/403/429, other) is unchanged.api/blob_fetcher.rs:stream_cache_entry_atomicreplaceswrite_cache_entry_atomic. It copies the body into the stage file one chunk at a time. For blobs it then hashes the staged file withcompute_git_sha256_from_reader, and finally renames it into place.create_dir_allmade before it failed. This keeps the old guarantee that a fetch that lands nothing leaves no.socket/blobs/or.socket/diffs/behind.download_entries(…, Entry::Blob | Entry::Diff)replacesdownload_hashesand the inline diff loop infetch_missing_diff_archives.CHANGELOG.md: one Fixed entry.Deleted
resp.bytes()read infetch_binary.From
git diff --numstat origin/main,srclines split atmod tests:Behavior
None, apart from memory use. Error strings,
downloaded/failedcounts, the exit codes, the JSON and the no-litter guarantees are unchanged.One corner case changes. If the cache directory can't be created and the server serves bytes that don't match the hash, the entry now reports "Failed to write blob to disk" instead of "Content hash mismatch". The mismatch can only be known after the bytes are on disk.
Test evidence
blob_fetcher_edges_e2e::blob_and_diff_bodies_reach_disk_before_the_response_completes: a raw TCP server sends the first 256 KiB of a 512 KiB body, then holds back the rest. The test asserts that those 256 KiB are already in the stage file, for both blob and diff mode.main(src restored toorigin/main):FAILED … panicked at blob_fetcher_edges_e2e.rs:849.failed_streams_leave_no_stage_and_no_created_cache_dir: covers a body cut short mid-stream (blob and diff), and a streamed blob whose content doesn't match its hash. In each case there is no stage file and no created.socket/or.socket/blobs/.partly_created_cache_dirs_are_removed_on_failure, for the Bugbot finding: its blobs path is.socket/plus a 300-byte leaf, so.socket/is created and then the leaf fails. It fails before 3362893 and passes after.api_timeout_e2e::stalled_binary_bodies_are_network_errors_on_both_clients: the idle read bound fires on a streamed chunk read, on both reqwest clients.cargo clippy --workspace --all-features -- -D warnings: clean.blob_fetcher_edges_e2e,api_timeout_e2e,binary_fetch_error_classification_e2eandcovgap_api_blob_fetcher: all pass.cargo test -p socket-patch-core --lib: passes, apart from the 4 known root-sandbox permission tests, which fail onmaintoo.apply,remove,cli,get,repair,diff_created_file_e2eandremove_rollback_api_overrides: all pass, apart from 2repairtests that chmod a directory read-only (root ignores the mode bits). Both pass in CI.Risk
M. Every apply/get/repair/rollback download goes through this path. Mitigations:
🤖 Generated with Claude Code
https://claude.ai/code/session_01V8joXSoT8cmkgc1AVv1wrq
Generated by Claude Code