mirror of https://github.com/0xERR0R/blocky.git
239 lines
7.2 KiB
Go
239 lines
7.2 KiB
Go
package expirationcache
|
|
|
|
import (
|
|
"time"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Expiration cache", func() {
|
|
Describe("Basic operations", func() {
|
|
When("string cache was created", func() {
|
|
It("Initial cache should be empty", func() {
|
|
cache := NewCache[string](Options{})
|
|
Expect(cache.TotalCount()).Should(Equal(0))
|
|
})
|
|
It("Initial cache should not contain any elements", func() {
|
|
cache := NewCache[string](Options{})
|
|
val, expiration := cache.Get("key1")
|
|
Expect(val).Should(BeNil())
|
|
Expect(expiration).Should(Equal(time.Duration(0)))
|
|
})
|
|
})
|
|
When("Put new value with positive TTL", func() {
|
|
It("Should return the value before element expires", func() {
|
|
cache := NewCache[string](Options{CleanupInterval: 100 * time.Millisecond})
|
|
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))
|
|
})
|
|
It("Should return nil after expiration", func() {
|
|
cache := NewCache[string](Options{CleanupInterval: 100 * time.Millisecond})
|
|
v := "v1"
|
|
cache.Put("key1", &v, 50*time.Millisecond)
|
|
|
|
// wait for expiration
|
|
Eventually(func(g Gomega) {
|
|
val, ttl := cache.Get("key1")
|
|
g.Expect(val).Should(HaveValue(Equal("v1")))
|
|
g.Expect(ttl.Milliseconds()).Should(BeNumerically("==", 0))
|
|
}, "100ms").Should(Succeed())
|
|
|
|
// wait for cleanup run
|
|
Eventually(func() int {
|
|
return cache.lru.Len()
|
|
}, "100ms").Should(Equal(0))
|
|
})
|
|
})
|
|
When("Put new value without expiration", func() {
|
|
It("Should not cache the value", func() {
|
|
cache := NewCache[string](Options{CleanupInterval: 50 * time.Millisecond})
|
|
v := "x"
|
|
cache.Put("key1", &v, 0)
|
|
val, expiration := cache.Get("key1")
|
|
Expect(val).Should(BeNil())
|
|
Expect(expiration.Milliseconds()).Should(BeNumerically("==", 0))
|
|
Expect(cache.TotalCount()).Should(Equal(0))
|
|
})
|
|
})
|
|
When("Put updated value", func() {
|
|
It("Should return updated value", func() {
|
|
cache := NewCache[string](Options{})
|
|
v1 := "v1"
|
|
v2 := "v2"
|
|
cache.Put("key1", &v1, 50*time.Millisecond)
|
|
cache.Put("key1", &v2, 200*time.Millisecond)
|
|
|
|
val, expiration := cache.Get("key1")
|
|
|
|
Expect(val).Should(HaveValue(Equal("v2")))
|
|
Expect(expiration.Milliseconds()).Should(BeNumerically(">", 100))
|
|
Expect(expiration.Milliseconds()).Should(BeNumerically("<=", 200))
|
|
Expect(cache.TotalCount()).Should(Equal(1))
|
|
})
|
|
})
|
|
When("Purging after usage", func() {
|
|
It("Should be empty after purge", func() {
|
|
cache := NewCache[string](Options{})
|
|
v1 := "y"
|
|
cache.Put("key1", &v1, time.Second)
|
|
|
|
Expect(cache.TotalCount()).Should(Equal(1))
|
|
|
|
cache.Clear()
|
|
|
|
Expect(cache.TotalCount()).Should(Equal(0))
|
|
})
|
|
})
|
|
})
|
|
Describe("Hook functions", func() {
|
|
When("Hook functions are defined", func() {
|
|
It("should call each hook function", func() {
|
|
onCacheHitChannel := make(chan string, 10)
|
|
onCacheMissChannel := make(chan string, 10)
|
|
onAfterPutChannel := make(chan int, 10)
|
|
cache := NewCache[string](Options{
|
|
OnCacheHitFn: func(key string) {
|
|
onCacheHitChannel <- key
|
|
},
|
|
OnCacheMissFn: func(key string) {
|
|
onCacheMissChannel <- key
|
|
},
|
|
OnAfterPutFn: func(newSize int) {
|
|
onAfterPutChannel <- newSize
|
|
},
|
|
})
|
|
|
|
By("Get non existing value", func() {
|
|
val, _ := cache.Get("notExists")
|
|
Expect(val).Should(BeNil())
|
|
|
|
Expect(onCacheMissChannel).Should(Receive(Equal("notExists")))
|
|
Expect(onCacheHitChannel).Should(Not(Receive()))
|
|
Expect(onAfterPutChannel).Should(Not(Receive()))
|
|
})
|
|
|
|
By("Put new cache entry", func() {
|
|
v1 := "v1"
|
|
cache.Put("key1", &v1, time.Second)
|
|
Expect(onCacheMissChannel).Should(Not(Receive()))
|
|
Expect(onCacheMissChannel).Should(Not(Receive()))
|
|
Expect(onAfterPutChannel).Should(Receive(Equal(1)))
|
|
})
|
|
|
|
By("Get existing value", func() {
|
|
val, _ := cache.Get("key1")
|
|
Expect(val).Should(HaveValue(Equal("v1")))
|
|
|
|
Expect(onCacheMissChannel).Should(Not(Receive()))
|
|
Expect(onCacheHitChannel).Should(Receive(Equal("key1")))
|
|
Expect(onAfterPutChannel).Should(Not(Receive()))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
Describe("preExpiration function", func() {
|
|
When("function is defined", func() {
|
|
It("should update the value and TTL if function returns values", func() {
|
|
fn := func(key string) (val *string, ttl time.Duration) {
|
|
v2 := "v2"
|
|
|
|
return &v2, time.Second
|
|
}
|
|
|
|
cache := NewCacheWithOnExpired[string](Options{}, fn)
|
|
v1 := "v1"
|
|
cache.Put("key1", &v1, 50*time.Millisecond)
|
|
|
|
// wait for expiration
|
|
Eventually(func(g Gomega) {
|
|
val, ttl := cache.Get("key1")
|
|
g.Expect(val).Should(HaveValue(Equal("v1")))
|
|
g.Expect(ttl.Milliseconds()).Should(
|
|
BeNumerically("==", 0))
|
|
}, "150ms").Should(Succeed())
|
|
})
|
|
|
|
It("should update the value and TTL if function returns values on cleanup if element is expired", func() {
|
|
fn := func(key string) (val *string, ttl time.Duration) {
|
|
v2 := "val2"
|
|
|
|
return &v2, time.Second
|
|
}
|
|
cache := NewCacheWithOnExpired[string](Options{}, fn)
|
|
v1 := "somval"
|
|
cache.Put("key1", &v1, time.Millisecond)
|
|
|
|
time.Sleep(2 * time.Millisecond)
|
|
|
|
// trigger cleanUp manually -> onExpiredFn will be executed, because element is expired
|
|
cache.cleanUp()
|
|
|
|
// wait for expiration
|
|
val, ttl := cache.Get("key1")
|
|
Expect(val).Should(HaveValue(Equal("val2")))
|
|
Expect(ttl.Milliseconds()).Should(And(
|
|
BeNumerically(">", 900),
|
|
BeNumerically("<=", 1000)))
|
|
})
|
|
|
|
It("should delete the key if function returns nil", func() {
|
|
fn := func(key string) (val *string, ttl time.Duration) {
|
|
return nil, 0
|
|
}
|
|
cache := NewCacheWithOnExpired[string](Options{CleanupInterval: 100 * time.Microsecond}, fn)
|
|
v1 := "z"
|
|
cache.Put("key1", &v1, 50*time.Millisecond)
|
|
|
|
Eventually(func() (interface{}, time.Duration) {
|
|
return cache.Get("key1")
|
|
}, "200ms").Should(BeNil())
|
|
})
|
|
})
|
|
})
|
|
Describe("LRU behaviour", func() {
|
|
When("Defined max size is reached", func() {
|
|
It("should remove old elements", func() {
|
|
cache := NewCache[string](Options{MaxSize: 3})
|
|
|
|
v1 := "val1"
|
|
v2 := "val2"
|
|
v3 := "val3"
|
|
v4 := "val4"
|
|
v5 := "val5"
|
|
|
|
cache.Put("key1", &v1, time.Second)
|
|
cache.Put("key2", &v2, time.Second)
|
|
cache.Put("key3", &v3, time.Second)
|
|
cache.Put("key4", &v4, time.Second)
|
|
|
|
Expect(cache.TotalCount()).Should(Equal(3))
|
|
|
|
// key1 was removed
|
|
Expect(cache.Get("key1")).Should(BeNil())
|
|
// key2,3,4 still in the cache
|
|
Expect(cache.lru.Contains("key2")).Should(BeTrue())
|
|
Expect(cache.lru.Contains("key3")).Should(BeTrue())
|
|
Expect(cache.lru.Contains("key4")).Should(BeTrue())
|
|
|
|
// now get key2 to increase usage count
|
|
_, _ = cache.Get("key2")
|
|
|
|
// put key5
|
|
cache.Put("key5", &v5, time.Second)
|
|
|
|
// now key3 should be removed
|
|
Expect(cache.lru.Contains("key2")).Should(BeTrue())
|
|
Expect(cache.lru.Contains("key3")).Should(BeFalse())
|
|
Expect(cache.lru.Contains("key4")).Should(BeTrue())
|
|
Expect(cache.lru.Contains("key5")).Should(BeTrue())
|
|
})
|
|
})
|
|
})
|
|
})
|