package dialogs import ( "context" "time" "telesrv/internal/domain" "telesrv/internal/readmodelcache" ) const ( defaultDialogListHashCacheTTL = 24 * time.Hour dialogListHashCacheMaxEntries = 4096 ) type dialogListHashCacheKey struct { userID int64 pinnedOnly bool excludePinned bool hasFolderID bool folderID int limit int } type dialogListHashValue struct { hash int64 count int } // dialogListHashCache 由统一缓存原语承载,走「外部构建再写回」(GetDialogs 在加载前 cacheEpoch // 快照 epoch → rememberDialogListHash 经 putIfEpoch 写回)。原语的 epoch 仅在 Invalidate/Flush // (写驱动失效)自增,被动 TTL 过期纯 per-key 不动 epoch——正是本缓存所需(避免误返 NotModified)。 type dialogListHashCache struct { cache *readmodelcache.Cache[dialogListHashCacheKey, dialogListHashValue] } func newDialogListHashCache(ttl time.Duration) *dialogListHashCache { if ttl <= 0 { ttl = defaultDialogListHashCacheTTL } return &dialogListHashCache{ cache: readmodelcache.New[dialogListHashCacheKey, dialogListHashValue](readmodelcache.Config[dialogListHashCacheKey, dialogListHashValue]{ MaxEntries: dialogListHashCacheMaxEntries, TTL: ttl, }), } } func (s *Service) GetDialogsHash(_ context.Context, userID int64, filter domain.DialogFilter) (domain.DialogHashCheck, error) { if s == nil || s.listHashCache == nil || filter.Hash == 0 { return domain.DialogHashCheck{}, nil } key, ok := dialogListHashKey(userID, filter) if !ok { return domain.DialogHashCheck{}, nil } snap, ok := s.listHashCache.lookup(key) if !ok { return domain.DialogHashCheck{}, nil } return domain.DialogHashCheck{ Known: true, Matched: snap.hash == filter.Hash, Hash: snap.hash, Count: snap.count, }, nil } func (s *Service) rememberDialogListHash(userID int64, filter domain.DialogFilter, list domain.DialogList, loadEpoch uint64) { if s == nil || s.listHashCache == nil || list.Hash == 0 { return } key, ok := dialogListHashKey(userID, filter) if !ok { return } s.listHashCache.putIfEpoch(key, list.Hash, list.Count, loadEpoch) } func dialogListHashKey(userID int64, filter domain.DialogFilter) (dialogListHashCacheKey, bool) { if userID == 0 || filter.OffsetDate != 0 || filter.OffsetID != 0 || filter.HasOffsetPeer { return dialogListHashCacheKey{}, false } limit := filter.Limit if limit <= 0 || limit > 100 { limit = 100 } return dialogListHashCacheKey{ userID: userID, pinnedOnly: filter.PinnedOnly, excludePinned: filter.ExcludePinned, hasFolderID: filter.HasFolderID, folderID: filter.FolderID, limit: limit, }, true } func (c *dialogListHashCache) lookup(key dialogListHashCacheKey) (dialogListHashValue, bool) { if c == nil { return dialogListHashValue{}, false } return c.cache.Peek(key) } // putIfEpoch 仅在 epoch 未变(加载期间没有写驱动失效)时写入,堵住 stale write-back race。 func (c *dialogListHashCache) putIfEpoch(key dialogListHashCacheKey, hash int64, count int, loadEpoch uint64) { if c == nil || key.userID == 0 || hash == 0 { return } c.cache.StoreIfEpoch(key, dialogListHashValue{hash: hash, count: count}, loadEpoch) } func (c *dialogListHashCache) cacheEpoch() uint64 { if c == nil { return 0 } return c.cache.LoadEpoch() } func (c *dialogListHashCache) invalidateOwner(userID int64) { if c == nil || userID == 0 { return } c.cache.InvalidateWhere(func(key dialogListHashCacheKey) bool { return key.userID == userID }) } func (c *dialogListHashCache) flush() { if c == nil { return } c.cache.Flush() }