package platform import ( "context" "errors" "sync" "sync/atomic" "testing" "time" ) func TestReadCacheCoalescesBurstAndCoolsFailures(t *testing.T) { for _, fail := range []bool{false, true} { cache := NewBoundedReadCache[string, int](4, 1024, 2, time.Minute) var calls atomic.Int32 loadErr := errors.New("database unavailable") load := func(context.Context) (int, int, error) { calls.Add(1) if fail { return 0, 0, loadErr } return 42, 256, nil } var wg sync.WaitGroup for range 100 { wg.Add(1) go func() { defer wg.Done() value, err := cache.Get(context.Background(), "hot", load) if (fail && !errors.Is(err, loadErr)) || (!fail && (err != nil || value != 42)) { t.Errorf("value=%d error=%v fail=%v", value, err, fail) } }() } wg.Wait() if calls.Load() != 1 { t.Fatalf("burst caused %d loads (fail=%v)", calls.Load(), fail) } } } func TestReadCacheBoundsLoadsAndInvalidation(t *testing.T) { cache := NewBoundedReadCache[string, int](2, 512, 1, time.Minute) started, release, done := make(chan struct{}), make(chan struct{}), make(chan struct{}) go func() { defer close(done) _, _ = cache.Get(context.Background(), "one", func(context.Context) (int, int, error) { close(started) <-release return 1, 256, nil }) }() <-started load := func(context.Context) (int, int, error) { return 2, 256, nil } if _, err := cache.Get(context.Background(), "two", load); !errors.Is(err, ErrReadCacheBusy) { t.Fatalf("unbounded distinct load: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() if _, err := cache.Get(ctx, "one", load); !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("waiting reader did not cancel: %v", err) } cache.Clear() if _, err := cache.Get(context.Background(), "two", load); !errors.Is(err, ErrReadCacheBusy) { t.Fatalf("invalidation bypassed in-flight limit: %v", err) } close(release) <-done value, err := cache.Get(context.Background(), "one", load) if err != nil || value != 2 { t.Fatalf("in-flight load repopulated invalidated cache: %d %v", value, err) } } func TestReadCacheBudgetsExpiryAndNoStaleSuccess(t *testing.T) { cache := NewBoundedReadCache[int, int](3, 512, 1, time.Minute) load := func(context.Context) (int, int, error) { return 1, 256, nil } for i := range 100 { if _, err := cache.Get(context.Background(), i, load); err != nil { t.Fatal(err) } } if len(cache.entries) > 2 || cache.bytes > 512 { t.Fatalf("budget exceeded: entries=%d bytes=%d", len(cache.entries), cache.bytes) } cache.entries[99].expires = time.Now().Add(-time.Second) wantErr := errors.New("load failed") value, err := cache.Get(context.Background(), 99, func(context.Context) (int, int, error) { return 0, 0, wantErr }) if value != 0 || !errors.Is(err, wantErr) { t.Fatalf("stale success served: %d %v", value, err) } var calls int for range 2 { value, err = cache.Get(context.Background(), 101, func(context.Context) (int, int, error) { calls++ return 42, 1024, nil }) if value != 42 || err != nil { t.Fatalf("oversized load should still return to caller: %d %v", value, err) } } if calls != 2 { t.Fatal("oversized entry was retained") } } func TestReadCacheCancelledLoadCanBeRetried(t *testing.T) { cache := NewBoundedReadCache[string, int](1, 512, 1, time.Minute) _, err := cache.Get(context.Background(), "key", func(context.Context) (int, int, error) { return 0, 0, context.Canceled }) if !errors.Is(err, context.Canceled) { t.Fatal(err) } value, err := cache.Get(context.Background(), "key", func(context.Context) (int, int, error) { return 7, 256, nil }) if err != nil || value != 7 { t.Fatalf("cancellation poisoned cache: %d %v", value, err) } } func TestReadCachePanicDoesNotLeakCapacity(t *testing.T) { cache := NewBoundedReadCache[string, int](1, 512, 1, time.Minute) func() { defer func() { if recover() != "loader panic" { t.Error("original panic was swallowed") } }() _, _ = cache.Get(context.Background(), "key", func(context.Context) (int, int, error) { panic("loader panic") }) }() if cache.loads != 0 || len(cache.entries) != 0 { t.Fatal("panic leaked load capacity") } if _, err := cache.Get(context.Background(), "key", func(context.Context) (int, int, error) { return 1, 256, nil }); err != nil { t.Fatal(err) } }