Shared-layer lifecycle: accounting, eviction, recovery, materialization - #458
Shared-layer lifecycle: accounting, eviction, recovery, materialization#458chruffins wants to merge 33 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
fe533bb to
962f4d2
Compare
962f4d2 to
2250812
Compare
2250812 to
3787792
Compare
Drop the context-less wrapper pairs (materializeLayerArtifact, installLayerArtifact, unpackCachedLayer, unpackLayerBlob, applyLayerTree) in favor of single ctx-taking functions; two of the wrappers had no callers. Reuse unpackCachedLayer from materializeLayerArtifactOnce instead of duplicating the blob lookup and diff-id check. Split the lexical confinement out of safeJoinForComposition into confineToRoot so safeJoin no longer performs and discards a full symlink resolution per tar entry. safeJoin also returns early when the entry resolves to the root itself, which previously looped forever on a "." entry. Pass the walked DirEntry's FileInfo into the copy helpers instead of re-Lstat'ing, remove the unreachable symlink branch in copyDirectoryEntry (callers already resolve through resolveCompositionDirTarget), and lean on removePath's own tree-writability handling for whiteout removal.
Restoring atime required per-platform Stat_t accessors for a value that tar headers almost never carry and that nothing reads back. Set atime to mtime on both the tar extraction and tree copy paths instead.
Replace the hand-rolled tar extractor and tree merger with umoci's layer.UnpackLayer, which the pull path already depends on. umoci confines entries with filepath-securejoin, interprets OCI whiteouts, and follows the same directory-over-symlink rule as containerd and docker (replace, do not write through), so composition no longer diverges from other runtimes. Per-layer artifacts are extracted in OverlayfsRootfs form: whiteouts become 0:0 character devices and opaque directories carry the overlayfs opaque xattr, the on-disk representation an overlayfs mount of stacked layers understands. Composition applies each layer directly onto the staging tree with DirRootfs, removing the unpack-then-copy pass and the directory mode bookkeeping it required. Ownership is preserved when running as root; unprivileged runs use umoci's rootless mode as before. The record drops the entry count, which is no longer observable without a second pass over the tar, and reports the decompressed tar size as unpacked bytes.
The singleflight body ran with the first caller's context, so cancelling one pull failed materialization for every concurrent pull waiting on the same layer. Run the build under context.WithoutCancel; callers still return on their own cancellation.
Fold contextReader into layer_artifact.go, its only user. Drop the context-aware mkfs.erofs wrapper: the shared build now runs detached from caller cancellation, so the context could never fire during conversion, and ExportRootfs already dispatches on the format.


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 only degrades sharing). Newlayer_materializationbuild phase with cache-hit attribution..unpack-*/.install-*temp directories are swept at startup (age-gated so live builds are untouched).TotalImageBytesnow includes the layer store's physical bytes (TotalLayerBytesexposes them separately), so 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).Note
Medium Risk
Changes image deletion, startup cleanup, and disk admission accounting; incorrect eviction could remove data still needed by images, though manifest references and a grace period mitigate that.
Overview
Completes the shared-layer storage path: pulled images materialize per-layer artifacts under
images/layers/(best effort, with alayer_materializationbuild phase), and reference-aware eviction removes layer dirs only when no manifest model still references them.Deletion and startup run eviction plus a sweep of stale
.unpack-*/.install-*temps, gated by a 10-minute grace period so in-flight builds are not raced.TotalImageBytesnow counts ready rootfs metadata plus physical layer-store bytes (TotalLayerBytesexposes layers alone); disk-usage caching tracks the newlayerBytescomponent.Adds
hypeman_images_layer_artifacts_evicted_totaland lifecycle tests (shared base layer materialized once, survives partial deletes, full eviction when the last reference goes).Reviewed by Cursor Bugbot for commit 33c8ca3. Configure here.