diff options
author | 2024-01-19 12:57:29 +0000 | |
---|---|---|
committer | 2024-01-19 12:57:29 +0000 | |
commit | 7ec1e1332e7d04e74451acef18b41f389722b698 (patch) | |
tree | 9c69eca7fc664ab5564279a2e065dfd5c2ddd17b /vendor/codeberg.org | |
parent | [chore] chore rationalise http return codes for activitypub handlers (#2540) (diff) | |
download | gotosocial-7ec1e1332e7d04e74451acef18b41f389722b698.tar.xz |
[performance] overhaul struct (+ result) caching library for simplicity, performance and multiple-result lookups (#2535)
* rewrite cache library as codeberg.org/gruf/go-structr, implement in gotosocial
* use actual go-structr release version (not just commit hash)
* revert go toolchain changes (damn you go for auto changing this)
* fix go mod woes
* ensure %w is used in calls to errs.Appendf()
* fix error checking
* fix possible panic
* remove unnecessary start/stop functions, move to main Cache{} struct, add note regarding which caches require start/stop
* fix copy-paste artifact... :innocent:
* fix all comment copy-paste artifacts
* remove dropID() function, now we can just use slices.DeleteFunc()
* use util.Deduplicate() instead of collate(), move collate to util
* move orderByIDs() to util package and "generify"
* add a util.DeleteIf() function, use this to delete entries on failed population
* use slices.DeleteFunc() instead of util.DeleteIf() (i had the logic mixed up in my head somehow lol)
* add note about how collate differs from deduplicate
Diffstat (limited to 'vendor/codeberg.org')
-rw-r--r-- | vendor/codeberg.org/gruf/go-cache/v3/result/cache.go | 433 | ||||
-rw-r--r-- | vendor/codeberg.org/gruf/go-cache/v3/result/key.go | 282 | ||||
-rw-r--r-- | vendor/codeberg.org/gruf/go-cache/v3/result/pool.go | 25 | ||||
-rw-r--r-- | vendor/codeberg.org/gruf/go-structr/LICENSE | 9 | ||||
-rw-r--r-- | vendor/codeberg.org/gruf/go-structr/README.md | 5 | ||||
-rw-r--r-- | vendor/codeberg.org/gruf/go-structr/cache.go | 731 | ||||
-rw-r--r-- | vendor/codeberg.org/gruf/go-structr/debug.go | 41 | ||||
-rw-r--r-- | vendor/codeberg.org/gruf/go-structr/index.go | 213 | ||||
-rw-r--r-- | vendor/codeberg.org/gruf/go-structr/key.go | 204 | ||||
-rw-r--r-- | vendor/codeberg.org/gruf/go-structr/list.go | 130 | ||||
-rw-r--r-- | vendor/codeberg.org/gruf/go-structr/result.go | 76 | ||||
-rw-r--r-- | vendor/codeberg.org/gruf/go-structr/util.go | 118 |
12 files changed, 1527 insertions, 740 deletions
diff --git a/vendor/codeberg.org/gruf/go-cache/v3/result/cache.go b/vendor/codeberg.org/gruf/go-cache/v3/result/cache.go deleted file mode 100644 index eded6e7f4..000000000 --- a/vendor/codeberg.org/gruf/go-cache/v3/result/cache.go +++ /dev/null @@ -1,433 +0,0 @@ -package result - -import ( - "context" - "reflect" - _ "unsafe" - - "codeberg.org/gruf/go-cache/v3/simple" - "codeberg.org/gruf/go-errors/v2" -) - -// 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 - - // Multi allows specifying a key capable of storing - // multiple results. Note this only supports invalidate. - Multi bool -} - -// 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. -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 -} - -// New returns a new initialized Cache, with given lookups, underlying value copy function and provided capacity. -func New[T any](lookups []Lookup, copy func(T) T, cap int) *Cache[T] { - var z T - - // 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 - c := new(Cache[T]) - c.copy = copy // use copy fn. - c.lookups = make([]structKey, len(lookups)) - - for i, lookup := range lookups { - // Create keyed field info for lookup - c.lookups[i] = newStructKey(lookup, t) - } - - // Create and initialize underlying cache - c.cache.Init(0, cap) - c.SetEvictionCallback(nil) - c.SetInvalidateCallback(nil) - c.IgnoreErrors(nil) - return c -} - -// SetEvictionCallback sets the eviction callback to the provided hook. -func (c *Cache[T]) SetEvictionCallback(hook func(T)) { - if hook == nil { - // Ensure non-nil hook. - hook = func(T) {} - } - c.cache.SetEvictionCallback(func(pkey int64, res *result) { - c.cache.Lock() - for _, key := range res.Keys { - // Delete key->pkey lookup - pkeys := key.info.pkeys - delete(pkeys, key.key) - } - c.cache.Unlock() - - if res.Error != nil { - // Skip value hooks - putResult(res) - return - } - - // Free result and call hook. - v := res.Value.(T) - putResult(res) - hook(v) - }) -} - -// SetInvalidateCallback sets the invalidate callback to the provided hook. -func (c *Cache[T]) SetInvalidateCallback(hook func(T)) { - if hook == nil { - // Ensure non-nil hook. - hook = func(T) {} - } // store hook. - c.invalid = hook - c.cache.SetInvalidateCallback(func(pkey int64, res *result) { - c.cache.Lock() - for _, key := range res.Keys { - // Delete key->pkey lookup - pkeys := key.info.pkeys - delete(pkeys, key.key) - } - c.cache.Unlock() - - if res.Error != nil { - // Skip value hooks - putResult(res) - return - } - - // Free result and call hook. - v := res.Value.(T) - putResult(res) - hook(v) - }) -} - -// IgnoreErrors allows setting a function hook to determine which error types should / not be cached. -func (c *Cache[T]) IgnoreErrors(ignore func(error) bool) { - if ignore == nil { - ignore = func(err error) bool { - return errors.Is(err, context.Canceled) || - errors.Is(err, 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. -func (c *Cache[T]) Load(lookup string, load func() (T, error), keyParts ...any) (T, error) { - info := c.lookups.get(lookup) - key := info.genKey(keyParts) - return c.load(info, key, load) -} - -// Has checks the cache for a positive result under the given lookup and key parts. -func (c *Cache[T]) Has(lookup string, keyParts ...any) bool { - info := c.lookups.get(lookup) - key := info.genKey(keyParts) - return c.has(info, key) -} - -// Store will call the given store function, and on success store the value in the cache as a positive result. -func (c *Cache[T]) Store(value T, store func() error) error { - // Attempt to store this value. - if err := store(); err != nil { - return err - } - - // Prepare cached result. - result := getResult() - result.Keys = c.lookups.generate(value) - result.Value = c.copy(value) - result.Error = nil - - var evict func() - - // Lock cache. - c.cache.Lock() - - defer func() { - // Unlock cache. - c.cache.Unlock() - - if evict != nil { - // Call evict. - evict() - } - - // Call invalidate. - c.invalid(value) - }() - - // Store result in cache. - evict = c.store(result) - - return nil -} - -// Invalidate will invalidate any result from the cache found under given lookup and key parts. -func (c *Cache[T]) Invalidate(lookup string, keyParts ...any) { - info := c.lookups.get(lookup) - key := info.genKey(keyParts) - c.invalidate(info, key) -} - -// 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) } - -func (c *Cache[T]) load(lookup *structKey, key string, load func() (T, error)) (T, error) { - if !lookup.unique { // ensure this lookup only returns 1 result - panic("non-unique lookup does not support load: " + lookup.name) - } - - var ( - zero T - res *result - ) - - // Acquire cache lock - c.cache.Lock() - - // Look for primary key for cache key (only accept len=1) - if pkeys := lookup.pkeys[key]; len(pkeys) == 1 { - // Fetch the result for primary key - entry, ok := c.cache.Cache.Get(pkeys[0]) - - 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. - res = entry.Value.(*result) - } - } - - // Done with lock - c.cache.Unlock() - - if res == nil { - // Generate fresh result. - value, err := load() - - if err != nil { - if c.ignore(err) { - // don't cache this error type - return zero, err - } - - // Alloc result. - res = getResult() - - // Store error result. - res.Error = err - - // This load returned an error, only - // store this item under provided key. - res.Keys = []cacheKey{{ - info: lookup, - key: key, - }} - } else { - // Alloc result. - res = getResult() - - // Store value result. - res.Value = value - - // This was a successful load, generate keys. - res.Keys = c.lookups.generate(res.Value) - } - - var evict func() - - // Lock cache. - c.cache.Lock() - - defer func() { - // Unlock cache. - c.cache.Unlock() - - if evict != nil { - // Call evict. - evict() - } - }() - - // Store result in cache. - evict = c.store(res) - } - - // Catch and return cached error - if err := res.Error; err != nil { - return zero, err - } - - // Copy value from cached result. - v := c.copy(res.Value.(T)) - - return v, nil -} - -func (c *Cache[T]) has(lookup *structKey, key string) bool { - var res *result - - // Acquire cache lock - c.cache.Lock() - - // Look for primary key for cache key (only accept len=1) - if pkeys := lookup.pkeys[key]; len(pkeys) == 1 { - // Fetch the result for primary key - entry, ok := c.cache.Cache.Get(pkeys[0]) - - 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. - res = entry.Value.(*result) - } - } - - // Check for result AND non-error result. - ok := (res != nil && res.Error == nil) - - // Done with lock - c.cache.Unlock() - - return ok -} - -func (c *Cache[T]) store(res *result) (evict func()) { - var toEvict []*result - - // Get primary key - res.PKey = c.next - c.next++ - if res.PKey > c.next { - panic("cache primary key overflow") - } - - for _, key := range res.Keys { - // 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, ok := c.cache.Cache.Get(conflict) - - 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. - continue - } - - // From conflicting entry, drop this key, this - // will prevent eviction cleanup key confusion. - confRes := entry.Value.(*result) - confRes.Keys.drop(key.info.name) - - if len(res.Keys) == 0 { - // We just over-wrote the only lookup key for - // this value, so we drop its primary key too. - _ = c.cache.Cache.Delete(conflict) - - // Add finished result to evict queue. - toEvict = append(toEvict, confRes) - } - } - - // Drop existing. - pkeys = pkeys[:0] - } - - // Store primary key lookup. - pkeys = append(pkeys, res.PKey) - key.info.pkeys[key.key] = pkeys - } - - // 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)) - } - - // Store main entry under primary key, catch evicted. - c.cache.Cache.SetWithHook(res.PKey, entry, evictFn) - - if len(toEvict) == 0 { - // none evicted. - return nil - } - - return func() { - for i := range toEvict { - // Rescope result. - res := toEvict[i] - - // Call evict hook on each entry. - c.cache.Evict(res.PKey, res) - } - } -} - -func (c *Cache[T]) invalidate(lookup *structKey, key string) { - // Look for primary key for cache key - c.cache.Lock() - pkeys := lookup.pkeys[key] - delete(lookup.pkeys, key) - c.cache.Unlock() - - // Invalidate all primary keys. - c.cache.InvalidateAll(pkeys...) -} - -type result struct { - // Result primary key - PKey int64 - - // keys accessible under - Keys cacheKeys - - // cached value - Value any - - // cached error - Error error -} diff --git a/vendor/codeberg.org/gruf/go-cache/v3/result/key.go b/vendor/codeberg.org/gruf/go-cache/v3/result/key.go deleted file mode 100644 index 5508936ba..000000000 --- a/vendor/codeberg.org/gruf/go-cache/v3/result/key.go +++ /dev/null @@ -1,282 +0,0 @@ -package result - -import ( - "fmt" - "reflect" - "strings" - "sync" - "unicode" - "unicode/utf8" - - "codeberg.org/gruf/go-byteutil" - "codeberg.org/gruf/go-mangler" -) - -// structKeys provides convience methods for a list -// of structKey field combinations used for cache keys. -type structKeys []structKey - -// get fetches the structKey info for given lookup name (else, panics). -func (sk structKeys) get(name string) *structKey { - for i := range sk { - if sk[i].name == name { - return &sk[i] - } - } - panic("unknown lookup: \"" + name + "\"") -} - -// generate will calculate and produce a slice of cache keys the given value -// can be stored under in the, as determined by receiving struct keys. -func (sk structKeys) generate(a any) []cacheKey { - var keys []cacheKey - - // Get reflected value in order - // to access the struct fields - v := reflect.ValueOf(a) - - // Iteratively deref pointer value - for v.Kind() == reflect.Pointer { - if v.IsNil() { - panic("nil ptr") - } - v = v.Elem() - } - - // Acquire buffer - buf := getBuf() - -outer: - for i := range sk { - // Reset buffer - buf.Reset() - - // Append each field value to buffer. - for _, field := range sk[i].fields { - fv := v.Field(field.index) - fi := fv.Interface() - - // Mangle this key part into buffer. - ok := field.manglePart(buf, fi) - - if !ok { - // don't generate keys - // for zero value parts. - continue outer - } - - // Append part separator. - buf.B = append(buf.B, '.') - } - - // Drop last '.' - buf.Truncate(1) - - // Append new cached key to slice - keys = append(keys, cacheKey{ - info: &sk[i], - key: string(buf.B), // copy - }) - } - - // Release buf - putBuf(buf) - - return keys -} - -type cacheKeys []cacheKey - -// drop will drop the cachedKey with lookup name from receiving cacheKeys slice. -func (ck *cacheKeys) drop(name string) { - _ = *ck // move out of loop - for i := range *ck { - if (*ck)[i].info.name == name { - (*ck) = append((*ck)[:i], (*ck)[i+1:]...) - break - } - } -} - -// cacheKey represents an actual cached key. -type cacheKey struct { - // info is a reference to the structKey this - // cacheKey is representing. This is a shared - // reference and as such only the structKey.pkeys - // lookup map is expecting to be modified. - info *structKey - - // value is the actual string representing - // this cache key for hashmap lookups. - key string -} - -// structKey represents a list of struct fields -// encompassing a single cache key, the string name -// of the lookup, the lookup map to primary cache -// keys, and the key's possible zero value string. -type structKey struct { - // name is the provided cache lookup name for - // this particular struct key, consisting of - // period ('.') separated struct field names. - name string - - // unique determines whether this structKey supports - // multiple or just the singular unique result. - unique bool - - // fields is a slice of runtime struct field - // indices, of fields encompassed by this key. - fields []structField - - // pkeys is a lookup of stored struct key values - // to the primary cache lookup key (int64). this - // is protected by the main cache mutex. - pkeys map[string][]int64 -} - -// newStructKey will generate a structKey{} information object for user-given lookup -// key information, and the receiving generic paramter's type information. Panics on error. -func newStructKey(lk Lookup, t reflect.Type) structKey { - var sk structKey - - // Set the lookup name - sk.name = lk.Name - - // Split dot-separated lookup to get - // the individual struct field names - names := strings.Split(lk.Name, ".") - - // Allocate the mangler and field indices slice. - sk.fields = make([]structField, len(names)) - - for i, name := range names { - // Get field info for given name - ft, ok := t.FieldByName(name) - if !ok { - panic("no field found for name: \"" + name + "\"") - } - - // Check field is usable - if !isExported(name) { - panic("field must be exported") - } - - // Set the runtime field index - sk.fields[i].index = ft.Index[0] - - // Allocate new instance of field - v := reflect.New(ft.Type) - v = v.Elem() - - // Fetch mangler for field type. - sk.fields[i].mangle = mangler.Get(ft.Type) - - if !lk.AllowZero { - // Append the mangled zero value interface - zero := sk.fields[i].mangle(nil, v.Interface()) - sk.fields[i].zero = string(zero) - } - } - - // Set unique lookup flag. - sk.unique = !lk.Multi - - // Allocate primary lookup map - sk.pkeys = make(map[string][]int64) - - return sk -} - -// genKey generates a cache key string for given key parts (i.e. serializes them using "go-mangler"). -func (sk *structKey) genKey(parts []any) string { - // Check this expected no. key parts. - if len(parts) != len(sk.fields) { - panic(fmt.Sprintf("incorrect no. key parts provided: want=%d received=%d", len(parts), len(sk.fields))) - } - - // Acquire buffer - buf := getBuf() - buf.Reset() - - for i, part := range parts { - // Mangle this key part into buffer. - // specifically ignoring whether this - // is returning a zero value key part. - _ = sk.fields[i].manglePart(buf, part) - - // Append part separator. - buf.B = append(buf.B, '.') - } - - // Drop last '.' - buf.Truncate(1) - - // Create str copy - str := string(buf.B) - - // Release buf - putBuf(buf) - - return str -} - -type structField struct { - // index is the reflect index of this struct field. - index int - - // zero is the possible zero value for this - // key part. if set, this will _always_ be - // non-empty due to how the mangler works. - // - // i.e. zero = "" --> allow zero value keys - // zero != "" --> don't allow zero value keys - zero string - - // mangle is the mangler function for - // serializing values of this struct field. - mangle mangler.Mangler -} - -// manglePart ... -func (field *structField) manglePart(buf *byteutil.Buffer, part any) bool { - // Start of part bytes. - start := len(buf.B) - - // Mangle this key part into buffer. - buf.B = field.mangle(buf.B, part) - - // End of part bytes. - end := len(buf.B) - - // Return whether this is zero value. - return (field.zero == "" || - string(buf.B[start:end]) != field.zero) -} - -// isExported checks whether function name is exported. -func isExported(fnName string) bool { - r, _ := utf8.DecodeRuneInString(fnName) - return unicode.IsUpper(r) -} - -// bufpool provides a memory pool of byte -// buffers use when encoding key types. -var bufPool = sync.Pool{ - New: func() any { - return &byteutil.Buffer{B: make([]byte, 0, 512)} - }, -} - -// getBuf acquires a byte buffer from memory pool. -func getBuf() *byteutil.Buffer { - return bufPool.Get().(*byteutil.Buffer) -} - -// putBuf replaces a byte buffer back in memory pool. -func putBuf(buf *byteutil.Buffer) { - if buf.Cap() > int(^uint16(0)) { - return // drop large bufs - } - bufPool.Put(buf) -} diff --git a/vendor/codeberg.org/gruf/go-cache/v3/result/pool.go b/vendor/codeberg.org/gruf/go-cache/v3/result/pool.go deleted file mode 100644 index 6f194be5a..000000000 --- a/vendor/codeberg.org/gruf/go-cache/v3/result/pool.go +++ /dev/null @@ -1,25 +0,0 @@ -package result - -import "sync" - -// resultPool is a global pool for result -// objects, regardless of cache type. -var resultPool sync.Pool - -// getEntry fetches a result from pool, or allocates new. -func getResult() *result { - v := resultPool.Get() - if v == nil { - return new(result) - } - return v.(*result) -} - -// putResult replaces a result in the pool. -func putResult(r *result) { - r.PKey = 0 - r.Keys = nil - r.Value = nil - r.Error = nil - resultPool.Put(r) -} diff --git a/vendor/codeberg.org/gruf/go-structr/LICENSE b/vendor/codeberg.org/gruf/go-structr/LICENSE new file mode 100644 index 000000000..d6f08d0ab --- /dev/null +++ b/vendor/codeberg.org/gruf/go-structr/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) gruf + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/codeberg.org/gruf/go-structr/README.md b/vendor/codeberg.org/gruf/go-structr/README.md new file mode 100644 index 000000000..e2a9bdc15 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-structr/README.md @@ -0,0 +1,5 @@ +# go-structr + +A performant struct caching library with automated indexing by arbitrary combinations of fields, including support for negative results (errors!). An example use case is in database lookups. + +This is a core underpinning of [GoToSocial](https://github.com/superseriousbusiness/gotosocial)'s performance.
\ No newline at end of file diff --git a/vendor/codeberg.org/gruf/go-structr/cache.go b/vendor/codeberg.org/gruf/go-structr/cache.go new file mode 100644 index 000000000..b958fdfdb --- /dev/null +++ b/vendor/codeberg.org/gruf/go-structr/cache.go @@ -0,0 +1,731 @@ +package structr + +import ( + "context" + "errors" + "reflect" + "sync" +) + +// DefaultIgnoreErr is the default function used to +// ignore (i.e. not cache) incoming error results during +// Load() calls. By default ignores context pkg errors. +func DefaultIgnoreErr(err error) bool { + return errors.Is(err, context.Canceled) || + errors.Is(err, context.DeadlineExceeded) +} + +// Config defines config variables +// for initializing a struct cache. +type Config[StructType any] struct { + + // Indices defines indices to create + // in the Cache for the receiving + // generic struct type parameter. + Indices []IndexConfig + + // MaxSize defines the maximum number + // of results allowed in the Cache at + // one time, before old results start + // getting evicted. + MaxSize int + + // IgnoreErr defines which errors to + // ignore (i.e. not cache) returned + // from load function callback calls. + // This may be left as nil, on which + // DefaultIgnoreErr will be used. + IgnoreErr func(error) bool + + // CopyValue provides a means of copying + // cached values, to ensure returned values + // do not share memory with those in cache. + CopyValue func(StructType) StructType + + // Invalidate is called when cache values + // (NOT errors) are invalidated, either + // as the values passed to Put() / Store(), + // or by the keys by calls to Invalidate(). + Invalidate func(StructType) +} + +// Cache provides a structure cache with automated +// indexing and lookups by any initialization-defined +// combination of fields (as long as serialization is +// supported by codeberg.org/gruf/go-mangler). This +// also supports caching of negative results by errors +// returned from the LoadOne() series of functions. +type Cache[StructType any] struct { + + // indices used in storing passed struct + // types by user defined sets of fields. + indices []Index[StructType] + + // keeps track of all indexed results, + // in order of last recently used (LRU). + lruList list[*result[StructType]] + + // memory pools of common types. + llsPool []*list[*result[StructType]] + resPool []*result[StructType] + keyPool []*indexkey[StructType] + + // max cache size, imposes size + // limit on the lruList in order + // to evict old entries. + maxSize int + + // hook functions. + ignore func(error) bool + copy func(StructType) StructType + invalid func(StructType) + + // protective mutex, guards: + // - Cache{}.lruList + // - Index{}.data + // - Cache{} hook fns + // - Cache{} pools + mutex sync.Mutex +} + +// Init initializes the cache with given configuration +// including struct fields to index, and necessary fns. +func (c *Cache[T]) Init(config Config[T]) { + if len(config.Indices) == 0 { + panic("no indices provided") + } + + if config.IgnoreErr == nil { + config.IgnoreErr = DefaultIgnoreErr + } + + if config.CopyValue == nil { + panic("copy value function must be provided") + } + + if config.MaxSize < 2 { + panic("minimum cache size is 2 for LRU to work") + } + + // Safely copy over + // provided config. + c.mutex.Lock() + c.indices = make([]Index[T], len(config.Indices)) + for i, config := range config.Indices { + c.indices[i].init(config) + } + c.ignore = config.IgnoreErr + c.copy = config.CopyValue + c.invalid = config.Invalidate + c.maxSize = config.MaxSize + c.mutex.Unlock() +} + +// Index selects index with given name from cache, else panics. +func (c *Cache[T]) Index(name string) *Index[T] { + for i := range c.indices { + if c.indices[i].name == name { + return &c.indices[i] + } + } + panic("unknown index: " + name) +} + +// GetOne fetches one value from the cache stored under index, using key generated from key parts. +// Note that given number of key parts MUST match expected number and types of the given index name. +func (c *Cache[T]) GetOne(index string, keyParts ...any) (T, bool) { + // Get index with name. + idx := c.Index(index) + + // Generate index key from provided parts. + key, ok := idx.keygen.FromParts(keyParts...) + if !ok { + var zero T + return zero, false + } + + // Fetch one value for key. + return c.GetOneBy(idx, key) +} + +// GetOneBy fetches value from cache stored under index, using precalculated index key. +func (c *Cache[T]) GetOneBy(index *Index[T], key string) (T, bool) { + if index == nil { + panic("no index given") + } else if !index.unique { + panic("cannot get one by non-unique index") + } + values := c.GetBy(index, key) + if len(values) == 0 { + var zero T + return zero, false + } + return values[0], true +} + +// Get fetches values from the cache stored under index, using keys generated from given key parts. +// Note that each number of key parts MUST match expected number and types of the given index name. +func (c *Cache[T]) Get(index string, keysParts ...[]any) []T { + // Get index with name. + idx := c.Index(index) + + // Preallocate expected keys slice length. + keys := make([]string, 0, len(keysParts)) + + // Acquire buf. + buf := getBuf() + + for _, parts := range keysParts { + // Reset buf. + buf.Reset() + + // Generate key from provided parts into buffer. + if !idx.keygen.AppendFromParts(buf, parts...) { + continue + } + + // Get string copy of + // genarated idx key. + key := string(buf.B) + + // Append key to keys. + keys = append(keys, key) + } + + // Done with buf. + putBuf(buf) + + // Continue fetching values. + return c.GetBy(idx, keys...) +} + +// GetBy fetches values from the cache stored under index, using precalculated index keys. +func (c *Cache[T]) GetBy(index *Index[T], keys ...string) []T { + if index == nil { + panic("no index given") + } + + // Preallocate a slice of est. len. + values := make([]T, 0, len(keys)) + + // Acquire lock. + c.mutex.Lock() + + // Check cache init. + if c.copy == nil { + c.mutex.Unlock() + panic("not initialized") + } + + // Check index for all keys. + for _, key := range keys { + + // Get indexed results. + list := index.data[key] + + if list != nil { + // Concatenate all results with values. + list.rangefn(func(e *elem[*result[T]]) { + if e.Value.err != nil { + return + } + + // Append a copy of value. + value := c.copy(e.Value.value) + values = append(values, value) + + // Push to front of LRU list, USING + // THE RESULT'S LRU ENTRY, NOT THE + // INDEX KEY ENTRY. VERY IMPORTANT!! + c.lruList.moveFront(&e.Value.entry) + }) + } + } + + // Done with lock. + c.mutex.Unlock() + + return values +} + +// Put will insert the given values into cache, +// calling any invalidate hook on each value. +func (c *Cache[T]) Put(values ...T) { + // Acquire lock. + c.mutex.Lock() + + // Get func ptrs. + invalid := c.invalid + + // Check cache init. + if c.copy == nil { + c.mutex.Unlock() + panic("not initialized") + } + + // Store all the passed values. + for _, value := range values { + c.store(nil, "", value, nil) + } + + // Done with lock. + c.mutex.Unlock() + + if invalid != nil { + // Pass all invalidated values + // to given user hook (if set). + for _, value := range values { + invalid(value) + } + } +} + +// LoadOne fetches one result from the cache stored under index, using key generated from key parts. +// In the case that no result is found, the provided load callback will be used to hydrate the cache. +// Note that given number of key parts MUST match expected number and types of the given index name. +func (c *Cache[T]) LoadOne(index string, load func() (T, error), keyParts ...any) (T, error) { + // Get index with name. + idx := c.Index(index) + + // Generate cache from from provided parts. + key, _ := idx.keygen.FromParts(keyParts...) + + // Continue loading this result. + return c.LoadOneBy(idx, load, key) +} + +// LoadOneBy fetches one result from the cache stored under index, using precalculated index key. +// In the case that no result is found, provided load callback will be used to hydrate the cache. +func (c *Cache[T]) LoadOneBy(index *Index[T], load func() (T, error), key string) (T, error) { + if index == nil { + panic("no index given") + } else if !index.unique { + panic("cannot get one by non-unique index") + } + + var ( + // whether a result was found + // (and so val / err are set). + ok bool + + // separate value / error ptrs + // as the result is liable to + // change outside of lock. + val T + err error + ) + + // Acquire lock. + c.mutex.Lock() + + // Get func ptrs. + ignore := c.ignore + + // Check init'd. + if c.copy == nil || + ignore == nil { + c.mutex.Unlock() + panic("not initialized") + } + + // Get indexed results. + list := index.data[key] + + if ok = (list != nil && list.head != nil); ok { + e := list.head + + // Extract val / err. + val = e.Value.value + err = e.Value.err + + if err == nil { + // We only ever ret + // a COPY of value. + val = c.copy(val) + } + + // Push to front of LRU list, USING + // THE RESULT'S LRU ENTRY, NOT THE + // INDEX KEY ENTRY. VERY IMPORTANT!! + c.lruList.moveFront(&e.Value.entry) + } + + // Done with lock. + c.mutex.Unlock() + + if ok { + // result found! + return val, err + } + + // Load new result. + val, err = load() + + // Check for ignored + // (transient) errors. + if ignore(err) { + return val, err + } + + // Acquire lock. + c.mutex.Lock() + + // Index this new loaded result. + // Note this handles copying of + // the provided value, so it is + // safe for us to return as-is. + c.store(index, key, val, err) + + // Done with lock. + c.mutex.Unlock() + + return val, err +} + +// Load fetches values from the cache stored under index, using keys generated from given key parts. The provided get callback is used +// to load groups of values from the cache by the key generated from the key parts provided to the inner callback func, where the returned +// boolean indicates whether any values are currently stored. After the get callback has returned, the cache will then call provided load +// callback to hydrate the cache with any other values. Example usage here is that you may see which values are cached using 'get', and load +// the remaining uncached values using 'load', to minimize database queries. Cached error results are not included or returned by this func. +// Note that given number of key parts MUST match expected number and types of the given index name, in those provided to the get callback. +func (c *Cache[T]) Load(index string, get func(load func(keyParts ...any) bool), load func() ([]T, error)) (values []T, err error) { + return c.LoadBy(c.Index(index), get, load) +} + +// LoadBy fetches values from the cache stored under index, using precalculated index key. The provided get callback is used to load +// groups of values from the cache by the key generated from the key parts provided to the inner callback func, where the returned boolea +// indicates whether any values are currently stored. After the get callback has returned, the cache will then call provided load callback +// to hydrate the cache with any other values. Example usage here is that you may see which values are cached using 'get', and load the +// remaining uncached values using 'load', to minimize database queries. Cached error results are not included or returned by this func. +// Note that given number of key parts MUST match expected number and types of the given index name, in those provided to the get callback. +func (c *Cache[T]) LoadBy(index *Index[T], get func(load func(keyParts ...any) bool), load func() ([]T, error)) (values []T, err error) { + if index == nil { + panic("no index given") + } + + // Acquire lock. + c.mutex.Lock() + + // Check init'd. + if c.copy == nil { + c.mutex.Unlock() + panic("not initialized") + } + + var unlocked bool + defer func() { + // Deferred unlock to catch + // any user function panics. + if !unlocked { + c.mutex.Unlock() + } + }() + + // Acquire buf. + buf := getBuf() + + // Pass cache check to user func. + get(func(keyParts ...any) bool { + + // Reset buf. + buf.Reset() + + // Generate index key from provided key parts. + if !index.keygen.AppendFromParts(buf, keyParts...) { + return false + } + + // Get temp generated key str, + // (not needed after return). + keyStr := buf.String() + + // Get all indexed results. + list := index.data[keyStr] + + if list != nil && list.len > 0 { + // Value length before + // any below appends. + before := len(values) + + // Concatenate all results with values. + list.rangefn(func(e *elem[*result[T]]) { + if e.Value.err != nil { + return + } + + // Append a copy of value. + value := c.copy(e.Value.value) + values = append(values, value) + + // Push to front of LRU list, USING + // THE RESULT'S LRU ENTRY, NOT THE + // INDEX KEY ENTRY. VERY IMPORTANT!! + c.lruList.moveFront(&e.Value.entry) + }) + + // Only if values changed did + // we actually find anything. + return len(values) != before + } + + return false + }) + + // Done with buf. + putBuf(buf) + + // Done with lock. + c.mutex.Unlock() + unlocked = true + + // Load uncached values. + uncached, err := load() + if err != nil { + return nil, err + } + + // Insert uncached. + c.Put(uncached...) + + // Append uncached to return values. + values = append(values, uncached...) + + return +} + +// Store will call the given store callback, on non-error then +// passing the provided value to the Put() function. On error +// return the value is still passed to stored invalidate hook. +func (c *Cache[T]) Store(value T, store func() error) error { + // Store value. + err := store() + + if err != nil { + // Get func ptrs. + c.mutex.Lock() + invalid := c.invalid + c.mutex.Unlock() + + // On error don't store + // value, but still pass + // to invalidate hook. + if invalid != nil { + invalid(value) + } + + return err + } + + // Store value. + c.Put(value) + + return nil +} + +// Invalidate generates index key from parts and invalidates all stored under it. +func (c *Cache[T]) Invalidate(index string, keyParts ...any) { + // Get index with name. + idx := c.Index(index) + + // Generate cache from from provided parts. + key, ok := idx.keygen.FromParts(keyParts...) + if !ok { + return + } + + // Continue invalidation. + c.InvalidateBy(idx, key) +} + +// InvalidateBy invalidates all results stored under index key. +func (c *Cache[T]) InvalidateBy(index *Index[T], key string) { + if index == nil { + panic("no index given") + } + + var values []T + + // Acquire lock. + c.mutex.Lock() + + // Get func ptrs. + invalid := c.invalid + + // Delete all results under key from index, collecting + // value results and dropping them from all their indices. + index_delete(c, index, key, func(del *result[T]) { + if del.err == nil { + values = append(values, del.value) + } + c.delete(del) + }) + + // Done with lock. + c.mutex.Unlock() + + if invalid != nil { + // Pass all invalidated values + // to given user hook (if set). + for _, value := range values { + invalid(value) + } + } +} + +// Trim will truncate the cache to ensure it +// stays within given percentage of MaxSize. +func (c *Cache[T]) Trim(perc float64) { + // Acquire lock. + c.mutex.Lock() + + // Calculate number of cache items to drop. + max := (perc / 100) * float64(c.maxSize) + diff := c.lruList.len - int(max) + + if diff <= 0 { + // Trim not needed. + c.mutex.Unlock() + return + } + + // Iterate over 'diff' results + // from back (oldest) of cache. + for i := 0; i < diff; i++ { + + // Get oldest LRU element. + oldest := c.lruList.tail + + if oldest == nil { + // reached end. + break + } + + // Drop oldest from cache. + c.delete(oldest.Value) + } + + // Done with lock. + c.mutex.Unlock() +} + +// Clear empties the cache by calling .Trim(0). +func (c *Cache[T]) Clear() { c.Trim(0) } + +// Clean drops unused items from its memory pools. +// Useful to free memory if cache has downsized. +func (c *Cache[T]) Clean() { + c.mutex.Lock() + c.llsPool = nil + c.resPool = nil + c.keyPool = nil + c.mutex.Unlock() +} + +// Len returns the current length of cache. +func (c *Cache[T]) Len() int { + c.mutex.Lock() + l := c.lruList.len + c.mutex.Unlock() + return l +} + +// Cap returns the maximum capacity (size) of cache. +func (c *Cache[T]) Cap() int { + c.mutex.Lock() + m := c.maxSize + c.mutex.Unlock() + return m +} + +// store will store the given value / error result in the cache, storing it under the +// already provided index + key if provided, else generating keys from provided value. +func (c *Cache[T]) store(index *Index[T], key string, value T, err error) { + // Acquire new result. + res := result_acquire(c) + + if index != nil { + // Append result to the provided + // precalculated key and its index. + index_append(c, index, key, res) + + } else if err != nil { + + // This is an error result without + // an index provided, nothing we + // can do here so release result. + result_release(c, res) + return + } + + // Set and check the result error. + if res.err = err; res.err == nil { + + // This is value result, we need to + // store it under all other indices + // other than the provided. + // + // Create COPY of value. + res.value = c.copy(value) + + // Get reflected value of incoming + // value, used during cache key gen. + rvalue := reflect.ValueOf(value) + + // Acquire buf. + buf := getBuf() + + for i := range c.indices { + // Get current index ptr. + idx := &(c.indices[i]) + + if idx == index { + // Already stored under + // this index, ignore. + continue + } + + // Generate key from reflect value, + // (this ignores zero value keys). + buf.Reset() // reset buf first + if !idx.keygen.appendFromRValue(buf, rvalue) { + continue + } + + // Alloc key copy. + key := string(buf.B) + + // Append result to index at key. + index_append(c, idx, key, res) + } + + // Done with buf. + putBuf(buf) + } + + if c.lruList.len > c.maxSize { + // Cache has hit max size! + // Drop the oldest element. + res := c.lruList.tail.Value + c.delete(res) + } +} + +// delete will delete the given result from the cache, deleting +// it from all indices it is stored under, and main LRU list. +func (c *Cache[T]) delete(res *result[T]) { + for len(res.keys) != 0 { + + // Pop indexkey at end of list. + ikey := res.keys[len(res.keys)-1] + res.keys = res.keys[:len(res.keys)-1] + + // Drop this result from list at key. + index_deleteOne(c, ikey.index, ikey) + + // Release ikey to pool. + indexkey_release(c, ikey) + } + + // Release res to pool. + result_release(c, res) +} diff --git a/vendor/codeberg.org/gruf/go-structr/debug.go b/vendor/codeberg.org/gruf/go-structr/debug.go new file mode 100644 index 000000000..842f4cfe1 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-structr/debug.go @@ -0,0 +1,41 @@ +package structr + +// String returns a useful debugging repr of result. +// func (r *result[T]) String() string { +// keysbuf := getBuf() +// keysbuf.B = append(keysbuf.B, '[') +// for i := range r.keys { +// keysbuf.B = strconv.AppendQuote(keysbuf.B, r.keys[i].key) +// keysbuf.B = append(keysbuf.B, ',') +// } +// if len(keysbuf.B) > 0 { +// keysbuf.B = keysbuf.B[:len(keysbuf.B)-1] +// } +// keysbuf.B = append(keysbuf.B, ']') +// str := fmt.Sprintf("{value=%v err=%v keys=%s}", r.value, r.err, keysbuf.B) +// putBuf(keysbuf) +// return str +// } + +// String returns a useful debugging repr of index. +// func (i *Index[T]) String() string { +// databuf := getBuf() +// for key, values := range i.data { +// databuf.WriteString("key") +// databuf.B = strconv.AppendQuote(databuf.B, key) +// databuf.B = append(databuf.B, '=') +// fmt.Fprintf(databuf, "%v", values) +// databuf.B = append(databuf.B, ' ') +// } +// if len(i.data) > 0 { +// databuf.B = databuf.B[:len(databuf.B)-1] +// } +// str := fmt.Sprintf("{name=%s data={%s}}", i.name, databuf.B) +// putBuf(databuf) +// return str +// } + +// String returns a useful debugging repr of indexkey. +// func (i *indexkey[T]) String() string { +// return i.index.name + "[" + strconv.Quote(i.key) + "]" +// } diff --git a/vendor/codeberg.org/gruf/go-structr/index.go b/vendor/codeberg.org/gruf/go-structr/index.go new file mode 100644 index 000000000..bacf6142e --- /dev/null +++ b/vendor/codeberg.org/gruf/go-structr/index.go @@ -0,0 +1,213 @@ +package structr + +import ( + "strings" +) + +// IndexConfig defines config variables +// for initializing a struct index. +type IndexConfig struct { + + // Fields should contain a comma-separated + // list of struct fields used when generating + // keys for this index. Nested fields should + // be specified using periods. An example: + // "Username,Favorites.Color" + Fields string + + // Multiple indicates whether to accept multiple + // possible values for any single index key. The + // default behaviour is to only accept one value + // and overwrite existing on any write operation. + Multiple bool + + // AllowZero indicates whether to accept zero + // value fields in index keys. i.e. whether to + // index structs for this set of field values + // IF any one of those field values is the zero + // value for that type. The default behaviour + // is to skip indexing structs for this lookup + // when any of the indexing fields are zero. + AllowZero bool +} + +// Index is an exposed Cache internal model, used to +// generate keys and store struct results by the init +// defined key generation configuration. This model is +// exposed to provide faster lookups in the case that +// you would like to manually provide the used index +// via the Cache.___By() series of functions, or access +// the underlying index key generator. +type Index[StructType any] struct { + + // name is the actual name of this + // index, which is the unparsed + // string value of contained fields. + name string + + // struct field key serializer. + keygen KeyGen[StructType] + + // backing in-memory data store of + // generated index keys to result lists. + data map[string]*list[*result[StructType]] + + // whether to allow + // multiple results + // per index key. + unique bool +} + +// init initializes this index with the given configuration. +func (i *Index[T]) init(config IndexConfig) { + fields := strings.Split(config.Fields, ",") + i.name = config.Fields + i.keygen = NewKeyGen[T](fields, config.AllowZero) + i.unique = !config.Multiple + i.data = make(map[string]*list[*result[T]]) +} + +// KeyGen returns the key generator associated with this index. +func (i *Index[T]) KeyGen() *KeyGen[T] { + return &i.keygen +} + +func index_append[T any](c *Cache[T], i *Index[T], key string, res *result[T]) { + // Acquire + setup indexkey. + ikey := indexkey_acquire(c) + ikey.entry.Value = res + ikey.key = key + ikey.index = i + + // Append to result's indexkeys. + res.keys = append(res.keys, ikey) + + // Get list at key. + l := i.data[key] + + if l == nil { + + // Allocate new list. + l = list_acquire(c) + i.data[key] = l + + } else if i.unique { + + // Remove currently + // indexed result. + old := l.head + l.remove(old) + + // Get ptr to old + // result before we + // release to pool. + res := old.Value + + // Drop this index's key from + // old res now not indexed here. + result_dropIndex(c, res, i) + if len(res.keys) == 0 { + + // Old res now unused, + // release to mem pool. + result_release(c, res) + } + } + + // Add result indexkey to + // front of results list. + l.pushFront(&ikey.entry) +} + +func index_deleteOne[T any](c *Cache[T], i *Index[T], ikey *indexkey[T]) { + // Get list at key. + l := i.data[ikey.key] + if l == nil { + return + } + + // Remove from list. + l.remove(&ikey.entry) + if l.len == 0 { + + // Remove list from map. + delete(i.data, ikey.key) + + // Release list to pool. + list_release(c, l) + } +} + +func index_delete[T any](c *Cache[T], i *Index[T], key string, fn func(*result[T])) { + if fn == nil { + panic("nil fn") + } + + // Get list at key. + l := i.data[key] + if l == nil { + return + } + + // Delete data at key. + delete(i.data, key) + + // Iterate results in list. + for x := 0; x < l.len; x++ { + + // Pop current head. + res := l.head.Value + l.remove(l.head) + + // Delete index's key + // from result tracking. + result_dropIndex(c, res, i) + + // Call hook. + fn(res) + } + + // Release list to pool. + list_release(c, l) +} + +type indexkey[T any] struct { + // linked list entry the related + // result is stored under in the + // Index.data[key] linked list. + entry elem[*result[T]] + + // key is the generated index key + // the related result is indexed + // under, in the below index. + key string + + // index is the index that the + // related result is indexed in. + index *Index[T] +} + +func indexkey_acquire[T any](c *Cache[T]) *indexkey[T] { + var ikey *indexkey[T] + + if len(c.keyPool) == 0 { + // Allocate new key. + ikey = new(indexkey[T]) + } else { + // Pop result from pool slice. + ikey = c.keyPool[len(c.keyPool)-1] + c.keyPool = c.keyPool[:len(c.keyPool)-1] + } + + return ikey +} + +func indexkey_release[T any](c *Cache[T], ikey *indexkey[T]) { + // Reset indexkey. + ikey.entry.Value = nil + ikey.key = "" + ikey.index = nil + + // Release indexkey to memory pool. + c.keyPool = append(c.keyPool, ikey) +} diff --git a/vendor/codeberg.org/gruf/go-structr/key.go b/vendor/codeberg.org/gruf/go-structr/key.go new file mode 100644 index 000000000..557a5f033 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-structr/key.go @@ -0,0 +1,204 @@ +package structr + +import ( + "reflect" + "strings" + + "codeberg.org/gruf/go-byteutil" + "codeberg.org/gruf/go-mangler" +) + +// KeyGen is the underlying index key generator +// used within Index, and therefore Cache itself. +type KeyGen[StructType any] struct { + + // fields contains our representation of + // the struct fields contained in the + // creation of keys by this generator. + fields []structfield + + // zero specifies whether zero + // value fields are permitted. + zero bool +} + +// NewKeyGen returns a new initialized KeyGen for the receiving generic +// parameter type, comprising of the given field strings, and whether to +// allow zero values to be included within generated output strings. +func NewKeyGen[T any](fields []string, allowZero bool) KeyGen[T] { + var kgen KeyGen[T] + + // Preallocate expected struct field slice. + kgen.fields = make([]structfield, len(fields)) + + // Get the reflected struct ptr type. + t := reflect.TypeOf((*T)(nil)).Elem() + + for i, fieldName := range fields { + // Split name to account for nesting. + names := strings.Split(fieldName, ".") + + // Look for a usable struct field from type. + sfield, ok := findField(t, names, allowZero) + if !ok { + panicf("failed finding field: %s", fieldName) + } + + // Set parsed struct field. + kgen.fields[i] = sfield + } + + // Set config flags. + kgen.zero = allowZero + + return kgen +} + +// FromParts generates key string from individual key parts. +func (kgen *KeyGen[T]) FromParts(parts ...any) (key string, ok bool) { + buf := getBuf() + if ok = kgen.AppendFromParts(buf, parts...); ok { + key = string(buf.B) + } + putBuf(buf) + return +} + +// FromValue generates key string from a value, via reflection. +func (kgen *KeyGen[T]) FromValue(value T) (key string, ok bool) { + buf := getBuf() + rvalue := reflect.ValueOf(value) + if ok = kgen.appendFromRValue(buf, rvalue); ok { + key = string(buf.B) + } + putBuf(buf) + return +} + +// AppendFromParts generates key string into provided buffer, from individual key parts. +func (kgen *KeyGen[T]) AppendFromParts(buf *byteutil.Buffer, parts ...any) bool { + if len(parts) != len(kgen.fields) { + // User must provide correct number of parts for key. + panicf("incorrect number key parts: want=%d received=%d", + len(parts), + len(kgen.fields), + ) + } + + if kgen.zero { + // Zero values are permitted, + // mangle all values and ignore + // zero value return booleans. + for i, part := range parts { + + // Mangle this value into buffer. + _ = kgen.fields[i].Mangle(buf, part) + + // Append part separator. + buf.B = append(buf.B, '.') + } + } else { + // Zero values are NOT permitted. + for i, part := range parts { + + // Mangle this value into buffer. + z := kgen.fields[i].Mangle(buf, part) + + if z { + // The value was zero for + // this type, return early. + return false + } + + // Append part separator. + buf.B = append(buf.B, '.') + } + } + + // Drop the last separator. + buf.B = buf.B[:len(buf.B)-1] + + return true +} + +// AppendFromValue generates key string into provided buffer, from a value via reflection. +func (kgen *KeyGen[T]) AppendFromValue(buf *byteutil.Buffer, value T) bool { + return kgen.appendFromRValue(buf, reflect.ValueOf(value)) +} + +// appendFromRValue is the underlying generator function for the exported ___FromValue() functions, +// accepting a reflected input. We do not expose this as the reflected value is EXPECTED to be right. +func (kgen *KeyGen[T]) appendFromRValue(buf *byteutil.Buffer, rvalue reflect.Value) bool { + // Follow any ptrs leading to value. + for rvalue.Kind() == reflect.Pointer { + rvalue = rvalue.Elem() + } + + if kgen.zero { + // Zero values are permitted, + // mangle all values and ignore + // zero value return booleans. + for i := range kgen.fields { + + // Get the reflect value's field at idx. + fv := rvalue.FieldByIndex(kgen.fields[i].index) + fi := fv.Interface() + + // Mangle this value into buffer. + _ = kgen.fields[i].Mangle(buf, fi) + + // Append part separator. + buf.B = append(buf.B, '.') + } + } else { + // Zero values are NOT permitted. + for i := range kgen.fields { + + // Get the reflect value's field at idx. + fv := rvalue.FieldByIndex(kgen.fields[i].index) + fi := fv.Interface() + + // Mangle this value into buffer. + z := kgen.fields[i].Mangle(buf, fi) + + if z { + // The value was zero for + // this type, return early. + return false + } + + // Append part separator. + buf.B = append(buf.B, '.') + } + } + + // Drop the last separator. + buf.B = buf.B[:len(buf.B)-1] + + return true +} + +type structfield struct { + // index is the reflected index + // of this field (this takes into + // account struct nesting). + index []int + + // zero is the possible mangled + // zero value for this field. + zero string + + // mangler is the mangler function for + // serializing values of this field. + mangler mangler.Mangler +} + +// Mangle mangles the given value, using the determined type-appropriate +// field's type. The returned boolean indicates whether this is a zero value. +func (f *structfield) Mangle(buf *byteutil.Buffer, value any) (isZero bool) { + s := len(buf.B) // start pos. + buf.B = f.mangler(buf.B, value) + e := len(buf.B) // end pos. + isZero = (f.zero == string(buf.B[s:e])) + return +} diff --git a/vendor/codeberg.org/gruf/go-structr/list.go b/vendor/codeberg.org/gruf/go-structr/list.go new file mode 100644 index 000000000..07787c44c --- /dev/null +++ b/vendor/codeberg.org/gruf/go-structr/list.go @@ -0,0 +1,130 @@ +package structr + +// elem represents an element +// in a doubly-linked list. +type elem[T any] struct { + next *elem[T] + prev *elem[T] + Value T +} + +// list implements a doubly-linked list, where: +// - head = index 0 (i.e. the front) +// - tail = index n-1 (i.e. the back) +type list[T any] struct { + head *elem[T] + tail *elem[T] + len int +} + +func list_acquire[T any](c *Cache[T]) *list[*result[T]] { + var l *list[*result[T]] + + if len(c.llsPool) == 0 { + // Allocate new list. + l = new(list[*result[T]]) + } else { + // Pop list from pool slice. + l = c.llsPool[len(c.llsPool)-1] + c.llsPool = c.llsPool[:len(c.llsPool)-1] + } + + return l +} + +func list_release[T any](c *Cache[T], l *list[*result[T]]) { + // Reset list. + l.head = nil + l.tail = nil + l.len = 0 + + // Release list to memory pool. + c.llsPool = append(c.llsPool, l) +} + +// pushFront pushes new 'elem' to front of list. +func (l *list[T]) pushFront(elem *elem[T]) { + if l.len == 0 { + // Set new tail + head + l.head = elem + l.tail = elem + + // Link elem to itself + elem.next = elem + elem.prev = elem + } else { + oldHead := l.head + + // Link to old head + elem.next = oldHead + oldHead.prev = elem + + // Link up to tail + elem.prev = l.tail + l.tail.next = elem + + // Set new head + l.head = elem + } + + // Incr count + l.len++ +} + +// moveFront calls remove() on elem, followed by pushFront(). +func (l *list[T]) moveFront(elem *elem[T]) { + l.remove(elem) + l.pushFront(elem) +} + +// remove removes the 'elem' from the list. +func (l *list[T]) remove(elem *elem[T]) { + if l.len <= 1 { + // Drop elem's links + elem.next = nil + elem.prev = nil + + // Only elem in list + l.head = nil + l.tail = nil + l.len = 0 + return + } + + // Get surrounding elems + next := elem.next + prev := elem.prev + + // Relink chain + next.prev = prev + prev.next = next + + switch elem { + // Set new head + case l.head: + l.head = next + + // Set new tail + case l.tail: + l.tail = prev + } + + // Drop elem's links + elem.next = nil + elem.prev = nil + + // Decr count + l.len-- +} + +// rangefn ranges all the elements in the list, passing each to fn. +func (l *list[T]) rangefn(fn func(*elem[T])) { + if fn == nil { + panic("nil fn") + } + elem := l.head + for i := 0; i < l.len; i++ { + fn(elem) + elem = elem.next + } +} diff --git a/vendor/codeberg.org/gruf/go-structr/result.go b/vendor/codeberg.org/gruf/go-structr/result.go new file mode 100644 index 000000000..4468338b5 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-structr/result.go @@ -0,0 +1,76 @@ +package structr + +type result[T any] struct { + // linked list entry this result is + // stored under in Cache.lruList. + entry elem[*result[T]] + + // keys tracks the indices + // result is stored under. + keys []*indexkey[T] + + // cached value. + value T + + // cached error. + err error +} + +func result_acquire[T any](c *Cache[T]) *result[T] { + var res *result[T] + + if len(c.resPool) == 0 { + // Allocate new result. + res = new(result[T]) + } else { + // Pop result from pool slice. + res = c.resPool[len(c.resPool)-1] + c.resPool = c.resPool[:len(c.resPool)-1] + } + + // Push to front of LRU list. + c.lruList.pushFront(&res.entry) + res.entry.Value = res + + return res +} + +func result_release[T any](c *Cache[T], res *result[T]) { + // Remove from the LRU list. + c.lruList.remove(&res.entry) + res.entry.Value = nil + + var zero T + + // Reset all result fields. + res.keys = res.keys[:0] + res.value = zero + res.err = nil + + // Release result to memory pool. + c.resPool = append(c.resPool, res) +} + +func result_dropIndex[T any](c *Cache[T], res *result[T], index *Index[T]) { + for i := 0; i < len(res.keys); i++ { + + if res.keys[i].index != index { + // Prof. Obiwan: + // this is not the index + // we are looking for. + continue + } + + // Get index key ptr. + ikey := res.keys[i] + + // Move all index keys down + reslice. + copy(res.keys[i:], res.keys[i+1:]) + res.keys = res.keys[:len(res.keys)-1] + + // Release ikey to memory pool. + indexkey_release(c, ikey) + + return + } +} diff --git a/vendor/codeberg.org/gruf/go-structr/util.go b/vendor/codeberg.org/gruf/go-structr/util.go new file mode 100644 index 000000000..d8f227baf --- /dev/null +++ b/vendor/codeberg.org/gruf/go-structr/util.go @@ -0,0 +1,118 @@ +package structr + +import ( + "fmt" + "reflect" + "sync" + "unicode" + "unicode/utf8" + + "codeberg.org/gruf/go-byteutil" + "codeberg.org/gruf/go-mangler" +) + +// findField will search for a struct field with given set of names, where names is a len > 0 slice of names account for nesting. +func findField(t reflect.Type, names []string, allowZero bool) (sfield structfield, ok bool) { + var ( + // isExported returns whether name is exported + // from a package; can be func or struct field. + isExported = func(name string) bool { + r, _ := utf8.DecodeRuneInString(name) + return unicode.IsUpper(r) + } + + // popName pops the next name from + // the provided slice of field names. + popName = func() string { + // Pop next name. + name := names[0] + names = names[1:] + + // Ensure valid name. + if !isExported(name) { + panicf("field is not exported: %s", name) + } + + return name + } + + // field is the iteratively searched-for + // struct field value in below loop. + field reflect.StructField + ) + + for len(names) > 0 { + // Pop next name. + name := popName() + + // Follow any ptrs leading to field. + for t.Kind() == reflect.Pointer { + t = t.Elem() + } + + if t.Kind() != reflect.Struct { + // The end type after following ptrs must be struct. + panicf("field %s is not struct (ptr): %s", t, name) + } + + // Look for next field by name. + field, ok = t.FieldByName(name) + if !ok { + return + } + + // Append next set of indices required to reach field. + sfield.index = append(sfield.index, field.Index...) + + // Set the next type. + t = field.Type + } + + // Get final type mangler func. + sfield.mangler = mangler.Get(t) + + if allowZero { + var buf []byte + + // Allocate field instance. + v := reflect.New(field.Type) + v = v.Elem() + + // Serialize this zero value into buf. + buf = sfield.mangler(buf, v.Interface()) + + // Set zero value str. + sfield.zero = string(buf) + } + + return +} + +// panicf provides a panic with string formatting. +func panicf(format string, args ...any) { + panic(fmt.Sprintf(format, args...)) +} + +// bufpool provides a memory pool of byte +// buffers used when encoding key types. +var bufPool sync.Pool + +// getBuf fetches buffer from memory pool. +func getBuf() *byteutil.Buffer { + v := bufPool.Get() + if v == nil { + buf := new(byteutil.Buffer) + buf.B = make([]byte, 0, 512) + v = buf + } + return v.(*byteutil.Buffer) +} + +// putBuf replaces buffer in memory pool. +func putBuf(buf *byteutil.Buffer) { + if buf.Cap() > int(^uint16(0)) { + return // drop large bufs + } + buf.Reset() + bufPool.Put(buf) +} |