Shared-layer lifecycle: accounting, eviction, recovery, materialization - #458
Shared-layer lifecycle: accounting, eviction, recovery, materialization#458chruffins wants to merge 19 commits into
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 33c8ca3. Configure here.
| info, statErr := os.Stat(dirPath) | ||
| if statErr != nil || info.ModTime().After(cutoff) { | ||
| continue | ||
| } |
There was a problem hiding this comment.
Eviction can wipe in-flight layers
Medium Severity
Layer eviction decides freshness from the digest directory ModTime, and it runs concurrently with materialization. Unpack and mkfs.erofs write inside child temp dirs, so the parent mtime goes stale; DeleteImage can then RemoveAll that tree while a build is still using it, especially during the later unlocked ExportRootfs window before the manifest is written.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 33c8ca3. Configure here.
| if result.CacheHit { | ||
| cacheStatus = "hit" | ||
| } | ||
| m.recordImageBuildPhase(ctx, ref.Digest(), "layer_materialization", time.Since(materializeStart), "success", cacheStatus) |
There was a problem hiding this comment.
Layer bytes skipped after failed builds
Medium Severity
Materialized layer artifacts are written to the layer store before conversion, but refreshDiskUsageTotals only runs on successful finalize, delete, or startup. After a failed conversion the artifacts remain on disk while TotalImageBytes keeps the stale cached total, so capacity admission undercounts the real footprint.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 33c8ca3. Configure here.
41f4080 to
35de95e
Compare
35de95e to
f9a5fcc
Compare
d9caa0e to
b0a27b4
Compare
b0a27b4 to
37b8128
Compare
37b8128 to
13adf76
Compare
13adf76 to
c0e6c32
Compare
c0e6c32 to
aac8b57
Compare
aac8b57 to
7464627
Compare
4467fe9 to
588fff8
Compare
ac3de8e to
165e42f
Compare
165e42f to
3cac19c
Compare
8ca57c1 to
419c294
Compare
c238f03 to
5051c46
Compare
e6597bd to
fe533bb
Compare
fe533bb to
962f4d2
Compare
962f4d2 to
2250812
Compare
2250812 to
3787792
Compare
Skip layer materialization for image formats without layer artifact support (cpio), and set the rootfs type and config media type that the manifest model validation now requires in the lifecycle test fixture.
Invoke layer store reconciliation on image deletion and at manager startup so reference-protected eviction and stale temp-dir sweeping actually run; previously the GC had no production call sites. Hold in-flight layer references until the manifest model protecting them is durable in finalizeImage, closing a window where reconciliation could evict cache-hit layers a build was about to reference. Fix the remaining review findings: abort eviction when the manifest reference walk fails instead of evicting against a truncated set, use removePath so eviction handles read-only layer trees, batch in-flight reference registration per build, drop the redundant layerRefMu, delete the dead totalLayerArtifactBytes duplicate, sweep .tag-stage-* by walking the whole images tree, and revert TotalImageBytes to ready-only bytes so consumers summing it with TotalOCICacheBytes no longer double count the cache.
finalizeImage released the in-flight layer refs and buildImage's deferred release fired again afterwards, so two builds sharing a layer could drop each other's protection mid-materialization; the release is now an idempotent handle shared by both sites, with finalization releasing under createMu before the ready notification. The eviction reference scan also missed manifest models written at legacy layout paths, so ready legacy-layout images could lose their layers; the scan now walks the whole images tree, which covers both layouts. Also make layer materialization best effort as documented (log and continue, since the rootfs is already composed from blobs), drop the redundant inflight snapshot helper and single-element roots slice, move the eviction metric behind the usual recorder, and reuse writeLayerTestLayout for the shared-layout test fixture.
The deferred layer-ref release mutated the in-flight map without createMu, racing retention and reconciliation reads; release now takes the lock while finalization keeps the already-locked variant. The eviction walk resolved every manifest model through the content-layout path, so legacy-layout models were skipped and their layers left unprotected; the walk now reads the model at the path it found. tryEvictLayerArtifact also drops its unreachable in-flight check, which the reference set already covers.
cleanupReplacedTag now evicts the replaced image's unreferenced layers like the other delete paths instead of leaving them until the next delete or restart, the manifest-walk error names what it walks, and the compose doc no longer claims .compose-* staging gets the same startup sweep as .unpack-* dirs.
Umoci extraction restores read-only directory modes from layer metadata, so os.RemoveAll cannot unlink through a stale .unpack-* dir and the startup sweep would leave it behind indefinitely.
A crashed build can strand a .compose-* staging directory in the digest-keyed build directory, invisible to disk accounting; each compose attempt now removes stale siblings before creating its own staging directory.
Inline tryEvictLayerArtifact into the eviction loop, use the walk entry's cached info instead of a second stat in the temp-dir sweep, and share the import-and-wait and digest-extraction sequences in the lifecycle tests through small helpers.


summary
Capstone stage of the image-storage project — finishes migration, eviction, accounting, recovery, and observability on the shared-layer path.
images/layers/<digest>/(best effort; the composed rootfs still comes from blobs, so an artifact failure is logged and only degrades sharing)..unpack-*/.install-*/.tag-stage-*temp directories are swept at startup (age-gated so live builds are untouched).TotalOCICacheBytes, soCurrentImageStorageBytes(ready rootfs + cache/layers) counts them exactly once and capacity admission sees the real footprint.hypeman_images_layer_artifacts_evicted_totalcounter plus slog eviction summaries and per-layer materialization warnings.validation
go test ./lib/images ./lib/paths ./lib/builds ./lib/scopesgreen;cmd/api/apigreen except Docker Hub pulls (anonymous rate limit — confirmed via direct probe returning TOOMANYREQUESTS) and VM lifecycle tests (need bridge privileges; verified failing identically on unmodified main).