2022-11-11 11:18:38 +00:00
package result
import (
2022-12-16 22:36:52 +00:00
"context"
2023-08-03 09:34:35 +00:00
"fmt"
"os"
2022-11-11 11:18:38 +00:00
"reflect"
2023-06-21 19:08:48 +00:00
_ "unsafe"
2022-11-11 11:18:38 +00:00
2023-08-03 09:34:35 +00:00
"codeberg.org/gruf/go-cache/v3/simple"
2022-12-16 22:36:52 +00:00
"codeberg.org/gruf/go-errors/v2"
2022-11-11 11:18:38 +00:00
)
2023-08-04 11:28:33 +00:00
var ErrUnsupportedZero = errors . New ( "" )
2023-08-03 09:34:35 +00:00
2022-11-13 13:02:07 +00:00
// Lookup represents a struct object lookup method in the cache.
type Lookup struct {
// Name is a period ('.') separated string
// of struct fields this Key encompasses.
Name string
// AllowZero indicates whether to accept and cache
// under zero value keys, otherwise ignore them.
AllowZero bool
2022-12-16 22:36:52 +00:00
2023-05-09 14:17:43 +00:00
// Multi allows specifying a key capable of storing
// multiple results. Note this only supports invalidate.
Multi bool
2022-11-13 13:02:07 +00:00
}
// Cache provides a means of caching value structures, along with
// the results of attempting to load them. An example usecase of this
// cache would be in wrapping a database, allowing caching of sql.ErrNoRows.
2023-08-03 09:34:35 +00:00
type Cache [ T any ] struct {
cache simple . Cache [ int64 , * result ] // underlying result cache
lookups structKeys // pre-determined struct lookups
invalid func ( T ) // store unwrapped invalidate callback.
ignore func ( error ) bool // determines cacheable errors
copy func ( T ) T // copies a Value type
next int64 // update key counter
2022-11-11 11:18:38 +00:00
}
2022-12-16 22:36:52 +00:00
// New returns a new initialized Cache, with given lookups, underlying value copy function and provided capacity.
2023-08-03 09:34:35 +00:00
func New [ T any ] ( lookups [ ] Lookup , copy func ( T ) T , cap int ) * Cache [ T ] {
var z T
2022-11-11 11:18:38 +00:00
// Determine generic type
t := reflect . TypeOf ( z )
// Iteratively deref pointer type
for t . Kind ( ) == reflect . Pointer {
t = t . Elem ( )
}
// Ensure that this is a struct type
if t . Kind ( ) != reflect . Struct {
panic ( "generic parameter type must be struct (or ptr to)" )
}
// Allocate new cache object
2023-08-03 09:34:35 +00:00
c := & Cache [ T ] { copy : copy }
2022-11-13 13:02:07 +00:00
c . lookups = make ( [ ] structKey , len ( lookups ) )
2022-11-11 11:18:38 +00:00
for i , lookup := range lookups {
2023-01-06 10:16:09 +00:00
// Create keyed field info for lookup
c . lookups [ i ] = newStructKey ( lookup , t )
2022-11-11 11:18:38 +00:00
}
// Create and initialize underlying cache
2023-08-03 09:34:35 +00:00
c . cache . Init ( 0 , cap )
2022-11-11 11:18:38 +00:00
c . SetEvictionCallback ( nil )
c . SetInvalidateCallback ( nil )
2022-12-16 22:36:52 +00:00
c . IgnoreErrors ( nil )
2022-11-11 11:18:38 +00:00
return c
}
// SetEvictionCallback sets the eviction callback to the provided hook.
2023-08-03 09:34:35 +00:00
func ( c * Cache [ T ] ) SetEvictionCallback ( hook func ( T ) ) {
2022-11-11 11:18:38 +00:00
if hook == nil {
// Ensure non-nil hook.
2023-08-03 09:34:35 +00:00
hook = func ( T ) { }
2022-11-11 11:18:38 +00:00
}
2023-08-03 09:34:35 +00:00
c . cache . SetEvictionCallback ( func ( pkey int64 , res * result ) {
2023-05-14 13:17:03 +00:00
c . cache . Lock ( )
for _ , key := range res . Keys {
2022-11-11 11:18:38 +00:00
// Delete key->pkey lookup
2022-12-16 22:36:52 +00:00
pkeys := key . info . pkeys
delete ( pkeys , key . key )
2022-11-11 11:18:38 +00:00
}
2023-05-14 13:17:03 +00:00
c . cache . Unlock ( )
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
if res . Error != nil {
2023-08-03 09:34:35 +00:00
// Skip value hooks
2022-11-11 11:18:38 +00:00
return
}
2023-08-03 09:34:35 +00:00
// Free result and call hook.
v := getResultValue [ T ] ( res )
putResult ( res )
hook ( v )
2022-11-11 11:18:38 +00:00
} )
}
// SetInvalidateCallback sets the invalidate callback to the provided hook.
2023-08-03 09:34:35 +00:00
func ( c * Cache [ T ] ) SetInvalidateCallback ( hook func ( T ) ) {
2022-11-11 11:18:38 +00:00
if hook == nil {
// Ensure non-nil hook.
2023-08-03 09:34:35 +00:00
hook = func ( T ) { }
2023-04-19 11:46:42 +00:00
} // store hook.
c . invalid = hook
2023-08-03 09:34:35 +00:00
c . cache . SetInvalidateCallback ( func ( pkey int64 , res * result ) {
2023-05-14 13:17:03 +00:00
c . cache . Lock ( )
for _ , key := range res . Keys {
2022-11-13 13:02:07 +00:00
// Delete key->pkey lookup
2022-12-16 22:36:52 +00:00
pkeys := key . info . pkeys
delete ( pkeys , key . key )
2022-11-11 11:18:38 +00:00
}
2023-05-14 13:17:03 +00:00
c . cache . Unlock ( )
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
if res . Error != nil {
2023-08-03 09:34:35 +00:00
// Skip value hooks
2022-11-11 11:18:38 +00:00
return
}
2023-08-03 09:34:35 +00:00
// Free result and call hook.
v := getResultValue [ T ] ( res )
putResult ( res )
hook ( v )
2022-11-11 11:18:38 +00:00
} )
}
2022-12-16 22:36:52 +00:00
// IgnoreErrors allows setting a function hook to determine which error types should / not be cached.
2023-08-03 09:34:35 +00:00
func ( c * Cache [ T ] ) IgnoreErrors ( ignore func ( error ) bool ) {
2022-12-16 22:36:52 +00:00
if ignore == nil {
ignore = func ( err error ) bool {
2023-04-29 16:44:20 +00:00
return errors . Comparable (
2022-12-16 22:36:52 +00:00
err ,
context . Canceled ,
context . DeadlineExceeded ,
)
}
}
c . cache . Lock ( )
c . ignore = ignore
c . cache . Unlock ( )
}
// 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.
2023-08-03 09:34:35 +00:00
func ( c * Cache [ T ] ) Load ( lookup string , load func ( ) ( T , error ) , keyParts ... any ) ( T , error ) {
2022-11-11 11:18:38 +00:00
var (
2023-08-03 09:34:35 +00:00
zero T
res * result
2022-11-11 11:18:38 +00:00
)
2022-11-13 13:02:07 +00:00
// Get lookup key info by name.
keyInfo := c . lookups . get ( lookup )
2023-05-09 14:17:43 +00:00
if ! keyInfo . unique {
panic ( "non-unique lookup does not support load: " + lookup )
}
2022-11-11 11:18:38 +00:00
// Generate cache key string.
2023-01-06 10:16:09 +00:00
ckey := keyInfo . genKey ( keyParts )
2022-11-11 11:18:38 +00:00
// Acquire cache lock
c . cache . Lock ( )
2022-12-16 22:36:52 +00:00
// Look for primary cache key
2023-05-09 14:17:43 +00:00
pkeys := keyInfo . pkeys [ ckey ]
2022-11-11 11:18:38 +00:00
2023-08-03 09:34:35 +00:00
if len ( pkeys ) > 0 {
2022-11-11 11:18:38 +00:00
// Fetch the result for primary key
2023-08-03 09:34:35 +00:00
entry , ok := c . cache . Cache . Get ( pkeys [ 0 ] )
2023-05-14 13:17:03 +00:00
if ok {
// Since the invalidation / eviction hooks acquire a mutex
// lock separately, and only at this point are the pkeys
// updated, there is a chance that a primary key may return
// no matching entry. Hence we have to check for it here.
2023-08-03 09:34:35 +00:00
res = entry . Value . ( * result )
2023-05-14 13:17:03 +00:00
}
2022-11-11 11:18:38 +00:00
}
// Done with lock
c . cache . Unlock ( )
2023-08-03 09:34:35 +00:00
if res == nil {
2022-12-16 22:36:52 +00:00
// Generate fresh result.
value , err := load ( )
if err != nil {
if c . ignore ( err ) {
// don't cache this error type
return zero , err
}
2023-08-03 09:34:35 +00:00
// Alloc result.
res = getResult ( )
2022-12-16 22:36:52 +00:00
// Store error result.
res . Error = err
2022-11-11 11:18:38 +00:00
// This load returned an error, only
// store this item under provided key.
2022-12-16 22:36:52 +00:00
res . Keys = [ ] cacheKey { {
info : keyInfo ,
key : ckey ,
2022-11-11 11:18:38 +00:00
} }
} else {
2023-08-03 09:34:35 +00:00
// Alloc result.
res = getResult ( )
2022-12-16 22:36:52 +00:00
// Store value result.
res . Value = value
2022-11-11 11:18:38 +00:00
// This was a successful load, generate keys.
res . Keys = c . lookups . generate ( res . Value )
}
2023-05-14 13:17:03 +00:00
var evict func ( )
2023-07-24 10:20:11 +00:00
// Lock cache.
2022-11-11 11:18:38 +00:00
c . cache . Lock ( )
2023-07-24 10:20:11 +00:00
2023-05-14 13:17:03 +00:00
defer func ( ) {
2023-07-24 10:20:11 +00:00
// Unlock cache.
2023-05-14 13:17:03 +00:00
c . cache . Unlock ( )
2023-07-24 10:20:11 +00:00
2023-05-14 13:17:03 +00:00
if evict != nil {
2023-07-24 10:20:11 +00:00
// Call evict.
2023-05-14 13:17:03 +00:00
evict ( )
}
} ( )
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// Store result in cache.
evict = c . store ( res )
2022-11-11 11:18:38 +00:00
}
2023-08-04 11:28:33 +00:00
// Catch and return cached error
if err := res . Error ; err != nil {
return zero , err
2022-11-11 11:18:38 +00:00
}
2023-08-04 11:28:33 +00:00
// Copy value from cached result.
v := c . copy ( getResultValue [ T ] ( res ) )
return v , nil
2022-11-11 11:18:38 +00:00
}
2022-11-13 13:02:07 +00:00
// Store will call the given store function, and on success store the value in the cache as a positive result.
2023-08-03 09:34:35 +00:00
func ( c * Cache [ T ] ) Store ( value T , store func ( ) error ) error {
2022-11-11 11:18:38 +00:00
// Attempt to store this value.
if err := store ( ) ; err != nil {
return err
}
// Prepare cached result.
2023-08-03 09:34:35 +00:00
result := getResult ( )
result . Keys = c . lookups . generate ( value )
result . Value = c . copy ( value )
result . Error = nil
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
var evict func ( )
2023-07-24 10:20:11 +00:00
// Lock cache.
2022-11-11 11:18:38 +00:00
c . cache . Lock ( )
2023-07-24 10:20:11 +00:00
2023-05-14 13:17:03 +00:00
defer func ( ) {
2023-07-24 10:20:11 +00:00
// Unlock cache.
2023-05-14 13:17:03 +00:00
c . cache . Unlock ( )
2023-07-24 10:20:11 +00:00
2023-05-14 13:17:03 +00:00
if evict != nil {
2023-07-24 10:20:11 +00:00
// Call evict.
2023-05-14 13:17:03 +00:00
evict ( )
}
2023-07-24 10:20:11 +00:00
// Call invalidate.
c . invalid ( value )
2023-05-14 13:17:03 +00:00
} ( )
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// Store result in cache.
evict = c . store ( result )
2022-11-11 11:18:38 +00:00
return nil
}
2022-11-13 13:02:07 +00:00
// Has checks the cache for a positive result under the given lookup and key parts.
2023-08-03 09:34:35 +00:00
func ( c * Cache [ T ] ) Has ( lookup string , keyParts ... any ) bool {
var res * result
2022-11-11 11:18:38 +00:00
2023-01-06 10:16:09 +00:00
// Get lookup key info by name.
keyInfo := c . lookups . get ( lookup )
2023-05-09 14:17:43 +00:00
if ! keyInfo . unique {
panic ( "non-unique lookup does not support has: " + lookup )
}
2022-11-11 11:18:38 +00:00
// Generate cache key string.
2023-01-06 10:16:09 +00:00
ckey := keyInfo . genKey ( keyParts )
2022-11-11 11:18:38 +00:00
// Acquire cache lock
c . cache . Lock ( )
2022-11-13 13:02:07 +00:00
// Look for primary key for cache key
2023-05-09 14:17:43 +00:00
pkeys := keyInfo . pkeys [ ckey ]
2022-11-11 11:18:38 +00:00
2023-08-03 09:34:35 +00:00
if len ( pkeys ) > 0 {
2022-11-11 11:18:38 +00:00
// Fetch the result for primary key
2023-08-03 09:34:35 +00:00
entry , ok := c . cache . Cache . Get ( pkeys [ 0 ] )
2023-05-14 13:17:03 +00:00
if ok {
// Since the invalidation / eviction hooks acquire a mutex
// lock separately, and only at this point are the pkeys
// updated, there is a chance that a primary key may return
// no matching entry. Hence we have to check for it here.
2023-08-03 09:34:35 +00:00
res = entry . Value . ( * result )
2023-05-14 13:17:03 +00:00
}
2022-11-11 11:18:38 +00:00
}
2023-08-04 11:28:33 +00:00
// Check for result AND non-error result.
ok := ( res != nil && res . Error == nil )
2022-11-11 11:18:38 +00:00
// Done with lock
c . cache . Unlock ( )
2023-08-04 11:28:33 +00:00
return ok
2022-11-11 11:18:38 +00:00
}
2022-11-13 13:02:07 +00:00
// Invalidate will invalidate any result from the cache found under given lookup and key parts.
2023-08-03 09:34:35 +00:00
func ( c * Cache [ T ] ) Invalidate ( lookup string , keyParts ... any ) {
2023-01-06 10:16:09 +00:00
// Get lookup key info by name.
keyInfo := c . lookups . get ( lookup )
2022-11-11 11:18:38 +00:00
// Generate cache key string.
2023-01-06 10:16:09 +00:00
ckey := keyInfo . genKey ( keyParts )
2022-11-11 11:18:38 +00:00
2022-11-13 13:02:07 +00:00
// Look for primary key for cache key
2022-11-11 11:18:38 +00:00
c . cache . Lock ( )
2023-05-09 14:17:43 +00:00
pkeys := keyInfo . pkeys [ ckey ]
2023-05-14 13:17:03 +00:00
delete ( keyInfo . pkeys , ckey )
2022-11-11 11:18:38 +00:00
c . cache . Unlock ( )
2023-05-14 13:17:03 +00:00
// Invalidate all primary keys.
c . cache . InvalidateAll ( pkeys ... )
2022-11-11 11:18:38 +00:00
}
2023-08-03 09:34:35 +00:00
// Clear empties the cache, calling the invalidate callback where necessary.
func ( c * Cache [ T ] ) Clear ( ) { c . Trim ( 100 ) }
// Trim ensures the cache stays within percentage of total capacity, truncating where necessary.
func ( c * Cache [ T ] ) Trim ( perc float64 ) { c . cache . Trim ( perc ) }
2022-11-11 11:18:38 +00:00
2022-12-16 22:36:52 +00:00
// store will cache this result under all of its required cache keys.
2023-08-03 09:34:35 +00:00
func ( c * Cache [ T ] ) store ( res * result ) ( evict func ( ) ) {
var toEvict [ ] * result
2022-11-11 11:18:38 +00:00
// Get primary key
2023-08-03 09:34:35 +00:00
res . PKey = c . next
2022-11-11 11:18:38 +00:00
c . next ++
2023-08-03 09:34:35 +00:00
if res . PKey > c . next {
2022-12-16 22:36:52 +00:00
panic ( "cache primary key overflow" )
}
2022-11-11 11:18:38 +00:00
for _ , key := range res . Keys {
2023-05-09 14:17:43 +00:00
// Look for cache primary keys.
pkeys := key . info . pkeys [ key . key ]
if key . info . unique && len ( pkeys ) > 0 {
for _ , conflict := range pkeys {
// Get the overlapping result with this key.
entry , _ := c . cache . Cache . Get ( conflict )
2023-08-03 09:34:35 +00:00
confRes := entry . Value . ( * result )
2023-05-09 14:17:43 +00:00
// From conflicting entry, drop this key, this
// will prevent eviction cleanup key confusion.
2023-08-03 09:34:35 +00:00
confRes . Keys . drop ( key . info . name )
2023-05-09 14:17:43 +00:00
2023-08-03 09:34:35 +00:00
if len ( res . Keys ) == 0 {
2023-05-09 14:17:43 +00:00
// We just over-wrote the only lookup key for
// this value, so we drop its primary key too.
c . cache . Cache . Delete ( conflict )
2023-08-03 09:34:35 +00:00
// Add finished result to evict queue.
toEvict = append ( toEvict , confRes )
2023-05-09 14:17:43 +00:00
}
}
2023-05-14 13:17:03 +00:00
// Drop existing.
2023-05-09 14:17:43 +00:00
pkeys = pkeys [ : 0 ]
}
// Store primary key lookup.
2023-08-03 09:34:35 +00:00
pkeys = append ( pkeys , res . PKey )
2023-05-09 14:17:43 +00:00
key . info . pkeys [ key . key ] = pkeys
2022-11-11 11:18:38 +00:00
}
2023-08-04 11:28:33 +00:00
// Acquire new cache entry.
entry := simple . GetEntry ( )
entry . Key = res . PKey
entry . Value = res
evictFn := func ( _ int64 , entry * simple . Entry ) {
// on evict during set, store evicted result.
toEvict = append ( toEvict , entry . Value . ( * result ) )
}
2023-08-03 09:34:35 +00:00
// Store main entry under primary key, catch evicted.
2023-08-04 11:28:33 +00:00
c . cache . Cache . SetWithHook ( res . PKey , entry , evictFn )
2023-05-14 13:17:03 +00:00
2023-08-03 09:34:35 +00:00
if len ( toEvict ) == 0 {
// none evicted.
return nil
2023-06-21 19:08:48 +00:00
}
2022-11-11 11:18:38 +00:00
2023-08-03 09:34:35 +00:00
return func ( ) {
2023-08-04 11:28:33 +00:00
for i := range toEvict {
// Rescope result.
res := toEvict [ i ]
2023-08-03 09:34:35 +00:00
// Call evict hook on each entry.
c . cache . Evict ( res . PKey , res )
}
}
2022-11-11 11:18:38 +00:00
}
2023-08-04 11:28:33 +00:00
type result struct {
// Result primary key
PKey int64
// keys accessible under
Keys cacheKeys
// cached value
Value any
// cached error
Error error
}
// getResultValue is a safe way of casting and fetching result value.
func getResultValue [ T any ] ( res * result ) T {
v , ok := res . Value . ( T )
if ! ok {
fmt . Fprintf ( os . Stderr , "!! BUG: unexpected value type in result: %T\n" , res . Value )
}
return v
}