github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/proxy/schemacaching/standardcache.go (about)

     1  package schemacaching
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"sync"
     7  	"unsafe"
     8  
     9  	"github.com/authzed/spicedb/pkg/datastore/options"
    10  	"github.com/authzed/spicedb/pkg/genutil/mapz"
    11  
    12  	"golang.org/x/sync/singleflight"
    13  
    14  	internaldatastore "github.com/authzed/spicedb/internal/datastore"
    15  	"github.com/authzed/spicedb/pkg/cache"
    16  	"github.com/authzed/spicedb/pkg/datastore"
    17  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    18  )
    19  
    20  // definitionCachingProxy is a datastore proxy that caches schema (namespaces and caveat definitions)
    21  // via the supplied cache.
    22  type definitionCachingProxy struct {
    23  	datastore.Datastore
    24  	c         cache.Cache
    25  	readGroup singleflight.Group
    26  }
    27  
    28  func (p *definitionCachingProxy) Close() error {
    29  	p.c.Close()
    30  	return p.Datastore.Close()
    31  }
    32  
    33  func (p *definitionCachingProxy) SnapshotReader(rev datastore.Revision) datastore.Reader {
    34  	delegateReader := p.Datastore.SnapshotReader(rev)
    35  	return &definitionCachingReader{delegateReader, rev, p}
    36  }
    37  
    38  func (p *definitionCachingProxy) ReadWriteTx(
    39  	ctx context.Context,
    40  	f datastore.TxUserFunc,
    41  	opts ...options.RWTOptionsOption,
    42  ) (datastore.Revision, error) {
    43  	return p.Datastore.ReadWriteTx(ctx, func(ctx context.Context, delegateRWT datastore.ReadWriteTransaction) error {
    44  		rwt := &definitionCachingRWT{delegateRWT, &sync.Map{}}
    45  		return f(ctx, rwt)
    46  	}, opts...)
    47  }
    48  
    49  const (
    50  	namespaceCacheKeyPrefix = "n"
    51  	caveatCacheKeyPrefix    = "c"
    52  )
    53  
    54  type definitionCachingReader struct {
    55  	datastore.Reader
    56  	rev datastore.Revision
    57  	p   *definitionCachingProxy
    58  }
    59  
    60  func (r *definitionCachingReader) ReadNamespaceByName(
    61  	ctx context.Context,
    62  	name string,
    63  ) (*core.NamespaceDefinition, datastore.Revision, error) {
    64  	return readAndCache(ctx, r, namespaceCacheKeyPrefix, name,
    65  		func(ctx context.Context, name string) (*core.NamespaceDefinition, datastore.Revision, error) {
    66  			return r.Reader.ReadNamespaceByName(ctx, name)
    67  		},
    68  		estimatedNamespaceDefinitionSize)
    69  }
    70  
    71  func (r *definitionCachingReader) LookupNamespacesWithNames(
    72  	ctx context.Context,
    73  	nsNames []string,
    74  ) ([]datastore.RevisionedNamespace, error) {
    75  	return listAndCache(ctx, r, namespaceCacheKeyPrefix, nsNames,
    76  		func(ctx context.Context, names []string) ([]datastore.RevisionedNamespace, error) {
    77  			return r.Reader.LookupNamespacesWithNames(ctx, names)
    78  		},
    79  		estimatedNamespaceDefinitionSize)
    80  }
    81  
    82  func (r *definitionCachingReader) ReadCaveatByName(
    83  	ctx context.Context,
    84  	name string,
    85  ) (*core.CaveatDefinition, datastore.Revision, error) {
    86  	return readAndCache(ctx, r, caveatCacheKeyPrefix, name,
    87  		func(ctx context.Context, name string) (*core.CaveatDefinition, datastore.Revision, error) {
    88  			return r.Reader.ReadCaveatByName(ctx, name)
    89  		},
    90  		estimatedCaveatDefinitionSize)
    91  }
    92  
    93  func (r *definitionCachingReader) LookupCaveatsWithNames(
    94  	ctx context.Context,
    95  	caveatNames []string,
    96  ) ([]datastore.RevisionedCaveat, error) {
    97  	return listAndCache(ctx, r, caveatCacheKeyPrefix, caveatNames,
    98  		func(ctx context.Context, names []string) ([]datastore.RevisionedCaveat, error) {
    99  			return r.Reader.LookupCaveatsWithNames(ctx, names)
   100  		},
   101  		estimatedCaveatDefinitionSize)
   102  }
   103  
   104  func listAndCache[T schemaDefinition](
   105  	ctx context.Context,
   106  	r *definitionCachingReader,
   107  	prefix string,
   108  	names []string,
   109  	reader func(ctx context.Context, names []string) ([]datastore.RevisionedDefinition[T], error),
   110  	estimator func(sizeVT int) int64,
   111  ) ([]datastore.RevisionedDefinition[T], error) {
   112  	if len(names) == 0 {
   113  		return nil, nil
   114  	}
   115  
   116  	// Check the cache for each entry.
   117  	remainingToLoad := mapz.NewSet[string]()
   118  	remainingToLoad.Extend(names)
   119  
   120  	foundDefs := make([]datastore.RevisionedDefinition[T], 0, len(names))
   121  	for _, name := range names {
   122  		cacheRevisionKey := prefix + ":" + name + "@" + r.rev.String()
   123  		loadedRaw, found := r.p.c.Get(cacheRevisionKey)
   124  		if !found {
   125  			continue
   126  		}
   127  
   128  		remainingToLoad.Delete(name)
   129  		loaded := loadedRaw.(*cacheEntry)
   130  		foundDefs = append(foundDefs, datastore.RevisionedDefinition[T]{
   131  			Definition:          loaded.definition.(T),
   132  			LastWrittenRevision: loaded.updated,
   133  		})
   134  	}
   135  
   136  	if !remainingToLoad.IsEmpty() {
   137  		// Load and cache the remaining names.
   138  		loadedDefs, err := reader(ctx, remainingToLoad.AsSlice())
   139  		if err != nil {
   140  			return nil, err
   141  		}
   142  
   143  		for _, def := range loadedDefs {
   144  			foundDefs = append(foundDefs, def)
   145  
   146  			cacheRevisionKey := prefix + ":" + def.Definition.GetName() + "@" + r.rev.String()
   147  			estimatedDefinitionSize := estimator(def.Definition.SizeVT())
   148  			entry := &cacheEntry{def.Definition, def.LastWrittenRevision, estimatedDefinitionSize, err}
   149  			r.p.c.Set(cacheRevisionKey, entry, entry.Size())
   150  		}
   151  
   152  		// We have to call wait here or else Ristretto may not have the key(s)
   153  		// available to a subsequent caller.
   154  		r.p.c.Wait()
   155  	}
   156  
   157  	return foundDefs, nil
   158  }
   159  
   160  func readAndCache[T schemaDefinition](
   161  	ctx context.Context,
   162  	r *definitionCachingReader,
   163  	prefix string,
   164  	name string,
   165  	reader func(ctx context.Context, name string) (T, datastore.Revision, error),
   166  	estimator func(sizeVT int) int64,
   167  ) (T, datastore.Revision, error) {
   168  	// Check the cache.
   169  	cacheRevisionKey := prefix + ":" + name + "@" + r.rev.String()
   170  	loadedRaw, found := r.p.c.Get(cacheRevisionKey)
   171  	if !found {
   172  		// We couldn't use the cached entry, load one
   173  		var err error
   174  		loadedRaw, err, _ = r.p.readGroup.Do(cacheRevisionKey, func() (any, error) {
   175  			// sever the context so that another branch doesn't cancel the
   176  			// single-flighted read
   177  			loaded, updatedRev, err := reader(internaldatastore.SeparateContextWithTracing(ctx), name)
   178  			if err != nil && !errors.As(err, &datastore.ErrNamespaceNotFound{}) && !errors.As(err, &datastore.ErrCaveatNameNotFound{}) {
   179  				// Propagate this error to the caller
   180  				return nil, err
   181  			}
   182  
   183  			estimatedDefinitionSize := estimator(loaded.SizeVT())
   184  			entry := &cacheEntry{loaded, updatedRev, estimatedDefinitionSize, err}
   185  			r.p.c.Set(cacheRevisionKey, entry, entry.Size())
   186  
   187  			// We have to call wait here or else Ristretto may not have the key
   188  			// available to a subsequent caller.
   189  			r.p.c.Wait()
   190  			return entry, nil
   191  		})
   192  		if err != nil {
   193  			return *new(T), datastore.NoRevision, err
   194  		}
   195  	}
   196  
   197  	loaded := loadedRaw.(*cacheEntry)
   198  	return loaded.definition.(T), loaded.updated, loaded.notFound
   199  }
   200  
   201  type definitionCachingRWT struct {
   202  	datastore.ReadWriteTransaction
   203  	definitionCache *sync.Map
   204  }
   205  
   206  type definitionEntry struct {
   207  	loaded   schemaDefinition
   208  	updated  datastore.Revision
   209  	notFound error
   210  }
   211  
   212  func (rwt *definitionCachingRWT) ReadNamespaceByName(
   213  	ctx context.Context,
   214  	nsName string,
   215  ) (*core.NamespaceDefinition, datastore.Revision, error) {
   216  	return readAndCacheInTransaction(
   217  		ctx, rwt, "namespace", nsName, func(ctx context.Context, name string) (*core.NamespaceDefinition, datastore.Revision, error) {
   218  			return rwt.ReadWriteTransaction.ReadNamespaceByName(ctx, name)
   219  		})
   220  }
   221  
   222  func (rwt *definitionCachingRWT) ReadCaveatByName(
   223  	ctx context.Context,
   224  	nsName string,
   225  ) (*core.CaveatDefinition, datastore.Revision, error) {
   226  	return readAndCacheInTransaction(
   227  		ctx, rwt, "caveat", nsName, func(ctx context.Context, name string) (*core.CaveatDefinition, datastore.Revision, error) {
   228  			return rwt.ReadWriteTransaction.ReadCaveatByName(ctx, name)
   229  		})
   230  }
   231  
   232  func readAndCacheInTransaction[T schemaDefinition](
   233  	ctx context.Context,
   234  	rwt *definitionCachingRWT,
   235  	prefix string,
   236  	name string,
   237  	reader func(ctx context.Context, name string) (T, datastore.Revision, error),
   238  ) (T, datastore.Revision, error) {
   239  	key := prefix + ":" + name
   240  	untypedEntry, ok := rwt.definitionCache.Load(key)
   241  
   242  	var entry definitionEntry
   243  	if ok {
   244  		entry = untypedEntry.(definitionEntry)
   245  	} else {
   246  		loaded, updatedRev, err := reader(ctx, name)
   247  		if err != nil && !errors.As(err, &datastore.ErrNamespaceNotFound{}) && !errors.As(err, &datastore.ErrCaveatNameNotFound{}) {
   248  			// Propagate this error to the caller
   249  			return *new(T), datastore.NoRevision, err
   250  		}
   251  
   252  		entry = definitionEntry{loaded, updatedRev, err}
   253  		rwt.definitionCache.Store(key, entry)
   254  	}
   255  
   256  	return entry.loaded.(T), entry.updated, entry.notFound
   257  }
   258  
   259  func (rwt *definitionCachingRWT) WriteNamespaces(ctx context.Context, newConfigs ...*core.NamespaceDefinition) error {
   260  	if err := rwt.ReadWriteTransaction.WriteNamespaces(ctx, newConfigs...); err != nil {
   261  		return err
   262  	}
   263  
   264  	for _, nsDef := range newConfigs {
   265  		rwt.definitionCache.Delete("namespace:" + nsDef.Name)
   266  	}
   267  
   268  	return nil
   269  }
   270  
   271  func (rwt *definitionCachingRWT) WriteCaveats(ctx context.Context, newConfigs []*core.CaveatDefinition) error {
   272  	if err := rwt.ReadWriteTransaction.WriteCaveats(ctx, newConfigs); err != nil {
   273  		return err
   274  	}
   275  
   276  	for _, caveatDef := range newConfigs {
   277  		rwt.definitionCache.Delete("caveat:" + caveatDef.Name)
   278  	}
   279  
   280  	return nil
   281  }
   282  
   283  type cacheEntry struct {
   284  	definition              schemaDefinition
   285  	updated                 datastore.Revision
   286  	estimatedDefinitionSize int64
   287  	notFound                error
   288  }
   289  
   290  func (c *cacheEntry) Size() int64 {
   291  	return c.estimatedDefinitionSize + int64(unsafe.Sizeof(c))
   292  }
   293  
   294  var (
   295  	_ datastore.Datastore = &definitionCachingProxy{}
   296  	_ datastore.Reader    = &definitionCachingReader{}
   297  )
   298  
   299  func estimatedNamespaceDefinitionSize(sizevt int) int64 {
   300  	size := int64(sizevt * namespaceDefinitionSizeVTMultiplier)
   301  	if size < namespaceDefinitionMinimumSize {
   302  		return namespaceDefinitionMinimumSize
   303  	}
   304  	return size
   305  }
   306  
   307  func estimatedCaveatDefinitionSize(sizevt int) int64 {
   308  	size := int64(sizevt * caveatDefinitionSizeVTMultiplier)
   309  	if size < caveatDefinitionMinimumSize {
   310  		return caveatDefinitionMinimumSize
   311  	}
   312  	return size
   313  }