Files
go-omni/cache/cache_test.go

119 lines
3.3 KiB
Go

package cache
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
)
func TestMemoryOwnershipCapacityExpiryAndClose(t *testing.T) {
m, err := NewMemory(MemoryConfig{MaxEntries: 2, MaxBytes: 10})
if err != nil {
t.Fatal(err)
}
now := time.Now()
m.now = func() time.Time { return now }
ctx := context.Background()
value := []byte("aa")
if err := m.Set(ctx, "a", value, time.Second); err != nil {
t.Fatal(err)
}
value[0] = 'x'
got, _ := m.Get(ctx, "a")
if string(got) != "aa" {
t.Fatal("retained caller memory")
}
got[0] = 'z'
got, _ = m.Get(ctx, "a")
if string(got) != "aa" {
t.Fatal("returned mutable cache memory")
}
m.Set(ctx, "b", []byte("bb"), 0)
m.Get(ctx, "a")
m.Set(ctx, "c", []byte("cc"), 0)
if _, err := m.Get(ctx, "b"); !errors.Is(err, ErrMiss) {
t.Fatal("LRU eviction failed")
}
now = now.Add(time.Second)
if _, err := m.Get(ctx, "a"); !errors.Is(err, ErrMiss) {
t.Fatal("expiration boundary ignored")
}
if err := m.Set(ctx, "large", []byte("123456"), 0); !errors.Is(err, ErrTooLarge) {
t.Fatal(err)
}
if _, err := m.Get(ctx, "c"); err != nil {
t.Fatal("oversized write evicted valid data")
}
canceled, cancel := context.WithCancel(ctx)
cancel()
if err := m.Delete(canceled, "c"); !errors.Is(err, context.Canceled) {
t.Fatal(err)
}
m.Close()
m.Close()
if _, err := m.Get(ctx, "c"); !errors.Is(err, ErrClosed) {
t.Fatal(err)
}
}
func TestLoaderCoalescesAndSeparatesCallerCancellation(t *testing.T) {
m, _ := NewMemory(MemoryConfig{})
loader, _ := NewLoader(m, LoaderConfig{MaxLoads: 1, Timeout: time.Second})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
started, release := make(chan struct{}), make(chan struct{})
var calls atomic.Int32
load := func(ctx context.Context) ([]byte, error) {
calls.Add(1)
close(started)
select {
case <-release:
return []byte("value"), nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
first := make(chan error, 1)
go func() { _, err := loader.GetOrLoad(ctx, "key", time.Minute, load); first <- err }()
<-started
second := make(chan error, 1)
go func() {
v, err := loader.GetOrLoad(context.Background(), "key", time.Minute, load)
if err == nil && string(v) != "value" {
err = errors.New("wrong value")
}
second <- err
}()
if _, err := loader.GetOrLoad(context.Background(), "other", 0, load); !errors.Is(err, ErrBusy) {
t.Fatal(err)
}
cancel()
if err := <-first; !errors.Is(err, context.Canceled) {
t.Fatal(err)
}
close(release)
if err := <-second; err != nil {
t.Fatal(err)
}
if calls.Load() != 1 {
t.Fatalf("loads=%d", calls.Load())
}
}
func TestLoaderPanicDeadlineAndStoreFailure(t *testing.T) {
m, _ := NewMemory(MemoryConfig{})
l, _ := NewLoader(m, LoaderConfig{Timeout: 20 * time.Millisecond})
if _, err := l.GetOrLoad(context.Background(), "panic", 0, func(context.Context) ([]byte, error) { panic("failure") }); err == nil {
t.Fatal("panic escaped")
}
if _, err := l.GetOrLoad(context.Background(), "panic", 0, func(ctx context.Context) ([]byte, error) { <-ctx.Done(); return nil, ctx.Err() }); !errors.Is(err, context.DeadlineExceeded) {
t.Fatal(err)
}
m.Close()
if _, err := l.GetOrLoad(context.Background(), "closed", 0, func(context.Context) ([]byte, error) { t.Fatal("store outage triggered load"); return nil, nil }); !errors.Is(err, ErrClosed) {
t.Fatal(err)
}
}