mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(cache): implement TTLCache and TimeCache (#1822)
* feat(pkg): implement ttlcache and timecache
This commit is contained in:
parent
acef4ac624
commit
c1d8a4a850
8 changed files with 742 additions and 21 deletions
|
@ -7,21 +7,19 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jellydator/ttlcache/v3"
|
"github.com/autobrr/autobrr/pkg/ttlcache"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cache = ttlcache.New[string, *regexp.Regexp](
|
var cache = ttlcache.New[string, *regexp.Regexp](
|
||||||
ttlcache.WithTTL[string, *regexp.Regexp](5 * time.Minute),
|
ttlcache.Options[string, *regexp.Regexp]{}.
|
||||||
|
SetTimerResolution(5 * time.Minute).
|
||||||
|
SetDefaultTTL(15 * time.Minute),
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
go cache.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustCompilePOSIX(pattern string) *regexp.Regexp {
|
func MustCompilePOSIX(pattern string) *regexp.Regexp {
|
||||||
item := cache.Get(pattern)
|
item, ok := cache.Get(pattern)
|
||||||
if item != nil {
|
if ok {
|
||||||
return item.Value()
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
reg := regexp.MustCompilePOSIX(pattern)
|
reg := regexp.MustCompilePOSIX(pattern)
|
||||||
|
@ -30,9 +28,9 @@ func MustCompilePOSIX(pattern string) *regexp.Regexp {
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustCompile(pattern string) *regexp.Regexp {
|
func MustCompile(pattern string) *regexp.Regexp {
|
||||||
item := cache.Get(pattern)
|
item, ok := cache.Get(pattern)
|
||||||
if item != nil {
|
if ok {
|
||||||
return item.Value()
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
reg := regexp.MustCompile(pattern)
|
reg := regexp.MustCompile(pattern)
|
||||||
|
@ -41,9 +39,9 @@ func MustCompile(pattern string) *regexp.Regexp {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CompilePOSIX(pattern string) (*regexp.Regexp, error) {
|
func CompilePOSIX(pattern string) (*regexp.Regexp, error) {
|
||||||
item := cache.Get(pattern)
|
item, ok := cache.Get(pattern)
|
||||||
if item != nil {
|
if ok {
|
||||||
return item.Value(), nil
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := regexp.CompilePOSIX(pattern)
|
reg, err := regexp.CompilePOSIX(pattern)
|
||||||
|
@ -56,9 +54,9 @@ func CompilePOSIX(pattern string) (*regexp.Regexp, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Compile(pattern string) (*regexp.Regexp, error) {
|
func Compile(pattern string) (*regexp.Regexp, error) {
|
||||||
item := cache.Get(pattern)
|
item, ok := cache.Get(pattern)
|
||||||
if item != nil {
|
if ok {
|
||||||
return item.Value(), nil
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := regexp.Compile(pattern)
|
reg, err := regexp.Compile(pattern)
|
||||||
|
@ -75,9 +73,9 @@ func SubmitOriginal(plain string, reg *regexp.Regexp) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindOriginal(plain string) (*regexp.Regexp, bool) {
|
func FindOriginal(plain string) (*regexp.Regexp, bool) {
|
||||||
item := cache.Get(plain)
|
item, ok := cache.Get(plain)
|
||||||
if item != nil {
|
if ok {
|
||||||
return item.Value(), true
|
return item, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, false
|
return nil, false
|
||||||
|
|
74
pkg/timecache/timecache.go
Normal file
74
pkg/timecache/timecache.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package timecache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache struct {
|
||||||
|
m sync.RWMutex
|
||||||
|
t time.Time
|
||||||
|
o Options
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
round time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(o Options) *Cache {
|
||||||
|
c := Cache{
|
||||||
|
o: o,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Cache) Now() time.Time {
|
||||||
|
t.m.RLock()
|
||||||
|
if !t.t.IsZero() {
|
||||||
|
defer t.m.RUnlock()
|
||||||
|
return t.t
|
||||||
|
}
|
||||||
|
|
||||||
|
t.m.RUnlock()
|
||||||
|
return t.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Cache) update() time.Time {
|
||||||
|
t.m.Lock()
|
||||||
|
defer t.m.Unlock()
|
||||||
|
if !t.t.IsZero() {
|
||||||
|
return t.t
|
||||||
|
}
|
||||||
|
|
||||||
|
var d time.Duration
|
||||||
|
if t.o.round > time.Nanosecond {
|
||||||
|
d = t.o.round
|
||||||
|
} else {
|
||||||
|
d = time.Second * 1
|
||||||
|
}
|
||||||
|
|
||||||
|
t.t = time.Now().Round(d)
|
||||||
|
|
||||||
|
go func(duration time.Duration) {
|
||||||
|
if t.o.round > time.Nanosecond {
|
||||||
|
duration = t.o.round / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(duration)
|
||||||
|
t.reset()
|
||||||
|
}(d)
|
||||||
|
|
||||||
|
return t.t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Cache) reset() {
|
||||||
|
t.m.Lock()
|
||||||
|
defer t.m.Unlock()
|
||||||
|
t.t = time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Options) Round(d time.Duration) Options {
|
||||||
|
o.round = d
|
||||||
|
return o
|
||||||
|
}
|
49
pkg/timecache/timecache_test.go
Normal file
49
pkg/timecache/timecache_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package timecache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTime(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tc := (&Cache{}).Now()
|
||||||
|
if tc.IsZero() {
|
||||||
|
t.Fatalf("time is zero")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRounding(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ti := New(Options{}.Round(time.Minute * 5)).Now()
|
||||||
|
|
||||||
|
if ti.Minute()%5 != 0 {
|
||||||
|
t.Fatalf("time is not a 5 multiple")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolution(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const magicNumber = 3
|
||||||
|
const rounds = 700
|
||||||
|
ti := New(Options{}.Round(time.Millisecond * magicNumber))
|
||||||
|
|
||||||
|
unique := 0
|
||||||
|
old := ti.Now().UnixMilli()
|
||||||
|
for i := 0; i < rounds; i++ {
|
||||||
|
new := ti.Now().UnixMilli()
|
||||||
|
if new > old {
|
||||||
|
unique++
|
||||||
|
old = new
|
||||||
|
}
|
||||||
|
|
||||||
|
if div := new % magicNumber; div != 0 {
|
||||||
|
t.Fatalf("not a multiple of %d: %d", magicNumber, div)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond * 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if unique < rounds/magicNumber-1 {
|
||||||
|
t.Fatalf("not enough resolution rounds %d", unique)
|
||||||
|
}
|
||||||
|
}
|
41
pkg/ttlcache/domain.go
Normal file
41
pkg/ttlcache/domain.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package ttlcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/pkg/timecache"
|
||||||
|
)
|
||||||
|
|
||||||
|
const NoTTL time.Duration = 0
|
||||||
|
const DefaultTTL time.Duration = time.Nanosecond * 1
|
||||||
|
|
||||||
|
type Cache[K comparable, V any] struct {
|
||||||
|
tc timecache.Cache
|
||||||
|
l sync.RWMutex
|
||||||
|
o Options[K, V]
|
||||||
|
ch chan time.Time
|
||||||
|
m map[K]Item[V]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item[V any] struct {
|
||||||
|
t time.Time
|
||||||
|
d time.Duration
|
||||||
|
v V
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options[K comparable, V any] struct {
|
||||||
|
defaultTTL time.Duration
|
||||||
|
defaultResolution time.Duration
|
||||||
|
deallocationFunc DeallocationFunc[K, V]
|
||||||
|
noUpdateTime bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeallocationReason int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReasonTimedOut = DeallocationReason(iota)
|
||||||
|
ReasonDeleted = DeallocationReason(iota)
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeallocationFunc[K comparable, V any] func(key K, value V, reason DeallocationReason)
|
76
pkg/ttlcache/expiration.go
Normal file
76
pkg/ttlcache/expiration.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package ttlcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) startExpirations() {
|
||||||
|
timer := time.NewTimer(1 * time.Second)
|
||||||
|
stopTimer(timer) // wasteful, but makes the loop cleaner because this is initialized.
|
||||||
|
defer stopTimer(timer)
|
||||||
|
|
||||||
|
var timeSleep time.Time
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case t, ok := <-c.ch:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
} else if t.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeSleep.IsZero() || timeSleep.After(t) {
|
||||||
|
timeSleep = t
|
||||||
|
restartTimer(timer, timeSleep.Sub(c.tc.Now()))
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-timer.C:
|
||||||
|
stopTimer(timer)
|
||||||
|
c.expire()
|
||||||
|
timeSleep = time.Time{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func restartTimer(t *time.Timer, d time.Duration) {
|
||||||
|
stopTimer(t)
|
||||||
|
t.Reset(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopTimer(t *time.Timer) {
|
||||||
|
t.Stop()
|
||||||
|
|
||||||
|
// go < 1.23 returns stale values on expired timers.
|
||||||
|
if len(t.C) != 0 {
|
||||||
|
<-t.C
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) expire() {
|
||||||
|
t := c.tc.Now()
|
||||||
|
var soon time.Time
|
||||||
|
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
for k, v := range c.m {
|
||||||
|
if v.t.IsZero() {
|
||||||
|
continue
|
||||||
|
} else if v.t.After(t) {
|
||||||
|
if soon.IsZero() || soon.After(v.t) {
|
||||||
|
soon = v.t
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.deleteUnsafe(k, v, ReasonTimedOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !soon.IsZero() { // wake-up feedback loop
|
||||||
|
go func(s time.Time) { // we need to release the lock, if the input pipeline has exceeded the wakeup budget.
|
||||||
|
defer func() {
|
||||||
|
_ = recover() // if the channel is closed, this doesn't matter on shutdown because this is expected.
|
||||||
|
}()
|
||||||
|
c.ch <- s
|
||||||
|
}(soon)
|
||||||
|
}
|
||||||
|
}
|
111
pkg/ttlcache/internal.go
Normal file
111
pkg/ttlcache/internal.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package ttlcache
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) get(key K) (Item[V], bool) {
|
||||||
|
c.l.RLock()
|
||||||
|
defer c.l.RUnlock()
|
||||||
|
return c._g(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) _g(key K) (Item[V], bool) {
|
||||||
|
v, ok := c.m[key]
|
||||||
|
if !ok {
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) set(key K, it Item[V]) Item[V] {
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
return c._s(key, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) _s(key K, it Item[V]) Item[V] {
|
||||||
|
it.d, it.t = c.getDuration(it.d)
|
||||||
|
c.m[key] = it
|
||||||
|
c.ch <- it.t
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) getOrSet(key K, it Item[V]) (Item[V], bool) {
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
return c._gos(key, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) _gos(key K, it Item[V]) (Item[V], bool) {
|
||||||
|
if g, ok := c._g(key); ok {
|
||||||
|
return g, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return c._s(key, it), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) delete(key K, reason DeallocationReason) {
|
||||||
|
var v Item[V]
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
|
||||||
|
if c.o.deallocationFunc != nil {
|
||||||
|
var ok bool
|
||||||
|
v, ok = c.m[key]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.deleteUnsafe(key, v, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) deleteUnsafe(key K, v Item[V], reason DeallocationReason) {
|
||||||
|
delete(c.m, key)
|
||||||
|
|
||||||
|
if c.o.deallocationFunc != nil {
|
||||||
|
c.o.deallocationFunc(key, v.v, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) getkeys() []K {
|
||||||
|
c.l.RLock()
|
||||||
|
defer c.l.RUnlock()
|
||||||
|
|
||||||
|
keys := make([]K, len(c.m))
|
||||||
|
for k := range c.m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) close() {
|
||||||
|
c.l.Lock()
|
||||||
|
defer c.l.Unlock()
|
||||||
|
close(c.ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) getDuration(d time.Duration) (time.Duration, time.Time) {
|
||||||
|
switch d {
|
||||||
|
case NoTTL:
|
||||||
|
case DefaultTTL:
|
||||||
|
return c.o.defaultTTL, c.tc.Now().Add(c.o.defaultTTL)
|
||||||
|
default:
|
||||||
|
return d, c.tc.Now().Add(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NoTTL, time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item[V]) getDuration() time.Duration {
|
||||||
|
return i.d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item[V]) getTime() time.Time {
|
||||||
|
return i.t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item[V]) getValue() V {
|
||||||
|
return i.v
|
||||||
|
}
|
127
pkg/ttlcache/ttlcache.go
Normal file
127
pkg/ttlcache/ttlcache.go
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
package ttlcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/pkg/timecache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New[K comparable, V any](options Options[K, V]) *Cache[K, V] {
|
||||||
|
c := Cache[K, V]{
|
||||||
|
o: options,
|
||||||
|
ch: make(chan time.Time, 1000),
|
||||||
|
m: make(map[K]Item[V]),
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.defaultTTL != NoTTL && options.defaultResolution == 0 {
|
||||||
|
c.tc = *timecache.New(timecache.Options{}.Round(options.defaultTTL / 2))
|
||||||
|
} else if options.defaultResolution != 0 {
|
||||||
|
c.tc = *timecache.New(timecache.Options{}.Round(options.defaultResolution))
|
||||||
|
}
|
||||||
|
|
||||||
|
go c.startExpirations()
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) Get(key K) (V, bool) {
|
||||||
|
it, ok := c.GetItem(key)
|
||||||
|
if !ok {
|
||||||
|
return *new(V), ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return it.GetValue(), ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) GetItem(key K) (Item[V], bool) {
|
||||||
|
it, ok := c.get(key)
|
||||||
|
if !ok {
|
||||||
|
return it, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.o.noUpdateTime && !it.t.IsZero() {
|
||||||
|
if _, t := c.getDuration(it.d); t.After(it.t) {
|
||||||
|
c.set(key, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return it, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) GetOrSet(key K, value V, duration time.Duration) (V, bool) {
|
||||||
|
it, ok := c.GetOrSetItem(key, value, duration)
|
||||||
|
if !ok {
|
||||||
|
return *new(V), ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return it.GetValue(), ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) fixupDuration(duration time.Duration) time.Duration {
|
||||||
|
if c.o.defaultTTL == NoTTL && duration == DefaultTTL {
|
||||||
|
return NoTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) GetOrSetItem(key K, value V, duration time.Duration) (Item[V], bool) {
|
||||||
|
it, ok := c.getOrSet(key, Item[V]{v: value, d: c.fixupDuration(duration)})
|
||||||
|
if !ok {
|
||||||
|
return Item[V]{}, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return it, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) Set(key K, value V, duration time.Duration) bool {
|
||||||
|
c.SetItem(key, value, duration)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) SetItem(key K, value V, duration time.Duration) Item[V] {
|
||||||
|
return c.set(key, Item[V]{v: value, d: c.fixupDuration(duration)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) Delete(key K) {
|
||||||
|
c.delete(key, ReasonDeleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) GetKeys() []K {
|
||||||
|
return c.getkeys()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) Close() {
|
||||||
|
c.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item[V]) GetDuration() time.Duration {
|
||||||
|
return i.getDuration()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item[V]) GetTime() time.Time {
|
||||||
|
return i.getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item[V]) GetValue() V {
|
||||||
|
return i.getValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Options[K, V]) SetTimerResolution(d time.Duration) Options[K, V] {
|
||||||
|
o.defaultResolution = d
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Options[K, V]) SetDefaultTTL(d time.Duration) Options[K, V] {
|
||||||
|
o.defaultTTL = d
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Options[K, V]) SetDeallocationFunc(f DeallocationFunc[K, V]) Options[K, V] {
|
||||||
|
o.deallocationFunc = f
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Options[K, V]) DisableUpdateTime(val bool) Options[K, V] {
|
||||||
|
o.noUpdateTime = val
|
||||||
|
return o
|
||||||
|
}
|
245
pkg/ttlcache/ttlcache_test.go
Normal file
245
pkg/ttlcache/ttlcache_test.go
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
package ttlcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := New[int, bool](Options[int, bool]{}.SetDefaultTTL(1 * time.Second))
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
c.Set(i, true, DefaultTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
val, ok := c.Get(i)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("missing key: %d", i)
|
||||||
|
} else if !val {
|
||||||
|
t.Fatalf("bad value on key: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpirations(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := New[int, bool](Options[int, bool]{}.SetDefaultTTL(200 * time.Millisecond))
|
||||||
|
defer c.Close()
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
c.Set(i, true, DefaultTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
if _, ok := c.Get(i); ok {
|
||||||
|
t.Fatalf("found key: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwaps(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := New[int, bool](Options[int, bool]{}.SetDefaultTTL(200 * time.Millisecond))
|
||||||
|
defer c.Close()
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
c.Set(i, true, DefaultTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
if _, ok := c.Get(i); ok {
|
||||||
|
t.Fatalf("found key: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 10; i < 20; i++ {
|
||||||
|
c.Set(i, true, DefaultTTL)
|
||||||
|
if _, ok := c.Get(i); !ok {
|
||||||
|
t.Fatalf("missing key: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetimer(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := New[int, bool](Options[int, bool]{}.SetDefaultTTL(200 * time.Millisecond))
|
||||||
|
defer c.Close()
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
c.Set(i, true, time.Duration(10-i)*100*time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
if _, ok := c.Get(i); ok {
|
||||||
|
t.Fatalf("found key: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSchedule(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := New[int, bool](Options[int, bool]{}.SetDefaultTTL(1 * time.Second))
|
||||||
|
defer c.Close()
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
c.Set(i, true, time.Duration(i)*100*time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
if _, ok := c.Get(i); ok {
|
||||||
|
t.Fatalf("found key: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterlace(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := New[int, bool](Options[int, bool]{}.SetDefaultTTL(100 * time.Millisecond))
|
||||||
|
defer c.Close()
|
||||||
|
swap := false
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
swap = !swap
|
||||||
|
ttl := DefaultTTL
|
||||||
|
if swap {
|
||||||
|
ttl = NoTTL
|
||||||
|
}
|
||||||
|
c.Set(i, true, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
swap = false
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
swap = !swap
|
||||||
|
if !swap {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := c.Get(i); !ok {
|
||||||
|
t.Fatalf("found key: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReschedule(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := New[int, bool](Options[int, bool]{}.SetDefaultTTL(100 * time.Millisecond))
|
||||||
|
defer c.Close()
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
c.Set(i, true, NoTTL)
|
||||||
|
c.Set(i, true, DefaultTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
if _, ok := c.Get(i); ok {
|
||||||
|
t.Fatalf("found key: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRescheduleNoTTL(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := New[int, bool](Options[int, bool]{}.SetDefaultTTL(100 * time.Millisecond))
|
||||||
|
defer c.Close()
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
c.Set(i, true, DefaultTTL)
|
||||||
|
c.Set(i, true, NoTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
if _, ok := c.Get(i); !ok {
|
||||||
|
t.Fatalf("found key: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
c := New[int, bool](Options[int, bool]{}.SetDefaultTTL(100 * time.Millisecond))
|
||||||
|
defer c.Close()
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
c.Set(i, true, NoTTL)
|
||||||
|
c.Delete(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < 10; i++ {
|
||||||
|
if _, ok := c.Get(i); ok {
|
||||||
|
t.Fatalf("found key: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeallocationTimeout(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
hit := false
|
||||||
|
o := Options[int, bool]{}.
|
||||||
|
SetDefaultTTL(time.Millisecond * 100).
|
||||||
|
SetDeallocationFunc(func(key int, value bool, reason DeallocationReason) { hit = reason == ReasonTimedOut })
|
||||||
|
|
||||||
|
c := New[int, bool](o)
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
for i := 0; i < 1; i++ {
|
||||||
|
c.Set(i, true, DefaultTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
if !hit {
|
||||||
|
t.Fatalf("Deallocation not hit.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeallocationDeleted(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
hit := false
|
||||||
|
o := Options[int, bool]{}.
|
||||||
|
SetDefaultTTL(time.Millisecond * 100).
|
||||||
|
SetDeallocationFunc(func(key int, value bool, reason DeallocationReason) { hit = reason == ReasonDeleted })
|
||||||
|
|
||||||
|
c := New[int, bool](o)
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
for i := 0; i < 1; i++ {
|
||||||
|
c.Set(i, true, DefaultTTL)
|
||||||
|
c.Delete(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hit {
|
||||||
|
t.Fatalf("Deallocation not hit.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimerReset(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ch := make(chan struct{})
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
c := New[int, bool](Options[int, bool]{}.
|
||||||
|
SetDefaultTTL(time.Millisecond * 100).
|
||||||
|
SetDeallocationFunc(func(key int, value bool, reason DeallocationReason) { ch <- struct{}{} }))
|
||||||
|
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
const base = 0
|
||||||
|
const rounds = 1
|
||||||
|
for i := base; i < rounds; i++ {
|
||||||
|
c.Set(i, true, DefaultTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := base; i < rounds; i++ {
|
||||||
|
<-ch
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 1; i++ {
|
||||||
|
c.Set(i, true, DefaultTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := base; i < rounds; i++ {
|
||||||
|
<-ch
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue