s3store: fix silent truncation of deferred-length uploads - #1385
Open
abh wants to merge 2 commits into
Open
Conversation
FinishUpload discarded the incomplete-part size and completed the multipart upload with only the real parts, silently dropping a sub-MinPartSize tail parked in a .part object. This happens for deferred-length uploads whose final chunk is written while the length is still deferred (reachable via the IETF Upload-Complete flow), and for uploads smaller than MinPartSize (which completed as a 0-byte object). Promote the .part object into a real final part before completing, guarded by the declared size so retried completions are idempotent and a genuine size mismatch returns an error instead of truncating. The promotion runs only when an incomplete part actually lingers, so the concatenation path (which builds parts without real sizes) and ordinary completions are unaffected. Reuses the same download/upload/delete helpers WriteChunk already uses. Also reset the cached incompletePartSize in WriteChunk once the .part is deleted, so a FinishUpload later in the same request does not act on a stale value and issue a redundant DeleteObject on the completion path. Document the strong read-after-write consistency the backing store must provide, since WriteChunk and FinishUpload read back objects they just wrote. Refs tus#396, tus#798.
There was a problem hiding this comment.
Pull request overview
This PR fixes a long-standing S3Store correctness bug where deferred-length uploads could be completed successfully while silently dropping a sub-MinPartSize tail that had been parked in the <id>.part object (not yet promotable to a multipart part). The change ensures FinishUpload accounts for and, when appropriate, promotes that lingering incomplete part before completing the multipart upload, preventing truncated (or empty) final objects.
Changes:
- Update
S3Store’sFinishUploadto detect a lingering.partobject and promote it into a real final multipart part (or delete it if stale), validating against the declared size to keep retries safe. - Reset
incompletePartSizecache inWriteChunkafter deleting the.partobject to avoid redundant cleanup within the same request. - Add gomock regression tests covering promotion scenarios (tail after real part, whole upload under
MinPartSize, and retry behavior), plus documentation about consistency/locking expectations.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| pkg/s3store/s3store.go | Promotes lingering incomplete .part during FinishUpload to prevent silent truncation; keeps cached incomplete-part size consistent after deletion. |
| pkg/s3store/s3store_deferred_length_repro_test.go | Adds regression tests reproducing and guarding the deferred-length truncation scenarios. |
| docs/_storage-backends/aws-s3.md | Documents read-after-write consistency and multi-instance locking considerations for the S3 backend. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Deferred-length uploads to S3 can lose their final chunk with no error. The
.infoobject says the upload is done and the offset matches the declared size, but the S3 object is short (or 0 bytes for uploads underMinPartSize). The client gets a 2xx and a truncated file.Root cause
A chunk smaller than
MinPartSize, written while the length is still deferred, can't become a real multipart part yet, soWriteChunkstashes it in a side object (<id>.part).FinishUploadassembled the object only from real multipart parts (ListParts) and never looked at.part. So when a client declares the length and finishes without one moreWriteChunk, the stashed bytes are dropped.This is the promotion gap from #396 (2021) and #798 (2022). The value-receiver half of #396 got fixed; promoting the incomplete part in
FinishUploadwas proposed but never implemented. The classic tus v1 flow stayed safe by accident, because tusd always sends one moreWriteChunkafterDeclareLength. The IETFUpload-Completeflow doesn't, which reopens it.Fix
FinishUploadpromotes the.partobject into a real final part before completing, reusing the same download/upload/delete helpers asWriteChunk. It's guarded by the declared size so retries stay correct: parts already summing to the declared size mean a stale.part(delete it, don't re-promote); parts plus.partsumming to it mean promote; anything else returns an error instead of completing short. Promotion runs only when an incomplete part actually lingers, so normal completions and the concat path are untouched.This also resets the cached
incompletePartSizeinWriteChunkafter the.partis deleted, so aFinishUploadlater in the same request doesn't fire a redundantDeleteObject.Tests
Three gomock regression tests drive the real NewUpload/WriteChunk/DeclareLength/FinishUpload flow: promote a tail after a real part, promote when the whole upload is under
MinPartSize(this used to complete as 0 bytes), and don't re-upload an already-promoted part on retry. (Thanks to Claude for writing these tedious, detailed, inscrutable mock tests.)Storage-layer only, no dependency on the in-flight handler idempotency work (#1374/#1366).
Refs #396, #798.