19 Commits

Author SHA1 Message Date
Josh Creek 801fca7cb0 fix(matchmaking): make regional RTT evidence obtainable end to end
domain.validCandidate hard-requires a non-empty PredictedRTT map, but
CreateQueueTicket persisted an empty one and the only endpoint that
could fill it returned 503 in every real binary, because Service.Probe
was assigned nowhere outside api tests. No client-created ticket could
ever be selected by the matcher. The Godot client had no probe method at
all, so even a wired backend was unreachable from the game.

Four distinct defects had to be fixed for this path to work:

Nothing issued the nonce ProbeProvider was meant to compare against, so
the contract could not be satisfied even in principle. Add
POST /v1/probes/{region}/challenge, backed by a durable single-use
challenge -- durable because any replica may serve the answer for a
challenge another replica issued. RTT is the interval between issuing
and receiving, so no client-reported latency reaches placement.

CreateQueueTicket marshalled a nil map to JSON `null`, a JSONB scalar
rather than an object, and jsonb_set rejects that with "cannot set path
in scalar". RecordProbe would have failed at runtime even once wired.
Persist an object, and normalise non-object values in the update for
rows already written.

A nil ProbeRecorder made the handler report success while persisting
nothing, which silently leaves the ticket unmatchable. That is a
misconfiguration, not a successful probe; it now returns 503.

A successful probe updated PostgreSQL only. The candidate inserted at
enqueue time carries an empty RTT map, and the Redis keyspace has its
TTL continually refreshed, so the stale entry need never repair itself.
Refresh that player's projection after the probe commits.

Client side: add the challenge/answer round trip and have the
matchmaking screen collect evidence before creating a ticket, since
queueing first produces a search that can never match. Probing every
region fully is not required -- placement uses whichever regions
answered -- but queueing with none is refused rather than silently
stalling.

New integration test drives the real enqueue and probe paths and then
asks the actual matcher predicate, rather than hand-building a candidate
the way the unit tests do -- which is exactly why they missed this.

Also make the integration schema reset drop the whole public schema: the
enumerated table list silently broke with each new migration.
2026-09-05 10:49:28 +01:00
Josh Creek 5765532409 fix(allocator): publish signed assignment rosters before servers start
The root blocker (issue #14). The worker bound the provider allocation
and stopped. Service.PublishRoster and store.SaveVerifiedAssignmentRoster
both existed, fully tested, with zero non-test callers, and the
production allocator configured neither a roster store nor a signing
key. Nothing ever wrote the assignments table.

The allocated supervisor fetches a non-empty roster before it launches
the game child, so every real allocation failed at that fetch: no match
could reach ASSIGNMENT_READY or accept a player. Existing tests seeded
assignments directly, which is exactly why the missing hand-off went
unnoticed.

The worker now builds one join authorisation per durable participant,
signs each with the active key, and publishes them. Participants are
read through the same query SaveVerifiedAssignmentRoster re-validates
against, so the allocator cannot construct a roster the persistence
boundary would reject. The manifest commits to a digest over the whole
roster, so a server cannot be handed a truncated roster whose surviving
entries are each individually valid.

Persist the provider endpoint on the allocation: it arrived on the
provider response and was never stored, so a worker crashing between
allocating and publishing had no endpoint to recover and would have
stranded the match permanently. Republishing is idempotent, so that
crash now simply retries.

cmd/allocator refuses to start without key material rather than running
an allocator that binds allocations and silently strands every match.
The k8s allocator Deployment mounts the same key set the Fleet does, and
both now take the JSON key map so a rotation can publish several.

New integration test drives the real worker through to the supervisor's
own roster read path without seeding the assignments table. Verified it
fails with "assignments = 0, want 2" when the publish step is removed.
2026-09-05 10:42:31 +01:00
Josh Creek bef71e1dcf fix(multiplayer): fence provider allocation results 2026-09-03 00:19:03 +01:00
Josh Creek cbefa86c5c fix(multiplayer): report allocator readiness 2026-09-02 19:11:01 +01:00
Josh Creek 950e879861 fix(multiplayer): bind allocation to accepted proposal 2026-09-01 21:41:44 +01:00
Josh Creek d3a457d8d0 fix(multiplayer): avoid quota double charge on recovery 2026-09-01 21:39:18 +01:00
Josh Creek 3eb47bb9d3 fix(multiplayer): fence recovered arena identity 2026-09-01 21:21:10 +01:00
Josh Creek 9a72dd5eab feat(multiplayer): expose allocator quota metrics 2026-09-01 18:43:55 +01:00
Josh Creek 72e8d27633 feat(multiplayer): add shared allocation quota 2026-09-01 18:38:06 +01:00
Josh Creek 181a928c87 feat(multiplayer): add regional allocation budget 2026-09-01 18:25:25 +01:00
Josh Creek 3317574bf2 fix(multiplayer): fence recovered allocation tuples 2026-09-01 18:06:56 +01:00
Josh Creek bf396afcaf fix(multiplayer): recover ambiguous agones allocations 2026-09-01 18:05:43 +01:00
Josh Creek 9ae01ecc5a fix(multiplayer): propagate allocated playlist 2026-09-01 17:00:37 +01:00
Josh Creek 207ab47866 test(multiplayer): verify allocator worker lifecycle 2026-09-01 15:34:51 +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 11599889fa feat: gate allocation on accepted proposals 2026-09-01 10:18:22 +01:00
Josh Creek febc69bdef feat: gate allocator roster publication 2026-09-01 09:55:09 +01:00
Josh Creek 0023bdab6e feat: reconcile Agones allocations durably 2026-09-01 09:51:52 +01:00