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 }