Commit Graph

66 Commits

Author SHA1 Message Date
Josh Creek 270ab00c52 test(multiplayer): add real concurrent queue-heartbeat revision race test
Races 5 concurrent HeartbeatQueueTicket calls, all at the same expected
revision, against a real PostgreSQL instance -- a genuinely plausible
client scenario (slow-response retry, duplicate tab/process), and one
the existing sequential stale-revision test can't exercise since it
only calls the second heartbeat after the first has already
committed. Asserts exactly one wins, the durable revision ends at
exactly 1 (not higher, which a stale winner slipping through would
produce), and every loser fails cleanly rather than hanging or
returning a raw serialization error. Stable across 6 runs with -race,
plus the full integration and unit suites.
2026-09-01 13:21:59 +01:00
Josh Creek 17bd7768eb test(multiplayer): add real-Redis integration coverage for the candidate index
Every existing RedisCandidateIndex test runs against miniredis -- a
from-scratch Go reimplementation of the Redis command set, not real
Redis's own float64 score encoding, TTL/expiry, or RESP behavior.
Add an opt-in integration suite (mirroring postgres_integration_test.go's
pattern: //go:build integration, COSMIC_CLASH_REDIS_ADDR-gated) plus
scripts/run_redis_integration.sh against a disposable redis:7-alpine
container, covering:

  - upsert/snapshot/remove against a real server
  - a real TTL actually waited out (not miniredis's manual
    FastForward), proving expiry really happens on the wire
  - the documented 'Redis restart or lost keyspace' repair path
    exercised against an actual FLUSHALL, not a simulated empty map,
    including that the repair actually persists back to Redis (a
    second snapshot reads it without a second durable-source call)

Verified stable across 3 runs with -race against a real container.
2026-09-01 13:20:09 +01:00
Josh Creek caece00e7f test(multiplayer): add real concurrent result-submission rating test
Races 5 concurrent identical CompleteResultWithResult calls for the
same RESULT_PENDING match against a real PostgreSQL instance -- the
scenario behind 8.25's 'identical duplicates idempotent' claim, which
every existing result test only exercised sequentially. All five must
succeed (idempotent replay, not conflict), and the rating update must
apply exactly once: asserted by computing the expected post-match
rating independently via the same domain.CasualOpponents/UpdateRating
functions and requiring an exact match, since a doubled application
would compound the rating further away from baseline rather than
merely producing 'some' change that a looser inequality check would
miss.

First draft asserted ranked_games == 1, which doesn't hold for a
casual result (rankedIncrement is unconditionally 0 for casual by
design) -- caught by actually running it, not by inspection. Verified
clean across 6 runs with -race, plus the full integration and unit
suites.
2026-09-01 13:17:58 +01:00
Josh Creek 42ac34ca70 test(multiplayer): add real concurrent allocation-claim integration test
Fires more concurrent ClaimAllocation calls than there is Ready
capacity at a real PostgreSQL instance and asserts: exactly as many
win as there was capacity, every winner gets a distinct server (no
double-booking), every loser gets ErrNoCapacity rather than a raw
serialization error or a hang, and the durable game_servers.state
count matches. This is the cross-allocator-replica race 8.30 calls
out as untested -- the existing capacity test in this file claims
strictly one request at a time. Verified clean across 6 runs with
-race, plus the full integration and unit suites.
2026-09-01 13:14:46 +01:00
Josh Creek 63eb43d50a test(multiplayer): add real concurrent proposal-claim integration test
Every existing Postgres integration test runs strictly one transaction
at a time, so none of them exercise the SERIALIZABLE retry-and-fence
path CreateProposal actually depends on for correctness under real
matcher-replica contention -- only concurrent goroutines against a
real connection can. Add a test that races two goroutines each
proposing a formation that shares one contested ticket (a realistic
scenario: nothing stops two matcher replicas reading the same QUEUED
ticket in the same poll window), and asserts exactly one proposal
commits, the loser's proposal and participant rows are fully rolled
back, the contested ticket ends up claimed by the winner, and -- the
part a single-threaded test can't show -- the loser's OWN uncontested
ticket also rolls back to QUEUED rather than being left stranded as
PROPOSED with no surviving proposal.

Adversarial review of my own first draft: it initially failed
deterministically (5/5 runs), but the failure was in the test itself
-- the winner/loser branch picking the loser's uncontested ticket had
the two branches swapped, so it was checking the WINNER's ticket
against the QUEUED expectation. Fixed and re-verified clean across 8
runs with -race, plus the full integration suite.
2026-09-01 13:13:16 +01:00
Josh Creek 66d114bfe3 test(server): seed a seasons row for the ranked rollover integration test
ranked_season_rollovers.season_id has a foreign key into seasons, but
the integration test never inserted a seasons row for 'season-1' --
ApplyRankedSeasonRollover failed on the FK constraint before the
rollover logic itself ran at all. Insert a matching seasons row,
mirroring how a real 12-week season would already exist when
maintenance's rollover sweep runs. Verified against a real
PostgreSQL instance.
2026-09-01 12:44:48 +01:00
Josh Creek 6d3490da14 fix(server): gate proposal participant timeout on actual expiry
ProposalParticipantExpireSQL marked every PENDING participant on a
proposal TIMED_OUT unconditionally -- it took a proposal_id and 'now'
but never actually compared 'now' against the proposal's expires_at,
unlike its sibling ProposalExpireSQL (which does gate on
'expires_at <= $2'). Both GetProposal and RespondToProposal run this
statement on every call as a recovery step, so the very first
RespondToProposal for any proposal timed out every participant
(including the one about to respond) before checking their response,
then rejected the real accept/decline with ErrConflict. Add the same
expiry gate via an EXISTS against proposals.expires_at, matching
ProposalExpireSQL's own condition, and update the SQL-fragment test to
assert the gate is present. Verified end to end against a real
PostgreSQL instance: TestPostgreSQLProposalClaimAndResponseAreAtomic
now passes a two-participant accept/accept sequence that previously
failed on the first response.
2026-09-01 12:44:43 +01:00
Josh Creek e23243ff56 fix(server): drop extra unused argument in queue ticket insert
CreateQueueTicket passed 9 arguments to QueueTicketInsertSQL, which
only has 8 placeholders (state is a hardcoded 'QUEUED' literal in the
SQL, not $4) -- every real queue-ticket creation against PostgreSQL
failed with 'mismatched param and argument count'. Found by actually
running the opt-in Postgres integration suite (previously never
exercised locally, per its own gating) rather than trusting the unit
tests, which mock the driver and can't catch a placeholder-count
mismatch. Verified fixed against a real postgres:17-alpine container.
2026-09-01 12:44:36 +01:00
Josh Creek 67609d71c0 feat(multiplayer): add down migrations and a rollback runner
Add migrations.Rollback(ctx, db, dir, steps): reverses the N most
recently applied migrations, newest first, each in its own committed
transaction under the same advisory lock Apply uses. Down SQL lives in
migrations/down/<version>.sql (a subdirectory, so Apply's *.sql glob
over the main directory is untouched); a missing down file for a
migration being rolled back is a hard error rather than a silent
partial reversal. Wire it into cmd/migrate as --rollback=N.

Add down files for all six existing migrations, each dropping objects
in FK-safe reverse dependency order.

Adversarial review: could not run the new integration test
(TestPostgreSQLMigrationsRollBackAndReapplyCleanly, gated behind
COSMIC_CLASH_POSTGRES_DSN / scripts/run_postgres_integration.sh)
against a real database in this sandbox - Docker Desktop's own
overlayfs ran out of space pulling postgres:17-alpine, unrelated to
this change. Verified instead by hand-tracing every DROP against its
forward migration's FK graph, confirming Apply's directory glob does
not pick up the down/ subdirectory, and a clean go build/vet/test
-tags integration. Worth an explicit real run before this is trusted
in CI.
2026-09-01 12:41:30 +01:00
Josh Creek 7e5cfdeceb style(server): gofmt allocation_match_sql_test.go 2026-09-01 12:41:18 +01:00
Josh Creek d937cb153c feat(multiplayer): add server process/assignment-ready registration API
Add POST /v1/servers/{id}/register (and its /api/v1 contract alias),
authenticated by the same workload binding as the result route. A
game server reports its protocol version and image digest and asks
to advance ALLOCATING -> PROCESS_READY -> ASSIGNMENT_READY; the store
boundary (AdvanceServerRegistration) does this as one idempotent
SERIALIZABLE transaction that also advances every participant's queue
ticket, and gates the final transition on every participant having a
live, unexpired assignment.

Adversarial review of the surrounding routing turned up a pre-existing
bug: contractServerMutation rejected any path containing '/', so the
already-documented /api/v1/servers/{id}/result route (and this new
/register route) 404'd for every real caller despite being declared
in the OpenAPI contract. Fix it to delegate shape validation to
serverMutation, matching how contractQueueMutation handles its own
two-segment paths, and add a regression test covering both contract
routes end to end.
2026-09-01 12:35:30 +01:00
Josh Creek 9dc1cc2d6f feat(multiplayer): refresh allocator ready servers 2026-09-01 10:44:43 +01:00
Josh Creek 013eb0778b fix(multiplayer): advance tickets on allocation bind 2026-09-01 10:42:10 +01:00
Josh Creek 25fdc2c2c8 fix(multiplayer): recover recorded allocations 2026-09-01 10:40:10 +01:00
Josh Creek 55c46f56ec feat(multiplayer): run leased allocator worker 2026-09-01 10:38:45 +01:00
Josh Creek 6f7d61eafb feat(multiplayer): lease allocating match claims 2026-09-01 10:36:13 +01:00
Josh Creek 03ff8e485e feat: promote accepted proposals from API 2026-09-01 10:29:50 +01:00
Josh Creek e0ba6c6ead fix: enforce ranked match promotion size 2026-09-01 10:22:26 +01:00
Josh Creek 7807b9706b feat: promote accepted proposals into matches 2026-09-01 10:21:41 +01:00
Josh Creek 2d750cbcab feat: enable guarded ranked matcher role 2026-09-01 10:14:22 +01:00
Josh Creek d882469c79 feat: repair matcher candidates through redis projection 2026-09-01 10:11:54 +01:00
Josh Creek 7a3d520608 fix: bind rated result to durable receipt 2026-09-01 10:09:40 +01:00
Josh Creek dd80b52a11 feat: apply certified ratings during result completion 2026-09-01 10:08:11 +01:00
Josh Creek 388300c553 fix: validate durable result submissions 2026-09-01 10:02:29 +01:00
Josh Creek eebab1bc19 feat: add workload-authenticated result API 2026-09-01 10:01:04 +01:00
Josh Creek febc69bdef feat: gate allocator roster publication 2026-09-01 09:55:09 +01:00
Josh Creek 8b5b5333c6 fix: verify signed assignment rosters 2026-09-01 09:53:45 +01:00
Josh Creek 55e07648cf test: cover provider allocation reconciliation SQL 2026-09-01 09:52:00 +01:00
Josh Creek 0023bdab6e feat: reconcile Agones allocations durably 2026-09-01 09:51:52 +01:00
Josh Creek 4bbaf0976f feat: add ranked season maintenance role 2026-09-01 09:46:57 +01:00
Josh Creek e729570010 test: cover allocator PostgreSQL claims 2026-09-01 09:45:02 +01:00
Josh Creek b1966a3423 feat: add durable allocator claim boundary 2026-09-01 09:43:33 +01:00
Josh Creek def60169a8 feat: persist authenticated queue probe RTT 2026-09-01 09:40:25 +01:00
Josh Creek 25cc182793 feat: persist queue probe metadata 2026-09-01 09:37:47 +01:00
Josh Creek e170bcf0ef fix: bind matcher source to playlist 2026-09-01 09:35:00 +01:00
Josh Creek d0952adaf9 feat: add runnable casual matcher role 2026-09-01 09:32:22 +01:00
Josh Creek 18888ed520 feat: add PostgreSQL migration runner 2026-09-01 09:21:26 +01:00
Josh Creek 5b80c97337 test: cover PostgreSQL season rollover 2026-09-01 09:06:10 +01:00
Josh Creek bd26aa3dc4 test: cover PostgreSQL result and outbox flow 2026-09-01 09:05:15 +01:00
Josh Creek 9d210d254a test: cover PostgreSQL proposal transactions 2026-09-01 09:04:16 +01:00
Josh Creek c8a542a3af feat: repair Redis candidates from durable source 2026-09-01 09:02:55 +01:00
Josh Creek eadaa7b54e feat: add rebuildable Redis candidate index 2026-09-01 09:01:32 +01:00
Josh Creek 414df530f0 test: expand PostgreSQL adapter coverage 2026-09-01 08:59:39 +01:00
Josh Creek 5234390931 test: add PostgreSQL integration harness 2026-09-01 08:58:28 +01:00
Josh Creek 23134796ee feat: add durable outbox dispatcher 2026-09-01 08:12:33 +01:00
Josh Creek fec4f91672 feat: bind signed rosters to assignments 2026-09-01 08:10:05 +01:00
Josh Creek 63f4f11aa8 feat: publish assignment rosters atomically 2026-09-01 08:08:00 +01:00
Josh Creek f520584368 fix: expire proposals on response boundary 2026-09-01 08:06:17 +01:00
Josh Creek eef7cf28da feat: make proposal responses durable 2026-09-01 07:52:52 +01:00
Josh Creek 30b4560bd5 feat: persist participant-scoped proposal recovery 2026-09-01 07:50:11 +01:00