refactor(*): 平台架构 - 拆分后端领域模块并同步插件协议与模型能力
This commit is contained in:
135
backend/internal/platform/read_cache_test.go
Normal file
135
backend/internal/platform/read_cache_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user