owpengram-server/internal/store/postgres/channel_difference_cache.go
2026-09-01 12:06:31 +03:00

174 lines
5.2 KiB
Go

package postgres
import (
"context"
"strconv"
"sync/atomic"
"time"
"telesrv/internal/domain"
"telesrv/internal/readmodelcache"
)
type channelDifferenceBaseKey struct {
channelID int64
requestPts int
capturedPts int
capturedTopID int
limit int
}
// channelDifferenceBase contains only viewer-independent durable facts. Access,
// available-min, monoforum visibility, unread flags and dialog state are applied
// after the cache lookup by ListChannelDifference.
type channelDifferenceBase struct {
retainedThroughPts int
lastPts int
tooLong bool
events []domain.ChannelUpdateEvent
messages []domain.ChannelMessage
// mentionCandidateIDs is a viewer-independent sparse gate sourced from
// channel_unread_mention_index. When candidatesKnown is true, messages not
// present in this set cannot have a viewer mention overlay and must not cause
// a channel_unread_mentions query.
mentionCandidateIDs map[int]struct{}
candidatesKnown bool
}
type ChannelDifferenceCacheSnapshot struct {
Entries int
Weight int64
Hits uint64
Misses uint64
Loads uint64
LoadErrors uint64
}
// ChannelDifferenceBaseCache deduplicates immutable channel event/message pages
// shared by many viewers catching up from the same cursor. It never stores a
// permission decision or a final ChannelDifference response.
type ChannelDifferenceBaseCache struct {
cache *readmodelcache.Cache[channelDifferenceBaseKey, channelDifferenceBase]
hits atomic.Uint64
misses atomic.Uint64
loads atomic.Uint64
loadErrors atomic.Uint64
}
func NewChannelDifferenceBaseCache(maxEntries int, maxWeight int64, ttl time.Duration) *ChannelDifferenceBaseCache {
cache := readmodelcache.New[channelDifferenceBaseKey, channelDifferenceBase](readmodelcache.Config[channelDifferenceBaseKey, channelDifferenceBase]{
MaxEntries: maxEntries,
MaxWeight: maxWeight,
TTL: ttl,
Clone: cloneChannelDifferenceBase,
Weight: channelDifferenceBaseWeight,
KeyString: func(key channelDifferenceBaseKey) string {
return strconv.FormatInt(key.channelID, 10) + ":" +
strconv.Itoa(key.requestPts) + ":" + strconv.Itoa(key.capturedPts) + ":" +
strconv.Itoa(key.capturedTopID) + ":" + strconv.Itoa(key.limit)
},
})
if cache == nil {
return nil
}
return &ChannelDifferenceBaseCache{cache: cache}
}
func (c *ChannelDifferenceBaseCache) getOrLoad(
ctx context.Context,
key channelDifferenceBaseKey,
load func() (channelDifferenceBase, error),
) (channelDifferenceBase, error) {
if c == nil {
return load()
}
if _, ok := c.cache.Peek(key); ok {
c.hits.Add(1)
} else {
c.misses.Add(1)
}
return c.cache.GetOrLoad(ctx, key, func() (channelDifferenceBase, error) {
c.loads.Add(1)
value, err := load()
if err != nil {
c.loadErrors.Add(1)
}
return value, err
})
}
func (c *ChannelDifferenceBaseCache) deleteChannel(channelID int64) {
if c == nil || channelID == 0 {
return
}
c.cache.InvalidateWhere(func(key channelDifferenceBaseKey) bool { return key.channelID == channelID })
}
func (c *ChannelDifferenceBaseCache) flush() {
if c == nil {
return
}
c.cache.Flush()
}
func (c *ChannelDifferenceBaseCache) Snapshot() ChannelDifferenceCacheSnapshot {
if c == nil {
return ChannelDifferenceCacheSnapshot{}
}
return ChannelDifferenceCacheSnapshot{
Entries: c.cache.Len(),
Weight: c.cache.Weight(),
Hits: c.hits.Load(),
Misses: c.misses.Load(),
Loads: c.loads.Load(),
LoadErrors: c.loadErrors.Load(),
}
}
func cloneChannelDifferenceBase(base channelDifferenceBase) channelDifferenceBase {
candidates := base.mentionCandidateIDs
base.events = append([]domain.ChannelUpdateEvent(nil), base.events...)
for i := range base.events {
base.events[i].MessageIDs = append([]int(nil), base.events[i].MessageIDs...)
base.events[i].UserIDs = append([]int64(nil), base.events[i].UserIDs...)
base.events[i].Message = cloneChannelTopMessage(base.events[i].Message)
}
base.messages = append([]domain.ChannelMessage(nil), base.messages...)
for i := range base.messages {
base.messages[i] = cloneChannelTopMessage(base.messages[i])
}
if base.mentionCandidateIDs != nil {
base.mentionCandidateIDs = make(map[int]struct{}, len(base.mentionCandidateIDs))
for id := range candidates {
base.mentionCandidateIDs[id] = struct{}{}
}
}
return base
}
func channelDifferenceBaseWeight(base channelDifferenceBase) int64 {
weight := int64(96 + len(base.events)*192 + len(base.messages)*192 + len(base.mentionCandidateIDs)*16)
for _, event := range base.events {
weight += int64(len(event.MessageIDs)*8 + len(event.UserIDs)*8)
weight += channelDifferenceMessageWeight(event.Message)
}
for _, message := range base.messages {
weight += channelDifferenceMessageWeight(message)
}
return weight
}
func channelDifferenceMessageWeight(message domain.ChannelMessage) int64 {
if message.ID == 0 {
return 0
}
weight := int64(len(message.Body) + len(message.PostAuthor) + len(message.Entities)*48)
if message.RichMessage != nil {
weight += int64(len(message.RichMessage.Blocks) + len(message.RichMessage.BotAPIProjection))
}
if message.Action != nil {
weight += int64(len(message.Action.Title) + len(message.Action.UserIDs)*8 + len(message.Action.TodoItems)*64)
}
return weight
}