github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/ttlcache/cache_test.go (about) 1 package ttlcache 2 3 import ( 4 "container/list" 5 "context" 6 "fmt" 7 "sync" 8 "testing" 9 "time" 10 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 "go.uber.org/goleak" 14 "golang.org/x/sync/singleflight" 15 ) 16 17 func TestMain(m *testing.M) { 18 goleak.VerifyTestMain(m) 19 } 20 21 func Test_New(t *testing.T) { 22 c := New[string, string]( 23 WithTTL[string, string](time.Hour), 24 WithCapacity[string, string](1), 25 ) 26 require.NotNil(t, c) 27 assert.NotNil(t, c.stopCh) 28 assert.NotNil(t, c.items.values) 29 assert.NotNil(t, c.items.lru) 30 assert.NotNil(t, c.items.expQueue) 31 assert.NotNil(t, c.items.timerCh) 32 assert.NotNil(t, c.events.insertion.fns) 33 assert.NotNil(t, c.events.eviction.fns) 34 assert.Equal(t, time.Hour, c.options.ttl) 35 assert.Equal(t, uint64(1), c.options.capacity) 36 } 37 38 func Test_Cache_updateExpirations(t *testing.T) { 39 oldExp, newExp := time.Now().Add(time.Hour), time.Now().Add(time.Minute) 40 41 cc := map[string]struct { 42 TimerChValue time.Duration 43 Fresh bool 44 EmptyQueue bool 45 OldExpiresAt time.Time 46 NewExpiresAt time.Time 47 Result time.Duration 48 }{ 49 "Update with fresh item and zero new and non zero old expiresAt fields": { 50 Fresh: true, 51 OldExpiresAt: oldExp, 52 }, 53 "Update with non fresh item and zero new and non zero old expiresAt fields": { 54 OldExpiresAt: oldExp, 55 }, 56 "Update with fresh item and matching new and old expiresAt fields": { 57 Fresh: true, 58 OldExpiresAt: oldExp, 59 NewExpiresAt: oldExp, 60 }, 61 "Update with non fresh item and matching new and old expiresAt fields": { 62 OldExpiresAt: oldExp, 63 NewExpiresAt: oldExp, 64 }, 65 "Update with non zero new expiresAt field and empty queue": { 66 Fresh: true, 67 EmptyQueue: true, 68 NewExpiresAt: newExp, 69 Result: time.Until(newExp), 70 }, 71 "Update with fresh item and non zero new and zero old expiresAt fields": { 72 Fresh: true, 73 NewExpiresAt: newExp, 74 Result: time.Until(newExp), 75 }, 76 "Update with non fresh item and non zero new and zero old expiresAt fields": { 77 NewExpiresAt: newExp, 78 Result: time.Until(newExp), 79 }, 80 "Update with fresh item and non zero new and old expiresAt fields": { 81 Fresh: true, 82 OldExpiresAt: oldExp, 83 NewExpiresAt: newExp, 84 Result: time.Until(newExp), 85 }, 86 "Update with non fresh item and non zero new and old expiresAt fields": { 87 OldExpiresAt: oldExp, 88 NewExpiresAt: newExp, 89 Result: time.Until(newExp), 90 }, 91 "Update with full timerCh (lesser value), fresh item and non zero new and old expiresAt fields": { 92 TimerChValue: time.Second, 93 Fresh: true, 94 OldExpiresAt: oldExp, 95 NewExpiresAt: newExp, 96 Result: time.Second, 97 }, 98 "Update with full timerCh (lesser value), non fresh item and non zero new and old expiresAt fields": { 99 TimerChValue: time.Second, 100 OldExpiresAt: oldExp, 101 NewExpiresAt: newExp, 102 Result: time.Second, 103 }, 104 "Update with full timerCh (greater value), fresh item and non zero new and old expiresAt fields": { 105 TimerChValue: time.Hour, 106 Fresh: true, 107 OldExpiresAt: oldExp, 108 NewExpiresAt: newExp, 109 Result: time.Until(newExp), 110 }, 111 "Update with full timerCh (greater value), non fresh item and non zero new and old expiresAt fields": { 112 TimerChValue: time.Hour, 113 OldExpiresAt: oldExp, 114 NewExpiresAt: newExp, 115 Result: time.Until(newExp), 116 }, 117 } 118 119 for cn, c := range cc { 120 c := c 121 122 t.Run(cn, func(t *testing.T) { 123 t.Parallel() 124 125 cache := prepCache(time.Hour) 126 127 if c.TimerChValue > 0 { 128 cache.items.timerCh <- c.TimerChValue 129 } 130 131 elem := &list.Element{ 132 Value: &Item[string, string]{ 133 expiresAt: c.NewExpiresAt, 134 }, 135 } 136 137 if !c.EmptyQueue { 138 cache.items.expQueue.push(&list.Element{ 139 Value: &Item[string, string]{ 140 expiresAt: c.OldExpiresAt, 141 }, 142 }) 143 144 if !c.Fresh { 145 elem = &list.Element{ 146 Value: &Item[string, string]{ 147 expiresAt: c.OldExpiresAt, 148 }, 149 } 150 cache.items.expQueue.push(elem) 151 152 elem.Value.(*Item[string, string]).expiresAt = c.NewExpiresAt 153 } 154 } 155 156 cache.updateExpirations(c.Fresh, elem) 157 158 var res time.Duration 159 160 select { 161 case res = <-cache.items.timerCh: 162 default: 163 } 164 165 assert.InDelta(t, c.Result, res, float64(time.Second)) 166 }) 167 } 168 } 169 170 func Test_Cache_set(t *testing.T) { 171 const newKey, existingKey, evictedKey = "newKey123", "existingKey", "evicted" 172 173 cc := map[string]struct { 174 Capacity uint64 175 Key string 176 TTL time.Duration 177 Metrics Metrics 178 ExpectFns bool 179 }{ 180 "Set with existing key and custom TTL": { 181 Key: existingKey, 182 TTL: time.Minute, 183 }, 184 "Set with existing key and NoTTL": { 185 Key: existingKey, 186 TTL: NoTTL, 187 }, 188 "Set with existing key and DefaultTTL": { 189 Key: existingKey, 190 TTL: DefaultTTL, 191 }, 192 "Set with new key and eviction caused by small capacity": { 193 Capacity: 3, 194 Key: newKey, 195 TTL: DefaultTTL, 196 Metrics: Metrics{ 197 Insertions: 1, 198 Evictions: 1, 199 }, 200 ExpectFns: true, 201 }, 202 "Set with new key and no eviction caused by large capacity": { 203 Capacity: 10, 204 Key: newKey, 205 TTL: DefaultTTL, 206 Metrics: Metrics{ 207 Insertions: 1, 208 }, 209 ExpectFns: true, 210 }, 211 "Set with new key and custom TTL": { 212 Key: newKey, 213 TTL: time.Minute, 214 Metrics: Metrics{ 215 Insertions: 1, 216 }, 217 ExpectFns: true, 218 }, 219 "Set with new key and NoTTL": { 220 Key: newKey, 221 TTL: NoTTL, 222 Metrics: Metrics{ 223 Insertions: 1, 224 }, 225 ExpectFns: true, 226 }, 227 "Set with new key and DefaultTTL": { 228 Key: newKey, 229 TTL: DefaultTTL, 230 Metrics: Metrics{ 231 Insertions: 1, 232 }, 233 ExpectFns: true, 234 }, 235 } 236 237 for cn, c := range cc { 238 c := c 239 240 t.Run(cn, func(t *testing.T) { 241 t.Parallel() 242 243 var ( 244 insertFnsCalls int 245 evictionFnsCalls int 246 ) 247 248 cache := prepCache(time.Hour, evictedKey, existingKey, "test3") 249 cache.options.capacity = c.Capacity 250 cache.options.ttl = time.Minute * 20 251 cache.events.insertion.fns[1] = func(item *Item[string, string]) { 252 assert.Equal(t, newKey, item.key) 253 insertFnsCalls++ 254 } 255 cache.events.insertion.fns[2] = cache.events.insertion.fns[1] 256 cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { 257 assert.Equal(t, EvictionReasonCapacityReached, r) 258 assert.Equal(t, evictedKey, item.key) 259 evictionFnsCalls++ 260 } 261 cache.events.eviction.fns[2] = cache.events.eviction.fns[1] 262 263 total := 3 264 if c.Key == newKey && (c.Capacity == 0 || c.Capacity >= 4) { 265 total++ 266 } 267 268 item := cache.set(c.Key, "value123", c.TTL) 269 270 if c.ExpectFns { 271 assert.Equal(t, 2, insertFnsCalls) 272 273 if c.Capacity > 0 && c.Capacity < 4 { 274 assert.Equal(t, 2, evictionFnsCalls) 275 } 276 } 277 278 assert.Same(t, cache.items.values[c.Key].Value.(*Item[string, string]), item) 279 assert.Len(t, cache.items.values, total) 280 assert.Equal(t, c.Key, item.key) 281 assert.Equal(t, "value123", item.value) 282 assert.Equal(t, c.Key, cache.items.lru.Front().Value.(*Item[string, string]).key) 283 assert.Equal(t, c.Metrics, cache.metrics) 284 285 if c.Capacity > 0 && c.Capacity < 4 { 286 assert.NotEqual(t, evictedKey, cache.items.lru.Back().Value.(*Item[string, string]).key) 287 } 288 289 switch { 290 case c.TTL == DefaultTTL: 291 assert.Equal(t, cache.options.ttl, item.ttl) 292 assert.WithinDuration(t, time.Now(), item.expiresAt, cache.options.ttl) 293 assert.Equal(t, c.Key, cache.items.expQueue[0].Value.(*Item[string, string]).key) 294 case c.TTL > DefaultTTL: 295 assert.Equal(t, c.TTL, item.ttl) 296 assert.WithinDuration(t, time.Now(), item.expiresAt, c.TTL) 297 assert.Equal(t, c.Key, cache.items.expQueue[0].Value.(*Item[string, string]).key) 298 default: 299 assert.Equal(t, c.TTL, item.ttl) 300 assert.Zero(t, item.expiresAt) 301 assert.NotEqual(t, c.Key, cache.items.expQueue[0].Value.(*Item[string, string]).key) 302 } 303 }) 304 } 305 } 306 307 func Test_Cache_get(t *testing.T) { 308 const existingKey, notFoundKey, expiredKey = "existing", "notfound", "expired" 309 310 cc := map[string]struct { 311 Key string 312 Touch bool 313 WithTTL bool 314 }{ 315 "Retrieval of non-existent item": { 316 Key: notFoundKey, 317 }, 318 "Retrieval of expired item": { 319 Key: expiredKey, 320 }, 321 "Retrieval of existing item without update": { 322 Key: existingKey, 323 }, 324 "Retrieval of existing item with touch and non zero TTL": { 325 Key: existingKey, 326 Touch: true, 327 WithTTL: true, 328 }, 329 "Retrieval of existing item with touch and zero TTL": { 330 Key: existingKey, 331 Touch: true, 332 }, 333 } 334 335 for cn, c := range cc { 336 c := c 337 338 t.Run(cn, func(t *testing.T) { 339 t.Parallel() 340 341 cache := prepCache(time.Hour, existingKey, "test2", "test3") 342 addToCache(cache, time.Nanosecond, expiredKey) 343 time.Sleep(time.Millisecond) // force expiration 344 345 oldItem := cache.items.values[existingKey].Value.(*Item[string, string]) 346 oldQueueIndex := oldItem.queueIndex 347 oldExpiresAt := oldItem.expiresAt 348 349 if c.WithTTL { 350 oldItem.ttl = time.Hour * 30 351 } else { 352 oldItem.ttl = 0 353 } 354 355 elem := cache.get(c.Key, c.Touch) 356 357 if c.Key == notFoundKey { 358 assert.Nil(t, elem) 359 return 360 } 361 362 if c.Key == expiredKey { 363 assert.True(t, time.Now().After(cache.items.values[expiredKey].Value.(*Item[string, string]).expiresAt)) 364 assert.Nil(t, elem) 365 return 366 } 367 368 require.NotNil(t, elem) 369 item := elem.Value.(*Item[string, string]) 370 371 if c.Touch && c.WithTTL { 372 assert.True(t, item.expiresAt.After(oldExpiresAt)) 373 assert.NotEqual(t, oldQueueIndex, item.queueIndex) 374 } else { 375 assert.True(t, item.expiresAt.Equal(oldExpiresAt)) 376 assert.Equal(t, oldQueueIndex, item.queueIndex) 377 } 378 379 assert.Equal(t, c.Key, cache.items.lru.Front().Value.(*Item[string, string]).key) 380 }) 381 } 382 } 383 384 func Test_Cache_evict(t *testing.T) { 385 var ( 386 key1FnsCalls int 387 key2FnsCalls int 388 key3FnsCalls int 389 key4FnsCalls int 390 ) 391 392 cache := prepCache(time.Hour, "1", "2", "3", "4") 393 cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { 394 assert.Equal(t, EvictionReasonDeleted, r) 395 switch item.key { 396 case "1": 397 key1FnsCalls++ 398 case "2": 399 key2FnsCalls++ 400 case "3": 401 key3FnsCalls++ 402 case "4": 403 key4FnsCalls++ 404 } 405 } 406 cache.events.eviction.fns[2] = cache.events.eviction.fns[1] 407 408 // delete only specified 409 cache.evict(EvictionReasonDeleted, cache.items.lru.Back(), cache.items.lru.Back().Prev()) 410 411 assert.Equal(t, 2, key1FnsCalls) 412 assert.Equal(t, 2, key2FnsCalls) 413 assert.Zero(t, key3FnsCalls) 414 assert.Zero(t, key4FnsCalls) 415 assert.Len(t, cache.items.values, 2) 416 assert.NotContains(t, cache.items.values, "1") 417 assert.NotContains(t, cache.items.values, "2") 418 assert.Equal(t, uint64(2), cache.metrics.Evictions) 419 420 // delete all 421 key1FnsCalls, key2FnsCalls = 0, 0 422 cache.metrics.Evictions = 0 423 424 cache.evict(EvictionReasonDeleted) 425 426 assert.Zero(t, key1FnsCalls) 427 assert.Zero(t, key2FnsCalls) 428 assert.Equal(t, 2, key3FnsCalls) 429 assert.Equal(t, 2, key4FnsCalls) 430 assert.Empty(t, cache.items.values) 431 assert.NotContains(t, cache.items.values, "3") 432 assert.NotContains(t, cache.items.values, "4") 433 assert.Equal(t, uint64(2), cache.metrics.Evictions) 434 } 435 436 func Test_Cache_Set(t *testing.T) { 437 cache := prepCache(time.Hour, "test1", "test2", "test3") 438 item := cache.Set("hello", "value123", time.Minute) 439 require.NotNil(t, item) 440 assert.Same(t, item, cache.items.values["hello"].Value) 441 442 item = cache.Set("test1", "value123", time.Minute) 443 require.NotNil(t, item) 444 assert.Same(t, item, cache.items.values["test1"].Value) 445 } 446 447 func Test_Cache_Get(t *testing.T) { 448 const notFoundKey, foundKey = "notfound", "test1" 449 cc := map[string]struct { 450 Key string 451 DefaultOptions options[string, string] 452 CallOptions []Option[string, string] 453 Metrics Metrics 454 Result *Item[string, string] 455 }{ 456 "Get without loader when item is not found": { 457 Key: notFoundKey, 458 Metrics: Metrics{ 459 Misses: 1, 460 }, 461 }, 462 "Get with default loader that returns non nil value when item is not found": { 463 Key: notFoundKey, 464 DefaultOptions: options[string, string]{ 465 loader: LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] { 466 return &Item[string, string]{key: "test"} 467 }), 468 }, 469 Metrics: Metrics{ 470 Misses: 1, 471 }, 472 Result: &Item[string, string]{key: "test"}, 473 }, 474 "Get with default loader that returns nil value when item is not found": { 475 Key: notFoundKey, 476 DefaultOptions: options[string, string]{ 477 loader: LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] { 478 return nil 479 }), 480 }, 481 Metrics: Metrics{ 482 Misses: 1, 483 }, 484 }, 485 "Get with call loader that returns non nil value when item is not found": { 486 Key: notFoundKey, 487 DefaultOptions: options[string, string]{ 488 loader: LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] { 489 return &Item[string, string]{key: "test"} 490 }), 491 }, 492 CallOptions: []Option[string, string]{ 493 WithLoader[string, string](LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] { 494 return &Item[string, string]{key: "hello"} 495 })), 496 }, 497 Metrics: Metrics{ 498 Misses: 1, 499 }, 500 Result: &Item[string, string]{key: "hello"}, 501 }, 502 "Get with call loader that returns nil value when item is not found": { 503 Key: notFoundKey, 504 DefaultOptions: options[string, string]{ 505 loader: LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] { 506 return &Item[string, string]{key: "test"} 507 }), 508 }, 509 CallOptions: []Option[string, string]{ 510 WithLoader[string, string](LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] { 511 return nil 512 })), 513 }, 514 Metrics: Metrics{ 515 Misses: 1, 516 }, 517 }, 518 "Get when TTL extension is disabled by default and item is found": { 519 Key: foundKey, 520 DefaultOptions: options[string, string]{ 521 disableTouchOnHit: true, 522 }, 523 Metrics: Metrics{ 524 Hits: 1, 525 }, 526 }, 527 "Get when TTL extension is disabled and item is found": { 528 Key: foundKey, 529 CallOptions: []Option[string, string]{ 530 WithDisableTouchOnHit[string, string](), 531 }, 532 Metrics: Metrics{ 533 Hits: 1, 534 }, 535 }, 536 "Get when item is found": { 537 Key: foundKey, 538 Metrics: Metrics{ 539 Hits: 1, 540 }, 541 }, 542 } 543 544 for cn, c := range cc { 545 c := c 546 547 t.Run(cn, func(t *testing.T) { 548 t.Parallel() 549 550 cache := prepCache(time.Minute, foundKey, "test2", "test3") 551 oldExpiresAt := cache.items.values[foundKey].Value.(*Item[string, string]).expiresAt 552 cache.options = c.DefaultOptions 553 554 res := cache.Get(c.Key, c.CallOptions...) 555 556 if c.Key == foundKey { 557 c.Result = cache.items.values[foundKey].Value.(*Item[string, string]) 558 assert.Equal(t, foundKey, cache.items.lru.Front().Value.(*Item[string, string]).key) 559 } 560 561 assert.Equal(t, c.Metrics, cache.metrics) 562 563 if !assert.Equal(t, c.Result, res) || res == nil || res.ttl == 0 { 564 return 565 } 566 567 applyOptions(&c.DefaultOptions, c.CallOptions...) 568 569 if c.DefaultOptions.disableTouchOnHit { 570 assert.Equal(t, oldExpiresAt, res.expiresAt) 571 return 572 } 573 574 assert.True(t, oldExpiresAt.Before(res.expiresAt)) 575 assert.WithinDuration(t, time.Now(), res.expiresAt, res.ttl) 576 }) 577 } 578 } 579 580 func Test_Cache_Delete(t *testing.T) { 581 var fnsCalls int 582 583 cache := prepCache(time.Hour, "1", "2", "3", "4") 584 cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { 585 assert.Equal(t, EvictionReasonDeleted, r) 586 fnsCalls++ 587 } 588 cache.events.eviction.fns[2] = cache.events.eviction.fns[1] 589 590 // not found 591 cache.Delete("1234") 592 assert.Zero(t, fnsCalls) 593 assert.Len(t, cache.items.values, 4) 594 595 // success 596 cache.Delete("1") 597 assert.Equal(t, 2, fnsCalls) 598 assert.Len(t, cache.items.values, 3) 599 assert.NotContains(t, cache.items.values, "1") 600 } 601 602 func Test_Cache_DeleteAll(t *testing.T) { 603 var ( 604 key1FnsCalls int 605 key2FnsCalls int 606 key3FnsCalls int 607 key4FnsCalls int 608 ) 609 610 cache := prepCache(time.Hour, "1", "2", "3", "4") 611 cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { 612 assert.Equal(t, EvictionReasonDeleted, r) 613 switch item.key { 614 case "1": 615 key1FnsCalls++ 616 case "2": 617 key2FnsCalls++ 618 case "3": 619 key3FnsCalls++ 620 case "4": 621 key4FnsCalls++ 622 } 623 } 624 cache.events.eviction.fns[2] = cache.events.eviction.fns[1] 625 626 cache.DeleteAll() 627 assert.Empty(t, cache.items.values) 628 assert.Equal(t, 2, key1FnsCalls) 629 assert.Equal(t, 2, key2FnsCalls) 630 assert.Equal(t, 2, key3FnsCalls) 631 assert.Equal(t, 2, key4FnsCalls) 632 } 633 634 func Test_Cache_DeleteExpired(t *testing.T) { 635 var ( 636 key1FnsCalls int 637 key2FnsCalls int 638 ) 639 640 cache := prepCache(time.Hour) 641 cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) { 642 assert.Equal(t, EvictionReasonExpired, r) 643 switch item.key { 644 case "5": 645 key1FnsCalls++ 646 case "6": 647 key2FnsCalls++ 648 } 649 } 650 cache.events.eviction.fns[2] = cache.events.eviction.fns[1] 651 652 // one item 653 addToCache(cache, time.Nanosecond, "5") 654 655 cache.DeleteExpired() 656 assert.Empty(t, cache.items.values) 657 assert.NotContains(t, cache.items.values, "5") 658 assert.Equal(t, 2, key1FnsCalls) 659 660 key1FnsCalls = 0 661 662 // empty 663 cache.DeleteExpired() 664 assert.Empty(t, cache.items.values) 665 666 // non empty 667 addToCache(cache, time.Hour, "1", "2", "3", "4") 668 addToCache(cache, time.Nanosecond, "5") 669 addToCache(cache, time.Nanosecond, "6") // we need multiple calls to avoid adding time.Minute to ttl 670 time.Sleep(time.Millisecond) // force expiration 671 672 cache.DeleteExpired() 673 assert.Len(t, cache.items.values, 4) 674 assert.NotContains(t, cache.items.values, "5") 675 assert.NotContains(t, cache.items.values, "6") 676 assert.Equal(t, 2, key1FnsCalls) 677 assert.Equal(t, 2, key2FnsCalls) 678 } 679 680 func Test_Cache_Touch(t *testing.T) { 681 cache := prepCache(time.Hour, "1", "2") 682 oldExpiresAt := cache.items.values["1"].Value.(*Item[string, string]).expiresAt 683 684 cache.Touch("1") 685 686 newExpiresAt := cache.items.values["1"].Value.(*Item[string, string]).expiresAt 687 assert.True(t, newExpiresAt.After(oldExpiresAt)) 688 assert.Equal(t, "1", cache.items.lru.Front().Value.(*Item[string, string]).key) 689 } 690 691 func Test_Cache_Len(t *testing.T) { 692 cache := prepCache(time.Hour, "1", "2") 693 assert.Equal(t, 2, cache.Len()) 694 } 695 696 func Test_Cache_Keys(t *testing.T) { 697 cache := prepCache(time.Hour, "1", "2", "3") 698 assert.ElementsMatch(t, []string{"1", "2", "3"}, cache.Keys()) 699 } 700 701 func Test_Cache_Items(t *testing.T) { 702 cache := prepCache(time.Hour, "1", "2", "3") 703 items := cache.Items() 704 require.Len(t, items, 3) 705 706 require.Contains(t, items, "1") 707 assert.Equal(t, "1", items["1"].key) 708 require.Contains(t, items, "2") 709 assert.Equal(t, "2", items["2"].key) 710 require.Contains(t, items, "3") 711 assert.Equal(t, "3", items["3"].key) 712 } 713 714 func Test_Cache_Metrics(t *testing.T) { 715 cache := Cache[string, string]{ 716 metrics: Metrics{Evictions: 10}, 717 } 718 719 assert.Equal(t, Metrics{Evictions: 10}, cache.Metrics()) 720 } 721 722 func Test_Cache_Start(t *testing.T) { 723 cache := prepCache(0) 724 cache.stopCh = make(chan struct{}) 725 726 addToCache(cache, time.Nanosecond, "1") 727 time.Sleep(time.Millisecond) // force expiration 728 729 fn := func(r EvictionReason, _ *Item[string, string]) { 730 go func() { 731 assert.Equal(t, EvictionReasonExpired, r) 732 733 cache.metricsMu.RLock() 734 v := cache.metrics.Evictions 735 cache.metricsMu.RUnlock() 736 737 switch v { 738 case 1: 739 cache.items.mu.Lock() 740 addToCache(cache, time.Nanosecond, "2") 741 cache.items.mu.Unlock() 742 cache.options.ttl = time.Hour 743 cache.items.timerCh <- time.Millisecond 744 case 2: 745 cache.items.mu.Lock() 746 addToCache(cache, time.Second, "3") 747 addToCache(cache, NoTTL, "4") 748 cache.items.mu.Unlock() 749 cache.items.timerCh <- time.Millisecond 750 default: 751 close(cache.stopCh) 752 } 753 }() 754 } 755 cache.events.eviction.fns[1] = fn 756 757 cache.Start() 758 } 759 760 func Test_Cache_Stop(t *testing.T) { 761 cache := Cache[string, string]{ 762 stopCh: make(chan struct{}, 1), 763 } 764 cache.Stop() 765 assert.Len(t, cache.stopCh, 1) 766 } 767 768 func Test_Cache_OnInsertion(t *testing.T) { 769 checkCh := make(chan struct{}) 770 resCh := make(chan struct{}) 771 cache := prepCache(time.Hour) 772 del1 := cache.OnInsertion(func(_ context.Context, _ *Item[string, string]) { 773 checkCh <- struct{}{} 774 }) 775 del2 := cache.OnInsertion(func(_ context.Context, _ *Item[string, string]) { 776 checkCh <- struct{}{} 777 }) 778 779 require.Len(t, cache.events.insertion.fns, 2) 780 assert.Equal(t, uint64(2), cache.events.insertion.nextID) 781 782 cache.events.insertion.fns[0](nil) 783 784 go func() { 785 del1() 786 resCh <- struct{}{} 787 }() 788 assert.Never(t, func() bool { 789 select { 790 case <-resCh: 791 return true 792 default: 793 return false 794 } 795 }, time.Millisecond*200, time.Millisecond*100) 796 assert.Eventually(t, func() bool { 797 select { 798 case <-checkCh: 799 return true 800 default: 801 return false 802 } 803 }, time.Millisecond*500, time.Millisecond*250) 804 assert.Eventually(t, func() bool { 805 select { 806 case <-resCh: 807 return true 808 default: 809 return false 810 } 811 }, time.Millisecond*500, time.Millisecond*250) 812 813 require.Len(t, cache.events.insertion.fns, 1) 814 assert.NotContains(t, cache.events.insertion.fns, uint64(0)) 815 assert.Contains(t, cache.events.insertion.fns, uint64(1)) 816 817 cache.events.insertion.fns[1](nil) 818 819 go func() { 820 del2() 821 resCh <- struct{}{} 822 }() 823 assert.Never(t, func() bool { 824 select { 825 case <-resCh: 826 return true 827 default: 828 return false 829 } 830 }, time.Millisecond*200, time.Millisecond*100) 831 assert.Eventually(t, func() bool { 832 select { 833 case <-checkCh: 834 return true 835 default: 836 return false 837 } 838 }, time.Millisecond*500, time.Millisecond*250) 839 assert.Eventually(t, func() bool { 840 select { 841 case <-resCh: 842 return true 843 default: 844 return false 845 } 846 }, time.Millisecond*500, time.Millisecond*250) 847 848 assert.Empty(t, cache.events.insertion.fns) 849 assert.NotContains(t, cache.events.insertion.fns, uint64(1)) 850 } 851 852 func Test_Cache_OnEviction(t *testing.T) { 853 checkCh := make(chan struct{}) 854 resCh := make(chan struct{}) 855 cache := prepCache(time.Hour) 856 del1 := cache.OnEviction(func(_ context.Context, _ EvictionReason, _ *Item[string, string]) { 857 checkCh <- struct{}{} 858 }) 859 del2 := cache.OnEviction(func(_ context.Context, _ EvictionReason, _ *Item[string, string]) { 860 checkCh <- struct{}{} 861 }) 862 863 require.Len(t, cache.events.eviction.fns, 2) 864 assert.Equal(t, uint64(2), cache.events.eviction.nextID) 865 866 cache.events.eviction.fns[0](0, nil) 867 868 go func() { 869 del1() 870 resCh <- struct{}{} 871 }() 872 assert.Never(t, func() bool { 873 select { 874 case <-resCh: 875 return true 876 default: 877 return false 878 } 879 }, time.Millisecond*200, time.Millisecond*100) 880 assert.Eventually(t, func() bool { 881 select { 882 case <-checkCh: 883 return true 884 default: 885 return false 886 } 887 }, time.Millisecond*500, time.Millisecond*250) 888 assert.Eventually(t, func() bool { 889 select { 890 case <-resCh: 891 return true 892 default: 893 return false 894 } 895 }, time.Millisecond*500, time.Millisecond*250) 896 897 require.Len(t, cache.events.eviction.fns, 1) 898 assert.NotContains(t, cache.events.eviction.fns, uint64(0)) 899 assert.Contains(t, cache.events.eviction.fns, uint64(1)) 900 901 cache.events.eviction.fns[1](0, nil) 902 903 go func() { 904 del2() 905 resCh <- struct{}{} 906 }() 907 assert.Never(t, func() bool { 908 select { 909 case <-resCh: 910 return true 911 default: 912 return false 913 } 914 }, time.Millisecond*200, time.Millisecond*100) 915 assert.Eventually(t, func() bool { 916 select { 917 case <-checkCh: 918 return true 919 default: 920 return false 921 } 922 }, time.Millisecond*500, time.Millisecond*250) 923 assert.Eventually(t, func() bool { 924 select { 925 case <-resCh: 926 return true 927 default: 928 return false 929 } 930 }, time.Millisecond*500, time.Millisecond*250) 931 932 assert.Empty(t, cache.events.eviction.fns) 933 assert.NotContains(t, cache.events.eviction.fns, uint64(1)) 934 } 935 936 func Test_LoaderFunc_Load(t *testing.T) { 937 var called bool 938 939 fn := LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] { 940 called = true 941 return nil 942 }) 943 944 assert.Nil(t, fn(nil, "")) 945 assert.True(t, called) 946 } 947 948 func Test_SuppressedLoader_Load(t *testing.T) { 949 var ( 950 mu sync.Mutex 951 loadCalls int 952 releaseCh = make(chan struct{}) 953 res *Item[string, string] 954 ) 955 956 l := SuppressedLoader[string, string]{ 957 Loader: LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] { 958 mu.Lock() 959 loadCalls++ 960 mu.Unlock() 961 962 <-releaseCh 963 964 if res == nil { 965 return nil 966 } 967 968 res1 := *res 969 970 return &res1 971 }), 972 group: &singleflight.Group{}, 973 } 974 975 var ( 976 wg sync.WaitGroup 977 item1, item2 *Item[string, string] 978 ) 979 980 cache := prepCache(time.Hour) 981 982 // nil result 983 wg.Add(2) 984 985 go func() { 986 item1 = l.Load(cache, "test") 987 wg.Done() 988 }() 989 990 go func() { 991 item2 = l.Load(cache, "test") 992 wg.Done() 993 }() 994 995 time.Sleep(time.Millisecond * 100) // wait for goroutines to halt 996 releaseCh <- struct{}{} 997 998 wg.Wait() 999 require.Nil(t, item1) 1000 require.Nil(t, item2) 1001 assert.Equal(t, 1, loadCalls) 1002 1003 // non nil result 1004 res = &Item[string, string]{key: "test"} 1005 loadCalls = 0 1006 wg.Add(2) 1007 1008 go func() { 1009 item1 = l.Load(cache, "test") 1010 wg.Done() 1011 }() 1012 1013 go func() { 1014 item2 = l.Load(cache, "test") 1015 wg.Done() 1016 }() 1017 1018 time.Sleep(time.Millisecond * 100) // wait for goroutines to halt 1019 releaseCh <- struct{}{} 1020 1021 wg.Wait() 1022 require.Same(t, item1, item2) 1023 assert.Equal(t, "test", item1.key) 1024 assert.Equal(t, 1, loadCalls) 1025 } 1026 1027 func prepCache(ttl time.Duration, keys ...string) *Cache[string, string] { 1028 c := &Cache[string, string]{} 1029 c.options.ttl = ttl 1030 c.items.values = make(map[string]*list.Element) 1031 c.items.lru = list.New() 1032 c.items.expQueue = newExpirationQueue[string, string]() 1033 c.items.timerCh = make(chan time.Duration, 1) 1034 c.events.eviction.fns = make(map[uint64]func(EvictionReason, *Item[string, string])) 1035 c.events.insertion.fns = make(map[uint64]func(*Item[string, string])) 1036 1037 addToCache(c, ttl, keys...) 1038 1039 return c 1040 } 1041 1042 func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) { 1043 for i, key := range keys { 1044 item := newItem( 1045 key, 1046 fmt.Sprint("value of", key), 1047 ttl+time.Duration(i)*time.Minute, 1048 ) 1049 elem := c.items.lru.PushFront(item) 1050 c.items.values[key] = elem 1051 c.items.expQueue.push(elem) 1052 } 1053 }