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 }