chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
258
internal/store/read_model_cache.go
Normal file
258
internal/store/read_model_cache.go
Normal 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
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue