From 033a523d381eae84ee1a83c30852a8f351588d42 Mon Sep 17 00:00:00 2001 From: Sunny Date: Thu, 17 Oct 2024 14:15:43 +0000 Subject: [PATCH] Simplify the cache - Remove dependency on kubernetes. - Simplify the Store interface to only have Get, Set and Delete. - Add new RecordCacheEvent() method in the cache implementations for recording the cache events, hit or miss, along with labels of the associated flux object being reconciled. The labels name, namespace and kind for this metric are not required to be configured when creating the cache. They are predefined in the created metrics recorder, similar to the runtime metrics recorder for the rest of flux. RecordCacheEvent() has to be called by the caller of cache operations explicitly along with the label information of the associated object being reconciled. The cache no longer has the knowledge of the object being reconciled, decoupled. - With the decoupled cache and reconciling object for metrics, the storeOptions no longer need to have the generic type defined. Simplifying the usage. - Add new DeleteCacheEvent() method in the cache implementations for deleting the cache events, hit or miss, which are associated with the object being reconciled. When the reconciling object is deleted, these metrics associated with the object must also be deleted. - Updated all the tests to use simple string cache. - Get() now returns a pointer instead of a separate exists boolean. If the pointer is nil, the item is not found in the cache. - Get() takes a key and returns a cached item for it. GetByKey() is removed as Get() does the same. Signed-off-by: Sunny --- cache/cache.go | 167 ++++++----------- cache/cache_test.go | 423 +++++++++++++++--------------------------- cache/errors.go | 11 +- cache/go.mod | 48 +---- cache/go.sum | 250 ------------------------- cache/lru.go | 134 +++++-------- cache/lru_test.go | 402 +++++++++++---------------------------- cache/metrics.go | 38 +--- cache/metrics_test.go | 6 +- cache/store.go | 110 ++--------- 10 files changed, 388 insertions(+), 1201 deletions(-) diff --git a/cache/cache.go b/cache/cache.go index 3e68f733..4f7c6ecf 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -32,22 +32,17 @@ const ( defaultInterval = time.Minute ) -// Cache[T] is a thread-safe in-memory key/object store. -// It can be used to store objects with optional expiration. -// A function to extract the key from the object must be provided. -// Use the New function to create a new cache that is ready to use. +// Cache[T] is a thread-safe in-memory key/value store. +// It can be used to store items with optional expiration. type Cache[T any] struct { *cache[T] - // keyFunc is used to make the key for objects stored in and retrieved from index, and - // should be deterministic. - keyFunc KeyFunc[T] } // item is an item stored in the cache. type item[T any] struct { key string - // object is the item's object. - object T + // value is the item's value. + value T // expiresAt is the item's expiration time. expiresAt time.Time } @@ -61,11 +56,10 @@ type cache[T any] struct { // It is initially true, and set to false when the items are not sorted. sorted bool // capacity is the maximum number of index the cache can hold. - capacity int - metrics *cacheMetrics - labelsFunc GetLvsFunc[T] - janitor *janitor[T] - closed bool + capacity int + metrics *cacheMetrics + janitor *janitor[T] + closed bool mu sync.RWMutex } @@ -73,18 +67,17 @@ type cache[T any] struct { var _ Expirable[any] = &Cache[any]{} // New creates a new cache with the given configuration. -func New[T any](capacity int, keyFunc KeyFunc[T], opts ...Options[T]) (*Cache[T], error) { +func New[T any](capacity int, opts ...Options) (*Cache[T], error) { opt, err := makeOptions(opts...) if err != nil { return nil, fmt.Errorf("failed to apply options: %w", err) } c := &cache[T]{ - index: make(map[string]*item[T]), - items: make([]*item[T], 0, capacity), - sorted: true, - capacity: capacity, - labelsFunc: opt.labelsFunc, + index: make(map[string]*item[T]), + items: make([]*item[T], 0, capacity), + sorted: true, + capacity: capacity, janitor: &janitor[T]{ interval: opt.interval, stop: make(chan bool), @@ -92,10 +85,10 @@ func New[T any](capacity int, keyFunc KeyFunc[T], opts ...Options[T]) (*Cache[T] } if opt.registerer != nil { - c.metrics = newCacheMetrics(opt.registerer, opt.extraLabels...) + c.metrics = newCacheMetrics(opt.registerer) } - C := &Cache[T]{cache: c, keyFunc: keyFunc} + C := &Cache[T]{cache: c} if opt.interval > 0 { go c.janitor.run(c) @@ -104,8 +97,8 @@ func New[T any](capacity int, keyFunc KeyFunc[T], opts ...Options[T]) (*Cache[T] return C, nil } -func makeOptions[T any](opts ...Options[T]) (*storeOptions[T], error) { - opt := storeOptions[T]{} +func makeOptions(opts ...Options) (*storeOptions, error) { + opt := storeOptions{} for _, o := range opts { err := o(&opt) if err != nil { @@ -131,14 +124,8 @@ func (c *Cache[T]) Close() error { } // Set an item in the cache, existing index will be overwritten. -// If the cache is full, Add will return an error. -func (c *Cache[T]) Set(object T) error { - key, err := c.keyFunc(object) - if err != nil { - recordRequest(c.metrics, StatusFailure) - return &CacheError{Reason: ErrInvalidKey, Err: err} - } - +// If the cache is full, an error is returned. +func (c *Cache[T]) Set(key string, value T) error { c.mu.Lock() if c.closed { c.mu.Unlock() @@ -147,14 +134,14 @@ func (c *Cache[T]) Set(object T) error { } _, found := c.index[key] if found { - c.set(key, object) + c.set(key, value) c.mu.Unlock() recordRequest(c.metrics, StatusSuccess) return nil } if c.capacity > 0 && len(c.index) < c.capacity { - c.set(key, object) + c.set(key, value) c.mu.Unlock() recordRequest(c.metrics, StatusSuccess) recordItemIncrement(c.metrics) @@ -165,10 +152,10 @@ func (c *Cache[T]) Set(object T) error { return ErrCacheFull } -func (c *cache[T]) set(key string, object T) { +func (c *cache[T]) set(key string, value T) { item := item[T]{ key: key, - object: object, + value: value, expiresAt: time.Now().Add(noExpiration), } @@ -181,86 +168,41 @@ func (c *cache[T]) set(key string, object T) { c.items = append(c.items, &item) } -// Get an item from the cache. Returns the item or nil, and a bool indicating -// whether the key was found. -func (c *Cache[T]) Get(object T) (item T, exists bool, err error) { - var res T - lvs := []string{} - if c.labelsFunc != nil { - lvs, err = c.labelsFunc(object, len(c.metrics.getExtraLabels())) - if err != nil { - recordRequest(c.metrics, StatusFailure) - return res, false, &CacheError{Reason: ErrInvalidLabels, Err: err} - } - } - key, err := c.keyFunc(object) - if err != nil { - recordRequest(c.metrics, StatusFailure) - return res, false, &CacheError{Reason: ErrInvalidKey, Err: err} - } - item, found, err := c.get(key) - if err != nil { - return res, false, err - } - if !found { - recordEvent(c.metrics, CacheEventTypeMiss, lvs...) - return res, false, nil - } - recordEvent(c.metrics, CacheEventTypeHit, lvs...) - return item, true, nil -} - -// GetByKey returns the object for the given key. -func (c *Cache[T]) GetByKey(key string) (T, bool, error) { - var res T - index, found, err := c.get(key) - if err != nil { - return res, false, err - } - if !found { - recordEvent(c.metrics, CacheEventTypeMiss) - return res, false, nil - } - - recordEvent(c.metrics, CacheEventTypeHit) - return index, true, nil -} - -func (c *cache[T]) get(key string) (T, bool, error) { - var res T +// Get returns a pointer to an item in the cache for the given key. If no item +// is found, it's a nil pointer. +// The caller can record cache hit or miss based on the result with +// Cache.RecordCacheEvent(). +func (c *Cache[T]) Get(key string) (*T, error) { c.mu.RLock() if c.closed { c.mu.RUnlock() recordRequest(c.metrics, StatusFailure) - return res, false, ErrCacheClosed + return nil, ErrCacheClosed } item, found := c.index[key] if !found { c.mu.RUnlock() recordRequest(c.metrics, StatusSuccess) - return res, false, nil + return nil, nil } if !item.expiresAt.IsZero() { if item.expiresAt.Compare(time.Now()) < 0 { c.mu.RUnlock() recordRequest(c.metrics, StatusSuccess) - return res, false, nil + return nil, nil } } c.mu.RUnlock() recordRequest(c.metrics, StatusSuccess) - return item.object, true, nil + // Copy the value to prevent writes to the cached item. + r := item.value + return &r, nil } // Delete an item from the cache. Does nothing if the key is not in the cache. // It actually sets the item expiration to `now“, so that it will be deleted at // the cleanup. -func (c *Cache[T]) Delete(object T) error { - key, err := c.keyFunc(object) - if err != nil { - recordRequest(c.metrics, StatusFailure) - return &CacheError{Reason: ErrInvalidKey, Err: err} - } +func (c *Cache[T]) Delete(key string) error { c.mu.Lock() if c.closed { c.mu.Unlock() @@ -355,13 +297,7 @@ func (c *cache[T]) Resize(size int) (int, error) { } // HasExpired returns true if the item has expired. -func (c *Cache[T]) HasExpired(object T) (bool, error) { - key, err := c.keyFunc(object) - if err != nil { - recordRequest(c.metrics, StatusFailure) - return false, &CacheError{Reason: ErrInvalidKey, Err: err} - } - +func (c *Cache[T]) HasExpired(key string) (bool, error) { c.mu.RLock() if c.closed { c.mu.RUnlock() @@ -387,13 +323,7 @@ func (c *Cache[T]) HasExpired(object T) (bool, error) { } // SetExpiration sets the expiration for the given key. -func (c *Cache[T]) SetExpiration(object T, expiration time.Time) error { - key, err := c.keyFunc(object) - if err != nil { - recordRequest(c.metrics, StatusFailure) - return &CacheError{Reason: ErrInvalidKey, Err: err} - } - +func (c *Cache[T]) SetExpiration(key string, expiration time.Time) error { c.mu.Lock() if c.closed { c.mu.Unlock() @@ -417,12 +347,7 @@ func (c *Cache[T]) SetExpiration(object T, expiration time.Time) error { // GetExpiration returns the expiration for the given key. // Returns zero if the key is not in the cache or the item // has already expired. -func (c *Cache[T]) GetExpiration(object T) (time.Time, error) { - key, err := c.keyFunc(object) - if err != nil { - recordRequest(c.metrics, StatusFailure) - return time.Time{}, &CacheError{Reason: ErrInvalidKey, Err: err} - } +func (c *Cache[T]) GetExpiration(key string) (time.Time, error) { c.mu.RLock() if c.closed { c.mu.RUnlock() @@ -481,6 +406,22 @@ func (c *cache[T]) deleteExpired() { c.mu.Unlock() } +// RecordCacheEvent records a cache event (cache_miss or cache_hit) with kind, +// name and namespace of the associated object being reconciled. +func (c *Cache[T]) RecordCacheEvent(event, kind, name, namespace string) { + if c.metrics != nil { + c.metrics.incCacheEvents(event, kind, name, namespace) + } +} + +// DeleteCacheEvent deletes the cache event (cache_miss or cache_hit) metric for +// the associated object being reconciled, given their kind, name and namespace. +func (c *Cache[T]) DeleteCacheEvent(event, kind, name, namespace string) { + if c.metrics != nil { + c.metrics.deleteCacheEvent(event, kind, name, namespace) + } +} + type janitor[T any] struct { interval time.Duration stop chan bool diff --git a/cache/cache_test.go b/cache/cache_test.go index 62dfe393..ffd5ad9b 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -23,94 +23,68 @@ import ( "testing" "time" - "github.com/fluxcd/cli-utils/pkg/object" . "github.com/onsi/gomega" "github.com/prometheus/client_golang/prometheus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - kc "k8s.io/client-go/tools/cache" ) func TestCache(t *testing.T) { t.Run("Add and update keys", func(t *testing.T) { g := NewWithT(t) // create a cache that can hold 2 items and have no cleanup - cache, err := New(3, kc.MetaNamespaceKeyFunc, - WithMetricsRegisterer[any](prometheus.NewPedanticRegistry()), - WithCleanupInterval[any](1*time.Second)) + cache, err := New[string](3, + WithMetricsRegisterer(prometheus.NewPedanticRegistry()), + WithCleanupInterval(1*time.Second)) g.Expect(err).ToNot(HaveOccurred()) - obj := &metav1.PartialObjectMetadata{ - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test", - }, - } - // Get an Item from the cache - _, found, err := cache.Get(obj) + key1 := "key1" + value1 := "val1" + got, err := cache.Get(key1) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeFalse()) + g.Expect(got).To(BeNil()) // Add an item to the cache - err = cache.Set(obj) + err = cache.Set(key1, value1) g.Expect(err).ToNot(HaveOccurred()) // Get the item from the cache - item, found, err := cache.Get(obj) + got, err = cache.Get(key1) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeTrue()) - g.Expect(item).To(Equal(obj)) + g.Expect(*got).To(Equal(value1)) + + // Writing to the obtained value doesn't update the cache. + *got = "val2" + got2, err := cache.Get(key1) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(*got2).To(Equal(value1)) - obj2 := &metav1.PartialObjectMetadata{ - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test2", - }, - } // Add another item to the cache - err = cache.Set(obj2) + key2 := "key2" + value2 := "val2" + err = cache.Set(key2, value2) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cache.ListKeys()).To(ConsistOf("test-ns/test", "test-ns/test2")) + g.Expect(cache.ListKeys()).To(ConsistOf(key1, key2)) // Get the item from the cache - item, found, err = cache.GetByKey("test-ns/test2") + got, err = cache.Get(key2) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeTrue()) - g.Expect(item).To(Equal(obj2)) - - //Update an item in the cache - obj3 := &metav1.PartialObjectMetadata{ - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test3", - }, - } - err = cache.Set(obj3) + g.Expect(*got).To(Equal(value2)) + + // Update an item in the cache + key3 := "key3" + value3 := "val3" + value4 := "val4" + err = cache.Set(key3, value3) g.Expect(err).ToNot(HaveOccurred()) // Replace an item in the cache - obj3.Labels = map[string]string{"pp.kubernetes.io/created-by: ": "flux"} - err = cache.Set(obj3) + err = cache.Set(key3, value4) g.Expect(err).ToNot(HaveOccurred()) // Get the item from the cache - item, found, err = cache.Get(obj3) + got, err = cache.Get(key3) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeTrue()) - g.Expect(item).To(Equal(obj3)) + g.Expect(*got).To(Equal(value4)) // cleanup the cache cache.Clear() @@ -124,183 +98,121 @@ func TestCache(t *testing.T) { t.Run("Add expiring keys", func(t *testing.T) { g := NewWithT(t) // new cache with a cleanup interval of 1 second - cache, err := New(2, IdentifiableObjectKeyFunc, - WithCleanupInterval[IdentifiableObject](1*time.Second), - WithMetricsRegisterer[IdentifiableObject](prometheus.NewPedanticRegistry())) + + cache, err := New[string](2, + WithCleanupInterval(1*time.Second), + WithMetricsRegisterer(prometheus.NewPedanticRegistry())) g.Expect(err).ToNot(HaveOccurred()) // Add an object representing an expiring token - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: "test", - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: struct { - token string - }{ - token: "test-token", - }, - } + key := "key1" + value := "val1" - err = cache.Set(obj) + err = cache.Set(key, value) g.Expect(err).ToNot(HaveOccurred()) // set expiration time to 2 seconds - err = cache.SetExpiration(obj, time.Now().Add(2*time.Second)) + err = cache.SetExpiration(key, time.Now().Add(2*time.Second)) g.Expect(err).ToNot(HaveOccurred()) // Get the item from the cache - item, found, err := cache.Get(obj) + item, err := cache.Get(key) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeTrue()) - g.Expect(item).To(Equal(obj)) + g.Expect(*item).To(Equal(value)) // wait for the item to expire time.Sleep(3 * time.Second) // Get the item from the cache - item, found, err = cache.Get(obj) + item, err = cache.Get(key) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeFalse()) - g.Expect(item.Object).To(BeNil()) + g.Expect(item).To(BeNil()) }) } -func Test_Cache_Add(t *testing.T) { +func Test_Cache_Set(t *testing.T) { g := NewWithT(t) reg := prometheus.NewPedanticRegistry() - cache, err := New[IdentifiableObject](1, IdentifiableObjectKeyFunc, - WithMetricsRegisterer[IdentifiableObject](reg), - WithCleanupInterval[IdentifiableObject](10*time.Millisecond)) + cache, err := New[string](1, + WithMetricsRegisterer(reg), + WithCleanupInterval(10*time.Millisecond)) g.Expect(err).ToNot(HaveOccurred()) // Add an object representing an expiring token - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: "test", - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: "test-token", - } - err = cache.Set(obj) + key1 := "key1" + value1 := "val1" + err = cache.Set(key1, value1) g.Expect(err).ToNot(HaveOccurred()) - err = cache.SetExpiration(obj, time.Now().Add(10*time.Millisecond)) + err = cache.SetExpiration(key1, time.Now().Add(10*time.Millisecond)) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cache.ListKeys()).To(ConsistOf("test-ns_test_test-group_TestObject")) + g.Expect(cache.ListKeys()).To(ConsistOf(key1)) // try adding the same object again, it should overwrite the existing one - err = cache.Set(obj) + err = cache.Set(key1, value1) g.Expect(err).ToNot(HaveOccurred()) // wait for the item to expire time.Sleep(20 * time.Millisecond) - ok, err := cache.HasExpired(obj) + ok, err := cache.HasExpired(key1) g.Expect(err).ToNot(HaveOccurred()) g.Expect(ok).To(BeTrue()) // add another object - obj.Name = "test2" - err = cache.Set(obj) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cache.ListKeys()).To(ConsistOf("test-ns_test2_test-group_TestObject")) - - // validate metrics - validateMetrics(reg, ` - # HELP gotk_cache_evictions_total Total number of cache evictions. - # TYPE gotk_cache_evictions_total counter - gotk_cache_evictions_total 1 - # HELP gotk_cache_requests_total Total number of cache requests partioned by success or failure. - # TYPE gotk_cache_requests_total counter - gotk_cache_requests_total{status="success"} 7 - # HELP gotk_cached_items Total number of items in the cache. - # TYPE gotk_cached_items gauge - gotk_cached_items 1 -`, t) -} - -func Test_Cache_Update(t *testing.T) { - g := NewWithT(t) - reg := prometheus.NewPedanticRegistry() - cache, err := New[IdentifiableObject](1, IdentifiableObjectKeyFunc, - WithMetricsRegisterer[IdentifiableObject](reg)) - g.Expect(err).ToNot(HaveOccurred()) - - // Add an object representing an expiring token - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: "test", - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: "test-token", - } - err = cache.Set(obj) + key2 := "key2" + value2 := "val2" + err = cache.Set(key2, value2) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cache.ListKeys()).To(ConsistOf("test-ns_test_test-group_TestObject")) + g.Expect(cache.ListKeys()).To(ConsistOf(key2)) - obj.Object = "test-token2" - err = cache.Set(obj) + // Update the value of existing item. + value3 := "val3" + err = cache.Set(key2, value3) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cache.ListKeys()).To(ConsistOf("test-ns_test_test-group_TestObject")) - g.Expect(cache.index["test-ns_test_test-group_TestObject"].object.Object).To(Equal("test-token2")) + g.Expect(cache.ListKeys()).To(ConsistOf(key2)) + g.Expect(cache.index[key2].value).To(Equal(value3)) // validate metrics validateMetrics(reg, ` # HELP gotk_cache_evictions_total Total number of cache evictions. # TYPE gotk_cache_evictions_total counter - gotk_cache_evictions_total 0 + gotk_cache_evictions_total 1 # HELP gotk_cache_requests_total Total number of cache requests partioned by success or failure. # TYPE gotk_cache_requests_total counter - gotk_cache_requests_total{status="success"} 4 + gotk_cache_requests_total{status="success"} 9 # HELP gotk_cached_items Total number of items in the cache. # TYPE gotk_cached_items gauge gotk_cached_items 1 - `, t) + +`, t) } func Test_Cache_Get(t *testing.T) { g := NewWithT(t) reg := prometheus.NewPedanticRegistry() - cache, err := New[IdentifiableObject](5, IdentifiableObjectKeyFunc, - WithMetricsRegisterer[IdentifiableObject](reg), - WithMetricsLabels[IdentifiableObject](IdentifiableObjectLabels, IdentifiableObjectLVSFunc)) + cache, err := New[string](5, WithMetricsRegisterer(reg)) g.Expect(err).ToNot(HaveOccurred()) + // Reconciling object label values for cache event metric. + recObjKind := "TestObject" + recObjName := "test" + recObjNamespace := "test-ns" + // Add an object representing an expiring token - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: "test", - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: "test-token", - } + key := "key1" + value := "val1" - _, found, err := cache.Get(obj) + got, err := cache.Get(key) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeFalse()) + g.Expect(got).To(BeNil()) + cache.RecordCacheEvent(CacheEventTypeMiss, recObjKind, recObjName, recObjNamespace) - err = cache.Set(obj) + err = cache.Set(key, value) g.Expect(err).ToNot(HaveOccurred()) - item, found, err := cache.Get(obj) + got, err = cache.Get(key) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeTrue()) - g.Expect(item).To(Equal(obj)) + g.Expect(*got).To(Equal(value)) + cache.RecordCacheEvent(CacheEventTypeHit, recObjKind, recObjName, recObjNamespace) validateMetrics(reg, ` # HELP gotk_cache_events_total Total number of cache retrieval events for a Gitops Toolkit resource reconciliation. @@ -317,34 +229,41 @@ func Test_Cache_Get(t *testing.T) { # TYPE gotk_cached_items gauge gotk_cached_items 1 `, t) + + cache.DeleteCacheEvent(CacheEventTypeHit, recObjKind, recObjName, recObjNamespace) + cache.DeleteCacheEvent(CacheEventTypeMiss, recObjKind, recObjName, recObjNamespace) + + validateMetrics(reg, ` + # HELP gotk_cache_evictions_total Total number of cache evictions. + # TYPE gotk_cache_evictions_total counter + gotk_cache_evictions_total 0 + # HELP gotk_cache_requests_total Total number of cache requests partioned by success or failure. + # TYPE gotk_cache_requests_total counter + gotk_cache_requests_total{status="success"} 3 + # HELP gotk_cached_items Total number of items in the cache. + # TYPE gotk_cached_items gauge + gotk_cached_items 1 +`, t) + } func Test_Cache_Delete(t *testing.T) { g := NewWithT(t) reg := prometheus.NewPedanticRegistry() - cache, err := New[IdentifiableObject](5, IdentifiableObjectKeyFunc, - WithMetricsRegisterer[IdentifiableObject](reg), - WithCleanupInterval[IdentifiableObject](1*time.Millisecond)) + cache, err := New[string](5, + WithMetricsRegisterer(reg), + WithCleanupInterval(1*time.Millisecond)) g.Expect(err).ToNot(HaveOccurred()) // Add an object representing an expiring token - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: "test", - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: "test-token", - } + key := "key1" + value := "value1" - err = cache.Set(obj) + err = cache.Set(key, value) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cache.ListKeys()).To(ConsistOf("test-ns_test_test-group_TestObject")) + g.Expect(cache.ListKeys()).To(ConsistOf(key)) - err = cache.Delete(obj) + err = cache.Delete(key) g.Expect(err).ToNot(HaveOccurred()) time.Sleep(5 * time.Millisecond) @@ -365,7 +284,8 @@ func Test_Cache_Delete(t *testing.T) { func Test_Cache_deleteExpired(t *testing.T) { type expiringItem struct { - object StoreObject[string] + key string + value string expiresAt time.Time expire bool } @@ -378,17 +298,13 @@ func Test_Cache_deleteExpired(t *testing.T) { name: "non expiring items", items: []expiringItem{ { - object: StoreObject[string]{ - Object: "test-token", - Key: "test", - }, + key: "test", + value: "test-token", expiresAt: time.Now().Add(noExpiration), }, { - object: StoreObject[string]{ - Object: "test-token2", - Key: "test2", - }, + key: "test2", + value: "test-token2", expiresAt: time.Now().Add(noExpiration), }, }, @@ -398,18 +314,14 @@ func Test_Cache_deleteExpired(t *testing.T) { name: "expiring items", items: []expiringItem{ { - object: StoreObject[string]{ - Object: "test-token", - Key: "test", - }, + key: "test", + value: "test-token", expiresAt: time.Now().Add(1 * time.Millisecond), expire: true, }, { - object: StoreObject[string]{ - Object: "test-token2", - Key: "test2", - }, + key: "test2", + value: "test-token2", expiresAt: time.Now().Add(1 * time.Millisecond), expire: true, }, @@ -420,25 +332,19 @@ func Test_Cache_deleteExpired(t *testing.T) { name: "mixed items", items: []expiringItem{ { - object: StoreObject[string]{ - Object: "test-token", - Key: "test", - }, + key: "test", + value: "test-token", expiresAt: time.Now().Add(1 * time.Millisecond), expire: true, }, { - object: StoreObject[string]{ - Object: "test-token2", - Key: "test2", - }, + key: "test2", + value: "test-token2", expiresAt: time.Now().Add(noExpiration), }, { - object: StoreObject[string]{ - Object: "test-token3", - Key: "test3", - }, + key: "test3", + value: "test-token3", expiresAt: time.Now().Add(1 * time.Minute), expire: true, }, @@ -451,16 +357,16 @@ func Test_Cache_deleteExpired(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) reg := prometheus.NewPedanticRegistry() - cache, err := New[StoreObject[string]](5, StoreObjectKeyFunc, - WithMetricsRegisterer[StoreObject[string]](reg), - WithCleanupInterval[StoreObject[string]](1*time.Millisecond)) + cache, err := New[string](5, + WithMetricsRegisterer(reg), + WithCleanupInterval(1*time.Millisecond)) g.Expect(err).ToNot(HaveOccurred()) for _, item := range tt.items { - err := cache.Set(item.object) + err := cache.Set(item.key, item.value) g.Expect(err).ToNot(HaveOccurred()) if item.expire { - err = cache.SetExpiration(item.object, item.expiresAt) + err = cache.SetExpiration(item.key, item.expiresAt) g.Expect(err).ToNot(HaveOccurred()) } } @@ -477,26 +383,18 @@ func Test_Cache_Resize(t *testing.T) { n := 100 g := NewWithT(t) reg := prometheus.NewPedanticRegistry() - cache, err := New[IdentifiableObject](n, IdentifiableObjectKeyFunc, - WithMetricsRegisterer[IdentifiableObject](reg), - WithCleanupInterval[IdentifiableObject](10*time.Millisecond)) + + cache, err := New[string](n, + WithMetricsRegisterer(reg), + WithCleanupInterval(10*time.Millisecond)) g.Expect(err).ToNot(HaveOccurred()) for i := range n { - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: fmt.Sprintf("test-%d", i), - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: "test-token", - } - err = cache.Set(obj) + key := fmt.Sprintf("test-%d", i) + value := "test-token" + err = cache.Set(key, value) g.Expect(err).ToNot(HaveOccurred()) - err = cache.SetExpiration(obj, time.Now().Add(10*time.Minute)) + err = cache.SetExpiration(key, time.Now().Add(10*time.Minute)) g.Expect(err).ToNot(HaveOccurred()) } @@ -520,12 +418,16 @@ func TestCache_Concurrent(t *testing.T) { ) g := NewWithT(t) // create a cache that can hold 10 items and have no cleanup - cache, err := New(10, IdentifiableObjectKeyFunc, - WithCleanupInterval[IdentifiableObject](1*time.Second), - WithMetricsRegisterer[IdentifiableObject](prometheus.NewPedanticRegistry())) + cache, err := New[string](10, + WithCleanupInterval(1*time.Second), + WithMetricsRegisterer(prometheus.NewPedanticRegistry())) g.Expect(err).ToNot(HaveOccurred()) - objmap := createObjectMap(keysNum) + keymap := map[int]string{} + for i := 0; i < keysNum; i++ { + key := fmt.Sprintf("test-%d", i) + keymap[i] = key + } wg := sync.WaitGroup{} run := make(chan bool) @@ -536,13 +438,13 @@ func TestCache_Concurrent(t *testing.T) { wg.Add(2) go func() { defer wg.Done() - _ = cache.Set(objmap[key]) + _ = cache.Set(keymap[key], "test-token") }() go func() { defer wg.Done() <-run - _, _, _ = cache.Get(objmap[key]) - _ = cache.SetExpiration(objmap[key], time.Now().Add(noExpiration)) + _, _ = cache.Get(keymap[key]) + _ = cache.SetExpiration(keymap[key], time.Now().Add(noExpiration)) }() } close(run) @@ -550,35 +452,12 @@ func TestCache_Concurrent(t *testing.T) { keys, err := cache.ListKeys() g.Expect(err).ToNot(HaveOccurred()) - g.Expect(len(keys)).To(Equal(len(objmap))) + g.Expect(len(keys)).To(Equal(len(keymap))) - for _, obj := range objmap { - val, found, err := cache.Get(obj) + for _, key := range keymap { + val, err := cache.Get(key) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeTrue(), "object %s not found", obj.Name) - g.Expect(val).To(Equal(obj)) - } -} - -func createObjectMap(num int) map[int]IdentifiableObject { - objMap := make(map[int]IdentifiableObject) - for i := 0; i < num; i++ { - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: fmt.Sprintf("test-%d", i), - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: struct { - token string - }{ - token: "test-token", - }, - } - objMap[i] = obj + g.Expect(val).ToNot(BeNil(), "object %s not found", key) + g.Expect(*val).To(Equal("test-token")) } - return objMap } diff --git a/cache/errors.go b/cache/errors.go index 2b69055b..b52af260 100644 --- a/cache/errors.go +++ b/cache/errors.go @@ -64,11 +64,8 @@ func (e *CacheError) Unwrap() error { } var ( - ErrNotFound = CacheErrorReason{"NotFound", "object not found"} - ErrAlreadyExists = CacheErrorReason{"AlreadyRxists", "object already exists"} - ErrCacheClosed = CacheErrorReason{"CacheClosed", "cache is closed"} - ErrCacheFull = CacheErrorReason{"CacheFull", "cache is full"} - ErrInvalidSize = CacheErrorReason{"InvalidSize", "invalid size"} - ErrInvalidKey = CacheErrorReason{"InvalidKey", "invalid key"} - ErrInvalidLabels = CacheErrorReason{"InvalidLabels", "invalid labels"} + ErrNotFound = CacheErrorReason{"NotFound", "object not found"} + ErrCacheClosed = CacheErrorReason{"CacheClosed", "cache is closed"} + ErrCacheFull = CacheErrorReason{"CacheFull", "cache is full"} + ErrInvalidSize = CacheErrorReason{"InvalidSize", "invalid size"} ) diff --git a/cache/go.mod b/cache/go.mod index 60b4967c..77d1723d 100644 --- a/cache/go.mod +++ b/cache/go.mod @@ -3,68 +3,24 @@ module github.com/fluxcd/pkg/cache go 1.22.0 require ( - github.com/fluxcd/cli-utils v0.36.0-flux.9 github.com/onsi/gomega v1.34.2 github.com/prometheus/client_golang v1.20.0 - k8s.io/apimachinery v0.31.1 - k8s.io/client-go v0.31.1 ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-errors/errors v1.5.1 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.4 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/x448/float16 v0.8.4 // indirect - github.com/xlab/treeprint v1.2.0 // indirect - go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect golang.org/x/net v0.28.0 // indirect - golang.org/x/oauth2 v0.22.0 // indirect - golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect - golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect - golang.org/x/time v0.6.0 // indirect google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.31.1 // indirect - k8s.io/cli-runtime v0.31.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/kustomize/api v0.17.3 // indirect - sigs.k8s.io/kustomize/kyaml v0.17.2 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/cache/go.sum b/cache/go.sum index f442c017..0899dbca 100644 --- a/cache/go.sum +++ b/cache/go.sum @@ -1,146 +1,32 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= -github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= -github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= -github.com/fluxcd/cli-utils v0.36.0-flux.9 h1:RITKdwIAqT3EFKXl7B91mj6usVjxcy7W8PJZlxqUa84= -github.com/fluxcd/cli-utils v0.36.0-flux.9/go.mod h1:q6lXQpbAlrZmTB4Qe5oAENkv0y2kwMWcqTMDHrRo2Is= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= -github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= -github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= -github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= -github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= -github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI= github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= @@ -149,154 +35,18 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= -github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= -go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= -k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= -k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= -k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/cli-runtime v0.31.1 h1:/ZmKhmZ6hNqDM+yf9s3Y4KEYakNXUn5sod2LWGGwCuk= -k8s.io/cli-runtime v0.31.1/go.mod h1:pKv1cDIaq7ehWGuXQ+A//1OIF+7DI+xudXtExMCbe9U= -k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= -k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= -k8s.io/component-base v0.31.0 h1:/KIzGM5EvPNQcYgwq5NwoQBaOlVFrghoVGr8lG6vNRs= -k8s.io/component-base v0.31.0/go.mod h1:TYVuzI1QmN4L5ItVdMSXKvH7/DtvIuas5/mm8YT3rTo= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/kubectl v0.31.0 h1:kANwAAPVY02r4U4jARP/C+Q1sssCcN/1p9Nk+7BQKVg= -k8s.io/kubectl v0.31.0/go.mod h1:pB47hhFypGsaHAPjlwrNbvhXgmuAr01ZBvAIIUaI8d4= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/kustomize/api v0.17.3 h1:6GCuHSsxq7fN5yhF2XrC+AAr8gxQwhexgHflOAD/JJU= -sigs.k8s.io/kustomize/api v0.17.3/go.mod h1:TuDH4mdx7jTfK61SQ/j1QZM/QWR+5rmEiNjvYlhzFhc= -sigs.k8s.io/kustomize/kyaml v0.17.2 h1:+AzvoJUY0kq4QAhH/ydPHHMRLijtUKiyVyh7fOSshr0= -sigs.k8s.io/kustomize/kyaml v0.17.2/go.mod h1:9V0mCjIEYjlXuCdYsSXvyoy2BTsLESH7TlGV81S282U= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/cache/lru.go b/cache/lru.go index 4f2337b1..96ddb727 100644 --- a/cache/lru.go +++ b/cache/lru.go @@ -24,10 +24,10 @@ import ( // node is a node in a doubly linked list // that is used to implement an LRU cache type node[T any] struct { - object T - key string - prev *node[T] - next *node[T] + value T + key string + prev *node[T] + next *node[T] } func (n *node[T]) addNext(node *node[T]) { @@ -38,7 +38,7 @@ func (n *node[T]) addPrev(node *node[T]) { n.prev = node } -// LRU is a thread-safe in-memory key/object store. +// LRU is a thread-safe in-memory key/value store. // All methods are safe for concurrent use. // All operations are O(1). The hash map lookup is O(1) and so is the doubly // linked list insertion/deletion. @@ -62,25 +62,20 @@ func (n *node[T]) addPrev(node *node[T]) { // │ │ // └───────────────────────────────────────────────────┘ // -// A function to extract the key from the object must be provided. // Use the NewLRU function to create a new cache that is ready to use. type LRU[T any] struct { cache map[string]*node[T] capacity int - // keyFunc is used to make the key for objects stored in and retrieved from items, and - // should be deterministic. - keyFunc KeyFunc[T] - metrics *cacheMetrics - labelsFunc GetLvsFunc[T] - head *node[T] - tail *node[T] - mu sync.RWMutex + metrics *cacheMetrics + head *node[T] + tail *node[T] + mu sync.RWMutex } var _ Store[any] = &LRU[any]{} -// NewLRU creates a new LRU cache with the given capacity and keyFunc. -func NewLRU[T any](capacity int, keyFunc KeyFunc[T], opts ...Options[T]) (*LRU[T], error) { +// NewLRU creates a new LRU cache with the given capacity. +func NewLRU[T any](capacity int, opts ...Options) (*LRU[T], error) { opt, err := makeOptions(opts...) if err != nil { return nil, fmt.Errorf("failed to apply options: %w", err) @@ -92,41 +87,33 @@ func NewLRU[T any](capacity int, keyFunc KeyFunc[T], opts ...Options[T]) (*LRU[T tail.addPrev(head) lru := &LRU[T]{ - cache: make(map[string]*node[T]), - keyFunc: keyFunc, - labelsFunc: opt.labelsFunc, - capacity: capacity, - head: head, - tail: tail, + cache: make(map[string]*node[T]), + capacity: capacity, + head: head, + tail: tail, } if opt.registerer != nil { - lru.metrics = newCacheMetrics(opt.registerer, opt.extraLabels...) + lru.metrics = newCacheMetrics(opt.registerer) } return lru, nil } // Set an item in the cache, existing index will be overwritten. -func (c *LRU[T]) Set(object T) error { - key, err := c.keyFunc(object) - if err != nil { - recordRequest(c.metrics, StatusFailure) - return &CacheError{Reason: ErrInvalidKey, Err: err} - } - +func (c *LRU[T]) Set(key string, value T) error { // if node is already in cache, return error c.mu.Lock() newNode, ok := c.cache[key] if ok { c.delete(newNode) - _ = c.add(&node[T]{key: key, object: object}) + _ = c.add(&node[T]{key: key, value: value}) c.mu.Unlock() recordRequest(c.metrics, StatusSuccess) return nil } - evicted := c.add(&node[T]{key: key, object: object}) + evicted := c.add(&node[T]{key: key, value: value}) c.mu.Unlock() recordRequest(c.metrics, StatusSuccess) if evicted { @@ -154,13 +141,7 @@ func (c *LRU[T]) add(node *node[T]) (evicted bool) { } // Delete removes a node from the list -func (c *LRU[T]) Delete(object T) error { - key, err := c.keyFunc(object) - if err != nil { - recordRequest(c.metrics, StatusFailure) - return &CacheError{Reason: ErrInvalidKey, Err: err} - } - +func (c *LRU[T]) Delete(key string) error { // if node is head or tail, do nothing if key == c.head.key || key == c.tail.key { recordRequest(c.metrics, StatusSuccess) @@ -189,66 +170,25 @@ func (c *LRU[T]) delete(node *node[T]) { delete(c.cache, node.key) } -// Get returns the given object from the cache. -// If the object is not in the cache, it returns false. -func (c *LRU[T]) Get(object T) (item T, exists bool, err error) { - var res T - lvs := []string{} - if c.labelsFunc != nil { - lvs, err = c.labelsFunc(object, len(c.metrics.getExtraLabels())) - if err != nil { - recordRequest(c.metrics, StatusFailure) - return res, false, &CacheError{Reason: ErrInvalidLabels, Err: err} - } - } - key, err := c.keyFunc(object) - if err != nil { - recordRequest(c.metrics, StatusFailure) - return item, false, &CacheError{Reason: ErrInvalidKey, Err: err} - } - - item, exists, err = c.get(key) - if err != nil { - return res, false, ErrInvalidKey - } - if !exists { - recordEvent(c.metrics, CacheEventTypeMiss, lvs...) - return res, false, nil - } - recordEvent(c.metrics, CacheEventTypeHit, lvs...) - return item, true, nil -} - -// GetByKey returns the object for the given key. -func (c *LRU[T]) GetByKey(key string) (T, bool, error) { - var res T - item, found, err := c.get(key) - if err != nil { - return res, false, err - } - if !found { - recordEvent(c.metrics, CacheEventTypeMiss) - return res, false, nil - } - - recordEvent(c.metrics, CacheEventTypeHit) - return item, true, nil -} - -func (c *LRU[T]) get(key string) (item T, exists bool, err error) { - var res T +// Get returns a pointer to an item in the cache for the given key. If no item +// is found, it's a nil pointer. +// The caller can record cache hit or miss based on the result with +// LRU.RecordCacheEvent(). +func (c *LRU[T]) Get(key string) (*T, error) { c.mu.Lock() node, ok := c.cache[key] if !ok { c.mu.Unlock() recordRequest(c.metrics, StatusSuccess) - return res, false, nil + return nil, nil } c.delete(node) _ = c.add(node) c.mu.Unlock() recordRequest(c.metrics, StatusSuccess) - return node.object, true, nil + // Copy the value to prevent writes to the cached item. + r := node.value + return &r, nil } // ListKeys returns a list of keys in the cache. @@ -288,3 +228,19 @@ func (c *LRU[T]) Resize(size int) (int, error) { recordRequest(c.metrics, StatusSuccess) return overflow, nil } + +// RecordCacheEvent records a cache event (cache_miss or cache_hit) with kind, +// name and namespace of the associated object being reconciled. +func (c *LRU[T]) RecordCacheEvent(event, kind, name, namespace string) { + if c.metrics != nil { + c.metrics.incCacheEvents(event, kind, name, namespace) + } +} + +// DeleteCacheEvent deletes the cache event (cache_miss or cache_hit) metric for +// the associated object being reconciled, given their kind, name and namespace. +func (c *LRU[T]) DeleteCacheEvent(event, kind, name, namespace string) { + if c.metrics != nil { + c.metrics.deleteCacheEvent(event, kind, name, namespace) + } +} diff --git a/cache/lru_test.go b/cache/lru_test.go index e484171e..f911911c 100644 --- a/cache/lru_test.go +++ b/cache/lru_test.go @@ -22,195 +22,57 @@ import ( "sync" "testing" - "github.com/fluxcd/cli-utils/pkg/object" . "github.com/onsi/gomega" "github.com/prometheus/client_golang/prometheus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - kc "k8s.io/client-go/tools/cache" ) func Test_LRU(t *testing.T) { + type keyVal struct { + key string + value string + } testCases := []struct { name string - inputs []*metav1.PartialObjectMetadata - expectedCache map[string]*node[metav1.PartialObjectMetadata] + inputs []keyVal + expectedCache map[string]*node[string] }{ { name: "empty cache", - inputs: []*metav1.PartialObjectMetadata{}, - expectedCache: map[string]*node[metav1.PartialObjectMetadata]{}, + inputs: []keyVal{}, + expectedCache: map[string]*node[string]{}, }, { name: "add one node", - inputs: []*metav1.PartialObjectMetadata{ + inputs: []keyVal{ { - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test", - }, + key: "test", + value: "test-token", }, }, - expectedCache: map[string]*node[metav1.PartialObjectMetadata]{ - "test-ns/test": { - object: metav1.PartialObjectMetadata{ - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test", - }, - }, - key: "test-ns/test", + expectedCache: map[string]*node[string]{ + "test": { + key: "test", + value: "test-token", }, }, }, { name: "add seven nodes", - inputs: []*metav1.PartialObjectMetadata{ - { - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test", - }, - }, - { - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test2", - }, - }, - { - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test3", - }, - }, - { - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test4", - }, - }, - { - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test5", - }, - }, - { - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test6", - }, - }, - { - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test7", - }, - }, + inputs: []keyVal{ + {key: "test", value: "test-token"}, + {key: "test2", value: "test-token"}, + {key: "test3", value: "test-token"}, + {key: "test4", value: "test-token"}, + {key: "test5", value: "test-token"}, + {key: "test6", value: "test-token"}, + {key: "test7", value: "test-token"}, }, - expectedCache: map[string]*node[metav1.PartialObjectMetadata]{ - "test-ns/test3": { - object: metav1.PartialObjectMetadata{ - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test3", - }, - }, - key: "test-ns/test3", - }, - "test-ns/test4": { - object: metav1.PartialObjectMetadata{ - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test4", - }, - }, - key: "test-ns/test4", - }, - "test-ns/test5": { - object: metav1.PartialObjectMetadata{ - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test5", - }, - }, - key: "test-ns/test5", - }, - "test-ns/test6": { - object: metav1.PartialObjectMetadata{ - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test6", - }, - }, - key: "test-ns/test6", - }, - "test-ns/test7": { - object: metav1.PartialObjectMetadata{ - TypeMeta: metav1.TypeMeta{ - Kind: "TestObject", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-ns", - Name: "test7", - }, - }, - key: "test-ns/test7", - }, + expectedCache: map[string]*node[string]{ + "test3": {key: "test3", value: "test-token"}, + "test4": {key: "test4", value: "test-token"}, + "test5": {key: "test5", value: "test-token"}, + "test6": {key: "test6", value: "test-token"}, + "test7": {key: "test7", value: "test-token"}, }, }, } @@ -218,11 +80,11 @@ func Test_LRU(t *testing.T) { for _, v := range testCases { t.Run(v.name, func(t *testing.T) { g := NewWithT(t) - cache, err := NewLRU(5, kc.MetaNamespaceKeyFunc, - WithMetricsRegisterer[any](prometheus.NewPedanticRegistry())) + cache, err := NewLRU[string](5, + WithMetricsRegisterer(prometheus.NewPedanticRegistry())) g.Expect(err).ToNot(HaveOccurred()) for _, input := range v.inputs { - err := cache.Set(input) + err := cache.Set(input.key, input.value) g.Expect(err).ToNot(HaveOccurred()) } @@ -237,38 +99,37 @@ func Test_LRU(t *testing.T) { } } -func Test_LRU_Add(t *testing.T) { +func Test_LRU_Set(t *testing.T) { g := NewWithT(t) reg := prometheus.NewPedanticRegistry() - cache, err := NewLRU[IdentifiableObject](1, IdentifiableObjectKeyFunc, - WithMetricsRegisterer[IdentifiableObject](reg)) + cache, err := NewLRU[string](1, + WithMetricsRegisterer(reg)) g.Expect(err).ToNot(HaveOccurred()) // Add an object representing an expiring token - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: "test", - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: "test-token", - } - err = cache.Set(obj) + key1 := "key1" + value1 := "val1" + err = cache.Set(key1, value1) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cache.ListKeys()).To(ConsistOf("test-ns_test_test-group_TestObject")) + g.Expect(cache.ListKeys()).To(ConsistOf(key1)) // try adding the same object again, it should overwrite the existing one - err = cache.Set(obj) + err = cache.Set(key1, value1) g.Expect(err).ToNot(HaveOccurred()) // add another object - obj.Name = "test2" - err = cache.Set(obj) + key2 := "key2" + value2 := "val2" + err = cache.Set(key2, value2) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cache.ListKeys()).To(ConsistOf(key2)) + + // Update the value of existing item. + value3 := "val3" + err = cache.Set(key2, value3) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cache.ListKeys()).To(ConsistOf("test-ns_test2_test-group_TestObject")) + g.Expect(cache.ListKeys()).To(ConsistOf(key2)) + g.Expect(cache.cache[key2].value).To(Equal(value3)) // validate metrics validateMetrics(reg, ` @@ -277,88 +138,40 @@ func Test_LRU_Add(t *testing.T) { gotk_cache_evictions_total 1 # HELP gotk_cache_requests_total Total number of cache requests partioned by success or failure. # TYPE gotk_cache_requests_total counter - gotk_cache_requests_total{status="success"} 5 + gotk_cache_requests_total{status="success"} 7 # HELP gotk_cached_items Total number of items in the cache. # TYPE gotk_cached_items gauge gotk_cached_items 1 `, t) } -func Test_LRU_Update(t *testing.T) { - g := NewWithT(t) - reg := prometheus.NewPedanticRegistry() - cache, err := NewLRU[IdentifiableObject](1, IdentifiableObjectKeyFunc, - WithMetricsRegisterer[IdentifiableObject](reg)) - g.Expect(err).ToNot(HaveOccurred()) - - // Add an object representing an expiring token - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: "test", - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: "test-token", - } - err = cache.Set(obj) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cache.ListKeys()).To(ConsistOf("test-ns_test_test-group_TestObject")) - - obj.Object = "test-token2" - err = cache.Set(obj) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(cache.ListKeys()).To(ConsistOf("test-ns_test_test-group_TestObject")) - g.Expect(cache.cache["test-ns_test_test-group_TestObject"].object.Object).To(Equal("test-token2")) - - // validate metrics - validateMetrics(reg, ` - # HELP gotk_cache_evictions_total Total number of cache evictions. - # TYPE gotk_cache_evictions_total counter - gotk_cache_evictions_total 0 - # HELP gotk_cache_requests_total Total number of cache requests partioned by success or failure. - # TYPE gotk_cache_requests_total counter - gotk_cache_requests_total{status="success"} 4 - # HELP gotk_cached_items Total number of items in the cache. - # TYPE gotk_cached_items gauge - gotk_cached_items 1 - `, t) -} - func Test_LRU_Get(t *testing.T) { g := NewWithT(t) reg := prometheus.NewPedanticRegistry() - cache, err := NewLRU[IdentifiableObject](5, IdentifiableObjectKeyFunc, - WithMetricsRegisterer[IdentifiableObject](reg), - WithMetricsLabels[IdentifiableObject](IdentifiableObjectLabels, IdentifiableObjectLVSFunc)) + cache, err := NewLRU[string](5, + WithMetricsRegisterer(reg)) g.Expect(err).ToNot(HaveOccurred()) - // Add an object representing an expiring token - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: "test", - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: "test-token", - } + // Reconciling object label values for cache event metric. + recObjKind := "TestObject" + recObjName := "test" + recObjNamespace := "test-ns" - _, found, err := cache.Get(obj) + // Add an object representing an expiring token + key1 := "key1" + value1 := "val1" + got, err := cache.Get(key1) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeFalse()) + g.Expect(got).To(BeNil()) + cache.RecordCacheEvent(CacheEventTypeMiss, recObjKind, recObjName, recObjNamespace) - err = cache.Set(obj) + err = cache.Set(key1, value1) g.Expect(err).ToNot(HaveOccurred()) - item, found, err := cache.Get(obj) + got, err = cache.Get(key1) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeTrue()) - g.Expect(item).To(Equal(obj)) + g.Expect(*got).To(Equal(value1)) + cache.RecordCacheEvent(CacheEventTypeHit, recObjKind, recObjName, recObjNamespace) validateMetrics(reg, ` # HELP gotk_cache_events_total Total number of cache retrieval events for a Gitops Toolkit resource reconciliation. @@ -375,32 +188,37 @@ func Test_LRU_Get(t *testing.T) { # TYPE gotk_cached_items gauge gotk_cached_items 1 `, t) + + cache.DeleteCacheEvent(CacheEventTypeHit, recObjKind, recObjName, recObjNamespace) + cache.DeleteCacheEvent(CacheEventTypeMiss, recObjKind, recObjName, recObjNamespace) + + validateMetrics(reg, ` + # HELP gotk_cache_evictions_total Total number of cache evictions. + # TYPE gotk_cache_evictions_total counter + gotk_cache_evictions_total 0 + # HELP gotk_cache_requests_total Total number of cache requests partioned by success or failure. + # TYPE gotk_cache_requests_total counter + gotk_cache_requests_total{status="success"} 3 + # HELP gotk_cached_items Total number of items in the cache. + # TYPE gotk_cached_items gauge + gotk_cached_items 1 +`, t) } func Test_LRU_Delete(t *testing.T) { g := NewWithT(t) reg := prometheus.NewPedanticRegistry() - cache, err := NewLRU[IdentifiableObject](5, IdentifiableObjectKeyFunc, - WithMetricsRegisterer[IdentifiableObject](reg)) + cache, err := NewLRU[string](5, + WithMetricsRegisterer(reg)) g.Expect(err).ToNot(HaveOccurred()) // Add an object representing an expiring token - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: "test", - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: "test-token", - } - - err = cache.Set(obj) + key := "key1" + value := "val1" + err = cache.Set(key, value) g.Expect(err).ToNot(HaveOccurred()) - err = cache.Delete(obj) + err = cache.Delete(key) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cache.ListKeys()).To(BeEmpty()) @@ -421,23 +239,13 @@ func Test_LRU_Resize(t *testing.T) { n := 100 g := NewWithT(t) reg := prometheus.NewPedanticRegistry() - cache, err := NewLRU[IdentifiableObject](n, IdentifiableObjectKeyFunc, - WithMetricsRegisterer[IdentifiableObject](reg)) + cache, err := NewLRU[string](n, + WithMetricsRegisterer(reg)) g.Expect(err).ToNot(HaveOccurred()) for i := range n { - obj := IdentifiableObject{ - ObjMetadata: object.ObjMetadata{ - Namespace: "test-ns", - Name: fmt.Sprintf("test-%d", i), - GroupKind: schema.GroupKind{ - Group: "test-group", - Kind: "TestObject", - }, - }, - Object: "test-token", - } - err = cache.Set(obj) + key := fmt.Sprintf("test-%d", i) + err = cache.Set(key, "test-token") g.Expect(err).ToNot(HaveOccurred()) } @@ -461,11 +269,15 @@ func TestLRU_Concurrent(t *testing.T) { ) g := NewWithT(t) // create a cache that can hold 10 items and have no cleanup - cache, err := NewLRU(10, IdentifiableObjectKeyFunc, - WithMetricsRegisterer[IdentifiableObject](prometheus.NewPedanticRegistry())) + cache, err := NewLRU[string](10, + WithMetricsRegisterer(prometheus.NewPedanticRegistry())) g.Expect(err).ToNot(HaveOccurred()) - objmap := createObjectMap(keysNum) + keymap := map[int]string{} + for i := 0; i < keysNum; i++ { + key := fmt.Sprintf("test-%d", i) + keymap[i] = key + } wg := sync.WaitGroup{} run := make(chan bool) @@ -476,12 +288,12 @@ func TestLRU_Concurrent(t *testing.T) { wg.Add(2) go func() { defer wg.Done() - _ = cache.Set(objmap[key]) + _ = cache.Set(keymap[key], "test-token") }() go func() { defer wg.Done() <-run - _, _, _ = cache.Get(objmap[key]) + _, _ = cache.Get(keymap[key]) }() } close(run) @@ -489,12 +301,12 @@ func TestLRU_Concurrent(t *testing.T) { keys, err := cache.ListKeys() g.Expect(err).ToNot(HaveOccurred()) - g.Expect(len(keys)).To(Equal(len(objmap))) + g.Expect(len(keys)).To(Equal(len(keymap))) - for _, obj := range objmap { - val, found, err := cache.Get(obj) + for _, key := range keymap { + val, err := cache.Get(key) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(found).To(BeTrue(), "object %s not found", obj.Name) - g.Expect(val).To(Equal(obj)) + g.Expect(val).ToNot(BeNil(), "object %s not found", key) + g.Expect(*val).To(Equal("test-token")) } } diff --git a/cache/metrics.go b/cache/metrics.go index c6dd29ea..bf0cbed8 100644 --- a/cache/metrics.go +++ b/cache/metrics.go @@ -17,8 +17,6 @@ limitations under the License. package cache import ( - "fmt" - "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) @@ -40,12 +38,11 @@ type cacheMetrics struct { cacheItemsGauge prometheus.Gauge cacheRequestsCounter *prometheus.CounterVec cacheEvictionCounter prometheus.Counter - extraLabels []string } // newcacheMetrics returns a new cacheMetrics. -func newCacheMetrics(reg prometheus.Registerer, extraLabels ...string) *cacheMetrics { - labels := append([]string{"event_type"}, extraLabels...) +func newCacheMetrics(reg prometheus.Registerer) *cacheMetrics { + labels := []string{"event_type", "kind", "name", "namespace"} return &cacheMetrics{ cacheEventsCounter: promauto.With(reg).NewCounterVec( prometheus.CounterOpts{ @@ -73,14 +70,9 @@ func newCacheMetrics(reg prometheus.Registerer, extraLabels ...string) *cacheMet Help: "Total number of cache evictions.", }, ), - extraLabels: extraLabels, } } -func (m *cacheMetrics) getExtraLabels() []string { - return m.extraLabels -} - // collectors returns the metrics.Collector objects for the cacheMetrics. func (m *cacheMetrics) collectors() []prometheus.Collector { return []prometheus.Collector{ @@ -151,34 +143,8 @@ func recordDecrement(metrics *cacheMetrics) { } } -func recordEvent(metrics *cacheMetrics, event string, lvs ...string) { - if metrics != nil { - metrics.incCacheEvents(event, lvs...) - } -} - func recordItemIncrement(metrics *cacheMetrics) { if metrics != nil { metrics.incCacheItems() } } - -// IdentifiableObjectLabels are the labels for an IdentifiableObject. -var IdentifiableObjectLabels []string = []string{"name", "namespace", "kind"} - -// GetLvsFunc is a function that returns the label's values for a metric. -type GetLvsFunc[T any] func(obj T, cardinality int) ([]string, error) - -// IdentifiableObjectLVSFunc returns the label's values for a metric for an IdentifiableObject. -func IdentifiableObjectLVSFunc[T any](object T, cardinality int) ([]string, error) { - n, ok := any(object).(IdentifiableObject) - if !ok { - return nil, fmt.Errorf("object is not an IdentifiableObject") - } - lvs := []string{n.Name, n.Namespace, n.GroupKind.Kind} - if len(lvs) != cardinality { - return nil, fmt.Errorf("expected cardinality %d, got %d", cardinality, len(lvs)) - } - - return []string{n.Name, n.Namespace, n.GroupKind.Kind}, nil -} diff --git a/cache/metrics_test.go b/cache/metrics_test.go index 5a2d90b8..c2291687 100644 --- a/cache/metrics_test.go +++ b/cache/metrics_test.go @@ -28,12 +28,12 @@ import ( func TestCacheMetrics(t *testing.T) { g := NewWithT(t) reg := prometheus.NewPedanticRegistry() - m := newCacheMetrics(reg, IdentifiableObjectLabels...) + m := newCacheMetrics(reg) g.Expect(m).ToNot(BeNil()) // CounterVec is a collection of counters and is not exported until it has counters in it. - m.incCacheEvents(CacheEventTypeHit, []string{"test", "test-ns", "TestObject"}...) - m.incCacheEvents(CacheEventTypeMiss, []string{"test", "test-ns", "TestObject"}...) + m.incCacheEvents(CacheEventTypeHit, []string{"TestObject", "test", "test-ns"}...) + m.incCacheEvents(CacheEventTypeMiss, []string{"TestObject", "test", "test-ns"}...) m.incCacheRequests("success") m.incCacheRequests("failure") diff --git a/cache/store.go b/cache/store.go index ba80e232..464cba3e 100644 --- a/cache/store.go +++ b/cache/store.go @@ -17,122 +17,52 @@ limitations under the License. package cache import ( - "fmt" "time" - "github.com/fluxcd/cli-utils/pkg/object" "github.com/prometheus/client_golang/prometheus" ) // Store is an interface for a cache store. -// It is a generic version of the Kubernetes client-go cache.Store interface. -// See https://pkg.go.dev/k8s.io/client-go/tools/cache#Store -// The implementation should know how to extract a key from an object. type Store[T any] interface { - // Set adds an object to the store. - // It will overwrite the item if it already exists. - Set(object T) error - // Delete deletes an object from the store. - Delete(object T) error - // ListKeys returns a list of keys in the store. - ListKeys() ([]string, error) - // Get returns the object stored in the store. - Get(object T) (item T, exists bool, err error) - // GetByKey returns the object stored in the store by key. - GetByKey(key string) (item T, exists bool, err error) - // Resize resizes the store and returns the number of items removed. - Resize(int) (int, error) + // Set adds an item to the store for the given key. + Set(key string, value T) error + // Get returns an item stored in the store for the given key. + Get(key string) (*T, error) + // Delete deletes an item in the store for the given key. + Delete(key string) error } // Expirable is an interface for a cache store that supports expiration. type Expirable[T any] interface { Store[T] - // SetExpiration sets the expiration time for the object. - SetExpiration(object T, expiresAt time.Time) error - // GetExpiration returns the expiration time for the object in unix time. - GetExpiration(object T) (time.Time, error) - // HasExpired returns true if the object has expired. - HasExpired(object T) (bool, error) + // SetExpiration sets the expiration time for a cached item. + SetExpiration(key string, expiresAt time.Time) error + // GetExpiration returns the expiration time of an item. + GetExpiration(key string) (time.Time, error) + // HasExpired returns if an item has expired. + HasExpired(key string) (bool, error) } -type storeOptions[T any] struct { - interval time.Duration - registerer prometheus.Registerer - extraLabels []string - labelsFunc GetLvsFunc[T] +type storeOptions struct { + interval time.Duration + registerer prometheus.Registerer } // Options is a function that sets the store options. -type Options[T any] func(*storeOptions[T]) error - -// WithMetricsLabels sets the extra labels for the cache metrics. -func WithMetricsLabels[T any](labels []string, f GetLvsFunc[T]) Options[T] { - return func(o *storeOptions[T]) error { - if labels != nil && f == nil { - return fmt.Errorf("labelsFunc must be set if labels are provided") - } - o.extraLabels = labels - o.labelsFunc = f - return nil - } -} +type Options func(*storeOptions) error // WithCleanupInterval sets the interval for the cache cleanup. -func WithCleanupInterval[T any](interval time.Duration) Options[T] { - return func(o *storeOptions[T]) error { +func WithCleanupInterval(interval time.Duration) Options { + return func(o *storeOptions) error { o.interval = interval return nil } } // WithMetricsRegisterer sets the Prometheus registerer for the cache metrics. -func WithMetricsRegisterer[T any](r prometheus.Registerer) Options[T] { - return func(o *storeOptions[T]) error { +func WithMetricsRegisterer(r prometheus.Registerer) Options { + return func(o *storeOptions) error { o.registerer = r return nil } } - -// KeyFunc knows how to make a key from an object. Implementations should be deterministic. -type KeyFunc[T any] func(object T) (string, error) - -// IdentifiableObject is a wrapper for an object with its identifying metadata. -type IdentifiableObject struct { - object.ObjMetadata - // Object is the object that is being stored. - Object any -} - -// ExplicitKey can be passed to IdentifiableObjectKeyFunc if you have the key for -// the objectec but not the object itself. -type ExplicitKey string - -// IdentifiableObjectKeyFunc is a convenient default KeyFunc which knows how to make -// keys from IdentifiableObject objects. -func IdentifiableObjectKeyFunc[T any](object T) (string, error) { - if key, ok := any(object).(ExplicitKey); ok { - return string(key), nil - } - n, ok := any(object).(IdentifiableObject) - if !ok { - return "", fmt.Errorf("object has no meta: %v", object) - } - return n.String(), nil -} - -// StoreObject is a wrapper for an object with its identifying key. -// It is used to store objects in a Store. -// This helper is useful when the object does not have metadata to extract the key from. -// The supplied key can be retrieved with the StoreObjectKeyFunc. -// When the object has metadata, use IdentifiableObject instead if possible. -type StoreObject[T any] struct { - // Object is the object that is being stored. - Object T - // Key is the key for the object. - Key string -} - -// StoreObjectKeyFunc returns the key for a StoreObject. -func StoreObjectKeyFunc[T any](object StoreObject[T]) (string, error) { - return object.Key, nil -}