feat: add redis cache and fanout support

This commit is contained in:
richjohnrambo
2026-08-25 20:54:03 +08:00
parent 07376c70e3
commit f900a08a01
68 changed files with 3949 additions and 5696 deletions
+85
View File
@@ -0,0 +1,85 @@
package store
import (
"sort"
"strings"
"sync"
"time"
"github.com/tinode/chat/server/store/types"
)
const configCacheTTL = time.Minute
type configCacheEntry[T any] struct {
value T
expiresAt time.Time
}
type configCache[T any] struct {
mu sync.RWMutex
items map[string]configCacheEntry[T]
}
func newConfigCache[T any]() *configCache[T] {
return &configCache[T]{items: make(map[string]configCacheEntry[T])}
}
func (c *configCache[T]) get(key string) (T, bool) {
var zero T
c.mu.RLock()
entry, ok := c.items[key]
c.mu.RUnlock()
if !ok || time.Now().After(entry.expiresAt) {
return zero, false
}
return entry.value, true
}
func (c *configCache[T]) set(key string, value T) {
c.mu.Lock()
c.items[key] = configCacheEntry[T]{value: value, expiresAt: time.Now().Add(configCacheTTL)}
c.mu.Unlock()
}
var (
tenantByCodeCache = newConfigCache[*types.Tenant]()
tenantByIDCache = newConfigCache[*types.Tenant]()
registrationSMSCache = newConfigCache[bool]()
thirdPartyConfigCache = newConfigCache[map[string]string]()
thirdPartyConfigAllKeys = "*"
)
func cloneTenant(t *types.Tenant) *types.Tenant {
if t == nil {
return nil
}
cp := *t
return &cp
}
func cloneStringMap(src map[string]string) map[string]string {
if src == nil {
return nil
}
dst := make(map[string]string, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}
func thirdPartyCacheKey(group string, keys ...string) string {
group = strings.TrimSpace(group)
if len(keys) == 0 {
return group + ":" + thirdPartyConfigAllKeys
}
normalized := make([]string, 0, len(keys))
for _, key := range keys {
if key = strings.TrimSpace(key); key != "" {
normalized = append(normalized, key)
}
}
sort.Strings(normalized)
return group + ":" + strings.Join(normalized, ",")
}
+866
View File
@@ -0,0 +1,866 @@
package store
import (
"context"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/redis/go-redis/v9"
"github.com/tinode/chat/server/store/types"
)
type redisConfig struct {
Addr string `json:"addr"`
Username string `json:"username"`
Password string `json:"password"`
DB int `json:"db"`
KeyPrefix string `json:"key_prefix"`
DialMS int `json:"dial_timeout_ms"`
ReadMS int `json:"read_timeout_ms"`
WriteMS int `json:"write_timeout_ms"`
PendingTTL int `json:"pending_ttl_seconds"`
DoneTTL int `json:"done_ttl_seconds"`
FailedTTL int `json:"failed_ttl_seconds"`
HotMsgTTL int `json:"hot_message_ttl_seconds"`
PresenceTTL int `json:"presence_ttl_seconds"`
CacheTTL int `json:"cache_ttl_seconds"`
ShortTTL int `json:"short_state_ttl_seconds"`
GroupFanout bool `json:"group_fanout_enabled"`
}
type groupMessageUniqueRecord struct {
Status string `json:"status"`
Seq int64 `json:"seq,omitempty"`
MessageID string `json:"message_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type groupFanoutEnvelope struct {
OriginNodeID string `json:"origin_node_id"`
Message json.RawMessage `json:"message"`
}
type groupMessageRuntime interface {
Close() error
Claim(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) (*groupMessageUniqueRecord, bool, error)
NextSeq(tenantID types.TenantID, topic string, baseline int64) (int64, error)
MarkDone(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string, seq int64, msgID string) error
MarkFailed(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) error
CacheHotMessage(tenantID types.TenantID, msg *types.Message) error
GetHotMessages(tenantID types.TenantID, topic string, opts *types.QueryOpt, maxLimit int) ([]types.Message, bool, error)
InvalidateHotMessages(tenantID types.TenantID, topic string, ranges []types.Range) error
MarkUserOnline(tenantID types.TenantID, uid types.Uid, sid, nodeID, device, userAgent string) error
RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error
MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error
GetUserPresence(tenantID types.TenantID, uid types.Uid) (*UserPresence, error)
MarkTopicMemberOnline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error
MarkTopicMemberOffline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error
GetTopicNodeOnlineMembers(tenantID types.TenantID, topic, nodeID string) ([]types.Uid, error)
PublishGroupFanout(originNodeID string, message any) error
StartGroupFanoutSubscriber(ctx context.Context, nodeID string, handler func([]byte)) error
CacheObject(tenantID types.TenantID, namespace, id string, value any) error
GetCachedObject(tenantID types.TenantID, namespace, id string, value any) (bool, error)
DeleteCachedObject(tenantID types.TenantID, namespace, id string) error
AddCachedSetMember(tenantID types.TenantID, namespace, id, member string) error
RemoveCachedSetMember(tenantID types.TenantID, namespace, id, member string) error
CachedSetContains(tenantID types.TenantID, namespace, id, member string) (bool, error)
AllowRate(tenantID types.TenantID, scope string, limit int, window time.Duration) (bool, int, time.Duration, error)
}
var groupMsgRuntime groupMessageRuntime
type UserPresence struct {
Online bool `json:"online"`
NodeID string `json:"node_id"`
SessionCount int `json:"session_count"`
LastActiveAt time.Time `json:"last_active_at"`
LastSeenAt time.Time `json:"last_seen_at"`
}
func MarkUserOnline(tenantID types.TenantID, uid types.Uid, sid, nodeID, device, userAgent string) error {
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() || sid == "" {
return nil
}
return groupMsgRuntime.MarkUserOnline(tenantID, uid, sid, nodeID, device, userAgent)
}
func RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() || sid == "" {
return nil
}
return groupMsgRuntime.RefreshUserPresence(tenantID, uid, sid, nodeID)
}
func MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() || sid == "" {
return nil
}
return groupMsgRuntime.MarkUserOffline(tenantID, uid, sid, nodeID)
}
func GetUserPresence(tenantID types.TenantID, uid types.Uid) (*UserPresence, error) {
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() {
return &UserPresence{}, nil
}
return groupMsgRuntime.GetUserPresence(tenantID, uid)
}
func UserPresenceEnabled() bool {
return groupMsgRuntime != nil
}
func MarkTopicMemberOnline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error {
if groupMsgRuntime == nil || !tenantID.IsValid() || topic == "" || uid.IsZero() {
return nil
}
return groupMsgRuntime.MarkTopicMemberOnline(tenantID, topic, uid, nodeID)
}
func MarkTopicMemberOffline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error {
if groupMsgRuntime == nil || !tenantID.IsValid() || topic == "" || uid.IsZero() {
return nil
}
return groupMsgRuntime.MarkTopicMemberOffline(tenantID, topic, uid, nodeID)
}
func GetTopicNodeOnlineMembers(tenantID types.TenantID, topic, nodeID string) ([]types.Uid, error) {
if groupMsgRuntime == nil || !tenantID.IsValid() || topic == "" {
return nil, nil
}
return groupMsgRuntime.GetTopicNodeOnlineMembers(tenantID, topic, nodeID)
}
func PublishGroupFanout(originNodeID string, message any) error {
if groupMsgRuntime == nil || originNodeID == "" || message == nil {
return nil
}
return groupMsgRuntime.PublishGroupFanout(originNodeID, message)
}
func StartGroupFanoutSubscriber(ctx context.Context, nodeID string, handler func([]byte)) error {
if groupMsgRuntime == nil || nodeID == "" || handler == nil {
return nil
}
return groupMsgRuntime.StartGroupFanoutSubscriber(ctx, nodeID, handler)
}
func CacheObject(tenantID types.TenantID, namespace, id string, value any) error {
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || value == nil {
return nil
}
return groupMsgRuntime.CacheObject(tenantID, namespace, id, value)
}
func GetCachedObject(tenantID types.TenantID, namespace, id string, value any) (bool, error) {
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || value == nil {
return false, nil
}
return groupMsgRuntime.GetCachedObject(tenantID, namespace, id, value)
}
func DeleteCachedObject(tenantID types.TenantID, namespace, id string) error {
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" {
return nil
}
return groupMsgRuntime.DeleteCachedObject(tenantID, namespace, id)
}
func AddCachedSetMember(tenantID types.TenantID, namespace, id, member string) error {
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || member == "" {
return nil
}
return groupMsgRuntime.AddCachedSetMember(tenantID, namespace, id, member)
}
func RemoveCachedSetMember(tenantID types.TenantID, namespace, id, member string) error {
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || member == "" {
return nil
}
return groupMsgRuntime.RemoveCachedSetMember(tenantID, namespace, id, member)
}
func CachedSetContains(tenantID types.TenantID, namespace, id, member string) (bool, error) {
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || member == "" {
return false, nil
}
return groupMsgRuntime.CachedSetContains(tenantID, namespace, id, member)
}
func AllowRate(tenantID types.TenantID, scope string, limit int, window time.Duration) (bool, int, time.Duration, error) {
if groupMsgRuntime == nil || !tenantID.IsValid() || scope == "" || limit <= 0 || window <= 0 {
return true, 0, 0, nil
}
return groupMsgRuntime.AllowRate(tenantID, scope, limit, window)
}
func initGroupMessageRuntime(raw json.RawMessage) error {
closeGroupMessageRuntime()
if len(raw) == 0 || string(raw) == "null" {
return nil
}
var cfg redisConfig
if err := json.Unmarshal(raw, &cfg); err != nil {
return err
}
cfg.Addr = strings.TrimSpace(cfg.Addr)
if cfg.Addr == "" {
return nil
}
if cfg.KeyPrefix == "" {
cfg.KeyPrefix = "im"
}
if cfg.PendingTTL <= 0 {
cfg.PendingTTL = 120
}
if cfg.DoneTTL <= 0 {
cfg.DoneTTL = 86400
}
if cfg.FailedTTL <= 0 {
cfg.FailedTTL = 600
}
if cfg.HotMsgTTL <= 0 {
cfg.HotMsgTTL = 86400
}
if cfg.PresenceTTL <= 0 {
cfg.PresenceTTL = 120
}
if cfg.CacheTTL <= 0 {
cfg.CacheTTL = 300
}
if cfg.ShortTTL <= 0 {
cfg.ShortTTL = 600
}
dialTimeout := timeoutOrDefault(cfg.DialMS, 2*time.Second)
readTimeout := timeoutOrDefault(cfg.ReadMS, 2*time.Second)
writeTimeout := timeoutOrDefault(cfg.WriteMS, 2*time.Second)
client := redis.NewClient(&redis.Options{
Addr: cfg.Addr,
Username: cfg.Username,
Password: cfg.Password,
DB: cfg.DB,
DialTimeout: dialTimeout,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
})
ctx, cancel := context.WithTimeout(context.Background(), dialTimeout)
defer cancel()
if err := client.Ping(ctx).Err(); err != nil {
_ = client.Close()
return err
}
groupMsgRuntime = &redisGroupMessageRuntime{client: client, prefix: cfg.KeyPrefix, cfg: cfg}
return nil
}
func closeGroupMessageRuntime() {
if groupMsgRuntime != nil {
_ = groupMsgRuntime.Close()
groupMsgRuntime = nil
}
}
func timeoutOrDefault(ms int, fallback time.Duration) time.Duration {
if ms <= 0 {
return fallback
}
return time.Duration(ms) * time.Millisecond
}
type redisGroupMessageRuntime struct {
client *redis.Client
prefix string
cfg redisConfig
}
func (r *redisGroupMessageRuntime) Close() error {
return r.client.Close()
}
func (r *redisGroupMessageRuntime) Claim(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) (*groupMessageUniqueRecord, bool, error) {
if clientMsgID == "" {
return nil, true, nil
}
key := r.uniqueKey(tenantID, topic, from, clientMsgID)
rec := &groupMessageUniqueRecord{Status: "pending", CreatedAt: time.Now().UTC()}
raw, _ := json.Marshal(rec)
ctx := context.Background()
ok, err := r.client.SetNX(ctx, key, raw, time.Duration(r.cfg.PendingTTL)*time.Second).Result()
if err != nil {
return nil, false, err
}
if ok {
return rec, true, nil
}
existing, err := r.client.Get(ctx, key).Bytes()
if err == redis.Nil {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
var current groupMessageUniqueRecord
if err = json.Unmarshal(existing, &current); err != nil {
return nil, false, err
}
if current.Status == "failed" {
_ = r.client.Del(ctx, key).Err()
ok, err = r.client.SetNX(ctx, key, raw, time.Duration(r.cfg.PendingTTL)*time.Second).Result()
if err != nil {
return nil, false, err
}
if ok {
return rec, true, nil
}
}
return &current, false, nil
}
func (r *redisGroupMessageRuntime) NextSeq(tenantID types.TenantID, topic string, baseline int64) (int64, error) {
if baseline < 0 {
baseline = 0
}
seqKey := r.seqKey(tenantID, topic)
seq, err := r.client.Eval(context.Background(), `
if redis.call("EXISTS", KEYS[1]) == 0 then
redis.call("SET", KEYS[1], ARGV[1])
end
return redis.call("INCR", KEYS[1])
`, []string{seqKey}, baseline).Int64()
if err != nil {
return 0, err
}
return seq, nil
}
func (r *redisGroupMessageRuntime) MarkDone(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string, seq int64, msgID string) error {
if clientMsgID == "" {
return nil
}
rec := &groupMessageUniqueRecord{Status: "done", Seq: seq, MessageID: msgID, CreatedAt: time.Now().UTC()}
raw, _ := json.Marshal(rec)
return r.client.Set(context.Background(), r.uniqueKey(tenantID, topic, from, clientMsgID), raw, time.Duration(r.cfg.DoneTTL)*time.Second).Err()
}
func (r *redisGroupMessageRuntime) MarkFailed(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) error {
if clientMsgID == "" {
return nil
}
rec := &groupMessageUniqueRecord{Status: "failed", CreatedAt: time.Now().UTC()}
raw, _ := json.Marshal(rec)
return r.client.Set(context.Background(), r.uniqueKey(tenantID, topic, from, clientMsgID), raw, time.Duration(r.cfg.FailedTTL)*time.Second).Err()
}
func (r *redisGroupMessageRuntime) CacheHotMessage(tenantID types.TenantID, msg *types.Message) error {
if msg == nil || msg.Topic == "" || msg.SeqId <= 0 {
return types.ErrMalformed
}
payload, err := json.Marshal(msg)
if err != nil {
return err
}
ctx := context.Background()
ttl := time.Duration(r.cfg.HotMsgTTL) * time.Second
seq := strconv.FormatInt(msg.SeqId, 10)
pipe := r.client.Pipeline()
pipe.Set(ctx, r.hotMsgKey(tenantID, msg.Topic, msg.SeqId), payload, ttl)
pipe.ZAdd(ctx, r.hotSeqKey(tenantID, msg.Topic), redis.Z{Score: float64(msg.SeqId), Member: seq})
pipe.Expire(ctx, r.hotSeqKey(tenantID, msg.Topic), ttl+time.Hour)
_, err = pipe.Exec(ctx)
return err
}
func (r *redisGroupMessageRuntime) GetHotMessages(tenantID types.TenantID, topic string, opts *types.QueryOpt, maxLimit int) ([]types.Message, bool, error) {
if topic == "" {
return nil, false, types.ErrMalformed
}
limit := maxLimit
if limit <= 0 {
limit = 100
}
if opts != nil && opts.Limit > 0 && opts.Limit < limit {
limit = opts.Limit
}
seqs, completeRange, err := r.hotSeqIDs(tenantID, topic, opts, limit)
if err != nil || len(seqs) == 0 || !completeRange {
return nil, false, err
}
keys := make([]string, 0, len(seqs))
for _, seq := range seqs {
keys = append(keys, r.hotMsgKey(tenantID, topic, seq))
}
values, err := r.client.MGet(context.Background(), keys...).Result()
if err != nil {
return nil, false, err
}
if len(values) != len(seqs) {
return nil, false, nil
}
msgs := make([]types.Message, 0, len(values))
for _, value := range values {
if value == nil {
return nil, false, nil
}
raw, ok := value.(string)
if !ok {
return nil, false, nil
}
var msg types.Message
if err := json.Unmarshal([]byte(raw), &msg); err != nil {
return nil, false, err
}
msg.TenantID = tenantID
msgs = append(msgs, msg)
}
return msgs, true, nil
}
func (r *redisGroupMessageRuntime) InvalidateHotMessages(tenantID types.TenantID, topic string, ranges []types.Range) error {
if topic == "" {
return types.ErrMalformed
}
seqs := expandRanges(ranges, int(^uint(0)>>1))
ctx := context.Background()
if len(seqs) == 0 {
pipe := r.client.Pipeline()
pipe.Del(ctx, r.hotSeqKey(tenantID, topic))
keys, err := r.client.Keys(ctx, r.hotMsgKey(tenantID, topic, "*")).Result()
if err != nil {
return err
}
if len(keys) > 0 {
pipe.Del(ctx, keys...)
}
_, err = pipe.Exec(ctx)
return err
}
pipe := r.client.Pipeline()
members := make([]any, 0, len(seqs))
for _, seq := range seqs {
pipe.Del(ctx, r.hotMsgKey(tenantID, topic, seq))
members = append(members, strconv.FormatInt(int64(seq), 10))
}
pipe.ZRem(ctx, r.hotSeqKey(tenantID, topic), members...)
_, err := pipe.Exec(ctx)
return err
}
func (r *redisGroupMessageRuntime) MarkUserOnline(tenantID types.TenantID, uid types.Uid, sid, nodeID, device, userAgent string) error {
if nodeID == "" {
nodeID = "local"
}
now := time.Now().UTC()
ttl := time.Duration(r.cfg.PresenceTTL) * time.Second
ctx := context.Background()
userKey := r.presenceUserKey(tenantID, uid)
sessionsKey := r.presenceSessionsKey(tenantID, uid)
sessionKey := r.presenceSessionKey(tenantID, uid, sid)
nodeKey := r.presenceNodeUsersKey(tenantID, nodeID)
member := nodeID + ":" + sid
pipe := r.client.Pipeline()
pipe.HSet(ctx, sessionKey, map[string]any{
"node_id": nodeID,
"session_id": sid,
"device": device,
"user_agent": userAgent,
"last_active_at": now.Format(time.RFC3339Nano),
})
pipe.Expire(ctx, sessionKey, ttl)
pipe.SAdd(ctx, sessionsKey, member)
pipe.Expire(ctx, sessionsKey, ttl)
pipe.SAdd(ctx, nodeKey, uid.UserId())
pipe.Expire(ctx, nodeKey, ttl)
pipe.HSet(ctx, userKey, map[string]any{
"online": "1",
"node_id": nodeID,
"last_active_at": now.Format(time.RFC3339Nano),
"last_seen_at": now.Format(time.RFC3339Nano),
})
pipe.Expire(ctx, userKey, ttl)
if _, err := pipe.Exec(ctx); err != nil {
return err
}
count, err := r.client.SCard(ctx, sessionsKey).Result()
if err != nil {
return err
}
return r.client.HSet(ctx, userKey, "session_count", count).Err()
}
func (r *redisGroupMessageRuntime) RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
if nodeID == "" {
nodeID = "local"
}
now := time.Now().UTC()
ttl := time.Duration(r.cfg.PresenceTTL) * time.Second
ctx := context.Background()
userKey := r.presenceUserKey(tenantID, uid)
sessionsKey := r.presenceSessionsKey(tenantID, uid)
sessionKey := r.presenceSessionKey(tenantID, uid, sid)
nodeKey := r.presenceNodeUsersKey(tenantID, nodeID)
pipe := r.client.Pipeline()
pipe.HSet(ctx, sessionKey, "last_active_at", now.Format(time.RFC3339Nano), "node_id", nodeID)
pipe.Expire(ctx, sessionKey, ttl)
pipe.Expire(ctx, sessionsKey, ttl)
pipe.SAdd(ctx, nodeKey, uid.UserId())
pipe.Expire(ctx, nodeKey, ttl)
pipe.HSet(ctx, userKey, "online", "1", "node_id", nodeID, "last_active_at", now.Format(time.RFC3339Nano), "last_seen_at", now.Format(time.RFC3339Nano))
pipe.Expire(ctx, userKey, ttl)
_, err := pipe.Exec(ctx)
return err
}
func (r *redisGroupMessageRuntime) MarkUserOffline(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
if nodeID == "" {
nodeID = "local"
}
now := time.Now().UTC()
ttl := time.Duration(r.cfg.PresenceTTL) * time.Second
ctx := context.Background()
userKey := r.presenceUserKey(tenantID, uid)
sessionsKey := r.presenceSessionsKey(tenantID, uid)
sessionKey := r.presenceSessionKey(tenantID, uid, sid)
nodeKey := r.presenceNodeUsersKey(tenantID, nodeID)
member := nodeID + ":" + sid
pipe := r.client.Pipeline()
pipe.Del(ctx, sessionKey)
pipe.SRem(ctx, sessionsKey, member)
pipe.SRem(ctx, nodeKey, uid.UserId())
if _, err := pipe.Exec(ctx); err != nil {
return err
}
count, err := r.client.SCard(ctx, sessionsKey).Result()
if err != nil {
return err
}
if count > 0 {
pipe = r.client.Pipeline()
pipe.HSet(ctx, userKey, "online", "1", "session_count", count, "last_seen_at", now.Format(time.RFC3339Nano))
pipe.Expire(ctx, userKey, ttl)
pipe.Expire(ctx, sessionsKey, ttl)
_, err = pipe.Exec(ctx)
return err
}
pipe = r.client.Pipeline()
pipe.HSet(ctx, userKey, "online", "0", "session_count", 0, "node_id", nodeID, "last_seen_at", now.Format(time.RFC3339Nano))
pipe.Expire(ctx, userKey, ttl)
pipe.Del(ctx, sessionsKey)
_, err = pipe.Exec(ctx)
return err
}
func (r *redisGroupMessageRuntime) GetUserPresence(tenantID types.TenantID, uid types.Uid) (*UserPresence, error) {
raw, err := r.client.HGetAll(context.Background(), r.presenceUserKey(tenantID, uid)).Result()
if err != nil {
return nil, err
}
presence := &UserPresence{}
if len(raw) == 0 {
return presence, nil
}
presence.Online = raw["online"] == "1"
presence.NodeID = raw["node_id"]
presence.SessionCount, _ = strconv.Atoi(raw["session_count"])
presence.LastActiveAt, _ = time.Parse(time.RFC3339Nano, raw["last_active_at"])
presence.LastSeenAt, _ = time.Parse(time.RFC3339Nano, raw["last_seen_at"])
return presence, nil
}
func (r *redisGroupMessageRuntime) MarkTopicMemberOnline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error {
if nodeID == "" {
nodeID = "local"
}
ttl := time.Duration(r.cfg.PresenceTTL) * time.Second
ctx := context.Background()
pipe := r.client.Pipeline()
pipe.SAdd(ctx, r.topicOnlineMembersKey(tenantID, topic), uid.UserId())
pipe.Expire(ctx, r.topicOnlineMembersKey(tenantID, topic), ttl)
pipe.SAdd(ctx, r.topicNodeOnlineMembersKey(tenantID, topic, nodeID), uid.UserId())
pipe.Expire(ctx, r.topicNodeOnlineMembersKey(tenantID, topic, nodeID), ttl)
_, err := pipe.Exec(ctx)
return err
}
func (r *redisGroupMessageRuntime) MarkTopicMemberOffline(tenantID types.TenantID, topic string, uid types.Uid, nodeID string) error {
if nodeID == "" {
nodeID = "local"
}
ctx := context.Background()
pipe := r.client.Pipeline()
pipe.SRem(ctx, r.topicOnlineMembersKey(tenantID, topic), uid.UserId())
pipe.SRem(ctx, r.topicNodeOnlineMembersKey(tenantID, topic, nodeID), uid.UserId())
_, err := pipe.Exec(ctx)
return err
}
func (r *redisGroupMessageRuntime) GetTopicNodeOnlineMembers(tenantID types.TenantID, topic, nodeID string) ([]types.Uid, error) {
if nodeID == "" {
nodeID = "local"
}
members, err := r.client.SMembers(context.Background(), r.topicNodeOnlineMembersKey(tenantID, topic, nodeID)).Result()
if err != nil {
return nil, err
}
uids := make([]types.Uid, 0, len(members))
for _, member := range members {
uid := types.ParseUserId(member)
if !uid.IsZero() {
uids = append(uids, uid)
}
}
return uids, nil
}
func (r *redisGroupMessageRuntime) PublishGroupFanout(originNodeID string, message any) error {
if !r.cfg.GroupFanout {
return nil
}
payload, err := json.Marshal(message)
if err != nil {
return err
}
envelope, err := json.Marshal(groupFanoutEnvelope{
OriginNodeID: originNodeID,
Message: payload,
})
if err != nil {
return err
}
return r.client.Publish(context.Background(), r.groupFanoutChannel(), envelope).Err()
}
func (r *redisGroupMessageRuntime) StartGroupFanoutSubscriber(ctx context.Context, nodeID string, handler func([]byte)) error {
if !r.cfg.GroupFanout {
return nil
}
pubsub := r.client.Subscribe(ctx, r.groupFanoutChannel())
if _, err := pubsub.Receive(ctx); err != nil {
_ = pubsub.Close()
return err
}
go func() {
defer pubsub.Close()
ch := pubsub.Channel()
for {
select {
case <-ctx.Done():
return
case msg, ok := <-ch:
if !ok {
return
}
var envelope groupFanoutEnvelope
if err := json.Unmarshal([]byte(msg.Payload), &envelope); err != nil || envelope.OriginNodeID == nodeID || len(envelope.Message) == 0 {
continue
}
handler(envelope.Message)
}
}
}()
return nil
}
func (r *redisGroupMessageRuntime) CacheObject(tenantID types.TenantID, namespace, id string, value any) error {
payload, err := json.Marshal(value)
if err != nil {
return err
}
return r.client.Set(context.Background(), r.objectCacheKey(tenantID, namespace, id), payload, r.cacheTTL(namespace)).Err()
}
func (r *redisGroupMessageRuntime) GetCachedObject(tenantID types.TenantID, namespace, id string, value any) (bool, error) {
payload, err := r.client.Get(context.Background(), r.objectCacheKey(tenantID, namespace, id)).Bytes()
if err == redis.Nil {
return false, nil
}
if err != nil {
return false, err
}
if err = json.Unmarshal(payload, value); err != nil {
_ = r.client.Del(context.Background(), r.objectCacheKey(tenantID, namespace, id)).Err()
return false, err
}
return true, nil
}
func (r *redisGroupMessageRuntime) DeleteCachedObject(tenantID types.TenantID, namespace, id string) error {
return r.client.Del(context.Background(), r.objectCacheKey(tenantID, namespace, id)).Err()
}
func (r *redisGroupMessageRuntime) AddCachedSetMember(tenantID types.TenantID, namespace, id, member string) error {
key := r.setCacheKey(tenantID, namespace, id)
ctx := context.Background()
pipe := r.client.Pipeline()
pipe.SAdd(ctx, key, member)
pipe.Expire(ctx, key, r.cacheTTL(namespace))
_, err := pipe.Exec(ctx)
return err
}
func (r *redisGroupMessageRuntime) RemoveCachedSetMember(tenantID types.TenantID, namespace, id, member string) error {
key := r.setCacheKey(tenantID, namespace, id)
ctx := context.Background()
pipe := r.client.Pipeline()
pipe.SRem(ctx, key, member)
pipe.Expire(ctx, key, r.cacheTTL(namespace))
_, err := pipe.Exec(ctx)
return err
}
func (r *redisGroupMessageRuntime) CachedSetContains(tenantID types.TenantID, namespace, id, member string) (bool, error) {
return r.client.SIsMember(context.Background(), r.setCacheKey(tenantID, namespace, id), member).Result()
}
func (r *redisGroupMessageRuntime) AllowRate(tenantID types.TenantID, scope string, limit int, window time.Duration) (bool, int, time.Duration, error) {
key := r.rateKey(tenantID, scope)
ctx := context.Background()
count, err := r.client.Incr(ctx, key).Result()
if err != nil {
return true, 0, 0, err
}
if count == 1 {
if err = r.client.Expire(ctx, key, window).Err(); err != nil {
return true, int(count), 0, err
}
}
ttl, _ := r.client.TTL(ctx, key).Result()
return count <= int64(limit), int(count), ttl, nil
}
func (r *redisGroupMessageRuntime) hotSeqIDs(tenantID types.TenantID, topic string, opts *types.QueryOpt, limit int) ([]int64, bool, error) {
if opts != nil && len(opts.IdRanges) > 0 {
seqs := expandRanges(opts.IdRanges, limit)
sort.Slice(seqs, func(i, j int) bool { return seqs[i] > seqs[j] })
return seqs, len(seqs) > 0 && len(seqs) <= limit, nil
}
min := "-inf"
max := "+inf"
if opts != nil {
if opts.Since > 0 {
min = strconv.FormatInt(opts.Since, 10)
}
if opts.Before > 1 {
max = strconv.FormatInt(opts.Before-1, 10)
}
}
raw, err := r.client.ZRevRangeByScore(context.Background(), r.hotSeqKey(tenantID, topic), &redis.ZRangeBy{
Min: min,
Max: max,
Offset: 0,
Count: int64(limit),
}).Result()
if err != nil {
return nil, false, err
}
if len(raw) < limit {
return nil, false, nil
}
seqs := make([]int64, 0, len(raw))
for _, value := range raw {
seq, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, false, err
}
seqs = append(seqs, seq)
}
return seqs, true, nil
}
func expandRanges(ranges []types.Range, limit int) []int64 {
if limit <= 0 {
return nil
}
seqs := make([]int64, 0)
for _, rng := range ranges {
if rng.Low <= 0 {
continue
}
if rng.Hi == 0 {
seqs = append(seqs, rng.Low)
} else {
for seq := rng.Low; seq < rng.Hi; seq++ {
seqs = append(seqs, seq)
if len(seqs) >= limit {
return seqs
}
}
}
if len(seqs) >= limit {
return seqs
}
}
return seqs
}
func (r *redisGroupMessageRuntime) seqKey(tenantID types.TenantID, topic string) string {
return r.key(tenantID, "topic", topic, "last_seq")
}
func (r *redisGroupMessageRuntime) uniqueKey(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) string {
return r.key(tenantID, "topic", topic, "uniq", "msg", from.UserId(), clientMsgID)
}
func (r *redisGroupMessageRuntime) hotSeqKey(tenantID types.TenantID, topic string) string {
return r.key(tenantID, "topic", topic, "hot_msg_seqs")
}
func (r *redisGroupMessageRuntime) hotMsgKey(tenantID types.TenantID, topic string, seq any) string {
return r.key(tenantID, "topic", topic, "hot_msg", fmt.Sprint(seq))
}
func (r *redisGroupMessageRuntime) presenceUserKey(tenantID types.TenantID, uid types.Uid) string {
return r.key(tenantID, "presence", "user", uid.UserId())
}
func (r *redisGroupMessageRuntime) presenceSessionsKey(tenantID types.TenantID, uid types.Uid) string {
return r.key(tenantID, "presence", "user", uid.UserId(), "sessions")
}
func (r *redisGroupMessageRuntime) presenceSessionKey(tenantID types.TenantID, uid types.Uid, sid string) string {
return r.key(tenantID, "presence", "user", uid.UserId(), "session", sid)
}
func (r *redisGroupMessageRuntime) presenceNodeUsersKey(tenantID types.TenantID, nodeID string) string {
return r.key(tenantID, "presence", "node", nodeID, "users")
}
func (r *redisGroupMessageRuntime) topicOnlineMembersKey(tenantID types.TenantID, topic string) string {
return r.key(tenantID, "topic", topic, "online_members")
}
func (r *redisGroupMessageRuntime) topicNodeOnlineMembersKey(tenantID types.TenantID, topic, nodeID string) string {
return r.key(tenantID, "topic", topic, "node", nodeID, "online_members")
}
func (r *redisGroupMessageRuntime) groupFanoutChannel() string {
return r.key(types.ZeroTenantID, "group_fanout")
}
func (r *redisGroupMessageRuntime) objectCacheKey(tenantID types.TenantID, namespace, id string) string {
return r.key(tenantID, "cache", namespace, id)
}
func (r *redisGroupMessageRuntime) setCacheKey(tenantID types.TenantID, namespace, id string) string {
return r.key(tenantID, "set", namespace, id)
}
func (r *redisGroupMessageRuntime) rateKey(tenantID types.TenantID, scope string) string {
return r.key(tenantID, "rate", scope)
}
func (r *redisGroupMessageRuntime) cacheTTL(namespace string) time.Duration {
ttl := r.cfg.CacheTTL
if namespace == "verification" || namespace == "action_token" {
ttl = r.cfg.ShortTTL
}
return time.Duration(ttl) * time.Second
}
func (r *redisGroupMessageRuntime) key(tenantID types.TenantID, parts ...string) string {
all := []string{strings.TrimSuffix(r.prefix, ":"), strconv.FormatInt(int64(tenantID), 10)}
all = append(all, parts...)
return strings.Join(all, ":")
}
+3 -3
View File
@@ -1114,7 +1114,7 @@ func (mr *MockMessagesPersistenceInterfaceMockRecorder) DeleteList(tenantID, top
}
// Edit mocks base method.
func (m *MockMessagesPersistenceInterface) Edit(tenantID types.TenantID, topic string, seq int, actor types.Uid, expectedVersion int, head types.KVMap, content any, reason string, editedAt time.Time) (*types.Message, error) {
func (m *MockMessagesPersistenceInterface) Edit(tenantID types.TenantID, topic string, seq int64, actor types.Uid, expectedVersion int, head types.KVMap, content any, reason string, editedAt time.Time) (*types.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Edit", tenantID, topic, seq, actor, expectedVersion, head, content, reason, editedAt)
ret0, _ := ret[0].(*types.Message)
@@ -1176,7 +1176,7 @@ func (mr *MockMessagesPersistenceInterfaceMockRecorder) Search(tenantID, topic,
}
// GetOne mocks base method.
func (m *MockMessagesPersistenceInterface) GetOne(tenantID types.TenantID, topic string, seq int) (*types.Message, error) {
func (m *MockMessagesPersistenceInterface) GetOne(tenantID types.TenantID, topic string, seq int64) (*types.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetOne", tenantID, topic, seq)
ret0, _ := ret[0].(*types.Message)
@@ -1191,7 +1191,7 @@ func (mr *MockMessagesPersistenceInterfaceMockRecorder) GetOne(tenantID, topic,
}
// Recall mocks base method.
func (m *MockMessagesPersistenceInterface) Recall(tenantID types.TenantID, topic string, seq int, actor types.Uid, expectedVersion int, recalledAt time.Time) (*types.Message, error) {
func (m *MockMessagesPersistenceInterface) Recall(tenantID types.TenantID, topic string, seq int64, actor types.Uid, expectedVersion int, recalledAt time.Time) (*types.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Recall", tenantID, topic, seq, actor, expectedVersion, recalledAt)
ret0, _ := ret[0].(*types.Message)
+645 -47
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -16,7 +16,7 @@ type CallRecord struct {
TenantID TenantID `db:"tenant_id" json:"-" bson:"tenant_id"`
CallID string `db:"call_id"`
Topic string
SeqID int `db:"seq_id"`
SeqID int64 `db:"seq_id"`
CallerID Uid `db:"caller_id"`
CalleeID Uid `db:"callee_id"`
CallType string `db:"call_type"`
+1 -1
View File
@@ -9,7 +9,7 @@ type FavoriteItem struct {
UserID Uid `db:"user_id"`
ItemType string `db:"item_type"`
SourceTopic string `db:"source_topic"`
SourceSeq int `db:"source_seq"`
SourceSeq int64 `db:"source_seq"`
Snapshot any
AttachmentRefs any `db:"attachment_refs"`
Availability string `db:"availability"`
+1 -1
View File
@@ -187,7 +187,7 @@ type GroupFile struct {
Topic string `db:"topic"`
FileID string `db:"file_id"`
SourceTopic string `db:"source_topic"`
SourceSeq int `db:"source_seq"`
SourceSeq int64 `db:"source_seq"`
UploaderID Uid `db:"-"`
FileName string `db:"file_name"`
MIMEType string `db:"mime_type"`
+1 -1
View File
@@ -63,7 +63,7 @@ type AbuseReport struct {
TargetType string
TargetUserID Uid
TargetTopic string
TargetSeq int
TargetSeq int64
ReasonCode string
Description string
EvidenceFileIDs []string
+14 -14
View File
@@ -922,9 +922,9 @@ type Subscription struct {
// ID of the latest Soft-delete operation
DelId int
// Last SeqId reported by user as received by at least one of his sessions
RecvSeqId int
RecvSeqId int64
// Last SeqID reported read by the user
ReadSeqId int
ReadSeqId int64
// Access mode requested by this user
ModeWant AccessMode
@@ -941,7 +941,7 @@ type Subscription struct {
// In case of P2P topics this is the Trusted value of the other user.
trusted any
// deserialized SeqID from user or topic
seqId int
seqId int64
// Deserialized TouchedAt from topic
touchedAt time.Time
// Timestamp & user agent of when the user was last online.
@@ -1013,12 +1013,12 @@ func (s *Subscription) LastModified() time.Time {
}
// GetSeqId returns seqId.
func (s *Subscription) GetSeqId() int {
func (s *Subscription) GetSeqId() int64 {
return s.seqId
}
// SetSeqId sets seqId field.
func (s *Subscription) SetSeqId(id int) {
func (s *Subscription) SetSeqId(id int64) {
s.seqId = id
}
@@ -1144,7 +1144,7 @@ type Topic struct {
Access DefaultAccess
// Server-issued sequential ID
SeqId int
SeqId int64
// If messages were deleted, sequential id of the last operation to delete them
DelId int
@@ -1239,7 +1239,7 @@ type Message struct {
DelId int `json:"DelId,omitempty" bson:",omitempty"`
// List of users who have marked this message as soft-deleted
DeletedFor []SoftDelete `json:"DeletedFor,omitempty" bson:",omitempty"`
SeqId int
SeqId int64
Topic string
// Sender's user ID as string (without 'usr' prefix), could be empty.
From string
@@ -1256,7 +1256,7 @@ type MessageRevision struct {
ObjHeader `bson:",inline"`
TenantID TenantID `db:"tenant_id" json:"-" bson:"tenant_id"`
Topic string
SeqId int
SeqId int64
Version int
Actor string
Head KVMap `json:"Head,omitempty" bson:",omitempty"`
@@ -1266,7 +1266,7 @@ type MessageRevision struct {
// MessageSaveResult describes the outcome of a message persistence attempt.
type MessageSaveResult struct {
SeqId int
SeqId int64
MarkedReadBySender bool
Duplicate bool
}
@@ -1274,8 +1274,8 @@ type MessageSaveResult struct {
// Range is a range of message SeqIDs. Low end is inclusive (closed), high end is exclusive (open): [Low, Hi).
// If the range contains just one ID, Hi is set to 0
type Range struct {
Low int
Hi int `json:"Hi,omitempty" bson:",omitempty"`
Low int64
Hi int64 `json:"Hi,omitempty" bson:",omitempty"`
}
// RangeSorter is a helper type required by 'sort' package.
@@ -1334,7 +1334,7 @@ func (rs RangeSorter) Normalize() RangeSorter {
// Convert a slice of int values to a slice of ranges.
// The int slice must be sorted low -> high.
func SliceToRanges(in []int) []Range {
func SliceToRanges(in []int64) []Range {
if len(in) == 0 {
return nil
}
@@ -1390,8 +1390,8 @@ type QueryOpt struct {
Topic string
IfModifiedSince *time.Time
// ID-based query parameters: Messages
Since int
Before int
Since int64
Before int64
// Common parameter
Limit int
// Ranges of IDs.