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

135 lines
4.1 KiB
Go

package redisstore
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/redis/go-redis/v9"
"telesrv/internal/domain"
"telesrv/internal/store"
)
const (
DefaultActiveChannelIDsPageTTL = 24 * time.Hour
activeChannelIDsPageSchemaV1 = 1
activeChannelIDsPageMaxBytes = 64 << 10
)
type ActiveChannelIDsPageCache struct {
c *redis.Client
ttl time.Duration
}
func NewActiveChannelIDsPageCache(c *redis.Client, ttl time.Duration) *ActiveChannelIDsPageCache {
if ttl <= 0 {
ttl = DefaultActiveChannelIDsPageTTL
}
return &ActiveChannelIDsPageCache{c: c, ttl: ttl}
}
type activeChannelIDsPageEnvelope struct {
Schema int `json:"schema"`
Key store.ActiveChannelIDsPageKey `json:"key"`
ChannelIDs []int64 `json:"channel_ids"`
}
func activeChannelIDsPageRedisKey(key store.ActiveChannelIDsPageKey) string {
return fmt.Sprintf(
"channel:active-ids:page:v1:%d:%d:%d:%d",
key.UserID, key.Generation, key.AfterChannelID, key.Limit,
)
}
func (s *ActiveChannelIDsPageCache) GetActiveChannelIDsPage(
ctx context.Context,
key store.ActiveChannelIDsPageKey,
) ([]int64, bool, error) {
if s == nil || s.c == nil {
return nil, false, fmt.Errorf("active channel IDs Redis cache unavailable")
}
if err := validateActiveChannelIDsPageKey(key); err != nil {
return nil, false, err
}
redisKey := activeChannelIDsPageRedisKey(key)
raw, err := s.c.Get(ctx, redisKey).Bytes()
if err == redis.Nil {
return nil, false, nil
}
if err != nil {
return nil, false, fmt.Errorf("redis get active channel IDs page: %w", err)
}
if len(raw) == 0 || len(raw) > activeChannelIDsPageMaxBytes {
_ = s.c.Del(ctx, redisKey).Err()
return nil, false, fmt.Errorf("invalid active channel IDs page size %d", len(raw))
}
var envelope activeChannelIDsPageEnvelope
if err := json.Unmarshal(raw, &envelope); err != nil {
_ = s.c.Del(ctx, redisKey).Err()
return nil, false, fmt.Errorf("decode active channel IDs page: %w", err)
}
if envelope.Schema != activeChannelIDsPageSchemaV1 || envelope.Key != key {
_ = s.c.Del(ctx, redisKey).Err()
return nil, false, fmt.Errorf("active channel IDs page identity/schema mismatch")
}
if err := validateActiveChannelIDsPage(key, envelope.ChannelIDs); err != nil {
_ = s.c.Del(ctx, redisKey).Err()
return nil, false, err
}
return append([]int64(nil), envelope.ChannelIDs...), true, nil
}
func (s *ActiveChannelIDsPageCache) PutActiveChannelIDsPage(
ctx context.Context,
key store.ActiveChannelIDsPageKey,
channelIDs []int64,
) error {
if s == nil || s.c == nil {
return fmt.Errorf("active channel IDs Redis cache unavailable")
}
if err := validateActiveChannelIDsPageKey(key); err != nil {
return err
}
if err := validateActiveChannelIDsPage(key, channelIDs); err != nil {
return err
}
raw, err := json.Marshal(activeChannelIDsPageEnvelope{
Schema: activeChannelIDsPageSchemaV1, Key: key, ChannelIDs: channelIDs,
})
if err != nil {
return fmt.Errorf("encode active channel IDs page: %w", err)
}
if len(raw) > activeChannelIDsPageMaxBytes {
return fmt.Errorf("active channel IDs page exceeds %d bytes: %d", activeChannelIDsPageMaxBytes, len(raw))
}
if err := s.c.Set(ctx, activeChannelIDsPageRedisKey(key), raw, s.ttl).Err(); err != nil {
return fmt.Errorf("redis set active channel IDs page: %w", err)
}
return nil
}
func validateActiveChannelIDsPageKey(key store.ActiveChannelIDsPageKey) error {
if key.UserID == 0 || key.Generation == 0 || key.AfterChannelID < 0 ||
key.Limit <= 0 || key.Limit > domain.MaxSynchronousChannelDialogFanout {
return fmt.Errorf("invalid active channel IDs page key")
}
return nil
}
func validateActiveChannelIDsPage(key store.ActiveChannelIDsPageKey, channelIDs []int64) error {
if len(channelIDs) > key.Limit {
return fmt.Errorf("active channel IDs page has %d rows, limit %d", len(channelIDs), key.Limit)
}
previous := key.AfterChannelID
for _, channelID := range channelIDs {
if channelID <= previous {
return fmt.Errorf("active channel IDs page is not strictly ordered after %d", previous)
}
previous = channelID
}
return nil
}
var _ store.ActiveChannelIDsPageCache = (*ActiveChannelIDsPageCache)(nil)