perf: batch group unread fanout

This commit is contained in:
richjohnrambo
2026-09-27 23:10:28 +08:00
parent fc875cc84e
commit 6b8ed4a2cc
10 changed files with 260 additions and 4 deletions
+98
View File
@@ -16,6 +16,7 @@ import (
const (
realtimeKindMessage = "message"
realtimeKindGroupUnread = "group_unread"
realtimeKindSessionControl = "session_control"
realtimeKindCallEvent = "call_event"
@@ -23,8 +24,21 @@ const (
realtimeTargetUser = "user"
realtimeTargetDevice = "device"
realtimeTargetPlatf = "platform"
realtimeTargetUsers = "users"
)
type realtimeGroupUnreadRecipient struct {
User string `json:"user"`
Source string `json:"source"`
}
type realtimeGroupUnreadMessage struct {
Topic string `json:"topic"`
Actor string `json:"actor,omitempty"`
SeqID int64 `json:"seq_id"`
Recipients []realtimeGroupUnreadRecipient `json:"recipients"`
}
type realtimeCallEventMessage struct {
Topic string `json:"topic"`
From string `json:"from"`
@@ -68,6 +82,12 @@ func publishRealtimeUserMessage(uid types.Uid, msg *ServerComMessage) {
immetrics.IncRealtimeFanout("publish", realtimeKindMessage, realtimeTargetUser, "skip_invalid")
return
}
// Native cluster routing already forwards user topics between pods. Publishing
// the same message through Redis duplicates work and may deliver it twice.
if globals.cluster != nil {
immetrics.IncRealtimeFanout("publish", realtimeKindMessage, realtimeTargetUser, "skip_native_cluster")
return
}
dedupKey := realtimeMessageDedupKey(uid.UserId(), msg)
if err := store.PublishRealtimeFanout(currentNodeID(), msg.TenantID, realtimeKindMessage, realtimeTargetUser, uid.UserId(), msg.SkipSid, dedupKey, msg); err != nil {
immetrics.IncRealtimeFanout("publish", realtimeKindMessage, realtimeTargetUser, "error")
@@ -77,6 +97,29 @@ func publishRealtimeUserMessage(uid types.Uid, msg *ServerComMessage) {
immetrics.IncRealtimeFanout("publish", realtimeKindMessage, realtimeTargetUser, "ok")
}
func publishRealtimeGroupUnread(tenantID types.TenantID, topic, actor string, seqID int64,
recipients []realtimeGroupUnreadRecipient) bool {
if !tenantID.IsValid() || topic == "" || seqID <= 0 || len(recipients) == 0 || !store.RealtimeFanoutEnabled() {
return false
}
payload := &realtimeGroupUnreadMessage{
Topic: topic,
Actor: actor,
SeqID: seqID,
Recipients: recipients,
}
dedupKey := "group-unread:" + topic + ":" + strconv.FormatInt(seqID, 10)
if err := store.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)
return false
}
immetrics.IncRealtimeFanout("publish", realtimeKindGroupUnread, realtimeTargetUsers, "ok")
deliverRealtimeGroupUnread(tenantID, payload)
return true
}
func publishRealtimeCallEvent(msg *ClientComMessage) {
if msg == nil || msg.Note == nil || !msg.TenantID.IsValid() ||
msg.RcptTo == "" || msg.AsUser == "" || msg.Note.What != "call" ||
@@ -193,6 +236,16 @@ func deliverRealtimeFanoutEnvelope(envelope *store.RealtimeFanoutEnvelope) {
return
}
immetrics.IncRealtimeFanout("receive", envelope.Kind, envelope.TargetType, "ok")
if envelope.Kind == realtimeKindGroupUnread {
var batch realtimeGroupUnreadMessage
if err := json.Unmarshal(envelope.Message, &batch); err != nil {
immetrics.IncRealtimeFanout("receive", envelope.Kind, envelope.TargetType, "malformed")
logs.Warn.Println("redis realtime fanout: malformed group unread batch", err)
return
}
deliverRealtimeGroupUnread(envelope.TenantID, &batch)
return
}
var msg ServerComMessage
if err := json.Unmarshal(envelope.Message, &msg); err != nil {
immetrics.IncRealtimeFanout("receive", envelope.Kind, envelope.TargetType, "malformed")
@@ -224,6 +277,51 @@ func deliverRealtimeFanoutEnvelope(envelope *store.RealtimeFanoutEnvelope) {
}
}
func deliverRealtimeGroupUnread(tenantID types.TenantID, batch *realtimeGroupUnreadMessage) {
if globals.sessionStore == nil || batch == nil || batch.Topic == "" || batch.SeqID <= 0 || len(batch.Recipients) == 0 {
immetrics.IncRealtimeFanout("deliver", realtimeKindGroupUnread, realtimeTargetUsers, "skip_invalid")
return
}
recipients := make(map[types.Uid]string, len(batch.Recipients))
for _, recipient := range batch.Recipients {
uid := types.ParseUserId(recipient.User)
if !uid.IsZero() {
recipients[uid] = recipient.Source
}
}
delivered := 0
globals.sessionStore.Range(func(_ string, sess *Session) bool {
if sess == nil || sess.isCluster() || sess.isProxy() || sess.tenantID != tenantID {
return true
}
source, ok := recipients[sess.uid]
if !ok || sessionHasTopicOrP2PAlias(sess, batch.Topic) {
return true
}
actor := batch.Actor
if actor == sess.uid.UserId() {
actor = ""
}
msg := &ServerComMessage{
TenantID: tenantID,
RcptTo: sess.uid.UserId(),
Pres: &MsgServerPres{
Topic: "me",
What: "msg",
Src: source,
AcsActor: actor,
SeqId: batch.SeqID,
SkipTopic: batch.Topic,
},
}
if sess.queueOut(msg) {
delivered++
}
return true
})
recordRealtimeFanoutDeliverResult(realtimeKindGroupUnread, realtimeTargetUsers, delivered)
}
func deliverRealtimeCallEvent(envelope *store.RealtimeFanoutEnvelope) {
var event realtimeCallEventMessage
if err := json.Unmarshal(envelope.Message, &event); err != nil {