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  }