...

Source file src/github.com/jellydator/ttlcache/v3/cache_test.go

Documentation: github.com/jellydator/ttlcache/v3

     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  			// calculated based on how addToCache sets ttl
   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) // force expiration
   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  	// delete only specified
   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  	// delete all
   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  	// not found
   613  	cache.Delete("1234")
   614  	assert.Zero(t, fnsCalls)
   615  	assert.Len(t, cache.items.values, 4)
   616  
   617  	// success
   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  	// one item
   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  	// empty
   770  	cache.DeleteExpired()
   771  	assert.Empty(t, cache.items.values)
   772  
   773  	// non empty
   774  	addToCache(cache, time.Hour, "1", "2", "3", "4")
   775  	addToCache(cache, time.Nanosecond, "5")
   776  	addToCache(cache, time.Nanosecond, "6") // we need multiple calls to avoid adding time.Minute to ttl
   777  	time.Sleep(time.Millisecond)            // force expiration
   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) // force expiration
   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  	// uses the provided loader and group parameters
  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  	// uses the provided loader and automatically creates a new instance
  1095  	// of *singleflight.Group as nil parameter is passed
  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  	// nil result
  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) // wait for goroutines to halt
  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  	// non nil result
  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) // wait for goroutines to halt
  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