From fe9055dbed9961778bdc0131ab231d3474faae30 Mon Sep 17 00:00:00 2001 From: Esteban Del Boca Date: Wed, 4 Sep 2024 17:05:07 -0300 Subject: [PATCH 1/3] Adding a Scan function to iterate safely over the keys and values --- go.mod | 2 +- go.sum | 3 +-- lazylru.go | 29 +++++++++++++++++++++++++++++ lazylru_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 74e9a46..af5d2a9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/TriggerMail/lazylru -go 1.20 +go 1.23 require ( github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 8b3c0c6..5873294 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -9,6 +7,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN 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/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= diff --git a/lazylru.go b/lazylru.go index 295786f..4410807 100644 --- a/lazylru.go +++ b/lazylru.go @@ -2,6 +2,7 @@ package lazylru import ( "errors" + "iter" "math/rand/v2" "sync" "sync/atomic" @@ -507,6 +508,34 @@ func (lru *LazyLRU[K, V]) Len() int { return len(lru.items) } +// Scan returns an iterator that yields current non-expired items from the cache. +// It iterates over a snapshot of keys taken at the beginning of iteration, +// checking each key's existence and expiration before yielding its associated value. +// Keys created after the scan begins will be ignored. +func (lru *LazyLRU[K, V]) Scan() iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for _, k := range lru.keys() { + if v, found := lru.Get(k); found { + if !yield(k, v) { + return + } + } + } + } +} + +// keys returns a copy of the cache's keys for the iterator. +func (lru *LazyLRU[K, V]) keys() []K { + lru.lock.RLock() + defer lru.lock.RUnlock() + // TODO: Replace with items iteration + keys := make([]K, 0, len(lru.index)) + for key := range lru.index { + keys = append(keys, key) + } + return keys +} + // Close stops the reaper process. This is safe to call multiple times. func (lru *LazyLRU[K, V]) Close() { lru.lock.Lock() diff --git a/lazylru_test.go b/lazylru_test.go index 3aa55e7..ebaebf2 100644 --- a/lazylru_test.go +++ b/lazylru_test.go @@ -1,6 +1,7 @@ package lazylru_test import ( + "sort" "strconv" "testing" "time" @@ -553,3 +554,45 @@ func TestConcurrentShouldBubble(t *testing.T) { _ = group.Wait() } + +func TestScan(t *testing.T) { + lru := lazylru.NewT[int, int](20, time.Hour) + lru.SetTTL(0, 0<<4, 1*time.Hour) + lru.SetTTL(1, 1<<4, 1*time.Hour) + lru.SetTTL(2, 2<<4, 1*time.Hour) + lru.SetTTL(3, 3<<4, 1*time.Hour) + lru.SetTTL(4, 4<<4, 1*time.Hour) + lru.Reap() + + keys, values := []int{}, []int{} + for k, v := range lru.Scan() { + keys = append(keys, k) + values = append(values, v) + } + + sort.Ints(keys) + sort.Ints(values) + require.Equal(t, []int{0, 1, 2, 3, 4}, keys) + require.Equal(t, []int{0, 16, 32, 48, 64}, values) +} + +func TestScanWithExpiration(t *testing.T) { + lru := lazylru.NewT[int, int](20, time.Hour) + lru.SetTTL(0, 0<<4, 1*time.Hour) + lru.SetTTL(1, 1<<4, 1*time.Hour) + lru.SetTTL(2, 2<<4, 1*time.Microsecond) // <~ almost expired + lru.SetTTL(3, 3<<4, 1*time.Hour) + lru.SetTTL(4, 4<<4, 1*time.Hour) + lru.Reap() + + keys, values := []int{}, []int{} + for k, v := range lru.Scan() { + keys = append(keys, k) + values = append(values, v) + } + + sort.Ints(keys) + sort.Ints(values) + require.Equal(t, []int{0, 1, 3, 4}, keys) + require.Equal(t, []int{0, 16, 48, 64}, values) +} From 69bb3b2e5f5d6d2633baa961b1da4dbfe9742ac4 Mon Sep 17 00:00:00 2001 From: Esteban Del Boca Date: Thu, 5 Sep 2024 15:00:12 -0300 Subject: [PATCH 2/3] Linting the code and benchmarking the Scan function --- Earthfile | 2 +- lazylru.go | 17 +++++++------- lazylru_benchmark_test.go | 48 +++++++++++++++++++++++++++++++++++++++ lazylru_test.go | 8 ++++--- 4 files changed, 62 insertions(+), 13 deletions(-) diff --git a/Earthfile b/Earthfile index 6266b81..9334eb4 100644 --- a/Earthfile +++ b/Earthfile @@ -1,6 +1,6 @@ VERSION 0.6 -FROM golang:1.22 +FROM golang:1.23 all-bench: BUILD +fmt-bench diff --git a/lazylru.go b/lazylru.go index 4410807..1f380d1 100644 --- a/lazylru.go +++ b/lazylru.go @@ -236,7 +236,7 @@ func (lru *LazyLRU[K, V]) reap(start int, deathList []*item[K, V]) { } // cut off all the expired items for 0 < lru.items.Len() && lru.items[0].insertNumber == 0 { - _ = heap.Pop[*item[K, V]](&lru.items) + _ = heap.Pop(&lru.items) } lru.lock.Unlock() } @@ -289,7 +289,7 @@ func (lru *LazyLRU[K, V]) Get(key K) (V, bool) { delete(lru.index, pqi.key) // cut off all the expired items. should only be one for lru.items.Len() > 0 && lru.items[0].insertNumber == 0 { - _ = heap.Pop[*item[K, V]](&lru.items) + _ = heap.Pop(&lru.items) } lru.stats.KeysReadExpired++ lru.lock.Unlock() @@ -378,7 +378,7 @@ func (lru *LazyLRU[K, V]) MGet(keys ...K) map[K]V { // cut off all the expired items for lru.items.Len() > 0 && lru.items[0].insertNumber == 0 { - _ = heap.Pop[*item[K, V]](&lru.items) + _ = heap.Pop(&lru.items) } for _, key := range needsShuffle { @@ -435,12 +435,12 @@ func (lru *LazyLRU[K, V]) setInternal(key K, value V, expiration time.Time) []*i // remove excess for lru.items.Len() >= lru.maxItems { - deadGuy := heap.Pop[*item[K, V]](&lru.items) + deadGuy := heap.Pop(&lru.items) delete(lru.index, deadGuy.key) deathList = append(deathList, deadGuy) lru.stats.Evictions++ } - heap.Push[*item[K, V]](&lru.items, pqi) + heap.Push(&lru.items, pqi) lru.index[key] = pqi } return deathList @@ -492,9 +492,9 @@ func (lru *LazyLRU[K, V]) Delete(key K) { lru.lock.Unlock() return } - delete(lru.index, pqi.key) // remove from search index - lru.items.update(pqi, 0) // move this item to the top of the heap - deadguy := heap.Pop[*item[K, V]](&lru.items) // pop item from the top of the heap + delete(lru.index, pqi.key) // remove from search index + lru.items.update(pqi, 0) // move this item to the top of the heap + deadguy := heap.Pop(&lru.items) // pop item from the top of the heap lru.lock.Unlock() if lru.numEvictCB.Load() > 0 { lru.execOnEvict([]*item[K, V]{deadguy}) @@ -528,7 +528,6 @@ func (lru *LazyLRU[K, V]) Scan() iter.Seq2[K, V] { func (lru *LazyLRU[K, V]) keys() []K { lru.lock.RLock() defer lru.lock.RUnlock() - // TODO: Replace with items iteration keys := make([]K, 0, len(lru.index)) for key := range lru.index { keys = append(keys, key) diff --git a/lazylru_benchmark_test.go b/lazylru_benchmark_test.go index faf4823..b6b8efa 100644 --- a/lazylru_benchmark_test.go +++ b/lazylru_benchmark_test.go @@ -235,3 +235,51 @@ func Benchmark(b *testing.B) { b.Run(bc.Name()+"/generic/value", bc.GenericValue) } } + +type benchScanConfig struct { + capacity int + keyCount int +} + +func (bc *benchScanConfig) ScanInterfaceValues(b *testing.B) { + lru := lazylru.New(bc.capacity, time.Minute) + defer lru.Close() + for i := 0; i < bc.keyCount; i++ { + lru.Set(keys[i], i) + } + runtime.GC() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for range lru.Scan() { + continue + } + } +} + +func (bc *benchScanConfig) ScanGenericValues(b *testing.B) { + lru := lazylru.NewT[string, int](bc.capacity, time.Minute) + defer lru.Close() + for i := 0; i < bc.keyCount; i++ { + lru.Set(keys[i], i) + } + runtime.GC() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for range lru.Scan() { + continue + } + } +} + +func BenchmarkScan(b *testing.B) { + for _, bc := range []benchScanConfig{ + {100000, 0}, + {100000, 1}, + {100000, 100}, + {100000, 1000}, + {100000, 100000}, + } { + b.Run(fmt.Sprintf("scan/interface/keys/%d", bc.keyCount), bc.ScanInterfaceValues) + b.Run(fmt.Sprintf("scan/generic/keys/%d", bc.keyCount), bc.ScanGenericValues) + } +} diff --git a/lazylru_test.go b/lazylru_test.go index ebaebf2..525037c 100644 --- a/lazylru_test.go +++ b/lazylru_test.go @@ -582,7 +582,9 @@ func TestScanWithExpiration(t *testing.T) { lru.SetTTL(1, 1<<4, 1*time.Hour) lru.SetTTL(2, 2<<4, 1*time.Microsecond) // <~ almost expired lru.SetTTL(3, 3<<4, 1*time.Hour) - lru.SetTTL(4, 4<<4, 1*time.Hour) + lru.SetTTL(4, 4<<4, 1*time.Microsecond) // <~ almost expired + lru.SetTTL(5, 5<<4, 1*time.Hour) + lru.SetTTL(6, 6<<4, 1*time.Hour) lru.Reap() keys, values := []int{}, []int{} @@ -593,6 +595,6 @@ func TestScanWithExpiration(t *testing.T) { sort.Ints(keys) sort.Ints(values) - require.Equal(t, []int{0, 1, 3, 4}, keys) - require.Equal(t, []int{0, 16, 48, 64}, values) + require.Equal(t, []int{0, 1, 3, 5, 6}, keys) + require.Equal(t, []int{0, 16, 48, 80, 96}, values) } From 46a31f6a235164cd16643d06490533a92a6eb2b6 Mon Sep 17 00:00:00 2001 From: Esteban Del Boca Date: Thu, 5 Sep 2024 17:47:55 -0300 Subject: [PATCH 3/3] Update golangci-lint --- Earthfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Earthfile b/Earthfile index 9334eb4..4d19ea2 100644 --- a/Earthfile +++ b/Earthfile @@ -261,7 +261,7 @@ goveralls: golangci-lint: RUN echo Installing golangci-lint... # see https://golangci-lint.run/usage/install/#other-ci - RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /go/bin v1.57.2 + RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /go/bin v1.60.3 SAVE ARTIFACT /go/bin/golangci-lint /go/bin/golangci-lint junit-report: