codeberg.org/gruf/go-cache/v3@v3.5.7/result/cache.go (about) 1 package result 2 3 import ( 4 "context" 5 "reflect" 6 _ "unsafe" 7 8 "codeberg.org/gruf/go-cache/v3/simple" 9 "codeberg.org/gruf/go-errors/v2" 10 ) 11 12 // Lookup represents a struct object lookup method in the cache. 13 type Lookup struct { 14 // Name is a period ('.') separated string 15 // of struct fields this Key encompasses. 16 Name string 17 18 // AllowZero indicates whether to accept and cache 19 // under zero value keys, otherwise ignore them. 20 AllowZero bool 21 22 // Multi allows specifying a key capable of storing 23 // multiple results. Note this only supports invalidate. 24 Multi bool 25 } 26 27 // Cache provides a means of caching value structures, along with 28 // the results of attempting to load them. An example usecase of this 29 // cache would be in wrapping a database, allowing caching of sql.ErrNoRows. 30 type Cache[T any] struct { 31 cache simple.Cache[int64, *result] // underlying result cache 32 lookups structKeys // pre-determined struct lookups 33 invalid func(T) // store unwrapped invalidate callback. 34 ignore func(error) bool // determines cacheable errors 35 copy func(T) T // copies a Value type 36 next int64 // update key counter 37 } 38 39 // New returns a new initialized Cache, with given lookups, underlying value copy function and provided capacity. 40 func New[T any](lookups []Lookup, copy func(T) T, cap int) *Cache[T] { 41 var z T 42 43 // Determine generic type 44 t := reflect.TypeOf(z) 45 46 // Iteratively deref pointer type 47 for t.Kind() == reflect.Pointer { 48 t = t.Elem() 49 } 50 51 // Ensure that this is a struct type 52 if t.Kind() != reflect.Struct { 53 panic("generic parameter type must be struct (or ptr to)") 54 } 55 56 // Allocate new cache object 57 c := new(Cache[T]) 58 c.copy = copy // use copy fn. 59 c.lookups = make([]structKey, len(lookups)) 60 61 for i, lookup := range lookups { 62 // Create keyed field info for lookup 63 c.lookups[i] = newStructKey(lookup, t) 64 } 65 66 // Create and initialize underlying cache 67 c.cache.Init(0, cap) 68 c.SetEvictionCallback(nil) 69 c.SetInvalidateCallback(nil) 70 c.IgnoreErrors(nil) 71 return c 72 } 73 74 // SetEvictionCallback sets the eviction callback to the provided hook. 75 func (c *Cache[T]) SetEvictionCallback(hook func(T)) { 76 if hook == nil { 77 // Ensure non-nil hook. 78 hook = func(T) {} 79 } 80 c.cache.SetEvictionCallback(func(pkey int64, res *result) { 81 c.cache.Lock() 82 for _, key := range res.Keys { 83 // Delete key->pkey lookup 84 pkeys := key.info.pkeys 85 delete(pkeys, key.key) 86 } 87 c.cache.Unlock() 88 89 if res.Error != nil { 90 // Skip value hooks 91 putResult(res) 92 return 93 } 94 95 // Free result and call hook. 96 v := res.Value.(T) 97 putResult(res) 98 hook(v) 99 }) 100 } 101 102 // SetInvalidateCallback sets the invalidate callback to the provided hook. 103 func (c *Cache[T]) SetInvalidateCallback(hook func(T)) { 104 if hook == nil { 105 // Ensure non-nil hook. 106 hook = func(T) {} 107 } // store hook. 108 c.invalid = hook 109 c.cache.SetInvalidateCallback(func(pkey int64, res *result) { 110 c.cache.Lock() 111 for _, key := range res.Keys { 112 // Delete key->pkey lookup 113 pkeys := key.info.pkeys 114 delete(pkeys, key.key) 115 } 116 c.cache.Unlock() 117 118 if res.Error != nil { 119 // Skip value hooks 120 putResult(res) 121 return 122 } 123 124 // Free result and call hook. 125 v := res.Value.(T) 126 putResult(res) 127 hook(v) 128 }) 129 } 130 131 // IgnoreErrors allows setting a function hook to determine which error types should / not be cached. 132 func (c *Cache[T]) IgnoreErrors(ignore func(error) bool) { 133 if ignore == nil { 134 ignore = func(err error) bool { 135 return errors.Is(err, context.Canceled) || 136 errors.Is(err, context.DeadlineExceeded) 137 } 138 } 139 c.cache.Lock() 140 c.ignore = ignore 141 c.cache.Unlock() 142 } 143 144 // Load will attempt to load an existing result from the cacche for the given lookup and key parts, else calling the provided load function and caching the result. 145 func (c *Cache[T]) Load(lookup string, load func() (T, error), keyParts ...any) (T, error) { 146 info := c.lookups.get(lookup) 147 key := info.genKey(keyParts) 148 return c.load(info, key, load) 149 } 150 151 // Has checks the cache for a positive result under the given lookup and key parts. 152 func (c *Cache[T]) Has(lookup string, keyParts ...any) bool { 153 info := c.lookups.get(lookup) 154 key := info.genKey(keyParts) 155 return c.has(info, key) 156 } 157 158 // Store will call the given store function, and on success store the value in the cache as a positive result. 159 func (c *Cache[T]) Store(value T, store func() error) error { 160 // Attempt to store this value. 161 if err := store(); err != nil { 162 return err 163 } 164 165 // Prepare cached result. 166 result := getResult() 167 result.Keys = c.lookups.generate(value) 168 result.Value = c.copy(value) 169 result.Error = nil 170 171 var evict func() 172 173 // Lock cache. 174 c.cache.Lock() 175 176 defer func() { 177 // Unlock cache. 178 c.cache.Unlock() 179 180 if evict != nil { 181 // Call evict. 182 evict() 183 } 184 185 // Call invalidate. 186 c.invalid(value) 187 }() 188 189 // Store result in cache. 190 evict = c.store(result) 191 192 return nil 193 } 194 195 // Invalidate will invalidate any result from the cache found under given lookup and key parts. 196 func (c *Cache[T]) Invalidate(lookup string, keyParts ...any) { 197 info := c.lookups.get(lookup) 198 key := info.genKey(keyParts) 199 c.invalidate(info, key) 200 } 201 202 // Clear empties the cache, calling the invalidate callback where necessary. 203 func (c *Cache[T]) Clear() { c.Trim(100) } 204 205 // Trim ensures the cache stays within percentage of total capacity, truncating where necessary. 206 func (c *Cache[T]) Trim(perc float64) { c.cache.Trim(perc) } 207 208 func (c *Cache[T]) load(lookup *structKey, key string, load func() (T, error)) (T, error) { 209 if !lookup.unique { // ensure this lookup only returns 1 result 210 panic("non-unique lookup does not support load: " + lookup.name) 211 } 212 213 var ( 214 zero T 215 res *result 216 ) 217 218 // Acquire cache lock 219 c.cache.Lock() 220 221 // Look for primary key for cache key (only accept len=1) 222 if pkeys := lookup.pkeys[key]; len(pkeys) == 1 { 223 // Fetch the result for primary key 224 entry, ok := c.cache.Cache.Get(pkeys[0]) 225 226 if ok { 227 // Since the invalidation / eviction hooks acquire a mutex 228 // lock separately, and only at this point are the pkeys 229 // updated, there is a chance that a primary key may return 230 // no matching entry. Hence we have to check for it here. 231 res = entry.Value.(*result) 232 } 233 } 234 235 // Done with lock 236 c.cache.Unlock() 237 238 if res == nil { 239 // Generate fresh result. 240 value, err := load() 241 242 if err != nil { 243 if c.ignore(err) { 244 // don't cache this error type 245 return zero, err 246 } 247 248 // Alloc result. 249 res = getResult() 250 251 // Store error result. 252 res.Error = err 253 254 // This load returned an error, only 255 // store this item under provided key. 256 res.Keys = []cacheKey{{ 257 info: lookup, 258 key: key, 259 }} 260 } else { 261 // Alloc result. 262 res = getResult() 263 264 // Store value result. 265 res.Value = value 266 267 // This was a successful load, generate keys. 268 res.Keys = c.lookups.generate(res.Value) 269 } 270 271 var evict func() 272 273 // Lock cache. 274 c.cache.Lock() 275 276 defer func() { 277 // Unlock cache. 278 c.cache.Unlock() 279 280 if evict != nil { 281 // Call evict. 282 evict() 283 } 284 }() 285 286 // Store result in cache. 287 evict = c.store(res) 288 } 289 290 // Catch and return cached error 291 if err := res.Error; err != nil { 292 return zero, err 293 } 294 295 // Copy value from cached result. 296 v := c.copy(res.Value.(T)) 297 298 return v, nil 299 } 300 301 func (c *Cache[T]) has(lookup *structKey, key string) bool { 302 var res *result 303 304 // Acquire cache lock 305 c.cache.Lock() 306 307 // Look for primary key for cache key (only accept len=1) 308 if pkeys := lookup.pkeys[key]; len(pkeys) == 1 { 309 // Fetch the result for primary key 310 entry, ok := c.cache.Cache.Get(pkeys[0]) 311 312 if ok { 313 // Since the invalidation / eviction hooks acquire a mutex 314 // lock separately, and only at this point are the pkeys 315 // updated, there is a chance that a primary key may return 316 // no matching entry. Hence we have to check for it here. 317 res = entry.Value.(*result) 318 } 319 } 320 321 // Check for result AND non-error result. 322 ok := (res != nil && res.Error == nil) 323 324 // Done with lock 325 c.cache.Unlock() 326 327 return ok 328 } 329 330 func (c *Cache[T]) store(res *result) (evict func()) { 331 var toEvict []*result 332 333 // Get primary key 334 res.PKey = c.next 335 c.next++ 336 if res.PKey > c.next { 337 panic("cache primary key overflow") 338 } 339 340 for _, key := range res.Keys { 341 // Look for cache primary keys. 342 pkeys := key.info.pkeys[key.key] 343 344 if key.info.unique && len(pkeys) > 0 { 345 for _, conflict := range pkeys { 346 // Get the overlapping result with this key. 347 entry, ok := c.cache.Cache.Get(conflict) 348 349 if !ok { 350 // Since the invalidation / eviction hooks acquire a mutex 351 // lock separately, and only at this point are the pkeys 352 // updated, there is a chance that a primary key may return 353 // no matching entry. Hence we have to check for it here. 354 continue 355 } 356 357 // From conflicting entry, drop this key, this 358 // will prevent eviction cleanup key confusion. 359 confRes := entry.Value.(*result) 360 confRes.Keys.drop(key.info.name) 361 362 if len(res.Keys) == 0 { 363 // We just over-wrote the only lookup key for 364 // this value, so we drop its primary key too. 365 _ = c.cache.Cache.Delete(conflict) 366 367 // Add finished result to evict queue. 368 toEvict = append(toEvict, confRes) 369 } 370 } 371 372 // Drop existing. 373 pkeys = pkeys[:0] 374 } 375 376 // Store primary key lookup. 377 pkeys = append(pkeys, res.PKey) 378 key.info.pkeys[key.key] = pkeys 379 } 380 381 // Acquire new cache entry. 382 entry := simple.GetEntry() 383 entry.Key = res.PKey 384 entry.Value = res 385 386 evictFn := func(_ int64, entry *simple.Entry) { 387 // on evict during set, store evicted result. 388 toEvict = append(toEvict, entry.Value.(*result)) 389 } 390 391 // Store main entry under primary key, catch evicted. 392 c.cache.Cache.SetWithHook(res.PKey, entry, evictFn) 393 394 if len(toEvict) == 0 { 395 // none evicted. 396 return nil 397 } 398 399 return func() { 400 for i := range toEvict { 401 // Rescope result. 402 res := toEvict[i] 403 404 // Call evict hook on each entry. 405 c.cache.Evict(res.PKey, res) 406 } 407 } 408 } 409 410 func (c *Cache[T]) invalidate(lookup *structKey, key string) { 411 // Look for primary key for cache key 412 c.cache.Lock() 413 pkeys := lookup.pkeys[key] 414 delete(lookup.pkeys, key) 415 c.cache.Unlock() 416 417 // Invalidate all primary keys. 418 c.cache.InvalidateAll(pkeys...) 419 } 420 421 type result struct { 422 // Result primary key 423 PKey int64 424 425 // keys accessible under 426 Keys cacheKeys 427 428 // cached value 429 Value any 430 431 // cached error 432 Error error 433 }