diff --git a/multiplayer-next.md b/multiplayer-next.md index 1a3f8ad7..69f048d1 100644 --- a/multiplayer-next.md +++ b/multiplayer-next.md @@ -1183,7 +1183,7 @@ production fallback. | 8.2 `[D:8.1]` | **DONE.** Encode the launch SLOs from `docs/MATCHMAKING.md`: RTT, allocation/connect latency, 99.9% allocation/result success, API latency and tick health | [`docs/MATCHMAKING-SLOs.md`](docs/MATCHMAKING-SLOs.md) defines each metric, denominator, percentile/window, owner, alert threshold and release evidence | | 8.3 `[D:8.1]` | **DONE.** Publish versioned OpenAPI + WebSocket contracts for Steam login/session, profile/rating, queue create/heartbeat/cancel/resume, proposal accept/decline, assignment/status, server registration/roster/result/shutdown | [`server/contracts/v1/`](server/contracts/v1/) contains machine-readable REST/events contracts and dependency-free structural tests; REST resync is specified by the contract; `server/api/service.go` also exposes the documented `/api/v1` route names (including server-assigned idempotent queue ticket IDs and DELETE cancellation) alongside the existing client `/v1` routes, covered by `TestDocumentedContractRoutesAdaptToServiceAPI` | | 8.4 `[D:8.3]` | **DONE.** Define opaque IDs, legal queue/match state transitions, revisions and idempotency keys | [`server/contracts/v1/state-transitions.json`](server/contracts/v1/state-transitions.json) locks terminal states, legal edges, stale-revision handling and same-key replay/conflict behavior; contract tests cover the invariants | -| 8.5 `[D:8.4]` | **IN PROGRESS.** Initial PostgreSQL migration now defines durable idempotency keys, queue ownership/active-participation fencing, identities, sessions/revocations, ranked seasons, ratings/events, matches/participants, penalties, results, audits and outbox; follow-up migrations persist server-derived queue probe RTT metadata, the allocator GameServer/allocation registry, matcher-selected proposal region/protocol/team/slot plans, leased allocating-match claims, optional shared regional allocation quotas, initial-connect timing, and participant disconnect lease timestamps | `server/migrations/0001_initial.sql` through `0012_validate_connection_leases.sql`, `migrations/runner.go`, `cmd/migrate` and static checks cover the durable tables, uniqueness/check constraints, Redis-as-cache boundary and serialized forward migration recording. Migration 0011 backfills legacy connected participants to generation one; 0012 then validates the lease check so an inconsistent legacy row halts rollout instead of surviving behind a `NOT VALID` constraint. `migrations.Rollback` reverses N most-applied migrations via matching down files; prior live rollback/reapply verification remains valid, while the new validation awaits a live database rerun because local Docker storage is exhausted | +| 8.5 `[D:8.4]` | **IN PROGRESS.** Initial PostgreSQL migration now defines durable idempotency keys, queue ownership/active-participation fencing, identities, sessions/revocations, ranked seasons, ratings/events, matches/participants, penalties, results, audits and outbox; follow-up migrations persist server-derived queue probe RTT metadata, the allocator GameServer/allocation registry, matcher-selected proposal region/protocol/team/slot plans, leased allocating-match claims, optional shared regional allocation quotas, initial-connect timing, and participant disconnect lease timestamps | `server/migrations/0001_initial.sql` through `0013_validate_initial_connect_ready.sql`, `migrations/runner.go`, `cmd/migrate` and static checks cover the durable tables, uniqueness/check constraints, Redis-as-cache boundary and serialized forward migration recording. Migration 0011 backfills legacy connected participants to generation one; 0012 validates the lease check, and 0013 validates 0010's initial-connect timestamp backfill, so inconsistent legacy lifecycle rows halt rollout instead of surviving behind `NOT VALID` constraints. The arena constraints remain deliberately `NOT VALID` for historical ranked records created before arena identity existed; they still fence every new write. `migrations.Rollback` reverses N most-applied migrations via matching down files; prior live rollback/reapply verification remains valid, while the new validations await a live database rerun because local Docker storage is exhausted | | 8.6 `[D:8.3,8.4]` | **IN PROGRESS.** Add allocated-mode `ServerConfig` compatibility fields as opt-in defaults | `ServerConfig` now validates allocation mode, match/server IDs, playlist version, client build, future assignment expiry, image digest, transport and EU/NA region; `server_boot.gd` fails closed for the not-yet-wired Steam SDR transport, constrains allocated processes to one match, and emits allocation identity/transport in `server_started`; signed-authorisation admission, dynamic endpoint wiring and full manifest/runtime tests remain | #### 8B — Authentication and secure control plane diff --git a/server/migrations/0013_validate_initial_connect_ready.sql b/server/migrations/0013_validate_initial_connect_ready.sql new file mode 100644 index 00000000..2ffcbd39 --- /dev/null +++ b/server/migrations/0013_validate_initial_connect_ready.sql @@ -0,0 +1,5 @@ +-- Migration 0010 backfilled every state that requires an initial-connect +-- timestamp. Validate that invariant now so an anomalous legacy row blocks +-- rollout instead of silently bypassing no-show reconciliation. +ALTER TABLE matches + VALIDATE CONSTRAINT matches_initial_connect_ready_at; diff --git a/server/migrations/down/0013_validate_initial_connect_ready.sql b/server/migrations/down/0013_validate_initial_connect_ready.sql new file mode 100644 index 00000000..c6a95b7b --- /dev/null +++ b/server/migrations/down/0013_validate_initial_connect_ready.sql @@ -0,0 +1 @@ +-- Constraint validation changes no schema and is intentionally irreversible. diff --git a/server/migrations/test_migration.py b/server/migrations/test_migration.py index 68027458..dea09be0 100644 --- a/server/migrations/test_migration.py +++ b/server/migrations/test_migration.py @@ -11,6 +11,7 @@ ARENAS_SQL = (Path(__file__).parent / "0008_match_arena_paths.sql").read_text() ALLOCATION_ARENAS_SQL = (Path(__file__).parent / "0009_allocation_arena_paths.sql").read_text() INITIAL_CONNECT_READY_SQL = (Path(__file__).parent / "0010_initial_connect_ready_at.sql").read_text() CONNECTION_LEASE_VALIDATION_SQL = (Path(__file__).parent / "0012_validate_connection_leases.sql").read_text() +INITIAL_CONNECT_VALIDATION_SQL = (Path(__file__).parent / "0013_validate_initial_connect_ready.sql").read_text() class MigrationTest(unittest.TestCase): @@ -82,6 +83,9 @@ class MigrationTest(unittest.TestCase): def test_connection_lease_backfill_is_validated_for_legacy_rows(self): self.assertIn("VALIDATE CONSTRAINT match_participants_connection_lease", CONNECTION_LEASE_VALIDATION_SQL) + def test_initial_connect_backfill_is_validated_for_legacy_rows(self): + self.assertIn("VALIDATE CONSTRAINT matches_initial_connect_ready_at", INITIAL_CONNECT_VALIDATION_SQL) + if __name__ == "__main__": unittest.main()