feat(multiplayer): cap websocket connections per player

This commit is contained in:
Josh Creek
2026-09-01 19:27:45 +01:00
parent 515b06d97c
commit da92be73ef
3 changed files with 48 additions and 7 deletions
+21 -7
View File
@@ -19,12 +19,13 @@ import (
)
const (
webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
maxWebSocketFrame = 64 << 10
eventQueueCapacity = 32
webSocketIdleLimit = 2 * time.Minute
webSocketMessageLimit = 120
webSocketMessageWindow = time.Minute
webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
maxWebSocketFrame = 64 << 10
eventQueueCapacity = 32
webSocketIdleLimit = 2 * time.Minute
webSocketMessageLimit = 120
webSocketMessageWindow = time.Minute
maxEventConnectionsPerPlayer = 2
)
// ControlPlaneEvent is the server-to-client envelope defined by the v1
@@ -56,8 +57,18 @@ func newEventHub() *eventHub {
}
func (h *eventHub) subscribe(playerID string) *eventSubscriber {
subscriber := &eventSubscriber{playerID: playerID, queue: make(chan []byte, eventQueueCapacity)}
h.mu.Lock()
connections := 0
for subscriber := range h.subscribers {
if subscriber.playerID == playerID {
connections++
}
}
if connections >= maxEventConnectionsPerPlayer {
h.mu.Unlock()
return nil
}
subscriber := &eventSubscriber{playerID: playerID, queue: make(chan []byte, eventQueueCapacity)}
h.subscribers[subscriber] = struct{}{}
h.mu.Unlock()
return subscriber
@@ -168,6 +179,9 @@ func (s *Service) controlPlaneEvent(w http.ResponseWriter, r *http.Request) {
return
}
subscriber := s.getEventHub().subscribe(playerID)
if subscriber == nil {
return
}
defer s.getEventHub().unsubscribe(subscriber)
var writeMu sync.Mutex
+22
View File
@@ -0,0 +1,22 @@
package api
import "testing"
func TestEventHubCapsConnectionsPerPlayerAndReleasesCapacity(t *testing.T) {
hub := newEventHub()
first := hub.subscribe("player-1")
second := hub.subscribe("player-1")
if first == nil || second == nil {
t.Fatal("connection within per-player cap was rejected")
}
if third := hub.subscribe("player-1"); third != nil {
t.Fatal("connection over per-player cap was accepted")
}
hub.unsubscribe(first)
if third := hub.subscribe("player-1"); third == nil {
t.Fatal("released connection capacity was not reusable")
} else {
hub.unsubscribe(third)
}
hub.unsubscribe(second)
}