File size: 2,562 Bytes
f1dd159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package resultcache

import (
	"context"
	"errors"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

func TestCacheExpiresAndEvictsOldestEntry(t *testing.T) {
	now := time.Date(2026, 7, 13, 0, 0, 0, 0, time.UTC)
	cache := New[string, int](2, time.Minute)
	cache.Set("first", 1, now)
	cache.Set("second", 2, now.Add(time.Second))
	cache.Set("third", 3, now.Add(2*time.Second))

	if _, ok := cache.Get("first", now.Add(3*time.Second)); ok {
		t.Fatal("oldest cache entry was not evicted")
	}
	if value, ok := cache.Get("third", now.Add(3*time.Second)); !ok || value != 3 {
		t.Fatalf("third entry = %d, %v", value, ok)
	}
	if _, ok := cache.Get("second", now.Add(2*time.Minute)); ok {
		t.Fatal("expired cache entry was returned")
	}
}

func TestCacheDeleteInvalidatesStoredValue(t *testing.T) {
	now := time.Date(2026, 7, 18, 0, 0, 0, 0, time.UTC)
	cache := New[string, int](2, time.Minute)
	cache.Set("account-risk", 1, now)
	cache.Delete("account-risk")

	if _, ok := cache.Get("account-risk", now); ok {
		t.Fatal("deleted cache entry was returned")
	}
}

func TestCacheCoalescesConcurrentLoads(t *testing.T) {
	now := time.Now()
	cache := New[string, int](2, time.Minute)
	var calls atomic.Int32
	start := make(chan struct{})
	var wait sync.WaitGroup
	results := make([]int, 2)
	wait.Add(2)
	for index := range results {
		go func() {
			defer wait.Done()
			<-start
			value, err := cache.Load(context.Background(), "shared", now, func() (int, error) {
				calls.Add(1)
				time.Sleep(10 * time.Millisecond)
				return 42, nil
			})
			if err != nil {
				t.Errorf("load: %v", err)
			}
			results[index] = value
		}()
	}
	close(start)
	wait.Wait()
	if calls.Load() != 1 || results[0] != 42 || results[1] != 42 {
		t.Fatalf("calls = %d, results = %#v", calls.Load(), results)
	}
}

func TestCacheWaiterHonorsCancellation(t *testing.T) {
	cache := New[string, int](2, time.Minute)
	loaderStarted := make(chan struct{})
	releaseLoader := make(chan struct{})
	loaderDone := make(chan struct{})
	go func() {
		defer close(loaderDone)
		_, _ = cache.Load(context.Background(), "shared", time.Now(), func() (int, error) {
			close(loaderStarted)
			<-releaseLoader
			return 42, nil
		})
	}()
	<-loaderStarted

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	if _, err := cache.Load(ctx, "shared", time.Now(), func() (int, error) {
		t.Fatal("canceled waiter unexpectedly became the loader")
		return 0, nil
	}); !errors.Is(err, context.Canceled) {
		t.Fatalf("load error = %v, want context canceled", err)
	}

	close(releaseLoader)
	<-loaderDone
}