fix(multiplayer): repair PostgreSQL integration invariants

This commit is contained in:
Josh Creek
2026-09-04 15:43:31 +01:00
parent 0acf144f45
commit e6733bd6cb
4 changed files with 43 additions and 12 deletions
+33 -7
View File
@@ -6,6 +6,7 @@ import (
"context"
"crypto/sha256"
"database/sql"
"encoding/json"
"errors"
"fmt"
"os"
@@ -288,7 +289,7 @@ func TestPostgreSQLAllocationMatchClaimLeaseAndBindFence(t *testing.T) {
}
}
claim, found, err := ClaimAllocatingMatch(ctx, db, "enet", now)
if err != nil || !found || claim.Request != (domain.AllocationRequest{AllocationID: "allocation-allocation-match", MatchID: "allocation-match", Region: "EU", Build: "build-1", Protocol: 1, Transport: "enet"}) {
if err != nil || !found || claim.Request != (domain.AllocationRequest{AllocationID: "allocation-allocation-match", MatchID: "allocation-match", Playlist: domain.Casual, Region: "EU", Build: "build-1", Protocol: 1, Transport: "enet"}) {
t.Fatalf("claim=%+v found=%t err=%v", claim, found, err)
}
if err := ReleaseAllocatedMatchClaim(ctx, db, claim.Request.MatchID, "different-allocation"); err != domain.ErrConflict {
@@ -325,7 +326,18 @@ func TestPostgreSQLAllocationMatchClaimLeaseAndBindFence(t *testing.T) {
if err := db.QueryRowContext(ctx, `SELECT event_type, payload FROM outbox WHERE aggregate_id = 'allocation-match' AND event_type = 'state_changed'`).Scan(&eventType, &eventPayload); err != nil {
t.Fatalf("allocation outbox event: %v", err)
}
if eventType != "state_changed" || !strings.Contains(string(eventPayload), `"state":"ALLOCATING"`) || !strings.Contains(string(eventPayload), `"allocation-match-a"`) || !strings.Contains(string(eventPayload), `"allocation-match-b"`) {
var event struct {
State string `json:"state"`
PlayerIDs []string `json:"player_ids"`
}
if err := json.Unmarshal(eventPayload, &event); err != nil {
t.Fatalf("decode allocation outbox event: %v", err)
}
players := make(map[string]bool, len(event.PlayerIDs))
for _, playerID := range event.PlayerIDs {
players[playerID] = true
}
if eventType != "state_changed" || event.State != "ALLOCATING" || !players["allocation-match-a"] || !players["allocation-match-b"] {
t.Fatalf("allocation outbox event = %s", eventPayload)
}
if _, found, err := ClaimAllocatingMatch(ctx, db, "enet", now.Add(2*time.Second)); err != nil || found {
@@ -588,12 +600,15 @@ func TestPostgreSQLConnectionReceiptsStartCompleteRelaxedCasualRoster(t *testing
if _, err := db.ExecContext(ctx, `INSERT INTO game_servers (server_id, region, build, protocol_version, transport, state) VALUES ('connect-server', 'EU', 'integration-build', 1, 'enet', 'ALLOCATED')`); err != nil {
t.Fatal(err)
}
if _, err := db.ExecContext(ctx, `INSERT INTO matches (match_id, playlist, state, region, protocol_version, server_id, allocation_id, initial_connect_ready_at) VALUES ('connect-match', 'casual', 'ASSIGNMENT_READY', 'EU', 1, 'connect-server', 'connect-allocation', $1)`, now); err != nil {
if _, err := db.ExecContext(ctx, `INSERT INTO matches (match_id, playlist, state, region, protocol_version) VALUES ('connect-match', 'casual', 'ALLOCATING', 'EU', 1)`); err != nil {
t.Fatal(err)
}
if _, err := db.ExecContext(ctx, `INSERT INTO allocations (allocation_id, match_id, server_id, region, build, protocol_version, transport, request_digest, state, allocated_at) VALUES ('connect-allocation', 'connect-match', 'connect-server', 'EU', 'integration-build', 1, 'enet', $1, 'ALLOCATED', $2)`, []byte("request"), now); err != nil {
t.Fatal(err)
}
if _, err := db.ExecContext(ctx, `UPDATE matches SET state = 'ASSIGNMENT_READY', server_id = 'connect-server', allocation_id = 'connect-allocation', allocation_claimed_at = $1, initial_connect_ready_at = $1 WHERE match_id = 'connect-match'`, now); err != nil {
t.Fatal(err)
}
for i := 0; i < 2; i++ {
playerID := fmt.Sprintf("connect-player-%d", i)
ticketID := fmt.Sprintf("connect-ticket-%d", i)
@@ -662,7 +677,7 @@ func TestPostgreSQLLiveReconnectGraceExpiryPersistsAbandonmentWithoutReleasingRe
t.Fatal(err)
}
}
if _, err := db.ExecContext(ctx, `INSERT INTO matches (match_id, playlist, state, region, protocol_version, server_id) VALUES ('live-abandon-match', 'ranked', 'LIVE', 'EU', 1, 'live-abandon-server')`); err != nil {
if _, err := db.ExecContext(ctx, `INSERT INTO matches (match_id, playlist, state, region, protocol_version, server_id, arena_path) VALUES ('live-abandon-match', 'ranked', 'LIVE', 'EU', 1, 'live-abandon-server', 'res://scenes/arena_01.tscn')`); err != nil {
t.Fatal(err)
}
if _, err := db.ExecContext(ctx, `INSERT INTO match_participants (match_id, player_id, ticket_id, slot, team, connection_generation, connected_at, disconnected_at) VALUES
@@ -1523,7 +1538,18 @@ func TestPostgreSQLStalledAllocationsAreReclaimedWithoutPenalisingPlayers(t *tes
if err := db.QueryRow(`SELECT event_type, payload FROM outbox WHERE event_id = 'stalled-allocation:stalled-match:1'`).Scan(&eventType, &eventPayload); err != nil {
t.Fatalf("stalled allocation state event missing: %v", err)
}
if eventType != "state_changed" || !strings.Contains(string(eventPayload), `"state":"FAILED"`) || !strings.Contains(string(eventPayload), `"stall-player-a"`) {
var event struct {
State string `json:"state"`
PlayerIDs []string `json:"player_ids"`
}
if err := json.Unmarshal(eventPayload, &event); err != nil {
t.Fatalf("decode stalled allocation outbox event: %v", err)
}
players := make(map[string]bool, len(event.PlayerIDs))
for _, playerID := range event.PlayerIDs {
players[playerID] = true
}
if eventType != "state_changed" || event.State != "FAILED" || !players["stall-player-a"] {
t.Fatalf("stalled allocation event = %s %s, want FAILED state and affected player IDs", eventType, eventPayload)
}
@@ -1653,8 +1679,8 @@ func TestPostgreSQLMigrationsRollBackAndReapplyCleanly(t *testing.T) {
// Roll back every migration one at a time, in reverse, checking each
// down file actually undoes what its forward file created — not just
// that Rollback returns nil.
if err := migrations.Rollback(context.Background(), db, dir, 4); err != nil {
t.Fatalf("rollback 0010 through 0007: %v", err)
if err := migrations.Rollback(context.Background(), db, dir, 7); err != nil {
t.Fatalf("rollback 0013 through 0007: %v", err)
}
var hasInitialConnectReadyColumn bool
if err := db.QueryRow(`SELECT count(*) > 0 FROM information_schema.columns WHERE table_name = 'matches' AND column_name = 'initial_connect_ready_at'`).Scan(&hasInitialConnectReadyColumn); err != nil {