feat: enhance chat service with Redis pub/sub for multi-instance message handling, add instance ID for deduplication
This commit is contained in:
1 parent
d991d6b6c7
commit
7244c1d658
4 files changed
+90
-19
No files matched your search
@@ -41,8 +41,17 @@ var (
|
||||
// Rate limit config (env-overridable)
|
||||
rateLimitMax = 10
|
||||
rateLimitWindow = 10 * time.Second
|
||||
|
||||
// Instance ID for multi-instance pub/sub deduplication
|
||||
instanceID = fmt.Sprintf("chat-%s", generateShortID())
|
||||
)
|
||||
|
||||
func generateShortID() string {
|
||||
b := make([]byte, 6)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
type clientInfo struct {
|
||||
Username string
|
||||
Room string
|
||||
@@ -2161,7 +2170,7 @@ func handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
fmt.Fprintln(w, "# HELP linearvoid_chat_build_info Static build info for chat service.")
|
||||
fmt.Fprintln(w, "# TYPE linearvoid_chat_build_info gauge")
|
||||
fmt.Fprintln(w, "linearvoid_chat_build_info{service=\"chat\",database_driver=\""+databaseDriverName+"\"} 1")
|
||||
fmt.Fprintln(w, "linearvoid_chat_build_info{service=\"chat\",database_driver=\""+databaseDriverName+"\",instance_id=\""+instanceID+"\"} 1")
|
||||
}
|
||||
|
||||
// broadcast sends a message to all connected clients in a room and saves to DB
|
||||
@@ -2172,12 +2181,23 @@ func broadcast(msg Message) {
|
||||
// Persist to configured SQL backend.
|
||||
db.Create(&msg)
|
||||
|
||||
// Local fan-out
|
||||
for client := range clients {
|
||||
if err := client.WriteJSON(msg); err != nil {
|
||||
client.Close()
|
||||
delete(clients, client)
|
||||
delete(clientMeta, client)
|
||||
}
|
||||
}
|
||||
|
||||
// Redis pub/sub for multi-instance fan-out
|
||||
if redisClient != nil && msg.Room != "" {
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"instance": instanceID,
|
||||
"msg": msg,
|
||||
})
|
||||
redisClient.Publish(context.Background(), fmt.Sprintf("room:%s:messages", msg.Room), payload)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnection handles WebSocket connections for chat (public rooms, private DMs, moderation)
|
||||
@@ -2333,6 +2353,47 @@ func pushChatHistory(conn *websocket.Conn, room string, limit int) {
|
||||
// dmsSent tracks total private messages for Prometheus metrics (set from handleMetrics).
|
||||
var dmsSent int64
|
||||
|
||||
// subscribeRoomMessages listens for Redis pub/sub messages and fans out locally.
|
||||
func subscribeRoomMessages() {
|
||||
if redisClient == nil {
|
||||
return
|
||||
}
|
||||
pubsub := redisClient.PSubscribe(context.Background(), "room:*:messages")
|
||||
defer pubsub.Close()
|
||||
|
||||
log.Printf("Redis pub/sub subscriber started (instance %s)", instanceID)
|
||||
|
||||
for {
|
||||
msg, err := pubsub.ReceiveMessage(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("Redis pub/sub error: %v — retrying in 5s", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
pubsub = redisClient.PSubscribe(context.Background(), "room:*:messages")
|
||||
continue
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
Instance string `json:"instance"`
|
||||
Msg Message `json:"msg"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(msg.Payload), &envelope); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip messages originating from this instance
|
||||
if envelope.Instance == instanceID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Fan out to local clients
|
||||
mutex.Lock()
|
||||
for client := range clients {
|
||||
client.WriteJSON(envelope.Msg)
|
||||
}
|
||||
mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// handlePrivateDM routes private messages to the specific user pair
|
||||
func handlePrivateDM(msg Message) {
|
||||
msg.Type = "private"
|
||||
@@ -2640,6 +2701,11 @@ func main() {
|
||||
initRedis()
|
||||
initDB()
|
||||
|
||||
// Subscribe to Redis pub/sub for multi-instance message fan-out
|
||||
go subscribeRoomMessages()
|
||||
|
||||
log.Printf("Instance ID: %s", instanceID)
|
||||
|
||||
http.HandleFunc("/ws", handleConnection)
|
||||
http.HandleFunc("/api/admin/login", handleAdminLogin)
|
||||
http.HandleFunc("/api/admin/logout", handleAdminLogout)
|
||||
|
||||
Reference in new issue
Block a user