Guard WebSocket broadcast buffers when clients stall (Related to #734)
This commit is contained in:
parent
335795d354
commit
1f2993f4a7
1 changed files with 53 additions and 31 deletions
|
|
@ -269,10 +269,10 @@ func cloneMetadataValue(value interface{}) interface{} {
|
|||
type Hub struct {
|
||||
clients map[*Client]bool
|
||||
broadcast chan []byte
|
||||
broadcastSeq chan Message // Sequenced broadcast channel for ordering
|
||||
broadcastSeq chan Message // Sequenced broadcast channel for ordering
|
||||
register chan *Client
|
||||
unregister chan *Client
|
||||
stopChan chan struct{} // Signals shutdown
|
||||
stopChan chan struct{} // Signals shutdown
|
||||
mu sync.RWMutex
|
||||
getState func() interface{} // Function to get current state
|
||||
allowedOrigins []string // Allowed origins for CORS
|
||||
|
|
@ -308,7 +308,7 @@ func NewHub(getState func() interface{}) *Hub {
|
|||
unregister: make(chan *Client),
|
||||
stopChan: make(chan struct{}),
|
||||
getState: getState,
|
||||
allowedOrigins: []string{}, // Default to empty (will be set based on actual host)
|
||||
allowedOrigins: []string{}, // Default to empty (will be set based on actual host)
|
||||
coalesceWindow: 100 * time.Millisecond, // Coalesce rapid updates within 100ms
|
||||
}
|
||||
}
|
||||
|
|
@ -475,9 +475,10 @@ func (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
clientID := utils.GenerateID("client")
|
||||
client := &Client{
|
||||
hub: h,
|
||||
conn: conn,
|
||||
send: make(chan []byte, 1024), // Increased buffer for high-frequency updates
|
||||
hub: h,
|
||||
conn: conn,
|
||||
// Keep buffer bounded to avoid holding large state snapshots indefinitely if a client stalls.
|
||||
send: make(chan []byte, 128),
|
||||
id: clientID,
|
||||
lastPing: time.Now(),
|
||||
}
|
||||
|
|
@ -491,6 +492,45 @@ func (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||
go client.readPump()
|
||||
}
|
||||
|
||||
// dispatchToClients fan-outs a marshaled payload to all clients, dropping any that
|
||||
// cannot keep up to prevent unbounded buffering.
|
||||
func (h *Hub) dispatchToClients(data []byte, dropLog string) {
|
||||
h.mu.RLock()
|
||||
clients := make([]*Client, 0, len(h.clients))
|
||||
for client := range h.clients {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
|
||||
for _, client := range clients {
|
||||
select {
|
||||
case client.send <- data:
|
||||
default:
|
||||
h.mu.Lock()
|
||||
if _, stillPresent := h.clients[client]; stillPresent {
|
||||
delete(h.clients, client)
|
||||
close(client.send)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
log.Warn().Str("client", client.id).Msg(dropLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) popCoalescedMessage() *Message {
|
||||
h.coalesceMutex.Lock()
|
||||
defer h.coalesceMutex.Unlock()
|
||||
|
||||
if h.coalescePending == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := *h.coalescePending
|
||||
h.coalescePending = nil
|
||||
h.coalesceTimer = nil
|
||||
return &msg
|
||||
}
|
||||
|
||||
// runBroadcastSequencer handles sequenced broadcasts with coalescing for rapid state updates
|
||||
func (h *Hub) runBroadcastSequencer() {
|
||||
for {
|
||||
|
|
@ -506,42 +546,24 @@ func (h *Hub) runBroadcastSequencer() {
|
|||
}
|
||||
|
||||
// Update pending message
|
||||
h.coalescePending = &msg
|
||||
current := msg
|
||||
h.coalescePending = ¤t
|
||||
|
||||
// Set timer to send after coalesce window
|
||||
h.coalesceTimer = time.AfterFunc(h.coalesceWindow, func() {
|
||||
h.coalesceMutex.Lock()
|
||||
if h.coalescePending != nil {
|
||||
// Send the coalesced message
|
||||
if data, err := json.Marshal(*h.coalescePending); err == nil {
|
||||
h.mu.RLock()
|
||||
for client := range h.clients {
|
||||
select {
|
||||
case client.send <- data:
|
||||
default:
|
||||
log.Warn().Str("client", client.id).Msg("Client send channel full, skipping coalesced message")
|
||||
}
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
pending := h.popCoalescedMessage()
|
||||
if pending != nil {
|
||||
if data, err := json.Marshal(*pending); err == nil {
|
||||
h.dispatchToClients(data, "Client send channel full, dropping coalesced message and closing connection")
|
||||
}
|
||||
h.coalescePending = nil
|
||||
}
|
||||
h.coalesceMutex.Unlock()
|
||||
})
|
||||
|
||||
h.coalesceMutex.Unlock()
|
||||
} else {
|
||||
// Non-state messages (alerts, etc.) - send immediately
|
||||
if data, err := json.Marshal(msg); err == nil {
|
||||
h.mu.RLock()
|
||||
for client := range h.clients {
|
||||
select {
|
||||
case client.send <- data:
|
||||
default:
|
||||
log.Warn().Str("client", client.id).Msg("Client send channel full, skipping message")
|
||||
}
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
h.dispatchToClients(data, "Client send channel full, dropping message and closing connection")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue