github.com/openfga/openfga@v1.5.4-rc1/pkg/storage/storagewrappers/caching.go (about)

     1  package storagewrappers
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/karlseguin/ccache/v3"
     9  	openfgav1 "github.com/openfga/api/proto/openfga/v1"
    10  	"golang.org/x/sync/singleflight"
    11  
    12  	"github.com/openfga/openfga/pkg/storage"
    13  )
    14  
    15  const ttl = time.Hour * 168
    16  
    17  var _ storage.OpenFGADatastore = (*cachedOpenFGADatastore)(nil)
    18  
    19  type cachedOpenFGADatastore struct {
    20  	storage.OpenFGADatastore
    21  	lookupGroup singleflight.Group
    22  	cache       *ccache.Cache[*openfgav1.AuthorizationModel]
    23  }
    24  
    25  // NewCachedOpenFGADatastore returns a wrapper over a datastore that caches up to maxSize
    26  // [*openfgav1.AuthorizationModel] on every call to storage.ReadAuthorizationModel.
    27  // It caches with unlimited TTL because models are immutable. It uses LRU for eviction.
    28  func NewCachedOpenFGADatastore(inner storage.OpenFGADatastore, maxSize int) *cachedOpenFGADatastore {
    29  	return &cachedOpenFGADatastore{
    30  		OpenFGADatastore: inner,
    31  		cache:            ccache.New(ccache.Configure[*openfgav1.AuthorizationModel]().MaxSize(int64(maxSize))),
    32  	}
    33  }
    34  
    35  // ReadAuthorizationModel reads the model corresponding to store and model ID.
    36  func (c *cachedOpenFGADatastore) ReadAuthorizationModel(ctx context.Context, storeID, modelID string) (*openfgav1.AuthorizationModel, error) {
    37  	cacheKey := fmt.Sprintf("%s:%s", storeID, modelID)
    38  	cachedEntry := c.cache.Get(cacheKey)
    39  
    40  	if cachedEntry != nil {
    41  		return cachedEntry.Value(), nil
    42  	}
    43  
    44  	model, err := c.OpenFGADatastore.ReadAuthorizationModel(ctx, storeID, modelID)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  
    49  	c.cache.Set(cacheKey, model, ttl) // These are immutable, once created, there cannot be edits, therefore they can be cached without ttl.
    50  
    51  	return model, nil
    52  }
    53  
    54  // FindLatestAuthorizationModel see [storage.AuthorizationModelReadBackend].FindLatestAuthorizationModel.
    55  func (c *cachedOpenFGADatastore) FindLatestAuthorizationModel(ctx context.Context, storeID string) (*openfgav1.AuthorizationModel, error) {
    56  	v, err, _ := c.lookupGroup.Do(fmt.Sprintf("FindLatestAuthorizationModel:%s", storeID), func() (interface{}, error) {
    57  		return c.OpenFGADatastore.FindLatestAuthorizationModel(ctx, storeID)
    58  	})
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	return v.(*openfgav1.AuthorizationModel), nil
    63  }
    64  
    65  // Close closes the datastore and cleans up any residual resources.
    66  func (c *cachedOpenFGADatastore) Close() {
    67  	c.cache.Stop()
    68  	c.OpenFGADatastore.Close()
    69  }