Skip to content

fix(artifact-cas): stream uploads to object-store backends to bound CAS memory - #3343

Merged
javirln merged 2 commits into
chainloop-dev:mainfrom
javirln:javier/pfm-6923-artifact-cas-buffers-the-entire-upload-in-memory-oom-kills
Aug 12, 2026
Merged

javirln merged 2 commits into
chainloop-dev:mainfrom
javirln:javier/pfm-6923-artifact-cas-buffers-the-entire-upload-in-memory-oom-kills

Conversation

@javirln

@javirln javirln commented Aug 12, 2026 •

Copy link
Copy Markdown
Member

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.

Review in cubic

@chainloop-platform

chainloop-platform Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

AI Session Checks — ⚠️ 1 session(s) missing

Missing AI Coding Sessions

We detected commits in this PR that were AI-assisted, but the matching Chainloop Trace session(s) could not be found in Chainloop.

Please make sure the AI coding session evidence has been sent by the Chainloop CLI, or add the skip-ai-session label to this PR to bypass this check.

Learn more about Chainloop Trace.


Security Checks — ✅ 2 passing

PR info

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

View attestation ↗


Powered by Chainloop and Chainloop Trace

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 12 files

Re-trigger cubic

…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
@javirln
javirln force-pushed the javier/pfm-6923-artifact-cas-buffers-the-entire-upload-in-memory-oom-kills branch from 3731d6c to 98693ec Compare August 12, 2026 07:50
@javirln
javirln marked this pull request as ready for review August 12, 2026 08:17
@javirln
javirln requested review from a team and a balanced review from Copilot August 12, 2026 08:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.StreamingUploader interface; S3/S3-access-point/Azure Blob implement it while OCI intentionally does not.
  • Splits the CAS Write handler into streamUpload (pipe-based) and bufferedUpload paths, with a shared checkUploadSize cap and explicit error classification (backendUploadError vs. 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=true message 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.

Comment thread app/artifact-cas/internal/service/bytestream.go Outdated
jiparis
jiparis previously approved these changes Aug 12, 2026

@jiparis jiparis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, please check my comment.
Thanks!

Signed-off-by: Javier Rodriguez <javier@chainloop.dev>
@javirln
javirln merged commit 6a32501 into chainloop-dev:main Aug 12, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants