Make OCI image layer cache safe for concurrent use#3150
Make OCI image layer cache safe for concurrent use#3150robnester-rh wants to merge 4 commits intoconforma:mainfrom
Conversation
Review Summary by QodoMake OCI image layer cache safe for concurrent use
WalkthroughsDescription• Wrap OCI image layer cache with thread-safe singleflight serialization • Prevents race conditions when multiple goroutines validate shared layers • Add comprehensive unit tests for concurrent cache access patterns • Add acceptance test validating parallel image validation with cache Diagramflowchart LR
A["FilesystemCache"] -->|wrapped by| B["SafeCache"]
B -->|serializes Put| C["singleflight.Group"]
B -->|wraps Layer| D["SafeLayer"]
D -->|serializes Compressed/Uncompressed| C
E["Multiple Goroutines"] -->|concurrent access| B
B -->|prevents races| F["Shared Layer Cache"]
File Changes1. internal/utils/oci/client.go
|
Code Review by Qodo
1. require in goroutines
|
f1b01de to
5be317b
Compare
Wrap go-containerregistry's FilesystemCache in a thread-safe wrapper that uses singleflight to serialize Put and layer stream reads. Prevents races when multiple goroutines validate the same image (e.g. parallel components sharing layers). Add unit tests for the safe cache and an acceptance scenario that runs validation with EC_CACHE=true. Ref: conforma#1109 Ref: EC-1669 Co-authored-by: Claude Code <noreply@anthropic.com> Signed-off-by: Rob Nester <rnester@redhat.com>
|
/review |
PR Reviewer Guide 🔍(Review updated until commit 37c6631)Here are some key observations to aid the review process:
|
Codecov Report❌ Patch coverage is
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
Use digest- and diffID-scoped singleflight inside Compressed() and Uncompressed() so only one goroutine runs the inner stream per digest across all safeLayer instances (e.g. concurrent Get(digest) callers). Waiters no longer depend on the first caller closing a reader, avoiding deadlock. Remove per-layer ready channels and streamingCloseReader. Add unit tests for error paths, cache-hit paths, and concurrent Get callers calling Compressed(); add a short comment in the acceptance scenario for the parallel-validation-with-cache step. Ref: conforma#1109 Ref: EC-1669 Co-authored-by: Claude Code <noreply@anthropic.com> Signed-off-by: Rob Nester <rnester@redhat.com>
|
/review |
|
Persistent review updated to latest commit 37c6631 |
st3penta
left a comment
There was a problem hiding this comment.
I'm not super familiar with concurrency and single flights, but as far as i can tell, it looks good.
Consider this as a low-confidence approval 😄
simonbaird
left a comment
There was a problem hiding this comment.
Not sure if I followed it all, but I guess we're trusting Claude here. 🤞 .
Lgtm, would be nice to comment a few of the less obvious parts.
This commit adds comments to better explain the purpose of the `Compressed()` and `Uncompressed()` functions in the `internal/utils/oci/safe_cache.go` file. Ref: conforma#1109 Ref: EC-1669 Signed-off-by: Rob Nester <rnester@redhat.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a concurrency-safe wrapper (SafeCache) around the OCI filesystem cache to serialize per-digest/diffID access using singleflight, updates client code to use it, and adds a feature test and a comprehensive unit-test suite verifying concurrent behavior and error propagation. Changes
Sequence Diagram(s)sequenceDiagram
participant Validator
participant SafeCache
participant InnerCache
participant Filesystem
Validator->>SafeCache: Put/Get layer (digest or diffID)
alt first writer for digest
SafeCache->>SafeCache: singleflight.Do(key)
SafeCache->>InnerCache: Put(layer) / Get stream
InnerCache->>Filesystem: write cache file (tmp -> final)
Filesystem-->>InnerCache: file written
InnerCache-->>SafeCache: layer stream / success
SafeCache-->>Validator: return safeLayer referencing cached file
else concurrent waiters
SafeCache-->>SafeCache: wait for singleflight result
SafeCache-->>Validator: open cached file and return layer
end
Validator->>SafeCache: Compressed()/Uncompressed()
SafeCache->>Filesystem: open cached file by digest/diffID
Filesystem-->>SafeCache: stream file handle
SafeCache-->>Validator: provide reader (shared, read-only)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.5.0)Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/utils/oci/safe_cache.go`:
- Around line 144-163: The singleflight call (l.flight.Do) currently returns
before the background drain finishes (fn returns ready channel immediately),
letting other callers race with inner.Compressed/Uncompressed and suppressing
io.Copy/Close errors; change the fn passed to l.flight.Do in both the Compressed
and Uncompressed paths so it performs the drain and Close synchronously (or
waits for the drain to complete) before returning, propagate any io.Copy or
rc.Close errors out of the fn instead of discarding them, and only return the
ready state (or success) after the file is fully written so that subsequent
callers see the completed file; adjust callers that use the returned value (v
from l.flight.Do) to handle the propagated error and open the file only after
the singleflight-protected operation completes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 117574e7-0be3-49c3-b32c-69ad718ffec8
⛔ Files ignored due to path filters (1)
features/__snapshots__/validate_image.snapis excluded by!**/*.snap
📒 Files selected for processing (4)
features/validate_image.featureinternal/utils/oci/client.gointernal/utils/oci/safe_cache.gointernal/utils/oci/safe_cache_test.go
Perform io.Copy and rc.Close inside the callback passed to flight.Do so the callback returns only after the cache file is written. Propagate io.Copy and Close errors instead of discarding them. Update doc comments for Compressed and Uncompressed to describe the flow. reference: EC-1669 Made-with: Cursor
|
@robnester-rh does the cache get freed once the Conforma is done with a particular component? (This is the pattern Joe had to debug and fix a few weeks back.) The concern is if we fill up all the memory with unfreed cache data for 200 or so components we'll OOM and crash the task and OpenShift folks will be unhappy. @joejstuart Should we do a little memory profiling before/after on this? Otherwise lgtm. |
The OCI image cache is disk-backed ( |
Wrap go-containerregistry's FilesystemCache in a thread-safe wrapper that uses singleflight to serialize Put and layer stream reads. Prevents races when multiple goroutines validate the same image (e.g. parallel components sharing layers).
Add unit tests for the safe cache and an acceptance scenario that runs validation with EC_CACHE=true.
Ref: #1109
Ref: EC-1669