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 }) }