blocky/cache/expirationcache/prefetching_cache_test.go

184 lines
6.0 KiB
Go

package expirationcache
import (
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Prefetching expiration cache", func() {
Describe("Basic operations", func() {
When("string cache was created", func() {
It("Initial cache should be empty", func() {
cache := NewPrefetchingCache[string](PrefetchingOptions[string]{})
Expect(cache.TotalCount()).Should(Equal(0))
})
It("Initial cache should not contain any elements", func() {
cache := NewPrefetchingCache[string](PrefetchingOptions[string]{})
val, expiration := cache.Get("key1")
Expect(val).Should(BeNil())
Expect(expiration).Should(Equal(time.Duration(0)))
})
It("Should work as cache (basic operations)", func() {
cache := NewPrefetchingCache[string](PrefetchingOptions[string]{})
v := "v1"
cache.Put("key1", &v, 50*time.Millisecond)
val, expiration := cache.Get("key1")
Expect(val).Should(HaveValue(Equal("v1")))
Expect(expiration.Milliseconds()).Should(BeNumerically("<=", 50))
Expect(cache.TotalCount()).Should(Equal(1))
cache.Clear()
Expect(cache.TotalCount()).Should(Equal(0))
})
})
Context("Prefetching", func() {
It("Should prefetch element", func() {
cache := NewPrefetchingCache[string](PrefetchingOptions[string]{
Options: Options{
CleanupInterval: 100 * time.Millisecond,
},
PrefetchThreshold: 2,
PrefetchExpires: 100 * time.Millisecond,
ReloadFn: func(cacheKey string) (*string, time.Duration) {
v := "v2"
return &v, 50 * time.Millisecond
},
})
By("put a value and get it again", func() {
v := "v1"
cache.Put("key1", &v, 50*time.Millisecond)
val, expiration := cache.Get("key1")
Expect(val).Should(HaveValue(Equal("v1")))
Expect(expiration.Milliseconds()).Should(BeNumerically("<=", 50))
Expect(cache.TotalCount()).Should(Equal(1))
})
By("Get value twice to trigger prefetching", func() {
val, _ := cache.Get("key1")
Expect(val).Should(HaveValue(Equal("v1")))
Eventually(func(g Gomega) {
val, _ := cache.Get("key1")
g.Expect(val).Should(HaveValue(Equal("v2")))
}).Should(Succeed())
})
})
It("Should not prefetch element", func() {
cache := NewPrefetchingCache[string](PrefetchingOptions[string]{
Options: Options{
CleanupInterval: 100 * time.Millisecond,
},
PrefetchThreshold: 2,
PrefetchExpires: 100 * time.Millisecond,
ReloadFn: func(cacheKey string) (*string, time.Duration) {
v := "v2"
return &v, 50 * time.Millisecond
},
})
By("put a value and get it again", func() {
v := "v1"
cache.Put("key1", &v, 50*time.Millisecond)
val, expiration := cache.Get("key1")
Expect(val).Should(HaveValue(Equal("v1")))
Expect(expiration.Milliseconds()).Should(BeNumerically("<=", 50))
Expect(cache.TotalCount()).Should(Equal(1))
})
By("Wait for expiration -> the entry should not be prefetched, threshold was not reached", func() {
Eventually(func(g Gomega) {
val, _ := cache.Get("key1")
g.Expect(val).Should(BeNil())
}, "5s", "500ms").Should(Succeed())
})
})
It("With default config (threshold = 0) should always prefetch", func() {
cache := NewPrefetchingCache[string](PrefetchingOptions[string]{
Options: Options{
CleanupInterval: 100 * time.Millisecond,
},
ReloadFn: func(cacheKey string) (*string, time.Duration) {
v := "v2"
return &v, 50 * time.Millisecond
},
})
By("put a value and get it again", func() {
v := "v1"
cache.Put("key1", &v, 50*time.Millisecond)
val, expiration := cache.Get("key1")
Expect(val).Should(HaveValue(Equal("v1")))
Expect(expiration.Milliseconds()).Should(BeNumerically("<=", 50))
})
By("Should return new prefetched value after expiration", func() {
Eventually(func(g Gomega) {
val, _ := cache.Get("key1")
g.Expect(val).Should(HaveValue(Equal("v2")))
}, "5s").Should(Succeed())
})
})
It("Should execute hook functions", func() {
onPrefetchAfterPutChannel := make(chan int, 10)
onPrefetchEntryReloaded := make(chan string, 10)
onnPrefetchCacheHit := make(chan string, 10)
cache := NewPrefetchingCache[string](PrefetchingOptions[string]{
Options: Options{
CleanupInterval: 100 * time.Millisecond,
},
PrefetchThreshold: 2,
PrefetchExpires: 100 * time.Millisecond,
ReloadFn: func(cacheKey string) (*string, time.Duration) {
v := "v2"
return &v, 50 * time.Millisecond
},
OnPrefetchAfterPut: func(newSize int) { onPrefetchAfterPutChannel <- newSize },
OnPrefetchEntryReloaded: func(key string) { onPrefetchEntryReloaded <- key },
OnPrefetchCacheHit: func(key string) { onnPrefetchCacheHit <- key },
})
By("put a value", func() {
v := "v1"
cache.Put("key1", &v, 50*time.Millisecond)
Expect(onPrefetchAfterPutChannel).Should(Not(Receive()))
Expect(onPrefetchEntryReloaded).Should(Not(Receive()))
Expect(onnPrefetchCacheHit).Should(Not(Receive()))
})
By("get a value 3 times to trigger prefetching", func() {
// first get
cache.Get("key1")
Expect(onPrefetchAfterPutChannel).Should(Receive(Equal(1)))
Expect(onnPrefetchCacheHit).Should(Not(Receive()))
Expect(onPrefetchEntryReloaded).Should(Not(Receive()))
// secont get
val, _ := cache.Get("key1")
Expect(val).Should(HaveValue(Equal("v1")))
// third get -> this should trigger prefetching after expiration
cache.Get("key1")
// reload was executed
Eventually(onPrefetchEntryReloaded).Should(Receive(Equal("key1")))
Expect(onnPrefetchCacheHit).Should(Not(Receive()))
// has new value
Eventually(func(g Gomega) {
val, _ := cache.Get("key1")
g.Expect(val).Should(HaveValue(Equal("v2")))
}, "5s").Should(Succeed())
// prefetch hit
Eventually(onnPrefetchCacheHit).Should(Receive(Equal("key1")))
})
})
})
})
})