blob: add multipart uploads - #3769
Conversation
Large objects on S3, GCS and Azure can be assembled from parts uploaded
in any order, concurrently, or from separate processes. blob.Writer
cannot express that: it is a single sequential stream owned by one
goroutine. This adds a portable API for it.
u, err := b.NewMultipartUploader(ctx, key, nil)
part, err := u.UploadPart(ctx, blob.UploaderPart{Number: 1}, r)
err = u.Commit(ctx, parts)
Parts are numbered from 1 and assembled in ascending order regardless of
the order they were uploaded in. Nothing is readable at the key until
Commit. UploadID is durable, so OpenMultipartUploader can resume an
upload begun elsewhere.
Driver support is optional. blob.Bucket type-asserts
driver.MultipartUploaderBucket and returns Unimplemented when a driver
does not provide it, so no existing driver -- in this repo or outside
it -- has to change. The prefixed and single-key bucket wrappers forward
the interface, so blob.PrefixedBucket and "?prefix=" URLs keep it.
memblob implements it, which makes the feature demonstrable in a test
and in a runnable example without credentials. drivertest covers
out-of-order upload, abort, resume, and that the committed object
carries the content type, cache control and metadata it was created
with.
… s3blob s3blob maps onto S3's native multipart upload: CreateMultipartUpload, UploadPart, CompleteMultipartUpload. Part numbers are validated against S3's documented 1-10000 range, and OpenMultipartUploader confirms the upload exists with ListParts so a mistyped or expired upload ID fails where it was passed rather than much later at Commit. azureblob uses block blobs: StageBlock per part, CommitBlockList to assemble. gcsblob composes objects. fileblob writes parts as sidecar files and concatenates them on commit, renaming the assembled file into place so nothing is visible at the key until Commit succeeds. Each driver persists its upload state, so an UploadID outlives the process that created it and OpenMultipartUploader can resume from another one.
|
Hmm. I don't think this is something we want or need to expose. The drivers should do this automatically. For example, S3's transfermanager (which |
|
Fair enough on throughput — you're right, it plainly can, and BufferSize/MaxConcurrency already tune it. s3blob wires them into transfermanager, azureblob into UploadStream. Three things I'd still like your read on. gcsblob is the odd one out today. It sets ChunkSize from BufferSize but never reads MaxConcurrency, and doesn't use EnableParallelUpload. So of the three, GCS is the only one not getting the automatic behaviour. Resumability isn't covered by any of them. transfermanager.Client exposes UploadObject and no upload ID, no resume, no abort handle. If the process dies mid-upload the transfer starts from zero, and the incomplete MPU is orphaned in S3 until a lifecycle rule aborts it. Is that a gap you'd consider in scope for blob, or deliberately out of it? That last clause is the crux for me. When the driver owns the upload ID, a crash leaves state the caller cannot see, resume, or abort; recovery is only possible out-of-band via lifecycle rules. When the caller owns it, they can persist it and either finish or abort on restart. So perhaps the real question isn't multipart vs. automatic, but whether blob wants to expose any handle on in-flight upload state at all. If the answer is no, I'm happy to close this. no problem. |
|
"gcsblob is the odd one out today" -> yes, let's deal with that explicitly. "whether blob wants to expose any handle on in-flight upload state" -> I think the answer to that is "no". I'd prefer to keep the simple semantics we have, which seem to be good enough for ~everyone (nobody has asked for this feature in the last few years). Escape hatches exist for users who want to handle the edge cases. |
Assembles one blob from parts uploaded in any order, concurrently, or from separate processes. blob.Writer cannot express that: drivers do fan out underneath it -- s3blob via transfermanager, azureblob via UploadStream -- but the upload belongs to one process and dies with it. This gives the caller a durable UploadID instead, so an upload can be started on one machine, written from others, committed by a third, and resumed after a crash. The implementation here uses only the public gocloud.dev/blob API: parts are staged as ordinary objects and Commit streams them, in ascending part number, into a single Writer at the destination. Because every read and write goes through the bucket, the driver applies its own key escaping and any PrefixedBucket wrapping, so a committed object always lands where blob.Bucket will find it. That covers fileblob, sftpblob, httpblob over WebDAV and memblob with no backend-specific code. Its cost is that Commit re-reads and rewrites each part, which is why native s3mp/gcsmp/azmp packages will follow. Two decisions come from bugs found in the rejected upstream proposal (google/go-cloud#3769): UploadPart takes a part number and nothing else. That PR's UploaderPart carried an Offset, and fileblob assembled by it while memblob sorted by Number, so portable code passing only Number silently produced corrupt bytes. With no offset in the signature the failure is unrepresentable. The mptest conformance suite passes only what the documented API requires. Its upstream counterpart always supplied Offset and Size, which is precisely why it never caught the above. It also asserts that Parts survive encoding/json, since a Part that cannot cross a process boundary defeats the purpose of the package. Options are stored in a manifest object beside the parts, so a process that resumes an upload commits with the content type and metadata it was created with rather than whatever the resuming caller happened to pass. 60 conformance subtests pass across memblob, fileblob and a prefixed bucket, with none skipped. Resume is exercised through an independent bucket handle on fileblob, which is the cross-process path this package exists for.
Adds multipart upload to
blob.S3, GCS and Azure can all assemble one object from parts uploaded in any order, concurrently, or from separate processes.
blob.Writercannot express that — it is a single sequential stream owned by one goroutine — so today the only portable way to write a large object is to stream it whole, and callers who need the native behaviour drop out of the portable API intoAs.API
UploadPartreturns the part valueCommitneeds; the caller collects them.Commit.UploadIDis durable, soOpenMultipartUploaderresumes an upload started in another process.MultipartUploaderOptionsmirrorsWriterOptions(content type, cache control, metadata,ContentCRC32C,BeforeUpload), minus the content-type sniffing, which a multipart upload cannot do.Compatibility
Driver support is optional.
blob.Buckettype-assertsdriver.MultipartUploaderBucketand returnsUnimplementedwhen the driver doesn't provide it, so no existing driver — in this repo or outside it — has to change. The prefixed and single-key wrappers forward the interface, soblob.PrefixedBucketand?prefix=URLs keep multipart rather than reporting it unsupported.Drivers
s3blob uses S3's native multipart upload. azureblob uses block blobs (
StageBlock/CommitBlockList), gcsblob composes objects, and fileblob and memblob assemble the object themselves — fileblob by renaming the assembled file into place, soCommitstays atomic.Testing
drivertestcovers out-of-order upload, abort, resume, and that the committed object carries the content type, cache control and metadata it was created with. memblob and fileblob run it for real, including through the prefixed wrapper, and need no recordings. There's a runnableExampleon memblob, so the feature is demonstrable without credentials.The cloud drivers need replay recordings before this can go green. azureblob, gcsblob and s3blob test against recorded HTTP replays, and none exist for the new multipart calls, so the new conformance subtest fails for them:
Recording them needs real cloud credentials, which I don't have.
How I'd suggest splitting this
The first commit — core API, memblob, conformance coverage, docs — builds and tests green on its own and needs no recordings. The second adds the four remaining drivers, three of which cannot pass CI until someone with credentials records them.
Happy to reshape along that line: land the core with memblob and fileblob (both fully tested), and let azureblob, gcsblob and s3blob follow in a separate PR once recordings exist. Or leave it as-is if you'd rather record them and take it in one go. Let me know which you prefer and I'll push it.
Worth arguing about
The exported surface:
Bucketgains two methods and the package gains four types, and those are hard to change once released even at v0.x. I'd rather adjust the names and the shape ofUploaderPartnow than after.