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,19 +2,31 @@ package channels
import (
"context"
"errors"
"fmt"
"time"
"telesrv/internal/app/readmodel"
"telesrv/internal/domain"
"telesrv/internal/readmodelcache"
"telesrv/internal/store"
)
const (
defaultActiveChannelIDsReadModelTTL = 24 * time.Hour
activeChannelIDsReadModelMaxEntries = 8192
activeChannelIDsReadModelMaxEntries = 32768
activeChannelIDsNoVersionHash = -1
activeChannelIDsStableCutAttempts = 2
)
var errActiveChannelIDsGenerationChanged = errors.New("active channel IDs generation changed")
// ActiveChannelIDsReadModelMetrics records bounded shared-cache outcomes.
// User IDs and page selectors are deliberately excluded.
type ActiveChannelIDsReadModelMetrics interface {
ActiveChannelIDsCache(outcome string)
}
type activeChannelIDsCacheKey struct {
userID int64
afterChannelID int64
@ -27,13 +39,16 @@ type activeChannelIDsReadModelCache struct {
cache *readmodelcache.Cache[activeChannelIDsCacheKey, []int64]
}
func newActiveChannelIDsReadModelCache(ttl time.Duration) *activeChannelIDsReadModelCache {
func newActiveChannelIDsReadModelCache(maxEntries int, ttl time.Duration) *activeChannelIDsReadModelCache {
if maxEntries <= 0 {
maxEntries = activeChannelIDsReadModelMaxEntries
}
if ttl <= 0 {
ttl = defaultActiveChannelIDsReadModelTTL
}
return &activeChannelIDsReadModelCache{
cache: readmodelcache.New[activeChannelIDsCacheKey, []int64](readmodelcache.Config[activeChannelIDsCacheKey, []int64]{
MaxEntries: activeChannelIDsReadModelMaxEntries,
MaxEntries: maxEntries,
TTL: ttl,
Clone: cloneInt64s,
}),
@ -67,20 +82,114 @@ func (c *activeChannelIDsReadModelCache) invalidateUsers(userIDs ...int64) {
}
func (s *Service) cachedActiveChannelIDsForUser(ctx context.Context, userID, afterChannelID int64, limit int) ([]int64, error) {
if s.activeIDsCache == nil || s.versions == nil {
return s.channels.ListActiveChannelIDsForUser(ctx, userID, afterChannelID, limit)
if s.activeIDsShared == nil {
if s.activeIDsCache == nil || s.versions == nil {
return s.channels.ListActiveChannelIDsForUser(ctx, userID, afterChannelID, limit)
}
hash, err := s.activeChannelIDsGeneration(ctx, userID)
if err != nil {
return nil, err
}
key := activeChannelIDsCacheKey{userID: userID, afterChannelID: afterChannelID, limit: limit}
return s.activeIDsCache.getOrLoad(ctx, key, hash, func() ([]int64, error) {
return s.channels.ListActiveChannelIDsForUser(ctx, userID, afterChannelID, limit)
})
}
if s.activeIDsCache == nil || s.versions == nil || s.activeIDsLoader == nil {
return nil, errors.New("shared active channel IDs read model is incompletely configured")
}
key := activeChannelIDsCacheKey{userID: userID, afterChannelID: afterChannelID, limit: limit}
for attempt := 0; attempt < activeChannelIDsStableCutAttempts; attempt++ {
generation, err := s.activeChannelIDsGeneration(ctx, userID)
if err != nil {
return nil, err
}
channelIDs, err := s.activeIDsCache.getOrLoad(ctx, key, generation, func() ([]int64, error) {
return s.loadSharedActiveChannelIDsPage(ctx, key, generation)
})
if errors.Is(err, errActiveChannelIDsGenerationChanged) {
s.recordActiveChannelIDsCache("generation_retry")
continue
}
if err != nil {
return nil, err
}
currentGeneration, err := s.activeChannelIDsGeneration(ctx, userID)
if err != nil {
return nil, err
}
if currentGeneration != generation {
s.recordActiveChannelIDsCache("generation_retry")
s.activeIDsCache.invalidateUsers(userID)
continue
}
s.recordActiveChannelIDsCache("served")
return channelIDs, nil
}
return nil, errActiveChannelIDsGenerationChanged
}
func (s *Service) activeChannelIDsGeneration(ctx context.Context, userID int64) (int64, error) {
if s == nil || s.versions == nil {
return 0, errors.New("active channel IDs read model requires durable versions")
}
hash, ok, err := s.versions.ReadModelHash(ctx, readmodel.ModelChannelActiveIDs, userID, domain.PeerTypeUser, userID)
if err != nil {
return nil, err
return 0, err
}
if !ok || hash == 0 {
hash = activeChannelIDsNoVersionHash
return activeChannelIDsNoVersionHash, nil
}
return hash, nil
}
func (s *Service) loadSharedActiveChannelIDsPage(
ctx context.Context,
key activeChannelIDsCacheKey,
generation int64,
) ([]int64, error) {
sharedKey := store.ActiveChannelIDsPageKey{
UserID: key.userID, Generation: generation,
AfterChannelID: key.afterChannelID, Limit: key.limit,
}
channelIDs, found, err := s.activeIDsShared.GetActiveChannelIDsPage(ctx, sharedKey)
if err != nil {
s.recordActiveChannelIDsCache("read_error")
return nil, err
}
if found {
s.recordActiveChannelIDsCache("hit")
return channelIDs, nil
}
s.recordActiveChannelIDsCache("miss")
channelIDs, err = s.activeIDsLoader.ListActiveChannelIDsForUser(
ctx, key.userID, key.afterChannelID, key.limit,
)
if err != nil {
return nil, err
}
if generation == activeChannelIDsNoVersionHash && len(channelIDs) != 0 {
return nil, fmt.Errorf("active channel IDs generation missing for non-empty owner %d", key.userID)
}
currentGeneration, err := s.activeChannelIDsGeneration(ctx, key.userID)
if err != nil {
return nil, err
}
if currentGeneration != generation {
return nil, errActiveChannelIDsGenerationChanged
}
if err := s.activeIDsShared.PutActiveChannelIDsPage(ctx, sharedKey, channelIDs); err != nil {
s.recordActiveChannelIDsCache("write_error")
return nil, err
}
s.recordActiveChannelIDsCache("fill")
return channelIDs, nil
}
func (s *Service) recordActiveChannelIDsCache(outcome string) {
if s != nil && s.activeIDsMetrics != nil {
s.activeIDsMetrics.ActiveChannelIDsCache(outcome)
}
key := activeChannelIDsCacheKey{userID: userID, afterChannelID: afterChannelID, limit: limit}
return s.activeIDsCache.getOrLoad(ctx, key, hash, func() ([]int64, error) {
return s.channels.ListActiveChannelIDsForUser(ctx, userID, afterChannelID, limit)
})
}
func cloneInt64s(in []int64) []int64 {