package memory import ( "context" "math" "github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3/registry/storage/cache" "github.com/distribution/reference" "github.com/hashicorp/golang-lru/arc/v2" "github.com/opencontainers/go-digest" ) const ( // DefaultSize is the default cache size to use if no size is explicitly // configured. DefaultSize = 10000 // UnlimitedSize indicates the cache size should not be limited. UnlimitedSize = math.MaxInt ) type descriptorCacheKey struct { digest digest.Digest repo string } type inMemoryBlobDescriptorCacheProvider struct { lru *arc.ARCCache[descriptorCacheKey, distribution.Descriptor] } // NewInMemoryBlobDescriptorCacheProvider returns a new mapped-based cache for // storing blob descriptor data. func NewInMemoryBlobDescriptorCacheProvider(size int) cache.BlobDescriptorCacheProvider { if size <= 0 { size = math.MaxInt } lruCache, err := arc.NewARC[descriptorCacheKey, distribution.Descriptor](size) if err != nil { // NewARC can only fail if size is <= 0, so this unreachable panic(err) } return &inMemoryBlobDescriptorCacheProvider{ lru: lruCache, } } func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { if _, err := reference.ParseNormalizedNamed(repo); err != nil { return nil, err } return &repositoryScopedInMemoryBlobDescriptorCache{ repo: repo, parent: imbdcp, }, nil } func (imbdcp *inMemoryBlobDescriptorCacheProvider) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { if err := dgst.Validate(); err != nil { return distribution.Descriptor{}, err } key := descriptorCacheKey{ digest: dgst, } descriptor, ok := imbdcp.lru.Get(key) if ok { return descriptor, nil } return distribution.Descriptor{}, distribution.ErrBlobUnknown } func (imbdcp *inMemoryBlobDescriptorCacheProvider) Clear(ctx context.Context, dgst digest.Digest) error { key := descriptorCacheKey{ digest: dgst, } imbdcp.lru.Remove(key) return nil } func (imbdcp *inMemoryBlobDescriptorCacheProvider) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { _, err := imbdcp.Stat(ctx, dgst) if err == distribution.ErrBlobUnknown { if dgst.Algorithm() != desc.Digest.Algorithm() && dgst != desc.Digest { // if the digests differ, set the other canonical mapping if err := imbdcp.SetDescriptor(ctx, desc.Digest, desc); err != nil { return err } } if err := dgst.Validate(); err != nil { return err } if err := cache.ValidateDescriptor(desc); err != nil { return err } key := descriptorCacheKey{ digest: dgst, } imbdcp.lru.Add(key, desc) return nil } // we already know it, do nothing return err } // repositoryScopedInMemoryBlobDescriptorCache provides the request scoped // repository cache. Instances are not thread-safe but the delegated // operations are. type repositoryScopedInMemoryBlobDescriptorCache struct { repo string parent *inMemoryBlobDescriptorCacheProvider // allows lazy allocation of repo's map } func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { if err := dgst.Validate(); err != nil { return distribution.Descriptor{}, err } key := descriptorCacheKey{ digest: dgst, repo: rsimbdcp.repo, } descriptor, ok := rsimbdcp.parent.lru.Get(key) if ok { return descriptor, nil } return distribution.Descriptor{}, distribution.ErrBlobUnknown } func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) Clear(ctx context.Context, dgst digest.Digest) error { key := descriptorCacheKey{ digest: dgst, repo: rsimbdcp.repo, } rsimbdcp.parent.lru.Remove(key) return nil } func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { if err := dgst.Validate(); err != nil { return err } if err := cache.ValidateDescriptor(desc); err != nil { return err } key := descriptorCacheKey{ digest: dgst, repo: rsimbdcp.repo, } rsimbdcp.parent.lru.Add(key, desc) return rsimbdcp.parent.SetDescriptor(ctx, dgst, desc) }