From bf7aa68d6b11bbaaec93bc4ca4683e487b462b6e Mon Sep 17 00:00:00 2001 From: samber Date: Mon, 20 May 2024 22:32:02 +0000 Subject: [PATCH] bump v0.4.0 --- config_test.go | 130 --- go.mod | 1 - go.sum | 2 - helpers_test.go | 24 - hot_test.go | 2015 ----------------------------------------------- item_test.go | 146 ---- loader_test.go | 56 -- main_test.go | 11 - 8 files changed, 2385 deletions(-) delete mode 100644 config_test.go delete mode 100644 helpers_test.go delete mode 100644 hot_test.go delete mode 100644 item_test.go delete mode 100644 loader_test.go delete mode 100644 main_test.go diff --git a/config_test.go b/config_test.go deleted file mode 100644 index 79f3c98..0000000 --- a/config_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package hot - -import ( - "testing" - "time" - - "github.com/samber/hot/pkg/safe" - "github.com/stretchr/testify/assert" -) - -func TestComposeInternalCache(t *testing.T) { - is := assert.New(t) - - cache := composeInternalCache[string, int](true, LRU, 42, 0, nil, nil) - is.Equal(42, cache.Capacity()) - is.Equal("lru", cache.Algorithm()) - _, ok := cache.(*safe.SafeInMemoryCache[string, *item[int]]) - is.True(ok) - - cache = composeInternalCache[string, int](true, LFU, 42, 0, nil, nil) - is.Equal(42, cache.Capacity()) - is.Equal("lfu", cache.Algorithm()) - _, ok = cache.(*safe.SafeInMemoryCache[string, *item[int]]) - is.True(ok) - - cache = composeInternalCache[string, int](true, TwoQueue, 42, 0, nil, nil) - is.Equal(52, cache.Capacity()) - is.Equal("2q", cache.Algorithm()) - _, ok = cache.(*safe.SafeInMemoryCache[string, *item[int]]) - is.True(ok) - - is.Panics(func() { - _ = composeInternalCache[string, int](true, ARC, 0, 0, nil, nil) - }) - - cache = composeInternalCache[string, int](false, LRU, 42, 0, nil, nil) - is.Equal(42, cache.Capacity()) - is.Equal("lru", cache.Algorithm()) - _, ok = cache.(*safe.SafeInMemoryCache[string, *item[int]]) - is.False(ok) - - cache = composeInternalCache[string, int](false, LFU, 42, 0, nil, nil) - is.Equal(42, cache.Capacity()) - is.Equal("lfu", cache.Algorithm()) - _, ok = cache.(*safe.SafeInMemoryCache[string, *item[int]]) - is.False(ok) - - cache = composeInternalCache[string, int](false, TwoQueue, 42, 0, nil, nil) - is.Equal(52, cache.Capacity()) - is.Equal("2q", cache.Algorithm()) - _, ok = cache.(*safe.SafeInMemoryCache[string, *item[int]]) - is.False(ok) - - is.Panics(func() { - _ = composeInternalCache[string, int](false, ARC, 0, 0, nil, nil) - }) - -} - -func TestAssertValue(t *testing.T) { - is := assert.New(t) - - is.NotPanics(func() { - assertValue(true, "error") - }) - is.PanicsWithValue("error", func() { - assertValue(false, "error") - }) -} - -func TestHotCacheConfig(t *testing.T) { - is := assert.New(t) - - // loader1 := func(keys []string) (map[string]int, []string, error) { return map[string]int{}, []string{}, nil } - // loader2 := func(keys []string) (map[string]int, []string, error) { return map[string]int{}, []string{}, nil } - // loaders := []Loader[string, int]{loader1, loader2} - // warmUp := func(f func(map[string]int)) error { return nil } - // twice := func(v int) { return v*2 } - - opts := NewHotCache[string, int](LRU, 42) - is.EqualValues(HotCacheConfig[string, int]{LRU, 42, false, 0, 0, 0, 0, 0, 0, nil, false, false, nil, nil, nil, DropOnError, nil, nil, nil}, opts) - - opts = opts.WithMissingSharedCache() - is.EqualValues(HotCacheConfig[string, int]{LRU, 42, true, 0, 0, 0, 0, 0, 0, nil, false, false, nil, nil, nil, DropOnError, nil, nil, nil}, opts) - - opts = NewHotCache[string, int](LRU, 42).WithMissingCache(LFU, 21) - is.EqualValues(HotCacheConfig[string, int]{LRU, 42, false, LFU, 21, 0, 0, 0, 0, nil, false, false, nil, nil, nil, DropOnError, nil, nil, nil}, opts) - - is.Panics(func() { - opts = opts.WithTTL(-42 * time.Second) - }) - opts = opts.WithTTL(42 * time.Second) - is.EqualValues(HotCacheConfig[string, int]{LRU, 42, false, LFU, 21, 42 * time.Second, 0, 0, 0, nil, false, false, nil, nil, nil, DropOnError, nil, nil, nil}, opts) - - is.Panics(func() { - opts = opts.WithRevalidation(-21 * time.Second) - }) - // opts = opts.WithRevalidation(21*time.Second, loader1, loader2) - // is.EqualValues(HotCacheConfig[string, int]{LRU, 42, false, LFU, 21, 42 * time.Second, 21 * time.Second, 0,0, nil, false, false, nil, nil, loaders,DropOnError, nil,nil, nil}, opts) - - is.Panics(func() { - opts = opts.WithJitter(-0.1) - }) - is.Panics(func() { - opts = opts.WithJitter(1.1) - }) - opts = opts.WithJitter(0.1) - is.EqualValues(HotCacheConfig[string, int]{LRU, 42, false, LFU, 21, 42 * time.Second, 0, 0.1, 0, nil, false, false, nil, nil, nil, DropOnError, nil, nil, nil}, opts) - - // opts = opts.WithWarmUp(warmUp) - // is.EqualValues(HotCacheConfig[string, int]{LRU, 42, false, LFU, 21, 42 * time.Second, 0, 0.1, 0, nil,false, false, warmUp, nil, nil,DropOnError,nil, nil, nil}, opts) - - opts = opts.WithoutLocking() - is.EqualValues(HotCacheConfig[string, int]{LRU, 42, false, LFU, 21, 42 * time.Second, 0, 0.1, 0, nil, true, false, nil, nil, nil, DropOnError, nil, nil, nil}, opts) - - opts = opts.WithJanitor() - is.EqualValues(HotCacheConfig[string, int]{LRU, 42, false, LFU, 21, 42 * time.Second, 0, 0.1, 0, nil, true, true, nil, nil, nil, DropOnError, nil, nil, nil}, opts) - - // opts = opts.WithCopyOnRead(twice) - // is.EqualValues(HotCacheConfig[string, int]{LRU, 42, false, LFU, 21, 42 * time.Second, 0, 0.1,0, nil, true, true, nil, nil,nil,DropOnError, nil, twice, nil}, opts) - - // opts = opts.WithCopyOnRead(twice) - // is.EqualValues(HotCacheConfig[string, int]{LRU, 42, false, LFU, 21, 42 * time.Second, 0, 0.1, 0, nil,true, true, nil, nil,nil,DropOnError, nil, twice, twice}, opts) - - is.Panics(func() { - opts.Build() - }) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} diff --git a/go.mod b/go.mod index 12248d6..e6b3c36 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/prometheus/client_golang v1.19.0 github.com/samber/go-singleflightx v0.3.0 github.com/stretchr/testify v1.9.0 - go.uber.org/goleak v1.3.0 ) require ( diff --git a/go.sum b/go.sum index b24080b..cdca867 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,6 @@ github.com/samber/go-singleflightx v0.3.0 h1:FZWGYtYp4VfvnoCsk7gDuDn/XqNhI58p/VS github.com/samber/go-singleflightx v0.3.0/go.mod h1:X2BR+oheHIYc73PvxRMlcASg6KYYTQyUYpdVU7t/ux4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= diff --git a/helpers_test.go b/helpers_test.go deleted file mode 100644 index 00b42e5..0000000 --- a/helpers_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package hot - -import ( - "os" - "testing" - "time" -) - -// https://github.com/stretchr/testify/issues/1101 -func testWithTimeout(t *testing.T, timeout time.Duration) { //nolint:unused - t.Helper() - - testFinished := make(chan struct{}) - t.Cleanup(func() { close(testFinished) }) - - go func() { - select { - case <-testFinished: - case <-time.After(timeout): - t.Errorf("test timed out after %s", timeout) - os.Exit(1) - } - }() -} diff --git a/hot_test.go b/hot_test.go deleted file mode 100644 index 053a2e1..0000000 --- a/hot_test.go +++ /dev/null @@ -1,2015 +0,0 @@ -package hot - -import ( - "sync/atomic" - "testing" - "time" - - "github.com/samber/go-singleflightx" - "github.com/samber/hot/pkg/safe" - "github.com/stretchr/testify/assert" -) - -func TestNewHotCache(t *testing.T) { - is := assert.New(t) - - lru := composeInternalCache[int, int](false, LRU, 42, 0, nil, nil) - safeLru := composeInternalCache[int, int](true, LRU, 42, 0, nil, nil) - - // locking - cache := newHotCache(lru, false, nil, 0, 0, 0, nil, nil, DropOnError, nil, nil, nil) - _, ok := cache.cache.(*safe.SafeInMemoryCache[int, *item[int]]) - is.False(ok) - cache = newHotCache(safeLru, false, safeLru, 0, 0, 0, nil, nil, DropOnError, nil, nil, nil) - _, ok = cache.cache.(*safe.SafeInMemoryCache[int, *item[int]]) - is.True(ok) - _, ok = cache.missingCache.(*safe.SafeInMemoryCache[int, *item[int]]) - is.True(ok) - - // ttl, stale, jitter - cache = newHotCache(safeLru, false, nil, 42_000, 21_000, 0.1, nil, nil, DropOnError, nil, nil, nil) - cache.metrics = nil - is.EqualValues(&HotCache[int, int]{nil, nil, nil, safeLru, false, nil, 42, 21, 0.1, nil, nil, DropOnError, nil, nil, nil, singleflightx.Group[int, int]{}, nil}, cache) - - // @TODO: test locks - // @TODO: more tests -} - -func TestHotCache_Set(t *testing.T) { - is := assert.New(t) - - // simple set - cache := NewHotCache[string, int](LRU, 10). - Build() - cache.Set("a", 1) - is.Equal(1, cache.cache.Len()) - v, ok := cache.cache.Get("a") - is.True(ok) - is.EqualValues(&item[int]{true, 1, 8, 0, 0}, v) - - // simple set with copy on write - cache = NewHotCache[string, int](LRU, 10). - WithCopyOnWrite(func(v int) int { - return v * 2 - }). - Build() - cache.Set("a", 1) - is.Equal(1, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.EqualValues(&item[int]{true, 2, 8, 0, 0}, v) - - // simple set with default ttl + stale + jitter - cache = NewHotCache[string, int](LRU, 10). - WithTTL(1 * time.Second). - WithRevalidation(1 * time.Second). - WithJitter(0.1). - Build() - cache.Set("a", 1) - is.Equal(1, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+1_000_000, v.expiryMicro, 1_100_000) - is.InEpsilon(time.Now().UnixNano()+1_000_000+1_000_000, v.staleExpiryMicro, 1_100_000) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_SetMissing(t *testing.T) { - is := assert.New(t) - - // no missing cache - cache := NewHotCache[string, int](LRU, 10). - Build() - is.Panics(func() { - cache.SetMissing("a") - }) - - // dedicated cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 42). - WithTTL(1 * time.Second). - WithRevalidation(100 * time.Millisecond). - WithJitter(0.1). - Build() - cache.SetMissing("a") - is.Equal(0, cache.cache.Len()) - is.Equal(1, cache.missingCache.Len()) - v, ok := cache.missingCache.Get("a") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+1_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+1_000_000+100_000, v.staleExpiryMicro, 110_000) - - // shared cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - WithTTL(1 * time.Second). - WithRevalidation(100 * time.Millisecond). - WithJitter(0.1). - Build() - cache.SetMissing("a") - is.Equal(1, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+1_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+1_000_000+100_000, v.staleExpiryMicro, 110_000) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_SetWithTTL(t *testing.T) { - is := assert.New(t) - - // simple set - cache := NewHotCache[string, int](LRU, 10). - Build() - cache.SetWithTTL("a", 1, 10*time.Second) - is.Equal(1, cache.cache.Len()) - v, ok := cache.cache.Get("a") - is.True(ok) - is.Equal(1, v.value) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 10_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 10_000) - - // simple set with copy on write - cache = NewHotCache[string, int](LRU, 10). - WithCopyOnWrite(func(v int) int { - return v * 2 - }). - Build() - cache.SetWithTTL("a", 1, 10*time.Second) - is.Equal(1, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.Equal(2, v.value) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 10_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 10_000) - - // simple set with default ttl + stale + jitter - cache = NewHotCache[string, int](LRU, 10). - WithTTL(1 * time.Second). - WithRevalidation(100 * time.Millisecond). - WithJitter(0.1). - Build() - cache.SetWithTTL("a", 1, 10*time.Second) - is.Equal(1, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 110_000) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_SetMissingWithTTL(t *testing.T) { - is := assert.New(t) - - // no missing cache - cache := NewHotCache[string, int](LRU, 10). - Build() - is.Panics(func() { - cache.SetMissingWithTTL("a", 10*time.Second) - }) - - // dedicated cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 42). - WithTTL(1 * time.Second). - WithRevalidation(100 * time.Millisecond). - WithJitter(0.1). - Build() - cache.SetMissingWithTTL("a", 10*time.Second) - is.Equal(0, cache.cache.Len()) - is.Equal(1, cache.missingCache.Len()) - v, ok := cache.missingCache.Get("a") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 110_000) - - // shared cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - WithTTL(1 * time.Second). - WithRevalidation(100 * time.Millisecond). - WithJitter(0.1). - Build() - cache.SetMissingWithTTL("a", 10*time.Second) - is.Equal(1, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 110_000) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_SetMany(t *testing.T) { - is := assert.New(t) - - // simple set - cache := NewHotCache[string, int](LRU, 10). - Build() - cache.SetMany(map[string]int{"a": 1, "b": 2}) - is.Equal(2, cache.cache.Len()) - v, ok := cache.cache.Get("a") - is.True(ok) - is.EqualValues(&item[int]{true, 1, 8, 0, 0}, v) - v, ok = cache.cache.Get("b") - is.True(ok) - is.EqualValues(&item[int]{true, 2, 8, 0, 0}, v) - - // simple set with copy on write - cache = NewHotCache[string, int](LRU, 10). - WithCopyOnWrite(func(v int) int { - return v * 2 - }). - Build() - cache.SetMany(map[string]int{"a": 1, "b": 2}) - is.Equal(2, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.EqualValues(&item[int]{true, 2, 8, 0, 0}, v) - v, ok = cache.cache.Get("b") - is.True(ok) - is.EqualValues(&item[int]{true, 4, 8, 0, 0}, v) - - // simple set with default ttl + stale + jitter - cache = NewHotCache[string, int](LRU, 10). - WithTTL(1 * time.Second). - WithRevalidation(100 * time.Millisecond). - WithJitter(0.1). - Build() - cache.SetMany(map[string]int{"a": 1, "b": 2}) - is.Equal(2, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+1_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+1_000_000+100_000, v.staleExpiryMicro, 110_000) - v, ok = cache.cache.Get("b") - is.True(ok) - is.True(v.hasValue) - is.Equal(2, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+1_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+1_000_000+100_000, v.staleExpiryMicro, 110_000) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_SetMissingMany(t *testing.T) { - is := assert.New(t) - - // no missing cache - cache := NewHotCache[string, int](LRU, 10). - Build() - is.Panics(func() { - cache.SetMissingMany([]string{"a", "b"}) - }) - - // dedicated cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 42). - WithTTL(1 * time.Second). - WithRevalidation(100 * time.Millisecond). - WithJitter(0.1). - Build() - cache.SetMissingMany([]string{"a", "b"}) - is.Equal(0, cache.cache.Len()) - is.Equal(2, cache.missingCache.Len()) - v, ok := cache.missingCache.Get("a") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+1_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+1_000_000+100_000, v.staleExpiryMicro, 110_000) - v, ok = cache.missingCache.Get("b") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+1_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+1_000_000+100_000, v.staleExpiryMicro, 110_000) - - // shared cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - WithTTL(1 * time.Second). - WithRevalidation(100 * time.Millisecond). - WithJitter(0.1). - Build() - cache.SetMissingMany([]string{"a", "b"}) - is.Equal(2, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+1_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+1_000_000+100_000, v.staleExpiryMicro, 110_000) - v, ok = cache.cache.Get("b") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+1_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+1_000_000+100_000, v.staleExpiryMicro, 110_000) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_SetManyWithTTL(t *testing.T) { - is := assert.New(t) - - // simple set - cache := NewHotCache[string, int](LRU, 10). - Build() - cache.SetManyWithTTL(map[string]int{"a": 1, "b": 2}, 10*time.Second) - is.Equal(2, cache.cache.Len()) - v, ok := cache.cache.Get("a") - is.True(ok) - is.Equal(1, v.value) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 10_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 10_000) - v, ok = cache.cache.Get("b") - is.True(ok) - is.Equal(2, v.value) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 10_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 10_000) - - // simple set with copy on write - cache = NewHotCache[string, int](LRU, 10). - WithCopyOnWrite(func(v int) int { - return v * 2 - }). - Build() - cache.SetManyWithTTL(map[string]int{"a": 1, "b": 2}, 10*time.Second) - is.Equal(2, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.Equal(2, v.value) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 10_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 10_000) - v, ok = cache.cache.Get("b") - is.True(ok) - is.Equal(4, v.value) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 10_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 10_000) - - // simple set with default ttl + stale + jitter - cache = NewHotCache[string, int](LRU, 10). - WithTTL(1 * time.Second). - WithRevalidation(100 * time.Millisecond). - WithJitter(0.1). - Build() - cache.SetManyWithTTL(map[string]int{"a": 1, "b": 2}, 10*time.Second) - is.Equal(2, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 110_000) - v, ok = cache.cache.Get("b") - is.True(ok) - is.True(v.hasValue) - is.Equal(2, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 110_000) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_SetMissingManyWithTTL(t *testing.T) { - is := assert.New(t) - - // no missing cache - cache := NewHotCache[string, int](LRU, 10). - Build() - is.Panics(func() { - cache.SetMissingManyWithTTL([]string{"a", "b"}, 10*time.Second) - }) - - // dedicated cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 42). - WithTTL(1 * time.Second). - WithRevalidation(100 * time.Millisecond). - WithJitter(0.1). - Build() - cache.SetMissingManyWithTTL([]string{"a", "b"}, 10*time.Second) - is.Equal(0, cache.cache.Len()) - is.Equal(2, cache.missingCache.Len()) - v, ok := cache.missingCache.Get("a") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 110_000) - v, ok = cache.missingCache.Get("b") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 110_000) - - // shared cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - WithTTL(1 * time.Second). - WithRevalidation(100 * time.Millisecond). - WithJitter(0.1). - Build() - cache.SetMissingManyWithTTL([]string{"a", "b"}, 10*time.Second) - is.Equal(2, cache.cache.Len()) - v, ok = cache.cache.Get("a") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 110_000) - v, ok = cache.cache.Get("b") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.NotEqual(v.expiryMicro, v.staleExpiryMicro) - is.InEpsilon(time.Now().UnixNano()+10_000_000, v.expiryMicro, 110_000) - is.InEpsilon(time.Now().UnixNano()+10_000_000+100_000, v.staleExpiryMicro, 110_000) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_Has(t *testing.T) { - is := assert.New(t) - - // simple - cache := NewHotCache[string, int](LRU, 10). - Build() - is.False(cache.Has("a")) - cache.Set("a", 1) - is.True(cache.Has("a")) - - // with shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - is.False(cache.Has("a")) - cache.Set("a", 1) - is.True(cache.Has("a")) - cache.SetMissing("a") - is.False(cache.Has("a")) - - // with dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 10). - Build() - is.False(cache.Has("a")) - cache.Set("a", 1) - is.True(cache.Has("a")) - cache.SetMissing("a") - is.False(cache.Has("a")) -} - -func TestHotCache_HasMany(t *testing.T) { - is := assert.New(t) - - // simple - cache := NewHotCache[string, int](LRU, 10). - Build() - is.Equal(map[string]bool{"a": false, "b": false}, cache.HasMany([]string{"a", "b"})) - cache.Set("a", 1) - is.Equal(map[string]bool{"a": true, "b": false}, cache.HasMany([]string{"a", "b"})) - cache.Set("b", 2) - is.Equal(map[string]bool{"a": true, "b": true}, cache.HasMany([]string{"a", "b"})) - - // with shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - is.Equal(map[string]bool{"a": false, "b": false}, cache.HasMany([]string{"a", "b"})) - cache.Set("a", 1) - is.Equal(map[string]bool{"a": true, "b": false}, cache.HasMany([]string{"a", "b"})) - cache.SetMissing("b") - is.Equal(map[string]bool{"a": true, "b": false}, cache.HasMany([]string{"a", "b"})) - - // with dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 10). - Build() - is.Equal(map[string]bool{"a": false, "b": false}, cache.HasMany([]string{"a", "b"})) - cache.Set("a", 1) - is.Equal(map[string]bool{"a": true, "b": false}, cache.HasMany([]string{"a", "b"})) - cache.SetMissing("b") - is.Equal(map[string]bool{"a": true, "b": false}, cache.HasMany([]string{"a", "b"})) -} - -func TestHotCache_Get(t *testing.T) { - is := assert.New(t) - - // simple - cache := NewHotCache[string, int](LRU, 10). - Build() - v, ok, err := cache.Get("a") - is.False(ok) - is.Nil(err) - is.Equal(0, v) - cache.Set("a", 42) - v, ok, err = cache.Get("a") - is.True(ok) - is.Nil(err) - is.Equal(42, v) - - // with shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - v, ok, err = cache.Get("a") - is.False(ok) - is.Nil(err) - is.Equal(0, v) - cache.Set("a", 42) - v, ok, err = cache.Get("a") - is.True(ok) - is.Nil(err) - is.Equal(42, v) - cache.SetMissing("a") - v, ok, err = cache.Get("a") - is.False(ok) - is.Nil(err) - is.Equal(0, v) - - // with dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 10). - Build() - v, ok, err = cache.Get("a") - is.False(ok) - is.Nil(err) - is.Equal(0, v) - cache.Set("a", 42) - v, ok, err = cache.Get("a") - is.True(ok) - is.Nil(err) - is.Equal(42, v) - cache.SetMissing("a") - v, ok, err = cache.Get("a") - is.False(ok) - is.Nil(err) - is.Equal(0, v) - - // with copy on read - cache = NewHotCache[string, int](LRU, 10). - WithCopyOnRead(func(v int) int { - return v * 2 - }). - Build() - v, ok, err = cache.Get("a") - is.False(ok) - is.Nil(err) - is.Equal(0, v) - cache.Set("a", 42) - v, ok, err = cache.Get("a") - is.True(ok) - is.Nil(err) - is.Equal(84, v) - - // with loader - cache = NewHotCache[string, int](LRU, 10). - WithLoaders(func(keys []string) (map[string]int, error) { - is.Equal([]string{"a"}, keys) - return map[string]int{"a": 42}, nil - }). - Build() - v, ok, err = cache.Get("a") - is.True(ok) - is.Nil(err) - is.Equal(42, v) - - // with failed loader - cache = NewHotCache[string, int](LRU, 10). - WithLoaders(func(keys []string) (map[string]int, error) { - is.Equal([]string{"a"}, keys) - return map[string]int{"a": 42}, assert.AnError - }). - Build() - v, ok, err = cache.Get("a") - is.False(ok) - is.Error(assert.AnError, err) - is.Equal(0, v) - - // with loader not found - cache = NewHotCache[string, int](LRU, 10). - WithLoaders(func(keys []string) (map[string]int, error) { - is.Equal([]string{"a"}, keys) - return map[string]int{}, nil - }). - Build() - v, ok, err = cache.Get("a") - is.False(ok) - is.Nil(err) - is.Equal(0, v) - - // with loader chain - loaded := 0 - cache = NewHotCache[string, int](LRU, 10). - WithLoaders( - func(keys []string) (map[string]int, error) { - loaded++ - is.Equal([]string{"a"}, keys) - return map[string]int{}, nil - }, - func(keys []string) (map[string]int, error) { - loaded++ - is.Equal([]string{"a"}, keys) - return nil, nil - }, - func(keys []string) (map[string]int, error) { - loaded++ - is.Equal([]string{"a"}, keys) - return map[string]int{"a": 42}, nil - }, - ). - Build() - v, ok, err = cache.Get("a") - is.True(ok) - is.Nil(err) - is.Equal(42, v) - is.Equal(3, loaded) -} - -// func TestHotCache_GetWithLoaders(t *testing.T) { -// } - -// func TestHotCache_GetMany(t *testing.T) { -// } - -// func TestHotCache_GetManyWithLoaders(t *testing.T) { -// } - -func TestHotCache_Peek(t *testing.T) { - is := assert.New(t) - - counter := int32(0) - - // simple - cache := NewHotCache[string, int](LRU, 10). - WithLoaders(func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter, 1) - return map[string]int{"a": 42}, nil - }). - WithCopyOnRead(func(nb int) int { - return nb * 2 - }). - Build() - v, ok := cache.Peek("a") - is.False(ok) - is.Equal(0, v) - cache.Set("a", 1) - v, ok = cache.Peek("a") - is.True(ok) - is.Equal(2, v) - is.Equal(int32(0), atomic.LoadInt32(&counter)) - - // shared missing - cache = NewHotCache[string, int](LRU, 10). - WithLoaders(func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter, 1) - return map[string]int{"a": 42}, nil - }). - WithCopyOnRead(func(nb int) int { - return nb * 2 - }). - WithMissingSharedCache(). - Build() - v, ok = cache.Peek("a") - is.False(ok) - is.Equal(0, v) - cache.Set("a", 1) - v, ok = cache.Peek("a") - is.True(ok) - is.Equal(2, v) - is.Equal(int32(0), atomic.LoadInt32(&counter)) - - // dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithLoaders(func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter, 1) - return map[string]int{"a": 42}, nil - }). - WithCopyOnRead(func(nb int) int { - return nb * 2 - }). - WithMissingCache(LRU, 10). - Build() - v, ok = cache.Peek("a") - is.False(ok) - is.Equal(0, v) - cache.Set("a", 1) - v, ok = cache.Peek("a") - is.True(ok) - is.Equal(2, v) - is.Equal(int32(0), atomic.LoadInt32(&counter)) -} - -func TestHotCache_PeekMany(t *testing.T) { - is := assert.New(t) - - counter := int32(0) - - // simple - cache := NewHotCache[string, int](LRU, 10). - WithLoaders(func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter, 1) - return map[string]int{"a": 42, "b": 84}, nil - }). - WithCopyOnRead(func(nb int) int { - return nb * 2 - }). - Build() - v, missing := cache.PeekMany([]string{"a", "b", "c"}) - is.EqualValues(map[string]int{}, v) - is.EqualValues([]string{"a", "b", "c"}, missing) - cache.Set("a", 1) - cache.Set("b", 2) - v, missing = cache.PeekMany([]string{"a", "b", "c"}) - is.EqualValues(map[string]int{"a": 2, "b": 4}, v) - is.EqualValues([]string{"c"}, missing) - is.Equal(int32(0), atomic.LoadInt32(&counter)) - - // shared missing - cache = NewHotCache[string, int](LRU, 10). - WithLoaders(func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter, 1) - return map[string]int{"a": 42, "b": 84}, nil - }). - WithCopyOnRead(func(nb int) int { - return nb * 2 - }). - WithMissingSharedCache(). - Build() - v, missing = cache.PeekMany([]string{"a", "b", "c"}) - is.EqualValues(map[string]int{}, v) - is.EqualValues([]string{"a", "b", "c"}, missing) - cache.Set("a", 1) - cache.Set("b", 2) - v, missing = cache.PeekMany([]string{"a", "b", "c"}) - is.EqualValues(map[string]int{"a": 2, "b": 4}, v) - is.EqualValues([]string{"c"}, missing) - is.Equal(int32(0), atomic.LoadInt32(&counter)) - - // dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithLoaders(func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter, 1) - return map[string]int{"a": 42, "b": 84}, nil - }). - WithCopyOnRead(func(nb int) int { - return nb * 2 - }). - WithMissingCache(LRU, 10). - Build() - v, missing = cache.PeekMany([]string{"a", "b", "c"}) - is.EqualValues(map[string]int{}, v) - is.EqualValues([]string{"a", "b", "c"}, missing) - cache.Set("a", 1) - cache.Set("b", 2) - v, missing = cache.PeekMany([]string{"a", "b", "c"}) - is.EqualValues(map[string]int{"a": 2, "b": 4}, v) - is.EqualValues([]string{"c"}, missing) - is.Equal(int32(0), atomic.LoadInt32(&counter)) -} - -func TestHotCache_Keys(t *testing.T) { - is := assert.New(t) - - // simple - cache := NewHotCache[string, int](LRU, 10). - Build() - is.Equal([]string{}, cache.Keys()) - cache.Set("a", 1) - is.Equal([]string{"a"}, cache.Keys()) - cache.Set("b", 2) - is.ElementsMatch([]string{"a", "b"}, cache.Keys()) - - // with shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - is.Equal([]string{}, cache.Keys()) - cache.Set("a", 1) - is.Equal([]string{"a"}, cache.Keys()) - cache.Set("b", 2) - is.ElementsMatch([]string{"a", "b"}, cache.Keys()) - cache.SetMissing("c") - is.ElementsMatch([]string{"a", "b"}, cache.Keys()) - - // with dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 10). - Build() - is.Equal([]string{}, cache.Keys()) - cache.Set("a", 1) - is.Equal([]string{"a"}, cache.Keys()) - cache.Set("b", 2) - is.ElementsMatch([]string{"a", "b"}, cache.Keys()) - cache.SetMissing("c") - is.ElementsMatch([]string{"a", "b"}, cache.Keys()) -} - -func TestHotCache_Values(t *testing.T) { - is := assert.New(t) - - // simple - cache := NewHotCache[string, int](LRU, 10). - Build() - is.ElementsMatch([]int{}, cache.Values()) - cache.Set("a", 1) - is.ElementsMatch([]int{1}, cache.Values()) - cache.Set("b", 2) - is.ElementsMatch([]int{1, 2}, cache.Values()) - - // with shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - is.ElementsMatch([]int{}, cache.Values()) - cache.Set("a", 1) - is.ElementsMatch([]int{1}, cache.Values()) - cache.Set("b", 2) - is.ElementsMatch([]int{1, 2}, cache.Values()) - cache.SetMissing("c") - is.ElementsMatch([]int{1, 2}, cache.Values()) - - // with dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 10). - Build() - is.ElementsMatch([]int{}, cache.Values()) - cache.Set("a", 1) - is.ElementsMatch([]int{1}, cache.Values()) - cache.Set("b", 2) - is.ElementsMatch([]int{1, 2}, cache.Values()) - cache.SetMissing("c") - is.ElementsMatch([]int{1, 2}, cache.Values()) -} - -func TestHotCache_Range(t *testing.T) { - is := assert.New(t) - - // normal - cache := NewHotCache[string, int](LRU, 10). - Build() - - counter1 := int32(0) - cache.Range(func(string, int) bool { - atomic.AddInt32(&counter1, 1) - return true - }) - is.Equal(int32(0), atomic.LoadInt32(&counter1)) - cache.Set("a", 1) - cache.Set("b", 2) - cache.Range(func(string, int) bool { - atomic.AddInt32(&counter1, 1) - return true - }) - is.Equal(int32(2), atomic.LoadInt32(&counter1)) - cache.Range(func(string, int) bool { - atomic.AddInt32(&counter1, 1) - return false - }) - is.Equal(int32(3), atomic.LoadInt32(&counter1)) - - // shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - - counter2 := int32(0) - cache.Range(func(string, int) bool { - atomic.AddInt32(&counter2, 1) - return true - }) - is.Equal(int32(0), atomic.LoadInt32(&counter2)) - cache.Set("a", 1) - cache.Set("b", 2) - cache.SetMissing("c") - cache.Range(func(string, int) bool { - atomic.AddInt32(&counter2, 1) - return true - }) - is.Equal(int32(2), atomic.LoadInt32(&counter2)) - cache.Range(func(string, int) bool { - atomic.AddInt32(&counter2, 1) - return false - }) - is.Equal(int32(3), atomic.LoadInt32(&counter2)) - - // dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - - counter3 := int32(0) - cache.Range(func(string, int) bool { - atomic.AddInt32(&counter3, 1) - return true - }) - is.Equal(int32(0), atomic.LoadInt32(&counter3)) - cache.Set("a", 1) - cache.Set("b", 2) - cache.SetMissing("c") - cache.Range(func(string, int) bool { - atomic.AddInt32(&counter3, 1) - return true - }) - is.Equal(int32(2), atomic.LoadInt32(&counter3)) - cache.Range(func(string, int) bool { - atomic.AddInt32(&counter3, 1) - return false - }) - is.Equal(int32(3), atomic.LoadInt32(&counter3)) - -} - -func TestHotCache_Delete(t *testing.T) { - is := assert.New(t) - - // normal - cache := NewHotCache[string, int](LRU, 10). - Build() - cache.Set("a", 1) - is.Equal(1, cache.Len()) - is.True(cache.Delete("a")) - is.False(cache.Delete("a")) - is.Equal(0, cache.Len()) - - // shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - cache.Set("a", 1) - cache.SetMissing("b") - is.Equal(2, cache.Len()) - is.True(cache.Delete("a")) - is.False(cache.Delete("a")) - is.True(cache.Delete("b")) - is.False(cache.Delete("b")) - is.Equal(0, cache.Len()) - - // dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LFU, 42). - Build() - cache.Set("a", 1) - cache.SetMissing("b") - is.Equal(2, cache.Len()) - is.True(cache.Delete("a")) - is.False(cache.Delete("a")) - is.True(cache.Delete("b")) - is.False(cache.Delete("b")) - is.Equal(0, cache.Len()) -} - -func TestHotCache_DeleteMany(t *testing.T) { - is := assert.New(t) - - // normal - cache := NewHotCache[string, int](LRU, 10). - Build() - cache.Set("a", 1) - is.Equal(1, cache.Len()) - is.EqualValues(map[string]bool{"a": true, "b": false}, cache.DeleteMany([]string{"a", "b"})) - is.EqualValues(map[string]bool{"a": false, "b": false}, cache.DeleteMany([]string{"a", "b"})) - is.Equal(0, cache.Len()) - - // shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - cache.Set("a", 1) - cache.SetMissing("b") - is.Equal(2, cache.Len()) - is.EqualValues(map[string]bool{"a": true, "b": true, "c": false}, cache.DeleteMany([]string{"a", "b", "c"})) - is.EqualValues(map[string]bool{"a": false, "b": false, "c": false}, cache.DeleteMany([]string{"a", "b", "c"})) - is.Equal(0, cache.Len()) - - // dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LFU, 42). - Build() - cache.Set("a", 1) - cache.SetMissing("b") - is.Equal(2, cache.Len()) - is.EqualValues(map[string]bool{"a": true, "b": true, "c": false}, cache.DeleteMany([]string{"a", "b", "c"})) - is.EqualValues(map[string]bool{"a": false, "b": false, "c": false}, cache.DeleteMany([]string{"a", "b", "c"})) - is.Equal(0, cache.Len()) -} - -func TestHotCache_Purge(t *testing.T) { - is := assert.New(t) - - // normal - cache := NewHotCache[string, int](LRU, 10). - Build() - cache.Set("a", 1) - is.Equal(1, cache.Len()) - cache.Purge() - is.Equal(0, cache.Len()) - - // shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - cache.Set("a", 1) - cache.SetMissing("b") - is.Equal(2, cache.Len()) - cache.Purge() - is.Equal(0, cache.Len()) - - // dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LFU, 42). - Build() - cache.Set("a", 1) - cache.SetMissing("b") - is.Equal(2, cache.Len()) - cache.Purge() - is.Equal(0, cache.Len()) -} - -func TestHotCache_Capacity(t *testing.T) { - is := assert.New(t) - - // normal - cache := NewHotCache[string, int](LRU, 10). - Build() - a, b := cache.Capacity() - is.Equal(10, a) - is.Equal(0, b) - - // shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - a, b = cache.Capacity() - is.Equal(10, a) - is.Equal(0, b) - - // dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LFU, 42). - Build() - a, b = cache.Capacity() - is.Equal(10, a) - is.Equal(42, b) -} - -func TestHotCache_Algorithm(t *testing.T) { - is := assert.New(t) - - // normal - cache := NewHotCache[string, int](LRU, 10). - Build() - a, b := cache.Algorithm() - is.Equal("lru", a) - is.Equal("", b) - - // shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - a, b = cache.Algorithm() - is.Equal("lru", a) - is.Equal("", b) - - // dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LFU, 42). - Build() - a, b = cache.Algorithm() - is.Equal("lru", a) - is.Equal("lfu", b) -} - -func TestHotCache_Len(t *testing.T) { - is := assert.New(t) - - // normal - cache := NewHotCache[string, int](LRU, 10). - Build() - is.Equal(0, cache.Len()) - cache.Set("a", 1) - is.Equal(1, cache.Len()) - cache.Set("b", 2) - is.Equal(2, cache.Len()) - - // shared missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - is.Equal(0, cache.Len()) - cache.Set("a", 1) - cache.SetMissing("c") - is.Equal(2, cache.Len()) - cache.Set("b", 2) - cache.SetMissing("d") - is.Equal(4, cache.Len()) - - // dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LFU, 42). - Build() - is.Equal(0, cache.Len()) - cache.Set("a", 1) - cache.SetMissing("c") - is.Equal(2, cache.Len()) - cache.Set("b", 2) - cache.SetMissing("d") - is.Equal(4, cache.Len()) -} - -func TestHotCache_WarmUp(t *testing.T) { - is := assert.New(t) - - is.Panics(func() { - _ = NewHotCache[string, int](LRU, 10). - WithCopyOnWrite(func(nb int) int { - return nb * 2 - }). - WithWarmUp(func() (map[string]int, []string, error) { - return map[string]int{"a": 1}, []string{"b"}, nil - }). - Build() - }) - - // simple - cache := NewHotCache[string, int](LRU, 10). - WithCopyOnWrite(func(nb int) int { - return nb * 2 - }). - WithWarmUp(func() (map[string]int, []string, error) { - return map[string]int{"a": 1}, []string{}, nil - }). - Build() - time.Sleep(5 * time.Millisecond) - v, ok, err := cache.Get("a") - is.True(ok) - is.Nil(err) - is.Equal(2, v) - - // with shared missing - cache = NewHotCache[string, int](LRU, 10). - WithCopyOnWrite(func(nb int) int { - return nb * 2 - }). - WithWarmUp(func() (map[string]int, []string, error) { - return map[string]int{"a": 1}, []string{"b"}, nil - }). - WithMissingSharedCache(). - Build() - time.Sleep(5 * time.Millisecond) - v2, ok2 := cache.cache.Get("a") - is.True(ok2) - is.True(v2.hasValue) - is.Equal(2, v2.value) - v2, ok2 = cache.cache.Get("b") - is.True(ok2) - is.False(v2.hasValue) - is.Equal(0, v2.value) - - // with dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithCopyOnWrite(func(nb int) int { - return nb * 2 - }). - WithWarmUp(func() (map[string]int, []string, error) { - return map[string]int{"a": 1}, []string{"b"}, nil - }). - WithMissingCache(LRU, 10). - Build() - time.Sleep(5 * time.Millisecond) - v2, ok2 = cache.cache.Get("a") - is.True(ok2) - is.True(v2.hasValue) - is.Equal(2, v2.value) - v2, ok2 = cache.missingCache.Get("b") - is.True(ok2) - is.False(v2.hasValue) - is.Equal(0, v2.value) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_Janitor(t *testing.T) { - is := assert.New(t) - - // simple - cache := NewHotCache[string, int](LRU, 10). - WithTTL(3*time.Millisecond). - WithRevalidation(20*time.Millisecond, func(keys []string) (found map[string]int, err error) { - return map[string]int{"a": 2}, nil - }). - WithJanitor(). - Build() - - cache.Set("a", 1) - is.Equal(1, cache.Len()) - time.Sleep(10 * time.Millisecond) - is.Equal(1, cache.Len()) - time.Sleep(30 * time.Millisecond) - is.Equal(0, cache.Len()) - - cache.StopJanitor() - - // with dedicated missing - cache = NewHotCache[string, int](LRU, 10). - WithTTL(3*time.Millisecond). - WithRevalidation(20*time.Millisecond, func(keys []string) (found map[string]int, err error) { - return map[string]int{"a": 2}, nil - }). - WithMissingCache(LRU, 10). - WithJanitor(). - Build() - - cache.Set("a", 1) - cache.SetMissing("b") - is.Equal(2, cache.Len()) - time.Sleep(10 * time.Millisecond) - is.Equal(2, cache.Len()) - time.Sleep(25 * time.Millisecond) - is.Equal(0, cache.Len()) - - cache.StopJanitor() - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_setUnsafe_noMissingCache(t *testing.T) { - is := assert.New(t) - - cache := NewHotCache[string, int](LRU, 10). - Build() - - cache.setUnsafe("a", false, 1, 0) // no value + no ttl + no jitter - v, ok := cache.cache.Get("a") - is.False(ok) - is.Nil(v) - - cache.setUnsafe("a", true, 1, 0) // value + no ttl + no jitter - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - cache.setUnsafe("a", true, 1, 100) // value + ttl + no jitter - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(int64(0), v.expiryMicro) - is.NotEqual(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - cache = NewHotCache[string, int](LRU, 10). - WithJitter(0.5). - Build() - - cache.setUnsafe("a", true, 1, 0) // value + no ttl + jitter - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - cache.setUnsafe("a", true, 1, 100) // value + ttl + jitter - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(int64(0), v.expiryMicro) - is.NotEqual(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_setUnsafe_sharedMissingCache(t *testing.T) { - is := assert.New(t) - - cache := NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - - cache.setUnsafe("a", false, 1, 0) // no value + no ttl + no jitter - v, ok := cache.cache.Get("a") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - cache.setUnsafe("a", true, 1, 0) // value + no ttl + no jitter - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - cache.setUnsafe("a", true, 1, 100) // value + ttl + no jitter - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(int64(0), v.expiryMicro) - is.NotEqual(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - WithJitter(0.5). - Build() - - cache.setUnsafe("a", true, 1, 0) // value + no ttl + jitter - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - cache.setUnsafe("a", true, 1, 100) // value + ttl + jitter - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(int64(0), v.expiryMicro) - is.NotEqual(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_setUnsafe_dedicatedMissingCache(t *testing.T) { - is := assert.New(t) - - cache := NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 10). - Build() - - cache.setUnsafe("a", false, 1, 0) // no value + no ttl + no jitter - v, ok := cache.cache.Get("a") - is.False(ok) - is.Nil(v) - v, ok = cache.missingCache.Get("a") - is.True(ok) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - cache.setUnsafe("a", true, 1, 0) // value + no ttl + no jitter - v, ok = cache.missingCache.Get("a") - is.False(ok) - is.Nil(v) - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - cache.setUnsafe("a", true, 1, 100) // value + ttl + no jitter - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(int64(0), v.expiryMicro) - is.NotEqual(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 10). - WithJitter(0.5). - Build() - - cache.setUnsafe("a", true, 1, 0) // value + no ttl + jitter - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - cache.setUnsafe("a", true, 1, 100) // value + ttl + jitter - v, ok = cache.cache.Get("a") - is.True(ok) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.NotEqual(int64(0), v.expiryMicro) - is.NotEqual(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_setManyUnsafe(t *testing.T) { - is := assert.New(t) - - // simple - cache := NewHotCache[string, int](LRU, 10). - Build() - cache.setManyUnsafe(map[string]int{"a": 1, "b": 2}, []string{"c"}, 0) - is.Equal(2, cache.cache.Len()) - - // shared missing cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - Build() - cache.setManyUnsafe(map[string]int{"a": 1, "b": 2}, []string{"c"}, 0) - is.Equal(3, cache.cache.Len()) - cache.setManyUnsafe(map[string]int{"a": 1}, []string{"c", "b"}, 0) - is.Equal(3, cache.cache.Len()) - - // dedicated missing cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 10). - Build() - cache.setManyUnsafe(map[string]int{"a": 1, "b": 2}, []string{"c"}, 0) - is.Equal(2, cache.cache.Len()) - is.Equal(1, cache.missingCache.Len()) - cache.setManyUnsafe(map[string]int{"a": 1}, []string{"c", "b"}, 0) - is.Equal(1, cache.cache.Len()) - is.Equal(2, cache.missingCache.Len()) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_getUnsafe(t *testing.T) { - t.Parallel() - is := assert.New(t) - - // simple - cache := NewHotCache[string, int](LRU, 10). - WithRevalidation(10 * time.Millisecond). - Build() - cache.setManyUnsafe(map[string]int{"a": 1}, []string{"b"}, 0) - cache.setUnsafe("c", true, 3, (2 * time.Millisecond).Microseconds()) - v, revalidate, found := cache.getUnsafe("a") - is.True(found) - is.False(revalidate) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - v, revalidate, found = cache.getUnsafe("b") - is.False(found) - is.False(revalidate) - is.Nil(v) - - v, revalidate, found = cache.getUnsafe("c") - is.True(found) - is.False(revalidate) - is.NotNil(v) - is.Equal(2, cache.cache.Len()) - - time.Sleep(5 * time.Millisecond) - v, revalidate, found = cache.getUnsafe("c") - is.True(found) - is.True(revalidate) - is.NotNil(v) - is.Equal(2, cache.cache.Len()) - - time.Sleep(15 * time.Millisecond) - v, revalidate, found = cache.getUnsafe("c") - is.False(found) - is.False(revalidate) - is.Nil(v) - is.Equal(1, cache.cache.Len()) - - // shared missing cache - cache = NewHotCache[string, int](LRU, 10). - WithRevalidation(10 * time.Millisecond). - WithMissingSharedCache(). - Build() - cache.setManyUnsafe(map[string]int{"a": 1}, []string{"b"}, 0) - cache.setUnsafe("c", true, 3, (2 * time.Millisecond).Microseconds()) - v, revalidate, found = cache.getUnsafe("a") - is.True(found) - is.False(revalidate) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - v, revalidate, found = cache.getUnsafe("b") - is.True(found) - is.False(revalidate) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - v, revalidate, found = cache.getUnsafe("c") - is.True(found) - is.False(revalidate) - is.NotNil(v) - is.Equal(3, cache.cache.Len()) - - time.Sleep(5 * time.Millisecond) - v, revalidate, found = cache.getUnsafe("c") - is.True(found) - is.True(revalidate) - is.NotNil(v) - is.Equal(3, cache.cache.Len()) - - time.Sleep(10 * time.Millisecond) - v, revalidate, found = cache.getUnsafe("c") - is.False(found) - is.False(revalidate) - is.Nil(v) - is.Equal(2, cache.cache.Len()) - - // dedicated missing cache - cache = NewHotCache[string, int](LRU, 10). - WithRevalidation(10*time.Millisecond). - WithMissingCache(LRU, 10). - Build() - cache.setManyUnsafe(map[string]int{"a": 1}, []string{"b"}, 0) - cache.setUnsafe("c", false, 0, (2 * time.Millisecond).Microseconds()) - v, revalidate, found = cache.getUnsafe("a") - is.True(found) - is.False(revalidate) - is.True(v.hasValue) - is.Equal(1, v.value) - is.Equal(uint(8), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - v, revalidate, found = cache.getUnsafe("b") - is.True(found) - is.False(revalidate) - is.False(v.hasValue) - is.Equal(0, v.value) - is.Equal(uint(0), v.bytes) - is.Equal(int64(0), v.expiryMicro) - is.Equal(int64(0), v.staleExpiryMicro) - is.Equal(v.expiryMicro, v.staleExpiryMicro) - - v, revalidate, found = cache.getUnsafe("c") - is.True(found) - is.False(revalidate) - is.NotNil(v) - is.Equal(2, cache.missingCache.Len()) - - time.Sleep(5 * time.Millisecond) - v, revalidate, found = cache.getUnsafe("c") - is.True(found) - is.True(revalidate) - is.NotNil(v) - is.Equal(2, cache.missingCache.Len()) - - time.Sleep(15 * time.Millisecond) - v, revalidate, found = cache.getUnsafe("c") - is.False(found) - is.False(revalidate) - is.Nil(v) - is.Equal(1, cache.missingCache.Len()) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_getManyUnsafe(t *testing.T) { - t.Parallel() - is := assert.New(t) - - // simple - cache := NewHotCache[string, int](LRU, 10). - WithRevalidation(10 * time.Millisecond). - Build() - cache.setManyUnsafe(map[string]int{"a": 1}, []string{"b"}, 0) - cache.setUnsafe("c", true, 3, (2 * time.Millisecond).Microseconds()) - v, missing, revalidate := cache.getManyUnsafe([]string{"a"}) - is.Len(v, 1) - is.Len(missing, 0) - is.Len(revalidate, 0) - - v, missing, revalidate = cache.getManyUnsafe([]string{"b"}) - is.Len(v, 0) - is.Len(missing, 1) - is.Len(revalidate, 0) - - v, missing, revalidate = cache.getManyUnsafe([]string{"c"}) - is.Len(v, 1) - is.Len(missing, 0) - is.Len(revalidate, 0) - is.Equal(2, cache.cache.Len()) - - time.Sleep(5 * time.Millisecond) - v, missing, revalidate = cache.getManyUnsafe([]string{"c"}) - is.Len(v, 1) - is.Len(missing, 0) - is.Len(revalidate, 1) - is.Equal(2, cache.cache.Len()) - - time.Sleep(10 * time.Millisecond) - v, missing, revalidate = cache.getManyUnsafe([]string{"c"}) - is.Len(v, 0) - is.Len(missing, 1) - is.Len(revalidate, 0) - is.Equal(1, cache.cache.Len()) - - // shared missing cache - cache = NewHotCache[string, int](LRU, 10). - WithRevalidation(10 * time.Millisecond). - WithMissingSharedCache(). - Build() - cache.setManyUnsafe(map[string]int{"a": 1}, []string{"b"}, 0) - cache.setUnsafe("c", true, 3, (2 * time.Millisecond).Microseconds()) - v, missing, revalidate = cache.getManyUnsafe([]string{"a"}) - is.Len(v, 1) - is.Len(missing, 0) - is.Len(revalidate, 0) - - v, missing, revalidate = cache.getManyUnsafe([]string{"b"}) - is.Len(v, 1) - is.Len(missing, 0) - is.Len(revalidate, 0) - - v, missing, revalidate = cache.getManyUnsafe([]string{"c"}) - is.Len(v, 1) - is.Len(missing, 0) - is.Len(revalidate, 0) - is.Equal(3, cache.cache.Len()) - - time.Sleep(5 * time.Millisecond) - v, missing, revalidate = cache.getManyUnsafe([]string{"c"}) - is.Len(v, 1) - is.Len(missing, 0) - is.Len(revalidate, 1) - is.Equal(3, cache.cache.Len()) - - time.Sleep(15 * time.Millisecond) - v, missing, revalidate = cache.getManyUnsafe([]string{"c"}) - is.Len(v, 0) - is.Len(missing, 1) - is.Len(revalidate, 0) - is.Equal(2, cache.cache.Len()) - - // dedicated missing cache - cache = NewHotCache[string, int](LRU, 10). - WithRevalidation(10*time.Millisecond). - WithMissingCache(LRU, 10). - Build() - cache.setManyUnsafe(map[string]int{"a": 1}, []string{"b"}, 0) - cache.setUnsafe("c", false, 0, (2 * time.Millisecond).Microseconds()) - v, missing, revalidate = cache.getManyUnsafe([]string{"a"}) - is.Len(v, 1) - is.Len(missing, 0) - is.Len(revalidate, 0) - - v, missing, revalidate = cache.getManyUnsafe([]string{"b"}) - is.Len(v, 1) - is.Len(missing, 0) - is.Len(revalidate, 0) - - v, missing, revalidate = cache.getManyUnsafe([]string{"c"}) - is.Len(v, 1) - is.Len(missing, 0) - is.Len(revalidate, 0) - is.Equal(2, cache.missingCache.Len()) - - time.Sleep(5 * time.Millisecond) - v, missing, revalidate = cache.getManyUnsafe([]string{"c"}) - is.Len(v, 1) - is.Len(missing, 0) - is.Len(revalidate, 1) - is.Equal(2, cache.missingCache.Len()) - - time.Sleep(15 * time.Millisecond) - v, missing, revalidate = cache.getManyUnsafe([]string{"c"}) - is.Len(v, 0) - is.Len(missing, 1) - is.Len(revalidate, 0) - is.Equal(1, cache.missingCache.Len()) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_loadAndSetMany(t *testing.T) { - is := assert.New(t) - - counter1 := int32(0) - counter2 := int32(0) - counter3 := int32(0) - - // simple - cache := NewHotCache[string, int](LRU, 10). - WithCopyOnWrite(func(nb int) int { - return nb * 42 - }). - Build() - - cache.Purge() - v, err := cache.loadAndSetMany( - []string{"a", "b"}, - LoaderChain[string, int]{}, - ) - is.Nil(err) - is.NotNil(v) - is.False(v["a"].hasValue) - is.False(v["b"].hasValue) - is.Len(v, 2) - - cache.Purge() - v, err = cache.loadAndSetMany( - []string{}, - LoaderChain[string, int]{ - func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter1, 1) - is.ElementsMatch([]string{"a", "b"}, keys) - return map[string]int{"a": 1, "c": 3}, nil - }, - func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter1, 1) - is.ElementsMatch([]string{"b"}, keys) - return map[string]int{"a": 2}, nil - }, - }, - ) - is.Nil(err) - is.NotNil(v) - is.Len(v, 0) - - cache.Purge() - v, err = cache.loadAndSetMany( - []string{"a"}, - LoaderChain[string, int]{ - func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter1, 1) - return nil, assert.AnError - }, - func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter1, 1) - is.ElementsMatch([]string{"b"}, keys) - return map[string]int{"a": 2}, nil - }, - }, - ) - is.EqualError(err, assert.AnError.Error()) - is.NotNil(v) - is.Len(v, 0) - is.Equal(int32(1), atomic.LoadInt32(&counter1)) - - cache.Purge() - v, err = cache.loadAndSetMany( - []string{"a", "b"}, - LoaderChain[string, int]{ - func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter1, 1) - is.ElementsMatch([]string{"a", "b"}, keys) - return map[string]int{"a": 1, "c": 3}, nil - }, - func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter1, 1) - is.ElementsMatch([]string{"b"}, keys) - return map[string]int{"a": 2}, nil - }, - }, - ) - is.Nil(err) - is.Len(v, 2) - is.True(v["a"].hasValue) - is.False(v["b"].hasValue) - is.Equal(84, v["a"].value) - is.Equal(int32(3), atomic.LoadInt32(&counter1)) - is.Equal(2, cache.cache.Len()) // "c=3" is cached - - // shared missing cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingSharedCache(). - WithCopyOnWrite(func(nb int) int { - return nb * 42 - }). - Build() - v, err = cache.loadAndSetMany( - []string{"a", "b"}, - LoaderChain[string, int]{ - func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter2, 1) - is.ElementsMatch([]string{"a", "b"}, keys) - return map[string]int{"a": 1, "c": 3}, nil - }, - func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter2, 1) - is.ElementsMatch([]string{"b"}, keys) - return map[string]int{"a": 2}, nil - }, - }, - ) - is.Nil(err) - is.Len(v, 2) - is.True(v["a"].hasValue) - is.False(v["b"].hasValue) - is.Equal(84, v["a"].value) - is.Equal(int32(2), atomic.LoadInt32(&counter2)) - is.Equal(3, cache.cache.Len()) - - // dedicated missing cache - cache = NewHotCache[string, int](LRU, 10). - WithMissingCache(LRU, 10). - WithCopyOnWrite(func(nb int) int { - return nb * 42 - }). - Build() - v, err = cache.loadAndSetMany( - []string{"a", "b"}, - LoaderChain[string, int]{ - func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter3, 1) - is.ElementsMatch([]string{"a", "b"}, keys) - return map[string]int{"a": 1, "c": 3}, nil - }, - func(keys []string) (map[string]int, error) { - atomic.AddInt32(&counter3, 1) - is.ElementsMatch([]string{"b"}, keys) - return map[string]int{"a": 2}, nil - }, - }, - ) - is.Nil(err) - is.Len(v, 2) - is.True(v["a"].hasValue) - is.False(v["b"].hasValue) - is.Equal(84, v["a"].value) - is.Equal(int32(2), atomic.LoadInt32(&counter3)) - is.Equal(2, cache.cache.Len()) - is.Equal(1, cache.missingCache.Len()) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} - -func TestHotCache_revalidate(t *testing.T) { - is := assert.New(t) - - counter1 := int32(0) - counter2 := int32(0) - - // with revalidation - cache := NewHotCache[string, int](LRU, 10). - WithTTL(1*time.Millisecond). - WithRevalidation(10*time.Millisecond, func(keys []string) (found map[string]int, err error) { - time.Sleep(1 * time.Millisecond) - atomic.AddInt32(&counter1, 1) - return map[string]int{"a": 2}, nil - }). - Build() - - cache.Set("a", 1) - - is.True(cache.Has("a")) - time.Sleep(5 * time.Millisecond) - is.True(cache.Has("a")) - _, _, _ = cache.Get("a") - is.True(cache.Has("a")) - time.Sleep(5 * time.Millisecond) - is.True(cache.Has("a")) - - v, ok, err := cache.Get("a") - is.True(ok) - is.Nil(err) - is.Equal(2, v) - is.Equal(int32(1), atomic.LoadInt32(&counter1)) // revalidated async - - time.Sleep(15 * time.Millisecond) - is.True(cache.Has("a")) - - v, ok, err = cache.Get("a") - is.False(ok) - is.Nil(err) - is.Equal(0, v) - is.Equal(int32(2), atomic.LoadInt32(&counter1)) - - cache.Purge() - - // with loader - cache = NewHotCache[string, int](LRU, 10). - WithTTL(1 * time.Millisecond). - WithLoaders(func(keys []string) (found map[string]int, err error) { - time.Sleep(1 * time.Millisecond) - atomic.AddInt32(&counter2, 1) - return map[string]int{"a": 2}, nil - }). - WithRevalidation(10 * time.Millisecond). - Build() - - cache.Set("a", 1) - - is.True(cache.Has("a")) - time.Sleep(5 * time.Millisecond) - is.True(cache.Has("a")) - _, _, _ = cache.Get("a") - is.True(cache.Has("a")) - time.Sleep(5 * time.Millisecond) - is.True(cache.Has("a")) - - v, ok, err = cache.Get("a") - is.True(ok) - is.Nil(err) - is.Equal(2, v) - is.Equal(int32(1), atomic.LoadInt32(&counter2)) // revalidated async - - time.Sleep(15 * time.Millisecond) - is.True(cache.Has("a")) - - v, ok, err = cache.Get("a") - is.True(ok) - is.Nil(err) - is.Equal(2, v) - is.Equal(int32(3), atomic.LoadInt32(&counter2)) - - time.Sleep(10 * time.Millisecond) // purge revalidation goroutine -} diff --git a/item_test.go b/item_test.go deleted file mode 100644 index fe909c0..0000000 --- a/item_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package hot - -import ( - "testing" - - "github.com/samber/hot/internal" - "github.com/stretchr/testify/assert" -) - -func TestNewItem(t *testing.T) { - is := assert.New(t) - - // no value without ttl - got := newItem[int64](0, false, 0, 0) - is.EqualValues(&item[int64]{false, 0, 0, 0, 0}, got) - got = newItem[int64](42, false, 0, 0) - is.EqualValues(&item[int64]{false, 0, 0, 0, 0}, got) - - // no value with ttl - got = newItem[int64](0, false, 2_000, 1_000) - is.False(got.hasValue) - is.Equal(int64(0), got.value) - is.InEpsilon(internal.NowMicro()+2_000, got.expiryMicro, 100) - is.InEpsilon(internal.NowMicro()+2_000+1_000, got.staleExpiryMicro, 100) - - // has value without ttl - is.EqualValues(&item[int64]{true, 42, 8, 0, 0}, newItem[int64](42, true, 0, 0)) - - // has value with ttl - got = newItem[int64](42, true, 2_000, 1_000) - is.True(got.hasValue) - is.Equal(int64(42), got.value) - is.InEpsilon(internal.NowMicro()+2_000, got.expiryMicro, 100) - is.InEpsilon(internal.NowMicro()+2_000+1_000, got.staleExpiryMicro, 100) - - // size - is.EqualValues(&item[map[string]int]{true, map[string]int{"a": 1, "b": 2}, 79, 0, 0}, newItem(map[string]int{"a": 1, "b": 2}, true, 0, 0)) - is.EqualValues(&item[*item[int64]]{true, &item[int64]{false, 0, 0, 0, 0}, 40, 0, 0}, newItem(newItem[int64](42, false, 0, 0), true, 0, 0)) -} - -func TestNewItemWithValue(t *testing.T) { - is := assert.New(t) - - is.Equal(&item[int64]{true, int64(42), 8, 0, 0}, newItemWithValue(int64(42), 0, 0)) - - item := newItemWithValue(int64(42), 2_000, 1_000) - is.True(item.hasValue) - is.Equal(int64(42), item.value) - is.InEpsilon(internal.NowMicro()+2_000, item.expiryMicro, 100) - is.InEpsilon(internal.NowMicro()+2_000+1_000, item.staleExpiryMicro, 100) -} - -func TestNewItemNoValue(t *testing.T) { - is := assert.New(t) - - is.Equal(&item[int64]{false, 0, 0, 0, 0}, newItemNoValue[int64](0, 0)) - - item := newItemNoValue[int](2_000_000, 1_000_000) - is.False(item.hasValue) - is.Equal(0, item.value) - is.InEpsilon(internal.NowMicro()+2_000, item.expiryMicro, 100) - is.InEpsilon(internal.NowMicro()+2_000+1_000, item.staleExpiryMicro, 100) -} - -func TestItem_isExpired(t *testing.T) { - is := assert.New(t) - - got := newItemNoValue[int64](0, 0) - is.False(got.isExpired(internal.NowMicro())) - - got = newItemNoValue[int64](-1_000, 0) - is.True(got.isExpired(internal.NowMicro())) - - got = newItemNoValue[int64](1_000, 0) - is.False(got.isExpired(internal.NowMicro())) - - got = newItemNoValue[int64](-1_000, 800) - is.True(got.isExpired(internal.NowMicro())) - - got = newItemNoValue[int64](-1_000, 1_200) - is.False(got.isExpired(internal.NowMicro())) -} - -func TestItem_shouldRevalidate(t *testing.T) { - is := assert.New(t) - - got := newItemNoValue[int64](0, 0) - is.False(got.shouldRevalidate(internal.NowMicro())) - - got = newItemNoValue[int64](-1_000, 0) - is.False(got.shouldRevalidate(internal.NowMicro())) - - got = newItemNoValue[int64](1_000, 0) - is.False(got.shouldRevalidate(internal.NowMicro())) - - got = newItemNoValue[int64](-1_000, 800) - is.False(got.shouldRevalidate(internal.NowMicro())) - - got = newItemNoValue[int64](-1_000, 1_200) - is.True(got.shouldRevalidate(internal.NowMicro())) -} - -func TestItemMapsToValues(t *testing.T) { - is := assert.New(t) - - twice := func(i int) int { return i * 2 } - itemNo := newItem(0, false, 0, 0) - itemA := newItem(42, true, 0, 0) - itemB := newItem(21, true, 0, 0) - - // no map - gotFound, gotMissing := itemMapsToValues[string, int](nil) - is.EqualValues(map[string]int{}, gotFound) - is.EqualValues([]string{}, gotMissing) - - // no map - gotFound, gotMissing = itemMapsToValues[string, int](twice) - is.EqualValues(map[string]int{}, gotFound) - is.EqualValues([]string{}, gotMissing) - - // has map - gotFound, gotMissing = itemMapsToValues[string, int](nil, map[string]*item[int]{"a": itemA, "b": itemB, "c": itemNo}) - is.EqualValues(map[string]int{"a": 42, "b": 21}, gotFound) - is.EqualValues([]string{"c"}, gotMissing) - gotFound, gotMissing = itemMapsToValues[string, int](nil, map[string]*item[int]{"a": itemA}, map[string]*item[int]{"b": itemB, "c": itemNo, "a": itemNo}) - is.EqualValues(map[string]int{"a": 42, "b": 21}, gotFound) - is.EqualValues([]string{"c"}, gotMissing) - - // has map - gotFound, gotMissing = itemMapsToValues[string, int](twice, map[string]*item[int]{"a": itemA, "b": itemB, "c": itemNo}) - is.EqualValues(map[string]int{"a": 84, "b": 42}, gotFound) - is.EqualValues([]string{"c"}, gotMissing) - gotFound, gotMissing = itemMapsToValues[string, int](twice, map[string]*item[int]{"a": itemA}, map[string]*item[int]{"b": itemB, "c": itemNo, "a": itemNo}) - is.EqualValues(map[string]int{"a": 84, "b": 42}, gotFound) - is.EqualValues([]string{"c"}, gotMissing) -} - -func TestApplyJitter(t *testing.T) { - is := assert.New(t) - - // no jitter - is.Equal(int64(1_000), applyJitter(1_000, 0)) - - // with jitter - is.InEpsilon(1_000, applyJitter(1_000, 0.1), 100) -} diff --git a/loader_test.go b/loader_test.go deleted file mode 100644 index cbe5aa5..0000000 --- a/loader_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package hot - -import ( - "sync/atomic" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLoaders_run(t *testing.T) { - is := assert.New(t) - - counter := int32(0) - - // without error - loaders := LoaderChain[int, int]{ - func(keys []int) (map[int]int, error) { - atomic.AddInt32(&counter, 1) - return map[int]int{1: 1, 2: 2}, nil - }, - func(keys []int) (map[int]int, error) { - atomic.AddInt32(&counter, 1) - return map[int]int{2: 42, 3: 3}, nil - }, - } - - results, missing, err := loaders.run([]int{1, 2, 3, 4}) - - is.EqualValues(map[int]int{1: 1, 2: 42, 3: 3}, results) - is.EqualValues([]int{4}, missing) - is.Nil(err) - is.EqualValues(2, atomic.LoadInt32(&counter)) - - // with error - loaders = LoaderChain[int, int]{ - func(keys []int) (map[int]int, error) { - atomic.AddInt32(&counter, 1) - return map[int]int{1: 1, 2: 2}, nil - }, - func(keys []int) (map[int]int, error) { - atomic.AddInt32(&counter, 1) - return map[int]int{2: 42, 3: 3}, nil - }, - func(keys []int) (map[int]int, error) { - atomic.AddInt32(&counter, 1) - return map[int]int{4: 4}, assert.AnError - }, - } - - results, missing, err = loaders.run([]int{1, 2, 3, 4}) - - is.EqualValues(map[int]int{}, results) - is.EqualValues([]int{}, missing) - is.ErrorIs(assert.AnError, err) - is.EqualValues(5, atomic.LoadInt32(&counter)) -} diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 53cabe7..0000000 --- a/main_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package hot - -import ( - "testing" - - "go.uber.org/goleak" -) - -func TestMain(m *testing.M) { - goleak.VerifyTestMain(m) -}