merged from gramsrv upstream

This commit is contained in:
onysd 2026-09-01 12:06:31 +03:00
parent 79c64ee916
commit 21a0856587
651 changed files with 54774 additions and 4590 deletions

View file

@ -3,6 +3,7 @@ package readmodelcache
import (
"context"
"fmt"
"reflect"
"sync"
"sync/atomic"
"testing"
@ -81,6 +82,28 @@ func TestGetOrLoadSingleflightsConcurrentMiss(t *testing.T) {
}
}
func TestInvalidateWhereValueUsesImmutableDependency(t *testing.T) {
type value struct{ channels []int64 }
c := New[int, value](Config[int, value]{MaxEntries: 4})
c.Store(1, value{channels: []int64{7, 8}})
c.Store(2, value{channels: []int64{9}})
c.InvalidateWhereValue(func(_ int, v value) bool {
for _, id := range v.channels {
if id == 8 {
return true
}
}
return false
})
if _, ok := c.Peek(1); ok {
t.Fatal("dependency match remained cached")
}
if got, ok := c.Peek(2); !ok || len(got.channels) != 1 || got.channels[0] != 9 {
t.Fatalf("unrelated value = %+v,%v, want cached channel 9", got, ok)
}
}
// TestEpochGuardRejectsStaleWriteback 证明 epoch 守卫堵住 lost-update:一次锁外 load
// 期间到达的 Invalidate 不得被这次 load 的(已陈旧)结果覆盖;在飞读者最终拿到的是
// 失效后重载的新值,且缓存未被陈旧值污染。
@ -178,6 +201,76 @@ func TestLRUTouchOnGet(t *testing.T) {
}
}
func TestWeightedLRUEvictsByTotalWeightAndSkipsOversize(t *testing.T) {
ctx := context.Background()
c := New[int, int](Config[int, int]{
MaxEntries: 10,
MaxWeight: 5,
Weight: func(v int) int64 { return int64(v) },
})
mustLoad(t, c, 1, 2)
mustLoad(t, c, 2, 2)
mustLoad(t, c, 3, 3)
if _, ok := c.Peek(1); ok {
t.Fatal("oldest entry should be evicted when aggregate weight exceeds five")
}
for _, key := range []int{2, 3} {
if _, ok := c.Peek(key); !ok {
t.Fatalf("weighted LRU lost retained key %d", key)
}
}
loads := 0
loadOversize := func() (int, error) { loads++; return 6, nil }
if v, err := c.GetOrLoad(ctx, 4, loadOversize); err != nil || v != 6 {
t.Fatalf("oversize first load = %d,%v", v, err)
}
if v, err := c.GetOrLoad(ctx, 4, loadOversize); err != nil || v != 6 {
t.Fatalf("oversize second load = %d,%v", v, err)
}
if loads != 2 {
t.Fatalf("oversize value unexpectedly retained: loads=%d, want 2", loads)
}
if _, ok := c.Peek(4); ok {
t.Fatal("single value above MaxWeight must not remain cached")
}
}
func TestLifecycleCallbacksCoverReplaceEvictAndFlush(t *testing.T) {
type event struct {
op string
key int
value string
}
var events []event
c := New[int, string](Config[int, string]{
MaxEntries: 2,
OnStore: func(key int, value string) {
events = append(events, event{op: "store", key: key, value: value})
},
OnRemove: func(key int, value string) {
events = append(events, event{op: "remove", key: key, value: value})
},
})
c.Store(1, "a")
c.Store(1, "b")
c.Store(2, "c")
c.Store(3, "d")
c.Flush()
want := []event{
{op: "store", key: 1, value: "a"},
{op: "remove", key: 1, value: "a"},
{op: "store", key: 1, value: "b"},
{op: "store", key: 2, value: "c"},
{op: "store", key: 3, value: "d"},
{op: "remove", key: 1, value: "b"},
{op: "remove", key: 3, value: "d"},
{op: "remove", key: 2, value: "c"},
}
if !reflect.DeepEqual(events, want) {
t.Fatalf("lifecycle events = %#v, want %#v", events, want)
}
}
func TestVersionGateReloadsOnHashChange(t *testing.T) {
ctx := context.Background()
c := New[int, string](Config[int, string]{MaxEntries: 16})
@ -389,6 +482,74 @@ func TestGetOrLoadBatchCachesHitsMissesAndNegatives(t *testing.T) {
}
}
func TestGetOrLoadBatchCoalescesOverlappingConcurrentMissesPerKey(t *testing.T) {
ctx := context.Background()
c := New[int, batchVal](Config[int, batchVal]{MaxEntries: 64})
noVersion := func(int) (int64, bool) { return 0, true }
firstStarted := make(chan struct{})
secondLoaded := make(chan struct{})
releaseFirst := make(chan struct{})
var calls atomic.Int32
var mu sync.Mutex
loadedKeys := make(map[int]int)
load := func(_ context.Context, missing []int) (map[int]batchVal, error) {
call := calls.Add(1)
mu.Lock()
for _, key := range missing {
loadedKeys[key]++
}
mu.Unlock()
if call == 1 {
close(firstStarted)
<-releaseFirst
} else {
close(secondLoaded)
}
out := make(map[int]batchVal, len(missing))
for _, key := range missing {
out[key] = batchVal{n: key * 10, found: true}
}
return out, nil
}
firstResult := make(chan map[int]batchVal, 1)
firstErr := make(chan error, 1)
go func() {
got, err := c.GetOrLoadBatch(ctx, []int{1, 2, 3}, noVersion, load)
firstResult <- got
firstErr <- err
}()
<-firstStarted
secondResult := make(chan map[int]batchVal, 1)
secondErr := make(chan error, 1)
go func() {
got, err := c.GetOrLoadBatch(ctx, []int{2, 3, 4}, noVersion, load)
secondResult <- got
secondErr <- err
}()
<-secondLoaded
close(releaseFirst)
first, second := <-firstResult, <-secondResult
if err := <-firstErr; err != nil {
t.Fatal(err)
}
if err := <-secondErr; err != nil {
t.Fatal(err)
}
if first[1].n != 10 || first[2].n != 20 || first[3].n != 30 ||
second[2].n != 20 || second[3].n != 30 || second[4].n != 40 {
t.Fatalf("overlapping results first=%+v second=%+v", first, second)
}
mu.Lock()
defer mu.Unlock()
if calls.Load() != 2 || loadedKeys[1] != 1 || loadedKeys[2] != 1 || loadedKeys[3] != 1 || loadedKeys[4] != 1 {
t.Fatalf("backend calls=%d loaded=%v, want two batches and every key exactly once", calls.Load(), loadedKeys)
}
}
func TestGetOrLoadBatchVersionGateReloadsOnHashChange(t *testing.T) {
ctx := context.Background()
c := New[int, batchVal](Config[int, batchVal]{MaxEntries: 64})