github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/ttlcache/cache.go (about) 1 package ttlcache 2 3 import ( 4 "container/list" 5 "context" 6 "fmt" 7 "sync" 8 "time" 9 10 "golang.org/x/sync/singleflight" 11 ) 12 13 // Available eviction reasons. 14 const ( 15 EvictionReasonDeleted EvictionReason = iota + 1 16 EvictionReasonCapacityReached 17 EvictionReasonExpired 18 ) 19 20 // EvictionReason is used to specify why a certain item was 21 // evicted/deleted. 22 type EvictionReason int 23 24 // Cache is a synchronised map of items that are automatically removed 25 // when they expire or the capacity is reached. 26 type Cache[K comparable, V any] struct { 27 items struct { 28 mu sync.RWMutex 29 values map[K]*list.Element 30 31 // a generic doubly linked list would be more convenient 32 // (and more performant?). It's possible that this 33 // will be introduced with/in go1.19+ 34 lru *list.List 35 expQueue expirationQueue[K, V] 36 37 timerCh chan time.Duration 38 } 39 40 metricsMu sync.RWMutex 41 metrics Metrics 42 43 events struct { 44 insertion struct { 45 mu sync.RWMutex 46 nextID uint64 47 fns map[uint64]func(*Item[K, V]) 48 } 49 eviction struct { 50 mu sync.RWMutex 51 nextID uint64 52 fns map[uint64]func(EvictionReason, *Item[K, V]) 53 } 54 } 55 56 stopCh chan struct{} 57 options options[K, V] 58 } 59 60 // New creates a new instance of cache. 61 func New[K comparable, V any](opts ...Option[K, V]) *Cache[K, V] { 62 c := &Cache[K, V]{ 63 stopCh: make(chan struct{}), 64 } 65 c.items.values = make(map[K]*list.Element) 66 c.items.lru = list.New() 67 c.items.expQueue = newExpirationQueue[K, V]() 68 c.items.timerCh = make(chan time.Duration, 1) // buffer is important 69 c.events.insertion.fns = make(map[uint64]func(*Item[K, V])) 70 c.events.eviction.fns = make(map[uint64]func(EvictionReason, *Item[K, V])) 71 72 applyOptions(&c.options, opts...) 73 74 return c 75 } 76 77 // updateExpirations updates the expiration queue and notifies 78 // the cache auto cleaner if needed. 79 // Not concurrently safe. 80 func (c *Cache[K, V]) updateExpirations(fresh bool, elem *list.Element) { 81 var oldExpiresAt time.Time 82 83 if !c.items.expQueue.isEmpty() { 84 oldExpiresAt = c.items.expQueue[0].Value.(*Item[K, V]).expiresAt 85 } 86 87 if fresh { 88 c.items.expQueue.push(elem) 89 } else { 90 c.items.expQueue.update(elem) 91 } 92 93 newExpiresAt := c.items.expQueue[0].Value.(*Item[K, V]).expiresAt 94 95 // check if the closest/soonest expiration timestamp changed 96 if newExpiresAt.IsZero() || (!oldExpiresAt.IsZero() && !newExpiresAt.Before(oldExpiresAt)) { 97 return 98 } 99 100 d := time.Until(newExpiresAt) 101 102 // It's possible that the auto cleaner isn't active or 103 // is busy, so we need to drain the channel before 104 // sending a new value. 105 // Also, since this method is called after locking the items' mutex, 106 // we can be sure that there is no other concurrent call of this 107 // method 108 if len(c.items.timerCh) > 0 { 109 // we need to drain this channel in a select with a default 110 // case because it's possible that the auto cleaner 111 // read this channel just after we entered this if 112 select { 113 case d1 := <-c.items.timerCh: 114 if d1 < d { 115 d = d1 116 } 117 default: 118 } 119 } 120 121 // since the channel has a size 1 buffer, we can be sure 122 // that the line below won't block (we can't overfill the buffer 123 // because we just drained it) 124 c.items.timerCh <- d 125 } 126 127 // set creates a new item, adds it to the cache and then returns it. 128 // Not concurrently safe. 129 func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] { 130 if ttl == DefaultTTL { 131 ttl = c.options.ttl 132 } 133 134 elem := c.get(key, false) 135 if elem != nil { 136 // update/overwrite an existing item 137 item := elem.Value.(*Item[K, V]) 138 item.update(value, ttl) 139 c.updateExpirations(false, elem) 140 141 return item 142 } 143 144 if c.options.capacity != 0 && uint64(len(c.items.values)) >= c.options.capacity { 145 // delete the oldest item 146 c.evict(EvictionReasonCapacityReached, c.items.lru.Back()) 147 } 148 149 // create a new item 150 item := newItem(key, value, ttl) 151 elem = c.items.lru.PushFront(item) 152 c.items.values[key] = elem 153 c.updateExpirations(true, elem) 154 155 c.metricsMu.Lock() 156 c.metrics.Insertions++ 157 c.metricsMu.Unlock() 158 159 c.events.insertion.mu.RLock() 160 for _, fn := range c.events.insertion.fns { 161 fn(item) 162 } 163 c.events.insertion.mu.RUnlock() 164 165 return item 166 } 167 168 // get retrieves an item from the cache and extends its expiration 169 // time if 'touch' is set to true. 170 // It returns nil if the item is not found or is expired. 171 // Not concurrently safe. 172 func (c *Cache[K, V]) get(key K, touch bool) *list.Element { 173 elem := c.items.values[key] 174 if elem == nil { 175 return nil 176 } 177 178 item := elem.Value.(*Item[K, V]) 179 if item.isExpiredUnsafe() { 180 return nil 181 } 182 183 c.items.lru.MoveToFront(elem) 184 185 if touch && item.ttl > 0 { 186 item.touch() 187 c.updateExpirations(false, elem) 188 } 189 190 return elem 191 } 192 193 // evict deletes items from the cache. 194 // If no items are provided, all currently present cache items 195 // are evicted. 196 // Not concurrently safe. 197 func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) { 198 if len(elems) > 0 { 199 c.metricsMu.Lock() 200 c.metrics.Evictions += uint64(len(elems)) 201 c.metricsMu.Unlock() 202 203 c.events.eviction.mu.RLock() 204 for i := range elems { 205 item := elems[i].Value.(*Item[K, V]) 206 delete(c.items.values, item.key) 207 c.items.lru.Remove(elems[i]) 208 c.items.expQueue.remove(elems[i]) 209 210 for _, fn := range c.events.eviction.fns { 211 fn(reason, item) 212 } 213 } 214 c.events.eviction.mu.RUnlock() 215 216 return 217 } 218 219 c.metricsMu.Lock() 220 c.metrics.Evictions += uint64(len(c.items.values)) 221 c.metricsMu.Unlock() 222 223 c.events.eviction.mu.RLock() 224 for _, elem := range c.items.values { 225 item := elem.Value.(*Item[K, V]) 226 227 for _, fn := range c.events.eviction.fns { 228 fn(reason, item) 229 } 230 } 231 c.events.eviction.mu.RUnlock() 232 233 c.items.values = make(map[K]*list.Element) 234 c.items.lru.Init() 235 c.items.expQueue = newExpirationQueue[K, V]() 236 } 237 238 // Set creates a new item from the provided key and value, adds 239 // it to the cache and then returns it. If an item associated with the 240 // provided key already exists, the new item overwrites the existing one. 241 func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) *Item[K, V] { 242 c.items.mu.Lock() 243 defer c.items.mu.Unlock() 244 245 return c.set(key, value, ttl) 246 } 247 248 // Get retrieves an item from the cache by the provided key. 249 // Unless this is disabled, it also extends/touches an item's 250 // expiration timestamp on successful retrieval. 251 // If the item is not found, a nil value is returned. 252 func (c *Cache[K, V]) Get(key K, opts ...Option[K, V]) *Item[K, V] { 253 getOpts := options[K, V]{ 254 loader: c.options.loader, 255 disableTouchOnHit: c.options.disableTouchOnHit, 256 } 257 258 applyOptions(&getOpts, opts...) 259 260 c.items.mu.Lock() 261 elem := c.get(key, !getOpts.disableTouchOnHit) 262 c.items.mu.Unlock() 263 264 if elem == nil { 265 c.metricsMu.Lock() 266 c.metrics.Misses++ 267 c.metricsMu.Unlock() 268 269 if getOpts.loader != nil { 270 return getOpts.loader.Load(c, key) 271 } 272 273 return nil 274 } 275 276 c.metricsMu.Lock() 277 c.metrics.Hits++ 278 c.metricsMu.Unlock() 279 280 return elem.Value.(*Item[K, V]) 281 } 282 283 // Delete deletes an item from the cache. If the item associated with 284 // the key is not found, the method is no-op. 285 func (c *Cache[K, V]) Delete(key K) { 286 c.items.mu.Lock() 287 defer c.items.mu.Unlock() 288 289 elem := c.items.values[key] 290 if elem == nil { 291 return 292 } 293 294 c.evict(EvictionReasonDeleted, elem) 295 } 296 297 // DeleteAll deletes all items from the cache. 298 func (c *Cache[K, V]) DeleteAll() { 299 c.items.mu.Lock() 300 c.evict(EvictionReasonDeleted) 301 c.items.mu.Unlock() 302 } 303 304 // DeleteExpired deletes all expired items from the cache. 305 func (c *Cache[K, V]) DeleteExpired() { 306 c.items.mu.Lock() 307 defer c.items.mu.Unlock() 308 309 if c.items.expQueue.isEmpty() { 310 return 311 } 312 313 e := c.items.expQueue[0] 314 for e.Value.(*Item[K, V]).isExpiredUnsafe() { 315 c.evict(EvictionReasonExpired, e) 316 317 if c.items.expQueue.isEmpty() { 318 break 319 } 320 321 // expiration queue has a new root 322 e = c.items.expQueue[0] 323 } 324 } 325 326 // Touch simulates an item's retrieval without actually returning it. 327 // Its main purpose is to extend an item's expiration timestamp. 328 // If the item is not found, the method is no-op. 329 func (c *Cache[K, V]) Touch(key K) { 330 c.items.mu.Lock() 331 c.get(key, true) 332 c.items.mu.Unlock() 333 } 334 335 // Len returns the number of items in the cache. 336 func (c *Cache[K, V]) Len() int { 337 c.items.mu.RLock() 338 defer c.items.mu.RUnlock() 339 340 return len(c.items.values) 341 } 342 343 // Keys returns all keys currently present in the cache. 344 func (c *Cache[K, V]) Keys() []K { 345 c.items.mu.RLock() 346 defer c.items.mu.RUnlock() 347 348 res := make([]K, 0, len(c.items.values)) 349 for k := range c.items.values { 350 res = append(res, k) 351 } 352 353 return res 354 } 355 356 // Items returns a copy of all items in the cache. 357 // It does not update any expiration timestamps. 358 func (c *Cache[K, V]) Items() map[K]*Item[K, V] { 359 c.items.mu.RLock() 360 defer c.items.mu.RUnlock() 361 362 items := make(map[K]*Item[K, V], len(c.items.values)) 363 for k := range c.items.values { 364 item := c.get(k, false) 365 if item != nil { 366 items[k] = item.Value.(*Item[K, V]) 367 } 368 } 369 370 return items 371 } 372 373 // Metrics returns the metrics of the cache. 374 func (c *Cache[K, V]) Metrics() Metrics { 375 c.metricsMu.RLock() 376 defer c.metricsMu.RUnlock() 377 378 return c.metrics 379 } 380 381 // Start starts an automatic cleanup process that 382 // periodically deletes expired items. 383 // It blocks until Stop is called. 384 func (c *Cache[K, V]) Start() { 385 waitDur := func() time.Duration { 386 c.items.mu.RLock() 387 defer c.items.mu.RUnlock() 388 389 if !c.items.expQueue.isEmpty() && 390 !c.items.expQueue[0].Value.(*Item[K, V]).expiresAt.IsZero() { 391 d := time.Until(c.items.expQueue[0].Value.(*Item[K, V]).expiresAt) 392 if d <= 0 { 393 // execute immediately 394 return time.Microsecond 395 } 396 397 return d 398 } 399 400 if c.options.ttl > 0 { 401 return c.options.ttl 402 } 403 404 return time.Hour 405 } 406 407 timer := time.NewTimer(waitDur()) 408 stop := func() { 409 if !timer.Stop() { 410 // drain the timer chan 411 select { 412 case <-timer.C: 413 default: 414 } 415 } 416 } 417 418 defer stop() 419 420 for { 421 select { 422 case <-c.stopCh: 423 return 424 case d := <-c.items.timerCh: 425 stop() 426 timer.Reset(d) 427 case <-timer.C: 428 c.DeleteExpired() 429 stop() 430 timer.Reset(waitDur()) 431 } 432 } 433 } 434 435 // Stop stops the automatic cleanup process. 436 // It blocks until the cleanup process exits. 437 func (c *Cache[K, V]) Stop() { 438 c.stopCh <- struct{}{} 439 } 440 441 // OnInsertion adds the provided function to be executed when 442 // a new item is inserted into the cache. The function is executed 443 // on a separate goroutine and does not block the flow of the cache 444 // manager. 445 // The returned function may be called to delete the subscription function 446 // from the list of insertion subscribers. 447 // When the returned function is called, it blocks until all instances of 448 // the same subscription function return. A context is used to notify the 449 // subscription function when the returned/deletion function is called. 450 func (c *Cache[K, V]) OnInsertion(fn func(context.Context, *Item[K, V])) func() { 451 var ( 452 wg sync.WaitGroup 453 ctx, cancel = context.WithCancel(context.Background()) 454 ) 455 456 c.events.insertion.mu.Lock() 457 id := c.events.insertion.nextID 458 c.events.insertion.fns[id] = func(item *Item[K, V]) { 459 wg.Add(1) 460 go func() { 461 fn(ctx, item) 462 wg.Done() 463 }() 464 } 465 c.events.insertion.nextID++ 466 c.events.insertion.mu.Unlock() 467 468 return func() { 469 cancel() 470 471 c.events.insertion.mu.Lock() 472 delete(c.events.insertion.fns, id) 473 c.events.insertion.mu.Unlock() 474 475 wg.Wait() 476 } 477 } 478 479 // OnEviction adds the provided function to be executed when 480 // an item is evicted/deleted from the cache. The function is executed 481 // on a separate goroutine and does not block the flow of the cache 482 // manager. 483 // The returned function may be called to delete the subscription function 484 // from the list of eviction subscribers. 485 // When the returned function is called, it blocks until all instances of 486 // the same subscription function return. A context is used to notify the 487 // subscription function when the returned/deletion function is called. 488 func (c *Cache[K, V]) OnEviction(fn func(context.Context, EvictionReason, *Item[K, V])) func() { 489 var ( 490 wg sync.WaitGroup 491 ctx, cancel = context.WithCancel(context.Background()) 492 ) 493 494 c.events.eviction.mu.Lock() 495 id := c.events.eviction.nextID 496 c.events.eviction.fns[id] = func(r EvictionReason, item *Item[K, V]) { 497 wg.Add(1) 498 go func() { 499 fn(ctx, r, item) 500 wg.Done() 501 }() 502 } 503 c.events.eviction.nextID++ 504 c.events.eviction.mu.Unlock() 505 506 return func() { 507 cancel() 508 509 c.events.eviction.mu.Lock() 510 delete(c.events.eviction.fns, id) 511 c.events.eviction.mu.Unlock() 512 513 wg.Wait() 514 } 515 } 516 517 // Loader is an interface that handles missing data loading. 518 type Loader[K comparable, V any] interface { 519 // Load should execute a custom item retrieval logic and 520 // return the item that is associated with the key. 521 // It should return nil if the item is not found/valid. 522 // The method is allowed to fetch data from the cache instance 523 // or update it for future use. 524 Load(c *Cache[K, V], key K) *Item[K, V] 525 } 526 527 // LoaderFunc type is an adapter that allows the use of ordinary 528 // functions as data loaders. 529 type LoaderFunc[K comparable, V any] func(*Cache[K, V], K) *Item[K, V] 530 531 // Load executes a custom item retrieval logic and returns the item that 532 // is associated with the key. 533 // It returns nil if the item is not found/valid. 534 func (l LoaderFunc[K, V]) Load(c *Cache[K, V], key K) *Item[K, V] { 535 return l(c, key) 536 } 537 538 // SuppressedLoader wraps another Loader and suppresses duplicate 539 // calls to its Load method. 540 type SuppressedLoader[K comparable, V any] struct { 541 Loader[K, V] 542 543 group *singleflight.Group 544 } 545 546 // Load executes a custom item retrieval logic and returns the item that 547 // is associated with the key. 548 // It returns nil if the item is not found/valid. 549 // It also ensures that only one execution of the wrapped Loader's Load 550 // method is in-flight for a given key at a time. 551 func (l *SuppressedLoader[K, V]) Load(c *Cache[K, V], key K) *Item[K, V] { 552 // there should be a better/generic way to create a 553 // singleflight Group's key. It's possible that a generic 554 // singleflight.Group will be introduced with/in go1.19+ 555 strKey := fmt.Sprint(key) 556 557 // the error can be discarded since the singleflight.Group 558 // itself does not return any of its errors, it returns 559 // the error that we return ourselves in the func below, which 560 // is also nil 561 res, _, _ := l.group.Do(strKey, func() (interface{}, error) { 562 item := l.Loader.Load(c, key) 563 if item == nil { 564 return nil, nil 565 } 566 567 return item, nil 568 }) 569 if res == nil { 570 return nil 571 } 572 573 return res.(*Item[K, V]) 574 }