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 existing key and PreviousOrDefaultTTL": {
193 Key: existingKey,
194 TTL: PreviousOrDefaultTTL,
195 },
196 "Set with new key and eviction caused by small capacity": {
197 Capacity: 3,
198 Key: newKey,
199 TTL: DefaultTTL,
200 Metrics: Metrics{
201 Insertions: 1,
202 Evictions: 1,
203 },
204 ExpectFns: true,
205 },
206 "Set with new key and no eviction caused by large capacity": {
207 Capacity: 10,
208 Key: newKey,
209 TTL: DefaultTTL,
210 Metrics: Metrics{
211 Insertions: 1,
212 },
213 ExpectFns: true,
214 },
215 "Set with new key and custom TTL": {
216 Key: newKey,
217 TTL: time.Minute,
218 Metrics: Metrics{
219 Insertions: 1,
220 },
221 ExpectFns: true,
222 },
223 "Set with new key and NoTTL": {
224 Key: newKey,
225 TTL: NoTTL,
226 Metrics: Metrics{
227 Insertions: 1,
228 },
229 ExpectFns: true,
230 },
231 "Set with new key and DefaultTTL": {
232 Key: newKey,
233 TTL: DefaultTTL,
234 Metrics: Metrics{
235 Insertions: 1,
236 },
237 ExpectFns: true,
238 },
239 "Set with new key and PreviousOrDefaultTTL": {
240 Key: newKey,
241 TTL: PreviousOrDefaultTTL,
242 Metrics: Metrics{
243 Insertions: 1,
244 },
245 ExpectFns: true,
246 },
247 }
248
249 for cn, c := range cc {
250 c := c
251
252 t.Run(cn, func(t *testing.T) {
253 t.Parallel()
254
255 var (
256 insertFnsCalls int
257 evictionFnsCalls int
258 )
259
260
261 existingKeyTTL := time.Hour + time.Minute
262
263 cache := prepCache(time.Hour, evictedKey, existingKey, "test3")
264 cache.options.capacity = c.Capacity
265 cache.options.ttl = time.Minute * 20
266 cache.events.insertion.fns[1] = func(item *Item[string, string]) {
267 assert.Equal(t, newKey, item.key)
268 insertFnsCalls++
269 }
270 cache.events.insertion.fns[2] = cache.events.insertion.fns[1]
271 cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) {
272 assert.Equal(t, EvictionReasonCapacityReached, r)
273 assert.Equal(t, evictedKey, item.key)
274 evictionFnsCalls++
275 }
276 cache.events.eviction.fns[2] = cache.events.eviction.fns[1]
277
278 total := 3
279 if c.Key == newKey && (c.Capacity == 0 || c.Capacity >= 4) {
280 total++
281 }
282
283 item := cache.set(c.Key, "value123", c.TTL)
284
285 if c.ExpectFns {
286 assert.Equal(t, 2, insertFnsCalls)
287
288 if c.Capacity > 0 && c.Capacity < 4 {
289 assert.Equal(t, 2, evictionFnsCalls)
290 }
291 }
292
293 assert.Same(t, cache.items.values[c.Key].Value.(*Item[string, string]), item)
294 assert.Len(t, cache.items.values, total)
295 assert.Equal(t, c.Key, item.key)
296 assert.Equal(t, "value123", item.value)
297 assert.Equal(t, c.Key, cache.items.lru.Front().Value.(*Item[string, string]).key)
298 assert.Equal(t, c.Metrics, cache.metrics)
299
300 if c.Capacity > 0 && c.Capacity < 4 {
301 assert.NotEqual(t, evictedKey, cache.items.lru.Back().Value.(*Item[string, string]).key)
302 }
303
304 switch {
305 case c.TTL == DefaultTTL:
306 assert.Equal(t, cache.options.ttl, item.ttl)
307 assert.WithinDuration(t, time.Now(), item.expiresAt, cache.options.ttl)
308 assert.Equal(t, c.Key, cache.items.expQueue[0].Value.(*Item[string, string]).key)
309 case c.TTL > DefaultTTL:
310 assert.Equal(t, c.TTL, item.ttl)
311 assert.WithinDuration(t, time.Now(), item.expiresAt, c.TTL)
312 assert.Equal(t, c.Key, cache.items.expQueue[0].Value.(*Item[string, string]).key)
313 case c.TTL == PreviousOrDefaultTTL:
314 expectedTTL := cache.options.ttl
315 if c.Key == existingKey {
316 expectedTTL = existingKeyTTL
317 }
318 assert.Equal(t, expectedTTL, item.ttl)
319 assert.WithinDuration(t, time.Now(), item.expiresAt, expectedTTL)
320 default:
321 assert.Equal(t, c.TTL, item.ttl)
322 assert.Zero(t, item.expiresAt)
323 assert.NotEqual(t, c.Key, cache.items.expQueue[0].Value.(*Item[string, string]).key)
324 }
325 })
326 }
327 }
328
329 func Test_Cache_get(t *testing.T) {
330 const existingKey, notFoundKey, expiredKey = "existing", "notfound", "expired"
331
332 cc := map[string]struct {
333 Key string
334 Touch bool
335 WithTTL bool
336 }{
337 "Retrieval of non-existent item": {
338 Key: notFoundKey,
339 },
340 "Retrieval of expired item": {
341 Key: expiredKey,
342 },
343 "Retrieval of existing item without update": {
344 Key: existingKey,
345 },
346 "Retrieval of existing item with touch and non zero TTL": {
347 Key: existingKey,
348 Touch: true,
349 WithTTL: true,
350 },
351 "Retrieval of existing item with touch and zero TTL": {
352 Key: existingKey,
353 Touch: true,
354 },
355 }
356
357 for cn, c := range cc {
358 c := c
359
360 t.Run(cn, func(t *testing.T) {
361 t.Parallel()
362
363 cache := prepCache(time.Hour, existingKey, "test2", "test3")
364 addToCache(cache, time.Nanosecond, expiredKey)
365 time.Sleep(time.Millisecond)
366
367 oldItem := cache.items.values[existingKey].Value.(*Item[string, string])
368 oldQueueIndex := oldItem.queueIndex
369 oldExpiresAt := oldItem.expiresAt
370
371 if c.WithTTL {
372 oldItem.ttl = time.Hour * 30
373 } else {
374 oldItem.ttl = 0
375 }
376
377 elem := cache.get(c.Key, c.Touch)
378
379 if c.Key == notFoundKey {
380 assert.Nil(t, elem)
381 return
382 }
383
384 if c.Key == expiredKey {
385 assert.True(t, time.Now().After(cache.items.values[expiredKey].Value.(*Item[string, string]).expiresAt))
386 assert.Nil(t, elem)
387 return
388 }
389
390 require.NotNil(t, elem)
391 item := elem.Value.(*Item[string, string])
392
393 if c.Touch && c.WithTTL {
394 assert.True(t, item.expiresAt.After(oldExpiresAt))
395 assert.NotEqual(t, oldQueueIndex, item.queueIndex)
396 } else {
397 assert.True(t, item.expiresAt.Equal(oldExpiresAt))
398 assert.Equal(t, oldQueueIndex, item.queueIndex)
399 }
400
401 assert.Equal(t, c.Key, cache.items.lru.Front().Value.(*Item[string, string]).key)
402 })
403 }
404 }
405
406 func Test_Cache_evict(t *testing.T) {
407 var (
408 key1FnsCalls int
409 key2FnsCalls int
410 key3FnsCalls int
411 key4FnsCalls int
412 )
413
414 cache := prepCache(time.Hour, "1", "2", "3", "4")
415 cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) {
416 assert.Equal(t, EvictionReasonDeleted, r)
417 switch item.key {
418 case "1":
419 key1FnsCalls++
420 case "2":
421 key2FnsCalls++
422 case "3":
423 key3FnsCalls++
424 case "4":
425 key4FnsCalls++
426 }
427 }
428 cache.events.eviction.fns[2] = cache.events.eviction.fns[1]
429
430
431 cache.evict(EvictionReasonDeleted, cache.items.lru.Back(), cache.items.lru.Back().Prev())
432
433 assert.Equal(t, 2, key1FnsCalls)
434 assert.Equal(t, 2, key2FnsCalls)
435 assert.Zero(t, key3FnsCalls)
436 assert.Zero(t, key4FnsCalls)
437 assert.Len(t, cache.items.values, 2)
438 assert.NotContains(t, cache.items.values, "1")
439 assert.NotContains(t, cache.items.values, "2")
440 assert.Equal(t, uint64(2), cache.metrics.Evictions)
441
442
443 key1FnsCalls, key2FnsCalls = 0, 0
444 cache.metrics.Evictions = 0
445
446 cache.evict(EvictionReasonDeleted)
447
448 assert.Zero(t, key1FnsCalls)
449 assert.Zero(t, key2FnsCalls)
450 assert.Equal(t, 2, key3FnsCalls)
451 assert.Equal(t, 2, key4FnsCalls)
452 assert.Empty(t, cache.items.values)
453 assert.NotContains(t, cache.items.values, "3")
454 assert.NotContains(t, cache.items.values, "4")
455 assert.Equal(t, uint64(2), cache.metrics.Evictions)
456 }
457
458 func Test_Cache_Set(t *testing.T) {
459 cache := prepCache(time.Hour, "test1", "test2", "test3")
460 item := cache.Set("hello", "value123", time.Minute)
461 require.NotNil(t, item)
462 assert.Same(t, item, cache.items.values["hello"].Value)
463
464 item = cache.Set("test1", "value123", time.Minute)
465 require.NotNil(t, item)
466 assert.Same(t, item, cache.items.values["test1"].Value)
467 }
468
469 func Test_Cache_Get(t *testing.T) {
470 const notFoundKey, foundKey = "notfound", "test1"
471 cc := map[string]struct {
472 Key string
473 DefaultOptions options[string, string]
474 CallOptions []Option[string, string]
475 Metrics Metrics
476 Result *Item[string, string]
477 }{
478 "Get without loader when item is not found": {
479 Key: notFoundKey,
480 Metrics: Metrics{
481 Misses: 1,
482 },
483 },
484 "Get with default loader that returns non nil value when item is not found": {
485 Key: notFoundKey,
486 DefaultOptions: options[string, string]{
487 loader: LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] {
488 return &Item[string, string]{key: "test"}
489 }),
490 },
491 Metrics: Metrics{
492 Misses: 1,
493 },
494 Result: &Item[string, string]{key: "test"},
495 },
496 "Get with default loader that returns nil value when item is not found": {
497 Key: notFoundKey,
498 DefaultOptions: options[string, string]{
499 loader: LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] {
500 return nil
501 }),
502 },
503 Metrics: Metrics{
504 Misses: 1,
505 },
506 },
507 "Get with call loader that returns non nil value when item is not found": {
508 Key: notFoundKey,
509 DefaultOptions: options[string, string]{
510 loader: LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] {
511 return &Item[string, string]{key: "test"}
512 }),
513 },
514 CallOptions: []Option[string, string]{
515 WithLoader[string, string](LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] {
516 return &Item[string, string]{key: "hello"}
517 })),
518 },
519 Metrics: Metrics{
520 Misses: 1,
521 },
522 Result: &Item[string, string]{key: "hello"},
523 },
524 "Get with call loader that returns nil value when item is not found": {
525 Key: notFoundKey,
526 DefaultOptions: options[string, string]{
527 loader: LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] {
528 return &Item[string, string]{key: "test"}
529 }),
530 },
531 CallOptions: []Option[string, string]{
532 WithLoader[string, string](LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] {
533 return nil
534 })),
535 },
536 Metrics: Metrics{
537 Misses: 1,
538 },
539 },
540 "Get when TTL extension is disabled by default and item is found": {
541 Key: foundKey,
542 DefaultOptions: options[string, string]{
543 disableTouchOnHit: true,
544 },
545 Metrics: Metrics{
546 Hits: 1,
547 },
548 },
549 "Get when TTL extension is disabled and item is found": {
550 Key: foundKey,
551 CallOptions: []Option[string, string]{
552 WithDisableTouchOnHit[string, string](),
553 },
554 Metrics: Metrics{
555 Hits: 1,
556 },
557 },
558 "Get when item is found": {
559 Key: foundKey,
560 Metrics: Metrics{
561 Hits: 1,
562 },
563 },
564 }
565
566 for cn, c := range cc {
567 c := c
568
569 t.Run(cn, func(t *testing.T) {
570 t.Parallel()
571
572 cache := prepCache(time.Minute, foundKey, "test2", "test3")
573 oldExpiresAt := cache.items.values[foundKey].Value.(*Item[string, string]).expiresAt
574 cache.options = c.DefaultOptions
575
576 res := cache.Get(c.Key, c.CallOptions...)
577
578 if c.Key == foundKey {
579 c.Result = cache.items.values[foundKey].Value.(*Item[string, string])
580 assert.Equal(t, foundKey, cache.items.lru.Front().Value.(*Item[string, string]).key)
581 }
582
583 assert.Equal(t, c.Metrics, cache.metrics)
584
585 if !assert.Equal(t, c.Result, res) || res == nil || res.ttl == 0 {
586 return
587 }
588
589 applyOptions(&c.DefaultOptions, c.CallOptions...)
590
591 if c.DefaultOptions.disableTouchOnHit {
592 assert.Equal(t, oldExpiresAt, res.expiresAt)
593 return
594 }
595
596 assert.True(t, oldExpiresAt.Before(res.expiresAt))
597 assert.WithinDuration(t, time.Now(), res.expiresAt, res.ttl)
598 })
599 }
600 }
601
602 func Test_Cache_Delete(t *testing.T) {
603 var fnsCalls int
604
605 cache := prepCache(time.Hour, "1", "2", "3", "4")
606 cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) {
607 assert.Equal(t, EvictionReasonDeleted, r)
608 fnsCalls++
609 }
610 cache.events.eviction.fns[2] = cache.events.eviction.fns[1]
611
612
613 cache.Delete("1234")
614 assert.Zero(t, fnsCalls)
615 assert.Len(t, cache.items.values, 4)
616
617
618 cache.Delete("1")
619 assert.Equal(t, 2, fnsCalls)
620 assert.Len(t, cache.items.values, 3)
621 assert.NotContains(t, cache.items.values, "1")
622 }
623
624 func Test_Cache_Has(t *testing.T) {
625 cc := map[string]struct {
626 keys []string
627 searchKey string
628 has bool
629 }{
630 "Empty cache": {
631 keys: []string{},
632 searchKey: "key1",
633 has: false,
634 },
635 "Key exists": {
636 keys: []string{"key1", "key2", "key3"},
637 searchKey: "key2",
638 has: true,
639 },
640 "Key doesn't exist": {
641 keys: []string{"key1", "key2", "key3"},
642 searchKey: "key4",
643 has: false,
644 },
645 }
646
647 for name, tc := range cc {
648 t.Run(name, func(t *testing.T) {
649 c := prepCache(NoTTL, tc.keys...)
650 has := c.Has(tc.searchKey)
651 assert.Equal(t, tc.has, has)
652 })
653 }
654 }
655
656 func Test_Cache_GetOrSet(t *testing.T) {
657 cache := prepCache(time.Hour)
658 item, retrieved := cache.GetOrSet("test", "1", WithTTL[string, string](time.Minute))
659 require.NotNil(t, item)
660 assert.Same(t, item, cache.items.values["test"].Value)
661 assert.False(t, retrieved)
662
663 item, retrieved = cache.GetOrSet("test", "1", WithTTL[string, string](time.Minute))
664 require.NotNil(t, item)
665 assert.Same(t, item, cache.items.values["test"].Value)
666 assert.True(t, retrieved)
667
668 item, retrieved = cache.GetOrSet("test2", "1", WithTTL[string, string](time.Microsecond))
669 require.NotNil(t, item)
670 assert.Same(t, item, cache.items.values["test2"].Value)
671 assert.False(t, retrieved)
672
673 time.Sleep(time.Millisecond)
674 item, retrieved = cache.GetOrSet("test2", "2", WithTTL[string, string](time.Minute))
675 require.NotNil(t, item)
676 assert.Same(t, item, cache.items.values["test2"].Value)
677 assert.False(t, retrieved)
678 }
679
680 func Test_Cache_GetAndDelete(t *testing.T) {
681 cache := prepCache(time.Hour, "test1", "test2", "test3")
682 listItem := cache.items.lru.Front()
683 require.NotNil(t, listItem)
684 assert.Same(t, listItem, cache.items.values["test3"])
685
686 item, present := cache.GetAndDelete("test3")
687 require.NotNil(t, item)
688 assert.Nil(t, cache.items.values["test3"])
689 assert.True(t, present)
690
691 item, present = cache.GetAndDelete("test3")
692 require.Nil(t, item)
693 assert.Nil(t, cache.items.values["test3"])
694 assert.False(t, present)
695
696 loadedItem := &Item[string, string]{key: "test"}
697 item, present = cache.GetAndDelete(
698 "test3",
699 WithLoader[string, string](
700 LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] { return loadedItem }),
701 ),
702 )
703 require.NotNil(t, item)
704 assert.Nil(t, cache.items.values["test3"])
705 assert.True(t, present)
706 assert.Same(t, item, loadedItem)
707 }
708
709 func Test_Cache_DeleteAll(t *testing.T) {
710 var (
711 key1FnsCalls int
712 key2FnsCalls int
713 key3FnsCalls int
714 key4FnsCalls int
715 )
716
717 cache := prepCache(time.Hour, "1", "2", "3", "4")
718 cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) {
719 assert.Equal(t, EvictionReasonDeleted, r)
720 switch item.key {
721 case "1":
722 key1FnsCalls++
723 case "2":
724 key2FnsCalls++
725 case "3":
726 key3FnsCalls++
727 case "4":
728 key4FnsCalls++
729 }
730 }
731 cache.events.eviction.fns[2] = cache.events.eviction.fns[1]
732
733 cache.DeleteAll()
734 assert.Empty(t, cache.items.values)
735 assert.Equal(t, 2, key1FnsCalls)
736 assert.Equal(t, 2, key2FnsCalls)
737 assert.Equal(t, 2, key3FnsCalls)
738 assert.Equal(t, 2, key4FnsCalls)
739 }
740
741 func Test_Cache_DeleteExpired(t *testing.T) {
742 var (
743 key1FnsCalls int
744 key2FnsCalls int
745 )
746
747 cache := prepCache(time.Hour)
748 cache.events.eviction.fns[1] = func(r EvictionReason, item *Item[string, string]) {
749 assert.Equal(t, EvictionReasonExpired, r)
750 switch item.key {
751 case "5":
752 key1FnsCalls++
753 case "6":
754 key2FnsCalls++
755 }
756 }
757 cache.events.eviction.fns[2] = cache.events.eviction.fns[1]
758
759
760 addToCache(cache, time.Nanosecond, "5")
761
762 cache.DeleteExpired()
763 assert.Empty(t, cache.items.values)
764 assert.NotContains(t, cache.items.values, "5")
765 assert.Equal(t, 2, key1FnsCalls)
766
767 key1FnsCalls = 0
768
769
770 cache.DeleteExpired()
771 assert.Empty(t, cache.items.values)
772
773
774 addToCache(cache, time.Hour, "1", "2", "3", "4")
775 addToCache(cache, time.Nanosecond, "5")
776 addToCache(cache, time.Nanosecond, "6")
777 time.Sleep(time.Millisecond)
778
779 cache.DeleteExpired()
780 assert.Len(t, cache.items.values, 4)
781 assert.NotContains(t, cache.items.values, "5")
782 assert.NotContains(t, cache.items.values, "6")
783 assert.Equal(t, 2, key1FnsCalls)
784 assert.Equal(t, 2, key2FnsCalls)
785 }
786
787 func Test_Cache_Touch(t *testing.T) {
788 cache := prepCache(time.Hour, "1", "2")
789 oldExpiresAt := cache.items.values["1"].Value.(*Item[string, string]).expiresAt
790
791 cache.Touch("1")
792
793 newExpiresAt := cache.items.values["1"].Value.(*Item[string, string]).expiresAt
794 assert.True(t, newExpiresAt.After(oldExpiresAt))
795 assert.Equal(t, "1", cache.items.lru.Front().Value.(*Item[string, string]).key)
796 }
797
798 func Test_Cache_Len(t *testing.T) {
799 cache := prepCache(time.Hour, "1", "2")
800 assert.Equal(t, 2, cache.Len())
801 }
802
803 func Test_Cache_Keys(t *testing.T) {
804 cache := prepCache(time.Hour, "1", "2", "3")
805 assert.ElementsMatch(t, []string{"1", "2", "3"}, cache.Keys())
806 }
807
808 func Test_Cache_Items(t *testing.T) {
809 cache := prepCache(time.Hour, "1", "2", "3")
810 items := cache.Items()
811 require.Len(t, items, 3)
812
813 require.Contains(t, items, "1")
814 assert.Equal(t, "1", items["1"].key)
815 require.Contains(t, items, "2")
816 assert.Equal(t, "2", items["2"].key)
817 require.Contains(t, items, "3")
818 assert.Equal(t, "3", items["3"].key)
819 }
820
821 func Test_Cache_Range(t *testing.T) {
822 c := prepCache(DefaultTTL, "1", "2", "3", "4", "5")
823 var results []string
824
825 c.Range(func(item *Item[string, string]) bool {
826 results = append(results, item.Key())
827 return item.Key() != "4"
828 })
829
830 assert.Equal(t, []string{"5", "4"}, results)
831
832 emptyCache := New[string, string]()
833 assert.NotPanics(t, func() {
834 emptyCache.Range(func(item *Item[string, string]) bool {
835 return false
836 })
837 })
838 }
839
840 func Test_Cache_Metrics(t *testing.T) {
841 cache := Cache[string, string]{
842 metrics: Metrics{Evictions: 10},
843 }
844
845 assert.Equal(t, Metrics{Evictions: 10}, cache.Metrics())
846 }
847
848 func Test_Cache_Start(t *testing.T) {
849 cache := prepCache(0)
850 cache.stopCh = make(chan struct{})
851
852 addToCache(cache, time.Nanosecond, "1")
853 time.Sleep(time.Millisecond)
854
855 fn := func(r EvictionReason, _ *Item[string, string]) {
856 go func() {
857 assert.Equal(t, EvictionReasonExpired, r)
858
859 cache.metricsMu.RLock()
860 v := cache.metrics.Evictions
861 cache.metricsMu.RUnlock()
862
863 switch v {
864 case 1:
865 cache.items.mu.Lock()
866 addToCache(cache, time.Nanosecond, "2")
867 cache.items.mu.Unlock()
868 cache.options.ttl = time.Hour
869 cache.items.timerCh <- time.Millisecond
870 case 2:
871 cache.items.mu.Lock()
872 addToCache(cache, time.Second, "3")
873 addToCache(cache, NoTTL, "4")
874 cache.items.mu.Unlock()
875 cache.items.timerCh <- time.Millisecond
876 default:
877 close(cache.stopCh)
878 }
879 }()
880 }
881 cache.events.eviction.fns[1] = fn
882
883 cache.Start()
884 }
885
886 func Test_Cache_Stop(t *testing.T) {
887 cache := Cache[string, string]{
888 stopCh: make(chan struct{}, 1),
889 }
890 cache.Stop()
891 assert.Len(t, cache.stopCh, 1)
892 }
893
894 func Test_Cache_OnInsertion(t *testing.T) {
895 checkCh := make(chan struct{})
896 resCh := make(chan struct{})
897 cache := prepCache(time.Hour)
898 del1 := cache.OnInsertion(func(_ context.Context, _ *Item[string, string]) {
899 checkCh <- struct{}{}
900 })
901 del2 := cache.OnInsertion(func(_ context.Context, _ *Item[string, string]) {
902 checkCh <- struct{}{}
903 })
904
905 require.Len(t, cache.events.insertion.fns, 2)
906 assert.Equal(t, uint64(2), cache.events.insertion.nextID)
907
908 cache.events.insertion.fns[0](nil)
909
910 go func() {
911 del1()
912 resCh <- struct{}{}
913 }()
914 assert.Never(t, func() bool {
915 select {
916 case <-resCh:
917 return true
918 default:
919 return false
920 }
921 }, time.Millisecond*200, time.Millisecond*100)
922 assert.Eventually(t, func() bool {
923 select {
924 case <-checkCh:
925 return true
926 default:
927 return false
928 }
929 }, time.Millisecond*500, time.Millisecond*250)
930 assert.Eventually(t, func() bool {
931 select {
932 case <-resCh:
933 return true
934 default:
935 return false
936 }
937 }, time.Millisecond*500, time.Millisecond*250)
938
939 require.Len(t, cache.events.insertion.fns, 1)
940 assert.NotContains(t, cache.events.insertion.fns, uint64(0))
941 assert.Contains(t, cache.events.insertion.fns, uint64(1))
942
943 cache.events.insertion.fns[1](nil)
944
945 go func() {
946 del2()
947 resCh <- struct{}{}
948 }()
949 assert.Never(t, func() bool {
950 select {
951 case <-resCh:
952 return true
953 default:
954 return false
955 }
956 }, time.Millisecond*200, time.Millisecond*100)
957 assert.Eventually(t, func() bool {
958 select {
959 case <-checkCh:
960 return true
961 default:
962 return false
963 }
964 }, time.Millisecond*500, time.Millisecond*250)
965 assert.Eventually(t, func() bool {
966 select {
967 case <-resCh:
968 return true
969 default:
970 return false
971 }
972 }, time.Millisecond*500, time.Millisecond*250)
973
974 assert.Empty(t, cache.events.insertion.fns)
975 assert.NotContains(t, cache.events.insertion.fns, uint64(1))
976 }
977
978 func Test_Cache_OnEviction(t *testing.T) {
979 checkCh := make(chan struct{})
980 resCh := make(chan struct{})
981 cache := prepCache(time.Hour)
982 del1 := cache.OnEviction(func(_ context.Context, _ EvictionReason, _ *Item[string, string]) {
983 checkCh <- struct{}{}
984 })
985 del2 := cache.OnEviction(func(_ context.Context, _ EvictionReason, _ *Item[string, string]) {
986 checkCh <- struct{}{}
987 })
988
989 require.Len(t, cache.events.eviction.fns, 2)
990 assert.Equal(t, uint64(2), cache.events.eviction.nextID)
991
992 cache.events.eviction.fns[0](0, nil)
993
994 go func() {
995 del1()
996 resCh <- struct{}{}
997 }()
998 assert.Never(t, func() bool {
999 select {
1000 case <-resCh:
1001 return true
1002 default:
1003 return false
1004 }
1005 }, time.Millisecond*200, time.Millisecond*100)
1006 assert.Eventually(t, func() bool {
1007 select {
1008 case <-checkCh:
1009 return true
1010 default:
1011 return false
1012 }
1013 }, time.Millisecond*500, time.Millisecond*250)
1014 assert.Eventually(t, func() bool {
1015 select {
1016 case <-resCh:
1017 return true
1018 default:
1019 return false
1020 }
1021 }, time.Millisecond*500, time.Millisecond*250)
1022
1023 require.Len(t, cache.events.eviction.fns, 1)
1024 assert.NotContains(t, cache.events.eviction.fns, uint64(0))
1025 assert.Contains(t, cache.events.eviction.fns, uint64(1))
1026
1027 cache.events.eviction.fns[1](0, nil)
1028
1029 go func() {
1030 del2()
1031 resCh <- struct{}{}
1032 }()
1033 assert.Never(t, func() bool {
1034 select {
1035 case <-resCh:
1036 return true
1037 default:
1038 return false
1039 }
1040 }, time.Millisecond*200, time.Millisecond*100)
1041 assert.Eventually(t, func() bool {
1042 select {
1043 case <-checkCh:
1044 return true
1045 default:
1046 return false
1047 }
1048 }, time.Millisecond*500, time.Millisecond*250)
1049 assert.Eventually(t, func() bool {
1050 select {
1051 case <-resCh:
1052 return true
1053 default:
1054 return false
1055 }
1056 }, time.Millisecond*500, time.Millisecond*250)
1057
1058 assert.Empty(t, cache.events.eviction.fns)
1059 assert.NotContains(t, cache.events.eviction.fns, uint64(1))
1060 }
1061
1062 func Test_LoaderFunc_Load(t *testing.T) {
1063 var called bool
1064
1065 fn := LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] {
1066 called = true
1067 return nil
1068 })
1069
1070 assert.Nil(t, fn(nil, ""))
1071 assert.True(t, called)
1072 }
1073
1074 func Test_NewSuppressedLoader(t *testing.T) {
1075 var called bool
1076
1077 loader := LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] {
1078 called = true
1079 return nil
1080 })
1081
1082
1083 group := &singleflight.Group{}
1084
1085 sl := NewSuppressedLoader[string, string](loader, group)
1086 require.NotNil(t, sl)
1087 require.NotNil(t, sl.loader)
1088
1089 sl.loader.Load(nil, "")
1090
1091 assert.True(t, called)
1092 assert.Equal(t, group, sl.group)
1093
1094
1095
1096 called = false
1097
1098 sl = NewSuppressedLoader[string, string](loader, nil)
1099 require.NotNil(t, sl)
1100 require.NotNil(t, sl.loader)
1101
1102 sl.loader.Load(nil, "")
1103
1104 assert.True(t, called)
1105 assert.NotNil(t, group, sl.group)
1106 }
1107
1108 func Test_SuppressedLoader_Load(t *testing.T) {
1109 var (
1110 mu sync.Mutex
1111 loadCalls int
1112 releaseCh = make(chan struct{})
1113 res *Item[string, string]
1114 )
1115
1116 l := SuppressedLoader[string, string]{
1117 loader: LoaderFunc[string, string](func(_ *Cache[string, string], _ string) *Item[string, string] {
1118 mu.Lock()
1119 loadCalls++
1120 mu.Unlock()
1121
1122 <-releaseCh
1123
1124 if res == nil {
1125 return nil
1126 }
1127
1128 res1 := *res
1129
1130 return &res1
1131 }),
1132 group: &singleflight.Group{},
1133 }
1134
1135 var (
1136 wg sync.WaitGroup
1137 item1, item2 *Item[string, string]
1138 )
1139
1140 cache := prepCache(time.Hour)
1141
1142
1143 wg.Add(2)
1144
1145 go func() {
1146 item1 = l.Load(cache, "test")
1147 wg.Done()
1148 }()
1149
1150 go func() {
1151 item2 = l.Load(cache, "test")
1152 wg.Done()
1153 }()
1154
1155 time.Sleep(time.Millisecond * 100)
1156 releaseCh <- struct{}{}
1157
1158 wg.Wait()
1159 require.Nil(t, item1)
1160 require.Nil(t, item2)
1161 assert.Equal(t, 1, loadCalls)
1162
1163
1164 res = &Item[string, string]{key: "test"}
1165 loadCalls = 0
1166 wg.Add(2)
1167
1168 go func() {
1169 item1 = l.Load(cache, "test")
1170 wg.Done()
1171 }()
1172
1173 go func() {
1174 item2 = l.Load(cache, "test")
1175 wg.Done()
1176 }()
1177
1178 time.Sleep(time.Millisecond * 100)
1179 releaseCh <- struct{}{}
1180
1181 wg.Wait()
1182 require.Same(t, item1, item2)
1183 assert.Equal(t, "test", item1.key)
1184 assert.Equal(t, 1, loadCalls)
1185 }
1186
1187 func prepCache(ttl time.Duration, keys ...string) *Cache[string, string] {
1188 c := &Cache[string, string]{}
1189 c.options.ttl = ttl
1190 c.items.values = make(map[string]*list.Element)
1191 c.items.lru = list.New()
1192 c.items.expQueue = newExpirationQueue[string, string]()
1193 c.items.timerCh = make(chan time.Duration, 1)
1194 c.events.eviction.fns = make(map[uint64]func(EvictionReason, *Item[string, string]))
1195 c.events.insertion.fns = make(map[uint64]func(*Item[string, string]))
1196
1197 addToCache(c, ttl, keys...)
1198
1199 return c
1200 }
1201
1202 func addToCache(c *Cache[string, string], ttl time.Duration, keys ...string) {
1203 for i, key := range keys {
1204 item := newItem(
1205 key,
1206 fmt.Sprint("value of", key),
1207 ttl+time.Duration(i)*time.Minute,
1208 false,
1209 )
1210 elem := c.items.lru.PushFront(item)
1211 c.items.values[key] = elem
1212 c.items.expQueue.push(elem)
1213 }
1214 }
1215
View as plain text