blob: 97091e59784181fb68b9d9d2874c4cab8939f31e [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lru
import (
"context"
"sync"
"testing"
"time"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/common/errors"
. "github.com/smartystreets/goconvey/convey"
)
func TestCache(t *testing.T) {
t.Parallel()
Convey(`An locking LRU cache with size heuristic 3`, t, func() {
ctx := context.Background()
cache := New(3)
Convey(`A Get() returns nil.`, func() {
_, has := cache.Get(ctx, "test")
So(has, ShouldBeFalse)
})
// Adds values to the cache sequentially, blocking on the values being
// processed.
addCacheValues := func(values ...string) {
for _, v := range values {
_, isPresent := cache.Peek(ctx, v)
_, has := cache.Put(ctx, v, v+"v", 0)
So(has, ShouldEqual, isPresent)
}
}
get := func(key interface{}) (val interface{}) {
val, _ = cache.Get(ctx, key)
return
}
Convey(`With three values, {a, b, c}`, func() {
addCacheValues("a", "b", "c")
So(cache.Len(), ShouldEqual, 3)
Convey(`Prune does nothing.`, func() {
cache.Prune(ctx)
So(cache.Len(), ShouldEqual, 3)
})
Convey(`Is empty after a reset.`, func() {
cache.Reset()
So(cache.Len(), ShouldEqual, 0)
})
Convey(`Can retrieve each of those values.`, func() {
So(get("a"), ShouldEqual, "av")
So(get("b"), ShouldEqual, "bv")
So(get("c"), ShouldEqual, "cv")
})
Convey(`Get()ting "a", then adding "d" will cause "b" to be evicted.`, func() {
So(get("a"), ShouldEqual, "av")
addCacheValues("d")
So(cache, shouldHaveValues, "a", "c", "d")
})
Convey(`Peek()ing "a", then adding "d" will cause "a" to be evicted.`, func() {
v, has := cache.Peek(ctx, "a")
So(has, ShouldBeTrue)
So(v, ShouldEqual, "av")
v, has = cache.Peek(ctx, "nonexist")
So(has, ShouldBeFalse)
So(v, ShouldBeNil)
addCacheValues("d")
So(cache, shouldHaveValues, "b", "c", "d")
})
})
Convey(`When adding {a, b, c, d}, "a" will be evicted.`, func() {
addCacheValues("a", "b", "c", "d")
So(cache.Len(), ShouldEqual, 3)
So(cache, shouldHaveValues, "b", "c", "d")
Convey(`Requests for "a" will be nil.`, func() {
So(get("a"), ShouldBeNil)
})
})
Convey(`When adding {a, b, c, a, d}, "b" will be evicted.`, func() {
addCacheValues("a", "b", "c", "a", "d")
So(cache.Len(), ShouldEqual, 3)
So(cache, shouldHaveValues, "a", "c", "d")
Convey(`When removing "c", will contain {a, d}.`, func() {
v, had := cache.Remove("c")
So(had, ShouldBeTrue)
So(v, ShouldEqual, "cv")
So(cache, shouldHaveValues, "a", "d")
Convey(`When adding {e, f}, "a" will be evicted.`, func() {
addCacheValues("e", "f")
So(cache, shouldHaveValues, "d", "e", "f")
})
})
})
Convey(`When removing a value that isn't there, returns nil.`, func() {
v, has := cache.Remove("foo")
So(has, ShouldBeFalse)
So(v, ShouldBeNil)
})
})
}
func TestCacheWithExpiry(t *testing.T) {
t.Parallel()
Convey(`A cache of size 3 with a Clock`, t, func() {
ctx, tc := testclock.UseTime(context.Background(), testclock.TestTimeUTC)
cache := New(3)
cache.Put(ctx, "a", "av", 1*time.Second)
cache.Put(ctx, "b", "bv", 2*time.Second)
cache.Put(ctx, "forever", "foreverv", 0)
Convey(`When "a" is expired`, func() {
tc.Add(time.Second)
Convey(`Get doesn't yield "a", but yields "b".`, func() {
_, has := cache.Get(ctx, "a")
So(has, ShouldBeFalse)
_, has = cache.Get(ctx, "b")
So(has, ShouldBeTrue)
})
Convey(`Mutate treats "a" as missing.`, func() {
v, ok := cache.Mutate(ctx, "a", func(it *Item) *Item {
So(it, ShouldBeNil)
return nil
})
So(ok, ShouldBeFalse)
So(v, ShouldBeNil)
So(cache.Len(), ShouldEqual, 2)
_, has := cache.Get(ctx, "a")
So(has, ShouldBeFalse)
})
Convey(`Mutate replaces "a" if a value is supplied.`, func() {
v, ok := cache.Mutate(ctx, "a", func(it *Item) *Item {
So(it, ShouldBeNil)
return &Item{"av", 0}
})
So(ok, ShouldBeTrue)
So(v, ShouldEqual, "av")
So(cache, shouldHaveValues, "a", "b", "forever")
v, has := cache.Get(ctx, "a")
So(has, ShouldBeTrue)
So(v, ShouldEqual, "av")
})
Convey(`Mutateing "b" yields the remaining time.`, func() {
v, ok := cache.Mutate(ctx, "b", func(it *Item) *Item {
So(it, ShouldResemble, &Item{"bv", 1 * time.Second})
return it
})
So(ok, ShouldBeTrue)
So(v, ShouldEqual, "bv")
v, has := cache.Get(ctx, "b")
So(has, ShouldBeTrue)
So(v, ShouldEqual, "bv")
tc.Add(time.Second)
_, has = cache.Get(ctx, "b")
So(has, ShouldBeFalse)
})
})
Convey(`Prune prunes all expired entries.`, func() {
tc.Add(1 * time.Hour)
cache.Prune(ctx)
So(cache, shouldHaveValues, "forever")
})
})
}
func TestUnboundedCache(t *testing.T) {
t.Parallel()
Convey(`An unbounded cache`, t, func() {
ctx := context.Background()
cache := New(0)
Convey(`Grows indefinitely`, func() {
for i := 0; i < 1000; i++ {
cache.Put(ctx, i, "hey", 0)
}
So(cache.Len(), ShouldEqual, 1000)
})
Convey(`Grows indefinitely even if elements have an (ignored) expiry`, func() {
for i := 0; i < 1000; i++ {
cache.Put(ctx, i, "hey", time.Second)
}
So(cache.Len(), ShouldEqual, 1000)
cache.Prune(ctx)
So(cache.Len(), ShouldEqual, 1000)
})
})
}
func TestUnboundedCacheWithExpiry(t *testing.T) {
t.Parallel()
Convey(`An unbounded cache with a clock`, t, func() {
ctx, tc := testclock.UseTime(context.Background(), testclock.TestTimeUTC)
cache := New(0)
Convey(`Grows indefinitely`, func() {
for i := 0; i < 1000; i++ {
cache.Put(ctx, i, "hey", 0)
}
So(cache.Len(), ShouldEqual, 1000)
cache.Prune(ctx)
So(cache.Len(), ShouldEqual, 1000)
})
Convey(`Grows indefinitely even if elements have an (ignored) expiry`, func() {
for i := 1; i <= 1000; i++ {
cache.Put(ctx, i, "hey", time.Duration(i)*time.Second)
}
So(cache.Len(), ShouldEqual, 1000)
// Expire the first half of entries.
tc.Add(500 * time.Second)
Convey(`Get works`, func() {
v, has := cache.Get(ctx, 1)
So(has, ShouldBeFalse)
So(v, ShouldBeNil)
v, has = cache.Get(ctx, 500)
So(has, ShouldBeFalse)
So(v, ShouldBeNil)
v, has = cache.Get(ctx, 501)
So(has, ShouldBeTrue)
So(v, ShouldEqual, "hey")
})
Convey(`Len works`, func() {
// Without explicit pruning, Len includes expired elements.
So(cache.Len(), ShouldEqual, 1000)
// After pruning, Len is accurate again.
cache.Prune(ctx)
So(cache.Len(), ShouldEqual, 500)
})
})
})
}
func TestGetOrCreate(t *testing.T) {
t.Parallel()
Convey(`An unbounded cache`, t, func() {
ctx := context.Background()
cache := New(0)
Convey(`Can create a new value, and will synchronize around that creation`, func() {
v, err := cache.GetOrCreate(ctx, "foo", func() (interface{}, time.Duration, error) {
return "bar", 0, nil
})
So(err, ShouldBeNil)
So(v, ShouldEqual, "bar")
v, ok := cache.Get(ctx, "foo")
So(ok, ShouldBeTrue)
So(v, ShouldEqual, "bar")
})
Convey(`Will not retain a value if an error is returned.`, func() {
errWat := errors.New("wat")
v, err := cache.GetOrCreate(ctx, "foo", func() (interface{}, time.Duration, error) {
return nil, 0, errWat
})
So(err, ShouldEqual, errWat)
So(v, ShouldBeNil)
_, ok := cache.Get(ctx, "foo")
So(ok, ShouldBeFalse)
})
Convey(`Will call Maker in series, even with multiple callers, and lock individually.`, func(cc C) {
const count = 16
const contention = 16
var wg sync.WaitGroup
vals := make([]int, count)
for i := 0; i < count; i++ {
for j := 0; j < contention; j++ {
i := i
wg.Add(1)
go func(cctx C) {
defer wg.Done()
v, err := cache.GetOrCreate(ctx, i, func() (interface{}, time.Duration, error) {
val := vals[i]
vals[i]++
return val, 0, nil
})
cc.So(v, ShouldEqual, 0)
cc.So(err, ShouldBeNil)
}(cc)
}
}
wg.Wait()
for i := 0; i < count; i++ {
v, ok := cache.Get(ctx, i)
So(ok, ShouldBeTrue)
So(v, ShouldEqual, 0)
So(vals[i], ShouldEqual, 1)
}
})
Convey(`Can retrieve values while a Maker is in-progress.`, func() {
cache.Put(ctx, "foo", "bar", 0)
// Value already exists, so retrieves current value.
v, err := cache.GetOrCreate(ctx, "foo", func() (interface{}, time.Duration, error) {
return "baz", 0, nil
})
So(err, ShouldBeNil)
So(v, ShouldEqual, "bar")
// Create a new value.
changingC := make(chan struct{})
waitC := make(chan struct{})
doneC := make(chan struct{})
var setV interface{}
var setErr error
go func() {
setV, setErr = cache.Create(ctx, "foo", func() (interface{}, time.Duration, error) {
close(changingC)
<-waitC
return "qux", 0, nil
})
close(doneC)
}()
// The goroutine's Create is in-progress, but the value is still present,
// so we should be able to get the old value.
<-changingC
v, err = cache.GetOrCreate(ctx, "foo", func() (interface{}, time.Duration, error) {
return "never", 0, nil
})
So(err, ShouldBeNil)
So(v, ShouldEqual, "bar")
// Our goroutine has finished setting. Validate its output.
close(waitC)
<-doneC
So(setErr, ShouldBeNil)
So(setV, ShouldEqual, "qux")
// Run GetOrCreate. The value should be present, and should hold the new
// value added by the goroutine.
v, err = cache.GetOrCreate(ctx, "foo", func() (interface{}, time.Duration, error) {
return "never", 0, nil
})
So(err, ShouldBeNil)
So(v, ShouldEqual, "qux")
})
})
}
func shouldHaveValues(actual interface{}, expected ...interface{}) string {
cache := actual.(*Cache)
actualSnapshot := cache.snapshot()
expectedSnapshot := snapshot{}
for _, k := range expected {
expectedSnapshot[k] = k.(string) + "v"
}
return ShouldResemble(actualSnapshot, expectedSnapshot)
}