package cache import ( "context" "sync" "time" "codeberg.org/gruf/go-runners" ) // TTLCache is the underlying Cache implementation, providing both the base // Cache interface and access to "unsafe" methods so that you may build your // customized caches ontop of this structure. type TTLCache[Key comparable, Value any] struct { cache map[Key](*entry[Value]) evict Hook[Key, Value] // the evict hook is called when an item is evicted from the cache, includes manual delete invalid Hook[Key, Value] // the invalidate hook is called when an item's data in the cache is invalidated ttl time.Duration // ttl is the item TTL svc runners.Service // svc manages running of the cache eviction routine mu sync.Mutex // mu protects TTLCache for concurrent access } // Init performs Cache initialization, this MUST be called. func (c *TTLCache[K, V]) Init() { c.cache = make(map[K](*entry[V]), 100) c.evict = emptyHook[K, V] c.invalid = emptyHook[K, V] c.ttl = time.Minute * 5 } func (c *TTLCache[K, V]) Start(freq time.Duration) bool { // Nothing to start if freq <= 0 { return false } // Track state of starting done := make(chan struct{}) started := false go func() { ran := c.svc.Run(func(ctx context.Context) { // Successfully started started = true close(done) // start routine c.run(ctx, freq) }) // failed to start if !ran { close(done) } }() <-done return started } func (c *TTLCache[K, V]) Stop() bool { return c.svc.Stop() } func (c *TTLCache[K, V]) run(ctx context.Context, freq time.Duration) { t := time.NewTimer(freq) for { select { // we got stopped case <-ctx.Done(): if !t.Stop() { <-t.C } return // next tick case <-t.C: c.sweep() t.Reset(freq) } } } // sweep attempts to evict expired items (with callback!) from cache. func (c *TTLCache[K, V]) sweep() { // Lock and defer unlock (in case of hook panic) c.mu.Lock() defer c.mu.Unlock() // Fetch current time for TTL check now := time.Now() // Sweep the cache for old items! for key, item := range c.cache { if now.After(item.expiry) { c.evict(key, item.value) delete(c.cache, key) } } } // Lock locks the cache mutex. func (c *TTLCache[K, V]) Lock() { c.mu.Lock() } // Unlock unlocks the cache mutex. func (c *TTLCache[K, V]) Unlock() { c.mu.Unlock() } func (c *TTLCache[K, V]) SetEvictionCallback(hook Hook[K, V]) { // Ensure non-nil hook if hook == nil { hook = emptyHook[K, V] } // Safely set evict hook c.Lock() c.evict = hook c.Unlock() } func (c *TTLCache[K, V]) SetInvalidateCallback(hook Hook[K, V]) { // Ensure non-nil hook if hook == nil { hook = emptyHook[K, V] } // Safely set invalidate hook c.Lock() c.invalid = hook c.Unlock() } func (c *TTLCache[K, V]) SetTTL(ttl time.Duration, update bool) { // Safely update TTL c.Lock() diff := ttl - c.ttl c.ttl = ttl if update { // Update existing cache entries for _, entry := range c.cache { entry.expiry.Add(diff) } } // We're done c.Unlock() } func (c *TTLCache[K, V]) Get(key K) (V, bool) { c.Lock() value, ok := c.GetUnsafe(key) c.Unlock() return value, ok } // GetUnsafe is the mutex-unprotected logic for Cache.Get(). func (c *TTLCache[K, V]) GetUnsafe(key K) (V, bool) { item, ok := c.cache[key] if !ok { var value V return value, false } item.expiry = time.Now().Add(c.ttl) return item.value, true } func (c *TTLCache[K, V]) Put(key K, value V) bool { c.Lock() success := c.PutUnsafe(key, value) c.Unlock() return success } // PutUnsafe is the mutex-unprotected logic for Cache.Put(). func (c *TTLCache[K, V]) PutUnsafe(key K, value V) bool { // If already cached, return if _, ok := c.cache[key]; ok { return false } // Create new cached item c.cache[key] = &entry[V]{ value: value, expiry: time.Now().Add(c.ttl), } return true } func (c *TTLCache[K, V]) Set(key K, value V) { c.Lock() defer c.Unlock() // defer in case of hook panic c.SetUnsafe(key, value) } // SetUnsafe is the mutex-unprotected logic for Cache.Set(), it calls externally-set functions. func (c *TTLCache[K, V]) SetUnsafe(key K, value V) { item, ok := c.cache[key] if ok { // call invalidate hook c.invalid(key, item.value) } else { // alloc new item item = &entry[V]{} c.cache[key] = item } // Update the item + expiry item.value = value item.expiry = time.Now().Add(c.ttl) } func (c *TTLCache[K, V]) CAS(key K, cmp V, swp V) bool { c.Lock() ok := c.CASUnsafe(key, cmp, swp) c.Unlock() return ok } // CASUnsafe is the mutex-unprotected logic for Cache.CAS(). func (c *TTLCache[K, V]) CASUnsafe(key K, cmp V, swp V) bool { // Check for item item, ok := c.cache[key] if !ok || !Compare(item.value, cmp) { return false } // Invalidate item c.invalid(key, item.value) // Update item + expiry item.value = swp item.expiry = time.Now().Add(c.ttl) return ok } func (c *TTLCache[K, V]) Swap(key K, swp V) V { c.Lock() old := c.SwapUnsafe(key, swp) c.Unlock() return old } // SwapUnsafe is the mutex-unprotected logic for Cache.Swap(). func (c *TTLCache[K, V]) SwapUnsafe(key K, swp V) V { // Check for item item, ok := c.cache[key] if !ok { var value V return value } // invalidate old item c.invalid(key, item.value) old := item.value // update item + expiry item.value = swp item.expiry = time.Now().Add(c.ttl) return old } func (c *TTLCache[K, V]) Has(key K) bool { c.Lock() ok := c.HasUnsafe(key) c.Unlock() return ok } // HasUnsafe is the mutex-unprotected logic for Cache.Has(). func (c *TTLCache[K, V]) HasUnsafe(key K) bool { _, ok := c.cache[key] return ok } func (c *TTLCache[K, V]) Invalidate(key K) bool { c.Lock() defer c.Unlock() return c.InvalidateUnsafe(key) } // InvalidateUnsafe is mutex-unprotected logic for Cache.Invalidate(). func (c *TTLCache[K, V]) InvalidateUnsafe(key K) bool { // Check if we have item with key item, ok := c.cache[key] if !ok { return false } // Call hook, remove from cache c.invalid(key, item.value) delete(c.cache, key) return true } func (c *TTLCache[K, V]) Clear() { c.Lock() defer c.Unlock() c.ClearUnsafe() } // ClearUnsafe is mutex-unprotected logic for Cache.Clean(). func (c *TTLCache[K, V]) ClearUnsafe() { for key, item := range c.cache { c.invalid(key, item.value) delete(c.cache, key) } } func (c *TTLCache[K, V]) Size() int { c.Lock() sz := c.SizeUnsafe() c.Unlock() return sz } // SizeUnsafe is mutex unprotected logic for Cache.Size(). func (c *TTLCache[K, V]) SizeUnsafe() int { return len(c.cache) } // entry represents an item in the cache, with // it's currently calculated expiry time. type entry[Value any] struct { value Value expiry time.Time }