Skip to content

OCI cache is keyed only by tag — collisions, orphaned blobs, no pruning #135

Description

@trevor-vaughan

OCI cache is keyed only by tag — collisions, orphaned blobs, no pruning

Relates to #88. #88's "Open Design Questions → Cache pruning" already asks what
pruning UX to build (manual clean, TTL, size-cap/LRU). This issue is the concrete defect
that motivates it: the current tag-only cache key causes collisions and orphaned blobs,
so pruning is not just a nice-to-have — without it the cache is actively corrupted by
normal multi-source use. Scope here is the two gaps #88 does not spell out: the
collision-prone key, and the untracked blobs it leaves behind.

Where: the on-disk cache design as a whole
(internal/source/source.go:67,81, internal/registry/client.go:57-76 ParseTag).

What happens: artifacts are stored in one shared OCI layout, referenced by the tag
that ParseTag extracts (v1, latest, a digest, …). Two different repositories that
share a tag map to the same index.json key:

ghcr.io/org/controls:latest  ->  index key "latest"
ghcr.io/org/policies:latest  ->  index key "latest"   # overwrites the previous entry

Each load re-tags the key to its own manifest, so the two sources can never be cached at
the same time — every run overwrites and re-pulls the other. The overwritten manifest's
blobs stay on disk but become untracked, and there is no garbage collection, so the
cache grows unboundedly until cache clean wipes everything.

Important — this is NOT a wrong-result bug. Within a single run each source opens its
own store and oras.Copy re-resolves the root from the correct remote right before
bundle.Unpack, so the correct manifest is always unpacked. The damage is limited to
cache efficiency: thrashing on tag collisions, and unbounded growth from dangling blobs.

Suggestions:

  • Key cache entries by full repository + reference, not just the bare tag. This removes
    the collision at the source and is a prerequisite for any correct GC — a prune that
    keys on the bare tag cannot tell two colliding sources apart.
  • Add a prune/GC path that removes manifests/blobs no longer referenced by any
    configured source (or a TTL/LRU policy) — this is the concrete design input for Persistent OCI cache with XDG_CACHE_HOME support #88's
    open pruning question. Today the only reclamation is a full cache clean wipe.

Drafted with LLM assistance (Claude Opus 4.6).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions