From 923d3297aa042821ffd924c7bbfda98004e4a8c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ken=E2=80=99ichiro=20Oyama?= Date: Tue, 27 Aug 2024 18:39:38 +0900 Subject: [PATCH] Add `RangeBackwards` method --- cache.go | 26 ++++++++++++++++++++++++++ cache_test.go | 30 ++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/cache.go b/cache.go index 3f2bd2d..0d86629 100644 --- a/cache.go +++ b/cache.go @@ -513,6 +513,32 @@ func (c *Cache[K, V]) Range(fn func(item *Item[K, V]) bool) { } } +// RangeBackwards calls fn for each unexpired item in the cache in reverse order. +// If fn returns false, RangeBackwards stops the iteration. +func (c *Cache[K, V]) RangeBackwards(fn func(item *Item[K, V]) bool) { + c.items.mu.RLock() + + // Check if cache is empty + if c.items.lru.Len() == 0 { + c.items.mu.RUnlock() + return + } + + for item := c.items.lru.Back(); item != c.items.lru.Front().Prev(); item = item.Prev() { + i := item.Value.(*Item[K, V]) + expired := i.isExpiredUnsafe() + c.items.mu.RUnlock() + + if !expired && !fn(i) { + return + } + + if item.Prev() != nil { + c.items.mu.RLock() + } + } +} + // Metrics returns the metrics of the cache. func (c *Cache[K, V]) Metrics() Metrics { c.metricsMu.RLock() diff --git a/cache_test.go b/cache_test.go index ac7db42..8020afc 100644 --- a/cache_test.go +++ b/cache_test.go @@ -329,18 +329,18 @@ func Test_Cache_set(t *testing.T) { // recreate situation when expired item gets updated // and not auto-cleaned up yet. c := New[string, struct{}]( - WithDisableTouchOnHit[string,struct{}](), + WithDisableTouchOnHit[string, struct{}](), ) // insert an item and let it expire c.Set("test", struct{}{}, 1) - time.Sleep(50*time.Millisecond) + time.Sleep(50 * time.Millisecond) // update the expired item updatedItem := c.Set("test", struct{}{}, 100*time.Millisecond) // eviction should not happen as we prolonged element - cl := c.OnEviction(func(_ context.Context, _ EvictionReason, item *Item[string, struct{}]){ + cl := c.OnEviction(func(_ context.Context, _ EvictionReason, item *Item[string, struct{}]) { t.Errorf("eviction happened even though item has not expired yet: key=%s, evicted item expiresAt=%s, updated item expiresAt=%s, now=%s", item.Key(), item.ExpiresAt().String(), @@ -351,7 +351,7 @@ func Test_Cache_set(t *testing.T) { // and update expired before its removal go c.Start() - time.Sleep(90*time.Millisecond) + time.Sleep(90 * time.Millisecond) cl() c.Stop() } @@ -848,6 +848,28 @@ func Test_Cache_Range(t *testing.T) { }) } +func Test_Cache_RangeBackwards(t *testing.T) { + c := prepCache(DefaultTTL) + addToCache(c, time.Nanosecond, "1") + addToCache(c, time.Hour, "2", "3", "4", "5") + + var results []string + + c.RangeBackwards(func(item *Item[string, string]) bool { + results = append(results, item.Key()) + return item.Key() != "4" + }) + + assert.Equal(t, []string{"2", "3", "4"}, results) + + emptyCache := New[string, string]() + assert.NotPanics(t, func() { + emptyCache.RangeBackwards(func(item *Item[string, string]) bool { + return false + }) + }) +} + func Test_Cache_Metrics(t *testing.T) { cache := Cache[string, string]{ metrics: Metrics{Evictions: 10},