fix(filereplication): a full-size .part with no final file is not "present" - #965
Conversation
…esent" Motivation: LocalBackend.StatFile falls back to the "<path>.part" staging file when the final file is absent, so the puller can resume a partial download. The puller's presence rule (presentAtSize, used by the worker's pre-pull check and the walks' self-origin check) is "stat succeeded and size equals the manifest's". A crash after the last byte reached the .part but before the rename leaves a staging file of exactly the manifest size, which then reads as "already here": the worker skips the pull, the walks skip it, and nothing renames it. Reads glob *.parquet, so the file's rows stay missing on that node. The window is one rename wide and nobody is known to have hit it. Approach: Puller.statLocal now confirms the final file exists whenever the backend reports a staged partial (StagedSize, implemented by LocalBackend only), and otherwise reports not-found. The entry is then pulled again; WriteReader truncates the stale .part and writes from zero. tryResumeFromPartial calls StatFile directly and is unchanged, so resume of short partials is as before. S3/Azure have no staging and are unaffected. presentAtSize stays the single shared rule. Validation: - Added TestCatchUpPullsFullSizeStagingFile (walk with RepullMissingSelfOrigin) and TestWorkerPullsFullSizeStagingFile (worker pre-pull check), seeding a manifest-size .part with no final file. Both fail without the change (entry skipped as local/present) and pass with it; the fake backend gained StagedSize to mirror LocalBackend. - go build ./... ; go vet and go test ./internal/cluster/... ./internal/storage/ pass. - golangci-lint on the package reports 4 errcheck findings, all in files this change does not touch. Not run: a real crash/kill against a live cluster. Report: Basekick-Labs#963 Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Assisted-by: claude-sonnet-5-5 (via Claude Code)
…file, cover the both-present branch; 26.09.3 notes Review fixups for Basekick-Labs#965, pushed to the contributor's branch: - statLocal asserts storage.StagingInspector instead of an anonymous one-method interface; the test fake implements the whole contract. - Debug line when a staged partial is found without its final file, so the re-pull (Basekick-Labs#963) is explainable from the log. - LocalBackend.StatFile's doc comment described the .part fallback as serving the pre-pull check's full-vs-partial decision, which this change retires; it now names the resume path and the presence rule. - Tests for the uncovered branch: a .part beside a present final file stays present through the walk and the worker, with no fetch. The walk test now pins catchup_skipped_local, the walk's own counter. - RELEASE_NOTES_2026.09.3.md: first entry, for Basekick-Labs#963, with the credit line. Live-verified on the enterprise-local rig: a full-size .part with no final file on writer2 was skipped as present on the released 26.09.2 (5 files, 25 rows) and pulled back on this branch (6 files, 30 rows). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…file, cover the both-present branch; 26.09.3 notes Review fixups for Basekick-Labs#965, pushed to the contributor's branch: - statLocal asserts storage.StagingInspector instead of an anonymous one-method interface; the test fake implements the whole contract. - Debug line when a staged partial is found without its final file, so the re-pull (Basekick-Labs#963) is explainable from the log. - LocalBackend.StatFile's doc comment described the .part fallback as serving the pre-pull check's full-vs-partial decision, which this change retires; it now names the resume path and the presence rule. - Tests for the uncovered branch: a .part beside a present final file stays present through the walk and the worker, with no fetch. The walk test now pins catchup_skipped_local, the walk's own counter. - RELEASE_NOTES_2026.09.3.md: first entry, for Basekick-Labs#963, with the credit line. Live-verified on the enterprise-local rig: a full-size .part with no final file on writer2 was skipped as present on the released 26.09.2 (5 files, 25 rows) and pulled back on this branch (6 files, 30 rows).
3e9ed7c to
406a532
Compare
|
Thanks, this is a clean fix for #963. I verified it three ways and pushed one fixup commit to your branch (details at the end). What I checkedUnit tests fail before the fix. With Live, pre-fix (released 26.09.2). Four-node per-node-storage cluster (3 writers + 1 reader, Live, with the fix. Same volume, writer2 recreated on an image built from this branch: So the stale staging file is truncated by Other Fixup commit pushed to your branch
I'll merge once CI is green on the new head. |
Resolves puller.go: keep the Basekick-Labs#961 self-origin fast-path condition (RepullMissingSelfOrigin) and the Basekick-Labs#965 statLocal/presentAtSize pre-pull check, adding this branch's !request.force to the latter.
Motivation:
LocalBackend.StatFile falls back to the ".part" staging file when the
final file is absent, so the puller can resume a partial download. The
puller's presence rule (presentAtSize, used by the worker's pre-pull check
and the walks' self-origin check) is "stat succeeded and size equals the
manifest's". A crash after the last byte reached the .part but before the
rename leaves a staging file of exactly the manifest size, which then reads
as "already here": the worker skips the pull, the walks skip it, and nothing
renames it. Reads glob *.parquet, so the file's rows stay missing on that
node. The window is one rename wide and nobody is known to have hit it.
Approach:
Puller.statLocal now confirms the final file exists whenever the backend
reports a staged partial (StagedSize, implemented by LocalBackend only), and
otherwise reports not-found. The entry is then pulled again; WriteReader
truncates the stale .part and writes from zero. tryResumeFromPartial calls
StatFile directly and is unchanged, so resume of short partials is as before.
S3/Azure have no staging and are unaffected. presentAtSize stays the single
shared rule.
Validation:
and TestWorkerPullsFullSizeStagingFile (worker pre-pull check), seeding a
manifest-size .part with no final file. Both fail without the change
(entry skipped as local/present) and pass with it; the fake backend gained
StagedSize to mirror LocalBackend.
pass.
change does not touch. Not run: a real crash/kill against a live cluster.
Report: #963
Signed-off-by: Pujitha Paladugu 10557236+pujitha24@users.noreply.github.com
Assisted-by: claude-sonnet-5-5 (via Claude Code)
Fixes #963