mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-08-19 04:16:27 +00:00
90 lines
2.9 KiB
Go
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
|
|
}
|