Files
hauler/pkg/artifacts/file/cache.go
T
2026-08-05 16:34:41 -04:00

90 lines
2.9 KiB
Go

package file
import (
"context"
"sync"
gv1 "github.com/google/go-containerregistry/pkg/v1"
"golang.org/x/sync/singleflight"
)
// LayerCache lets multiple File instances that share the same source Path
// (e.g. two manifest entries pointing at the same URL, one plain and one
// with a name override) skip redundant fetches of the underlying content.
// Scope it to the caller (typically one sync run) -- construct with
// NewLayerCache and attach via WithLayerCacheContext.
//
// Sharing the cached gv1.Layer is safe despite differing name overrides:
// compute() always rebuilds its own descriptor and overwrites the Title
// with its own client.Name(Path) afterward, so only the fetch and
// digest/diffID hashing (getter.Client.LayerFrom) is actually shared.
type LayerCache struct {
group singleflight.Group
mu sync.RWMutex
cache map[string]gv1.Layer
}
// NewLayerCache returns an empty LayerCache.
func NewLayerCache() *LayerCache {
return &LayerCache{cache: make(map[string]gv1.Layer)}
}
// getOrFetch returns the cached layer for path if present; otherwise it
// calls fetch, sharing the in-flight call across concurrent callers for the
// same path (singleflight) and caching only on success. A failed fetch is
// deliberately not cached: singleflight forgets the flight once it returns,
// so the next call -- a new job or a retry.Operation retry -- starts fresh
// rather than replaying a poisoned error.
func (c *LayerCache) getOrFetch(path string, fetch func() (gv1.Layer, error)) (gv1.Layer, error) {
c.mu.RLock()
l, ok := c.cache[path]
c.mu.RUnlock()
if ok {
return l, nil
}
v, err, _ := c.group.Do(path, func() (interface{}, error) {
// Re-check under the flight: another flight for this path may have
// completed and cached a result while this goroutine queued behind it.
c.mu.RLock()
l, ok := c.cache[path]
c.mu.RUnlock()
if ok {
return l, nil
}
l, err := fetch()
if err != nil {
return nil, err
}
c.mu.Lock()
c.cache[path] = l
c.mu.Unlock()
return l, nil
})
if err != nil {
return nil, err
}
return v.(gv1.Layer), nil
}
// layerCacheKey is the context key WithLayerCacheContext/layerCacheFromContext use.
type layerCacheKey struct{}
// WithLayerCacheContext attaches c to ctx so compute() picks it up via
// layerCacheFromContext -- the same ctx-attached side-channel idiom
// pkg/store uses for ImageStats (store.WithImageStats).
func WithLayerCacheContext(ctx context.Context, c *LayerCache) context.Context {
return context.WithValue(ctx, layerCacheKey{}, c)
}
// layerCacheFromContext returns the *LayerCache attached via
// WithLayerCacheContext, or nil if none was attached -- the common case for
// callers not coordinating a batch of File instances that might share a
// Path (a single `store add file` call, existing unit tests, etc).
func layerCacheFromContext(ctx context.Context) *LayerCache {
c, _ := ctx.Value(layerCacheKey{}).(*LayerCache)
return c
}