Skip to content

Shared-layer lifecycle: accounting, eviction, recovery, materialization - #458

Draft
chruffins wants to merge 19 commits into
hypeship/rootfs-compositionfrom
hypeship/storage-lifecycle
Draft

Shared-layer lifecycle: accounting, eviction, recovery, materialization#458
chruffins wants to merge 19 commits into
hypeship/rootfs-compositionfrom
hypeship/storage-lifecycle

Conversation

@chruffins

@chruffins chruffins commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

summary

Capstone stage of the image-storage project — finishes migration, eviction, accounting, recovery, and observability on the shared-layer path.

  • Materialization in builds: after a pull, each manifest layer is materialized into images/layers/<digest>/ (best effort; the composed rootfs still comes from blobs, so an artifact failure is logged and only degrades sharing).
  • Reference-protected eviction: image deletion evicts only layer artifacts unreferenced by any remaining manifest model; a 10-minute grace period keeps fresh artifacts safe from racing in-flight builds. Startup also evicts orphans left by an unclean shutdown.
  • Interrupted-build recovery: stale .unpack-* / .install-* / .tag-stage-* temp directories are swept at startup (age-gated so live builds are untouched).
  • Layer-aware accounting: layer artifact bytes are bucketed with the OCI cache in TotalOCICacheBytes, so CurrentImageStorageBytes (ready rootfs + cache/layers) counts them exactly once and capacity admission sees the real footprint.
  • Observability: hypeman_images_layer_artifacts_evicted_total counter plus slog eviction summaries and per-layer materialization warnings.
  • Legacy flattened images remain readable and bootable; they carry no manifest model and no layer references, and are retired only through the existing delete/promotion paths.

validation

  • End-to-end lifecycle test: two images sharing a base layer materialize the base once; deleting one image keeps the shared layer; deleting the last reference evicts everything.
  • Accounting, grace-period, reference-protection, and stale-temp-dir tests.
  • go test ./lib/images ./lib/paths ./lib/builds ./lib/scopes green; cmd/api/api green 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).
  • CI is unavailable during this work; all validation is local.

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread lib/images/layer_gc.go
info, statErr := os.Stat(dirPath)
if statErr != nil || info.ModTime().After(cutoff) {
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 33c8ca3. Configure here.

Comment thread lib/images/manager.go Outdated
if result.CacheHit {
cacheStatus = "hit"
}
m.recordImageBuildPhase(ctx, ref.Digest(), "layer_materialization", time.Since(materializeStart), "success", cacheStatus)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 33c8ca3. Configure here.

@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from 41f4080 to 35de95e Compare August 26, 2026 18:22
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from 35de95e to f9a5fcc Compare August 26, 2026 18:46
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from d9caa0e to b0a27b4 Compare August 26, 2026 18:51
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from b0a27b4 to 37b8128 Compare August 26, 2026 18:52
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from 37b8128 to 13adf76 Compare August 26, 2026 18:53
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from 13adf76 to c0e6c32 Compare August 26, 2026 18:54
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from c0e6c32 to aac8b57 Compare August 26, 2026 18:55
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from aac8b57 to 7464627 Compare August 26, 2026 18:56
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from 4467fe9 to 588fff8 Compare August 26, 2026 18:58
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from ac3de8e to 165e42f Compare August 26, 2026 19:30
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from 165e42f to 3cac19c Compare August 26, 2026 19:38
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from 8ca57c1 to 419c294 Compare August 26, 2026 19:41
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch 2 times, most recently from c238f03 to 5051c46 Compare August 26, 2026 19:49
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from e6597bd to fe533bb Compare September 3, 2026 12:50
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from fe533bb to 962f4d2 Compare September 3, 2026 13:54
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from 962f4d2 to 2250812 Compare September 3, 2026 14:02
@chruffins
chruffins force-pushed the hypeship/storage-lifecycle branch from 2250812 to 3787792 Compare September 3, 2026 14:06
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.
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.

1 participant