From 4ef9a53a2e11569bc0dcf5941f5c968b7001ddd6 Mon Sep 17 00:00:00 2001 From: Cerek Hillen Date: Fri, 16 Jun 2023 16:46:37 -0700 Subject: [PATCH 01/11] replace sync.Map with lru.Cache --- go.mod | 5 ++- go.sum | 2 ++ stats.go | 94 +++++++++++++++++++++++++++++++++------------------ stats_test.go | 6 ++-- 4 files changed, 71 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index 75bdc60..741080a 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/lyft/gostats go 1.18 -require github.com/kelseyhightower/envconfig v1.4.0 +require ( + github.com/hashicorp/golang-lru/v2 v2.0.3 + github.com/kelseyhightower/envconfig v1.4.0 +) diff --git a/go.sum b/go.sum index 8642a1a..85d02c3 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/hashicorp/golang-lru/v2 v2.0.3 h1:kmRrRLlInXvng0SmLxmQpQkpbYAvcXm7NPDrgxJa9mE= +github.com/hashicorp/golang-lru/v2 v2.0.3/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= diff --git a/stats.go b/stats.go index 4143dc6..f5ee002 100644 --- a/stats.go +++ b/stats.go @@ -7,6 +7,8 @@ import ( "sync/atomic" "time" + "github.com/hashicorp/golang-lru/v2" + tagspkg "github.com/lyft/gostats/internal/tags" ) @@ -214,7 +216,18 @@ type StatGenerator interface { // NewStore returns an Empty store that flushes to Sink passed as an argument. // Note: the export argument is unused. func NewStore(sink Sink, _ bool) Store { - return &statStore{sink: sink} + // TODO(crockeo): decide how this should be configurable + cacheSize := 8192 + + s := &statStore{sink: sink} + + // lru.NewWithEvict can only return a non-nil error when cacheSize < 0, + // so it's safe to ignore. + s.counters, _ = lru.NewWithEvict(cacheSize, s.flushCounter) + s.gauges, _ = lru.NewWithEvict(cacheSize, s.flushGauge) + s.timers, _ = lru.New[string, *timer](cacheSize) + + return s } // NewDefaultStore returns a Store with a TCP statsd sink, and a running flush timer. @@ -336,9 +349,9 @@ func (ts *timespan) CompleteWithDuration(value time.Duration) { } type statStore struct { - counters sync.Map - gauges sync.Map - timers sync.Map + counters *lru.Cache[string, *counter] + gauges *lru.Cache[string, *gauge] + timers *lru.Cache[string, *timer] mu sync.RWMutex statGenerators []StatGenerator @@ -369,18 +382,25 @@ func (s *statStore) Flush() { } s.mu.RUnlock() - s.counters.Range(func(key, v interface{}) bool { - // do not flush counters that are set to zero - if value := v.(*counter).latch(); value != 0 { - s.sink.FlushCounter(key.(string), value) + for _, name := range s.counters.Keys() { + counter, ok := s.counters.Peek(name) + if !ok { + // This counter was removed between retrieving the names + // and finding this specific counter. + continue } - return true - }) + s.flushCounter(name, counter) + } - s.gauges.Range(func(key, v interface{}) bool { - s.sink.FlushGauge(key.(string), v.(*gauge).Value()) - return true - }) + for _, name := range s.gauges.Keys() { + gauge, ok := s.gauges.Peek(name) + if !ok { + // This gauge was removed between retrieving the names + // and finding this specific gauge. + continue + } + s.flushGauge(name, gauge) + } flushableSink, ok := s.sink.(FlushableSink) if ok { @@ -388,6 +408,16 @@ func (s *statStore) Flush() { } } +func (s *statStore) flushCounter(name string, counter *counter) { + if value := counter.latch(); value != 0 { + s.sink.FlushCounter(name, value) + } +} + +func (s *statStore) flushGauge(key string, gauge *gauge) { + s.sink.FlushGauge(key, gauge.Value()) +} + func (s *statStore) AddStatGenerator(statGenerator StatGenerator) { s.mu.Lock() defer s.mu.Unlock() @@ -407,14 +437,14 @@ func (s *statStore) ScopeWithTags(name string, tags map[string]string) Scope { } func (s *statStore) newCounter(serializedName string) *counter { - if v, ok := s.counters.Load(serializedName); ok { - return v.(*counter) + if counter, ok := s.counters.Get(serializedName); ok { + return counter } - c := new(counter) - if v, loaded := s.counters.LoadOrStore(serializedName, c); loaded { - return v.(*counter) + counter := new(counter) + if existingCounter, ok, _ := s.counters.PeekOrAdd(serializedName, counter); ok { + return existingCounter } - return c + return counter } func (s *statStore) NewCounter(name string) Counter { @@ -442,14 +472,14 @@ func (s *statStore) NewPerInstanceCounter(name string, tags map[string]string) C } func (s *statStore) newGauge(serializedName string) *gauge { - if v, ok := s.gauges.Load(serializedName); ok { - return v.(*gauge) + if gauge, ok := s.gauges.Get(serializedName); ok { + return gauge } - g := new(gauge) - if v, loaded := s.gauges.LoadOrStore(serializedName, g); loaded { - return v.(*gauge) + gauge := new(gauge) + if existingGauge, ok, _ := s.gauges.PeekOrAdd(serializedName, gauge); ok { + return existingGauge } - return g + return gauge } func (s *statStore) NewGauge(name string) Gauge { @@ -475,14 +505,14 @@ func (s *statStore) NewPerInstanceGauge(name string, tags map[string]string) Gau } func (s *statStore) newTimer(serializedName string, base time.Duration) *timer { - if v, ok := s.timers.Load(serializedName); ok { - return v.(*timer) + if timer, ok := s.timers.Get(serializedName); ok { + return timer } - t := &timer{name: serializedName, sink: s.sink, base: base} - if v, loaded := s.timers.LoadOrStore(serializedName, t); loaded { - return v.(*timer) + timer := &timer{name: serializedName, sink: s.sink, base: base} + if existingTimer, ok, _ := s.timers.PeekOrAdd(serializedName, timer); ok { + return existingTimer } - return t + return timer } func (s *statStore) NewMilliTimer(name string) Timer { diff --git a/stats_test.go b/stats_test.go index 66ca14f..b2a16d2 100644 --- a/stats_test.go +++ b/stats_test.go @@ -173,8 +173,8 @@ func TestTagMapNotModified(t *testing.T) { } scopeGenerators := map[string]func() Scope{ - "statStore": func() Scope { return &statStore{} }, - "subScope": func() Scope { return newSubScope(&statStore{}, "name", nil) }, + "statStore": func() Scope { return NewStore(nil, false) }, + "subScope": func() Scope { return newSubScope(NewStore(nil, false).(*statStore), "name", nil) }, } methodTestCases := map[string]TagMethod{ @@ -333,7 +333,7 @@ func TestPerInstanceStats(t *testing.T) { testPerInstanceMethods := func(t *testing.T, setupScope func(Scope) Scope) { for _, x := range testCases { sink := mock.NewSink() - scope := setupScope(&statStore{sink: sink}) + scope := setupScope(NewStore(sink, false).(*statStore)) scope.NewPerInstanceCounter("name", x.tags).Inc() scope.NewPerInstanceGauge("name", x.tags).Inc() From f766165028ab44c7458b192f7ae09b5b3b4285c1 Mon Sep 17 00:00:00 2001 From: Cerek Hillen Date: Tue, 20 Jun 2023 14:02:33 -0700 Subject: [PATCH 02/11] revert gauges --- stats.go | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/stats.go b/stats.go index f5ee002..81b1d09 100644 --- a/stats.go +++ b/stats.go @@ -221,10 +221,8 @@ func NewStore(sink Sink, _ bool) Store { s := &statStore{sink: sink} - // lru.NewWithEvict can only return a non-nil error when cacheSize < 0, - // so it's safe to ignore. + // lru.NewWithEvict can only return a non-nil error when cacheSize < 0. s.counters, _ = lru.NewWithEvict(cacheSize, s.flushCounter) - s.gauges, _ = lru.NewWithEvict(cacheSize, s.flushGauge) s.timers, _ = lru.New[string, *timer](cacheSize) return s @@ -350,8 +348,10 @@ func (ts *timespan) CompleteWithDuration(value time.Duration) { type statStore struct { counters *lru.Cache[string, *counter] - gauges *lru.Cache[string, *gauge] timers *lru.Cache[string, *timer] + // Gauges must not be expunged because they are client-side stateful. + // We use a sync.Map instead of a cache to ensure they are kept indefinitely. + gauges sync.Map mu sync.RWMutex statGenerators []StatGenerator @@ -392,15 +392,10 @@ func (s *statStore) Flush() { s.flushCounter(name, counter) } - for _, name := range s.gauges.Keys() { - gauge, ok := s.gauges.Peek(name) - if !ok { - // This gauge was removed between retrieving the names - // and finding this specific gauge. - continue - } - s.flushGauge(name, gauge) - } + s.gauges.Range(func (key any, v any) bool { + s.sink.FlushGauge(key.(string), v.(*gauge).Value()) + return true + }) flushableSink, ok := s.sink.(FlushableSink) if ok { @@ -472,14 +467,14 @@ func (s *statStore) NewPerInstanceCounter(name string, tags map[string]string) C } func (s *statStore) newGauge(serializedName string) *gauge { - if gauge, ok := s.gauges.Get(serializedName); ok { - return gauge + if v, ok := s.gauges.Load(serializedName); ok { + return v.(*gauge) } - gauge := new(gauge) - if existingGauge, ok, _ := s.gauges.PeekOrAdd(serializedName, gauge); ok { - return existingGauge + g := new(gauge) + if v, loaded := s.gauges.LoadOrStore(serializedName, g); loaded { + return v.(*gauge) } - return gauge + return g } func (s *statStore) NewGauge(name string) Gauge { From a538625d3f863b636478291a7a7ae71fd164b704 Mon Sep 17 00:00:00 2001 From: Cerek Hillen Date: Tue, 20 Jun 2023 14:05:15 -0700 Subject: [PATCH 03/11] clean up unnecessary diffs --- stats.go | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/stats.go b/stats.go index 81b1d09..9f62ed3 100644 --- a/stats.go +++ b/stats.go @@ -409,10 +409,6 @@ func (s *statStore) flushCounter(name string, counter *counter) { } } -func (s *statStore) flushGauge(key string, gauge *gauge) { - s.sink.FlushGauge(key, gauge.Value()) -} - func (s *statStore) AddStatGenerator(statGenerator StatGenerator) { s.mu.Lock() defer s.mu.Unlock() @@ -432,14 +428,14 @@ func (s *statStore) ScopeWithTags(name string, tags map[string]string) Scope { } func (s *statStore) newCounter(serializedName string) *counter { - if counter, ok := s.counters.Get(serializedName); ok { - return counter + if v, ok := s.counters.Get(serializedName); ok { + return v } - counter := new(counter) - if existingCounter, ok, _ := s.counters.PeekOrAdd(serializedName, counter); ok { - return existingCounter + c := new(counter) + if v, ok, _ := s.counters.PeekOrAdd(serializedName, c); ok { + return v } - return counter + return c } func (s *statStore) NewCounter(name string) Counter { @@ -500,14 +496,14 @@ func (s *statStore) NewPerInstanceGauge(name string, tags map[string]string) Gau } func (s *statStore) newTimer(serializedName string, base time.Duration) *timer { - if timer, ok := s.timers.Get(serializedName); ok { - return timer + if v, ok := s.timers.Get(serializedName); ok { + return v } - timer := &timer{name: serializedName, sink: s.sink, base: base} - if existingTimer, ok, _ := s.timers.PeekOrAdd(serializedName, timer); ok { - return existingTimer + t := &timer{name: serializedName, sink: s.sink, base: base} + if v, ok, _ := s.timers.PeekOrAdd(serializedName, t); ok { + return v } - return timer + return t } func (s *statStore) NewMilliTimer(name string) Timer { From 09cd973de4ea0caabd9e0deae610d1553bef3541 Mon Sep 17 00:00:00 2001 From: Cerek Hillen Date: Tue, 20 Jun 2023 14:06:50 -0700 Subject: [PATCH 04/11] some formatting --- stats.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stats.go b/stats.go index 9f62ed3..89becdb 100644 --- a/stats.go +++ b/stats.go @@ -351,7 +351,7 @@ type statStore struct { timers *lru.Cache[string, *timer] // Gauges must not be expunged because they are client-side stateful. // We use a sync.Map instead of a cache to ensure they are kept indefinitely. - gauges sync.Map + gauges sync.Map mu sync.RWMutex statGenerators []StatGenerator @@ -392,7 +392,7 @@ func (s *statStore) Flush() { s.flushCounter(name, counter) } - s.gauges.Range(func (key any, v any) bool { + s.gauges.Range(func(key, v interface{}) bool { s.sink.FlushGauge(key.(string), v.(*gauge).Value()) return true }) From 4604e4c62faf92c47ac38ab6e4dd816c77697e5c Mon Sep 17 00:00:00 2001 From: Cerek Hillen Date: Wed, 21 Jun 2023 08:59:46 -0700 Subject: [PATCH 05/11] goimports --- stats.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stats.go b/stats.go index 89becdb..ca5bce1 100644 --- a/stats.go +++ b/stats.go @@ -7,7 +7,7 @@ import ( "sync/atomic" "time" - "github.com/hashicorp/golang-lru/v2" + lru "github.com/hashicorp/golang-lru/v2" tagspkg "github.com/lyft/gostats/internal/tags" ) From 0b47ff3a85f085a97b8614d18fbe3658d0d1805e Mon Sep 17 00:00:00 2001 From: Cerek Hillen Date: Wed, 21 Jun 2023 09:30:46 -0700 Subject: [PATCH 06/11] make cache size larger than we would expect to be filled --- stats.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/stats.go b/stats.go index ca5bce1..0128072 100644 --- a/stats.go +++ b/stats.go @@ -216,12 +216,10 @@ type StatGenerator interface { // NewStore returns an Empty store that flushes to Sink passed as an argument. // Note: the export argument is unused. func NewStore(sink Sink, _ bool) Store { - // TODO(crockeo): decide how this should be configurable - cacheSize := 8192 - s := &statStore{sink: sink} // lru.NewWithEvict can only return a non-nil error when cacheSize < 0. + cacheSize := 65536 s.counters, _ = lru.NewWithEvict(cacheSize, s.flushCounter) s.timers, _ = lru.New[string, *timer](cacheSize) From 1fa1009af08fba61302a3cd6b96818a6c92bfc8e Mon Sep 17 00:00:00 2001 From: Cerek Hillen Date: Wed, 21 Jun 2023 11:05:41 -0700 Subject: [PATCH 07/11] vendor hashicorp LRU --- go.mod | 5 +- go.sum | 2 - .../vendored/hashicorp/golang-lru/README.md | 7 + internal/vendored/hashicorp/golang-lru/lru.go | 250 ++++++++++++ .../vendored/hashicorp/golang-lru/lru_LICENSE | 364 ++++++++++++++++++ .../vendored/hashicorp/golang-lru/lru_test.go | 306 +++++++++++++++ .../golang-lru/simplelru/LICENSE_list | 29 ++ .../hashicorp/golang-lru/simplelru/list.go | 128 ++++++ .../hashicorp/golang-lru/simplelru/lru.go | 178 +++++++++ .../golang-lru/simplelru/lru_interface.go | 46 +++ .../golang-lru/simplelru/lru_test.go | 209 ++++++++++ stats.go | 3 +- 12 files changed, 1519 insertions(+), 8 deletions(-) create mode 100644 internal/vendored/hashicorp/golang-lru/README.md create mode 100644 internal/vendored/hashicorp/golang-lru/lru.go create mode 100644 internal/vendored/hashicorp/golang-lru/lru_LICENSE create mode 100644 internal/vendored/hashicorp/golang-lru/lru_test.go create mode 100644 internal/vendored/hashicorp/golang-lru/simplelru/LICENSE_list create mode 100644 internal/vendored/hashicorp/golang-lru/simplelru/list.go create mode 100644 internal/vendored/hashicorp/golang-lru/simplelru/lru.go create mode 100644 internal/vendored/hashicorp/golang-lru/simplelru/lru_interface.go create mode 100644 internal/vendored/hashicorp/golang-lru/simplelru/lru_test.go diff --git a/go.mod b/go.mod index 741080a..75bdc60 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,4 @@ module github.com/lyft/gostats go 1.18 -require ( - github.com/hashicorp/golang-lru/v2 v2.0.3 - github.com/kelseyhightower/envconfig v1.4.0 -) +require github.com/kelseyhightower/envconfig v1.4.0 diff --git a/go.sum b/go.sum index 85d02c3..8642a1a 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,2 @@ -github.com/hashicorp/golang-lru/v2 v2.0.3 h1:kmRrRLlInXvng0SmLxmQpQkpbYAvcXm7NPDrgxJa9mE= -github.com/hashicorp/golang-lru/v2 v2.0.3/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= diff --git a/internal/vendored/hashicorp/golang-lru/README.md b/internal/vendored/hashicorp/golang-lru/README.md new file mode 100644 index 0000000..3a0e671 --- /dev/null +++ b/internal/vendored/hashicorp/golang-lru/README.md @@ -0,0 +1,7 @@ +# golang-lru + +Vendored from Hashicorp's https://github.com/hashicorp/golang-lru implementation. +At time of writing the current snapshot is from [v2.0.4](https://github.com/hashicorp/golang-lru/releases/tag/v2.0.4). +Licensed under the Mozilla Public License 2.0, +and a copy of the original license is provided at +[lru_LICENSE](./lru_LICENSE). diff --git a/internal/vendored/hashicorp/golang-lru/lru.go b/internal/vendored/hashicorp/golang-lru/lru.go new file mode 100644 index 0000000..04c36bc --- /dev/null +++ b/internal/vendored/hashicorp/golang-lru/lru.go @@ -0,0 +1,250 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lru + +import ( + "sync" + + "github.com/lyft/gostats/internal/vendored/hashicorp/golang-lru/simplelru" +) + +const ( + // DefaultEvictedBufferSize defines the default buffer size to store evicted key/val + DefaultEvictedBufferSize = 16 +) + +// Cache is a thread-safe fixed size LRU cache. +type Cache[K comparable, V any] struct { + lru *simplelru.LRU[K, V] + evictedKeys []K + evictedVals []V + onEvictedCB func(k K, v V) + lock sync.RWMutex +} + +// New creates an LRU of the given size. +func New[K comparable, V any](size int) (*Cache[K, V], error) { + return NewWithEvict[K, V](size, nil) +} + +// NewWithEvict constructs a fixed size cache with the given eviction +// callback. +func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) (c *Cache[K, V], err error) { + // create a cache with default settings + c = &Cache[K, V]{ + onEvictedCB: onEvicted, + } + if onEvicted != nil { + c.initEvictBuffers() + onEvicted = c.onEvicted + } + c.lru, err = simplelru.NewLRU(size, onEvicted) + return +} + +func (c *Cache[K, V]) initEvictBuffers() { + c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize) + c.evictedVals = make([]V, 0, DefaultEvictedBufferSize) +} + +// onEvicted save evicted key/val and sent in externally registered callback +// outside of critical section +func (c *Cache[K, V]) onEvicted(k K, v V) { + c.evictedKeys = append(c.evictedKeys, k) + c.evictedVals = append(c.evictedVals, v) +} + +// Purge is used to completely clear the cache. +func (c *Cache[K, V]) Purge() { + var ks []K + var vs []V + c.lock.Lock() + c.lru.Purge() + if c.onEvictedCB != nil && len(c.evictedKeys) > 0 { + ks, vs = c.evictedKeys, c.evictedVals + c.initEvictBuffers() + } + c.lock.Unlock() + // invoke callback outside of critical section + if c.onEvictedCB != nil { + for i := 0; i < len(ks); i++ { + c.onEvictedCB(ks[i], vs[i]) + } + } +} + +// Add adds a value to the cache. Returns true if an eviction occurred. +func (c *Cache[K, V]) Add(key K, value V) (evicted bool) { + var k K + var v V + c.lock.Lock() + evicted = c.lru.Add(key, value) + if c.onEvictedCB != nil && evicted { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } + c.lock.Unlock() + if c.onEvictedCB != nil && evicted { + c.onEvictedCB(k, v) + } + return +} + +// Get looks up a key's value from the cache. +func (c *Cache[K, V]) Get(key K) (value V, ok bool) { + c.lock.Lock() + value, ok = c.lru.Get(key) + c.lock.Unlock() + return value, ok +} + +// Contains checks if a key is in the cache, without updating the +// recent-ness or deleting it for being stale. +func (c *Cache[K, V]) Contains(key K) bool { + c.lock.RLock() + containKey := c.lru.Contains(key) + c.lock.RUnlock() + return containKey +} + +// Peek returns the key value (or undefined if not found) without updating +// the "recently used"-ness of the key. +func (c *Cache[K, V]) Peek(key K) (value V, ok bool) { + c.lock.RLock() + value, ok = c.lru.Peek(key) + c.lock.RUnlock() + return value, ok +} + +// ContainsOrAdd checks if a key is in the cache without updating the +// recent-ness or deleting it for being stale, and if not, adds the value. +// Returns whether found and whether an eviction occurred. +func (c *Cache[K, V]) ContainsOrAdd(key K, value V) (ok, evicted bool) { + var k K + var v V + c.lock.Lock() + if c.lru.Contains(key) { + c.lock.Unlock() + return true, false + } + evicted = c.lru.Add(key, value) + if c.onEvictedCB != nil && evicted { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } + c.lock.Unlock() + if c.onEvictedCB != nil && evicted { + c.onEvictedCB(k, v) + } + return false, evicted +} + +// PeekOrAdd checks if a key is in the cache without updating the +// recent-ness or deleting it for being stale, and if not, adds the value. +// Returns whether found and whether an eviction occurred. +func (c *Cache[K, V]) PeekOrAdd(key K, value V) (previous V, ok, evicted bool) { + var k K + var v V + c.lock.Lock() + previous, ok = c.lru.Peek(key) + if ok { + c.lock.Unlock() + return previous, true, false + } + evicted = c.lru.Add(key, value) + if c.onEvictedCB != nil && evicted { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } + c.lock.Unlock() + if c.onEvictedCB != nil && evicted { + c.onEvictedCB(k, v) + } + return +} + +// Remove removes the provided key from the cache. +func (c *Cache[K, V]) Remove(key K) (present bool) { + var k K + var v V + c.lock.Lock() + present = c.lru.Remove(key) + if c.onEvictedCB != nil && present { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } + c.lock.Unlock() + if c.onEvictedCB != nil && present { + c.onEvictedCB(k, v) + } + return +} + +// Resize changes the cache size. +func (c *Cache[K, V]) Resize(size int) (evicted int) { + var ks []K + var vs []V + c.lock.Lock() + evicted = c.lru.Resize(size) + if c.onEvictedCB != nil && evicted > 0 { + ks, vs = c.evictedKeys, c.evictedVals + c.initEvictBuffers() + } + c.lock.Unlock() + if c.onEvictedCB != nil && evicted > 0 { + for i := 0; i < len(ks); i++ { + c.onEvictedCB(ks[i], vs[i]) + } + } + return evicted +} + +// RemoveOldest removes the oldest item from the cache. +func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) { + var k K + var v V + c.lock.Lock() + key, value, ok = c.lru.RemoveOldest() + if c.onEvictedCB != nil && ok { + k, v = c.evictedKeys[0], c.evictedVals[0] + c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] + } + c.lock.Unlock() + if c.onEvictedCB != nil && ok { + c.onEvictedCB(k, v) + } + return +} + +// GetOldest returns the oldest entry +func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) { + c.lock.RLock() + key, value, ok = c.lru.GetOldest() + c.lock.RUnlock() + return +} + +// Keys returns a slice of the keys in the cache, from oldest to newest. +func (c *Cache[K, V]) Keys() []K { + c.lock.RLock() + keys := c.lru.Keys() + c.lock.RUnlock() + return keys +} + +// Values returns a slice of the values in the cache, from oldest to newest. +func (c *Cache[K, V]) Values() []V { + c.lock.RLock() + values := c.lru.Values() + c.lock.RUnlock() + return values +} + +// Len returns the number of items in the cache. +func (c *Cache[K, V]) Len() int { + c.lock.RLock() + length := c.lru.Len() + c.lock.RUnlock() + return length +} diff --git a/internal/vendored/hashicorp/golang-lru/lru_LICENSE b/internal/vendored/hashicorp/golang-lru/lru_LICENSE new file mode 100644 index 0000000..0e5d580 --- /dev/null +++ b/internal/vendored/hashicorp/golang-lru/lru_LICENSE @@ -0,0 +1,364 @@ +Copyright (c) 2014 HashiCorp, Inc. + +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. diff --git a/internal/vendored/hashicorp/golang-lru/lru_test.go b/internal/vendored/hashicorp/golang-lru/lru_test.go new file mode 100644 index 0000000..4deb88b --- /dev/null +++ b/internal/vendored/hashicorp/golang-lru/lru_test.go @@ -0,0 +1,306 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lru + +import ( + "crypto/rand" + "math" + "math/big" + "testing" +) + +func getRand(tb testing.TB) int64 { + out, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + tb.Fatal(err) + } + return out.Int64() +} + +func BenchmarkLRU_Rand(b *testing.B) { + l, err := New[int64, int64](8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = getRand(b) % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + if _, ok := l.Get(trace[i]); ok { + hit++ + } else { + miss++ + } + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) +} + +func BenchmarkLRU_Freq(b *testing.B) { + l, err := New[int64, int64](8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = getRand(b) % 16384 + } else { + trace[i] = getRand(b) % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + if _, ok := l.Get(trace[i]); ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) +} + +func TestLRU(t *testing.T) { + evictCounter := 0 + onEvicted := func(k int, v int) { + if k != v { + t.Fatalf("Evict values not equal (%v!=%v)", k, v) + } + evictCounter++ + } + l, err := NewWithEvict(128, onEvicted) + if err != nil { + t.Fatalf("err: %v", err) + } + + for i := 0; i < 256; i++ { + l.Add(i, i) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + + if evictCounter != 128 { + t.Fatalf("bad evict count: %v", evictCounter) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i, v := range l.Values() { + if v != i+128 { + t.Fatalf("bad value: %v", v) + } + } + for i := 0; i < 128; i++ { + if _, ok := l.Get(i); ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + if _, ok := l.Get(i); !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + l.Remove(i) + if _, ok := l.Get(i); ok { + t.Fatalf("should be deleted") + } + } + + l.Get(192) // expect 192 to be last key in l.Keys() + + for i, k := range l.Keys() { + if (i < 63 && k != i+193) || (i == 63 && k != 192) { + t.Fatalf("out of order key: %v", k) + } + } + + l.Purge() + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +// test that Add returns true/false if an eviction occurred +func TestLRUAdd(t *testing.T) { + evictCounter := 0 + onEvicted := func(k int, v int) { + evictCounter++ + } + + l, err := NewWithEvict(1, onEvicted) + if err != nil { + t.Fatalf("err: %v", err) + } + + if l.Add(1, 1) == true || evictCounter != 0 { + t.Errorf("should not have an eviction") + } + if l.Add(2, 2) == false || evictCounter != 1 { + t.Errorf("should have an eviction") + } +} + +// test that Contains doesn't update recent-ness +func TestLRUContains(t *testing.T) { + l, err := New[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if !l.Contains(1) { + t.Errorf("1 should be contained") + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Contains should not have updated recent-ness of 1") + } +} + +// test that ContainsOrAdd doesn't update recent-ness +func TestLRUContainsOrAdd(t *testing.T) { + l, err := New[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + contains, evict := l.ContainsOrAdd(1, 1) + if !contains { + t.Errorf("1 should be contained") + } + if evict { + t.Errorf("nothing should be evicted here") + } + + l.Add(3, 3) + contains, evict = l.ContainsOrAdd(1, 1) + if contains { + t.Errorf("1 should not have been contained") + } + if !evict { + t.Errorf("an eviction should have occurred") + } + if !l.Contains(1) { + t.Errorf("now 1 should be contained") + } +} + +// test that PeekOrAdd doesn't update recent-ness +func TestLRUPeekOrAdd(t *testing.T) { + l, err := New[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + previous, contains, evict := l.PeekOrAdd(1, 1) + if !contains { + t.Errorf("1 should be contained") + } + if evict { + t.Errorf("nothing should be evicted here") + } + if previous != 1 { + t.Errorf("previous is not equal to 1") + } + + l.Add(3, 3) + contains, evict = l.ContainsOrAdd(1, 1) + if contains { + t.Errorf("1 should not have been contained") + } + if !evict { + t.Errorf("an eviction should have occurred") + } + if !l.Contains(1) { + t.Errorf("now 1 should be contained") + } +} + +// test that Peek doesn't update recent-ness +func TestLRUPeek(t *testing.T) { + l, err := New[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } +} + +// test that Resize can upsize and downsize +func TestLRUResize(t *testing.T) { + onEvictCounter := 0 + onEvicted := func(k int, v int) { + onEvictCounter++ + } + l, err := NewWithEvict(2, onEvicted) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Downsize + l.Add(1, 1) + l.Add(2, 2) + evicted := l.Resize(1) + if evicted != 1 { + t.Errorf("1 element should have been evicted: %v", evicted) + } + if onEvictCounter != 1 { + t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Element 1 should have been evicted") + } + + // Upsize + evicted = l.Resize(2) + if evicted != 0 { + t.Errorf("0 elements should have been evicted: %v", evicted) + } + + l.Add(4, 4) + if !l.Contains(3) || !l.Contains(4) { + t.Errorf("Cache should have contained 2 elements") + } +} diff --git a/internal/vendored/hashicorp/golang-lru/simplelru/LICENSE_list b/internal/vendored/hashicorp/golang-lru/simplelru/LICENSE_list new file mode 100644 index 0000000..c4764e6 --- /dev/null +++ b/internal/vendored/hashicorp/golang-lru/simplelru/LICENSE_list @@ -0,0 +1,29 @@ +This license applies to simplelru/list.go + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/internal/vendored/hashicorp/golang-lru/simplelru/list.go b/internal/vendored/hashicorp/golang-lru/simplelru/list.go new file mode 100644 index 0000000..c39da3c --- /dev/null +++ b/internal/vendored/hashicorp/golang-lru/simplelru/list.go @@ -0,0 +1,128 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE_list file. + +package simplelru + +// entry is an LRU entry +type entry[K comparable, V any] struct { + // Next and previous pointers in the doubly-linked list of elements. + // To simplify the implementation, internally a list l is implemented + // as a ring, such that &l.root is both the next element of the last + // list element (l.Back()) and the previous element of the first list + // element (l.Front()). + next, prev *entry[K, V] + + // The list to which this element belongs. + list *lruList[K, V] + + // The LRU key of this element. + key K + + // The value stored with this element. + value V +} + +// prevEntry returns the previous list element or nil. +func (e *entry[K, V]) prevEntry() *entry[K, V] { + if p := e.prev; e.list != nil && p != &e.list.root { + return p + } + return nil +} + +// lruList represents a doubly linked list. +// The zero value for lruList is an empty list ready to use. +type lruList[K comparable, V any] struct { + root entry[K, V] // sentinel list element, only &root, root.prev, and root.next are used + len int // current list length excluding (this) sentinel element +} + +// init initializes or clears list l. +func (l *lruList[K, V]) init() *lruList[K, V] { + l.root.next = &l.root + l.root.prev = &l.root + l.len = 0 + return l +} + +// newList returns an initialized list. +func newList[K comparable, V any]() *lruList[K, V] { return new(lruList[K, V]).init() } + +// length returns the number of elements of list l. +// The complexity is O(1). +func (l *lruList[K, V]) length() int { return l.len } + +// back returns the last element of list l or nil if the list is empty. +func (l *lruList[K, V]) back() *entry[K, V] { + if l.len == 0 { + return nil + } + return l.root.prev +} + +// lazyInit lazily initializes a zero List value. +func (l *lruList[K, V]) lazyInit() { + if l.root.next == nil { + l.init() + } +} + +// insert inserts e after at, increments l.len, and returns e. +func (l *lruList[K, V]) insert(e, at *entry[K, V]) *entry[K, V] { + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e + e.list = l + l.len++ + return e +} + +// insertValue is a convenience wrapper for insert(&Element{Value: v}, at). +func (l *lruList[K, V]) insertValue(k K, v V, at *entry[K, V]) *entry[K, V] { + return l.insert(&entry[K, V]{value: v, key: k}, at) +} + +// remove removes e from its list, decrements l.len +func (l *lruList[K, V]) remove(e *entry[K, V]) V { + e.prev.next = e.next + e.next.prev = e.prev + e.next = nil // avoid memory leaks + e.prev = nil // avoid memory leaks + e.list = nil + l.len-- + + return e.value +} + +// move moves e to next to at. +func (l *lruList[K, V]) move(e, at *entry[K, V]) { + if e == at { + return + } + e.prev.next = e.next + e.next.prev = e.prev + + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e +} + +// pushFront inserts a new element e with value v at the front of list l and returns e. +func (l *lruList[K, V]) pushFront(k K, v V) *entry[K, V] { + l.lazyInit() + return l.insertValue(k, v, &l.root) +} + +// moveToFront moves element e to the front of list l. +// If e is not an element of l, the list is not modified. +// The element must not be nil. +func (l *lruList[K, V]) moveToFront(e *entry[K, V]) { + if e.list != l || l.root.next == e { + return + } + // see comment in List.Remove about initialization of l + l.move(e, &l.root) +} diff --git a/internal/vendored/hashicorp/golang-lru/simplelru/lru.go b/internal/vendored/hashicorp/golang-lru/simplelru/lru.go new file mode 100644 index 0000000..b165ea2 --- /dev/null +++ b/internal/vendored/hashicorp/golang-lru/simplelru/lru.go @@ -0,0 +1,178 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package simplelru + +import ( + "errors" +) + +// EvictCallback is used to get a callback when a cache entry is evicted +type EvictCallback[K comparable, V any] func(key K, value V) + +// LRU implements a non-thread safe fixed size LRU cache +type LRU[K comparable, V any] struct { + size int + evictList *lruList[K, V] + items map[K]*entry[K, V] + onEvict EvictCallback[K, V] +} + +// NewLRU constructs an LRU of the given size +func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) { + if size <= 0 { + return nil, errors.New("must provide a positive size") + } + + c := &LRU[K, V]{ + size: size, + evictList: newList[K, V](), + items: make(map[K]*entry[K, V]), + onEvict: onEvict, + } + return c, nil +} + +// Purge is used to completely clear the cache. +func (c *LRU[K, V]) Purge() { + for k, v := range c.items { + if c.onEvict != nil { + c.onEvict(k, v.value) + } + delete(c.items, k) + } + c.evictList.init() +} + +// Add adds a value to the cache. Returns true if an eviction occurred. +func (c *LRU[K, V]) Add(key K, value V) (evicted bool) { + // Check for existing item + if ent, ok := c.items[key]; ok { + c.evictList.moveToFront(ent) + if c.onEvict != nil { + c.onEvict(key, ent.value) + } + ent.value = value + return false + } + + // Add new item + ent := c.evictList.pushFront(key, value) + c.items[key] = ent + + evict := c.evictList.length() > c.size + // Verify size not exceeded + if evict { + c.removeOldest() + } + return evict +} + +// Get looks up a key's value from the cache. +func (c *LRU[K, V]) Get(key K) (value V, ok bool) { + if ent, ok := c.items[key]; ok { + c.evictList.moveToFront(ent) + return ent.value, true + } + return +} + +// Contains checks if a key is in the cache, without updating the recent-ness +// or deleting it for being stale. +func (c *LRU[K, V]) Contains(key K) (ok bool) { + _, ok = c.items[key] + return ok +} + +// Peek returns the key value (or undefined if not found) without updating +// the "recently used"-ness of the key. +func (c *LRU[K, V]) Peek(key K) (value V, ok bool) { + var ent *entry[K, V] + if ent, ok = c.items[key]; ok { + return ent.value, true + } + return +} + +// Remove removes the provided key from the cache, returning if the +// key was contained. +func (c *LRU[K, V]) Remove(key K) (present bool) { + if ent, ok := c.items[key]; ok { + c.removeElement(ent) + return true + } + return false +} + +// RemoveOldest removes the oldest item from the cache. +func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { + if ent := c.evictList.back(); ent != nil { + c.removeElement(ent) + return ent.key, ent.value, true + } + return +} + +// GetOldest returns the oldest entry +func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) { + if ent := c.evictList.back(); ent != nil { + return ent.key, ent.value, true + } + return +} + +// Keys returns a slice of the keys in the cache, from oldest to newest. +func (c *LRU[K, V]) Keys() []K { + keys := make([]K, c.evictList.length()) + i := 0 + for ent := c.evictList.back(); ent != nil; ent = ent.prevEntry() { + keys[i] = ent.key + i++ + } + return keys +} + +// Values returns a slice of the values in the cache, from oldest to newest. +func (c *LRU[K, V]) Values() []V { + values := make([]V, len(c.items)) + i := 0 + for ent := c.evictList.back(); ent != nil; ent = ent.prevEntry() { + values[i] = ent.value + i++ + } + return values +} + +// Len returns the number of items in the cache. +func (c *LRU[K, V]) Len() int { + return c.evictList.length() +} + +// Resize changes the cache size. +func (c *LRU[K, V]) Resize(size int) (evicted int) { + diff := c.Len() - size + if diff < 0 { + diff = 0 + } + for i := 0; i < diff; i++ { + c.removeOldest() + } + c.size = size + return diff +} + +// removeOldest removes the oldest item from the cache. +func (c *LRU[K, V]) removeOldest() { + if ent := c.evictList.back(); ent != nil { + c.removeElement(ent) + } +} + +// removeElement is used to remove a given list element from the cache +func (c *LRU[K, V]) removeElement(e *entry[K, V]) { + c.evictList.remove(e) + delete(c.items, e.key) + if c.onEvict != nil { + c.onEvict(e.key, e.value) + } +} diff --git a/internal/vendored/hashicorp/golang-lru/simplelru/lru_interface.go b/internal/vendored/hashicorp/golang-lru/simplelru/lru_interface.go new file mode 100644 index 0000000..043b8bc --- /dev/null +++ b/internal/vendored/hashicorp/golang-lru/simplelru/lru_interface.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package simplelru provides simple LRU implementation based on build-in container/list. +package simplelru + +// LRUCache is the interface for simple LRU cache. +type LRUCache[K comparable, V any] interface { + // Adds a value to the cache, returns true if an eviction occurred and + // updates the "recently used"-ness of the key. + Add(key K, value V) bool + + // Returns key's value from the cache and + // updates the "recently used"-ness of the key. #value, isFound + Get(key K) (value V, ok bool) + + // Checks if a key exists in cache without updating the recent-ness. + Contains(key K) (ok bool) + + // Returns key's value without updating the "recently used"-ness of the key. + Peek(key K) (value V, ok bool) + + // Removes a key from the cache. + Remove(key K) bool + + // Removes the oldest entry from cache. + RemoveOldest() (K, V, bool) + + // Returns the oldest entry from the cache. #key, value, isFound + GetOldest() (K, V, bool) + + // Returns a slice of the keys in the cache, from oldest to newest. + Keys() []K + + // Values returns a slice of the values in the cache, from oldest to newest. + Values() []V + + // Returns the number of items in the cache. + Len() int + + // Clears all cache entries. + Purge() + + // Resizes cache, returning number evicted + Resize(int) int +} diff --git a/internal/vendored/hashicorp/golang-lru/simplelru/lru_test.go b/internal/vendored/hashicorp/golang-lru/simplelru/lru_test.go new file mode 100644 index 0000000..a6b247f --- /dev/null +++ b/internal/vendored/hashicorp/golang-lru/simplelru/lru_test.go @@ -0,0 +1,209 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package simplelru + +import "testing" + +func TestLRU(t *testing.T) { + evictCounter := 0 + onEvicted := func(k int, v int) { + if k != v { + t.Fatalf("Evict values not equal (%v!=%v)", k, v) + } + evictCounter++ + } + l, err := NewLRU(128, onEvicted) + if err != nil { + t.Fatalf("err: %v", err) + } + + for i := 0; i < 256; i++ { + l.Add(i, i) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + + if evictCounter != 128 { + t.Fatalf("bad evict count: %v", evictCounter) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i, v := range l.Values() { + if v != i+128 { + t.Fatalf("bad value: %v", v) + } + } + for i := 0; i < 128; i++ { + if _, ok := l.Get(i); ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + if _, ok := l.Get(i); !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + if ok := l.Remove(i); !ok { + t.Fatalf("should be contained") + } + if ok := l.Remove(i); ok { + t.Fatalf("should not be contained") + } + if _, ok := l.Get(i); ok { + t.Fatalf("should be deleted") + } + } + + l.Get(192) // expect 192 to be last key in l.Keys() + + for i, k := range l.Keys() { + if (i < 63 && k != i+193) || (i == 63 && k != 192) { + t.Fatalf("out of order key: %v", k) + } + } + + l.Purge() + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +func TestLRU_GetOldest_RemoveOldest(t *testing.T) { + l, err := NewLRU[int, int](128, nil) + if err != nil { + t.Fatalf("err: %v", err) + } + for i := 0; i < 256; i++ { + l.Add(i, i) + } + k, _, ok := l.GetOldest() + if !ok { + t.Fatalf("missing") + } + if k != 128 { + t.Fatalf("bad: %v", k) + } + + k, _, ok = l.RemoveOldest() + if !ok { + t.Fatalf("missing") + } + if k != 128 { + t.Fatalf("bad: %v", k) + } + + k, _, ok = l.RemoveOldest() + if !ok { + t.Fatalf("missing") + } + if k != 129 { + t.Fatalf("bad: %v", k) + } +} + +// Test that Add returns true/false if an eviction occurred +func TestLRU_Add(t *testing.T) { + evictCounter := 0 + onEvicted := func(k int, v int) { + evictCounter++ + } + + l, err := NewLRU(1, onEvicted) + if err != nil { + t.Fatalf("err: %v", err) + } + + if l.Add(1, 1) == true || evictCounter != 0 { + t.Errorf("should not have an eviction") + } + if l.Add(2, 2) == false || evictCounter != 1 { + t.Errorf("should have an eviction") + } +} + +// Test that Contains doesn't update recent-ness +func TestLRU_Contains(t *testing.T) { + l, err := NewLRU[int, int](2, nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if !l.Contains(1) { + t.Errorf("1 should be contained") + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Contains should not have updated recent-ness of 1") + } +} + +// Test that Peek doesn't update recent-ness +func TestLRU_Peek(t *testing.T) { + l, err := NewLRU[int, int](2, nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } +} + +// Test that Resize can upsize and downsize +func TestLRU_Resize(t *testing.T) { + onEvictCounter := 0 + onEvicted := func(k int, v int) { + onEvictCounter++ + } + l, err := NewLRU(2, onEvicted) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Downsize + l.Add(1, 1) + l.Add(2, 2) + evicted := l.Resize(1) + if evicted != 1 { + t.Errorf("1 element should have been evicted: %v", evicted) + } + if onEvictCounter != 1 { + t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Element 1 should have been evicted") + } + + // Upsize + evicted = l.Resize(2) + if evicted != 0 { + t.Errorf("0 elements should have been evicted: %v", evicted) + } + + l.Add(4, 4) + if !l.Contains(3) || !l.Contains(4) { + t.Errorf("Cache should have contained 2 elements") + } +} diff --git a/stats.go b/stats.go index 0128072..4fe07a3 100644 --- a/stats.go +++ b/stats.go @@ -7,8 +7,7 @@ import ( "sync/atomic" "time" - lru "github.com/hashicorp/golang-lru/v2" - + lru "github.com/lyft/gostats/internal/vendored/hashicorp/golang-lru" tagspkg "github.com/lyft/gostats/internal/tags" ) From ebc85de67d1fa2d79b79916682176fded107a5fb Mon Sep 17 00:00:00 2001 From: Cerek Hillen Date: Wed, 21 Jun 2023 11:11:24 -0700 Subject: [PATCH 08/11] linting again --- stats.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stats.go b/stats.go index 4fe07a3..fc5412f 100644 --- a/stats.go +++ b/stats.go @@ -7,8 +7,8 @@ import ( "sync/atomic" "time" - lru "github.com/lyft/gostats/internal/vendored/hashicorp/golang-lru" tagspkg "github.com/lyft/gostats/internal/tags" + lru "github.com/lyft/gostats/internal/vendored/hashicorp/golang-lru" ) // A Store holds statistics. From 59b83d9d1eac34b7cbeab9f68ba604b5452b9b1c Mon Sep 17 00:00:00 2001 From: Cerek Hillen Date: Fri, 23 Jun 2023 10:39:54 -0700 Subject: [PATCH 09/11] fix zero-value --- stats_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stats_test.go b/stats_test.go index b2a16d2..2f703a5 100644 --- a/stats_test.go +++ b/stats_test.go @@ -468,7 +468,7 @@ func BenchmarkParallelCounter(b *testing.B) { func BenchmarkStoreNewPerInstanceCounter(b *testing.B) { b.Run("HasTag", func(b *testing.B) { - var store statStore + store := NewStore(nil, false) tags := map[string]string{ "1": "1", "2": "2", @@ -481,7 +481,7 @@ func BenchmarkStoreNewPerInstanceCounter(b *testing.B) { }) b.Run("MissingTag", func(b *testing.B) { - var store statStore + store := NewStore(nil, false) tags := map[string]string{ "1": "1", "2": "2", From d2e64d2dbf63aaa7bcd47a879cc319deb7d8ffe5 Mon Sep 17 00:00:00 2001 From: Cerek Hillen Date: Fri, 23 Jun 2023 11:00:28 -0700 Subject: [PATCH 10/11] add new benchmark --- stats_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/stats_test.go b/stats_test.go index 2f703a5..2c44275 100644 --- a/stats_test.go +++ b/stats_test.go @@ -493,3 +493,20 @@ func BenchmarkStoreNewPerInstanceCounter(b *testing.B) { } }) } + +func BenchmarkStoreNewCounterParallel(b *testing.B) { + s := NewStore(nullSink{}, false) + t := time.NewTicker(time.Hour) // don't flush + defer t.Stop() + go s.Start(t) + names := new([2048]string) + for i := 0; i < len(names); i++ { + names[i] = "counter_" + strconv.Itoa(i) + } + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for i := 0; pb.Next(); i++ { + s.NewCounter(names[i%len(names)]) + } + }) +} From 15866421ef7d3994e52ace9f5920eb39c88efee8 Mon Sep 17 00:00:00 2001 From: Cerek Hillen Date: Fri, 23 Jun 2023 11:05:39 -0700 Subject: [PATCH 11/11] make range command (that might be broken?) --- internal/vendored/hashicorp/golang-lru/lru.go | 10 ++++++++++ stats.go | 11 +---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/internal/vendored/hashicorp/golang-lru/lru.go b/internal/vendored/hashicorp/golang-lru/lru.go index 04c36bc..b076861 100644 --- a/internal/vendored/hashicorp/golang-lru/lru.go +++ b/internal/vendored/hashicorp/golang-lru/lru.go @@ -225,6 +225,16 @@ func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) { return } +func (c *Cache[K, V]) Range(fn func (k K, v V)) { + c.lock.RLock() + keys := c.lru.Keys() + values := c.lru.Values() + c.lock.RUnlock() + for i := 0; i < len(keys); i++ { + fn(keys[i], values[i]) + } +} + // Keys returns a slice of the keys in the cache, from oldest to newest. func (c *Cache[K, V]) Keys() []K { c.lock.RLock() diff --git a/stats.go b/stats.go index fc5412f..33aaca9 100644 --- a/stats.go +++ b/stats.go @@ -379,16 +379,7 @@ func (s *statStore) Flush() { } s.mu.RUnlock() - for _, name := range s.counters.Keys() { - counter, ok := s.counters.Peek(name) - if !ok { - // This counter was removed between retrieving the names - // and finding this specific counter. - continue - } - s.flushCounter(name, counter) - } - + s.counters.Range(s.flushCounter) s.gauges.Range(func(key, v interface{}) bool { s.sink.FlushGauge(key.(string), v.(*gauge).Value()) return true