merged from gramsrv upstream

This commit is contained in:
onysd 2026-09-01 12:06:31 +03:00
parent 79c64ee916
commit 21a0856587
651 changed files with 54774 additions and 4590 deletions

View file

@ -2,6 +2,7 @@ package postgres
import (
"context"
"sync"
"telesrv/internal/domain"
"telesrv/internal/readmodelcache"
@ -12,6 +13,14 @@ type channelDialogCacheKey struct {
channelID int64
}
type channelDialogCacheEntry struct {
dialog domain.ChannelDialog
listVisible bool
topMentioned bool
topMediaUnread bool
topUnreadProjected bool
}
// ChannelDialogCache 缓存 viewer 作用域的频道 dialog 投影,由统一缓存原语
// readmodelcache.Cache 承载(LRU 单条驱逐 / epoch 守卫 / singleflight 内建)。
//
@ -19,39 +28,59 @@ type channelDialogCacheKey struct {
// dialog_light(viewer,channel) 三个 read model;ReadModelChangeListener 在写侧 NOTIFY 时
// 失效对应键,重连时 flush。warm-from-list 经 put 回填(含 DefaultSendAs)。
type ChannelDialogCache struct {
cache *readmodelcache.Cache[channelDialogCacheKey, domain.ChannelDialog]
cache *readmodelcache.Cache[channelDialogCacheKey, channelDialogCacheEntry]
indexMu sync.Mutex
channelKeys map[int64]map[channelDialogCacheKey]struct{}
}
func NewChannelDialogCache(max int) *ChannelDialogCache {
cache := readmodelcache.New[channelDialogCacheKey, domain.ChannelDialog](readmodelcache.Config[channelDialogCacheKey, domain.ChannelDialog]{
c := &ChannelDialogCache{channelKeys: make(map[int64]map[channelDialogCacheKey]struct{})}
cache := readmodelcache.New[channelDialogCacheKey, channelDialogCacheEntry](readmodelcache.Config[channelDialogCacheKey, channelDialogCacheEntry]{
MaxEntries: max,
Clone: cloneChannelDialog,
Clone: cloneChannelDialogCacheEntry,
OnStore: c.indexEntry,
OnRemove: c.unindexEntry,
})
if cache == nil {
return nil
}
return &ChannelDialogCache{cache: cache}
c.cache = cache
return c
}
func (c *ChannelDialogCache) get(userID, channelID int64) (domain.ChannelDialog, bool) {
if c == nil || userID == 0 || channelID == 0 {
return domain.ChannelDialog{}, false
}
return c.cache.Peek(channelDialogCacheKey{userID: userID, channelID: channelID})
entry, ok := c.cache.Peek(channelDialogCacheKey{userID: userID, channelID: channelID})
return entry.dialog, ok
}
func (c *ChannelDialogCache) getListProjection(userID, channelID int64) (channelDialogCacheEntry, bool) {
if c == nil || userID == 0 || channelID == 0 {
return channelDialogCacheEntry{}, false
}
entry, ok := c.cache.Peek(channelDialogCacheKey{userID: userID, channelID: channelID})
return entry, ok && entry.listVisible
}
func (c *ChannelDialogCache) getOrLoad(ctx context.Context, userID, channelID int64, load func() (domain.ChannelDialog, error)) (domain.ChannelDialog, error) {
if c == nil || userID == 0 || channelID == 0 {
return load()
}
return c.cache.GetOrLoad(ctx, channelDialogCacheKey{userID: userID, channelID: channelID}, load)
entry, err := c.cache.GetOrLoad(ctx, channelDialogCacheKey{userID: userID, channelID: channelID}, func() (channelDialogCacheEntry, error) {
dialog, err := load()
return channelDialogCacheEntry{dialog: dialog}, err
})
return entry.dialog, err
}
func (c *ChannelDialogCache) put(dialog domain.ChannelDialog) {
if c == nil || dialog.UserID == 0 || dialog.ChannelID == 0 {
return
}
c.cache.Store(channelDialogCacheKey{userID: dialog.UserID, channelID: dialog.ChannelID}, dialog)
c.cache.Store(channelDialogCacheKey{userID: dialog.UserID, channelID: dialog.ChannelID}, channelDialogCacheEntry{dialog: dialog})
}
// cacheEpoch 在「列表暖写回」前快照 epoch;配合 putIfEpoch 堵住 warm-vs-invalidation
@ -67,7 +96,30 @@ func (c *ChannelDialogCache) putIfEpoch(dialog domain.ChannelDialog, loadEpoch u
if c == nil || dialog.UserID == 0 || dialog.ChannelID == 0 {
return
}
c.cache.StoreIfEpoch(channelDialogCacheKey{userID: dialog.UserID, channelID: dialog.ChannelID}, dialog, loadEpoch)
c.cache.StoreIfEpoch(channelDialogCacheKey{userID: dialog.UserID, channelID: dialog.ChannelID}, channelDialogCacheEntry{dialog: dialog}, loadEpoch)
}
func (c *ChannelDialogCache) putListProjectionIfEpoch(
dialog domain.ChannelDialog,
topMentioned bool,
topMediaUnread bool,
topUnreadProjected bool,
loadEpoch uint64,
) {
if c == nil || dialog.UserID == 0 || dialog.ChannelID == 0 {
return
}
c.cache.StoreIfEpoch(
channelDialogCacheKey{userID: dialog.UserID, channelID: dialog.ChannelID},
channelDialogCacheEntry{
dialog: dialog,
listVisible: true,
topMentioned: topMentioned,
topMediaUnread: topMediaUnread,
topUnreadProjected: topUnreadProjected,
},
loadEpoch,
)
}
func (c *ChannelDialogCache) delete(userID, channelID int64) {
@ -81,7 +133,14 @@ func (c *ChannelDialogCache) deleteChannel(channelID int64) {
if c == nil || channelID == 0 {
return
}
c.cache.InvalidateWhere(func(k channelDialogCacheKey) bool { return k.channelID == channelID })
c.indexMu.Lock()
indexed := c.channelKeys[channelID]
keys := make([]channelDialogCacheKey, 0, len(indexed))
for key := range indexed {
keys = append(keys, key)
}
c.indexMu.Unlock()
c.cache.Invalidate(keys...)
}
func (c *ChannelDialogCache) flush() {
@ -89,12 +148,36 @@ func (c *ChannelDialogCache) flush() {
return
}
c.cache.Flush()
c.indexMu.Lock()
c.channelKeys = make(map[int64]map[channelDialogCacheKey]struct{})
c.indexMu.Unlock()
}
func cloneChannelDialog(dialog domain.ChannelDialog) domain.ChannelDialog {
if dialog.DefaultSendAs != nil {
peer := *dialog.DefaultSendAs
dialog.DefaultSendAs = &peer
func (c *ChannelDialogCache) indexEntry(key channelDialogCacheKey, _ channelDialogCacheEntry) {
c.indexMu.Lock()
keys := c.channelKeys[key.channelID]
if keys == nil {
keys = make(map[channelDialogCacheKey]struct{})
c.channelKeys[key.channelID] = keys
}
return dialog
keys[key] = struct{}{}
c.indexMu.Unlock()
}
func (c *ChannelDialogCache) unindexEntry(key channelDialogCacheKey, _ channelDialogCacheEntry) {
c.indexMu.Lock()
keys := c.channelKeys[key.channelID]
delete(keys, key)
if len(keys) == 0 {
delete(c.channelKeys, key.channelID)
}
c.indexMu.Unlock()
}
func cloneChannelDialogCacheEntry(entry channelDialogCacheEntry) channelDialogCacheEntry {
if entry.dialog.DefaultSendAs != nil {
peer := *entry.dialog.DefaultSendAs
entry.dialog.DefaultSendAs = &peer
}
return entry
}