mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-13 11:52:03 +00:00
fix(multiplayer): bound websocket writes
This commit is contained in:
@@ -1461,6 +1461,8 @@ The control plane now exports bounded Prometheus-compatible API request counters
|
|||||||
|
|
||||||
The authenticated event stream now rejects client data/reserved opcodes and oversized control frames at the parser boundary; only RFC 6455 close, ping, and pong frames are accepted from clients, preserving the bounded v1 stream contract.
|
The authenticated event stream now rejects client data/reserved opcodes and oversized control frames at the parser boundary; only RFC 6455 close, ping, and pong frames are accepted from clients, preserving the bounded v1 stream contract.
|
||||||
|
|
||||||
|
Event delivery also applies a bounded write deadline, so a client that stops reading cannot strand the event handler after the bounded subscriber queue evicts it.
|
||||||
|
|
||||||
`make verify-multiplayer-local` now provides one cloud-free regression gate for the current implementation: the complete Go suite, the Godot harness, OpenAPI parsing, and the migration/Fleet/Kubernetes/supply-chain checks. It fails clearly when the configured Godot executable is unavailable and does not weaken or replace the existing Phase 6/ENet gates; PostgreSQL, Redis, Steam, Agones, and multi-process Internet gates remain separate.
|
`make verify-multiplayer-local` now provides one cloud-free regression gate for the current implementation: the complete Go suite, the Godot harness, OpenAPI parsing, and the migration/Fleet/Kubernetes/supply-chain checks. It fails clearly when the configured Godot executable is unavailable and does not weaken or replace the existing Phase 6/ENet gates; PostgreSQL, Redis, Steam, Agones, and multi-process Internet gates remain separate.
|
||||||
|
|
||||||
That local gate now also runs `go test -race ./...`, `go vet ./...`, and each declared domain fuzz target for a bounded 2-second interval, aligning the one-command gate with the separately recorded 8.46 verification requirements.
|
That local gate now also runs `go test -race ./...`, `go vet ./...`, and each declared domain fuzz target for a bounded 2-second interval, aligning the one-command gate with the separately recorded 8.46 verification requirements.
|
||||||
|
|||||||
+13
-2
@@ -23,6 +23,7 @@ const (
|
|||||||
maxWebSocketFrame = 64 << 10
|
maxWebSocketFrame = 64 << 10
|
||||||
eventQueueCapacity = 32
|
eventQueueCapacity = 32
|
||||||
webSocketIdleLimit = 2 * time.Minute
|
webSocketIdleLimit = 2 * time.Minute
|
||||||
|
webSocketWriteLimit = 10 * time.Second
|
||||||
webSocketMessageLimit = 120
|
webSocketMessageLimit = 120
|
||||||
webSocketMessageWindow = time.Minute
|
webSocketMessageWindow = time.Minute
|
||||||
maxEventConnectionsPerPlayer = 2
|
maxEventConnectionsPerPlayer = 2
|
||||||
@@ -198,7 +199,7 @@ func (s *Service) controlPlaneEvent(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeMu.Lock()
|
writeMu.Lock()
|
||||||
err := writeWebSocketFrame(connection, 0x1, payload)
|
err := writeWebSocketFrameWithDeadline(connection, 0x1, payload, webSocketWriteLimit)
|
||||||
writeMu.Unlock()
|
writeMu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -284,7 +285,7 @@ func readWebSocketFrames(connection net.Conn, writeMu *sync.Mutex) {
|
|||||||
}
|
}
|
||||||
if opcode == 0x9 {
|
if opcode == 0x9 {
|
||||||
writeMu.Lock()
|
writeMu.Lock()
|
||||||
_ = writeWebSocketFrame(connection, 0xA, nil)
|
_ = writeWebSocketFrameWithDeadline(connection, 0xA, nil, webSocketWriteLimit)
|
||||||
writeMu.Unlock()
|
writeMu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,3 +382,13 @@ func writeWebSocketFrame(connection net.Conn, opcode byte, payload []byte) error
|
|||||||
_, err := connection.Write(payload)
|
_, err := connection.Write(payload)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeWebSocketFrameWithDeadline(connection net.Conn, opcode byte, payload []byte, timeout time.Duration) error {
|
||||||
|
if timeout <= 0 {
|
||||||
|
return errors.New("invalid websocket write timeout")
|
||||||
|
}
|
||||||
|
if err := connection.SetWriteDeadline(time.Now().Add(timeout)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeWebSocketFrame(connection, opcode, payload)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -25,6 +26,24 @@ func TestWebSocketReaderRejectsOversizedControlFrame(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebSocketWriterDoesNotBlockForeverOnSlowClient(t *testing.T) {
|
||||||
|
sender, receiver := net.Pipe()
|
||||||
|
defer sender.Close()
|
||||||
|
defer receiver.Close()
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- writeWebSocketFrameWithDeadline(sender, 0x1, bytes.Repeat([]byte{'x'}, maxWebSocketFrame), 20*time.Millisecond)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("write to a non-reading client unexpectedly succeeded")
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("write to a non-reading client blocked past its deadline")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWebSocketHandshakeRequiresRFC6455Version(t *testing.T) {
|
func TestWebSocketHandshakeRequiresRFC6455Version(t *testing.T) {
|
||||||
service := &Service{}
|
service := &Service{}
|
||||||
request := httptest.NewRequest(http.MethodGet, "/v1/events", nil)
|
request := httptest.NewRequest(http.MethodGet, "/v1/events", nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user