174 lines
5.2 KiB
Go
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
|
|
}
|