owpengram-server/internal/store/read_model_cache.go
2026-09-01 12:06:31 +03:00

349 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package store
import (
"container/list"
"context"
"sort"
"sync"
"time"
"telesrv/internal/domain"
)
const (
defaultReadModelHashCacheTTL = 30 * time.Minute
defaultReadModelHashCacheMax = 1000000
)
type readModelHashCacheEntry struct {
key ReadModelKey
hash int64
expireAt time.Time
}
type readModelHashInflight struct {
done chan struct{}
err error
hash int64
accepted bool
}
// CachedReadModelVersionStore caches read_model_versions hash tokens in-process.
// Correctness is driven by the same read-model NOTIFY stream that invalidates the
// heavier projection caches; TTL only bounds missed out-of-band writes.
type CachedReadModelVersionStore struct {
base ReadModelVersionStore
ttl time.Duration
max int
now func() time.Time
mu sync.Mutex
lru *list.List
m map[ReadModelKey]*list.Element
inflight map[ReadModelKey]*readModelHashInflight
// flushGeneration rejects every refill that started before a listener
// reconnect/full flush. keyGeneration rejects only the exact key whose
// NOTIFY arrived while it was loading; unrelated high-churn keys must not
// keep the complete version spine permanently cold.
flushGeneration uint64
keyGeneration map[ReadModelKey]uint64
}
func NewCachedReadModelVersionStore(base ReadModelVersionStore, ttl time.Duration, max int) *CachedReadModelVersionStore {
if base == nil {
return nil
}
if ttl <= 0 {
ttl = defaultReadModelHashCacheTTL
}
if max <= 0 {
max = defaultReadModelHashCacheMax
}
return &CachedReadModelVersionStore{
base: base,
ttl: ttl,
max: max,
now: time.Now,
lru: list.New(),
m: make(map[ReadModelKey]*list.Element, 1024),
inflight: make(map[ReadModelKey]*readModelHashInflight),
keyGeneration: make(map[ReadModelKey]uint64, 1024),
}
}
func (s *CachedReadModelVersionStore) ReadModelHash(ctx context.Context, model string, ownerUserID int64, peerType domain.PeerType, peerID int64) (int64, bool, error) {
if s == nil || s.base == nil || model == "" {
return 0, false, nil
}
key := ReadModelKey{Model: model, OwnerUserID: ownerUserID, PeerType: peerType, PeerID: peerID}
rows, err := s.ReadModelHashes(ctx, []ReadModelKey{key})
if err != nil {
return 0, false, err
}
hash := rows[key]
return hash, hash != 0, nil
}
func (s *CachedReadModelVersionStore) ReadModelHashes(ctx context.Context, keys []ReadModelKey) (map[ReadModelKey]int64, error) {
out := make(map[ReadModelKey]int64, len(keys))
if s == nil || s.base == nil || len(keys) == 0 {
return out, nil
}
now := s.now()
misses := make([]ReadModelKey, 0, len(keys))
done := make(map[ReadModelKey]struct{}, len(keys))
seen := make(map[ReadModelKey]struct{}, len(keys))
s.mu.Lock()
for _, key := range keys {
if key.Model == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
if entry, ok := s.readHashLocked(key, now); ok {
out[key] = entry.hash
done[key] = struct{}{}
continue
}
misses = append(misses, key)
}
s.mu.Unlock()
if len(misses) == 0 {
return out, nil
}
sortReadModelKeys(misses)
for len(done) < len(seen) {
owned := make([]ReadModelKey, 0, len(misses))
waiting := make(map[ReadModelKey]*readModelHashInflight)
var loadFlushGeneration uint64
ownedGeneration := make(map[ReadModelKey]uint64, len(misses))
ownedInflight := make(map[ReadModelKey]*readModelHashInflight, len(misses))
now = s.now()
s.mu.Lock()
loadFlushGeneration = s.flushGeneration
for _, key := range misses {
if _, ok := done[key]; ok {
continue
}
if entry, ok := s.readHashLocked(key, now); ok {
out[key] = entry.hash
done[key] = struct{}{}
continue
}
if inflight := s.inflight[key]; inflight != nil {
waiting[key] = inflight
continue
}
inflight := &readModelHashInflight{done: make(chan struct{})}
s.inflight[key] = inflight
owned = append(owned, key)
ownedGeneration[key] = s.keyGeneration[key]
ownedInflight[key] = inflight
}
s.mu.Unlock()
if len(owned) == 0 && len(waiting) == 0 {
break
}
if len(owned) > 0 {
loaded, err := s.base.ReadModelHashes(ctx, owned)
if err != nil {
s.finishReadModelHashInflight(owned, ownedGeneration, nil, err, time.Time{}, loadFlushGeneration)
return nil, err
}
expireAt := s.now().Add(s.ttl)
s.finishReadModelHashInflight(owned, ownedGeneration, loaded, nil, expireAt, loadFlushGeneration)
// 失效可能在 load 期间到达并写入更新的 hash优先返回缓存里的当前值
// (可能是 NOTIFY 刚写入的新 hash)。精确 invalidation/flush 后若没有
// 当前值则不返回旧 load而是在下一轮重新 claim/load。
effNow := s.now()
s.mu.Lock()
for _, key := range owned {
if entry, ok := s.readHashLocked(key, effNow); ok {
out[key] = entry.hash
done[key] = struct{}{}
continue
}
if inflight := ownedInflight[key]; inflight != nil && inflight.accepted {
// A capacity eviction may remove an otherwise generation-valid
// entry before this owner reacquires the lock. Its accepted value
// remains a valid result for this call even if it is not retained.
out[key] = inflight.hash
done[key] = struct{}{}
}
}
s.mu.Unlock()
}
for key, inflight := range waiting {
select {
case <-inflight.done:
case <-ctx.Done():
return nil, ctx.Err()
}
if inflight.err != nil {
return nil, inflight.err
}
s.mu.Lock()
entry, ok := s.readHashLocked(key, s.now())
s.mu.Unlock()
if ok {
out[key] = entry.hash
done[key] = struct{}{}
continue
}
if inflight.accepted {
out[key] = inflight.hash
done[key] = struct{}{}
}
}
}
return out, nil
}
func (s *CachedReadModelVersionStore) finishReadModelHashInflight(
keys []ReadModelKey,
keyGeneration map[ReadModelKey]uint64,
loaded map[ReadModelKey]int64,
err error,
expireAt time.Time,
loadFlushGeneration uint64,
) {
s.mu.Lock()
defer s.mu.Unlock()
if err == nil {
if expireAt.IsZero() {
expireAt = s.now().Add(s.ttl)
}
for _, key := range keys {
inflight := s.inflight[key]
if inflight == nil {
continue
}
if s.flushGeneration != loadFlushGeneration || s.keyGeneration[key] != keyGeneration[key] {
continue
}
inflight.hash = loaded[key]
inflight.accepted = true
s.storeHashLocked(key, inflight.hash, expireAt)
}
}
for _, key := range keys {
if inflight := s.inflight[key]; inflight != nil {
inflight.err = err
delete(s.inflight, key)
delete(s.keyGeneration, key)
close(inflight.done)
}
}
}
func (s *CachedReadModelVersionStore) InvalidateReadModel(key ReadModelKey) {
if s == nil || key.Model == "" {
return
}
s.mu.Lock()
s.removeHashLocked(key)
s.bumpReadModelKeyGenerationLocked(key)
s.mu.Unlock()
}
func (s *CachedReadModelVersionStore) UpdateReadModelHash(key ReadModelKey, hash int64) {
if s == nil || key.Model == "" {
return
}
if hash == 0 {
s.InvalidateReadModel(key)
return
}
s.mu.Lock()
s.storeHashLocked(key, hash, s.now().Add(s.ttl))
// 写入权威新 hash 后只推进该 exact key 的 generation其它 key 的
// inflight refill 仍可正常完成。
s.bumpReadModelKeyGenerationLocked(key)
s.mu.Unlock()
}
func (s *CachedReadModelVersionStore) bumpReadModelKeyGenerationLocked(key ReadModelKey) {
if s.inflight[key] == nil {
// Generations only guard a currently unlocked refill. Keeping tombstones
// for every historical notification would make this side map unbounded.
delete(s.keyGeneration, key)
return
}
s.keyGeneration[key]++
}
func (s *CachedReadModelVersionStore) FlushReadModelCache() {
if s == nil {
return
}
s.mu.Lock()
s.lru.Init()
s.m = make(map[ReadModelKey]*list.Element, 1024)
s.keyGeneration = make(map[ReadModelKey]uint64, 1024)
s.flushGeneration++
s.mu.Unlock()
}
func (s *CachedReadModelVersionStore) readHashLocked(key ReadModelKey, now time.Time) (readModelHashCacheEntry, bool) {
el := s.m[key]
if el == nil {
return readModelHashCacheEntry{}, false
}
entry := el.Value.(*readModelHashCacheEntry)
if !entry.expireAt.After(now) {
s.lru.Remove(el)
delete(s.m, key)
return readModelHashCacheEntry{}, false
}
s.lru.MoveToFront(el)
return *entry, true
}
func (s *CachedReadModelVersionStore) storeHashLocked(key ReadModelKey, hash int64, expireAt time.Time) {
if el := s.m[key]; el != nil {
entry := el.Value.(*readModelHashCacheEntry)
entry.hash = hash
entry.expireAt = expireAt
s.lru.MoveToFront(el)
return
}
entry := &readModelHashCacheEntry{key: key, hash: hash, expireAt: expireAt}
s.m[key] = s.lru.PushFront(entry)
for len(s.m) > s.max {
oldest := s.lru.Back()
if oldest == nil {
break
}
old := oldest.Value.(*readModelHashCacheEntry)
delete(s.m, old.key)
s.lru.Remove(oldest)
}
}
func (s *CachedReadModelVersionStore) removeHashLocked(key ReadModelKey) {
if el := s.m[key]; el != nil {
delete(s.m, key)
s.lru.Remove(el)
}
}
func sortReadModelKeys(keys []ReadModelKey) {
sort.Slice(keys, func(i, j int) bool {
a, b := keys[i], keys[j]
if a.Model != b.Model {
return a.Model < b.Model
}
if a.OwnerUserID != b.OwnerUserID {
return a.OwnerUserID < b.OwnerUserID
}
if a.PeerType != b.PeerType {
return a.PeerType < b.PeerType
}
return a.PeerID < b.PeerID
})
}