Skip to content

Stream patch blob and diff downloads to disk (#571) - #607

Open
Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
mainfrom
arch-refactor/571-stream-blob-downloads
Open

Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
mainfrom
arch-refactor/571-stream-blob-downloads

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

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_blob and fetch_diff return a BinaryBody stream, and the two near-identical download loops in api/blob_fetcher.rs are merged into one. As the maintainer asked on #571, there is no size cap.

Why

What changed

  • api/client.rs:
    • fetch_binary returns the unread 200 response as a BinaryBody.
    • BinaryBody::chunk() yields the next chunk. A failed or stalled read maps to the same ApiError::Network("Error reading {kind} body for {id}: …") as before.
    • Status classification (404 → None, 401/403/429, other) is unchanged.
  • api/blob_fetcher.rs:
    • stream_cache_entry_atomic replaces write_cache_entry_atomic. It copies the body into the stage file one chunk at a time. For blobs it then hashes the staged file with compute_git_sha256_from_reader, and finally renames it into place.
    • On any failure it removes the stage file and every cache directory the call created that is still empty. That includes ancestors that create_dir_all made 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) replaces download_hashes and the inline diff loop in fetch_missing_diff_archives.
  • CHANGELOG.md: one Fixed entry.

Deleted

  • The second download loop.
  • The buffered resp.bytes() read in fetch_binary.

From git diff --numstat origin/main, src lines split at mod tests:

  • production: about +193 / −160;
  • tests: about +304 / −12.

Behavior

None, apart from memory use. Error strings, downloaded/failed counts, 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

  • New 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.
    • Red on main (src restored to origin/main): FAILED … panicked at blob_fetcher_edges_e2e.rs:849.
    • Green on this branch, on Linux, macOS and Windows.
  • New 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/.
  • New 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.
  • New 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.
  • Commands run:
    • cargo clippy --workspace --all-features -- -D warnings: clean.
    • The core suites blob_fetcher_edges_e2e, api_timeout_e2e, binary_fetch_error_classification_e2e and covgap_api_blob_fetcher: all pass.
    • cargo test -p socket-patch-core --lib: passes, apart from the 4 known root-sandbox permission tests, which fail on main too.
    • The CLI suites apply, remove, cli, get, repair, diff_created_file_e2e and remove_rollback_api_overrides: all pass, apart from 2 repair tests that chmod a directory read-only (root ignores the mode bits). Both pass in CI.
  • CI on ae928ae: all 338 checks are green, and Bugbot is clean.

Risk

M. Every apply/get/repair/rollback download goes through this path. Mitigations:

  • The stage, hash and rename discipline is unchanged.
  • File handles are closed before the rename, which Windows requires.
  • The full CI matrix, including Windows, is green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V8joXSoT8cmkgc1AVv1wrq


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko Mikola Lysenko (mikolalysenko) added refactor Structural change: duplicated code or logic, missing abstraction, layering, dead code arch-refactor PR opened by the scheduled architecture refactor routine labels Oct 2, 2026
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
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 2, 2026 21:19
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 2, 2026
Assisted-by: Claude Code:claude-opus-5-5

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/api/blob_fetcher.rs Outdated
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
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

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
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 2, 2026
Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 2, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[burn-down agent] Labeled Ready for review at ae928ae91c2b1c57d9e5367d1d0c72bd66424651.

  • CI: 338/338 check runs completed on this head — 332 success, 6 skipped, 0 failing.
  • Bugbot: reviewed ae928ae — no new issues; 0 unresolved review threads (earlier finding about partly created cache dirs was fixed in 3362893 with a regression test).
  • Branch is up to date with main (0 behind) and mergeable.
  • Reviewer focus: api/blob_fetcher.rs stream_cache_entry_atomic cleanup-on-failure path (stage file + empty created cache dirs), and the one changed error string when both the cache dir can't be created and the hash mismatches.

Slack announcement: not sent (Slack send tool unavailable this run).


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Reviewed ae928ae91c2b1c57d9e5367d1d0c72bd66424651. Ready to merge as-is; no actionable findings.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-refactor PR opened by the scheduled architecture refactor routine Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review refactor Structural change: duplicated code or logic, missing abstraction, layering, dead code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Patch blob and diff downloads buffer the whole response body with no size cap

2 participants