fix(multiplayer): reconcile authoritative initial connections

This commit is contained in:
Josh Creek
2026-09-03 00:02:04 +01:00
parent 781cbc35aa
commit aa446cfbfe
40 changed files with 809 additions and 87 deletions
+5 -1
View File
@@ -1228,7 +1228,7 @@ production fallback.
| 8.32 `[D:8.2,8.26,8.30]` | **IN PROGRESS.** Provider-neutral FleetAutoscaler baseline preserves a two-process Ready buffer, caps warm capacity, and leaves Allocated scale-down independent of the Ready floor; Fleet image references remain digest-pinned for current/rollback pre-pull | `deploy/k8s/base/fleet-autoscaler.yaml` and manifest tests cover Fleet ownership, Buffer policy and floor/cap invariants; regional on-demand node pools/failure domains, pre-pull rollout, warm-allocation p95/p99 and N+1 certification remain |
| 8.33 `[D:8.26,8.32]` | **IN PROGRESS.** Fleet scheduling now requires on-demand capacity and spreads Ready processes across zones with skew 1; the autoscaler preserves the two-process Ready floor | `deploy/k8s/base/fleet.yaml` and manifest tests reject interruptible placement and single-zone concentration structurally; regional node pools, forced node-loss testing and measured N+1 headroom remain |
| 8.34 `[D:8.28,8.29]` | Native x86_64 benchmark of boot-to-process-ready and assignment-ready, p99 CPU/RSS/network and 60 Hz ticks; limits/node cap with 30% headroom | Measurements replace old estimates and certify density with no tick backlog |
| 8.35 `[D:8.17,8.19,8.20,8.30,8.31]` | **IN PROGRESS.** Pure Go initial-connect policy decides ranked 30 s no-show cancellation with abandon ladder and casual 60 s bot start only when each team has a human; empty-team casual allocations cancel | `server/domain/noshow.go` covers wait/deadline boundaries, deterministic no-show/innocent ordering, ranked cooldown history and no pre-live rating action; persistent ticket restoration, allocation shutdown, bot spawn and live integration remain |
| 8.35 `[D:8.17,8.19,8.20,8.30,8.31]` | **IN PROGRESS.** Allocated Godot servers now report each signed-roster admission through a match-bound workload-authenticated/idempotent API; PostgreSQL persists `connected_at`, starts the fair deadline at durable `ASSIGNMENT_READY`, and atomically starts complete rosters, applies ranked 30 s no-show cancellation/abandon ladders, or applies casual bot/cancel outcomes after 60 s. The maintenance role evaluates this path every second. Godot's local clock is armed only after the same durable readiness transition and applies the same complete/partial roster policy | Domain/store/API/supervisor/Godot tests cover forged workload/allocation/player bindings, replay after response loss, malformed rosters, complete ranked/casual starts, relaxed 25-human bot starts, canonical team/global-slot preservation, empty-team cancellation, stale-snapshot races, retryable datastore outages, and readiness-clock ordering. Migration `0010_initial_connect_ready_at.sql` gives deployed in-flight matches a fresh window. Live PostgreSQL execution, allocated process termination evidence, and real Agones multi-client verification remain |
| 8.36 `[D:8.10,8.25,8.28,8.30]` | **IN PROGRESS.** Supervisor exposes an authenticated loopback-only drain request boundary that never places the token in command arguments/logs and rejects remote/partial/query-bearing configurations; the base now includes a two-Ready PodDisruptionBudget; `Supervisor.Run` and `cmd/game-server-supervisor` now orchestrate signal-bound drain-before-kill with a bounded grace deadline | `server/supervisor/`, `server/cmd/game-server-supervisor/`, `server_control.gd` and `deploy/k8s/base/game-server-pdb.yaml` cover bearer-token enforcement, loopback URL validation, secret-safe configuration, missing drain credentials, readiness transitions, Ready-floor disruption protection, graceful child exit after drain and force-kill of an unresponsive child; live 300 s/285 s lifecycle, PDB/Fleet drain and infrastructure-abort classification remain |
| 8.37 `[D:8.5,8.10,8.25,8.26,8.31]` | Horizontally scaled primary control plane + warm standby, EU/NA fleets, RPO <=5 m/RTO <=30 m; signed result annotation/reconciliation preserves delivery outages | Restore/failover meet targets; live simulation continues; valid delayed result commits exactly once after recovery |
| 8.38 `[D:7.7,8.26,8.36,8.37]` | Provider migration after Valve approves both providers' EU/NA POPs/certs and public UDP: restore, validate coordinator trust, switch allocations, drain old | Both geographies complete Hosted-SDR matches on new provider and no old-provider live match is terminated |
@@ -1649,3 +1649,7 @@ Allocator probes now distinguish process liveness from useful progress. `/health
The timeout boundary is enforced inside both network adapters as well as in the production allocator wiring: an `agones.Client` or game-server `Supervisor` constructed without an injected HTTP client now receives a ten-second client rather than Go's unbounded `http.DefaultClient`. This prevents alternate binaries, tests, and future callers from restoring an infinite GameServer, roster, registration, or SDK wait by omission.
Control-plane probes now separate liveness from datastore readiness too. `/healthz` proves the process can serve without restarting it during a PostgreSQL outage; `/readyz` runs a one-second-bounded `PingContext` and the Deployment routes traffic only to replicas whose core durable store responds. Probe and metrics routes bypass the player request limiter, so operator-selected low limits cannot make Kubernetes evict a healthy replica. Missing checks, datastore errors, non-GET methods, and successful recovery are covered by API tests.
The task 8.35 adversarial pass closed the previously disconnected initial-connect implementations. An accepted signed player now produces a workload-authenticated `POST /servers/{serverId}/connect` receipt bound to the exact allocation, match, server, participant, and unexpired assignment; durable replay survives a lost response and keys include the match so a later match cannot conflict. Unknown datastore failures return retryable 503 responses. Player assignment reads are hidden until the match has durably reached `ASSIGNMENT_READY`, and the supervisor now fails closed if that transition never commits.
Initial-connect timing and topology now agree across every layer. Migration 0010 records `initial_connect_ready_at` at the assignment-ready transition instead of using match creation time; maintenance polls that path independently every second; and an authenticated loopback signal arms Godot's local timeout only after the durable transition. Complete rosters enter `LIVE` immediately, relaxed two-to-five-human casual rosters immediately fill their disclosed vacant slots with bots, and six-human casual no-shows use the 60-second policy. Casual lineup, reconnect, signed-roster, matcher, store, and Godot validation all use canonical global slots 02 for team 0 and 35 for team 1; the earlier alternating-slot bot layout has been removed. Focused Go tests, contract/migration/manifest checks, and the 204-test Godot harness pass; the committed PostgreSQL integration assertion remains unexecuted locally while Docker storage is exhausted.