fix(transfer): re-delta the phase-2 redo against the retained basis - #7234
Conversation
5870b72 to
c3cbb5e
Compare
|
Rebased onto master (d54f6e5) after #7229 and #7236 landed. Conflict resolution. #7236 did not just touch Both concerns verified together, re-measured on the rebased binary (daemon pull, oc client against an upstream 3.4.4 daemon): Byte-identical to upstream 3.4.4 on the same fixture, and the warning is now on stderr with the bare flist-relative name - #7236's behaviour, which my re-homed test asserts through the shared On the payload: my test seeds
One open item I am still chasing rather than asserting: |
c3cbb5e to
ebc3b3f
Compare
|
Follow-up to the rebase, all re-run rather than re-read. 1. Red-check after the rebase. Reverting only Restoring the change: 1 passed. So the test still bites post-rebase. 2. Daemon pull, re-measured on the rebased binary - 2 transfers, literal 205,300 / matched 101,900, destination identical, warning on stderr with the bare name and zero occurrences on stdout. Byte-identical to upstream 3.4.4. 3. The rsh cell I flagged is NOT a regression - it was my host.
In the same loaded runs my own daemon-pull test took 30.8s where it now takes 2.1s - a ~15x slowdown, which puts a normally-2s cell within reach of a 60s deadline. I chased this as a possible pipe deadlock from the redo now carrying ~5.8 KB of block sums instead of a null sum head; the manual runs over the identical lsh-stub pipe transport rule that out. Worth recording separately: 4. fmt. CI's |
4d1afe0 to
df14168
Compare
The receiver requested every phase-2 redo index with a null sum head, so the sender fell into match.c:403-409's literal branch and re-sent the whole file. A forced --append-verify redo over a daemon pull reported literal 307,200 / matched 0 where upstream 3.4.4 reports 205,300 / 101,900 on the same fixture. Upstream re-enters the ordinary recv_generator() for a redo index (generator.c:2200), which re-stats the destination and re-sends a block signature built from it (generator.c:1967). The destination still holds the phase-1 update because --append implies --inplace (options.c:2411) and receiver.c:1029 finishes the transfer in place even when recv_ok == 0, so that retained partial IS the redo's basis. Only csum_length (generator.c:2178) and the sign of append_mode (:2186) differ from phase 1, and both were already applied here - the negated append_mode is also what stops the redo tripping the append short-circuit at generator.c:1842, where the destination now equals F_LENGTH. Route the redo batch through the same basis lookup as phase 1. The former no-basis arm becomes unreachable once the redo no longer selects it (its only other caller was the empty batch, whose loop body never ran), so it is removed rather than left as dead code. The comment that licensed the bug claimed "the basis comparison is not re-run for the retry" and cited generator.c:1939 - refuted by :2200 and :1967 in the same function it cites. Replaced with the citation that actually holds. The existing unit test asserted the intended behaviour of find_basis_file_with_config directly, so it passed throughout while the production path never called it. The new integration test drives a real daemon pull and asserts the delta split: the redo must match every whole basis block inside the retained tail, and must not count the wrong prefix as matched. Its payload is index-hashed rather than the sibling's i % 251, whose period-251 repetition lets a delta match blocks the retained tail never covered; a guard asserts no two basis blocks are equal so the ceiling cannot go vacuous. Measured, daemon pull, 200 KiB source over a 100 KiB wrong prefix: before 2 transfers literal 307,200 matched 0 after 2 transfers literal 205,300 matched 101,900 upstream 3.4.4 literal 205,300 matched 101,900
df14168 to
394fb00
Compare
* ci: install cargo-nextest from one pinned composite action The Windows cells fetched cargo-nextest with a single Invoke-WebRequest against get.nexte.st/latest, under ErrorActionPreference=Stop. One transient failure skipped the whole cell - PRs #7234 and #7265 both died there with Build and Test never running. The block was copy-pasted 15 times across 10 workflow files and had already drifted into three comment variants. It is now one composite action. Pinned to an exact version instead of latest, so re-running an old commit installs what it was tested against. rust-cache already restores ~/.cargo/bin before this step, so when the pinned binary is present the download is skipped and no network I/O happens at all. No retry and no backoff: the fetch is attempted once when needed and fails loudly. * ci: parse only the first line of cargo-nextest --version cargo-nextest --version prints a multi-line block on Windows (cargo-nextest X.Y.Z, then release:, commit-hash:, commit-date:, host:), so the native call yields an ARRAY. -replace over an array returns an array - only the first element matches - and -ne against an array FILTERS rather than compares, so the non-empty result was truthy and the version guard threw on a correct install, taking down every Windows cell. Index the first line explicitly. @() forces an array so [0] is always safe, and indexing avoids terminating the native command pipeline the way Select-Object -First 1 would.
Stacked on #7229
Base is
perf/receiver-owned-inflight-window(#7229), notmaster. That PRconverts
run_pipeline_loop_decoupledfrom&'a selfto&mut selfwith anowned in-flight window and rewrites the same batch loop this change edits, in all
three files touched here. Rebasing that borrow-model change underneath this edit
later would be far more disruptive than stacking now. Merge #7229 first; this PR
retargets to
mastercleanly afterwards.The bug
The receiver requested every phase-2 redo index with a null sum head, so the
sender fell into
match.c:403-409's literal branch and re-sent the whole file.The bytes still landed correctly, which is why the existing redo-recovery test
passed throughout - the cost is invisible unless the delta split is asserted.
Measured on a forced
--append-verifyredo (200 KiB source, 100 KiB wrong-prefixdestination), oc as the pulling client against an upstream 3.4.4 daemon:
307,200 is exactly 102,400 (phase-1 append) + 204,800 (the whole file again).
Why upstream re-deltas
The redo re-enters the ordinary
recv_generator()for the redo index(
generator.c:2200) - not a reduced retry path. That re-stats the destination andre-sends a block signature built from it (
generator.c:1967). The destinationstill holds the phase-1 update:
--appendimplies--inplace(
options.c:2411), andreceiver.c:1029((recv_ok && ...) || inplace) finishesthe transfer in place even when
recv_ok == 0- which is also why the warningreads
retained(receiver.c:1074-1079). So the retained partial is theredo's basis.
Only two things differ from phase 1, and both were already applied on this side:
csum_length = SUM_LENGTH(generator.c:2178) and a negatedappend_mode(
:2186). The latter is load-bearing beyond the signature: without it the redowould trip the append short-circuit at
generator.c:1842(
append_mode > 0 && sx.st.st_size >= F_LENGTH(file)), where the destination nowequals
F_LENGTHexactly.The change
Route the redo batch through the same basis lookup as phase 1. The former
no-basis arm is then unreachable - its only other selector was the empty batch,
whose loop body never ran - so it is deleted rather than left as dead code.
The comment that licensed this bug claimed "the basis comparison is not re-run
for the retry" and cited
generator.c:1939, which is refuted by:2200and:1967inside the very function it points at. It is replaced with a citationthat actually holds. A wrong upstream citation is worse than none.
Testing
crates/transfer/src/receiver/basis.rsalready asserted "phase-2 redo must senda checksum-based delta, not a whole-file literal transfer" - but it called
find_basis_file_with_configdirectly, and the production redo path never calledthat function, so it passed for the whole life of the bug. The new test drives a
real daemon pull end to end and asserts the split:
(derived from
signature::calculate_signature_layout, not hardcoded);calls
matched()for the retained prefix -match.c:389-390);literal + matchedmust account for exactly the phase-1 tail plus the phase-2resend, so a redo that skipped bytes cannot satisfy the lower bound alone;
Verified red before / green after by restoring
pipeline.rsto its base revision:fails with
matched 0 < 101500, passes after.The test payload is index-hashed rather than reusing the sibling's
i % 251.That sequence repeats with period 251, so a source block matches a basis block
whenever the offsets are congruent mod 251 - the first run of this test reported
matched 204,400, matching blocks the retained tail never covered. A guard nowasserts no two basis blocks are equal, so the ceiling cannot go vacuous.
Verification
cargo fmt --all -- --checkcleancargo clippy --locked --workspace --all-targets --all-features --no-deps -- -D warningscleancargo nextest run --workspace --all-features -E 'package(transfer) or package(engine)':7627/7629 passed. The 2 failures are
engine local_copy::tests::{execute_preserves_read_only_permissions, archive_preserves_mixed_permissions_across_files}, both failing withremove extended attribute ... Permission deniedon a local macOS temp dir.Reproduced identically on the untouched base commit f01596f, and this
change touches only
crates/transfer, so they are pre-existing andenvironment-specific.
Scope
This fixes the receiver's redo request only. Two neighbouring divergences the same
fixture exposes are deliberately not in this PR, because each reproduces
independently of the redo:
is oc's sender never matching the basis's trailing short block, and it
reproduces with no
--append, no--append-verifyand no phase 2 at all(plain
--no-whole-filepush against the same basis: 103,300 / 101,500 vsupstream's 102,900 / 101,900). Separate PR against
crates/matching.executor has no phase-2 model at all and implements
--append-verifyas ana-priori prefix comparison. Separate work in
crates/engine.