Repository navigation
fix(artifact-cas): stream uploads to object-store backends to bound CAS memory - #3343
Conversation
AI Session Checks —
|
| Status | Policy | Messages |
|---|---|---|
| ✅ Passed | pr-description-required |
- |
| ✅ Passed | pr-user-story-linked |
- |
⏭️ 3 scans not applied
| Scan | Reason |
|---|---|
vulnerability-scan |
no manifest/lockfile changed |
github-actions-scan |
no workflow files changed |
iac-scan |
no IaC files changed |
Powered by Chainloop and Chainloop Trace
…AS memory The CAS bytestream Write RPC buffered the entire artifact in memory before flushing it to the storage backend, so a large upload grew CAS memory ~1:1 with artifact size and OOM-killed the pod mid-stream. Object-store backends (S3, S3 access point, Azure Blob) now advertise a StreamingUploader capability and are fed directly from the client stream through an io.Pipe, so peak CAS memory stays bounded regardless of artifact size. The OCI backend keeps the fully-buffered path because its push implementation needs the whole layer content up front. A backend upload failure is always surfaced as an internal error and never misclassified as a client disconnect, and the per-upload size cap is enforced on both paths. The download path, including digest verification, is unchanged. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez <javier@chainloop.dev> Chainloop-Trace-Sessions: 884134e6-0e9d-43aa-9637-2c5e1cfac1e5
3731d6c to
98693ec
Compare
There was a problem hiding this comment.
Pull request overview
This PR addresses a memory-exhaustion problem in the Artifact CAS bytestream Write RPC. Previously the service buffered the entire artifact in memory before flushing it to the storage backend, so CAS memory grew roughly 1:1 with artifact size and large uploads could OOM-kill the CAS pod. The change introduces an optional StreamingUploader capability: object-store backends (S3, S3 access point, Azure Blob) now advertise streaming support and are fed directly from the client stream through an io.Pipe, bounding peak CAS memory independently of artifact size. The OCI backend keeps the fully-buffered path because its push implementation needs the whole layer content up front.
Changes:
- Adds an optional
backend.StreamingUploaderinterface; S3/S3-access-point/Azure Blob implement it while OCI intentionally does not. - Splits the CAS
Writehandler intostreamUpload(pipe-based) andbufferedUploadpaths, with a sharedcheckUploadSizecap and explicit error classification (backendUploadErrorvs. feed/stream errors) so a backend failure is never misreported as a client disconnect. - Fixes a latent bug where data carried on a
finish_write=truemessage was dropped, and adds extensive streaming/buffered/download regression tests.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
pkg/blobmanager/backend.go |
Adds the optional StreamingUploader interface. |
pkg/blobmanager/s3/backend.go |
Declares SupportsStreaming()==true (manager.Uploader multipart). |
pkg/blobmanager/s3accesspoint/backend.go |
Declares SupportsStreaming()==true. |
pkg/blobmanager/azureblob/backend.go |
Declares SupportsStreaming()==true (UploadStream). |
pkg/blobmanager/*/backend_test.go |
Asserts each backend's streaming opt-in; OCI pinned as non-streaming. |
app/artifact-cas/internal/service/bytestream.go |
Adds streamUpload/bufferedUpload, feedPipe, error classification, shared size cap, and fixes the finish_write data-drop. |
app/artifact-cas/internal/service/bytestream_test.go |
Adds streaming backend wiring and buffered-path parity/error tests. |
app/artifact-cas/internal/service/bytestream_streaming_test.go |
New suite exercising the streaming path (integrity, size caps, disconnect/error classification). |
app/artifact-cas/internal/service/bytestream_download_test.go |
New regression battery pinning unchanged download digest-verification behavior. |
I reviewed the concurrency model (io.Pipe feed goroutine, done-channel happens-before ordering, and the feedErr/uploadErr/errPipeConsumerGone precedence), the shared size-cap enforcement, the finish_write reordering fix, backend Upload streaming semantics, and the test coverage. I did not find concrete defects. Because this is a concurrency-heavy change to a critical, security-sensitive upload path, I'm deferring the final approval to a human reviewer rather than storing speculative comments.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
jiparis
left a comment
There was a problem hiding this comment.
Looks great, please check my comment.
Thanks!
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>
Summary
The Artifact CAS bytestream Write RPC buffered the entire artifact in memory before flushing it to the storage backend, so CAS memory grew roughly 1:1 with artifact size and a large upload OOM-killed the CAS pod mid-stream.
Object-store backends (S3, S3 access point, Azure Blob) now advertise a streaming capability and are fed directly from the client stream through an
io.Pipe, so peak CAS memory stays bounded regardless of artifact size. The OCI backend keeps the fully-buffered path because its push implementation needs the whole layer content up front.A backend upload failure is always surfaced as an internal error and is never misclassified as a client disconnect. The per-upload size cap is enforced on both the streaming and buffered paths. The download path, including digest verification, is unchanged.
Resolves PFM-6923.
AI assistance
Claude Code was used in producing this change.