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

    enhancementNew feature or requestllm_assistedIssue triaged or authored with LLM assistance

    Type

    No type

    Fields

    Priority

    Medium

    Effort

    None yet

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions