imagecache

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 9, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

README

imagecache — node-local OCI image layer cache

internal/imagecache implements substrate's node-local OCI image cache: a content-addressed pool of unpacked image layers, stored once per node and shared by every actor on it, plus the machinery that composes an actor's rootfs from those layers as an overlayfs mount instead of extracting the image on every run.

It replaces the previous design (an in-memory LRU of flattened image tarballs in atelet, re-untarred into every bundle on every actor start/resume) and is the Phase 1 implementation of #463, addressing #120, #166, #228 and #437.

What it buys, concretely:

  • Actor start/resume composes the rootfs with one overlay mount (milliseconds) instead of a full image extraction (tens of seconds for GB-scale images). Restore timing breakdown logs show ate.actor.restore.duration.oci_unpack dropping from ~15–20 s to single-digit milliseconds on warm nodes.
  • Layers shared between images are downloaded and unpacked once per node, not once per image; actors sharing layers also share page cache.
  • The cache is on disk and survives atelet restarts and node reboots (the old in-memory cache was lost on every restart, and its unbounded heap retention could OOM atelet — #437).
  • Tag refs are cacheable: a tag costs one HEAD request to resolve to a manifest digest (the only safe cache key for mutable tags); digest refs hit the cache with zero network I/O.
  • Memory use during pulls is O(stream buffers), independent of image size (the old mutate.Extract path buffered entire flattened images — #120).

The privilege split

The design is shaped by an existing substrate boundary: atelet runs as plain root with every Linux capability dropped ("atelet does no mounts" — see manifests/ate-install/atelet.yaml), while the ateom worker pods are privileged and own all mounts on the node. The module is split accordingly:

Half Runs in Files Needs
Store: pull, parse, unpack, record atelet imagecache.go, unpack.go, spec.go (portable) nothing but file I/O
Consumer: finalize, mount, unmount ateom-gvisor / ateom-microvm bundle_linux.go (//go:build linux) CAP_MKNOD, CAP_SYS_ADMIN

The two halves communicate through the filesystem only: the shared cache directory (on the /var/lib/ateom-gvisor hostPath, so the same absolute paths resolve in every pod) and a small per-bundle spec file.

Because the consumer mounts the overlay in its own mount namespace — exactly where the workload resolves it (runsc's gofer for gVisor, virtiofsd for the micro-VM) — no Kubernetes mount-propagation configuration is needed anywhere.

On-disk layout

<cache-root>/                        default: /var/lib/ateom-gvisor/image-cache
  version                            layout version marker ("1")
  layers/sha256/<diffid-hex>/
      fs/                            the unpacked layer tree (an overlay lowerdir)
      whiteouts.json                 whiteout state recorded at unpack time
      finalized                      marker written by FinalizeLayer (consumer side)
      size                           byte count recorded at unpack (lazily
                                     backfilled for older layers), so sizing
                                     the pool never walks trees
  layers/sha256/.tmp-*/              in-flight unpack (swept at startup)
  layers/sha256/.rm-*/               retired by eviction, awaiting async
                                     removal (swept at startup)
  manifests/sha256/<digest-hex>.json image config + ordered diffID list; the
                                     file's mtime doubles as the image's
                                     last-use timestamp

A layer directory that exists is always complete: unpack streams into a .tmp-* sibling and moves it into place with a single atomic rename. Startup recovery (New) sweeps leftover .tmp-* and .rm-* dirs, verifies the layout version, and reclaims orphaned layers (see Garbage collection below). An "image" is nothing but a manifest record listing layer diffIDs in order — layers shared by N images exist once.

Pull path (atelet: Store.EnsureImage)

  1. Resolve the ref. Digest refs are parsed directly; tag refs cost one remote.Head. Localhost/loopback registries are rewritten for kind (--localhost-registry-replacement) and pulled over plain HTTP; gcr.io / pkg.dev registries get the configured GCP authenticator.
  2. Cache check: if the manifest record exists and every layer dir is present, return with no network I/O. Missing layers (only) are re-pulled.
  3. Pull by resolved digest: layers download in parallel (bounded at 4), each streamed download → decompress → untar directly into the pool. Concurrent pulls of the same image or layer are collapsed with singleflight, so simultaneous actor starts never duplicate work — and each completed layer lands individually, so an interrupted pull makes incremental progress across retries.
  4. Unpack (unpackLayer) is the repo's hardened untar: os.Root confinement (path traversal and symlink/hardlink escapes are refused), "later entry wins" within a layer, read-only-dir handling that works without CAP_DAC_OVERRIDE, and creation of parent directories that the layer tar omits (they may exist only in lower layers). Whiteout entries (.wh.*) are not written into the tree — overlayfs whiteouts are char devices atelet cannot create — they are recorded in whiteouts.json for the consumer to materialize.
  5. Record: the image config + diffID list is written under the requested digest (and the per-platform child digest for multi-arch refs).

prepareOCIDirectory in atelet then writes rootfs-overlay.json (OverlaySpec) into the bundle next to config.json, listing the layer directories bottom-first plus any ExtraDirs (in-rootfs bind-mount targets, e.g. the actor identity mount at /run/ate), and creates the empty bundle-local rootfs/, upper/, and work/ directories.

Compose path (ateom: SetupBundleRootfs)

Called immediately before runsc create/runsc restore (gVisor) and before staging the virtio-fs lower (micro-VM):

  1. FinalizeLayer for each referenced layer — materializes the recorded whiteouts as 0:0 char devices (mknod) and opaque dirs as trusted.overlay.opaque=y xattrs. Once per layer node-wide; idempotent and safe under concurrent ateom pods (EEXIST tolerated, marker written last). Paths from whiteouts.json are re-validated, so a crafted file cannot escape the layer tree.
  2. Mount an overlay at <bundle>/rootfs: lowerdir is the layer chain reversed into overlayfs's top-first order (duplicate layers — images can legitimately list the same diffID twice — are collapsed to the topmost occurrence, which overlayfs otherwise rejects with ELOOP), upperdir / workdir are the bundle-local dirs, holding this actor's private writes. The mount uses the new mount API (fsopen + one fsconfig lowerdir+ append per layer) rather than mount(2), whose single-page option-string cap the digest-derived layer paths would hit at ~34 layers. Minimum supported kernel: Linux 6.5 (lowerdir+); every current GKE channel ships ≥ 6.6 (Stable: COS 121 LTS).
  3. ExtraDirs are created through the mount (landing in the upper), again under os.Root confinement.
  4. Implicit-parent metadata repair. A layer tar routinely omits entries for parent directories that exist only in lower layers; unpack fabricates them (root:root 0755) and records them as implicitDirs in the layer metadata. Because overlayfs takes a merged directory's attributes from the top-most layer containing it, such a fabricated dir would shadow the real metadata a lower layer declared (/tmp losing its 1777 sticky bit, /root opening from 0700 to 0755). At compose time the consumer resolves each shadowed dir's true mode/ownership from the top-most non-implicit layer in this image's chain and applies it through the mount — the copy-up lands in the actor's private upper; the shared pool is never modified. Residual gaps: directory mtimes and xattrs are not repaired, and a dir implicit in every layer of the chain keeps the fabricated attrs.

A bundle without a spec file is left untouched (compatibility with bundles prepared by a pre-imagecache atelet). A zero-layer spec composes an empty rootfs with ExtraDirs and no mount.

Actor semantics are unchanged from the untar era: the upper is wiped by atelet's resetActorDirs between runs, so every run still starts from a bit-exact, pristine image rootfs — it just costs a mount instead of an extraction. The micro-VM path is nearly untouched: it bind-mounts the (now overlay-composed) bundle rootfs into virtiofsd's shared dir and the guest keeps building its own tmpfs upper, as before.

Teardown: UnmountAllUnder(bundleDir) lazily detaches every mount below an actor's bundle directory (via /proc/self/mountinfo) before atelet wipes it — called from the checkpoint cleanup path in ateom-gvisor and teardownActor in ateom-microvm.

Garbage collection

gc.go holds the eviction engine; atelet drives it as a periodic pass (--image-cache-gc-period, default 5m; 0 disables the periodic pass, but startup orphan recovery still runs at every atelet start). Each tick measures the cache volume with statfs and the pool's own size from the per-layer size files, then computes a byte target: free down to --image-cache-low-percent when volume usage reaches --image-cache-high-percent, and/or down to --image-cache-max-bytescapped at the pool's own size, because this cache is one tenant of a shared volume and an uncapped target would evict the whole cache trying to fix disk pressure it didn't cause. --image-cache-gc-dry-run computes and logs every decision while mutating nothing: the recommended way to soak the policy on a live fleet.

Two caveats on those numbers. The cap is the pool's total size, not the evictable subset (rooted and fresh layers can never be freed), so under sustained foreign pressure the target stays unreachable and every pass evicts everything unrooted and older than min-age — hit rate goes to zero until the pressure clears. The "could not reach target" WARNs are the signal; if this bites in practice, a retention floor (never evict below N bytes) is the intended extension. And usage is computed against the volume's raw capacity — kubelet's formula, so operator intuition transfers — which counts ext4's ~5% reserved blocks as used: eviction starts about five points below the configured percentage as df reports it.

One pass (Store.EvictUnused(ctx, targetBytes, dryRun)):

  1. Root set (Store.InUse): scan every bundle's rootfs-overlay.json under the actors dir (WithActorsDir). Overlay mounts live in the ateom pods' mount namespaces, so atelet cannot see them in its own /proc/mounts; the bundle specs are written by atelet itself before any ateom is asked to mount and removed only after unmount, so they are the authoritative "actively mounted" set. A spec roots its image digest, each layer dir it names, and its exact layer set — the last also roots the multi-arch twin record and records of digestless (pre-imageDigest) specs.
  2. Refcount layers across all image records, and list unrooted records older than min-age as eviction candidates, LRU-ordered by last use (the record's mtime — refreshed on every cache hit and on every completed layer of an in-flight pull).
  3. Evict candidates until ~targetBytes is freed: delete the record (after a freshness re-check under the same lock the cache-hit path holds), then retire each layer the removal left unreferenced. If any layer must be kept — still referenced by another record, rooted by a spec, younger than min-age, or its retirement failed — the record is restored byte-exact and the image simply is not evicted this pass: a layer is never left on disk without a record explaining it.

The pass reaches layers only through records: a layer is deleted exactly when its last referencing record goes, never by an independent scan of the pool.

Everything fails toward retention. If the image records or the bundle specs cannot be fully enumerated (an unreadable file or directory), the pass does nothing and logs at ERROR naming the culprit: refcounts and roots computed from partial data would retire layers that unread records still reference or running actors still mount. Dry-run mutates nothing at all — not even the lazy size-file backfill.

In-flight pulls need no separate protection. EnsureImage writes the image record before unpacking (the way Go allocates black during GC and containerd creates its ingest record before the bytes land), so every layer a pull produces is referenced — and kept fresh by a per-layer progress touch — from before it exists on disk. An interrupted pull's record is resumable progress, not garbage: the next pull of that digest re-fetches only the missing layers, and a pull that never resumes ages out through ordinary LRU.

Startup recovery. A layer no record references can only be crash debris (eviction interrupted between record-delete and layer-rename) or operator damage; New reclaims such orphans once, at startup (Store.RecoverOrphans), when no pull can be racing the scan — and skips the scan entirely, conservatively, if any record or bundle spec fails to read. There is no online whole-pool scan (ext4's split: bounded recovery at mount, fsck offline).

Deletion is two-phase. A layer is atomically renamed to .rm-* inside the layer's singleflight (one rename(2) — eviction can never stall a pull), then removed asynchronously; a crash in between leaves the dir for the startup sweep. This matters because the kernel offers no protection here: deleting a directory that is a live overlay lowerdir in another mount namespace succeeds silently, leaves the overlay's behavior undefined, and doesn't even free the space until the mount goes away.

Deleting the cache root by hand (while no actors are starting) remains safe — the store re-pulls whatever is missing.

This is Phase 2 of #463; the watermark loop, flags, and cache metrics complete it. Phase 3 adds the control-plane surface (reporting cached digests for scheduling affinity, and a PreloadImage API with expiring pins). The layer-materializer seam is also designed so a lazy-pull backend (eStargz/SOCI-style FUSE) can replace the untar backend later without restructuring.

Testing

  • Portable unit tests (run everywhere, including macOS): the unpack security suite (traversal, symlink/hardlink escape, whiteout capture, later-entry- wins, read-only dirs, missing parents), spec round-trips, overlay option assembly (including duplicate-layer dedup), mountinfo parsing, ref rewriting, options. End-to-end pull tests run against an in-memory registry (pkg/registry).
  • Linux-tagged tests (bundle_linux_test.go): unprivileged ones cover escape rejection and specless/zero-layer compose; root-gated ones execute the real mknod/xattr materialization and a full mount → write-isolation → unmount round trip (the write-isolation assertion — actor writes land in the bundle upper, never in the shared pool — is the key safety property). The root-gated ones self-skip via roottest.Require; CI (and hack/run-root-tests.sh locally) reruns the package under sudo so they execute.
  • tools/validate-image-cache batch-validates that arbitrary registry images can be pulled, parsed, and unpacked by the store half — useful for sweeping large image corpora before relying on them in production.

Documentation

Overview

Package imagecache implements the node-local OCI image cache: a content-addressed pool of unpacked image layers shared by every actor on the node, plus the per-bundle overlay spec that tells the ateom runtimes how to compose an actor rootfs from cached layers.

The work is split along the existing atelet/ateom privilege boundary:

  • atelet (plain root, all capabilities dropped) pulls layers and unpacks them into the pool (Store.EnsureImage), and writes a rootfs-overlay.json next to each bundle's config.json (WriteSpec). Whiteout entries are recorded in per-layer metadata rather than materialized, because overlayfs whiteouts are char devices (CAP_MKNOD) with trusted.* xattrs for opaque dirs (CAP_SYS_ADMIN).
  • ateom (privileged; it already owns every mount on the node) finalizes layers — materializing the recorded whiteout state, once per layer — and mounts the overlay rootfs (SetupBundleRootfs) just before `runsc create` / staging the micro-VM virtio-fs lower.

On-disk layout under the cache root (a directory on the BasePath hostPath, so the same absolute paths resolve in atelet and every ateom pod):

version                          layout version marker
layers/sha256/<diffid-hex>/
    fs/                          the unpacked layer tree (overlay lowerdir)
    whiteouts.json               whiteout state recorded at unpack time
    finalized                    marker written by FinalizeLayer (ateom)
manifests/sha256/<digest-hex>.json
                                 image config + ordered diffID list

Layers land in the pool via unpack-into-tempdir + atomic rename, so a layer directory that exists is always complete; startup recovery only has to sweep orphaned temp dirs.

Index

Constants

View Source
const OverlaySpecFileName = "rootfs-overlay.json"

OverlaySpecFileName is the file atelet writes into each container bundle, next to config.json, describing how to compose the bundle's rootfs from cached layers. Its absence means the bundle's rootfs is a plain directory (e.g. one prepared by a pre-imagecache atelet) and needs no mount.

Variables

View Source
var ErrIncompleteEnumeration = errors.New("image cache enumeration incomplete")

ErrIncompleteEnumeration marks a pass that did nothing because the image records or bundle specs could not be fully enumerated. Callers use errors.Is to tell "nothing was attempted" (repair the named file; not a shortfall) from per-item failures on a pass that ran.

Functions

func FinalizeLayer

func FinalizeLayer(layerDir string) error

FinalizeLayer materializes the whiteout state recorded at unpack time: 0:0 char devices for whiteouts and trusted.overlay.opaque=y on opaque dirs. This runs in ateom rather than atelet because mknod needs CAP_MKNOD and trusted.* xattrs need CAP_SYS_ADMIN, both of which atelet deliberately drops.

Idempotent and safe under concurrent callers (multiple ateom pods share the node's pool): EEXIST from mknod is success, setxattr is naturally idempotent, and the marker is written last.

func RemoveAllWritable

func RemoveAllWritable(path string) error

RemoveAllWritable removes path and everything under it, first making every directory owner-writable so its children can be unlinked. Unpacked image trees keep the image's (possibly read-only) directory modes, which atelet cannot remove as plain root without CAP_DAC_OVERRIDE — os.RemoveAll alone fails there with EACCES. atelet owns these files, so chmod needs no capability.

func SetupBundleRootfs

func SetupBundleRootfs(bundlePath string) error

SetupBundleRootfs composes the bundle's rootfs from cached layers per the bundle's overlay spec: it finalizes each layer (whiteout materialization, once per layer node-wide), mounts an overlay at <bundle>/rootfs with the cached layers as read-only lowerdirs and the bundle-local upper/ + work/ as the actor's private writable side, and creates the spec's ExtraDirs through the mount (so they land in the upper).

A bundle without an overlay spec is left untouched (its rootfs is a plain extracted directory). The mount lives in the calling process's mount namespace, which is exactly where the workload (runsc's gofer, virtiofsd) resolves it.

func UnmountAllUnder

func UnmountAllUnder(dir string) error

UnmountAllUnder lazily detaches every mount at or below dir in the calling process's mount namespace. It is the teardown counterpart of SetupBundleRootfs, keyed by directory rather than by container name so a single call cleans up all of an actor's bundle mounts. Missing mounts are not an error.

func WriteSpec

func WriteSpec(bundlePath string, spec *OverlaySpec) error

WriteSpec writes spec into the bundle at bundlePath.

The write is atomic (temp file + rename): concurrent readers — notably the cache GC's root-set scan — must never see a partial spec, which could parse with layers missing and leave them eligible for eviction while the actor is using them.

Types

type EvictStats

type EvictStats struct {
	// FreedBytes sums retired layers' recorded sizes, read from the size
	// files that rode along with the rename (walked read-only only when
	// absent) — consistent with CacheSize's accounting. An estimate; the
	// caller's next statfs self-corrects.
	FreedBytes int64
	// EvictedImages / EvictedLayers count deleted records and retired layer
	// dirs.
	EvictedImages, EvictedLayers int
	// Candidates is the number of LRU-ordered eviction candidates after all
	// listing-stage vetoes.
	Candidates int
	// RootedImages counts image records excluded because a bundle overlay
	// spec roots them (the "actively placed" protection).
	RootedImages int
	// SkippedRooted counts layers kept during the pass because a bundle
	// spec roots them (rooted images at listing time count into
	// RootedImages instead). SkippedFresh counts min-age vetoes, fired at
	// listing time or by the per-victim re-check.
	SkippedRooted, SkippedFresh int
	// OrphanLayers counts layers reclaimed by the startup scan
	// (RecoverOrphans) — always zero for periodic passes, which reach
	// layers only through records. Bytes are included in FreedBytes.
	OrphanLayers int
}

EvictStats reports what an eviction pass did (or, dry-run, would do).

type Image

type Image struct {
	// Digest is the manifest digest the caller's ref resolved to (for a
	// multi-arch ref, the index digest as requested, not the per-platform
	// child).
	Digest v1.Hash
	// Config is the OCI image config (entrypoint, env, ...).
	Config v1.Config
	// LayerDirs are the absolute cached layer directories, bottom-most layer
	// first. Each contains the unpacked tree under "fs/".
	LayerDirs []string
}

Image describes one cached, ready-to-compose image.

type ImageVolumeOverlay

type ImageVolumeOverlay struct {
	// Name is the ActorTemplate's name for the volume.
	Name string `json:"name"`
	// ImageDigest is the manifest digest the volume's ref resolved to, in the
	// same form and for the same reason as OverlaySpec.ImageDigest: the GC's
	// root-set scan protects an image by digest.
	ImageDigest string `json:"imageDigest,omitempty"`
	// Layers are the cached layer directories, bottom-most first.
	Layers []string `json:"layers"`
}

ImageVolumeOverlay is one image volume's contents.

type Option

type Option func(*Store)

Option configures a Store.

func WithActorsDir

func WithActorsDir(dir string) Option

WithActorsDir points the eviction root-set scan at the node's actors directory (the per-actor state dirs under ateompath.BasePath). Each <actorsDir>/<actorUID>/bundles/<container>/rootfs-overlay.json roots its image and layers against eviction. Empty disables the scan.

func WithAuthenticator

func WithAuthenticator(a authn.Authenticator) Option

WithAuthenticator attaches an authenticator used for gcr.io / pkg.dev registries. A nil authenticator is ignored.

func WithLocalhostRegistryReplacement

func WithLocalhostRegistryReplacement(replacement string) Option

WithLocalhostRegistryReplacement rewrites localhost/loopback registry refs to the given endpoint, mirroring the containerd mirror config used by kind local registries (https://kind.sigs.k8s.io/docs/user/local-registry/).

func WithMeter

func WithMeter(m metric.Meter) Option

WithMeter attaches the meter the store reports ate.imagecache.requests on. Without it the store records nothing, so a caller with no metrics pipeline needs no meter provider.

func WithMinAge

func WithMinAge(d time.Duration) Option

WithMinAge overrides the eviction minimum age (default 2m): layers and image records younger than this are never evicted.

func WithPlatform

func WithPlatform(p v1.Platform) Option

WithPlatform overrides the pull platform (default: linux/GOARCH).

func WithPullTimeout

func WithPullTimeout(d time.Duration) Option

WithPullTimeout overrides the per-pull timeout (default 10m).

type OverlaySpec

type OverlaySpec struct {
	Version int `json:"version"`
	// ImageDigest is the manifest digest the bundle's image ref resolved to
	// ("sha256:<hex>"). For a multi-arch ref this is the index digest; the
	// cache may hold a twin record under the platform-child digest, and GC
	// must treat the pair as one image. The GC's root-set scan uses it to
	// protect the image while the bundle exists; consumers ignore it.
	// Optional: older specs lack it.
	ImageDigest string `json:"imageDigest,omitempty"`
	// Layers are the cached layer directories (each holding its tree under
	// fs/), bottom-most layer first — the order the image manifest lists
	// them. Consumers reverse this into overlayfs's top-first lowerdir.
	Layers []string `json:"layers"`
	// ExtraDirs are absolute in-rootfs directories the consumer creates after
	// mounting (they land in the actor's private upper): bind-mount targets
	// that must exist for the runtime to attach them, e.g. the actor identity
	// mount.
	ExtraDirs []string `json:"extraDirs,omitempty"`
	// ImageVolumes are read-only image contents to expose beside the rootfs,
	// one per image-typed volume the container mounts. The consumer composes
	// each at the volume's bundle-local mount point, which the OCI spec binds
	// into the container.
	ImageVolumes []ImageVolumeOverlay `json:"imageVolumes,omitempty"`
}

OverlaySpec is the contract between atelet (which cannot mount) and the ateom runtimes (which mount the rootfs overlay just before running the workload). The overlay's mountpoint, upperdir, and workdir are always the bundle-local rootfs/, upper/, and work/ directories — derived from the bundle path by the consumer rather than trusted from the file.

func ReadSpec

func ReadSpec(bundlePath string) (*OverlaySpec, error)

ReadSpec reads the bundle's overlay spec. It returns (nil, nil) when the bundle has none.

type RootSet

type RootSet struct {
	// ImageDigests are rooted image digest strings ("sha256:<hex>").
	ImageDigests map[string]bool
	// LayerHexes are rooted layer diffID hexes (the layer dir base names),
	// covering bundle specs written before ImageDigest existed and
	// belt-and-suspenders for those written after.
	LayerHexes map[string]bool
	// LayerSets holds a signature per rooted bundle's *exact* layer set.
	// A record whose layer set matches one is rooted too: the multi-arch
	// twin and the digestless pre-ImageDigest spec, whose record would
	// otherwise be evicted while its layers survive — manufacturing an
	// orphan when the bundle goes. Exact match on purpose: rooting every
	// subset image (e.g. a running actor's base image) would quietly
	// weaken --image-cache-max-bytes under heavy layer sharing.
	LayerSets map[string]bool
}

RootSet is the set of images and layers that eviction must not touch, recomputed from disk at the start of every pass.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is atelet's handle to the on-disk layer pool. It is safe for concurrent use; concurrent pulls of the same image or layer are collapsed. The store assumes it is the only writer on the node (one atelet per node).

func New

func New(root string, opts ...Option) (*Store, error)

New opens (creating if needed) the layer pool rooted at root and runs startup recovery: verifying the layout version and sweeping temp dirs left by unpacks that were in flight when a previous atelet died.

func (*Store) CacheSize

func (s *Store) CacheSize() (int64, error)

CacheSize returns the sum of recorded sizes of every layer in the pool (the accounting behind --image-cache-max-bytes, distinct from volume usage).

func (*Store) EnsureImage

func (s *Store) EnsureImage(ctx context.Context, ref string) (_ *Image, err error)

EnsureImage makes ref's image available in the pool and returns its config and ordered layer directories. Digest refs hit the cache with no network I/O; tag refs cost one HEAD request to resolve the tag to a manifest digest (so tag refs are cacheable, and a moved tag is picked up on the next call).

func (*Store) EvictUnused

func (s *Store) EvictUnused(ctx context.Context, targetBytes int64, dryRun bool) (EvictStats, error)

EvictUnused evicts least-recently-used unprotected images — and the layers their removal leaves unreferenced — until ~targetBytes is freed or candidates run out. math.MaxInt64 means "free everything eligible"; targetBytes <= 0 evicts nothing. With dryRun nothing is deleted or renamed.

Per-item failures are aggregated into the error, not fatal: the pass continues and each item retries next pass. Passes are serialized. An incomplete record or bundle-spec enumeration skips the pass entirely — refcounts and roots from partial data would retire layers that unread records still reference or running actors still mount.

func (*Store) InUse

func (s *Store) InUse() (RootSet, error)

InUse scans the actors directory for bundle overlay specs and returns the images and layers referenced by actors placed on this node. Bundles exist exactly while an actor runs or transitions here (spec written before any mount, deleted after unmount), so the scan protects actively mounted images via the same authority that hands out mounts. Leftover bundles from crashed actors over-pin until wiped.

A non-nil error means the root set may be incomplete: an unreadable actors dir, bundles dir, or spec. Deleting callers must then do nothing — a missing root fails toward retiring a running actor's layers, and for a long-running actor the spec is the only protection (its record mtime can be arbitrarily old, so min-age would not save it). A missing dir or spec file is not an error: no actor was ever placed, the actor is torn down, or the bundle predates its spec write.

func (*Store) RecoverOrphans

func (s *Store) RecoverOrphans(ctx context.Context) (EvictStats, error)

RecoverOrphans reclaims layer dirs that no image record references. Called once from New, before the store serves — the one moment the scan is race-free: no pull is in flight, so a layer without a record is definitionally garbage. Orphans cannot arise in normal operation (pull writes the record first; eviction retires layers in the pass that drops their records): this reaps crash debris and operator damage, the accepted alternative to an fsck against a live pool every pass — mid-life debris leaks until the next restart, logged.

Skipped entirely (ERROR) when the record or bundle-spec enumeration is incomplete: refcounts from partial data make referenced layers look like garbage, and a missing spec root would sweep a mounted layer. Bundle-spec roots and min-age still veto.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL