chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,258 @@
package store
import (
"context"
"sort"
"sync"
"time"
"telesrv/internal/domain"
)
const (
defaultReadModelHashCacheTTL = 30 * time.Minute
defaultReadModelHashCacheMax = 65536
)
type readModelHashCacheEntry struct {
hash int64
expireAt time.Time
}
type readModelHashInflight struct {
done chan struct{}
err error
}
// 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.RWMutex
m map[ReadModelKey]readModelHashCacheEntry
inflight map[ReadModelKey]*readModelHashInflight
// epoch 在每次 invalidate/update/flush 时自增;一次锁外 DB load 若跨越了一次失效,
// finishReadModelHashInflight 会拒绝把 stale hash 写回,避免 NOTIFY 送来的新 hash 被覆盖。
// 与 contacts/privacy 读模缓存的 epoch 守卫同构。
epoch 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,
m: make(map[ReadModelKey]readModelHashCacheEntry, 1024),
inflight: make(map[ReadModelKey]*readModelHashInflight),
}
}
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.RLock()
for _, key := range keys {
if key.Model == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
if entry, ok := s.m[key]; ok && entry.expireAt.After(now) {
out[key] = entry.hash
done[key] = struct{}{}
continue
}
misses = append(misses, key)
}
s.mu.RUnlock()
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 loadEpoch uint64
now = s.now()
s.mu.Lock()
loadEpoch = s.epoch
for _, key := range misses {
if _, ok := done[key]; ok {
continue
}
if entry, ok := s.m[key]; ok && entry.expireAt.After(now) {
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)
}
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, nil, err, time.Time{}, loadEpoch)
return nil, err
}
expireAt := s.now().Add(s.ttl)
s.finishReadModelHashInflight(owned, loaded, nil, expireAt, loadEpoch)
// 失效可能在 load 期间到达并写入更新的 hash优先返回缓存里的当前值
// (可能是 NOTIFY 刚写入的新 hash),而不是这次 load 读到的可能已过期的值。
effNow := s.now()
s.mu.RLock()
for _, key := range owned {
if entry, ok := s.m[key]; ok && entry.expireAt.After(effNow) {
out[key] = entry.hash
} else {
out[key] = loaded[key]
}
done[key] = struct{}{}
}
s.mu.RUnlock()
}
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.RLock()
entry, ok := s.m[key]
s.mu.RUnlock()
if ok && entry.expireAt.After(s.now()) {
out[key] = entry.hash
}
done[key] = struct{}{}
}
}
return out, nil
}
func (s *CachedReadModelVersionStore) finishReadModelHashInflight(keys []ReadModelKey, loaded map[ReadModelKey]int64, err error, expireAt time.Time, loadEpoch uint64) {
s.mu.Lock()
defer s.mu.Unlock()
if err == nil && s.epoch == loadEpoch {
if len(s.m)+len(keys) > s.max {
s.m = make(map[ReadModelKey]readModelHashCacheEntry, 1024)
}
if expireAt.IsZero() {
expireAt = s.now().Add(s.ttl)
}
for _, key := range keys {
hash := loaded[key]
s.m[key] = readModelHashCacheEntry{hash: hash, expireAt: expireAt}
}
}
for _, key := range keys {
if inflight := s.inflight[key]; inflight != nil {
inflight.err = err
delete(s.inflight, key)
close(inflight.done)
}
}
}
func (s *CachedReadModelVersionStore) InvalidateReadModel(key ReadModelKey) {
if s == nil || key.Model == "" {
return
}
s.mu.Lock()
delete(s.m, key)
s.epoch++
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()
if len(s.m)+1 > s.max {
s.m = make(map[ReadModelKey]readModelHashCacheEntry, 1024)
}
s.m[key] = readModelHashCacheEntry{hash: hash, expireAt: s.now().Add(s.ttl)}
// 写入权威新 hash 后自增 epoch任何此刻在飞的 load 都不得再用旧值覆盖它。
s.epoch++
s.mu.Unlock()
}
func (s *CachedReadModelVersionStore) FlushReadModelCache() {
if s == nil {
return
}
s.mu.Lock()
s.m = make(map[ReadModelKey]readModelHashCacheEntry, 1024)
s.epoch++
s.mu.Unlock()
}
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
})
}