mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
fix(multiplayer): repair PostgreSQL integration invariants
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -39,10 +39,14 @@ func ApplyRankedSeasonRollover(ctx context.Context, db *sql.DB, playerID, season
|
||||
applied := false
|
||||
err := RunSerializable(ctx, db, DefaultSerializableAttempts, func(ctx context.Context, tx *sql.Tx) error {
|
||||
var locked domain.RankedProfile
|
||||
var lockedPlayerID string
|
||||
var revision int64
|
||||
if err := tx.QueryRowContext(ctx, SeasonRatingLockSQL, playerID).Scan(&locked.Value, &locked.RD, &locked.Volatility, &locked.RankedGames, &revision); err != nil {
|
||||
if err := tx.QueryRowContext(ctx, SeasonRatingLockSQL, playerID).Scan(&lockedPlayerID, &locked.Value, &locked.RD, &locked.Volatility, &locked.RankedGames, &revision); err != nil {
|
||||
return err
|
||||
}
|
||||
if lockedPlayerID != playerID {
|
||||
return fmt.Errorf("locked unexpected rating row")
|
||||
}
|
||||
var err error
|
||||
updated, _, err = domain.ApplySeasonRollover(locked, seasonID)
|
||||
if err != nil {
|
||||
|
||||
@@ -44,7 +44,7 @@ const ExpireStalledAllocationsSQL = `WITH stalled AS (
|
||||
'event', 'state_changed',
|
||||
'revision', failed.revision,
|
||||
'resource_id', failed.match_id,
|
||||
'occurred_at', $4,
|
||||
'occurred_at', $4::timestamptz,
|
||||
'state', 'FAILED',
|
||||
'match_id', failed.match_id,
|
||||
'player_ids', COALESCE((
|
||||
@@ -54,6 +54,7 @@ const ExpireStalledAllocationsSQL = `WITH stalled AS (
|
||||
)
|
||||
FROM failed
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING event_id
|
||||
)
|
||||
SELECT (SELECT count(*) FROM failed), (SELECT count(*) FROM requeued), (SELECT count(*) FROM events)`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user