S3: track manifest blob ownership through multipart completion (#10386)

* s3: track manifest blob ownership through multipart completion

Manifest blobs made three orphan paths. A partial fold that failed midway
kept its earlier batches on volume servers while the write fell back to
flat chunks; the fold now records each saved blob and deletes them on
error. A completion that failed after preparing left its fresh manifests
behind on every retry; the completion state now owns them and deletes
them unless a failed rollback left the version entry still holding them.
And a completed upload removes its parts metadata-only, which stranded
the part-manifest blobs superseded by flattening; those are collected
during flattening and deleted once the completion commits.

Two shared-chunk hazards nearby: the version-file rollback deleted its
data, destroying the still-registered parts (worse once manifests
resolve to inner chunks), and the idempotent-replay cleanup data-deleted
leftover parts whose chunks the live object references. Both are
metadata-only now.

* s3: trim chunk manifest comments

* s3: test manifest fold rollback and part range selection

The fold-with-rollback and the boundary-to-byte-range logic were only
exercised by hand against a live server; give both an injectable seam
and cover the fold, the below-threshold and SSE no-ops, the midway
failure deleting the blobs it saved, and offset-vs-legacy-index range
selection including indexes that no longer address the chunk list.
This commit is contained in:
Chris Lu authored and GitHub committed 2026-07-21 08:59:40 -07:00
1 parent 5a54beac80
commit 68a4e3347f
7 files changed
+235 -68

No files matched your search

+20 -14
View File
@@ -853,21 +853,14 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request)
// Note: ETag is NOT overridden - AWS S3 returns the complete object's ETag
// even when requesting a specific part via PartNumber
var startOffset, endOffset int64
if partInfo != nil && partInfo.EndOffset > partInfo.StartOffset {
// Byte-offset boundaries: valid regardless of how the entry's
// chunk list is laid out (flat or manifest chunks)
startOffset = partInfo.StartOffset
endOffset = partInfo.EndOffset - 1
} else if partInfo != nil {
// Legacy boundaries carry chunk indexes into the flat chunk list
if partInfo.StartChunk < 0 || partInfo.EndChunk <= partInfo.StartChunk || partInfo.EndChunk > len(objectEntryForSSE.Chunks) {
if partInfo != nil {
var ok bool
startOffset, endOffset, ok = partRange(partInfo, objectEntryForSSE.Chunks)
if !ok {
glog.Errorf("GetObject: part %d boundary chunks [%d,%d) out of range (chunks: %d)", partNumber, partInfo.StartChunk, partInfo.EndChunk, len(objectEntryForSSE.Chunks))
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
return
}
startOffset = objectEntryForSSE.Chunks[partInfo.StartChunk].Offset
lastChunk := objectEntryForSSE.Chunks[partInfo.EndChunk-1]
endOffset = lastChunk.Offset + int64(lastChunk.Size) - 1
} else {
// Fallback: assume 1:1 part-to-chunk mapping (backward compatibility)
chunkIndex := partNumber - 1
@@ -3047,13 +3040,26 @@ type PartBoundaryInfo struct {
StartChunk int `json:"start"`
EndChunk int `json:"end"` // exclusive
ETag string `json:"etag"`
// Byte offsets of the part within the object; preferred over the chunk
// indexes, which stop matching once the entry's chunk list is folded
// into manifest chunks. Zero EndOffset means a legacy boundary record.
// Byte offsets of the part; zero EndOffset means a legacy record carrying
// chunk indexes only.
StartOffset int64 `json:"startOffset,omitempty"`
EndOffset int64 `json:"endOffset,omitempty"` // exclusive
}
// partRange returns the inclusive byte range of a part, preferring byte
// offsets; ok is false when a legacy record's chunk indexes do not address
// the entry's chunk list.
func partRange(partInfo *PartBoundaryInfo, chunks []*filer_pb.FileChunk) (startOffset, endOffset int64, ok bool) {
if partInfo.EndOffset > partInfo.StartOffset {
return partInfo.StartOffset, partInfo.EndOffset - 1, true
}
if partInfo.StartChunk < 0 || partInfo.EndChunk <= partInfo.StartChunk || partInfo.EndChunk > len(chunks) {
return 0, 0, false
}
lastChunk := chunks[partInfo.EndChunk-1]
return chunks[partInfo.StartChunk].Offset, lastChunk.Offset + int64(lastChunk.Size) - 1, true
}
// rc is a helper type that wraps a Reader and Closer for proper resource cleanup
type rc struct {
io.Reader