gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

ttl.go (11574B)


      1 package ttl
      2 
      3 import (
      4 	"sync"
      5 	"time"
      6 	_ "unsafe"
      7 
      8 	"codeberg.org/gruf/go-maps"
      9 )
     10 
     11 // Entry represents an item in the cache, with it's currently calculated Expiry time.
     12 type Entry[Key comparable, Value any] struct {
     13 	Key    Key
     14 	Value  Value
     15 	Expiry uint64
     16 }
     17 
     18 // 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.
     19 type Cache[Key comparable, Value any] struct {
     20 	// TTL is the cache item TTL.
     21 	TTL time.Duration
     22 
     23 	// Evict is the hook that is called when an item is evicted from the cache.
     24 	Evict func(Key, Value)
     25 
     26 	// Invalid is the hook that is called when an item's data in the cache is invalidated, includes Add/Set.
     27 	Invalid func(Key, Value)
     28 
     29 	// Cache is the underlying hashmap used for this cache.
     30 	Cache maps.LRUMap[Key, *Entry[Key, Value]]
     31 
     32 	// stop is the eviction routine cancel func.
     33 	stop func()
     34 
     35 	// pool is a memory pool of entry objects.
     36 	pool []*Entry[Key, Value]
     37 
     38 	// Embedded mutex.
     39 	sync.Mutex
     40 }
     41 
     42 // New returns a new initialized Cache with given initial length, maximum capacity and item TTL.
     43 func New[K comparable, V any](len, cap int, ttl time.Duration) *Cache[K, V] {
     44 	c := new(Cache[K, V])
     45 	c.Init(len, cap, ttl)
     46 	return c
     47 }
     48 
     49 // Init will initialize this cache with given initial length, maximum capacity and item TTL.
     50 func (c *Cache[K, V]) Init(len, cap int, ttl time.Duration) {
     51 	if ttl <= 0 {
     52 		// Default duration
     53 		ttl = time.Second * 5
     54 	}
     55 	c.TTL = ttl
     56 	c.SetEvictionCallback(nil)
     57 	c.SetInvalidateCallback(nil)
     58 	c.Cache.Init(len, cap)
     59 }
     60 
     61 // Start: implements cache.Cache's Start().
     62 func (c *Cache[K, V]) Start(freq time.Duration) (ok bool) {
     63 	// Nothing to start
     64 	if freq <= 0 {
     65 		return false
     66 	}
     67 
     68 	// Safely start
     69 	c.Lock()
     70 
     71 	if ok = (c.stop == nil); ok {
     72 		// Not yet running, schedule us
     73 		c.stop = schedule(c.Sweep, freq)
     74 	}
     75 
     76 	// Done with lock
     77 	c.Unlock()
     78 
     79 	return
     80 }
     81 
     82 // Stop: implements cache.Cache's Stop().
     83 func (c *Cache[K, V]) Stop() (ok bool) {
     84 	// Safely stop
     85 	c.Lock()
     86 
     87 	if ok = (c.stop != nil); ok {
     88 		// We're running, cancel evicts
     89 		c.stop()
     90 		c.stop = nil
     91 	}
     92 
     93 	// Done with lock
     94 	c.Unlock()
     95 
     96 	return
     97 }
     98 
     99 // Sweep attempts to evict expired items (with callback!) from cache.
    100 func (c *Cache[K, V]) Sweep(_ time.Time) {
    101 	var (
    102 		// evicted key-values.
    103 		kvs []kv[K, V]
    104 
    105 		// hook func ptrs.
    106 		evict func(K, V)
    107 
    108 		// get current nanoseconds.
    109 		now = runtime_nanotime()
    110 	)
    111 
    112 	c.locked(func() {
    113 		if c.TTL <= 0 {
    114 			// sweep is
    115 			// disabled
    116 			return
    117 		}
    118 
    119 		// Sentinel value
    120 		after := -1
    121 
    122 		// The cache will be ordered by expiry date, we iterate until we reach the index of
    123 		// the youngest item that hsa expired, as all succeeding items will also be expired.
    124 		c.Cache.RangeIf(0, c.Cache.Len(), func(i int, _ K, item *Entry[K, V]) bool {
    125 			if now > item.Expiry {
    126 				after = i
    127 
    128 				// evict all older items
    129 				// than this (inclusive)
    130 				return false
    131 			}
    132 
    133 			// cont. loop.
    134 			return true
    135 		})
    136 
    137 		if after == -1 {
    138 			// No Truncation needed
    139 			return
    140 		}
    141 
    142 		// Set hook func ptr.
    143 		evict = c.Evict
    144 
    145 		// Truncate determined size.
    146 		sz := c.Cache.Len() - after
    147 		kvs = c.truncate(sz, evict)
    148 	})
    149 
    150 	if evict != nil {
    151 		for x := range kvs {
    152 			// Pass to eviction hook.
    153 			evict(kvs[x].K, kvs[x].V)
    154 		}
    155 	}
    156 }
    157 
    158 // SetEvictionCallback: implements cache.Cache's SetEvictionCallback().
    159 func (c *Cache[K, V]) SetEvictionCallback(hook func(K, V)) {
    160 	c.locked(func() {
    161 		c.Evict = hook
    162 	})
    163 }
    164 
    165 // SetInvalidateCallback: implements cache.Cache's SetInvalidateCallback().
    166 func (c *Cache[K, V]) SetInvalidateCallback(hook func(K, V)) {
    167 	c.locked(func() {
    168 		c.Invalid = hook
    169 	})
    170 }
    171 
    172 // SetTTL: implements cache.Cache's SetTTL().
    173 func (c *Cache[K, V]) SetTTL(ttl time.Duration, update bool) {
    174 	c.locked(func() {
    175 		// Set updated TTL
    176 		diff := ttl - c.TTL
    177 		c.TTL = ttl
    178 
    179 		if update {
    180 			// Update existing cache entries with new expiry time
    181 			c.Cache.Range(0, c.Cache.Len(), func(i int, _ K, item *Entry[K, V]) {
    182 				item.Expiry += uint64(diff)
    183 			})
    184 		}
    185 	})
    186 }
    187 
    188 // Get: implements cache.Cache's Get().
    189 func (c *Cache[K, V]) Get(key K) (V, bool) {
    190 	var (
    191 		// did exist in cache?
    192 		ok bool
    193 
    194 		// cached value.
    195 		v V
    196 	)
    197 
    198 	c.locked(func() {
    199 		var item *Entry[K, V]
    200 
    201 		// Check for item in cache
    202 		item, ok = c.Cache.Get(key)
    203 		if !ok {
    204 			return
    205 		}
    206 
    207 		// Update fetched's expiry
    208 		item.Expiry = c.expiry()
    209 
    210 		// Set value.
    211 		v = item.Value
    212 	})
    213 
    214 	return v, ok
    215 }
    216 
    217 // Add: implements cache.Cache's Add().
    218 func (c *Cache[K, V]) Add(key K, value V) bool {
    219 	var (
    220 		// did exist in cache?
    221 		ok bool
    222 
    223 		// was entry evicted?
    224 		ev bool
    225 
    226 		// evicted key values.
    227 		evcK K
    228 		evcV V
    229 
    230 		// hook func ptrs.
    231 		evict func(K, V)
    232 	)
    233 
    234 	c.locked(func() {
    235 		// Check if in cache.
    236 		ok = c.Cache.Has(key)
    237 		if ok {
    238 			return
    239 		}
    240 
    241 		// Alloc new entry.
    242 		new := c.alloc()
    243 		new.Expiry = c.expiry()
    244 		new.Key = key
    245 		new.Value = value
    246 
    247 		// Add new entry to cache and catched any evicted item.
    248 		c.Cache.SetWithHook(key, new, func(_ K, item *Entry[K, V]) {
    249 			evcK = item.Key
    250 			evcV = item.Value
    251 			ev = true
    252 			c.free(item)
    253 		})
    254 
    255 		// Set hook func ptr.
    256 		evict = c.Evict
    257 	})
    258 
    259 	if ev && evict != nil {
    260 		// Pass to eviction hook.
    261 		evict(evcK, evcV)
    262 	}
    263 
    264 	return !ok
    265 }
    266 
    267 // Set: implements cache.Cache's Set().
    268 func (c *Cache[K, V]) Set(key K, value V) {
    269 	var (
    270 		// did exist in cache?
    271 		ok bool
    272 
    273 		// was entry evicted?
    274 		ev bool
    275 
    276 		// old value.
    277 		oldV V
    278 
    279 		// evicted key values.
    280 		evcK K
    281 		evcV V
    282 
    283 		// hook func ptrs.
    284 		invalid func(K, V)
    285 		evict   func(K, V)
    286 	)
    287 
    288 	c.locked(func() {
    289 		var item *Entry[K, V]
    290 
    291 		// Check for item in cache
    292 		item, ok = c.Cache.Get(key)
    293 
    294 		if ok {
    295 			// Set old value.
    296 			oldV = item.Value
    297 
    298 			// Update the existing item.
    299 			item.Expiry = c.expiry()
    300 			item.Value = value
    301 		} else {
    302 			// Alloc new entry.
    303 			new := c.alloc()
    304 			new.Expiry = c.expiry()
    305 			new.Key = key
    306 			new.Value = value
    307 
    308 			// Add new entry to cache and catched any evicted item.
    309 			c.Cache.SetWithHook(key, new, func(_ K, item *Entry[K, V]) {
    310 				evcK = item.Key
    311 				evcV = item.Value
    312 				ev = true
    313 				c.free(item)
    314 			})
    315 		}
    316 
    317 		// Set hook func ptrs.
    318 		invalid = c.Invalid
    319 		evict = c.Evict
    320 	})
    321 
    322 	if ok && invalid != nil {
    323 		// Pass to invalidate hook.
    324 		invalid(key, oldV)
    325 	}
    326 
    327 	if ev && evict != nil {
    328 		// Pass to eviction hook.
    329 		evict(evcK, evcV)
    330 	}
    331 }
    332 
    333 // CAS: implements cache.Cache's CAS().
    334 func (c *Cache[K, V]) CAS(key K, old V, new V, cmp func(V, V) bool) bool {
    335 	var (
    336 		// did exist in cache?
    337 		ok bool
    338 
    339 		// swapped value.
    340 		oldV V
    341 
    342 		// hook func ptrs.
    343 		invalid func(K, V)
    344 	)
    345 
    346 	c.locked(func() {
    347 		var item *Entry[K, V]
    348 
    349 		// Check for item in cache
    350 		item, ok = c.Cache.Get(key)
    351 		if !ok {
    352 			return
    353 		}
    354 
    355 		// Perform the comparison
    356 		if !cmp(old, item.Value) {
    357 			return
    358 		}
    359 
    360 		// Set old value.
    361 		oldV = item.Value
    362 
    363 		// Update value + expiry.
    364 		item.Expiry = c.expiry()
    365 		item.Value = new
    366 
    367 		// Set hook func ptr.
    368 		invalid = c.Invalid
    369 	})
    370 
    371 	if ok && invalid != nil {
    372 		// Pass to invalidate hook.
    373 		invalid(key, oldV)
    374 	}
    375 
    376 	return ok
    377 }
    378 
    379 // Swap: implements cache.Cache's Swap().
    380 func (c *Cache[K, V]) Swap(key K, swp V) V {
    381 	var (
    382 		// did exist in cache?
    383 		ok bool
    384 
    385 		// swapped value.
    386 		oldV V
    387 
    388 		// hook func ptrs.
    389 		invalid func(K, V)
    390 	)
    391 
    392 	c.locked(func() {
    393 		var item *Entry[K, V]
    394 
    395 		// Check for item in cache
    396 		item, ok = c.Cache.Get(key)
    397 		if !ok {
    398 			return
    399 		}
    400 
    401 		// Set old value.
    402 		oldV = item.Value
    403 
    404 		// Update value + expiry.
    405 		item.Expiry = c.expiry()
    406 		item.Value = swp
    407 
    408 		// Set hook func ptr.
    409 		invalid = c.Invalid
    410 	})
    411 
    412 	if ok && invalid != nil {
    413 		// Pass to invalidate hook.
    414 		invalid(key, oldV)
    415 	}
    416 
    417 	return oldV
    418 }
    419 
    420 // Has: implements cache.Cache's Has().
    421 func (c *Cache[K, V]) Has(key K) (ok bool) {
    422 	c.locked(func() {
    423 		ok = c.Cache.Has(key)
    424 	})
    425 	return
    426 }
    427 
    428 // Invalidate: implements cache.Cache's Invalidate().
    429 func (c *Cache[K, V]) Invalidate(key K) (ok bool) {
    430 	var (
    431 		// old value.
    432 		oldV V
    433 
    434 		// hook func ptrs.
    435 		invalid func(K, V)
    436 	)
    437 
    438 	c.locked(func() {
    439 		var item *Entry[K, V]
    440 
    441 		// Check for item in cache
    442 		item, ok = c.Cache.Get(key)
    443 		if !ok {
    444 			return
    445 		}
    446 
    447 		// Set old value.
    448 		oldV = item.Value
    449 
    450 		// Remove from cache map
    451 		_ = c.Cache.Delete(key)
    452 
    453 		// Free entry
    454 		c.free(item)
    455 
    456 		// Set hook func ptrs.
    457 		invalid = c.Invalid
    458 	})
    459 
    460 	if ok && invalid != nil {
    461 		// Pass to invalidate hook.
    462 		invalid(key, oldV)
    463 	}
    464 
    465 	return
    466 }
    467 
    468 // InvalidateAll: implements cache.Cache's InvalidateAll().
    469 func (c *Cache[K, V]) InvalidateAll(keys ...K) (ok bool) {
    470 	var (
    471 		// invalidated kvs.
    472 		kvs []kv[K, V]
    473 
    474 		// hook func ptrs.
    475 		invalid func(K, V)
    476 	)
    477 
    478 	// Allocate a slice for invalidated.
    479 	kvs = make([]kv[K, V], 0, len(keys))
    480 
    481 	c.locked(func() {
    482 		for _, key := range keys {
    483 			var item *Entry[K, V]
    484 
    485 			// Check for item in cache
    486 			item, ok = c.Cache.Get(key)
    487 			if !ok {
    488 				return
    489 			}
    490 
    491 			// Append this old value to slice
    492 			kvs = append(kvs, kv[K, V]{
    493 				K: key,
    494 				V: item.Value,
    495 			})
    496 
    497 			// Remove from cache map
    498 			_ = c.Cache.Delete(key)
    499 
    500 			// Free entry
    501 			c.free(item)
    502 		}
    503 
    504 		// Set hook func ptrs.
    505 		invalid = c.Invalid
    506 	})
    507 
    508 	if invalid != nil {
    509 		for x := range kvs {
    510 			// Pass to invalidate hook.
    511 			invalid(kvs[x].K, kvs[x].V)
    512 		}
    513 	}
    514 
    515 	return
    516 }
    517 
    518 // Clear: implements cache.Cache's Clear().
    519 func (c *Cache[K, V]) Clear() {
    520 	var (
    521 		// deleted key-values.
    522 		kvs []kv[K, V]
    523 
    524 		// hook func ptrs.
    525 		invalid func(K, V)
    526 	)
    527 
    528 	c.locked(func() {
    529 		// Set hook func ptr.
    530 		invalid = c.Invalid
    531 
    532 		// Truncate the entire cache length.
    533 		kvs = c.truncate(c.Cache.Len(), invalid)
    534 	})
    535 
    536 	if invalid != nil {
    537 		for x := range kvs {
    538 			// Pass to invalidate hook.
    539 			invalid(kvs[x].K, kvs[x].V)
    540 		}
    541 	}
    542 }
    543 
    544 // Len: implements cache.Cache's Len().
    545 func (c *Cache[K, V]) Len() (l int) {
    546 	c.locked(func() { l = c.Cache.Len() })
    547 	return
    548 }
    549 
    550 // Cap: implements cache.Cache's Cap().
    551 func (c *Cache[K, V]) Cap() (l int) {
    552 	c.locked(func() { l = c.Cache.Cap() })
    553 	return
    554 }
    555 
    556 func (c *Cache[K, V]) locked(fn func()) {
    557 	c.Lock()
    558 	fn()
    559 	c.Unlock()
    560 }
    561 
    562 // truncate will truncate the cache by given size, returning deleted items.
    563 func (c *Cache[K, V]) truncate(sz int, hook func(K, V)) []kv[K, V] {
    564 	if hook == nil {
    565 		// No hook to execute, simply free all truncated entries.
    566 		c.Cache.Truncate(sz, func(_ K, e *Entry[K, V]) { c.free(e) })
    567 		return nil
    568 	}
    569 
    570 	// Allocate a slice for deleted k-v pairs.
    571 	deleted := make([]kv[K, V], 0, sz)
    572 
    573 	c.Cache.Truncate(sz, func(_ K, item *Entry[K, V]) {
    574 		// Store key-value pair for later access.
    575 		deleted = append(deleted, kv[K, V]{
    576 			K: item.Key,
    577 			V: item.Value,
    578 		})
    579 
    580 		// Free entry.
    581 		c.free(item)
    582 	})
    583 
    584 	return deleted
    585 }
    586 
    587 // alloc will acquire cache entry from pool, or allocate new.
    588 func (c *Cache[K, V]) alloc() *Entry[K, V] {
    589 	if len(c.pool) == 0 {
    590 		return &Entry[K, V]{}
    591 	}
    592 	idx := len(c.pool) - 1
    593 	e := c.pool[idx]
    594 	c.pool = c.pool[:idx]
    595 	return e
    596 }
    597 
    598 // clone allocates a new Entry and copies all info from passed Entry.
    599 func (c *Cache[K, V]) clone(e *Entry[K, V]) *Entry[K, V] {
    600 	e2 := c.alloc()
    601 	e2.Key = e.Key
    602 	e2.Value = e.Value
    603 	e2.Expiry = e.Expiry
    604 	return e2
    605 }
    606 
    607 // free will reset entry fields and place back in pool.
    608 func (c *Cache[K, V]) free(e *Entry[K, V]) {
    609 	var (
    610 		zk K
    611 		zv V
    612 	)
    613 	e.Expiry = 0
    614 	e.Key = zk
    615 	e.Value = zv
    616 	c.pool = append(c.pool, e)
    617 }
    618 
    619 //go:linkname runtime_nanotime runtime.nanotime
    620 func runtime_nanotime() uint64
    621 
    622 // expiry returns an the next expiry time to use for an entry,
    623 // which is equivalent to time.Now().Add(ttl), or zero if disabled.
    624 func (c *Cache[K, V]) expiry() uint64 {
    625 	if ttl := c.TTL; ttl > 0 {
    626 		return runtime_nanotime() +
    627 			uint64(c.TTL)
    628 	}
    629 	return 0
    630 }
    631 
    632 type kv[K comparable, V any] struct {
    633 	K K
    634 	V V
    635 }