pax_global_header00006660000000000000000000000064151315630260014514gustar00rootroot0000000000000052 comment=6baa8c3babd03bae5382358f610d5a32af5e99f8 bep-lazycache-6baa8c3/000077500000000000000000000000001513156302600147145ustar00rootroot00000000000000bep-lazycache-6baa8c3/.github/000077500000000000000000000000001513156302600162545ustar00rootroot00000000000000bep-lazycache-6baa8c3/.github/FUNDING.yml000066400000000000000000000000151513156302600200650ustar00rootroot00000000000000github: [bep]bep-lazycache-6baa8c3/.github/workflows/000077500000000000000000000000001513156302600203115ustar00rootroot00000000000000bep-lazycache-6baa8c3/.github/workflows/test.yml000066400000000000000000000031651513156302600220200ustar00rootroot00000000000000on: push: branches: [ main ] pull_request: name: Test jobs: test: strategy: matrix: go-version: [1.24.x, 1.25.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - name: Install Go uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: ${{ matrix.go-version }} - name: Install staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@latest shell: bash - name: Update PATH run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH shell: bash - name: Checkout code uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Fmt if: matrix.platform != 'windows-latest' # :( run: "diff <(gofmt -d .) <(printf '')" shell: bash - name: Vet run: go vet ./... - name: Staticcheck run: staticcheck ./... - name: Test run: go test -race ./... -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic - name: Upload coverage if: success() && matrix.platform == 'ubuntu-latest' run: | curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import # One-time curl -Os https://uploader.codecov.io/latest/linux/codecov curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig gpgv codecov.SHA256SUM.sig codecov.SHA256SUM shasum -a 256 -c codecov.SHA256SUM chmod +x codecov ./codecov bep-lazycache-6baa8c3/.gitignore000066400000000000000000000004151513156302600167040ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ bep-lazycache-6baa8c3/LICENSE000066400000000000000000000020651513156302600157240ustar00rootroot00000000000000MIT License Copyright (c) 2022 Bjørn Erik Pedersen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bep-lazycache-6baa8c3/README.md000066400000000000000000000027621513156302600162020ustar00rootroot00000000000000[![Tests on Linux, MacOS and Windows](https://github.com/bep/lazycache/workflows/Test/badge.svg)](https://github.com/bep/lazycache/actions?query=workflow:Test) [![Go Report Card](https://goreportcard.com/badge/github.com/bep/lazycache)](https://goreportcard.com/report/github.com/bep/lazycache) [![codecov](https://codecov.io/github/bep/lazycache/branch/main/graph/badge.svg?token=HJCUCT07CH)](https://codecov.io/github/bep/lazycache) [![GoDoc](https://godoc.org/github.com/bep/lazycache?status.svg)](https://godoc.org/github.com/bep/lazycache) **Lazycache** is a simple thread safe in-memory LRU cache. Under the hood it leverages the great [simpleru package in golang-lru](https://github.com/hashicorp/golang-lru), with its exellent performance. One big difference between `golang-lru` and this library is the [GetOrCreate](https://pkg.go.dev/github.com/bep/lazycache#Cache.GetOrCreate) method, which provides: * Non-blocking cache priming on cache misses. * A guarantee that the prime function is only called once for a given key. * The cache's [RWMutex](https://pkg.go.dev/sync#RWMutex) is not locked during the execution of the prime function, which should make it easier to reason about potential deadlocks. Other notable features: * The API is [generic](https://go.dev/doc/tutorial/generics) * The cache can be [resized](https://pkg.go.dev/github.com/bep/lazycache#Cache.Resize) while running. * When the number of entries overflows the defined cache size, the least recently used item gets discarded (LRU). bep-lazycache-6baa8c3/codecov.yml000066400000000000000000000002161513156302600170600ustar00rootroot00000000000000coverage: status: project: default: target: auto threshold: 0.5% patch: off comment: require_changes: true bep-lazycache-6baa8c3/go.mod000066400000000000000000000005071513156302600160240ustar00rootroot00000000000000module github.com/bep/lazycache go 1.24 require ( github.com/frankban/quicktest v1.14.6 github.com/hashicorp/golang-lru/v2 v2.0.7 ) require ( github.com/google/go-cmp v0.7.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect ) bep-lazycache-6baa8c3/go.sum000066400000000000000000000025751513156302600160600ustar00rootroot00000000000000github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= bep-lazycache-6baa8c3/lazycache.go000066400000000000000000000131461513156302600172130ustar00rootroot00000000000000// Copyright 2024 Bjørn Erik Pedersen // SPDX-License-Identifier: MIT package lazycache import ( "sync" "github.com/hashicorp/golang-lru/v2/simplelru" ) // New creates a new Cache. func New[K comparable, V any](options Options[K, V]) *Cache[K, V] { var onEvict simplelru.EvictCallback[K, *valueWrapper[V]] = nil if options.OnEvict != nil { onEvict = func(key K, value *valueWrapper[V]) { value.wait() if value.found { options.OnEvict(key, value.value) } } } lru, err := simplelru.NewLRU[K, *valueWrapper[V]](int(options.MaxEntries), onEvict) if err != nil { panic(err) } c := &Cache[K, V]{ lru: lru, } return c } // Options holds the cache options. type Options[K comparable, V any] struct { // MaxEntries is the maximum number of entries that the cache should hold. // Note that this can also be adjusted after the cache is created with Resize. MaxEntries int // OnEvict is an optional callback that is called when an entry is evicted. OnEvict func(key K, value V) } // Cache is a thread-safe resizable LRU cache. type Cache[K comparable, V any] struct { lru *simplelru.LRU[K, *valueWrapper[V]] mu sync.RWMutex zerov V } // Delete deletes the item with given key from the cache, returning if the // key was contained. func (c *Cache[K, V]) Delete(key K) bool { c.mu.Lock() defer c.mu.Unlock() return c.lru.Remove(key) } // DeleteFunc deletes all entries for which the given function returns true. func (c *Cache[K, V]) DeleteFunc(matches func(key K, item V) bool) int { c.mu.RLock() keys := c.lru.Keys() var keysToDelete []K for _, key := range keys { w, _ := c.lru.Peek(key) if !w.wait().found { continue } if matches(key, w.value) { keysToDelete = append(keysToDelete, key) } } c.mu.RUnlock() c.mu.Lock() defer c.mu.Unlock() var deleteCount int for _, key := range keysToDelete { if c.lru.Remove(key) { deleteCount++ } } return deleteCount } // Get returns the value associated with key. func (c *Cache[K, V]) Get(key K) (V, bool) { c.mu.Lock() w := c.get(key) c.mu.Unlock() if w == nil { return c.zerov, false } w.wait() return w.value, w.found } // GetOrCreate returns the value associated with key, or creates it if it doesn't. // It also returns a bool indicating if the value was found in the cache. // Note that create, the cache prime function, is called once and then not called again for a given key // unless the cache entry is evicted; it does not block other goroutines from calling GetOrCreate, // it is not called with the cache lock held. // Note that any error returned by create will be returned by GetOrCreate and repeated calls with the same key will // receive the same error. func (c *Cache[K, V]) GetOrCreate(key K, create func(key K) (V, error)) (V, bool, error) { c.mu.Lock() w := c.get(key) if w != nil { c.mu.Unlock() w.wait() // If w.ready is nil, we will repeat any error from the create function to concurrent callers. return w.value, true, w.err } w = &valueWrapper[V]{ ready: make(chan struct{}), } // Concurrent access to the same key will see w, but needs to wait for w.ready // to get the value. c.lru.Add(key, w) c.mu.Unlock() isPanic := true v, err := func() (v V, err error) { defer func() { w.err = err w.value = v w.found = err == nil && !isPanic close(w.ready) if err != nil || isPanic { v = c.zerov // Only delete if this wrapper is still the one in the cache. // Another goroutine may have replaced it via Set() or GetOrCreate() // if our entry was evicted while create() was running. c.deleteIfSame(key, w) } }() // Create the value with the lock released. v, err = create(key) isPanic = false return }() return v, false, err } // Resize changes the cache size and returns the number of entries evicted. func (c *Cache[K, V]) Resize(size int) (evicted int) { c.mu.Lock() evicted = c.lru.Resize(size) c.mu.Unlock() return evicted } // Set associates value with key. func (c *Cache[K, V]) Set(key K, value V) { c.mu.Lock() c.lru.Add(key, &valueWrapper[V]{value: value, found: true}) c.mu.Unlock() } func (c *Cache[K, V]) get(key K) *valueWrapper[V] { w, ok := c.lru.Get(key) if !ok { return nil } return w } // deleteIfSame removes the entry for key only if it's still the same wrapper. // This prevents a failing GetOrCreate from deleting a newer entry that was // created by another goroutine (via Set or GetOrCreate) after eviction. func (c *Cache[K, V]) deleteIfSame(key K, w *valueWrapper[V]) { c.mu.Lock() defer c.mu.Unlock() if current, ok := c.lru.Peek(key); ok && current == w { c.lru.Remove(key) } } // contains returns true if the given key is in the cache. // note that this wil also return true if the key is in the cache but the value is not yet ready. func (c *Cache[K, V]) contains(key K) bool { c.mu.RLock() b := c.lru.Contains(key) c.mu.RUnlock() return b } // keys returns a slice of the keys in the cache, oldest first. // note that this wil also include keys that are not yet ready. func (c *Cache[K, V]) keys() []K { c.mu.RLock() defer c.mu.RUnlock() return c.lru.Keys() } // Len returns the number of items in the cache. // note that this wil also include values that are not yet ready. func (c *Cache[K, V]) Len() int { c.mu.RLock() defer c.mu.RUnlock() return c.lru.Len() } // valueWrapper holds a cache value that is not available unless the done channel is nil or closed. // This construct makes more sense if you look at the code in GetOrCreate. type valueWrapper[V any] struct { value V found bool err error ready chan struct{} } func (w *valueWrapper[V]) wait() *valueWrapper[V] { if w.ready != nil { <-w.ready } return w } bep-lazycache-6baa8c3/lazycache_test.go000066400000000000000000000443771513156302600202640ustar00rootroot00000000000000// Copyright 2024 Bjørn Erik Pedersen // SPDX-License-Identifier: MIT package lazycache import ( "errors" "fmt" "math/rand" "sync" "sync/atomic" "testing" "time" qt "github.com/frankban/quicktest" ) func TestCache(t *testing.T) { c := qt.New(t) cache := New[int, any](Options[int, any]{MaxEntries: 10}) get := func(key int) any { v, found := cache.Get(key) if !found { return nil } return v } c.Assert(get(123456), qt.IsNil) cache.Set(123456, 32) c.Assert(get(123456), qt.Equals, 32) for i := range 20 { cache.Set(i, i) } c.Assert(get(123456), qt.IsNil) c.Assert(cache.Resize(5), qt.Equals, 5) c.Assert(get(3), qt.IsNil) c.Assert(cache.contains(18), qt.IsTrue) c.Assert(cache.keys(), qt.DeepEquals, []int{15, 16, 17, 18, 19}) c.Assert(cache.DeleteFunc( func(key int, value any) bool { return value.(int) > 15 }, ), qt.Equals, 4) c.Assert(cache.contains(18), qt.IsFalse) c.Assert(cache.contains(15), qt.IsTrue) c.Assert(cache.Delete(15), qt.IsTrue) c.Assert(cache.Delete(15), qt.IsFalse) c.Assert(cache.contains(15), qt.IsFalse) c.Assert(cache.Len(), qt.Equals, 0) c.Assert(func() { New[int, any](Options[int, any]{MaxEntries: -1}) }, qt.PanicMatches, "must provide a positive size") } func TestPanic(t *testing.T) { c := qt.New(t) cache := New(Options[int, any]{MaxEntries: 1000}) ep := &errorProducer{} willPanic := func(i int) func() { return func() { cache.GetOrCreate(i, func(key int) (any, error) { ep.Panic(i) return nil, nil }) } } for i := range 2 { for range 2 { c.Assert(willPanic(i), qt.PanicMatches, fmt.Sprintf("failed-%d", i)) } } for i := range 2 { v, _, err := cache.GetOrCreate(i, func(key int) (any, error) { return key + 2, nil }) c.Assert(err, qt.IsNil) c.Assert(v, qt.Equals, i+2) } } func TestDeleteFunc(t *testing.T) { c := qt.New(t) c.Run("Basic", func(c *qt.C) { cache := New(Options[int, any]{MaxEntries: 1000}) for i := range 10 { cache.Set(i, i) } c.Assert(cache.DeleteFunc(func(key int, value any) bool { return key%2 == 0 }), qt.Equals, 5) c.Assert(cache.Len(), qt.Equals, 5) }) c.Run("Temporary", func(c *qt.C) { var wg sync.WaitGroup // There's some timing involved in this test, so we'll need // to retry a few times to cover all the cases. for range 100 { cache := New(Options[int, any]{MaxEntries: 1000}) for i := range 10 { cache.Set(i, i) } wg.Add(1) go func() { defer wg.Done() for i := 10; i < 30; i++ { v, _, err := cache.GetOrCreate(i, func(key int) (any, error) { if key%2 == 0 { return nil, errors.New("failed") } time.Sleep(10 * time.Microsecond) return key, nil }) if err != nil { c.Assert(err, qt.ErrorMatches, "failed") } else { c.Assert(v, qt.Equals, i) } } }() time.Sleep(3 * time.Microsecond) c.Assert(cache.DeleteFunc(func(key int, value any) bool { return key%2 == 0 }), qt.Equals, 5) } wg.Wait() }) } func TestGetOrCreate(t *testing.T) { c := qt.New(t) cache := New(Options[int, any]{MaxEntries: 100}) counter := 0 create := func(key int) (any, error) { counter++ return fmt.Sprintf("value-%d-%d", key, counter), nil } for i := range 3 { res, found, err := cache.GetOrCreate(123456, create) c.Assert(err, qt.IsNil) c.Assert(res, qt.Equals, "value-123456-1") c.Assert(found, qt.Equals, i > 0) } v, found := cache.Get(123456) c.Assert(found, qt.IsTrue) c.Assert(v, qt.Equals, "value-123456-1") } func TestGetOrCreateError(t *testing.T) { c := qt.New(t) cache := New(Options[int, any]{MaxEntries: 100}) create := func(key int) (any, error) { return nil, fmt.Errorf("failed") } res, _, err := cache.GetOrCreate(123456, create) c.Assert(err, qt.ErrorMatches, "failed") c.Assert(res, qt.IsNil) } func TestOnEvict(t *testing.T) { c := qt.New(t) var onEvictCalled bool cache := New(Options[int, any]{MaxEntries: 20, OnEvict: func(key int, value any) { onEvictCalled = true }}) create := func(key int) (any, error) { return key, nil } for i := range 25 { cache.GetOrCreate(i, create) } c.Assert(onEvictCalled, qt.IsTrue) } func TestGetOrCreateConcurrent(t *testing.T) { c := qt.New(t) cache := New(Options[int, any]{MaxEntries: 1000}) var countersmu sync.Mutex counters := make(map[int]int) create := func(key int) (any, error) { countersmu.Lock() count := counters[key] counters[key]++ countersmu.Unlock() time.Sleep(time.Duration(rand.Intn(40)+1) * time.Millisecond) return fmt.Sprintf("%v-%d", key, count), nil } var wg sync.WaitGroup for i := range 20 { i := i expect := fmt.Sprintf("%d-0", i) wg.Add(1) go func() { defer wg.Done() for range 12 { res, _, err := cache.GetOrCreate(i, create) c.Assert(err, qt.IsNil) c.Assert(res, qt.Equals, expect) } }() } for i := range 20 { i := i expect := fmt.Sprintf("%d-0", i) wg.Add(1) go func() { defer wg.Done() for range 12 { countersmu.Lock() _, created := counters[i] countersmu.Unlock() res, found := cache.Get(i) if !found { c.Assert(created, qt.IsFalse) } else { c.Assert(res, qt.Equals, expect) } } }() } wg.Wait() } func TestGetOrCreateAndResizeConcurrent(t *testing.T) { c := qt.New(t) cache := New(Options[int, int]{MaxEntries: 1000}) var counter atomic.Uint32 create := func(key int) (int, error) { time.Sleep(time.Duration(rand.Intn(40)+1) * time.Millisecond) counter.Add(1) return int(counter.Load()), nil } var wg sync.WaitGroup for i := range 100 { i := i wg.Add(1) go func() { defer wg.Done() for range 12 { v, _, err := cache.GetOrCreate(i, create) c.Assert(err, qt.IsNil) c.Assert(v, qt.Not(qt.Equals), 0) } }() } wg.Add(1) go func() { defer wg.Done() for i := 100; i >= 0; i-- { cache.Resize(i) time.Sleep(10 * time.Millisecond) } }() wg.Wait() } func TestGetOrCreateRecursive(t *testing.T) { c := qt.New(t) var wg sync.WaitGroup n := 200 for range 30 { cache := New(Options[int, any]{MaxEntries: 1000}) for range 10 { wg.Add(1) go func() { defer wg.Done() for range 10 { // This test was added to test a deadlock situation with nested GetOrCreate calls on the same cache. // Note that the keys below are carefully selected to not overlap, as this case may still deadlock: // goroutine 1: GetOrCreate(1) => GetOrCreate(2) // goroutine 2: GetOrCreate(2) => GetOrCreate(1) key1, key2 := rand.Intn(n), rand.Intn(n)+n if key2 == key1 { key2++ } shouldFail := key1%10 == 0 v, _, err := cache.GetOrCreate(key1, func(key int) (any, error) { if shouldFail { return nil, fmt.Errorf("failed") } v, _, err := cache.GetOrCreate(key2, func(key int) (any, error) { return "inner", nil }) c.Assert(err, qt.IsNil) return v, nil }) if shouldFail { c.Assert(err, qt.ErrorMatches, "failed") c.Assert(v, qt.IsNil) } else { c.Assert(err, qt.IsNil) c.Assert(v, qt.Equals, "inner") } } }() } wg.Wait() } } // TestGetOrCreateEvictionRace tests the scenario where an entry is evicted // while its create function is still running, and a new entry for the same key // is created. The failing original create() should NOT delete the new entry. func TestGetOrCreateEvictionRace(t *testing.T) { c := qt.New(t) // Use a small cache to force evictions cache := New(Options[int, string]{MaxEntries: 2}) var ( wg sync.WaitGroup key1Started = make(chan struct{}) key1Continue = make(chan struct{}) key1Done = make(chan struct{}) key1NewCreated = make(chan struct{}) ) // Goroutine 1: Start creating key 1, but wait before completing wg.Add(1) go func() { defer wg.Done() v, _, err := cache.GetOrCreate(1, func(key int) (string, error) { close(key1Started) // Signal that we've started <-key1Continue // Wait for signal to continue return "", errors.New("intentional failure") }) c.Assert(err, qt.ErrorMatches, "intentional failure") c.Assert(v, qt.Equals, "") close(key1Done) }() // Wait for goroutine 1 to start creating <-key1Started // Fill the cache to evict key 1's pending entry cache.Set(2, "value2") cache.Set(3, "value3") // This should evict key 1 // Now create a new entry for key 1 that will succeed wg.Add(1) go func() { defer wg.Done() v, _, err := cache.GetOrCreate(1, func(key int) (string, error) { return "new-value-1", nil }) c.Assert(err, qt.IsNil) c.Assert(v, qt.Equals, "new-value-1") close(key1NewCreated) }() // Wait for the new entry to be created before letting the old one fail <-key1NewCreated // Now let goroutine 1 complete (with failure) - this will call Delete(1) close(key1Continue) <-key1Done wg.Wait() // The new entry for key 1 should still exist because deleteIfSame() // only deletes if the wrapper is the same instance. v, found := cache.Get(1) c.Assert(found, qt.IsTrue, qt.Commentf("new entry should not be deleted by failing old create")) c.Assert(v, qt.Equals, "new-value-1") } // TestSetDuringGetOrCreate tests that Set() during a failing GetOrCreate // preserves the Set() value. The failing create should NOT delete the newer entry. func TestSetDuringGetOrCreate(t *testing.T) { c := qt.New(t) cache := New(Options[int, string]{MaxEntries: 100}) var ( createStarted = make(chan struct{}) createContinue = make(chan struct{}) wg sync.WaitGroup ) // Start a GetOrCreate that will fail wg.Add(1) go func() { defer wg.Done() _, _, err := cache.GetOrCreate(1, func(key int) (string, error) { close(createStarted) <-createContinue return "", errors.New("intentional failure") }) c.Assert(err, qt.ErrorMatches, "intentional failure") }() <-createStarted // While create is running, Set a value for the same key cache.Set(1, "set-value") // Let the create continue and fail close(createContinue) wg.Wait() // The Set value should still exist because deleteIfSame() only // deletes if the wrapper is the same instance. v, found := cache.Get(1) c.Assert(found, qt.IsTrue, qt.Commentf("Set() value should not be deleted by failing GetOrCreate")) c.Assert(v, qt.Equals, "set-value") } // TestOnEvictWithPendingEntry tests that OnEvict correctly waits for // entries that are still being created. func TestOnEvictWithPendingEntry(t *testing.T) { c := qt.New(t) var ( evictedKeys []int evictedValues []string evictMu sync.Mutex createStarted = make(chan struct{}) ) cache := New(Options[int, string]{ MaxEntries: 2, OnEvict: func(key int, value string) { evictMu.Lock() evictedKeys = append(evictedKeys, key) evictedValues = append(evictedValues, value) evictMu.Unlock() }, }) var wg sync.WaitGroup // Start creating key 1 with a slow create function wg.Add(1) go func() { defer wg.Done() v, _, err := cache.GetOrCreate(1, func(key int) (string, error) { close(createStarted) time.Sleep(50 * time.Millisecond) return "value1", nil }) c.Assert(err, qt.IsNil) c.Assert(v, qt.Equals, "value1") }() <-createStarted // Add entries to evict key 1 cache.Set(2, "value2") cache.Set(3, "value3") // This should trigger eviction of key 1 wg.Wait() // Give time for eviction callback to complete time.Sleep(100 * time.Millisecond) evictMu.Lock() // Key 1 should have been evicted with its final value (after create completed) foundKey1 := false for i, k := range evictedKeys { if k == 1 { foundKey1 = true c.Assert(evictedValues[i], qt.Equals, "value1") } } evictMu.Unlock() // Note: foundKey1 might be false if the eviction timing differs, // which is acceptable as long as no panic/race occurred _ = foundKey1 } // TestGetOrCreateConcurrentErrors tests that multiple goroutines calling // GetOrCreate for the same key all receive the same error when create fails. func TestGetOrCreateConcurrentErrors(t *testing.T) { c := qt.New(t) cache := New(Options[int, string]{MaxEntries: 100}) var ( wg sync.WaitGroup createCount atomic.Int32 ) // Multiple goroutines try to get/create the same failing key for range 10 { wg.Add(1) go func() { defer wg.Done() _, _, err := cache.GetOrCreate(1, func(key int) (string, error) { createCount.Add(1) time.Sleep(10 * time.Millisecond) return "", errors.New("create failed") }) c.Assert(err, qt.ErrorMatches, "create failed") }() } wg.Wait() // The create function should only be called once // (subsequent callers wait on the first one and get the same error) c.Assert(createCount.Load(), qt.Equals, int32(1)) } // TestDeleteFuncConcurrentCreate tests DeleteFunc behavior when entries // are being created concurrently. func TestDeleteFuncConcurrentCreate(t *testing.T) { for range 50 { // Run multiple times to catch race conditions cache := New(Options[int, int]{MaxEntries: 100}) // Pre-populate with some entries for i := range 10 { cache.Set(i, i) } var wg sync.WaitGroup // Concurrently create new entries wg.Add(1) go func() { defer wg.Done() for i := 10; i < 20; i++ { cache.GetOrCreate(i, func(key int) (int, error) { time.Sleep(time.Microsecond) return key * 2, nil }) } }() // Concurrently delete entries matching a condition wg.Add(1) go func() { defer wg.Done() cache.DeleteFunc(func(key int, value int) bool { return key%2 == 0 }) }() wg.Wait() } // Success means no panics or races occurred } // TestResizeToZero tests behavior when resizing cache to zero. func TestResizeToZero(t *testing.T) { c := qt.New(t) cache := New(Options[int, int]{MaxEntries: 10}) for i := range 5 { cache.Set(i, i) } evicted := cache.Resize(0) c.Assert(evicted, qt.Equals, 5) c.Assert(cache.Len(), qt.Equals, 0) // Should still work after resize to 0 cache.Resize(10) cache.Set(1, 1) v, found := cache.Get(1) c.Assert(found, qt.IsTrue) c.Assert(v, qt.Equals, 1) } // TestGetOrCreatePanicRecovery tests that after a panic, the same key // can be created again successfully. func TestGetOrCreatePanicRecovery(t *testing.T) { c := qt.New(t) cache := New(Options[int, string]{MaxEntries: 100}) // First call panics c.Assert(func() { cache.GetOrCreate(1, func(key int) (string, error) { panic("intentional panic") }) }, qt.PanicMatches, "intentional panic") // Entry should be removed, so a new create should work v, found, err := cache.GetOrCreate(1, func(key int) (string, error) { return "recovered", nil }) c.Assert(err, qt.IsNil) c.Assert(found, qt.IsFalse) // Not found because previous entry was deleted c.Assert(v, qt.Equals, "recovered") } // TestConcurrentGetAndGetOrCreate tests concurrent Get and GetOrCreate // operations on the same keys. func TestConcurrentGetAndGetOrCreate(t *testing.T) { c := qt.New(t) cache := New(Options[int, int]{MaxEntries: 100}) var wg sync.WaitGroup // GetOrCreate goroutines for i := range 10 { wg.Add(1) go func() { defer wg.Done() for j := range 20 { key := (i + j) % 15 v, _, err := cache.GetOrCreate(key, func(k int) (int, error) { time.Sleep(time.Microsecond) return k * 10, nil }) c.Assert(err, qt.IsNil) c.Assert(v, qt.Equals, key*10) } }() } // Get goroutines (may or may not find entries) for range 10 { wg.Add(1) go func() { defer wg.Done() for j := range 20 { key := j % 15 v, found := cache.Get(key) if found { c.Assert(v, qt.Equals, key*10) } } }() } wg.Wait() } func BenchmarkGetOrCreateAndGet(b *testing.B) { const maxSize = 1000 cache := New(Options[int, any]{MaxEntries: maxSize}) r := rand.New(rand.NewSource(99)) var mu sync.Mutex // Partially fill the cache. for i := range maxSize / 2 { cache.Set(i, i) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { b.ResetTimer() for pb.Next() { mu.Lock() i1, i2 := r.Intn(maxSize), r.Intn(maxSize) mu.Unlock() // Just Get the value. v, found := cache.Get(i1) if found && v != i1 { b.Fatalf("got %v, want %v", v, i1) } res2, _, err := cache.GetOrCreate(i2, func(key int) (any, error) { if i2%100 == 0 { // Simulate a slow create. time.Sleep(1 * time.Second) } return i2, nil }) if err != nil { b.Fatal(err) } if v := res2; v != i2 { b.Fatalf("got %v, want %v", v, i2) } } }) } func BenchmarkGetOrCreate(b *testing.B) { const maxSize = 1000 r := rand.New(rand.NewSource(99)) var mu sync.Mutex cache := New(Options[int, any]{MaxEntries: maxSize}) // Partially fill the cache. for i := range maxSize / 3 { cache.Set(i, i) } b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { mu.Lock() key := r.Intn(maxSize) mu.Unlock() v, _, err := cache.GetOrCreate(key, func(int) (any, error) { if key%100 == 0 { // Simulate a slow create. time.Sleep(1 * time.Second) } return key, nil }) if err != nil { b.Fatal(err) } if v != key { b.Fatalf("got %v, want %v", v, key) } } }) } func BenchmarkCacheSerial(b *testing.B) { const maxSize = 1000 b.Run("Set", func(b *testing.B) { cache := New(Options[int, any]{MaxEntries: maxSize}) for i := 0; i < b.N; i++ { cache.Set(i, i) } }) b.Run("Get", func(b *testing.B) { cache := New(Options[int, any]{MaxEntries: maxSize}) numItems := maxSize - 200 for i := range numItems { cache.Set(i, i) } b.ResetTimer() for i := 0; i < b.N; i++ { key := i % numItems _, found := cache.Get(key) if !found { b.Fatalf("unexpected nil value for key %d", key) } } }) } func BenchmarkCacheParallel(b *testing.B) { const maxSize = 1000 b.Run("Set", func(b *testing.B) { cache := New(Options[int, any]{MaxEntries: maxSize}) var counter uint32 b.RunParallel(func(pb *testing.PB) { for pb.Next() { i := int(atomic.AddUint32(&counter, 1)) cache.Set(i, i) } }) }) b.Run("Get", func(b *testing.B) { cache := New(Options[int, any]{MaxEntries: maxSize}) r := rand.New(rand.NewSource(99)) var mu sync.Mutex numItems := maxSize - 200 for i := range numItems { cache.Set(i, i) } b.RunParallel(func(pb *testing.PB) { for pb.Next() { mu.Lock() key := r.Intn(numItems) mu.Unlock() _, found := cache.Get(key) if !found { b.Fatalf("unexpected nil value for key %d", key) } } }) }) } type errorProducer struct{} func (e *errorProducer) Panic(i int) { panic(fmt.Sprintf("failed-%d", i)) }