200 lines
6.1 KiB
Go
200 lines
6.1 KiB
Go
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 = 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
|
|
limit int
|
|
}
|
|
|
|
// activeChannelIDsReadModelCache 由统一缓存原语承载(版本闸门 / epoch 守卫 / LRU / clone)。
|
|
// 无版本(version 缺失)时用 activeChannelIDsNoVersionHash 哨兵作版本,仍享 TTL+epoch 失效。
|
|
type activeChannelIDsReadModelCache struct {
|
|
cache *readmodelcache.Cache[activeChannelIDsCacheKey, []int64]
|
|
}
|
|
|
|
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: maxEntries,
|
|
TTL: ttl,
|
|
Clone: cloneInt64s,
|
|
}),
|
|
}
|
|
}
|
|
|
|
func (c *activeChannelIDsReadModelCache) getOrLoad(ctx context.Context, key activeChannelIDsCacheKey, hash int64, load func() ([]int64, error)) ([]int64, error) {
|
|
if c == nil {
|
|
return load()
|
|
}
|
|
return c.cache.GetOrLoadVersioned(ctx, key, hash, load)
|
|
}
|
|
|
|
func (c *activeChannelIDsReadModelCache) invalidateUsers(userIDs ...int64) {
|
|
if c == nil || len(userIDs) == 0 {
|
|
return
|
|
}
|
|
seen := make(map[int64]struct{}, len(userIDs))
|
|
for _, userID := range userIDs {
|
|
if userID != 0 {
|
|
seen[userID] = struct{}{}
|
|
}
|
|
}
|
|
if len(seen) == 0 {
|
|
return
|
|
}
|
|
c.cache.InvalidateWhere(func(k activeChannelIDsCacheKey) bool {
|
|
_, ok := seen[k.userID]
|
|
return ok
|
|
})
|
|
}
|
|
|
|
func (s *Service) cachedActiveChannelIDsForUser(ctx context.Context, userID, afterChannelID int64, limit int) ([]int64, error) {
|
|
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 0, err
|
|
}
|
|
if !ok || hash == 0 {
|
|
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)
|
|
}
|
|
}
|
|
|
|
func cloneInt64s(in []int64) []int64 {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
return append([]int64(nil), in...)
|
|
}
|