2022-11-11 11:18:38 +00:00
package ttl
import (
"sync"
"time"
"codeberg.org/gruf/go-maps"
)
// Entry represents an item in the cache, with it's currently calculated Expiry time.
type Entry [ Key comparable , Value any ] struct {
Key Key
Value Value
Expiry time . Time
}
// Cache is the underlying Cache implementation, providing both the base Cache interface and unsafe access to underlying map to allow flexibility in building your own.
type Cache [ Key comparable , Value any ] struct {
// TTL is the cache item TTL.
TTL time . Duration
2023-04-19 11:46:42 +00:00
// Evict is the hook that is called when an item is evicted from the cache.
2023-05-14 13:17:03 +00:00
Evict func ( Key , Value )
2022-11-11 11:18:38 +00:00
2023-04-19 11:46:42 +00:00
// Invalid is the hook that is called when an item's data in the cache is invalidated, includes Add/Set.
2023-05-14 13:17:03 +00:00
Invalid func ( Key , Value )
2022-11-11 11:18:38 +00:00
// Cache is the underlying hashmap used for this cache.
Cache maps . LRUMap [ Key , * Entry [ Key , Value ] ]
// stop is the eviction routine cancel func.
stop func ( )
// pool is a memory pool of entry objects.
pool [ ] * Entry [ Key , Value ]
// Embedded mutex.
sync . Mutex
}
// New returns a new initialized Cache with given initial length, maximum capacity and item TTL.
func New [ K comparable , V any ] ( len , cap int , ttl time . Duration ) * Cache [ K , V ] {
c := new ( Cache [ K , V ] )
c . Init ( len , cap , ttl )
return c
}
// Init will initialize this cache with given initial length, maximum capacity and item TTL.
func ( c * Cache [ K , V ] ) Init ( len , cap int , ttl time . Duration ) {
if ttl <= 0 {
// Default duration
ttl = time . Second * 5
}
c . TTL = ttl
c . SetEvictionCallback ( nil )
c . SetInvalidateCallback ( nil )
c . Cache . Init ( len , cap )
}
// Start: implements cache.Cache's Start().
func ( c * Cache [ K , V ] ) Start ( freq time . Duration ) ( ok bool ) {
// Nothing to start
if freq <= 0 {
return false
}
// Safely start
c . Lock ( )
if ok = c . stop == nil ; ok {
// Not yet running, schedule us
c . stop = schedule ( c . Sweep , freq )
}
// Done with lock
c . Unlock ( )
return
}
// Stop: implements cache.Cache's Stop().
func ( c * Cache [ K , V ] ) Stop ( ) ( ok bool ) {
// Safely stop
c . Lock ( )
if ok = c . stop != nil ; ok {
// We're running, cancel evicts
c . stop ( )
c . stop = nil
}
// Done with lock
c . Unlock ( )
return
}
// Sweep attempts to evict expired items (with callback!) from cache.
func ( c * Cache [ K , V ] ) Sweep ( now time . Time ) {
2023-05-14 13:17:03 +00:00
var (
// evicted key-values.
kvs [ ] kv [ K , V ]
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// hook func ptrs.
evict func ( K , V )
)
c . locked ( func ( ) {
// Sentinel value
after := - 1
// The cache will be ordered by expiry date, we iterate until we reach the index of
// the youngest item that hsa expired, as all succeeding items will also be expired.
c . Cache . RangeIf ( 0 , c . Cache . Len ( ) , func ( i int , _ K , item * Entry [ K , V ] ) bool {
if now . After ( item . Expiry ) {
after = i
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// evict all older items
// than this (inclusive)
return false
}
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// cont. loop.
return true
} )
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
if after == - 1 {
// No Truncation needed
return
2022-11-11 11:18:38 +00:00
}
2023-05-14 13:17:03 +00:00
// Set hook func ptr.
evict = c . Evict
// Truncate determined size.
sz := c . Cache . Len ( ) - after
kvs = c . truncate ( sz , evict )
2022-11-11 11:18:38 +00:00
} )
2023-05-14 13:17:03 +00:00
if evict != nil {
for x := range kvs {
// Pass to eviction hook.
evict ( kvs [ x ] . K , kvs [ x ] . V )
}
2022-11-11 11:18:38 +00:00
}
}
// SetEvictionCallback: implements cache.Cache's SetEvictionCallback().
2023-05-14 13:17:03 +00:00
func ( c * Cache [ K , V ] ) SetEvictionCallback ( hook func ( K , V ) ) {
c . locked ( func ( ) {
c . Evict = hook
} )
2022-11-11 11:18:38 +00:00
}
// SetInvalidateCallback: implements cache.Cache's SetInvalidateCallback().
2023-05-14 13:17:03 +00:00
func ( c * Cache [ K , V ] ) SetInvalidateCallback ( hook func ( K , V ) ) {
c . locked ( func ( ) {
c . Invalid = hook
} )
2022-11-11 11:18:38 +00:00
}
// SetTTL: implements cache.Cache's SetTTL().
func ( c * Cache [ K , V ] ) SetTTL ( ttl time . Duration , update bool ) {
if ttl < 0 {
panic ( "ttl must be greater than zero" )
}
2023-05-14 13:17:03 +00:00
c . locked ( func ( ) {
// Set updated TTL
diff := ttl - c . TTL
c . TTL = ttl
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
if update {
// Update existing cache entries with new expiry time
c . Cache . Range ( 0 , c . Cache . Len ( ) , func ( i int , _ K , item * Entry [ K , V ] ) {
item . Expiry = item . Expiry . Add ( diff )
} )
}
} )
2022-11-11 11:18:38 +00:00
}
// Get: implements cache.Cache's Get().
func ( c * Cache [ K , V ] ) Get ( key K ) ( V , bool ) {
2023-05-14 13:17:03 +00:00
var (
// did exist in cache?
ok bool
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// cached value.
v V
)
c . locked ( func ( ) {
var item * Entry [ K , V ]
// Check for item in cache
item , ok = c . Cache . Get ( key )
if ! ok {
return
}
// Update fetched item's expiry
item . Expiry = time . Now ( ) . Add ( c . TTL )
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// Set value.
v = item . Value
} )
return v , ok
2022-11-11 11:18:38 +00:00
}
// Add: implements cache.Cache's Add().
func ( c * Cache [ K , V ] ) Add ( key K , value V ) bool {
2023-05-14 13:17:03 +00:00
var (
// did exist in cache?
ok bool
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// was entry evicted?
ev bool
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// evicted key values.
evcK K
evcV V
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// hook func ptrs.
evict func ( K , V )
)
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
c . locked ( func ( ) {
// Check if in cache.
ok = c . Cache . Has ( key )
if ok {
return
2022-11-11 11:18:38 +00:00
}
2023-05-14 13:17:03 +00:00
// Alloc new entry.
new := c . alloc ( )
new . Expiry = time . Now ( ) . Add ( c . TTL )
new . Key = key
new . Value = value
// Add new entry to cache and catched any evicted item.
c . Cache . SetWithHook ( key , new , func ( _ K , item * Entry [ K , V ] ) {
evcK = item . Key
evcV = item . Value
ev = true
c . free ( item )
} )
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// Set hook func ptr.
evict = c . Evict
} )
if ev && evict != nil {
// Pass to eviction hook.
evict ( evcK , evcV )
2023-04-19 11:46:42 +00:00
}
2023-05-14 13:17:03 +00:00
return ! ok
2022-11-11 11:18:38 +00:00
}
// Set: implements cache.Cache's Set().
func ( c * Cache [ K , V ] ) Set ( key K , value V ) {
2023-05-14 13:17:03 +00:00
var (
// did exist in cache?
ok bool
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// was entry evicted?
ev bool
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// old value.
oldV V
2023-04-19 11:46:42 +00:00
2023-05-14 13:17:03 +00:00
// evicted key values.
evcK K
evcV V
2023-04-19 11:46:42 +00:00
2023-05-14 13:17:03 +00:00
// hook func ptrs.
invalid func ( K , V )
evict func ( K , V )
)
c . locked ( func ( ) {
var item * Entry [ K , V ]
// Check for item in cache
item , ok = c . Cache . Get ( key )
if ok {
// Set old value.
oldV = item . Value
// Update the existing item.
item . Expiry = time . Now ( ) . Add ( c . TTL )
item . Value = value
} else {
// Alloc new entry.
new := c . alloc ( )
new . Expiry = time . Now ( ) . Add ( c . TTL )
new . Key = key
new . Value = value
// Add new entry to cache and catched any evicted item.
c . Cache . SetWithHook ( key , new , func ( _ K , item * Entry [ K , V ] ) {
evcK = item . Key
evcV = item . Value
ev = true
c . free ( item )
} )
2023-04-19 11:46:42 +00:00
}
2023-05-14 13:17:03 +00:00
// Set hook func ptrs.
invalid = c . Invalid
evict = c . Evict
} )
2023-04-19 11:46:42 +00:00
2023-05-14 13:17:03 +00:00
if ok && invalid != nil {
// Pass to invalidate hook.
invalid ( key , oldV )
2022-11-11 11:18:38 +00:00
}
2023-05-14 13:17:03 +00:00
if ev && evict != nil {
// Pass to eviction hook.
evict ( evcK , evcV )
}
2022-11-11 11:18:38 +00:00
}
// CAS: implements cache.Cache's CAS().
func ( c * Cache [ K , V ] ) CAS ( key K , old V , new V , cmp func ( V , V ) bool ) bool {
2023-05-14 13:17:03 +00:00
var (
// did exist in cache?
ok bool
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// swapped value.
oldV V
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// hook func ptrs.
invalid func ( K , V )
)
c . locked ( func ( ) {
var item * Entry [ K , V ]
// Check for item in cache
item , ok = c . Cache . Get ( key )
if ! ok {
return
}
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// Perform the comparison
if ! cmp ( old , item . Value ) {
return
}
// Set old value.
oldV = item . Value
// Update value + expiry.
item . Expiry = time . Now ( ) . Add ( c . TTL )
item . Value = new
// Set hook func ptr.
invalid = c . Invalid
} )
if ok && invalid != nil {
// Pass to invalidate hook.
invalid ( key , oldV )
}
2022-11-11 11:18:38 +00:00
return ok
}
// Swap: implements cache.Cache's Swap().
func ( c * Cache [ K , V ] ) Swap ( key K , swp V ) V {
2023-05-14 13:17:03 +00:00
var (
// did exist in cache?
ok bool
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// swapped value.
oldV V
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// hook func ptrs.
invalid func ( K , V )
)
c . locked ( func ( ) {
var item * Entry [ K , V ]
// Check for item in cache
item , ok = c . Cache . Get ( key )
if ! ok {
return
}
// Set old value.
oldV = item . Value
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// Update value + expiry.
item . Expiry = time . Now ( ) . Add ( c . TTL )
item . Value = swp
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// Set hook func ptr.
invalid = c . Invalid
} )
if ok && invalid != nil {
// Pass to invalidate hook.
invalid ( key , oldV )
}
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
return oldV
2022-11-11 11:18:38 +00:00
}
// Has: implements cache.Cache's Has().
2023-05-14 13:17:03 +00:00
func ( c * Cache [ K , V ] ) Has ( key K ) ( ok bool ) {
c . locked ( func ( ) {
ok = c . Cache . Has ( key )
} )
return
2022-11-11 11:18:38 +00:00
}
// Invalidate: implements cache.Cache's Invalidate().
2023-05-14 13:17:03 +00:00
func ( c * Cache [ K , V ] ) Invalidate ( key K ) ( ok bool ) {
var (
// old value.
oldV V
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// hook func ptrs.
invalid func ( K , V )
)
c . locked ( func ( ) {
var item * Entry [ K , V ]
// Check for item in cache
item , ok = c . Cache . Get ( key )
if ! ok {
return
}
// Set old value.
oldV = item . Value
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// Remove from cache map
_ = c . Cache . Delete ( key )
// Free entry
c . free ( item )
// Set hook func ptrs.
invalid = c . Invalid
} )
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
if ok && invalid != nil {
// Pass to invalidate hook.
invalid ( key , oldV )
2022-11-11 11:18:38 +00:00
}
2023-05-14 13:17:03 +00:00
return
}
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// InvalidateAll: implements cache.Cache's InvalidateAll().
func ( c * Cache [ K , V ] ) InvalidateAll ( keys ... K ) ( ok bool ) {
var (
// invalidated kvs.
kvs [ ] kv [ K , V ]
// hook func ptrs.
invalid func ( K , V )
)
// Allocate a slice for invalidated.
kvs = make ( [ ] kv [ K , V ] , 0 , len ( keys ) )
c . locked ( func ( ) {
for _ , key := range keys {
var item * Entry [ K , V ]
// Check for item in cache
item , ok = c . Cache . Get ( key )
if ! ok {
return
}
// Append this old value to slice
kvs = append ( kvs , kv [ K , V ] {
K : key ,
V : item . Value ,
} )
// Remove from cache map
_ = c . Cache . Delete ( key )
// Free entry
c . free ( item )
}
// Set hook func ptrs.
invalid = c . Invalid
} )
if invalid != nil {
for x := range kvs {
// Pass to invalidate hook.
invalid ( kvs [ x ] . K , kvs [ x ] . V )
}
}
return
2022-11-11 11:18:38 +00:00
}
// Clear: implements cache.Cache's Clear().
func ( c * Cache [ K , V ] ) Clear ( ) {
2023-05-14 13:17:03 +00:00
var (
// deleted key-values.
kvs [ ] kv [ K , V ]
// hook func ptrs.
invalid func ( K , V )
)
c . locked ( func ( ) {
// Set hook func ptr.
invalid = c . Invalid
// Truncate the entire cache length.
kvs = c . truncate ( c . Cache . Len ( ) , invalid )
} )
if invalid != nil {
for x := range kvs {
// Pass to invalidate hook.
invalid ( kvs [ x ] . K , kvs [ x ] . V )
}
}
2022-11-11 11:18:38 +00:00
}
// Len: implements cache.Cache's Len().
2023-05-14 13:17:03 +00:00
func ( c * Cache [ K , V ] ) Len ( ) ( l int ) {
c . locked ( func ( ) { l = c . Cache . Len ( ) } )
return
2022-11-11 11:18:38 +00:00
}
// Cap: implements cache.Cache's Cap().
2023-05-14 13:17:03 +00:00
func ( c * Cache [ K , V ] ) Cap ( ) ( l int ) {
c . locked ( func ( ) { l = c . Cache . Cap ( ) } )
return
}
func ( c * Cache [ K , V ] ) locked ( fn func ( ) ) {
2022-11-11 11:18:38 +00:00
c . Lock ( )
2023-05-14 13:17:03 +00:00
fn ( )
2022-11-11 11:18:38 +00:00
c . Unlock ( )
}
2023-05-14 13:17:03 +00:00
// truncate will truncate the cache by given size, returning deleted items.
func ( c * Cache [ K , V ] ) truncate ( sz int , hook func ( K , V ) ) [ ] kv [ K , V ] {
2022-11-11 11:18:38 +00:00
if hook == nil {
2023-05-14 13:17:03 +00:00
// No hook to execute, simply free all truncated entries.
c . Cache . Truncate ( sz , func ( _ K , e * Entry [ K , V ] ) { c . free ( e ) } )
return nil
2022-11-11 11:18:38 +00:00
}
2023-05-14 13:17:03 +00:00
// Allocate a slice for deleted k-v pairs.
deleted := make ( [ ] kv [ K , V ] , 0 , sz )
2022-11-11 11:18:38 +00:00
c . Cache . Truncate ( sz , func ( _ K , item * Entry [ K , V ] ) {
2023-05-14 13:17:03 +00:00
// Store key-value pair for later access.
deleted = append ( deleted , kv [ K , V ] {
K : item . Key ,
V : item . Value ,
} )
2022-11-11 11:18:38 +00:00
2023-05-14 13:17:03 +00:00
// Free entry.
2022-11-11 11:18:38 +00:00
c . free ( item )
2023-05-14 13:17:03 +00:00
} )
return deleted
2022-11-11 11:18:38 +00:00
}
// alloc will acquire cache entry from pool, or allocate new.
func ( c * Cache [ K , V ] ) alloc ( ) * Entry [ K , V ] {
if len ( c . pool ) == 0 {
return & Entry [ K , V ] { }
}
idx := len ( c . pool ) - 1
e := c . pool [ idx ]
c . pool = c . pool [ : idx ]
return e
}
2023-05-14 13:17:03 +00:00
// clone allocates a new Entry and copies all info from passed Entry.
func ( c * Cache [ K , V ] ) clone ( e * Entry [ K , V ] ) * Entry [ K , V ] {
e2 := c . alloc ( )
e2 . Key = e . Key
e2 . Value = e . Value
e2 . Expiry = e . Expiry
return e2
}
2022-11-11 11:18:38 +00:00
// free will reset entry fields and place back in pool.
func ( c * Cache [ K , V ] ) free ( e * Entry [ K , V ] ) {
var (
zk K
zv V
2023-05-14 13:17:03 +00:00
zt time . Time
2022-11-11 11:18:38 +00:00
)
2023-05-14 13:17:03 +00:00
e . Expiry = zt
2022-11-11 11:18:38 +00:00
e . Key = zk
e . Value = zv
c . pool = append ( c . pool , e )
}
2023-05-14 13:17:03 +00:00
type kv [ K comparable , V any ] struct {
K K
V V
}