diff --git a/imcache.go b/imcache.go index 678085f..fb592f3 100644 --- a/imcache.go +++ b/imcache.go @@ -296,6 +296,32 @@ func (c *Cache[K, V]) peekMultiple(now time.Time, keys ...K) map[K]V { return got } +// PeekAll returns a copy of all entries in the cache without +// actively evicting the encountered entry if it is expired and +// updating the entry's sliding expiration. +// +// If the max entries limit is set, it doesn't update +// the encountered entry's position in the eviction queue. +func (c *Cache[K, V]) PeekAll() map[K]V { + return c.peekAll(time.Now()) +} + +func (c *Cache[K, V]) peekAll(now time.Time) map[K]V { + c.mu.RLock() + defer c.mu.RUnlock() + if c.closed { + return nil + } + got := make(map[K]V, len(c.m)) + for key, node := range c.m { + if node.entry().expired(now) { + continue + } + got[key] = node.entry().val + } + return got +} + // Set sets the value for the given key. // If the entry already exists, it is replaced. // @@ -966,6 +992,35 @@ func (s *Sharded[K, V]) PeekMultiple(keys ...K) map[K]V { return result } +// PeekAll returns a copy of all entries in the cache without +// actively evicting the encountered entry if it is expired and +// updating the entry's sliding expiration. +// +// If the max entries limit is set, it doesn't update +// the encountered entry's position in the eviction queue. +func (s *Sharded[K, V]) PeekAll() map[K]V { + now := time.Now() + var n int + ms := make([]map[K]V, 0, len(s.shards)) + for _, shard := range s.shards { + m := shard.peekAll(now) + // If Cache.peekAll returns nil, it means that the shard is closed + // hence Sharded is closed too. + if m == nil { + return nil + } + n += len(m) + ms = append(ms, m) + } + all := make(map[K]V, n) + for _, m := range ms { + for key, val := range m { + all[key] = val + } + } + return all +} + // Set sets the value for the given key. // If the entry already exists, it is replaced. // diff --git a/imcache_test.go b/imcache_test.go index 1cb8096..02014b7 100644 --- a/imcache_test.go +++ b/imcache_test.go @@ -19,6 +19,7 @@ type imcache[K comparable, V any] interface { GetAll() map[K]V Peek(key K) (v V, present bool) PeekMultiple(keys ...K) map[K]V + PeekAll() map[K]V Set(key K, val V, exp Expiration) GetOrSet(key K, val V, exp Expiration) (v V, present bool) Replace(key K, val V, exp Expiration) (present bool) @@ -346,6 +347,48 @@ func TestImcache_PeekMultiple_SlidingExpiration(t *testing.T) { } } +func TestImcache_PeekAll(t *testing.T) { + for _, cache := range caches { + t.Run(cache.name, func(t *testing.T) { + c := cache.create() + c.Set("foo", "foo", WithNoExpiration()) + c.Set("foobar", "foobar", WithNoExpiration()) + c.Set("barfoo", "barfoo", WithNoExpiration()) + c.Set("bar", "bar", WithExpiration(time.Nanosecond)) + time.Sleep(time.Nanosecond) + want := map[string]string{ + "foo": "foo", + "foobar": "foobar", + "barfoo": "barfoo", + } + if got := c.PeekAll(); !reflect.DeepEqual(got, want) { + t.Errorf("got imcache.PeekAll() = %v, want %v", got, want) + } + }) + } +} + +func TestImcache_PeekAll_SlidingExpiration(t *testing.T) { + for _, cache := range caches { + t.Run(cache.name, func(t *testing.T) { + c := cache.create() + c.Set("foo", "foo", WithSlidingExpiration(500*time.Millisecond)) + c.Set("bar", "bar", WithSlidingExpiration(400*time.Millisecond)) + c.Set("foobar", "foobar", WithNoExpiration()) + time.Sleep(300 * time.Millisecond) + want := map[string]string{"foo": "foo", "bar": "bar", "foobar": "foobar"} + if got := c.PeekAll(); !reflect.DeepEqual(got, want) { + t.Fatalf("got imcache.PeekAll() = %v, want %v", got, want) + } + time.Sleep(300 * time.Millisecond) + want = map[string]string{"foobar": "foobar"} + if got := c.PeekAll(); !reflect.DeepEqual(got, want) { + t.Fatalf("got imcache.PeekAll() = %v, want %v", got, want) + } + }) + } +} + func TestImcache_Set(t *testing.T) { tests := []struct { name string @@ -1159,6 +1202,9 @@ func TestImcache_Close(t *testing.T) { if got := c.PeekMultiple("foo", "bar"); got != nil { t.Errorf("imcache.PeekMultiple(_) = %v, want %v", got, nil) } + if got := c.PeekAll(); got != nil { + t.Errorf("imcache.PeekAll() = %v, want %v", got, nil) + } v, ok := c.GetOrSet("foo", "bar", WithNoExpiration()) if ok { t.Error("imcache.GetOrSet(_, _, _) = _, true, want _, false") @@ -1296,6 +1342,27 @@ func TestImcache_PeekMultiple_EvictionCallback(t *testing.T) { } } +func TestImcache_PeekAll_EvictionCallback(t *testing.T) { + evictioncMock := &evictionCallbackMock[string, string]{} + for _, cache := range cachesWithEvictionCallback { + t.Run(cache.name, func(t *testing.T) { + defer evictioncMock.Reset() + c := cache.create(evictioncMock.Callback) + c.Set("foo", "foo", WithExpiration(time.Nanosecond)) + c.Set("bar", "bar", WithExpiration(time.Nanosecond)) + c.Set("foobar", "foobar", WithNoExpiration()) + time.Sleep(time.Nanosecond) + want := map[string]string{"foobar": "foobar"} + if got := c.PeekAll(); !reflect.DeepEqual(got, want) { + t.Fatalf("got imcache.PeekAll() = %v, want %v", got, want) + } + evictioncMock.HasNotBeenCalledWith(t, "foo", "foo", EvictionReasonExpired) + evictioncMock.HasNotBeenCalledWith(t, "bar", "bar", EvictionReasonExpired) + evictioncMock.HasEventuallyBeenCalledTimes(t, 0) + }) + } +} + func TestImcache_Set_EvictionCallback(t *testing.T) { evictioncMock := &evictionCallbackMock[string, string]{} for _, cache := range cachesWithEvictionCallback { @@ -1631,6 +1698,11 @@ func TestCache_MaxEntriesLimit_EvictionPolicyLRU(t *testing.T) { if _, ok := c.Get("nine"); !ok { t.Fatal("got Cache.Get(_) = _, false, want _, true") } + // PeekAll should not change the LRU queue. + want := map[string]int{"eight": 8, "nine": 9, "ten": 10, "eleven": 11, "twelve": 12} + if got := c.PeekAll(); !reflect.DeepEqual(got, want) { + t.Fatalf("got Cache.PeekAll() = %v, want %v", got, want) + } // LRU queue: nine -> twelve -> eleven -> eight -> ten. c.Set("thirteen", 13, WithNoExpiration()) // LRU queue: thirteen -> nine -> twelve -> eleven -> eight. @@ -1649,7 +1721,7 @@ func TestCache_MaxEntriesLimit_EvictionPolicyLRU(t *testing.T) { t.Fatal("got Cache.Get(_) = _, false, want _, true") } // PeekMultiple shouldn't move the entires to the front of the queue. - want := map[string]int{"fourteen": 14, "fifteen": 15} + want = map[string]int{"fourteen": 14, "fifteen": 15} if got := c.PeekMultiple("fourteen", "fifteen"); !reflect.DeepEqual(got, want) { t.Fatalf("got Cache.PeekMultiple(_) = %v, want %v", got, want) } @@ -1827,7 +1899,7 @@ func TestCache_MaxEntriesLimit_EvictionPolicyLFU(t *testing.T) { // LFU queue: six -> five -> four -> three -> two. evicted(t, "one", 1, EvictionReasonMaxEntriesExceeded) - // Get should update the entries frequency. + // Get should update the entry frequency. if _, ok := c.Get("two"); !ok { t.Fatal("got Cache.Get(_) = _, false, want _, true") } @@ -1864,7 +1936,7 @@ func TestCache_MaxEntriesLimit_EvictionPolicyLFU(t *testing.T) { // Replace should update the frequency of the entry if the entry already exists. c.Set("eight", 8, WithNoExpiration()) // LFU queue: two -> eight -> six -> five -> four. - // PeekMultiple shouldn't update the entries frequency. + // PeekMultiple shouldn't update the frequencies. want := map[string]int{"four": 4, "five": 5} if got := c.PeekMultiple("four", "five"); !reflect.DeepEqual(got, want) { t.Fatalf("got Cache.PeekMultiple(_) = %v, want %v", got, want) @@ -1921,6 +1993,11 @@ func TestCache_MaxEntriesLimit_EvictionPolicyLFU(t *testing.T) { c.GetAll() // LFU queue: eighteen -> sixteen -> fifteen. c.Set("twenty", 20, WithNoExpiration()) + // PeekAll shouldn't update the entries frequency. + want = map[string]int{"fifteen": 15, "sixteen": 16, "eighteen": 18, "twenty": 20} + if got := c.PeekAll(); !reflect.DeepEqual(got, want) { + t.Fatalf("got Cache.PeekAll() = %v, want %v", got, want) + } // LFU queue: eighteen -> sixteen -> fifteen -> twenty. c.Set("twentyone", 21, WithNoExpiration()) // LFU queue: eighteen -> sixteen -> fifteen -> twentyone -> twenty.