Skip to content

Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters - #257

Open
Mikola Lysenko (mikolalysenko) wants to merge 205 commits into
mainfrom
perf/scan-concurrency
Open

Mikola Lysenko (mikolalysenko) wants to merge 205 commits into
mainfrom
perf/scan-concurrency

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Hosted and vendored scans spent most of their time in two places:

  • waiting on HTTPS requests sent one at a time;
  • per-ecosystem work that re-read, re-parsed and re-wrote the same files, plus an npm crawl that made one async round trip per filesystem call.

This PR makes the network phases concurrent and the crawlers and rewriters single-pass. It also adds lazy vendored sources, in-memory repacking and per-run parse memos, and ends with a small set of approved behavior changes.

Up to the approved batch, stdout, stderr, lockfiles, ledgers and exit codes are byte-identical to main. The approved changes are listed below and detailed in this comment.

Results

"Before" is main without this PR and "after" is this branch's head. Both use a replay proxy (recorded API responses served locally) at 100 ms/request, 2 interleaved reps, medians. Instructions retired are in parentheses.

The "before" binary predates #251–#254. #254 made cargo vendoring heavier, so the cargo rows understate the gain. Measured against the pre-batch head of this branch, cargo vendored fresh drops 49.6 → 38.9 G.

Target Before After
depscan scan --mode hosted --dry-run (live API)¹ 18.3 s 4.1 s 4.5×
depscan hosted dry-run 17.1 s (17.9 G) 3.5 s (10.1 G) 4.9×
depscan hosted, real run (recorded latency)¹ 35.8 s 7.5 s 4.8×
depscan vendored, fresh 46.3 s (28.8 G) 8.8 s (13.4 G) 5.3×
depscan vendored, idempotent re-run 16.3 s (28.9 G) 3.1 s (12.1 G) 5.3×
cargo (zed) hosted dry-run 36.4 s (204 G) 2.9 s (8.2 G) 12.6×
cargo vendored, fresh 63.5 s (66.8 G) 34.1 s (38.9 G) 1.9×
cargo vendored, re-run 20.7 s (50.0 G) 4.3 s (26.6 G) 4.8×
maven hosted dry-run 16.7 s (28.5 G) 2.8 s (3.3 G) 6.0×
maven vendored, fresh 61.5 s (55.1 G) 31.1 s (7.6 G) 2.0×
maven vendored, re-run 17.2 s (36.0 G) 3.1 s (5.1 G) 5.6×
nuget vendored, fresh / re-run 22.7 s / 7.0 s 11.8 s / 1.1 s 1.9× / 6.6×
composer vendored, re-run 8.0 s (7.2 G) 1.5 s (1.0 G) 5.4×
poetry hosted, real run 5.5 s (15.4 G) 1.4 s (4.5 G) 3.9×
uv hosted dry-run 4.2 s (15.1 G) 1.6 s (2.3 G) 2.7×
go hosted, real run 13.4 s 2.1 s 6.2×
strapi (yarn v1) hosted, real run, full clone¹ 31.3 s 7.0 s 4.4×
187-project cross-ecosystem corpus, hosted / vendored¹ 161.9 s / 136.0 s 111.7 s / 98.1 s −31 % / −28 %

¹ Measured on the first slice of this PR and not re-run since.

  • Byte equality (behavior-preserving part):
    • Every commit before the approved batch was A/B'd against the previous head.
    • The results were identical in all cases: stdout, stderr, the full project and home tree digest (locks, ledgers, vendored artifacts), the file list, and request counts. This held across 11 hosted/vendored fixtures and 9 ecosystems.
    • The integrated behavior-preserving tree was also identical to the original binary in all 35 steps × 2 reps.
  • Request shape:
    • depscan hosted still sends 131 requests (56 batch + 74 by-package + 1 package-vendor), up to 8 in flight instead of 1.
    • Vendored fresh sends exactly main's 71 grants / 344 requests.
    • Cargo and uv send fewer requests only because of the approved deferred fetch (X1b).

What changed

Network (all ecosystems)

  • utils/concurrent.rs is an ordered-concurrency helper (buffered, never buffer_unordered). Results, warnings and errors are folded in input order.
  • By-package GETs, batch POSTs, hosted GET /patches/view, download views and vex record fetches run concurrently: 8 in flight on the API, 4 on the public proxy.
  • The batch loop reproduces the proxy fallback exactly, at any chunk.
  • The vendor prefetch plans downloads from each backend's own pre-flight gates, so it requests exactly what the serial loop will consume. Writes and wiring stay serial in record order, and the outage circuit breaker is replayed at consumption time.
  • Lockfile-only packages are fetched concurrently.
  • Telemetry is sent in the background and flushed before the next stdout write.
  • New env var SOCKET_API_CONCURRENCY: 1 restores strictly serial requests. A tight descriptor limit falls back to serial automatically.

Crawl

  • npm uses a parallel gather followed by an ordered single-threaded merge, checked against the old crawler kept as a randomized-equivalence oracle.
  • The nine ecosystem crawlers run concurrently and merge in a fixed order.
  • go, cargo, NuGet, site-packages and composer lookups run on one blocking task, with directory listings memoized only when complete.
  • Maven POMs are parsed in parallel with bounded buffers.
  • gem env and composer home are asked once per process.

Rewriters and vendoring

  • Each lock is parsed once, indexed, and spliced in one pass. This covers pnpm (depscan 868 ms → 8 ms), npm, yarn classic/berry, bun, composer, uv/pylock/PEP 723, poetry/pdm, go.mod/go.sum and Cargo.lock (block search stops at the next [[package]]). The old rewriters are kept as oracles.
  • Backend parse memos per run: Cargo.lock/config, composer.lock, packages.lock.json, the pypi locks, the JS lockfiles and the hatch ledger.
  • Lazy vendored source: a fetched package is extracted only where something reads it.
  • Zip members are inflated on a bounded pool, by name.
  • Jars, nupkgs and wheels are rebuilt from memory. Digests are computed where the bytes are already verified.
  • sha2 uses the aarch64 SHA-256 instructions.

Tooling: scripts/perf/ holds the record/replay benchmark harness.

Behavior changes (approved)

Details and measurements are in the batch comment.

  • Ledger schema version 2 (X11): whole-file wiring snapshots (maven, nuget, pylock/PEP 723/hatch) are stored as {snapshot, ops} edits of the record's own original. Version 1 ledgers are still read and revert byte for byte. Other ledgers stay byte-identical v1. state.json shrinks: maven 29.8 → 15.1 MB, pylock 43.9 → 22.0 MB, nuget 7.2 → 3.9 MB.
  • Deferred pristine fetch (X1b): a purl the ledger already covers, and a cargo crate the patch service serves, is not downloaded up front. An in-sync --offline / network-less re-run now succeeds (already_vendored, exit 0), and re-runs no longer report vendor_fetched_missing.
  • -e <eco> crawl scope (MVN-4): without --prune/--sync, only the named ecosystems are crawled. lockfileOnlyPackages counts only them (depscan+zed -e npm: 6920 → 5347).
  • Maven coordinates from the ~/.m2 path (MVN-1): canonical POMs are not opened once their top-level group directory is confirmed. A hand-placed POM that disagrees with its path reports the path's coordinates.
  • Per-run group commit with journal replay (V2): lock/manifest/config/ledger edits are committed once per run through .socket/vendor/.commit-journal.json. A crash leaves the pre-run wiring, or is rolled forward by the next locked command. A failed commit write now fails the run with the new top-level error vendor_commit_failed (exit 1), replacing the per-purl vendor_state_write_failed / redirect_ledger_write_failed events; locks and ledgers stay pre-run, and artifacts written in the loop remain until the next run. vendor_stale_artifact_removed events now follow the per-package events.
  • fsync policy (X12): vendored artifacts are written without a per-file fsync. One durability barrier runs before every durable commit point and at the end of the run, and lockfiles and ledgers stay durable.
  • Caveat: the crash/journal and fsync semantics are covered by unit, failpoint and injection tests only, not by a live crash fixture.

Resolved: exact plan, 71 grants

The earlier prefetch planned download grants before each backend's pre-flight checks ran, so depscan sent 74 grant POSTs where main sends 71. The plan now calls each backend's own pre-flight gates, so fresh depscan vendoring sends exactly 71 grants and 344 requests, with 0 replay misses. Parity tests pin each npm flavor's plan against its loop, refusal for refusal.

Other observable differences

  • With --debug, the [socket-patch debug] GET/POST <url> lines for concurrent requests interleave. Normal warnings keep serial order, and tests pin that.

Test plan

  • cargo clippy --workspace --all-targets -- -D warnings is clean.
  • cargo test --workspace --no-fail-fast: 9,218 passed, 0 failed, 136 ignored. No existing test was ignored or deleted. All existing test edits pin an approved behavior change: the two ledger-write-failure tests in covgap_commands_vendor.rs now expect the new vendor_commit_failed envelope error instead of a per-purl failed event (V2), and the redirect one no longer asserts that the artifact is absent, since it is written before the commit fails. It asserts instead that nothing is committed or rewired and no journal remains. The v1 legacy-ledger fixture for cargo was regenerated with the pre-batch binary to match main's Wire vendored cargo patches through Cargo.toml and tag the copy #254 cargo wiring. Every other change before the approved-behavior batch kept all existing assertions unchanged.
  • New tests:
    • ordering under reversed latencies;
    • proxy fallback at any chunk;
    • telemetry flush;
    • crawler, rewriter and lazy-source equivalence against oracles;
    • plan-vs-loop parity for every npm flavor;
    • SOCKET_API_CONCURRENCY=1 and tight fds;
    • failpoint crash tests at the durability barrier and inside the group commit;
    • journal replay, set-aside and restore-failure paths;
    • ledger v1 ↔ v2 round trips for every ecosystem.
  • Every commit went through adversarial review for behavior equivalence, concurrency and test integrity. Each finding was fixed or refuted with evidence.
  • A/B benchmarks with byte-equality checks for every batch, as above and in the batch comments.

The commits are the original reviewed commits, replayed onto current main (#251–#254). Where a conflict or an adaptation to main was needed, it is folded into the commit that needed it, and each batch comment lists them.

🤖 Generated with Claude Code


Note

High Risk
Changes vendoring crash semantics, ledger schema, offline/idempotent vendor behavior, Maven discovery coordinates, and concurrent API failure modes—areas that affect committed project state and patch correctness.

Overview
This PR speeds up hosted and vendored workflows by running patch-API traffic concurrently (default 8 authenticated / 4 on the public proxy) via ordered_concurrent, with SOCKET_API_CONCURRENCY (1–32) to throttle when a proxy or WAF limits in-flight requests. The same pacing applies to vex / scan --vex record fetches and hosted wheel-metadata downloads; hold_back_debug keeps per-request --debug lines in the same order as the old serial loop.

Beyond networking, it lands several v5.0 contract changes documented in CHANGELOG / CLI_CONTRACT: deferred pristine fetch so already-vendored / service-backed cargo re-runs can succeed offline without upfront registry pulls; scan -e crawls only selected ecosystems (except prune/sync); Maven derives coordinates from canonical ~/.m2 paths when the repo root is confirmed; vendor ledger v2 stores large whole-file wiring as snapshot+ops edits; one group commit per vendored run through .socket/vendor/.commit-journal.json (new top-level vendor_commit_failed on commit I/O failure); and batched fsync for artifacts before durable lockfile/ledger writes.

Smaller fixes include vex api_auth_fallback quoting the first refusal in pending order (not completion order), batched gem/Python stale-install probes using judge_installed_record, and confirm_waits so scan knows when a hosted confirm prompt will block.

Reviewed by Cursor Bugbot for commit f9fc676. Configure here.

`ordered_concurrent` / `map_ordered_concurrent` wrap
`stream::iter(..).map(f).buffered(limit)`: at most `limit` requests in
flight, results yielded in input order, nothing started until polled.
The serial patch-API loops can adopt it and fold results exactly as
before. `API_CONCURRENCY` (8) and `PROXY_API_CONCURRENCY` (4) carry the
per-client caps. futures-util was already in the lock; it is now a
direct dependency of core and the CLI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
New subprocess suite for the three patch-API loops `scan` drives (batch
POSTs, per-package detail GETs, hosted record views). Every mock answers
later requests first, so an implementation that folds in completion
order, or lets a discarded response leak in, changes the output:

- batch: a 401 on the first chunk sends that chunk and all later ones
  to the proxy with one auth request and one warning; a 401 on chunk 3
  of 6 folds 0-2 from the auth API and replays 3-5 on the proxy; per
  batch 500 warnings print in chunk order; the all-failed error carries
  the last chunk's error.
- details: partial-failure warnings print in package order and the
  whole human preview equals a zero-latency run; the all-failed error
  names the last package.
- hosted wet run: record_fetch_failed warnings keep confirmed order and
  stdout, lockfile and ledger equal a zero-latency run.

The suite passes against the current serial loops (checked with the
baseline binary) and is the oracle for making them concurrent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`fetch_patch_details` awaited one `by-package` GET per package with
patches (74 on depscan, ~10 s of serial round trips). The queries now
run through `ordered_concurrent` (8 in flight, 4 on the public proxy)
and are consumed in `packages` order, so `results`, `failures`, the
warn-after loop and the all-failed rule see exactly what the serial
loop produced. `ApiClient::uses_public_proxy` picks the cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The batch loop POSTed one chunk at a time (56 chunks on depscan). Chunks
now run through `ordered_concurrent` and are consumed strictly in chunk
order, so per-batch warnings, `batch_error_count`, `last_batch_error`
and the paid-access flag fold as before.

The authenticated-to-proxy downgrade keeps the serial loop's exact
sequence: the first chunk goes alone (a stale token still costs the
auth API one request), and at the first consumed chunk k whose error is
a fallback candidate — any index — the window is dropped, responses for
chunks past k are discarded unfolded, the same warning prints, chunk k
is retried on the proxy and the rest continue there (4 in flight).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A wet hosted run fetched `patches/view/{uuid}` for every confirmed
redirect one at a time (74 on depscan, ~9 s). The views now run through
`ordered_concurrent` and are consumed in `confirmed` order, so `records`
(newest wins) and the `record_fetch_failed` warnings are unchanged. The
ledger re-fetch on idempotent re-runs is deliberately kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Scan awaited each telemetry POST inline (150-300 ms typical, up to the
5 s budget on a bad network) before carrying on. Its three events now
go through `spawn_patch_scanned` / `spawn_patch_scan_failed`: the event
is built and its endpoint resolved where it fires (same body, timestamp,
env reads and "Sending telemetry" debug line), and only the POST runs in
a background task. `scan::run` awaits `PendingTelemetry::flush` before
returning, so every event is still delivered, or given up on within the
same 2 s connect / 5 s request budget, before the process exits. The
inline trackers and every other command are unchanged.

Tests: core unit tests pin that a background send posts the same bytes
and headers as an inline one and that flush waits for it; telemetry_e2e
pins that each scan terminal (success, empty crawl, all batches failed)
delivers its one event and stays alive until the slow endpoint answers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The background send was only awaited after run_scan returned, so a
process killed after the event fired but before that flush lost it:
`scan | head` / `scan | true` dies of SIGPIPE on its first result write
(main restores SIG_DFL), and a Ctrl-C at a confirm prompt or a CI
SIGTERM had the same effect. The inline send it replaced had always
landed before any output.

`PendingTelemetry::flush` now drains (`&mut self`), and scan flushes at
the first output point after each event fires: right after the send on
the empty-crawl and all-batches-failed terminals (they print at once),
at the start of the human section (before the table, prompts and every
human exit), before the plain `--json` envelope, and inside
`discover_selected` right after the detail fetches (before its error
line and whatever the `--apply`, hosted and vendored `--json` arms
print next). The send still overlaps the by-package detail fetches on
those arms; the flush at the end of `run` stays as the exit backstop.

Under `--debug` this also puts the human path's "Telemetry sent" line
back ahead of the per-package detail warnings, as in the inline order.

Tests: telemetry_e2e runs each JSON terminal with stdout closed before
the child writes and requires the event delivered (red on the previous
commit: SIGPIPE, 0 events); a core unit test pins that flush drains and
that sends started after it join the next flush.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`track_patch_scan_failed` and `spawn_patch_scan_failed` each spelled out
the `{"fallback_to_proxy": ...}` literal; build it in one place, as
`patch_scanned_metadata` already is for the success event, so the inline
and background paths cannot drift. The inline trackers stay: they are
public API of the published core crate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
No production caller used `map_ordered_concurrent`: every API loop
consumes `ordered_concurrent` directly. Move it into the tests module
so it no longer ships as unused public API.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… + ordered merge

`crawl_all` and the workspace roots walk made one `spawn_blocking` round
trip per readdir, stat and package.json read, strictly in sequence. Both
now run as one blocking-pool task: directory I/O is gathered in parallel
(rayon, already in the dependency graph via qbsdiff) into per-root event
trees that record the sequential visit order, and a single-threaded merge
replays them so the order-dependent `seen` dedup and the store entries'
`identity_seen` decisions see exactly the state the old walk saw — same
packages, same paths, same order.

Two probes are answered from listings the walk reads anyway, only where
that is provably the same answer:
- the roots walk skips the `is_dir(child/node_modules)` stat when the
  child's complete listing holds nothing that could alias `node_modules`
  on a case-insensitive filesystem (a listed dir still stats: a
  readable-but-unsearchable parent lists kinds while stats fail);
- a store entry's `node_modules` existence probe is the readdir the scan
  needs next; a dir that does not open falls back to the stat.

FIFO-safe package.json reads (read_regular_to_string_sync), the
NESTED_STORE depth/dir caps (kept sequential: the budget order decides
survivors), symlink-not-traversed rules and lossy-vs-raw name joins are
unchanged. The previous async implementation is kept verbatim as a
#[cfg(test)] oracle; a randomized fixture test (flat/nested/legacy stores,
scoped, live/dangling/store symlinks, duplicate identities, aliases,
broken/BOM/FIFO/dir package.json, unreadable and unsearchable dirs,
node_modules case variants) plus a kitchen-sink tree assert identical
roots, crawl output, find_by_purls results and store enumeration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ly listed names

The resolver opened `<nm>/<target>/package.json` for every pending
target in every visited node_modules — targets × dirs failed opens, each
its own spawn_blocking hop — then listed the same dir again for the
descent. Both passes now run as one blocking-pool task: each dequeued
dir is listed once, a target is probed there only when the listing could
hold its first path component, the surviving probes run in parallel and
fold back in target order, and the same listing drives the descent
(whose per-entry stats also run in parallel, appended in listing order).

The name filter is a strict superset: it only engages for a complete,
all-ASCII listing and matches ASCII-case-insensitively (APFS/NTFS),
and components a filesystem can resolve to a differently spelled entry
(non-ASCII, `~` 8.3 aliases, trailing dot/space) are always probed. BFS
root-first order, every-copy collection, the name+version identity
check, the pass-2 fallback and the store-entry name filter are
unchanged. `.pnpm` entry names are still filtered after the
`node_modules` stat, not before: an entry without one is a nested host
whose synthesized children can match, so the stat decides the result.

The oracle equivalence suite (now also covering case-variant package and
scope dirs) asserts identical find_by_purls maps on every generated root.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`crawl_all_ecosystems` awaited each crawler in turn, and the crawlers
that block (maven's walkdir walk + POM reads, `gem env`, the python
site-packages probe, `composer global config home`) did so inline on the
async task. The crawlers are independent — none prints, none mutates
shared state — so they are now joined, with every blocking walk or
subprocess moved onto the blocking pool, and their results are consumed
in the fixed Npm, Pypi, Cargo, Gem, Golang, Maven, Composer, Nuget, Deno
order, so packages and counts are exactly the serial run's. The joined
futures are heap-allocated from a non-async constructor so the caller's
poll frame does not grow by their combined size (Windows main-stack
budget).

`gem env gemdir` and `gem env gempath` run concurrently but are still two
calls consumed gemdir-then-gempath (no single-call merge: platform path
separators). A polyglot `--global-prefix` test pins the joined output
against the serial sequence.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The previous commit fanned each visited dir's probes and descent stats
out to the rayon pool separately, one injection per dir: on a deep
pnpm tree the per-dir handoff latency outweighed the parallelism, and
`apply --dry-run` on a large monorepo ran slower than the async walk.

A visit's reads depend only on the dir and the fixed target list, never
on what earlier dirs resolved, so the walk now proceeds level by level
(exactly the FIFO queue's order: everything a dir enqueues lands behind
the rest of its level). Each level's visits — listing, filtered probes,
nested-dir discovery with the virtual store's entries returned whole —
are gathered in one parallel pass, then the order-dependent part (folding
matches into the result, the unmatched-name store filter, next-level
order) is replayed sequentially in queue order. Output is unchanged;
the oracle equivalence suite still covers it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ix targets

Symlinks, FIFOs and permission stripping are generated on Unix only, so
the fields that record them are never read elsewhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…'s use block

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The parallel roots walk recursed once per directory level on rayon and
blocking-pool threads (2 MiB stacks), where the old async walk recursed
through boxed futures on the 8 MiB main thread. A deep enough directory
chain (reachable under Linux's 4096-byte PATH_MAX, and deeper on Windows
long paths) aborted the scan with a stack overflow the old walk
survived.

Read the tree one level at a time, each level's dirs in parallel, record
each dir's child range, then emit with an explicit stack in the same
depth-first order. Stack use no longer grows with depth; a new test runs
a 400-deep chain on 256 KiB walk threads (the recursive walk overflowed
there).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… budget

Two properties of the old sequential async walk did not survive the
move to parallel sync walks on rayon's global pool:

- Stack: the recursive node_modules gather ran on 2 MiB worker threads
  instead of the 8 MiB main thread. The npm walks now run on a dedicated
  walk pool whose threads get the main thread's 8 MiB.
- Descriptors: every walker treats a failed read_dir/open, EMFILE
  included, as an absent dir, and the old crawl held one descriptor at
  a time with the nine crawlers run back to back. With one walk thread
  per CPU plus concurrent crawlers, depscan lost packages silently
  below `ulimit -n 24` (5349 of 5520 at 20) where the old crawl was
  intact down to 14. Under a soft RLIMIT_NOFILE below 128 the walk pool
  now gets one thread and the crawlers run one at a time (the old
  descriptor profile); above it the pool is capped at half of what is
  left after a 64-descriptor reserve. depscan now matches the baseline
  byte-for-byte at every limit from 16 to 256.

New tests: pool sizing, a 4 MiB frame fitting on a walk thread, and an
e2e scan under `ulimit -n 16` that must match the ample-limit JSON (the
one-thread-per-CPU pool lost most of that tree there).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hosted pnpm rewriter re-parsed every lock (entries, the early
shrinkwrap sniff, the residual gate) and rebuilt the whole lock string
once per dep: O(deps x lock) work that cost ~430 ms of critical-path CPU
on depscan's 2 MB lock with 74 redirected deps.

Each lock is now parsed and key-indexed once; a dep's instances are
found by binary search, the residual gate judges each instance on its
post-splice body, and committed splices are applied in one pass at the
end. A later dep that hits an already-spliced entry (a duplicate
name@version override) folds the pending splices in and re-indexes
first, so it re-reads the rewritten text exactly as before, and the
vendored-marker scan runs over the post-splice text the same way.

Output bytes, the FileEdit list (order and original fragments),
warnings and refusals are unchanged: the previous implementation is kept
as a test oracle and compared on a depscan-sized synthetic lock set, on
300 randomized mixes of every lock flavor, and on duplicate-override and
peer-suffixed multi-instance cases. depscan wet run: pnpm-lock.yaml and
redirect-state.json byte-identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rently

Deciding which pypi deps need hosted wheel metadata ran a full
`rewrite_python_lock` (parse, a second parse for the source-scope check
on script locks, mutate, serialize) per dep per lock, only to test the
result for `Some`. The rewrite's refusal and not-applicable checks now
live in one planning step that `rewrite_python_lock` and a new
`PythonLockProbe` share: the probe parses each lock once and answers
exactly `matches!(rewrite_python_lock(..), Ok(Some(_)))` per dep, and
the rewrite no longer re-parses the lock for the scope check.

The qualifying wheels are then downloaded through an ordered
`buffered(8)` stream and folded in dep order, so `python_metadata`, the
withheld artifacts and the `python_metadata_unavailable` skips are
unchanged. The stream is inlined here (futures-util added with the same
workspace spec as the scan-concurrency branch); it moves onto the shared
ordered-concurrency helper once that lands.

New tests: a probe/rewrite equivalence sweep over every lock shape and
outcome, and a hosted scan whose slow first failure must still be
reported before a fast second one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… lock

The npm package-lock rewriter re-derived every `packages` entry's
identity (the `node_modules/` key split plus the `name`/`version`
lookups) for every dep, and the classic yarn.lock rewriter re-split
every block's key patterns for every dep: O(deps x entries) work that
dominated both rewriters' CPU.

Each identity is now computed once per lock. npm entries keep theirs by
map position (a rewrite only touches `resolved`/`integrity`, never a
key, `name` or `version`); a yarn block's key and sole real package are
recomputed whenever this run rewrites that block, so later deps still
see its current text.

Output bytes, FileEdits and warnings are unchanged: both previous
implementations are kept as test oracles and compared on 400 randomized
locks each (aliases, links, bundled copies, workspaces, v1/v2
dependency trees, alias-only and fork-substitution yarn keys, CRLF and
mixed line endings, duplicate overrides). Rewrite-phase CPU on the
lockfile-only benches: npm-socket 103 -> 61 ms, yarn-strapi 49 -> 32 ms
(whole-process medians).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The indexed rewriter judges residuals inline, so the boundary test over
`pnpm_unrewritten_instances` now covers only the test-only reference.
Feed the same boundary locks through `rewrite_registry_redirect`: hosted,
longer-version, scoped and snapshot keys never count, v6 nested-paren and
v5 `_` instances are repointed, and only the unparseable instance is
named in the refusal. The helper's doc comment now says what it is.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each in-flight wheel download buffers the whole wheel under its own body
timeout and retry budget, so memory and link sharing scale with the
limit; 4 keeps the overlapped round trips while halving that. The
comment records what concurrency changes that output cannot see (status
line names the awaited dep, debug lines interleave, Retry-After pauses
one fetch). The order test now also records request arrivals and fails
if `bbb` is not requested before `aaa`'s delayed response is due, so a
regression to serial fetching is caught.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scan time is dominated by API round trips, so live timings are noisy and
unrepeatable. scripts/perf/replay.py stands in for api.socket.dev,
patch.socket.dev and the public proxy: `record` forwards and stores every
response, `replay` serves only from the store with a fixed or recorded
per-request latency (plus optional per-connection latency) and reports
request counts per endpoint, max in-flight, connections and network span.
Batch POSTs replay per purl, so a build that changes chunking or order
still gets identical answers. The listener skips HTTPServer's getfqdn(),
which stalls ~35 s under the macOS sandbox.

scripts/perf/bench.sh drives it: `record`, `replay`, and `ab`, which runs
BASE and NEW interleaved against one store and fails unless every run's
stdout sha256 and exit code match the first BASE run. Stores hold real
API responses (possibly paid-patch data), so bench.sh refuses a store
path inside the repository.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Syntax-checks replay.py (py_compile) and bench.sh (bash -n), then drives
the harness against a local upstream stub: record-then-replay with batch
re-assembly across chunks and orders, miss/unknown-purl accounting,
--fill, a 502 (never stored) for an unreachable upstream, per-request
latency with max in-flight, the getfqdn-free bind, the in-repo store
refusal, and an end-to-end `bench.sh ab` pass and sha-mismatch failure
with fake CLI binaries. Picked up by the existing
`unittest discover -s scripts/tests` CI step.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…current helper

The inline stream::iter().buffered() from the wheel-metadata fan-out
predates utils::concurrent landing; route it through ordered_concurrent
with the same limit (4) and the same in-order fold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… too

0ef24902 flushed the scan event before the first stdout write after it
fires, but stderr raises SIGPIPE just as well (main restores SIG_DFL).
Two stderr writers could run in that window with the send still in
flight: the lenient redirect-ledger load's "Warning: <corrupt ledger>"
(non-hosted JSON and human paths, before discover_selected or the human
flush) and, on the report-only JSON arm, the GC and VEX build ahead of
the envelope. The inline send it replaced was always delivered first.

The ledger load is inlined at its scan call site so the send is flushed
right before its warning (only when it warns, so the overlap with the
detail fetches is kept), and the JSON arm flushes before the GC/VEX
step instead of just before the envelope. The --apply arm's warnings
already follow discover_selected's flush.

Test: telemetry_e2e runs a scan over a malformed redirect ledger with
stderr closed and requires the event delivered (red before: SIGPIPE,
0 events). It uses a well-shaped token so the token-shape warning does
not kill the child before the event fires.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scan now sends patch_scanned / patch_scan_failed through the spawn_*
variants, which leaves the inline trackers without an in-tree caller.
They stay: socket-patch-core is published to crates.io, removing a pub
fn is a breaking change there, and every other event keeps its inline
tracker. The doc comments now say so, so a later cleanup does not read
them as leftovers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
batch_fallback_mid_run_replays_from_the_failing_chunk checked the folded
uuids and the proxied tail, but not that chunks 4-5 were ever sent to
the authenticated API. A serial loop (or a window of 1) would never
request them and still pass. Assert all 6 authenticated requests: chunk
0 alone, then the whole 1..6 window in flight, so the discard path
really runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On the public proxy scan runs up to PROXY_API_CONCURRENCY (4) batch
windows at once. Each window's search_patches_batch degrades to the
legacy per-package GETs (10 at a time) when /patch/batch rejects the
chunk: a 400 from one exotic purl such as pkg:jsr, or an old proxy with
no batch route. So a polyglot project on the proxy could put 4 x 10
by-package GETs in flight where the serial loop peaked at 10. That
path swallows per-purl errors as "no patches", so extra load that
saturates the proxy could change which packages come back.

The client now holds a semaphore of PROXY_BATCH_PATH_CONCURRENCY (10)
slots, shared by clones. Every proxy /patch/batch POST and every legacy
per-package GET takes a slot, so all concurrent batch calls on one
client stay within the old peak. A single call never waits: its groups
of 10 fit the cap exactly as before. The authenticated API is untouched.

Test: four concurrent batch calls of 10 purls each, all rejected with
400, keep at most 10 by-package GETs in flight and still reach 10 (red
without the slots: 40).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…can't spawn

When the walk pool could not be built (the OS refusing threads: a tight
RLIMIT_NPROC or cgroup pids.max, or a huge RAYON_NUM_THREADS), run_walk
ran the walk on the calling thread and the first parallel iterator then
tried to build rayon's global pool, which needs the same refused threads
and panics (exit 101) where the sequential walk succeeded.

- Retry the pool build with half the threads on each failure, down to 1.
- Route every parallel gather through walk_pool::par_map, which maps
  sequentially (in order) on a thread outside any rayon pool, so the
  no-pool fallback never reaches the global pool.
- RAYON_NUM_THREADS can lower the walk thread count but no longer raise
  it past available_parallelism.

Tests: halving build, par_map's sequential/ordered contract, run_walk's
no-pool path, and the randomized oracle comparison with the pool off.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Under the group commit the purl is no longer failed closed: the whole
commit fails and nothing is written. Rename it and pin that the failed
commit leaves no journal behind.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and group commit

CHANGELOG and CLI_CONTRACT: the cargo deferral's verifiable-crate scope
and the file-artifact hash check; the gem gate's wet-only scope, text
and precedence; the per-record version-2 edit format and its safety
under an older binary; the barrier for in-place rebuilds (dropping the
stale "commit granularity is unchanged"); and the journal's recorded
originals, set-aside outcomes, symlink refusal, fail-closed replay,
kept-journal commit failure and the post-commit go-patches removal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A journal that is unreadable, or names a path outside the lockfiles and
ledgers or behind a symbolic link, was reported as "a file it covers
changed since". Give it its own outcome and warning.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko

Mikola Lysenko (mikolalysenko) commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator Author

⚠️ Behavior changes (approved)

Pushed 57a0d0c..5916139 (29 commits). Everything before this push kept output byte-identical to main. These commits change observable behavior on purpose; each change was approved before it was built. They come from perf/bcA (MVN-1, MVN-4) and perf/bcB (X1b, X12, V2, X11), replayed with messages and authorship kept. None were dropped.

What a user or consumer sees differently

  1. .socket/vendor/state.json schema version 2 (X11). A ledger that holds whole-file wiring snapshots is now written as "version": 2. That covers maven pom.xml, nuget.config, pylock/PEP 723/hatch documents, and only records of 1 KiB or more. Each such record's new is stored as {"snapshot": "<sha256>", "ops": [...]}, an edit of the same record's plain-text original. Nothing is stored outside the entries.
    • Version 1 ledgers are still read, and still revert byte for byte. This is pinned against ledgers checked in from the previous binary.
    • Ledgers with no whole-file records (cargo, go, gem, composer, poetry, uv, npm/pnpm) stay byte-identical version 1.
    • A tool that parses state.json itself and reads wiring[].new as a string must handle the object form. No known consumer outside socket-patch reads it.
    • An edit that does not reproduce its hash is vendor_state_unreadable.
    • Records are self-contained, so an older CLI that re-saves a v2 ledger loses nothing. On a v2 record, that CLI leaves the fragment alone and prints its drift warning.
  2. Offline re-runs succeed (X1b). Before, a purl with no installed copy always had its pristine artifact downloaded before the backend ran. The download now waits until something reads the pristine tree, and is skipped when the vendor ledger already covers the purl or a cargo crate is served by the patch service.
    • An in-sync vendor --offline (or network-less) re-run of an already-vendored pypi/cargo/go/lockfile-only-gem project now exits 0 with already_vendored. Before, it failed with package_not_installed / partialFailure.
    • Events are now reported in loop order.
  3. vendor_fetched_missing warnings are gone from re-runs (X1b). An idempotent re-run makes no registry requests for covered purls and no longer reports vendor_fetched_missing.
    • A deferred fetch that does happen (a drifted copy being rebuilt, a service miss) reports its warning just ahead of that package's own event.
    • Under --vendor-source build, a not-installed gem the lock can verify but no ledger covers is refused gem_spec_missing before download.
  4. scan -e <eco> crawls only the named ecosystems unless --prune/--sync (MVN-4). lockfileOnlyPackages and the human "not yet installed" note now count only the selected ecosystems. scannedPackages already did. A GC run still crawls everything and is unchanged.
  5. Maven coordinates come from the ~/.m2 path (MVN-1). A POM at its canonical <group>/<artifact>/<version>/<artifact>-<version>.pom path is not opened. Each top-level group directory is first confirmed against one POM that parses, so a MAVEN_REPO_LOCAL pointed above or inside the repository still reads content-first. Under a confirmed directory, a hand-placed POM whose contents disagree with its path now reports the path's coordinates.
  6. Crash semantics are now per run, with journal replay (V2). vendor, scan --mode vendored and get --mode vendored capture every lockfile/manifest/config edit and ledger save in memory, then commit them once after the loop through .socket/vendor/.commit-journal.json.
    • A crash mid-loop leaves the pre-run wiring, not the packages done so far.
    • A crash mid-commit is rolled forward (or back, around hand edits) by the next locked command.
    • A journal that cannot be trusted is set aside as .commit-journal.set-aside-<uuid>.json, and nothing is applied.
    • New top-level error vendor_commit_failed (exit 1). If the final commit cannot write a lockfile, manifest or ledger, the run now fails with this envelope error. Before, a ledger or redirect-ledger write failure was reported as a per-purl failed event (vendor_state_write_failed / redirect_ledger_write_failed). Locks and ledgers stay at their pre-run state, but artifacts written during the loop are left on disk until the next run cleans them up.
    • vendor_stale_artifact_removed events now come after the run's per-package events, because stale artifacts are removed after the commit.
    • Two existing tests in covgap_commands_vendor.rs were updated to pin this: vendor_state_write_failure_reports_failed_event and redirect_ledger_write_failure_fails_takeover_purl_closed, now renamed redirect_ledger_write_failure_commits_nothing. Their per-purl failed assertions became vendor_commit_failed checks. The redirect test's "tgz must not exist" assertion became "no ledger is committed, the lock is not rewired, and no journal is left behind", because the artifact is now written before the commit fails.
  7. fsync policy (X12). Content-verified artifacts under .socket/vendor/<eco>/<uuid>/ are written atomically but without a per-file fsync. One barrier (file + dir fsync, one F_FULLFSYNC per device on macOS) runs ahead of every durable commit point, at the end of the group commit, and when the apply lock is released. Lockfiles, manifests, configs and ledgers stay durable. A lost artifact byte is caught by the next run's hash re-verification and rebuilt.

Caveat: the crash, commit-failure and journal-replay semantics (6) and the fsync policy (7) are covered by unit, failpoint and injection tests only, not by a live crash or power-loss fixture. No benchmark below can reach those paths.

Integration with main (#251–#254)

Tests

clippy --workspace --all-targets -D warnings is clean. cargo test --workspace --no-fail-fast: 9,218 passed, 0 failed, 136 ignored (57a0d0c had 9,146).

A/B

Setup: b4 = 57a0d0c, b5 = 5916139. Replay proxy at 100 ms/request, 2 reps with rotated order, medians. Instructions retired, wall time in parentheses.

case b4 b5 approved item
maven hosted dry 8.36 G (3.02 s) 3.31 G (2.77 s) MVN-1
maven hosted wet 8.42 G 3.51 G MVN-1
maven scan --mode hosted --dry-run -e npm 7.00 G (0.69 s) 0.04 G (0.11 s) MVN-4
maven vendored fresh 32.4 G (38.6 s), RSS 1,070 MB 7.6 G (31.1 s), RSS 279 MB MVN-1 + X11
maven vendored re-run 10.2 G 5.1 G MVN-1
nuget fx-scale vendored fresh 15.2 G, RSS 266 MB 8.6 G, RSS 105 MB X11 + V2
nuget fx-real vendored fresh 2.76 G 2.12 G X11 + V2
pylock (12 patches) vendored fresh 17.1 G (3.9 s), RSS 500 MB 13.6 G (2.3 s), RSS 409 MB X11 + V2
cargo zed vendored fresh 49.6 G (40.0 s), 492 requests 38.9 G (34.1 s), 456 requests X1b
cargo zed vendored re-run 26.5 G (5.2 s), 162 requests 26.6 G (4.3 s), 126 requests X1b
cargo zed vendor --offline re-run exit 1 (17.9 G) exit 0 (26.9 G: the 36 in-sync checks now run instead of refusing early) X1b
uv-big vendored re-run 3.04 G (1.60 s), 28 requests 0.89 G (0.73 s), 17 requests X1b
uv-big vendor --offline re-run exit 1 exit 0 X1b
depscan vendored fresh 14.9 G (9.7 s) 13.4 G (8.8 s) V2 / X12
depscan vendored re-run 11.6 G 12.1 G (same 131 requests, same output; within this fixture's spread) —

state.json size:

fixture b4 (v1) b5
maven 29,843,313 15,065,386 (v2)
pylock 43,896,582 21,964,210 (v2)
nuget fx-scale 7,221,990 3,873,321 (v2)
nuget fx-real 743,209 449,784 (v2)
cargo, go, gem, composer, poetry, uv, depscan pnpm — unchanged, byte-identical v1

Every observed difference, and the approved item it maps to

54 steps × 2 binaries × 2 reps. The checks: canonical stdout, stderr, full project and home tree digest, file list, and request/grant counts.

  • cargo-v fresh/re-run (X1b). b4 emits 36 vendor_fetched_missing and makes 36 more registry GETs. Dropping those events, stdout is identical, and so are the tree and ledger.
  • uv-v re-run (X1b). 11 vendor_fetched_missing and 11 GETs are gone. Otherwise identical.
  • cargo-v / uv-v --offline re-run (X1b).
    • cargo: partialFailure, 36 package_not_installed + 74 already_vendored becomes success, 110 already_vendored.
    • uv: 11 + 1 becomes 12 already_vendored.
    • The tree is identical in both.
  • ds-cargo-enpm scan -e npm (MVN-4). lockfileOnlyPackages goes 6920 → 5347; nothing else changes. With --prune the output is byte-identical (6920).
  • maven-v, nuget-v, nuget-scale-v, pylock-v, every step (X11). .socket/vendor/state.json is the only differing file. Decoding the v2 ops and masking vendoredAt, it equals b4's ledger in all 24 comparisons. Every lock, manifest, config and artifact is byte-identical.
  • MVN-1, V2, X12. No output difference on any run, and no .commit-journal* remains in any tree.

Byte-identical on both binaries (stdout, stderr, tree, file list, requests):

  • Hosted, dry and wet: depscan (real tree, plain and -e npm), cargo, maven, nuget, composer, uv, poetry, go, gem.
  • Vendored, fresh / re-run / offline: depscan, composer, poetry, go, gem.
  • uv vendored fresh.

Nothing differs that is not listed above.

🤖 Generated with Claude Code

Edited after review: added the vendor_commit_failed error, the event-order change, and the two updated tests under item 6.

The durability barrier re-opened every deferred artifact by path with
File::open (read-only) and called sync_all on it. On Windows
FlushFileBuffers needs a handle with write access, so every vendored run
failed the barrier with "Access is denied. (os error 5)": the group
commit refused ("could not commit the vendored lockfile, manifest and
ledger edits") and the lock release warned "could not sync the vendored
artifacts". A write re-open is no answer either: it is refused for a
read-only artifact.

Off Unix, stage_and_rename now fsyncs every stage through its own
writable handle before the rename -- what the durable writer (and main)
always did there -- and the barrier syncs no files (directories were
already Unix-only). Artifacts are still recorded, so the barrier and its
failpoint run at the same points on every platform. Unix keeps the
deferred plain-fsync barrier unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
vendor_crash_safety_e2e and vendor_group_commit_e2e crash the real binary
through SOCKET_PATCH_FAILPOINT, which was compiled in under
debug_assertions only; the test-release job (cargo test --profile
ci-release) runs them against an optimized binary where the failpoint is
a no-op, so the "crash" run completed with status success and the suites
failed ("the failpoint must crash the run").

A new socket-patch-core `failpoints` feature compiles them into builds
without debug assertions as well. The CLI enables it from its
[dev-dependencies] only, so resolver 2 adds it to `cargo test` builds
(debug or release) and never to `cargo build --release` / `cargo
install` -- the shipped binary still has no environment-triggered exit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sinks

CodeQL's rust/cleartext-logging flags any value named *uuid* reaching a
log/panic sink. The prefetch-plan cross-checks interpolated the patch
uuid into their (unreachable) debug_assert! messages, and debug_log's
refactor for deferred lines moved its eprintln to a new line, which
re-raised main's existing alert as a new one. The assert messages now
name the plan they fell out of step with, and debug_log keeps main's
direct eprintln with the deferral split into defer_debug_line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Masked on Windows until now by the barrier's os error 5:

* vendor_ledger_schema_e2e replays the base binary's checked-in legacy
  ledgers and compares byte for byte; the Windows checkout (autocrlf)
  turned their LF into CRLF. Check tests/fixtures/legacy-ledgers out
  exactly as committed (-text), like the redirect/pnpm golden fixtures.
* go_crawler's randomized oracle equivalence ran a fixed 48 seeds, which
  on Windows (case-insensitive names fold together, modes ignored)
  yielded 92/80 modules against the unchanged >100/>100 non-vacuity
  bar. It now keeps adding seeds until the bar is met (cap 480).
* registry_fetch's parallel-zip test compared tree_of (backslash paths,
  mode 0 off Unix) with an oracle that used '/' and Unix modes. tree_of
  now reports '/'-separated paths and the oracle expects mode 0 where
  the disk holds none.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
write_zip_entries promises byte-identical archives for the same content,
but zip 8 stamps the central directory's "version made by" host as DOS
on a Windows build (Unix elsewhere). So a wheel/jar/nupkg rebuilt on
Windows hashed differently from the one a macOS/Linux run recorded, and
told readers to ignore the unix modes (the preserved exec bit) it carries.
vendor_ledger_schema_e2e caught it on the Windows test job: the
pypi-requirements wheel hash differed from the base binary's.

Pin System::Unix in write_zip_entries (every product zip rebuild goes
through it; berry_zip writes its own fixed headers) and in the ecosystem
fixtures' zip builder, whose nupkg hashes the checked-in legacy-ledger
fixtures carry. A unit test pins the host byte and the modes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

CI fixes for the 17 failing checks on 5916139

I pulled the logs for every failing check. They come down to four root causes, and fixing the first one exposed three more Windows-only failures that it had been hiding. Commits pushed as fast-forwards: 6dbbf0eb, bfbc088e, 76c90e9f, ed120d8c, f9fc6765.

1. Windows: Access is denied. (os error 5) on every vendored run

Checks: 13x native (windows-latest, *), binary (windows-latest), most of test (windows-latest) (29 test binaries)

Root cause: the durability barrier (utils::durability::sync_all_blocking) reopened every deferred artifact by path with File::open, which is read-only, and then called sync_all. On Windows, FlushFileBuffers needs a handle with write access, so the call fails with ERROR_ACCESS_DENIED. That made the group commit refuse ("could not commit the vendored lockfile, manifest and ledger edits") and the lock release warn "could not sync the vendored artifacts". Directory syncs were not the problem: they were already Unix-only. Reopening for write would not help either, because Windows refuses a write open on a read-only artifact.

Fix (6dbbf0eb): off Unix, stage_and_rename fsyncs every stage through its own writable handle before the rename. That is what the durable writer, and main, always did there. The barrier then has no files left to sync (DEFERS_FILE_SYNC = cfg!(unix)). Artifacts are still recorded, so the barrier and its durability_barrier failpoint run at the same points on every platform. Unix keeps the deferred plain-fsync barrier unchanged.

2. test-release: the crash-safety suites could not crash

Root cause: this was not a timeout. The job failed after 34 minutes in vendor_crash_safety_e2e with "the failpoint must crash the run". SOCKET_PATCH_FAILPOINT was compiled in under debug_assertions only, so in the optimized ci-release test binary the failpoint did nothing and the run finished with status success. vendor_group_commit_e2e would have failed the same way. The job has no --no-fail-fast, so it never got that far.

Fix (bfbc088e): a new socket-patch-core feature, failpoints, compiles the failpoints into non-debug builds. The CLI enables it only from [dev-dependencies], so resolver 2 adds it to cargo test builds, debug or release. cargo tree -e normal,features confirms it never reaches cargo build --release or cargo install, so a shipped binary still has no exit you can trigger from an environment variable. Both suites pass locally under --profile ci-release.

3. CodeQL: 5 new high rust/cleartext-logging alerts

Root cause: four prefetch-plan cross-checks put the patch uuid into their unreachable debug_assert! messages (fetch_stage.rs, get.rs ×2, scan/discovery.rs). Separately, debug_log's refactor for deferred lines moved its eprintln! to a new line, and CodeQL treated main's existing alert #48 there as a new one.

Fix (76c90e9f): the assert messages now name the plan that fell out of step instead of the uuid. debug_log keeps main's direct eprintln!, and the deferral moved into a separate defer_debug_line function. Nothing was dismissed. CodeQL now reports "No new alerts in code changed by this pull request".

4. Windows-only test failures exposed once #1 was fixed

  • vendor_ledger_schema_e2e: the checked-in tests/fixtures/legacy-ledgers/** files were converted to CRLF by the Windows autocrlf checkout. They are now -text in .gitattributes, the same treatment as the redirect and pnpm golden fixtures (ed120d8c).
  • vendor_ledger_schema_e2e (pypi-requirements wheel hash, nuget contentHash): a real cross-platform determinism bug. zip 8 stamps the central directory's "version made by" host as DOS on a Windows build. So write_zip_entries produced different bytes on Windows than on Unix, even though it promises byte-identical archives, and it also told readers to ignore the unix modes it carries (the preserved exec bit). It now pins System::Unix, as does the fixtures' zip builder. A new unit test pins the host byte and the modes (f9fc6765). Every product wheel, jar and nupkg rebuild goes through write_zip_entries. On Unix the bytes are unchanged.
  • go_crawler randomized_caches_match_the_async_oracle: the non-vacuity bar failed at 92/80 on Windows, where case-insensitive names fold together and file modes are ignored. The bar is still >100/>100. The test now adds seeds until the fixtures clear it (at least 48, at most 480) (ed120d8c).
  • registry_fetch parallel_zip_extraction_matches_the_in_order_reader: the test helper tree_of reported backslash paths and mode 0 off Unix, while the oracle used / and Unix modes. The paths are now normalized, and the oracle expects mode 0 where the disk stores no mode (ed120d8c).

External flakes (re-ran the failed jobs; no code change)

  • native (ubuntu-latest, 1.1.39), and native (macos-latest, 1.1.39 / 1.1.45) on the next run: the production API returned 503: upstream connect error or disconnect/reset before headers. reset reason: connection termination, as shown in the cells' repeat.log / cli.log.
  • native (macos-latest, 1.0.0): bun 1.0.0 IntegrityCheckFailed extracting tarball for minimist, once. The same cell passed on 76c90e9, and the binary code was identical between the two runs.
  • On f9fc676, native (ubuntu-latest, 1.4.0 / 0.8.1) hit the same production API 503/504 (API request failed with status 504: error code: 504 and the 503 above). 1.4.0 hit it a second time on its first re-run, in a different cell.
  • Every one of these passed on re-run.

Verification

  • Local macOS: cargo clippy --workspace --all-features -D warnings clean. The full cargo test --workspace --no-fail-fast passed (262 binaries) after each iteration. The failpoint suites also pass under --profile ci-release.
  • Not done: cargo check --target x86_64-pc-windows-gnu, because this machine has no mingw C toolchain. Windows was verified on CI.

Final CI state on f9fc6765: all 407 checks green. That includes CodeQL, test (windows-latest), test-release, binary (windows-latest) and every native (windows-latest, *) cell.

🤖 Generated with Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Claude (@claude) review

@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 f9fc676. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

External validation (depscan cross-OS Maven matrix): depscan's harness ran its full 25-job Linux/Windows/macOS Maven matrix against this PR's head f9fc6765, with real mvn 3.9.16 (run 36265269179). It found no Maven regressions: every job passed.

It also confirms that this PR fixes #275, the pre-existing Windows zip-host bug. All 55 vendored local-rebuild cells now produce identical jar digests on Linux, Windows and macOS. On main, the Windows jars differ.

🤖 Generated with Claude Code

reuse::prior_entry loaded the whole vendor ledger once per npm/pypi
package: a full re-parse outside a group commit, a deep clone of the
captured ledger inside one. On a monorepo whose ledger runs to megabytes
that was the vendored re-run's single largest cost. Read it through
load_state_shared instead (still reads the bytes every time, re-parses
when they moved, answers from the group's captured value) and clone only
the matching entry. The pre-change body stays as a test oracle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ying it per save

Inside a group commit every per-package save_state captured a deep copy
of the whole ledger (O(P^2) over a run). The vendor loop now holds its
ledger as an Arc and saves through save_state_shared, which drops the
group's reference to the value it captured last time, edits in place and
re-captures it, all under the overlay lock. The committed bytes are the
same ledger rendered once at the commit; outside a group the save is the
old write.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pnpm backend re-read, re-split and re-scanned pnpm-lock.yaml once per
patched npm package. The split now lives behind a ParseMemo keyed on the
bytes read (the read itself is kept), re-seeded with the lock the backend
writes, and its lazily built section index answers the pre-flight probes
(lock_has_target_package, check_rewritable_refs), the in-use probe and the
packages/snapshots edits' block lookups while the lines are unedited; the
first edit takes a private copy and the scans take over. The scans stay
as the fallback and as the oracles of a generated-lock equivalence test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Profiled on the polyglot re-run (unstripped build): per crate the backend
re-parsed the root Cargo.toml, rebuilt Cargo.lock's package list for each
of several probes and re-scanned every manifest for Socket registry pins.
Memoize each per input bytes: the pre-flight's manifest parse, the locked
package list, and a one-pass extraction of every crate's registry pin.
All reads are kept; the per-call versions stay as test oracles.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…onment

A scan that crawls and then vendors ran the python probe and the site
query (two process spawns) twice with identical inputs. Keep the last
answer keyed on the whole process environment and working directory, so
any change to either re-runs it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er walk

The vendored cargo backend's residue check walks the workspace members
once per patched crate, and once a project is vendored every
[patch.crates-io] path copy is a path dependency: the walk re-parsed every
vendored crate's manifest with toml_edit for every crate, O(P^2) parses
(profiled as the largest cost of the polyglot cargo in-sync re-run, on the
blocking pool the loop waits on). Keep the facts the walk needs per
manifest bytes; every manifest is still read on every walk. The per-call
walk stays as a test oracle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… check reads

The member walk returns every vendored crate's path copy, so the residue
check reads one manifest per vendored crate; eight slots thrashed and the
pins were re-extracted for every manifest on every crate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The DC-6 memo kept a failed interpreter query's None, so a transient
spawn failure during the crawl (EAGAIN/EMFILE, an interpreter exiting
non-zero once) also blanked the vendor phase's ask, where the unmemoized
build ran the query again. Store only Some; a failure re-runs on the
next call exactly as before.

Keep the unmemoized query as run_site_query, the oracle the memo is
checked against. Replace the unserialized stability test, which raced
the #[serial] PYENV_ROOT test and compared the memo against itself,
with a #[serial] test that asks under two PYTHONUSERBASE values and
requires the memoized answer to equal the oracle each time (a constant
memo key fails it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
edit_value releases the overlay's reference to the caller's ledger
before editing it in place. Had the edit (or make_mut's copy) unwound,
the key stayed missing, and a commit that still ran would write the
run's lock edits beside the pre-run ledger. Re-capture the caller's
value before resuming the unwind.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The member walk and the residue check read their manifests in a fixed
order, so a set larger than the slots evicts every slot before its next
use: each read then pays the parse plus the slot scan and bytes copy,
slightly more than no memo, not the same.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…gain

A fresh vendored run rewrites pnpm-lock.yaml after every package, so each
lock the memo is re-seeded with is probed by exactly one package. The
section index was built for that one probe and dropped, which cost more
than the scans it answered for: depscan vendored fresh went from 13.3G to
15.8G instructions.

The index is now built on a lock's second package probe (a pre-flight or
an in-use check); the first is answered by the scans, whose answers the
equivalence test already holds equal. An idempotent re-run and the
download plan's batch pre-flight probe one unchanged lock once per
package and still index it once. A new test pins that one probe leaves
the index unbuilt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

WP-P1: vendored CPU memos (11 commits, f9fc676..cfeafb0)

  • SC1: reuse::prior_entry answers the reuse anchor from the run's shared ledger parse (load_state_shared) and no longer re-parses the ledger.
  • SC5: the group-committed ledger is edited in place (group_commit::edit_value / state::save_state_shared) and no longer copied on every save.
  • V-2: the pnpm-lock.yaml split is memoized (a ParseMemo keyed on the lock bytes), with its sections indexed. The index is built only once the same lock lines get a second package probe (cfeafb0). On a fresh run every package rewrites the lock, so each re-seeded lock is probed exactly once. Building the index for that single probe had pushed depscan vendored fresh from 13.36G to 15.82G instructions (+18%). That probe now goes through the old scans; the equivalence test already holds their answers equal to the index's. An idempotent re-run probes one unchanged lock once per package and still indexes it once. A new test checks that a single probe leaves the index unbuilt.
  • V-7: byte-keyed memos for the cargo in-sync re-run (root manifest, packages, workspace members) and a registry-pin slot for each manifest the residue check reads.
  • DC-6: the python site-packages probe runs once per environment (KeyedMemo, which stores only successful queries).

Every item is meant to preserve behavior. Three adversarial reviewers checked the WP: 5 findings fixed, 1 deferred and documented (512-slot memo FIFO eviction).

Before → after (previous PR head f9fc676 vs cfeafb0, release builds, replay proxy)

Polyglot monorepo (~158k files, 8 ecosystems):

step lat instructions wall peak RSS
hdry 0 ms 40.51G → 40.33G 3.80s → 4.00s 158 → 155 MiB
hwet 0 ms 40.67G → 41.00G 3.68s → 3.92s 166 → 168 MiB
vfresh 0 ms 119.25G → 106.91G (−10.3%) 9.77s → 8.82s 524 → 451 MiB
vrerun 0 ms 78.99G → 39.79G (−50%) 6.01s → 3.54s (−41%) 416 → 388 MiB
hwet 100 ms 40.95G → 40.53G 18.76s → 18.58s 171 → 168 MiB
vfresh 100 ms 119.61G → 109.38G (−8.6%) 120.65s → 120.05s 601 → 457 MiB

The 0 ms vfresh/vrerun rows were re-measured on cfeafb0. The other rows are from e369bf4 (cfeafb0 without the V-2 fix). The fix only touches the pnpm vendored path, so those rows are not expected to move, and the 100 ms vfresh row is, if anything, pessimistic.

depscan (2 interleaved reps each, 0 ms):

run instructions wall peak RSS
hosted dry-run 12.44G / 10.45G → 10.51G / 10.48G 1.94s / 1.41s → 1.81s / 1.41s 116 / 117 → 117 / 115 MiB
vendored fresh 13.35G / 13.32G → 10.50G / 10.49G (−21%) 2.01s / 1.69s → 1.69s / 1.61s 97 / 99 → 107 / 104 MiB

The first hosted rep is cold for both binaries (12.4G then 10.4G). Warm, hosted is flat. Vendored fresh is now 21% cheaper, where the previous draft of this WP regressed it by 18%. It costs about +8 MiB peak RSS, because the memo holds the 2 MB lock's bytes and lines.

Byte-equality

Polyglot gate at 0 ms on vfresh/vrerun with cfeafb0, and on hdry/hwet with e369bf4. Canonical stdout, stderr, exit codes, request counts by kind and the full work-tree digest are identical between the two binaries. The digest covers 250-274k entries: every file hash with timestamps masked, plus symlinks and dirs. For depscan, the canonical stdout (hosted and vendored) is identical, and so are pnpm-lock.yaml, pnpm-workspace.yaml, package.json, the vendor ledger and every vendored artifact and marker (timestamps masked). Canonical stdout sorts arrays, because the order of the per-package vendored warnings follows fetch completion and already varies from run to run on f9fc676.

Tests

cargo clippy --workspace --all-targets -D warnings: clean. cargo test --workspace --no-fail-fast: 9230 passed, 0 failed, 136 ignored (262 test binaries).

🤖 Generated with Claude Code

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants