chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
243
internal/store/read_model_cache_test.go
Normal file
243
internal/store/read_model_cache_test.go
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
type blockingReadModelVersionStore struct {
|
||||
calls atomic.Int32
|
||||
once sync.Once
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
hashes map[ReadModelKey]int64
|
||||
mu sync.Mutex
|
||||
loaded map[ReadModelKey]int
|
||||
}
|
||||
|
||||
func (s *blockingReadModelVersionStore) ReadModelHash(ctx context.Context, model string, ownerUserID int64, peerType domain.PeerType, peerID int64) (int64, bool, error) {
|
||||
rows, err := s.ReadModelHashes(ctx, []ReadModelKey{{Model: model, OwnerUserID: ownerUserID, PeerType: peerType, PeerID: peerID}})
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
key := ReadModelKey{Model: model, OwnerUserID: ownerUserID, PeerType: peerType, PeerID: peerID}
|
||||
hash := rows[key]
|
||||
return hash, hash != 0, nil
|
||||
}
|
||||
|
||||
func (s *blockingReadModelVersionStore) ReadModelHashes(ctx context.Context, keys []ReadModelKey) (map[ReadModelKey]int64, error) {
|
||||
s.calls.Add(1)
|
||||
s.once.Do(func() { close(s.started) })
|
||||
select {
|
||||
case <-s.release:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
out := make(map[ReadModelKey]int64, len(keys))
|
||||
s.mu.Lock()
|
||||
if s.loaded == nil {
|
||||
s.loaded = make(map[ReadModelKey]int)
|
||||
}
|
||||
for _, key := range keys {
|
||||
s.loaded[key]++
|
||||
if hash := s.hashes[key]; hash != 0 {
|
||||
out[key] = hash
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func TestCachedReadModelVersionStoreSingleflightsBatchMiss(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
keys := []ReadModelKey{
|
||||
{Model: "channel_base", OwnerUserID: 0, PeerType: domain.PeerTypeChannel, PeerID: 10},
|
||||
{Model: "channel_member", OwnerUserID: 100, PeerType: domain.PeerTypeChannel, PeerID: 10},
|
||||
}
|
||||
base := &blockingReadModelVersionStore{
|
||||
started: make(chan struct{}),
|
||||
release: make(chan struct{}),
|
||||
hashes: map[ReadModelKey]int64{
|
||||
keys[0]: 11,
|
||||
keys[1]: 22,
|
||||
},
|
||||
}
|
||||
cache := NewCachedReadModelVersionStore(base, time.Hour, 100)
|
||||
|
||||
const goroutines = 16
|
||||
errs := make(chan error, goroutines)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(goroutines)
|
||||
for i := 0; i < goroutines; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
rows, err := cache.ReadModelHashes(ctx, keys)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
if rows[keys[0]] != 11 || rows[keys[1]] != 22 {
|
||||
errs <- fmt.Errorf("hashes = %+v, want 11/22", rows)
|
||||
}
|
||||
}()
|
||||
}
|
||||
<-base.started
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
close(base.release)
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if got := base.calls.Load(); got != 1 {
|
||||
t.Fatalf("base ReadModelHashes calls = %d, want 1", got)
|
||||
}
|
||||
if _, err := cache.ReadModelHashes(ctx, keys); err != nil {
|
||||
t.Fatalf("cached ReadModelHashes: %v", err)
|
||||
}
|
||||
if got := base.calls.Load(); got != 1 {
|
||||
t.Fatalf("cache hit called base again: calls=%d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedReadModelVersionStoreWaitsOverlappingBatchKeys(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
a := ReadModelKey{Model: "channel_base", OwnerUserID: 0, PeerType: domain.PeerTypeChannel, PeerID: 10}
|
||||
b := ReadModelKey{Model: "channel_member", OwnerUserID: 100, PeerType: domain.PeerTypeChannel, PeerID: 10}
|
||||
c := ReadModelKey{Model: "dialog_light", OwnerUserID: 100, PeerType: domain.PeerTypeChannel, PeerID: 10}
|
||||
base := &blockingReadModelVersionStore{
|
||||
started: make(chan struct{}),
|
||||
release: make(chan struct{}),
|
||||
hashes: map[ReadModelKey]int64{
|
||||
a: 11,
|
||||
b: 22,
|
||||
c: 33,
|
||||
},
|
||||
}
|
||||
cache := NewCachedReadModelVersionStore(base, time.Hour, 100)
|
||||
|
||||
firstErr := make(chan error, 1)
|
||||
go func() {
|
||||
rows, err := cache.ReadModelHashes(ctx, []ReadModelKey{a, b})
|
||||
if err == nil && (rows[a] != 11 || rows[b] != 22) {
|
||||
err = fmt.Errorf("first hashes = %+v, want A/B", rows)
|
||||
}
|
||||
firstErr <- err
|
||||
}()
|
||||
<-base.started
|
||||
|
||||
secondErr := make(chan error, 1)
|
||||
go func() {
|
||||
rows, err := cache.ReadModelHashes(ctx, []ReadModelKey{b, c})
|
||||
if err == nil && (rows[b] != 22 || rows[c] != 33) {
|
||||
err = fmt.Errorf("second hashes = %+v, want B/C", rows)
|
||||
}
|
||||
secondErr <- err
|
||||
}()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
close(base.release)
|
||||
|
||||
if err := <-firstErr; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := <-secondErr; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
base.mu.Lock()
|
||||
loadedB := base.loaded[b]
|
||||
loadedC := base.loaded[c]
|
||||
base.mu.Unlock()
|
||||
if loadedB != 1 {
|
||||
t.Fatalf("overlap key B loaded %d times, want 1", loadedB)
|
||||
}
|
||||
if loadedC != 1 {
|
||||
t.Fatalf("owned key C loaded %d times, want 1", loadedC)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCachedReadModelVersionStoreEpochGuardRejectsStaleWriteback 证明 epoch 守卫堵住了
|
||||
// lost-update:一次锁外 DB load 期间到达的 NOTIFY(写入新 hash)不得被 load 返回的旧 hash 覆盖。
|
||||
// 这是 4 个 hash-only 消费缓存(private/channel media counts、participants、active ids)正确性的根。
|
||||
func TestCachedReadModelVersionStoreEpochGuardRejectsStaleWriteback(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
key := ReadModelKey{Model: "private_media_counts", OwnerUserID: 100, PeerType: domain.PeerTypeUser, PeerID: 100}
|
||||
base := &blockingReadModelVersionStore{
|
||||
started: make(chan struct{}),
|
||||
release: make(chan struct{}),
|
||||
hashes: map[ReadModelKey]int64{key: 11}, // DB 此刻仍是旧 hash 11
|
||||
}
|
||||
cache := NewCachedReadModelVersionStore(base, time.Hour, 100)
|
||||
|
||||
resultCh := make(chan map[ReadModelKey]int64, 1)
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
rows, err := cache.ReadModelHashes(ctx, []ReadModelKey{key})
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
resultCh <- rows
|
||||
}()
|
||||
|
||||
// 等到 load 已 claim inflight 并阻塞在 base.ReadModelHashes 内。
|
||||
<-base.started
|
||||
// 模拟 NOTIFY 在 load 期间送来权威新 hash 99(写入并自增 epoch)。
|
||||
cache.UpdateReadModelHash(key, 99)
|
||||
// 放行 load:它会返回旧 hash 11;epoch 守卫必须拒绝用 11 覆盖 99。
|
||||
close(base.release)
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
t.Fatalf("ReadModelHashes: %v", err)
|
||||
case rows := <-resultCh:
|
||||
if rows[key] != 99 {
|
||||
t.Fatalf("in-flight reader got hash %d, want fresh 99 (stale 11 must not win)", rows[key])
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timeout waiting for in-flight reader")
|
||||
}
|
||||
|
||||
// 后续读仍须是 99,且不再打 base —— 证明缓存没被旧值污染、也没被错误驱逐。
|
||||
callsBefore := base.calls.Load()
|
||||
rows, err := cache.ReadModelHashes(ctx, []ReadModelKey{key})
|
||||
if err != nil {
|
||||
t.Fatalf("second ReadModelHashes: %v", err)
|
||||
}
|
||||
if rows[key] != 99 {
|
||||
t.Fatalf("cached hash = %d, want 99 (cache poisoned by stale 11)", rows[key])
|
||||
}
|
||||
if got := base.calls.Load(); got != callsBefore {
|
||||
t.Fatalf("second read hit base (calls %d -> %d): cache was poisoned or evicted", callsBefore, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedReadModelVersionStoreUpdateReadModelHashWarmsCache(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
key := ReadModelKey{Model: "channel_base", PeerType: domain.PeerTypeChannel, PeerID: 10}
|
||||
base := &blockingReadModelVersionStore{
|
||||
started: make(chan struct{}),
|
||||
release: make(chan struct{}),
|
||||
hashes: map[ReadModelKey]int64{key: 11},
|
||||
}
|
||||
cache := NewCachedReadModelVersionStore(base, time.Hour, 100)
|
||||
|
||||
cache.UpdateReadModelHash(key, 99)
|
||||
rows, err := cache.ReadModelHashes(ctx, []ReadModelKey{key})
|
||||
if err != nil {
|
||||
t.Fatalf("ReadModelHashes: %v", err)
|
||||
}
|
||||
if rows[key] != 99 {
|
||||
t.Fatalf("hash = %d, want notify-provided 99", rows[key])
|
||||
}
|
||||
if got := base.calls.Load(); got != 0 {
|
||||
t.Fatalf("base calls = %d, want cache warmed by notify", got)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue