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  }