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/gruf/go-structr | |
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/gruf/go-structr')
-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 |
9 files changed, 1527 insertions, 0 deletions
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) +} |