Stream PUT bodies directly to S3 instead of buffering in memory#11
Open
joh-klein wants to merge 1 commit into
Open
Stream PUT bodies directly to S3 instead of buffering in memory#11joh-klein wants to merge 1 commit into
joh-klein wants to merge 1 commit into
Conversation
Previously store_artifact collected the entire request body into memory
(and copied it a second time in S3Storage::store) before a single
PutObject, so peak memory was ~2x artifact size per concurrent upload.
Now the request body is piped straight through to S3:
- StorageProvider::store takes a byte stream plus the content length
(object stores need the size up front) instead of a ReaderStream
- the handler passes the body's data stream through when the client
sends Content-Length, and falls back to buffering only for chunked
transfer encoding
- S3Storage wraps the stream in an http_body with an exact size hint,
which the SDK needs to set Content-Length and stream the default
integrity checksum (an unsized body fails with UnsizedRequestBody)
- custom endpoints skip request checksums: the SDK streams the checksum
as a single aws-chunked chunk, which MinIO rejects for bodies >16MiB
("chunk too big"); plain AWS S3 keeps the default checksum behavior
All added dependencies were already in the tree transitively.
Verified against MinIO: a 512MB PUT holds server RSS at ~20MB (was
>1GB), round-trip sha256 matches, and 409/404/401 and the chunked
fallback behave as before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.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.
Problem
store_artifactcurrently collects the entire request body into memory (to_bytes(body, usize::MAX)), andS3Storage::storecopies it into a secondVecbefore a singlePutObject. Peak memory is roughly 2× artifact size per concurrent upload, while the README advertises direct streaming with <4MB RAM. GET already streams; this PR brings PUT in line.Changes
StorageProvider::storenow takes a byte stream (impl Stream<Item = Result<Bytes, io::Error>>) pluscontent_length— object stores need the size up front.body.into_data_stream()straight through when the client sendsContent-Length(the normal Nx client case). Requests without one (chunked transfer encoding) fall back to the previous buffering behavior, since the size must be known.S3Storagewraps the stream in a smallhttp_body::Bodyadapter reporting an exactsize_hint. Without it the SDK rejects streaming uploads (UnsizedRequestBodyfrom the checksum interceptor); with it the SDK setsContent-Lengthand streams the default integrity checksum itself.S3_ENDPOINT_URL) setRequestChecksumCalculation::WhenRequired: the SDK streams its default CRC32 checksum as one aws-chunked chunk covering the whole body, and MinIO rejects chunks >16MiB (chunk too big: choose chunk size <= 16MiB). Plain AWS S3 accepts it, so the default checksum behavior is kept there.Cargo.tomlentries (bytes,http-body,http-body-util,sync_wrapper,aws-smithy-types) were all already inCargo.locktransitively — no new crates in the tree.Trade-off to be aware of: a streamed body is non-replayable, so the SDK can't internally retry a failed
PutObject; the Nx client's own retry covers that. The buffered fallback path retains SDK retries.Testing
e2e against MinIO (
minio/miniolatest, bucket + token auth):Content-Lengthset)Transfer-Encoding: chunked(no length)cargo buildandcargo fmtclean. I don't have a scratch AWS account wired up here, so the real-S3 path (default checksums) wasn't e2e-tested — it's the SDK's standard sized-streaming-body path (same asByteStream::from_path), but happy to adjust if you'd rather keep it simpler.🤖 Generated with Claude Code