go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/caching/lru/lru_test.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package lru
    16  
    17  import (
    18  	"context"
    19  	"sync"
    20  	"testing"
    21  	"time"
    22  
    23  	"go.chromium.org/luci/common/clock/testclock"
    24  	"go.chromium.org/luci/common/errors"
    25  
    26  	. "github.com/smartystreets/goconvey/convey"
    27  )
    28  
    29  func TestCache(t *testing.T) {
    30  	t.Parallel()
    31  
    32  	Convey(`An locking LRU cache with size heuristic 3`, t, func() {
    33  		ctx := context.Background()
    34  		cache := New[string, string](3)
    35  
    36  		Convey(`A Get() returns "no item".`, func() {
    37  			_, has := cache.Get(ctx, "test")
    38  			So(has, ShouldBeFalse)
    39  		})
    40  
    41  		// Adds values to the cache sequentially, blocking on the values being
    42  		// processed.
    43  		addCacheValues := func(values ...string) {
    44  			for _, v := range values {
    45  				_, isPresent := cache.Peek(ctx, v)
    46  				_, has := cache.Put(ctx, v, v+"v", 0)
    47  				So(has, ShouldEqual, isPresent)
    48  			}
    49  		}
    50  
    51  		get := func(key string) (val string) {
    52  			val, _ = cache.Get(ctx, key)
    53  			return
    54  		}
    55  
    56  		Convey(`With three values, {a, b, c}`, func() {
    57  			addCacheValues("a", "b", "c")
    58  			So(cache.Len(), ShouldEqual, 3)
    59  
    60  			Convey(`Prune does nothing.`, func() {
    61  				cache.Prune(ctx)
    62  				So(cache.Len(), ShouldEqual, 3)
    63  			})
    64  
    65  			Convey(`Is empty after a reset.`, func() {
    66  				cache.Reset()
    67  				So(cache.Len(), ShouldEqual, 0)
    68  			})
    69  
    70  			Convey(`Can retrieve each of those values.`, func() {
    71  				So(get("a"), ShouldEqual, "av")
    72  				So(get("b"), ShouldEqual, "bv")
    73  				So(get("c"), ShouldEqual, "cv")
    74  			})
    75  
    76  			Convey(`Get()ting "a", then adding "d" will cause "b" to be evicted.`, func() {
    77  				So(get("a"), ShouldEqual, "av")
    78  				addCacheValues("d")
    79  				So(cache, shouldHaveValues, "a", "c", "d")
    80  			})
    81  
    82  			Convey(`Peek()ing "a", then adding "d" will cause "a" to be evicted.`, func() {
    83  				v, has := cache.Peek(ctx, "a")
    84  				So(has, ShouldBeTrue)
    85  				So(v, ShouldEqual, "av")
    86  
    87  				v, has = cache.Peek(ctx, "nonexist")
    88  				So(has, ShouldBeFalse)
    89  				So(v, ShouldEqual, "")
    90  
    91  				addCacheValues("d")
    92  				So(cache, shouldHaveValues, "b", "c", "d")
    93  			})
    94  		})
    95  
    96  		Convey(`When adding {a, b, c, d}, "a" will be evicted.`, func() {
    97  			addCacheValues("a", "b", "c", "d")
    98  			So(cache.Len(), ShouldEqual, 3)
    99  
   100  			So(cache, shouldHaveValues, "b", "c", "d")
   101  
   102  			Convey(`Requests for "a" will be empty.`, func() {
   103  				So(get("a"), ShouldEqual, "")
   104  			})
   105  		})
   106  
   107  		Convey(`When adding {a, b, c, a, d}, "b" will be evicted.`, func() {
   108  			addCacheValues("a", "b", "c", "a", "d")
   109  			So(cache.Len(), ShouldEqual, 3)
   110  
   111  			So(cache, shouldHaveValues, "a", "c", "d")
   112  
   113  			Convey(`When removing "c", will contain {a, d}.`, func() {
   114  				v, had := cache.Remove("c")
   115  				So(had, ShouldBeTrue)
   116  				So(v, ShouldEqual, "cv")
   117  				So(cache, shouldHaveValues, "a", "d")
   118  
   119  				Convey(`When adding {e, f}, "a" will be evicted.`, func() {
   120  					addCacheValues("e", "f")
   121  					So(cache, shouldHaveValues, "d", "e", "f")
   122  				})
   123  			})
   124  		})
   125  
   126  		Convey(`When removing a value that isn't there, returns "no item".`, func() {
   127  			v, has := cache.Remove("foo")
   128  			So(has, ShouldBeFalse)
   129  			So(v, ShouldEqual, "")
   130  		})
   131  	})
   132  }
   133  
   134  func TestCacheWithExpiry(t *testing.T) {
   135  	t.Parallel()
   136  
   137  	Convey(`A cache of size 3 with a Clock`, t, func() {
   138  		ctx, tc := testclock.UseTime(context.Background(), testclock.TestTimeUTC)
   139  		cache := New[string, string](3)
   140  
   141  		cache.Put(ctx, "a", "av", 1*time.Second)
   142  		cache.Put(ctx, "b", "bv", 2*time.Second)
   143  		cache.Put(ctx, "forever", "foreverv", 0)
   144  
   145  		Convey(`When "a" is expired`, func() {
   146  			tc.Add(time.Second)
   147  
   148  			Convey(`Get doesn't yield "a", but yields "b".`, func() {
   149  				_, has := cache.Get(ctx, "a")
   150  				So(has, ShouldBeFalse)
   151  
   152  				_, has = cache.Get(ctx, "b")
   153  				So(has, ShouldBeTrue)
   154  			})
   155  
   156  			Convey(`Mutate treats "a" as missing.`, func() {
   157  				v, ok := cache.Mutate(ctx, "a", func(it *Item[string]) *Item[string] {
   158  					So(it, ShouldBeNil)
   159  					return nil
   160  				})
   161  				So(ok, ShouldBeFalse)
   162  				So(v, ShouldEqual, "")
   163  				So(cache.Len(), ShouldEqual, 2)
   164  
   165  				_, has := cache.Get(ctx, "a")
   166  				So(has, ShouldBeFalse)
   167  			})
   168  
   169  			Convey(`Mutate replaces "a" if a value is supplied.`, func() {
   170  				v, ok := cache.Mutate(ctx, "a", func(it *Item[string]) *Item[string] {
   171  					So(it, ShouldBeNil)
   172  					return &Item[string]{"av", 0}
   173  				})
   174  				So(ok, ShouldBeTrue)
   175  				So(v, ShouldEqual, "av")
   176  				So(cache, shouldHaveValues, "a", "b", "forever")
   177  
   178  				v, has := cache.Get(ctx, "a")
   179  				So(has, ShouldBeTrue)
   180  				So(v, ShouldEqual, "av")
   181  			})
   182  
   183  			Convey(`Mutateing "b" yields the remaining time.`, func() {
   184  				v, ok := cache.Mutate(ctx, "b", func(it *Item[string]) *Item[string] {
   185  					So(it, ShouldResemble, &Item[string]{"bv", 1 * time.Second})
   186  					return it
   187  				})
   188  				So(ok, ShouldBeTrue)
   189  				So(v, ShouldEqual, "bv")
   190  
   191  				v, has := cache.Get(ctx, "b")
   192  				So(has, ShouldBeTrue)
   193  				So(v, ShouldEqual, "bv")
   194  
   195  				tc.Add(time.Second)
   196  
   197  				_, has = cache.Get(ctx, "b")
   198  				So(has, ShouldBeFalse)
   199  			})
   200  		})
   201  
   202  		Convey(`Prune prunes all expired entries.`, func() {
   203  			tc.Add(1 * time.Hour)
   204  			cache.Prune(ctx)
   205  			So(cache, shouldHaveValues, "forever")
   206  		})
   207  	})
   208  }
   209  
   210  func TestUnboundedCache(t *testing.T) {
   211  	t.Parallel()
   212  
   213  	Convey(`An unbounded cache`, t, func() {
   214  		ctx := context.Background()
   215  		cache := New[int, string](0)
   216  
   217  		Convey(`Grows indefinitely`, func() {
   218  			for i := 0; i < 1000; i++ {
   219  				cache.Put(ctx, i, "hey", 0)
   220  			}
   221  			So(cache.Len(), ShouldEqual, 1000)
   222  		})
   223  
   224  		Convey(`Grows indefinitely even if elements have an (ignored) expiry`, func() {
   225  			for i := 0; i < 1000; i++ {
   226  				cache.Put(ctx, i, "hey", time.Second)
   227  			}
   228  			So(cache.Len(), ShouldEqual, 1000)
   229  
   230  			cache.Prune(ctx)
   231  			So(cache.Len(), ShouldEqual, 1000)
   232  		})
   233  	})
   234  }
   235  
   236  func TestUnboundedCacheWithExpiry(t *testing.T) {
   237  	t.Parallel()
   238  
   239  	Convey(`An unbounded cache with a clock`, t, func() {
   240  		ctx, tc := testclock.UseTime(context.Background(), testclock.TestTimeUTC)
   241  		cache := New[int, string](0)
   242  
   243  		Convey(`Grows indefinitely`, func() {
   244  			for i := 0; i < 1000; i++ {
   245  				cache.Put(ctx, i, "hey", 0)
   246  			}
   247  			So(cache.Len(), ShouldEqual, 1000)
   248  
   249  			cache.Prune(ctx)
   250  			So(cache.Len(), ShouldEqual, 1000)
   251  		})
   252  
   253  		Convey(`Grows indefinitely even if elements have an (ignored) expiry`, func() {
   254  			for i := 1; i <= 1000; i++ {
   255  				cache.Put(ctx, i, "hey", time.Duration(i)*time.Second)
   256  			}
   257  			So(cache.Len(), ShouldEqual, 1000)
   258  
   259  			// Expire the first half of entries.
   260  			tc.Add(500 * time.Second)
   261  
   262  			Convey(`Get works`, func() {
   263  				v, has := cache.Get(ctx, 1)
   264  				So(has, ShouldBeFalse)
   265  				So(v, ShouldEqual, "")
   266  
   267  				v, has = cache.Get(ctx, 500)
   268  				So(has, ShouldBeFalse)
   269  				So(v, ShouldEqual, "")
   270  
   271  				v, has = cache.Get(ctx, 501)
   272  				So(has, ShouldBeTrue)
   273  				So(v, ShouldEqual, "hey")
   274  			})
   275  
   276  			Convey(`Len works`, func() {
   277  				// Without explicit pruning, Len includes expired elements.
   278  				So(cache.Len(), ShouldEqual, 1000)
   279  
   280  				// After pruning, Len is accurate again.
   281  				cache.Prune(ctx)
   282  				So(cache.Len(), ShouldEqual, 500)
   283  			})
   284  		})
   285  	})
   286  }
   287  
   288  func TestGetOrCreate(t *testing.T) {
   289  	t.Parallel()
   290  
   291  	Convey(`An unbounded cache`, t, func() {
   292  		ctx := context.Background()
   293  
   294  		Convey(`Can create a new value, and will synchronize around that creation`, func() {
   295  			cache := New[string, string](0)
   296  
   297  			v, err := cache.GetOrCreate(ctx, "foo", func() (string, time.Duration, error) {
   298  				return "bar", 0, nil
   299  			})
   300  			So(err, ShouldBeNil)
   301  			So(v, ShouldEqual, "bar")
   302  
   303  			v, ok := cache.Get(ctx, "foo")
   304  			So(ok, ShouldBeTrue)
   305  			So(v, ShouldEqual, "bar")
   306  		})
   307  
   308  		Convey(`Will not retain a value if an error is returned.`, func() {
   309  			cache := New[string, string](0)
   310  
   311  			errWat := errors.New("wat")
   312  			v, err := cache.GetOrCreate(ctx, "foo", func() (string, time.Duration, error) {
   313  				return "", 0, errWat
   314  			})
   315  			So(err, ShouldEqual, errWat)
   316  			So(v, ShouldEqual, "")
   317  
   318  			_, ok := cache.Get(ctx, "foo")
   319  			So(ok, ShouldBeFalse)
   320  		})
   321  
   322  		Convey(`Will call Maker in series, even with multiple callers, and lock individually.`, func(cc C) {
   323  			const count = 16
   324  			const contention = 16
   325  
   326  			cache := New[int, int](0)
   327  
   328  			var wg sync.WaitGroup
   329  			vals := make([]int, count)
   330  			for i := 0; i < count; i++ {
   331  				for j := 0; j < contention; j++ {
   332  					i := i
   333  					wg.Add(1)
   334  					go func(cctx C) {
   335  						defer wg.Done()
   336  						v, err := cache.GetOrCreate(ctx, i, func() (int, time.Duration, error) {
   337  							val := vals[i]
   338  							vals[i]++
   339  							return val, 0, nil
   340  						})
   341  						cc.So(v, ShouldEqual, 0)
   342  						cc.So(err, ShouldBeNil)
   343  					}(cc)
   344  				}
   345  			}
   346  
   347  			wg.Wait()
   348  			for i := 0; i < count; i++ {
   349  				v, ok := cache.Get(ctx, i)
   350  				So(ok, ShouldBeTrue)
   351  				So(v, ShouldEqual, 0)
   352  				So(vals[i], ShouldEqual, 1)
   353  			}
   354  		})
   355  
   356  		Convey(`Can retrieve values while a Maker is in-progress.`, func() {
   357  			cache := New[string, string](0)
   358  			cache.Put(ctx, "foo", "bar", 0)
   359  
   360  			// Value already exists, so retrieves current value.
   361  			v, err := cache.GetOrCreate(ctx, "foo", func() (string, time.Duration, error) {
   362  				return "baz", 0, nil
   363  			})
   364  			So(err, ShouldBeNil)
   365  			So(v, ShouldEqual, "bar")
   366  
   367  			// Create a new value.
   368  			changingC := make(chan struct{})
   369  			waitC := make(chan struct{})
   370  			doneC := make(chan struct{})
   371  
   372  			var setV any
   373  			var setErr error
   374  			go func() {
   375  				setV, setErr = cache.Create(ctx, "foo", func() (string, time.Duration, error) {
   376  					close(changingC)
   377  					<-waitC
   378  					return "qux", 0, nil
   379  				})
   380  
   381  				close(doneC)
   382  			}()
   383  
   384  			// The goroutine's Create is in-progress, but the value is still present,
   385  			// so we should be able to get the old value.
   386  			<-changingC
   387  			v, err = cache.GetOrCreate(ctx, "foo", func() (string, time.Duration, error) {
   388  				return "never", 0, nil
   389  			})
   390  			So(err, ShouldBeNil)
   391  			So(v, ShouldEqual, "bar")
   392  
   393  			// Our goroutine has finished setting. Validate its output.
   394  			close(waitC)
   395  			<-doneC
   396  
   397  			So(setErr, ShouldBeNil)
   398  			So(setV, ShouldEqual, "qux")
   399  
   400  			// Run GetOrCreate. The value should be present, and should hold the new
   401  			// value added by the goroutine.
   402  			v, err = cache.GetOrCreate(ctx, "foo", func() (string, time.Duration, error) {
   403  				return "never", 0, nil
   404  			})
   405  			So(err, ShouldBeNil)
   406  			So(v, ShouldEqual, "qux")
   407  		})
   408  	})
   409  }
   410  
   411  func shouldHaveValues(actual any, expected ...any) string {
   412  	cache := actual.(*Cache[string, string])
   413  
   414  	actualSnapshot := map[string]string{}
   415  	for k, e := range cache.cache {
   416  		actualSnapshot[k] = e.v
   417  	}
   418  
   419  	expectedSnapshot := map[string]string{}
   420  	for _, k := range expected {
   421  		expectedSnapshot[k.(string)] = k.(string) + "v"
   422  	}
   423  
   424  	return ShouldResemble(actualSnapshot, expectedSnapshot)
   425  }