package cache import ( "context" "fmt" "io" "forge.cadoles.com/arcad/edge/pkg/storage" "github.com/allegro/bigcache/v3" "github.com/hashicorp/golang-lru/v2/expirable" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" ) type BlobBucket struct { bucket storage.BlobBucket contentCache *bigcache.BigCache blobInfoCache *expirable.LRU[string, storage.BlobInfo] bucketCache *expirable.LRU[string, storage.BlobBucket] } // Close implements storage.BlobBucket. func (b *BlobBucket) Close() error { // Close only when bucket is evicted from cache return nil } // Delete implements storage.BlobBucket. func (b *BlobBucket) Delete(ctx context.Context, id storage.BlobID) error { defer b.clearCache(ctx, id) if err := b.bucket.Delete(ctx, id); err != nil { return errors.WithStack(err) } return nil } // Get implements storage.BlobBucket. func (b *BlobBucket) Get(ctx context.Context, id storage.BlobID) (storage.BlobInfo, error) { key := b.getCacheKey(id) if blobInfo, ok := b.blobInfoCache.Get(key); ok { logger.Debug( ctx, "found blob info in cache", logger.F("cacheKey", key), ) return blobInfo, nil } info, err := b.bucket.Get(ctx, id) if err != nil { if errors.Is(err, storage.ErrBucketClosed) { b.clearCache(ctx, id) b.bucketCache.Remove(b.Name()) } return nil, errors.WithStack(err) } b.blobInfoCache.Add(key, info) return info, nil } // List implements storage.BlobBucket. func (b *BlobBucket) List(ctx context.Context) ([]storage.BlobInfo, error) { infos, err := b.bucket.List(ctx) if err != nil { if errors.Is(err, storage.ErrBucketClosed) { b.bucketCache.Remove(b.Name()) } return nil, errors.WithStack(err) } for _, ifo := range infos { key := b.getCacheKey(ifo.ID()) b.blobInfoCache.Add(key, ifo) } return infos, nil } // Name implements storage.BlobBucket. func (b *BlobBucket) Name() string { return b.bucket.Name() } // NewReader implements storage.BlobBucket. func (b *BlobBucket) NewReader(ctx context.Context, id storage.BlobID) (io.ReadSeekCloser, error) { if cached, exist := b.inContentCache(id); exist { logger.Debug( ctx, "found blob content in cache", logger.F("cacheKey", b.getCacheKey(id)), logger.F("cacheStats", b.contentCache.Stats()), ) return cached, nil } reader, err := b.bucket.NewReader(ctx, id) if err != nil { if errors.Is(err, storage.ErrBucketClosed) { b.clearCache(ctx, id) b.bucketCache.Remove(b.Name()) } return nil, errors.WithStack(err) } return &readCacher{ reader: reader, cache: b.contentCache, key: b.getCacheKey(id), }, nil } func (b *BlobBucket) getCacheKey(id storage.BlobID) string { return fmt.Sprintf("%s-%s", b.Name(), id) } func (b *BlobBucket) inContentCache(id storage.BlobID) (io.ReadSeekCloser, bool) { key := b.getCacheKey(id) data, err := b.contentCache.Get(key) if err != nil { if errors.Is(err, bigcache.ErrEntryNotFound) { return nil, false } logger.Error(context.Background(), "could not retrieve cache value", logger.CapturedE(errors.WithStack(err))) return nil, false } return &cachedReader{data, 0}, true } func (b *BlobBucket) clearCache(ctx context.Context, id storage.BlobID) { key := b.getCacheKey(id) logger.Debug(ctx, "clearing cache", logger.F("cacheKey", key)) if err := b.contentCache.Delete(key); err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) { logger.Error(ctx, "could not clear cache", logger.CapturedE(errors.WithStack(err))) } b.blobInfoCache.Remove(key) } // NewWriter implements storage.BlobBucket. func (b *BlobBucket) NewWriter(ctx context.Context, id storage.BlobID) (io.WriteCloser, error) { defer b.clearCache(ctx, id) writer, err := b.bucket.NewWriter(ctx, id) if err != nil { if errors.Is(err, storage.ErrBucketClosed) { b.bucketCache.Remove(b.Name()) } return nil, errors.WithStack(err) } return writer, nil } // Size implements storage.BlobBucket. func (b *BlobBucket) Size(ctx context.Context) (int64, error) { size, err := b.bucket.Size(ctx) if err != nil { if errors.Is(err, storage.ErrBucketClosed) { b.bucketCache.Remove(b.Name()) } return 0, errors.WithStack(err) } return size, nil } var _ storage.BlobBucket = &BlobBucket{}