fix(server): coalesce platform evict fanout
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/auth"
|
||||
@@ -27,6 +28,29 @@ const (
|
||||
realtimeTargetUsers = "users"
|
||||
)
|
||||
|
||||
const platformEvictFanoutDebounce = 750 * time.Millisecond
|
||||
|
||||
type platformEvictFanoutKey struct {
|
||||
tenantID types.TenantID
|
||||
uid types.Uid
|
||||
platf string
|
||||
reason string
|
||||
}
|
||||
|
||||
type platformEvictFanoutPending struct {
|
||||
skipSid string
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
var platformEvictFanout = struct {
|
||||
sync.Mutex
|
||||
pending map[platformEvictFanoutKey]*platformEvictFanoutPending
|
||||
}{
|
||||
pending: make(map[platformEvictFanoutKey]*platformEvictFanoutPending),
|
||||
}
|
||||
|
||||
var publishRealtimeFanout = store.PublishRealtimeFanout
|
||||
|
||||
type realtimeGroupUnreadRecipient struct {
|
||||
User string `json:"user"`
|
||||
Source string `json:"source"`
|
||||
@@ -69,7 +93,7 @@ func publishRealtimeTopicMessage(topic string, msg *ServerComMessage) {
|
||||
return
|
||||
}
|
||||
dedupKey := realtimeMessageDedupKey(topic, msg)
|
||||
if err := store.PublishRealtimeFanout(currentNodeID(), msg.TenantID, realtimeKindMessage, realtimeTargetTopic, topic, msg.SkipSid, dedupKey, msg); err != nil {
|
||||
if err := publishRealtimeFanout(currentNodeID(), msg.TenantID, realtimeKindMessage, realtimeTargetTopic, topic, msg.SkipSid, dedupKey, msg); err != nil {
|
||||
immetrics.IncRealtimeFanout("publish", realtimeKindMessage, realtimeTargetTopic, "error")
|
||||
logs.Warn.Printf("redis realtime fanout: publish topic=%s failed: %v", topic, err)
|
||||
return
|
||||
@@ -89,7 +113,7 @@ func publishRealtimeUserMessage(uid types.Uid, msg *ServerComMessage) {
|
||||
return
|
||||
}
|
||||
dedupKey := realtimeMessageDedupKey(uid.UserId(), msg)
|
||||
if err := store.PublishRealtimeFanout(currentNodeID(), msg.TenantID, realtimeKindMessage, realtimeTargetUser, uid.UserId(), msg.SkipSid, dedupKey, msg); err != nil {
|
||||
if err := publishRealtimeFanout(currentNodeID(), msg.TenantID, realtimeKindMessage, realtimeTargetUser, uid.UserId(), msg.SkipSid, dedupKey, msg); err != nil {
|
||||
immetrics.IncRealtimeFanout("publish", realtimeKindMessage, realtimeTargetUser, "error")
|
||||
logs.Warn.Printf("redis realtime fanout: publish user=%s failed: %v", uid.UserId(), err)
|
||||
return
|
||||
@@ -109,7 +133,7 @@ func publishRealtimeGroupUnread(tenantID types.TenantID, topic, actor string, se
|
||||
Recipients: recipients,
|
||||
}
|
||||
dedupKey := "group-unread:" + topic + ":" + strconv.FormatInt(seqID, 10)
|
||||
if err := store.PublishRealtimeFanout(currentNodeID(), tenantID, realtimeKindGroupUnread,
|
||||
if err := publishRealtimeFanout(currentNodeID(), tenantID, realtimeKindGroupUnread,
|
||||
realtimeTargetUsers, topic, "", dedupKey, payload); err != nil {
|
||||
immetrics.IncRealtimeFanout("publish", realtimeKindGroupUnread, realtimeTargetUsers, "error")
|
||||
logs.Warn.Printf("redis realtime fanout: publish group unread topic=%s seq=%d failed: %v", topic, seqID, err)
|
||||
@@ -140,7 +164,7 @@ func publishRealtimeCallEvent(msg *ClientComMessage) {
|
||||
Timestamp: msg.Timestamp,
|
||||
}
|
||||
dedupKey := "call:" + msg.RcptTo + ":" + msg.AsUser + ":" + event.Event + ":" + strconv.FormatInt(msg.Note.SeqId, 10)
|
||||
if err := store.PublishRealtimeFanout(currentNodeID(), msg.TenantID, realtimeKindCallEvent, realtimeTargetTopic, msg.RcptTo, sid, dedupKey, event); err != nil {
|
||||
if err := publishRealtimeFanout(currentNodeID(), msg.TenantID, realtimeKindCallEvent, realtimeTargetTopic, msg.RcptTo, sid, dedupKey, event); err != nil {
|
||||
immetrics.IncRealtimeFanout("publish", realtimeKindCallEvent, realtimeTargetTopic, "error")
|
||||
logs.Warn.Printf("redis realtime fanout: publish call event topic=%s from=%s event=%s seq=%d failed: %v",
|
||||
msg.RcptTo, msg.AsUser, event.Event, msg.Note.SeqId, err)
|
||||
@@ -162,7 +186,7 @@ func publishRealtimeEvictUser(tenantID types.TenantID, uid types.Uid, reason str
|
||||
msg.Ctrl.Params = map[string]any{"reason": reason}
|
||||
}
|
||||
dedupKey := "evict:user:" + uid.UserId() + ":" + reason
|
||||
if err := store.PublishRealtimeFanout(currentNodeID(), tenantID, realtimeKindSessionControl, realtimeTargetUser, uid.UserId(), skipSid, dedupKey, msg); err != nil {
|
||||
if err := publishRealtimeFanout(currentNodeID(), tenantID, realtimeKindSessionControl, realtimeTargetUser, uid.UserId(), skipSid, dedupKey, msg); err != nil {
|
||||
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetUser, "error")
|
||||
logs.Warn.Printf("redis realtime fanout: publish evict user=%s failed: %v", uid.UserId(), err)
|
||||
return
|
||||
@@ -183,7 +207,7 @@ func publishRealtimeEvictDevice(tenantID types.TenantID, uid types.Uid, deviceID
|
||||
}
|
||||
target := uid.UserId() + ":" + strings.TrimSpace(deviceID)
|
||||
dedupKey := "evict:device:" + target + ":" + reason
|
||||
if err := store.PublishRealtimeFanout(currentNodeID(), tenantID, realtimeKindSessionControl, realtimeTargetDevice, target, "", dedupKey, msg); err != nil {
|
||||
if err := publishRealtimeFanout(currentNodeID(), tenantID, realtimeKindSessionControl, realtimeTargetDevice, target, "", dedupKey, msg); err != nil {
|
||||
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetDevice, "error")
|
||||
logs.Warn.Printf("redis realtime fanout: publish evict device=%s failed: %v", target, err)
|
||||
return
|
||||
@@ -191,24 +215,52 @@ func publishRealtimeEvictDevice(tenantID types.TenantID, uid types.Uid, deviceID
|
||||
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetDevice, "ok")
|
||||
}
|
||||
|
||||
// publishRealtimeEvictUserPlatform evicts all sessions of the user on the given
|
||||
// platform across cluster nodes. skipSid (the new login session) is part of the
|
||||
// dedup key because repeated logins on the same platform within the dedup window
|
||||
// must each reach the subscriber nodes.
|
||||
func publishRealtimeEvictUserPlatform(tenantID types.TenantID, uid types.Uid, platf, reason, skipSid string) {
|
||||
if !tenantID.IsValid() || uid.IsZero() || strings.TrimSpace(platf) == "" {
|
||||
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetPlatf, "skip_invalid")
|
||||
return
|
||||
}
|
||||
key := platformEvictFanoutKey{
|
||||
tenantID: tenantID,
|
||||
uid: uid,
|
||||
platf: strings.TrimSpace(platf),
|
||||
reason: reason,
|
||||
}
|
||||
platformEvictFanout.Lock()
|
||||
if pending := platformEvictFanout.pending[key]; pending != nil {
|
||||
pending.skipSid = skipSid
|
||||
pending.timer.Reset(platformEvictFanoutDebounce)
|
||||
platformEvictFanout.Unlock()
|
||||
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetPlatf, "coalesced")
|
||||
return
|
||||
}
|
||||
pending := &platformEvictFanoutPending{skipSid: skipSid}
|
||||
pending.timer = time.AfterFunc(platformEvictFanoutDebounce, func() {
|
||||
platformEvictFanout.Lock()
|
||||
current := platformEvictFanout.pending[key]
|
||||
if current != pending {
|
||||
platformEvictFanout.Unlock()
|
||||
return
|
||||
}
|
||||
delete(platformEvictFanout.pending, key)
|
||||
finalSkipSid := pending.skipSid
|
||||
platformEvictFanout.Unlock()
|
||||
publishRealtimeEvictUserPlatformNow(key.tenantID, key.uid, key.platf, key.reason, finalSkipSid)
|
||||
})
|
||||
platformEvictFanout.pending[key] = pending
|
||||
platformEvictFanout.Unlock()
|
||||
}
|
||||
|
||||
func publishRealtimeEvictUserPlatformNow(tenantID types.TenantID, uid types.Uid, platf, reason, skipSid string) {
|
||||
msg := NoErrEvicted("", "", types.TimeNow())
|
||||
msg.TenantID = tenantID
|
||||
msg.AsUser = uid.UserId()
|
||||
if reason != "" {
|
||||
msg.Ctrl.Params = map[string]any{"reason": reason}
|
||||
}
|
||||
target := uid.UserId() + ":" + strings.TrimSpace(platf)
|
||||
target := uid.UserId() + ":" + platf
|
||||
dedupKey := "evict:platform:" + target + ":" + skipSid
|
||||
if err := store.PublishRealtimeFanout(currentNodeID(), tenantID, realtimeKindSessionControl, realtimeTargetPlatf, target, skipSid, dedupKey, msg); err != nil {
|
||||
if err := publishRealtimeFanout(currentNodeID(), tenantID, realtimeKindSessionControl, realtimeTargetPlatf, target, skipSid, dedupKey, msg); err != nil {
|
||||
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetPlatf, "error")
|
||||
logs.Warn.Printf("redis realtime fanout: publish evict platform=%s failed: %v", target, err)
|
||||
return
|
||||
@@ -472,28 +524,33 @@ func deliverRealtimeSessionControl(envelope *store.RealtimeFanoutEnvelope, msg *
|
||||
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "skip_invalid_target")
|
||||
return
|
||||
}
|
||||
globals.sessionStore.EvictUserDevice(types.ParseUserId(uidText), deviceID, fanoutEvictReason(msg))
|
||||
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "evicted")
|
||||
recordRealtimeSessionControlEvict(envelope, globals.sessionStore.EvictUserDevice(types.ParseUserId(uidText), deviceID, fanoutEvictReason(msg)))
|
||||
case realtimeTargetPlatf:
|
||||
uidText, platf, ok := strings.Cut(envelope.Target, ":")
|
||||
if !ok {
|
||||
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "skip_invalid_target")
|
||||
return
|
||||
}
|
||||
globals.sessionStore.EvictUserPlatform(types.ParseUserId(uidText), platf, envelope.SkipSid, fanoutEvictReason(msg))
|
||||
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "evicted")
|
||||
recordRealtimeSessionControlEvict(envelope, globals.sessionStore.EvictUserPlatform(types.ParseUserId(uidText), platf, envelope.SkipSid, fanoutEvictReason(msg)))
|
||||
case realtimeTargetUser:
|
||||
uid := types.ParseUserId(envelope.Target)
|
||||
if uid.IsZero() {
|
||||
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "skip_invalid_target")
|
||||
return
|
||||
}
|
||||
globals.sessionStore.EvictUser(uid, envelope.SkipSid, fanoutEvictReason(msg))
|
||||
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "evicted")
|
||||
recordRealtimeSessionControlEvict(envelope, globals.sessionStore.EvictUser(uid, envelope.SkipSid, fanoutEvictReason(msg)))
|
||||
}
|
||||
_ = msg
|
||||
}
|
||||
|
||||
func recordRealtimeSessionControlEvict(envelope *store.RealtimeFanoutEnvelope, evicted int) {
|
||||
if evicted > 0 {
|
||||
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "evicted")
|
||||
return
|
||||
}
|
||||
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "no_local_session")
|
||||
}
|
||||
|
||||
func recordRealtimeFanoutDeliverResult(kind, targetType string, delivered int) {
|
||||
if delivered > 0 {
|
||||
immetrics.IncRealtimeFanout("deliver", kind, targetType, "local_delivered")
|
||||
|
||||
Reference in New Issue
Block a user