feat: expose websocket delivery pressure metrics

This commit is contained in:
richjohnrambo
2026-09-29 23:15:29 +08:00
parent b1545b6876
commit da8efb6e38
5 changed files with 272 additions and 26 deletions
+54 -9
View File
@@ -141,6 +141,10 @@ type Session struct {
// Outbound mesages, buffered.
// The content must be serialized in format suitable for the session.
send chan any
// WebSocket queue timestamps are kept separately to preserve the send channel's payload contract.
wsQueueMu sync.Mutex
wsQueueTimes []time.Time
wsQueueHead int
// Channel for shutting down the session, buffer 1.
// Content in the same format as for 'send'
@@ -182,6 +186,52 @@ type Subscription struct {
supd chan<- *sessionUpdate
}
func (s *Session) enqueueOutbound(payload any) bool {
if s.proto != WEBSOCK {
select {
case s.send <- payload:
return true
default:
return false
}
}
s.wsQueueMu.Lock()
defer s.wsQueueMu.Unlock()
select {
case s.send <- payload:
s.wsQueueTimes = append(s.wsQueueTimes, time.Now())
immetrics.ObserveWSSendQueueEnqueue(len(s.send), cap(s.send), true)
return true
default:
immetrics.ObserveWSSendQueueEnqueue(len(s.send), cap(s.send), false)
return false
}
}
func (s *Session) observeOutboundDequeue(purged bool) {
if s.proto != WEBSOCK {
return
}
s.wsQueueMu.Lock()
if s.wsQueueHead >= len(s.wsQueueTimes) {
s.wsQueueMu.Unlock()
return
}
queuedAt := s.wsQueueTimes[s.wsQueueHead]
s.wsQueueHead++
if s.wsQueueHead == len(s.wsQueueTimes) {
s.wsQueueTimes = s.wsQueueTimes[:0]
s.wsQueueHead = 0
} else if s.wsQueueHead >= 128 {
copy(s.wsQueueTimes, s.wsQueueTimes[s.wsQueueHead:])
s.wsQueueTimes = s.wsQueueTimes[:len(s.wsQueueTimes)-s.wsQueueHead]
s.wsQueueHead = 0
}
s.wsQueueMu.Unlock()
immetrics.ObserveWSSendQueueDequeue(queuedAt, purged)
}
func (s *Session) addSub(topic string, sub *Subscription) {
if s.multi != nil {
s.multi.addSub(topic, sub)
@@ -333,9 +383,7 @@ func (s *Session) queueOutBatch(msgs []*ServerComMessage) bool {
}
if s.supportsMessageBatching() {
select {
case s.send <- msgs:
default:
if !s.enqueueOutbound(msgs) {
// Never block here since it may also block the topic's run() goroutine.
logs.Err.Println("s.queueOut: session's send queue2 full", s.sid)
return false
@@ -394,9 +442,7 @@ func (s *Session) queueOut(msg *ServerComMessage) bool {
}
}
select {
case s.send <- msg:
default:
if !s.enqueueOutbound(msg) {
// Never block here since it may also block the topic's run() goroutine.
logs.Err.Println("s.queueOut: session's send queue full", s.sid)
return false
@@ -414,9 +460,7 @@ func (s *Session) queueOutBytes(data []byte) bool {
return true
}
select {
case s.send <- data:
default:
if !s.enqueueOutbound(data) {
logs.Err.Println("s.queueOutBytes: session's send queue full", s.sid)
return false
}
@@ -451,6 +495,7 @@ func (s *Session) stopSession(data any) {
func (s *Session) purgeChannels() {
for len(s.send) > 0 {
<-s.send
s.observeOutboundDequeue(true)
}
for len(s.stop) > 0 {
<-s.stop