Ctrl K
ComunidadGuías…

internal/store/postgres/channel_difference_cache.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}