239 lines
9.4 KiB
Go
239 lines
9.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/tinode/chat/server/logs"
|
|
immetrics "github.com/tinode/chat/server/metrics"
|
|
"github.com/tinode/chat/server/store"
|
|
"github.com/tinode/chat/server/store/types"
|
|
)
|
|
|
|
const (
|
|
realtimeKindMessage = "message"
|
|
realtimeKindSessionControl = "session_control"
|
|
|
|
realtimeTargetTopic = "topic"
|
|
realtimeTargetUser = "user"
|
|
realtimeTargetDevice = "device"
|
|
)
|
|
|
|
func startRedisRealtimeFanout(ctx context.Context) {
|
|
if err := store.StartRealtimeFanoutSubscriber(ctx, currentNodeID(), deliverRealtimeFanoutEnvelope); err != nil {
|
|
immetrics.IncRealtimeFanout("subscribe", realtimeKindMessage, realtimeTargetTopic, "error")
|
|
logs.Warn.Println("redis realtime fanout: subscriber disabled", err)
|
|
return
|
|
}
|
|
immetrics.IncRealtimeFanout("subscribe", realtimeKindMessage, realtimeTargetTopic, "ok")
|
|
logs.Info.Printf("redis realtime fanout: subscriber started node=%s", currentNodeID())
|
|
}
|
|
|
|
func publishRealtimeTopicMessage(topic string, msg *ServerComMessage) {
|
|
if msg == nil || !msg.TenantID.IsValid() || strings.TrimSpace(topic) == "" {
|
|
immetrics.IncRealtimeFanout("publish", realtimeKindMessage, realtimeTargetTopic, "skip_invalid")
|
|
return
|
|
}
|
|
if globals.cluster != nil {
|
|
immetrics.IncRealtimeFanout("publish", realtimeKindMessage, realtimeTargetTopic, "skip_native_cluster")
|
|
return
|
|
}
|
|
dedupKey := realtimeMessageDedupKey(topic, msg)
|
|
if err := store.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
|
|
}
|
|
immetrics.IncRealtimeFanout("publish", realtimeKindMessage, realtimeTargetTopic, "ok")
|
|
}
|
|
|
|
func publishRealtimeUserMessage(uid types.Uid, msg *ServerComMessage) {
|
|
if msg == nil || !msg.TenantID.IsValid() || uid.IsZero() {
|
|
immetrics.IncRealtimeFanout("publish", realtimeKindMessage, realtimeTargetUser, "skip_invalid")
|
|
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")
|
|
logs.Warn.Printf("redis realtime fanout: publish user=%s failed: %v", uid.UserId(), err)
|
|
return
|
|
}
|
|
immetrics.IncRealtimeFanout("publish", realtimeKindMessage, realtimeTargetUser, "ok")
|
|
}
|
|
|
|
func publishRealtimeEvictUser(tenantID types.TenantID, uid types.Uid, reason string, skipSid string) {
|
|
if !tenantID.IsValid() || uid.IsZero() {
|
|
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetUser, "skip_invalid")
|
|
return
|
|
}
|
|
msg := NoErrEvicted("", "", types.TimeNow())
|
|
msg.TenantID = tenantID
|
|
msg.AsUser = uid.UserId()
|
|
msg.SkipSid = skipSid
|
|
if reason != "" {
|
|
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 {
|
|
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetUser, "error")
|
|
logs.Warn.Printf("redis realtime fanout: publish evict user=%s failed: %v", uid.UserId(), err)
|
|
return
|
|
}
|
|
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetUser, "ok")
|
|
}
|
|
|
|
func publishRealtimeEvictDevice(tenantID types.TenantID, uid types.Uid, deviceID, reason string) {
|
|
if !tenantID.IsValid() || uid.IsZero() || strings.TrimSpace(deviceID) == "" {
|
|
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetDevice, "skip_invalid")
|
|
return
|
|
}
|
|
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(deviceID)
|
|
dedupKey := "evict:device:" + target + ":" + reason
|
|
if err := store.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
|
|
}
|
|
immetrics.IncRealtimeFanout("publish", realtimeKindSessionControl, realtimeTargetDevice, "ok")
|
|
}
|
|
|
|
func deliverRealtimeFanoutEnvelope(envelope *store.RealtimeFanoutEnvelope) {
|
|
if envelope == nil || !envelope.TenantID.IsValid() || len(envelope.Message) == 0 {
|
|
immetrics.IncRealtimeFanout("receive", realtimeKindMessage, realtimeTargetTopic, "skip_invalid")
|
|
return
|
|
}
|
|
immetrics.IncRealtimeFanout("receive", envelope.Kind, envelope.TargetType, "ok")
|
|
var msg ServerComMessage
|
|
if err := json.Unmarshal(envelope.Message, &msg); err != nil {
|
|
immetrics.IncRealtimeFanout("receive", envelope.Kind, envelope.TargetType, "malformed")
|
|
logs.Warn.Println("redis realtime fanout: malformed message", err)
|
|
return
|
|
}
|
|
msg.TenantID = envelope.TenantID
|
|
msg.RcptTo = envelope.Target
|
|
msg.SkipSid = envelope.SkipSid
|
|
msg.sess = nil
|
|
prepareRedisGroupFanoutMessage(&msg, envelope.Target)
|
|
|
|
switch envelope.Kind {
|
|
case realtimeKindSessionControl:
|
|
deliverRealtimeSessionControl(envelope, &msg)
|
|
default:
|
|
immetrics.ObserveRealtimeFanoutDelivery(envelope.Kind, envelope.TargetType, msg.Timestamp)
|
|
deliverRealtimeMessage(envelope, &msg)
|
|
}
|
|
}
|
|
|
|
func deliverRealtimeMessage(envelope *store.RealtimeFanoutEnvelope, msg *ServerComMessage) {
|
|
if globals.sessionStore == nil {
|
|
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "skip_no_session_store")
|
|
return
|
|
}
|
|
switch envelope.TargetType {
|
|
case realtimeTargetUser:
|
|
uid := types.ParseUserId(envelope.Target)
|
|
if uid.IsZero() {
|
|
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "skip_invalid_target")
|
|
return
|
|
}
|
|
delivered := globals.sessionStore.DeliverToUser(envelope.TenantID, uid, envelope.SkipSid, msg)
|
|
recordRealtimeFanoutDeliverResult(envelope.Kind, envelope.TargetType, delivered)
|
|
case realtimeTargetTopic, "":
|
|
deliverRealtimeTopicMessage(envelope, msg)
|
|
default:
|
|
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "skip_unknown_target_type")
|
|
}
|
|
}
|
|
|
|
func deliverRealtimeTopicMessage(envelope *store.RealtimeFanoutEnvelope, msg *ServerComMessage) {
|
|
topic := strings.TrimSpace(envelope.Target)
|
|
if topic == "" {
|
|
topic = strings.TrimSpace(msg.RcptTo)
|
|
}
|
|
if topic == "" {
|
|
immetrics.IncRealtimeFanout("deliver", envelope.Kind, realtimeTargetTopic, "skip_invalid_target")
|
|
return
|
|
}
|
|
msg.RcptTo = topic
|
|
key := types.TopicKey{TenantID: envelope.TenantID, Topic: topic}
|
|
if globals.hub != nil && globals.hub.topicGet(key) != nil {
|
|
select {
|
|
case globals.hub.routeSrv <- msg:
|
|
immetrics.IncRealtimeFanout("deliver", envelope.Kind, realtimeTargetTopic, "hub_queued")
|
|
default:
|
|
immetrics.IncRealtimeFanout("deliver", envelope.Kind, realtimeTargetTopic, "hub_full")
|
|
logs.Err.Println("redis realtime fanout: hub routeSrv queue full", topic)
|
|
}
|
|
return
|
|
}
|
|
if types.GetTopicCat(topic) == types.TopicCatP2P {
|
|
delivered := globals.sessionStore.DeliverToP2PTopic(envelope.TenantID, topic, envelope.SkipSid, msg)
|
|
recordRealtimeFanoutDeliverResult(envelope.Kind, realtimeTargetTopic, delivered)
|
|
return
|
|
}
|
|
delivered := globals.sessionStore.DeliverToTopicSubscribers(envelope.TenantID, topic, envelope.SkipSid, msg)
|
|
recordRealtimeFanoutDeliverResult(envelope.Kind, realtimeTargetTopic, delivered)
|
|
}
|
|
|
|
func deliverRealtimeSessionControl(envelope *store.RealtimeFanoutEnvelope, msg *ServerComMessage) {
|
|
if globals.sessionStore == nil {
|
|
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "skip_no_session_store")
|
|
return
|
|
}
|
|
switch envelope.TargetType {
|
|
case realtimeTargetDevice:
|
|
uidText, deviceID, ok := strings.Cut(envelope.Target, ":")
|
|
if !ok {
|
|
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "skip_invalid_target")
|
|
return
|
|
}
|
|
globals.sessionStore.EvictUserDevice(types.ParseUserId(uidText), deviceID)
|
|
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "evicted")
|
|
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)
|
|
immetrics.IncRealtimeFanout("deliver", envelope.Kind, envelope.TargetType, "evicted")
|
|
}
|
|
_ = msg
|
|
}
|
|
|
|
func recordRealtimeFanoutDeliverResult(kind, targetType string, delivered int) {
|
|
if delivered > 0 {
|
|
immetrics.IncRealtimeFanout("deliver", kind, targetType, "local_delivered")
|
|
return
|
|
}
|
|
immetrics.IncRealtimeFanout("deliver", kind, targetType, "no_local_session")
|
|
}
|
|
|
|
func realtimeMessageDedupKey(target string, msg *ServerComMessage) string {
|
|
if msg == nil {
|
|
return ""
|
|
}
|
|
if msg.Data != nil && msg.Data.SeqId > 0 {
|
|
topic := target
|
|
if msg.Data.Topic != "" {
|
|
topic = msg.Data.Topic
|
|
}
|
|
return "data:" + topic + ":" + strconv.FormatInt(msg.Data.SeqId, 10)
|
|
}
|
|
if msg.Info != nil {
|
|
return "info:" + target + ":" + msg.Info.Event + ":" + strconv.FormatInt(msg.Info.SeqId, 10)
|
|
}
|
|
if msg.Pres != nil {
|
|
return "pres:" + target + ":" + msg.Pres.What + ":" + strconv.FormatInt(msg.Pres.SeqId, 10)
|
|
}
|
|
return ""
|
|
}
|