owpengram-server/internal/app/dialogs/list_hash_cache.go

132 lines
3.6 KiB
Go

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