Skip to content

Commit

Permalink
Add GetIfExists method to cache
Browse files Browse the repository at this point in the history
  • Loading branch information
motoki317 committed Jun 5, 2023
1 parent 2f44f37 commit 391cae4
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 0 deletions.
25 changes: 25 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,31 @@ retry:
return cl.val.v, cl.err
}

// GetIfExists retrieves an item without triggering value replacements.
//
// This method doesn't wait for value replacement to finish, even if there is an ongoing one.
func (c *cache[K, V]) GetIfExists(key K) (v V, ok bool) {
// Record time as soon as Get is called *before acquiring the lock* - this maximizes the reuse of values
calledAt := monoTimeNow()
c.mu.Lock()
defer c.mu.Unlock()
val, ok := c.values.Get(key)

// value exists (includes stale values)
if ok && !val.isExpired(calledAt, c.ttl) {
if val.isFresh(calledAt, c.freshFor) {
c.stats.Hits++
} else {
c.stats.GraceHits++
}
return val.v, true
}

// value doesn't exist
c.stats.Misses++
return val.v, false
}

// Forget instructs the cache to forget about the key.
// Corresponding item will be deleted, ongoing cache replacement results (if any) will not be added to the cache,
// and any future Get calls will immediately retrieve a new item.
Expand Down
84 changes: 84 additions & 0 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,90 @@ func TestCache_Get_Error(t *testing.T) {
}
}

func TestCache_GetIfExists(t *testing.T) {
t.Parallel()

for _, c := range allCaches(10) {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()

var cnt int64
replaceFn := func(ctx context.Context, key string) (string, error) {
t.Log("replaceFn triggered")
atomic.AddInt64(&cnt, 1)
return "result-" + key, nil
}
cache, err := New[string, string](replaceFn, 500*time.Millisecond, 1*time.Second, c.cacheOpts...)
assert.NoError(t, err)

// Check empty
_, ok := cache.GetIfExists("k1")
assert.False(t, ok)
_, ok = cache.GetIfExists("k2")
assert.False(t, ok)
_, ok = cache.GetIfExists("k3")
assert.False(t, ok)
assert.EqualValues(t, 0, cnt)

// trigger value replacement
val, err := cache.Get(context.Background(), "k1")
assert.NoError(t, err)
assert.Equal(t, "result-k1", val)
assert.EqualValues(t, 1, cnt)
val, err = cache.Get(context.Background(), "k2")
assert.NoError(t, err)
assert.Equal(t, "result-k2", val)
assert.EqualValues(t, 2, cnt)

// Check k1 and k2 are present
val, ok = cache.GetIfExists("k1")
assert.True(t, ok)
assert.Equal(t, "result-k1", val)
val, ok = cache.GetIfExists("k2")
assert.True(t, ok)
assert.Equal(t, "result-k2", val)
_, ok = cache.GetIfExists("k3")
assert.False(t, ok)
assert.EqualValues(t, 2, cnt)

// test graceful hit
time.Sleep(750 * time.Millisecond)
val, ok = cache.GetIfExists("k1")
assert.True(t, ok)
assert.Equal(t, "result-k1", val)
val, ok = cache.GetIfExists("k2")
assert.True(t, ok)
assert.Equal(t, "result-k2", val)
_, ok = cache.GetIfExists("k3")
assert.False(t, ok)
assert.EqualValues(t, 2, cnt)

// test forget
cache.Forget("k2")

val, ok = cache.GetIfExists("k1")
assert.True(t, ok)
assert.Equal(t, "result-k1", val)
_, ok = cache.GetIfExists("k2")
assert.False(t, ok)
_, ok = cache.GetIfExists("k3")
assert.False(t, ok)
assert.EqualValues(t, 2, cnt)

// test expiration
time.Sleep(500 * time.Millisecond)
_, ok = cache.GetIfExists("k1")
assert.False(t, ok)
_, ok = cache.GetIfExists("k2")
assert.False(t, ok)
_, ok = cache.GetIfExists("k3")
assert.False(t, ok)
assert.EqualValues(t, 2, cnt)
})
}
}

// TestCache_Forget_Interrupt ensures that calling Cache.Forget will make later Get calls trigger replaceFn.
func TestCache_Forget_Interrupt(t *testing.T) {
t.Parallel()
Expand Down

0 comments on commit 391cae4

Please sign in to comment.