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 }