Skip to content

Commit

Permalink
Add Notify method closes #1
Browse files Browse the repository at this point in the history
  • Loading branch information
motoki317 committed Nov 5, 2023
1 parent 391cae4 commit 4fe1be1
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 0 deletions.
24 changes: 24 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,30 @@ func (c *cache[K, V]) GetIfExists(key K) (v V, ok bool) {
return val.v, false
}

// Notify instructs the cache to retrieve value for key if value does not exist or is stale, in a non-blocking manner.
func (c *cache[K, V]) Notify(key K) {
// Record time as soon as Get is called *before acquiring the lock* - this maximizes the reuse of values
calledAt := monoTimeNow()
c.mu.Lock()
val, ok := c.values.Get(key)

// value exists and is fresh - do nothing
if ok && val.isFresh(calledAt, c.freshFor) {
c.mu.Unlock()
return
}

// value exists and is stale, or value doesn't exist - launch goroutine to update in the background
_, ok = c.calls[key]
if !ok {
cl := &call[V]{}
cl.wg.Add(1)
c.calls[key] = cl
go c.set(context.Background(), cl, key) // Use empty context so as not to be cancelled by the original context
}
c.mu.Unlock()
}

// 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
53 changes: 53 additions & 0 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,59 @@ func TestCache_GetIfExists(t *testing.T) {
}
}

// TestCache_Notify tests that Cache.Notify will replace the value in background.
func TestCache_Notify(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) {
assert.Equal(t, "k1", key)
atomic.AddInt64(&cnt, 1)
time.Sleep(500 * time.Millisecond)
return "result1", nil
}
cache, err := New[string, string](replaceFn, 1*time.Second, 1*time.Second, c.cacheOpts...)
assert.NoError(t, err)

// Start test t=0ms
t0 := time.Now()

// Notify value retrieval - this should launch goroutine in background
cache.Notify("k1")
// Test that value is still not here
v, ok := cache.GetIfExists("k1")

Check failure on line 461 in cache_test.go

View workflow job for this annotation

GitHub Actions / Lint (1.20)

ineffectual assignment to v (ineffassign)
assert.False(t, ok)

time.Sleep(750 * time.Millisecond)
// t=750ms, value should be cached
// Check that both GetIfExists and Get returns value immediately
v, ok = cache.GetIfExists("k1")
assert.True(t, ok)
assert.Equal(t, "result1", v)
assert.InDelta(t, 750*time.Millisecond, time.Since(t0), float64(100*time.Millisecond))
assert.EqualValues(t, 1, cnt)

v, err = cache.Get(context.Background(), "k1")
assert.NoError(t, err)
assert.Equal(t, "result1", v)
assert.InDelta(t, 750*time.Millisecond, time.Since(t0), float64(100*time.Millisecond))
assert.EqualValues(t, 1, cnt)

// t=750ms, notify once again - this should do *nothing*
cache.Notify("k1")

time.Sleep(750 * time.Millisecond)
// t=1500ms, assert that value was replaced only once
assert.EqualValues(t, 1, 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 4fe1be1

Please sign in to comment.