mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): persist arena identity on allocations
This commit is contained in:
+1
-1
@@ -1451,7 +1451,7 @@ The same allocation path now carries the matcher-selected playlist, preventing a
|
||||
|
||||
Ranked proposal admission no longer trusts the matcher’s `--ranked-random-arena` boolean. The Go domain now owns a named allowlist for the three floor-goal `ArenaRegistry` entries, and rejects unknown and elevated IDs before any proposal is created.
|
||||
|
||||
The arena hand-off is now durable: the matcher deterministically selects an eligible floor-goal arena from the proposal ID, migration 0008 stores that path on proposals and matches and enforces it for new direct SQL writes, each durable transition rechecks the same allowlist, allocation claims and idempotency digests retain it, Agones applies it as a match-scoped annotation, and the supervisor overlays the allocated child’s `--arena-path`. Godot accepts only the same floor-goal `ArenaRegistry` paths and requires one for allocated ranked matches, so a stale Fleet default, an elevated variant, or an altered retry cannot substitute a ranked arena.
|
||||
The arena hand-off is now durable: the matcher deterministically selects an eligible floor-goal arena from the proposal ID, migration 0008 stores that path on proposals and matches and enforces it for new direct SQL writes, migration 0009 retains it on provider allocations, each durable transition and recovery lookup rechecks the same allowlist, allocation claims and idempotency digests retain it, Agones applies it as a match-scoped annotation, and the supervisor overlays the allocated child’s `--arena-path`. Godot accepts only the same floor-goal `ArenaRegistry` paths and requires one for allocated ranked matches, so a stale Fleet default, an elevated variant, or an altered retry cannot substitute a ranked arena.
|
||||
|
||||
The Godot control-plane client now retains the exact last idempotent mutation and exposes `retry_last_mutation()` for transport, timeout, rate-limit, and 5xx failures. Retries reuse the original idempotency key and expected revision, while 401 and 409 responses remain non-retryable; the harness covers the policy boundary. This closes the local duplicate-action recovery mechanism for heartbeat/cancel/proposal calls, with broader live UI retry verification still remaining.
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ func (c Client) RecoverAllocation(ctx context.Context, request domain.Allocation
|
||||
if err != nil {
|
||||
return AllocatedServer{}, false, err
|
||||
}
|
||||
recovered = AllocatedServer{Allocation: domain.Allocation{AllocationID: request.AllocationID, MatchID: request.MatchID, ServerID: item.Metadata.Name, Region: request.Region, Build: request.Build, Protocol: request.Protocol, Transport: request.Transport, State: domain.ServerAllocated, AllocatedAt: now}, Endpoint: net.JoinHostPort(item.Status.Address, strconv.Itoa(port)), GameServer: item.Metadata.Name}
|
||||
recovered = AllocatedServer{Allocation: domain.Allocation{AllocationID: request.AllocationID, MatchID: request.MatchID, ServerID: item.Metadata.Name, Region: request.Region, Build: request.Build, Protocol: request.Protocol, ArenaPath: request.ArenaPath, Transport: request.Transport, State: domain.ServerAllocated, AllocatedAt: now}, Endpoint: net.JoinHostPort(item.Status.Address, strconv.Itoa(port)), GameServer: item.Metadata.Name}
|
||||
found = true
|
||||
}
|
||||
return recovered, found, nil
|
||||
@@ -291,7 +291,7 @@ func (c Client) Allocate(ctx context.Context, request domain.AllocationRequest,
|
||||
if err != nil {
|
||||
return AllocatedServer{}, err
|
||||
}
|
||||
return AllocatedServer{Allocation: domain.Allocation{AllocationID: request.AllocationID, MatchID: request.MatchID, ServerID: decoded.Status.GameServerName, Region: request.Region, Build: request.Build, Protocol: request.Protocol, Transport: request.Transport, State: domain.ServerAllocated, AllocatedAt: now}, Endpoint: net.JoinHostPort(decoded.Status.Address, strconv.Itoa(port)), GameServer: decoded.Status.GameServerName}, nil
|
||||
return AllocatedServer{Allocation: domain.Allocation{AllocationID: request.AllocationID, MatchID: request.MatchID, ServerID: decoded.Status.GameServerName, Region: request.Region, Build: request.Build, Protocol: request.Protocol, ArenaPath: request.ArenaPath, Transport: request.Transport, State: domain.ServerAllocated, AllocatedAt: now}, Endpoint: net.JoinHostPort(decoded.Status.Address, strconv.Itoa(port)), GameServer: decoded.Status.GameServerName}, nil
|
||||
}
|
||||
|
||||
func (c Client) endpoint() (string, error) {
|
||||
|
||||
@@ -42,6 +42,7 @@ type Allocation struct {
|
||||
Region string
|
||||
Build string
|
||||
Protocol int
|
||||
ArenaPath string
|
||||
Transport string
|
||||
State ServerLifecycle
|
||||
AllocatedAt time.Time
|
||||
@@ -104,7 +105,7 @@ func (a *Allocator) Allocate(request AllocationRequest, now time.Time) (Allocati
|
||||
server := a.servers[ids[0]]
|
||||
server.State = ServerAllocated
|
||||
a.servers[server.ServerID] = server
|
||||
allocation := Allocation{AllocationID: request.AllocationID, MatchID: request.MatchID, ServerID: server.ServerID, Region: server.Region, Build: server.Build, Protocol: server.Protocol, Transport: server.Transport, State: ServerAllocated, AllocatedAt: now}
|
||||
allocation := Allocation{AllocationID: request.AllocationID, MatchID: request.MatchID, ServerID: server.ServerID, Region: server.Region, Build: server.Build, Protocol: server.Protocol, ArenaPath: request.ArenaPath, Transport: server.Transport, State: ServerAllocated, AllocatedAt: now}
|
||||
a.allocations[request.AllocationID] = allocation
|
||||
a.requestHashes[request.AllocationID] = digest
|
||||
return allocation, nil
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Keep arena identity in the durable provider allocation record so retries
|
||||
-- and provider recovery compare the complete match compatibility tuple.
|
||||
ALTER TABLE allocations
|
||||
ADD COLUMN arena_path TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE allocations
|
||||
DROP COLUMN IF EXISTS arena_path;
|
||||
@@ -8,6 +8,7 @@ SQL = (Path(__file__).parent / "0001_initial.sql").read_text()
|
||||
ASSIGNMENTS_SQL = (Path(__file__).parent / "0002_assignments.sql").read_text()
|
||||
QUOTAS_SQL = (Path(__file__).parent / "0007_allocation_quotas.sql").read_text()
|
||||
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()
|
||||
|
||||
|
||||
class MigrationTest(unittest.TestCase):
|
||||
@@ -68,6 +69,10 @@ class MigrationTest(unittest.TestCase):
|
||||
):
|
||||
self.assertIn(fragment, ARENAS_SQL)
|
||||
|
||||
def test_provider_allocation_retains_arena_identity(self):
|
||||
self.assertIn("ALTER TABLE allocations", ALLOCATION_ARENAS_SQL)
|
||||
self.assertIn("ADD COLUMN arena_path TEXT", ALLOCATION_ARENAS_SQL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -175,7 +175,7 @@ func FindProviderAllocation(ctx context.Context, db *sql.DB, request domain.Allo
|
||||
}
|
||||
var allocation domain.Allocation
|
||||
var digest []byte
|
||||
err := db.QueryRowContext(ctx, SelectAllocationSQL, request.AllocationID).Scan(&allocation.AllocationID, &allocation.MatchID, &allocation.ServerID, &allocation.Region, &allocation.Build, &allocation.Protocol, &allocation.Transport, &allocation.AllocatedAt, &digest)
|
||||
err := db.QueryRowContext(ctx, SelectAllocationSQL, request.AllocationID).Scan(&allocation.AllocationID, &allocation.MatchID, &allocation.ServerID, &allocation.Region, &allocation.Build, &allocation.Protocol, &allocation.ArenaPath, &allocation.Transport, &allocation.AllocatedAt, &digest)
|
||||
if err == sql.ErrNoRows {
|
||||
return domain.Allocation{}, false, nil
|
||||
}
|
||||
@@ -183,7 +183,7 @@ func FindProviderAllocation(ctx context.Context, db *sql.DB, request domain.Allo
|
||||
return domain.Allocation{}, false, err
|
||||
}
|
||||
want := allocationRequestDigest(request)
|
||||
if !bytes.Equal(digest, want[:]) || allocation.MatchID != request.MatchID || allocation.Region != request.Region || allocation.Build != request.Build || allocation.Protocol != request.Protocol || allocation.Transport != request.Transport {
|
||||
if !bytes.Equal(digest, want[:]) || allocation.MatchID != request.MatchID || allocation.Region != request.Region || allocation.Build != request.Build || allocation.Protocol != request.Protocol || allocation.ArenaPath != request.ArenaPath || allocation.Transport != request.Transport {
|
||||
return domain.Allocation{}, false, domain.ErrConflict
|
||||
}
|
||||
allocation.State = domain.ServerAllocated
|
||||
|
||||
@@ -31,11 +31,11 @@ WHERE server_id = (
|
||||
RETURNING server_id`
|
||||
|
||||
const InsertAllocationSQL = `INSERT INTO allocations
|
||||
(allocation_id, match_id, server_id, region, build, protocol_version, transport, request_digest, state, allocated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'ALLOCATED', $9)`
|
||||
(allocation_id, match_id, server_id, region, build, protocol_version, arena_path, transport, request_digest, state, allocated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'ALLOCATED', $10)`
|
||||
|
||||
const SelectAllocationSQL = `SELECT allocation_id, match_id, server_id, region, build,
|
||||
protocol_version, transport, allocated_at, request_digest
|
||||
protocol_version, arena_path, transport, allocated_at, request_digest
|
||||
FROM allocations WHERE allocation_id = $1`
|
||||
|
||||
const ProviderServerClaimSQL = `UPDATE game_servers SET state = 'ALLOCATED', updated_at = $6
|
||||
@@ -63,7 +63,7 @@ func ClaimAllocation(ctx context.Context, db *sql.DB, request domain.AllocationR
|
||||
err := RunSerializable(ctx, db, DefaultSerializableAttempts, func(ctx context.Context, tx *sql.Tx) error {
|
||||
var prior domain.Allocation
|
||||
var priorDigest []byte
|
||||
err := tx.QueryRowContext(ctx, SelectAllocationSQL, request.AllocationID).Scan(&prior.AllocationID, &prior.MatchID, &prior.ServerID, &prior.Region, &prior.Build, &prior.Protocol, &prior.Transport, &prior.AllocatedAt, &priorDigest)
|
||||
err := tx.QueryRowContext(ctx, SelectAllocationSQL, request.AllocationID).Scan(&prior.AllocationID, &prior.MatchID, &prior.ServerID, &prior.Region, &prior.Build, &prior.Protocol, &prior.ArenaPath, &prior.Transport, &prior.AllocatedAt, &priorDigest)
|
||||
if err == nil {
|
||||
if !bytes.Equal(priorDigest, digest[:]) {
|
||||
return domain.ErrConflict
|
||||
@@ -85,8 +85,8 @@ func ClaimAllocation(ctx context.Context, db *sql.DB, request domain.AllocationR
|
||||
}
|
||||
return err
|
||||
}
|
||||
allocation = domain.Allocation{AllocationID: request.AllocationID, MatchID: request.MatchID, ServerID: serverID, Region: request.Region, Build: request.Build, Protocol: request.Protocol, Transport: request.Transport, State: domain.ServerAllocated, AllocatedAt: now}
|
||||
_, err = tx.ExecContext(ctx, InsertAllocationSQL, request.AllocationID, request.MatchID, serverID, request.Region, request.Build, request.Protocol, request.Transport, digest[:], now)
|
||||
allocation = domain.Allocation{AllocationID: request.AllocationID, MatchID: request.MatchID, ServerID: serverID, Region: request.Region, Build: request.Build, Protocol: request.Protocol, ArenaPath: request.ArenaPath, Transport: request.Transport, State: domain.ServerAllocated, AllocatedAt: now}
|
||||
_, err = tx.ExecContext(ctx, InsertAllocationSQL, request.AllocationID, request.MatchID, serverID, request.Region, request.Build, request.Protocol, request.ArenaPath, request.Transport, digest[:], now)
|
||||
return err
|
||||
})
|
||||
return allocation, err
|
||||
@@ -97,7 +97,7 @@ func ClaimAllocation(ctx context.Context, db *sql.DB, request domain.AllocationR
|
||||
// Agones has already selected the server; no client-facing assignment may use
|
||||
// the result until this exact tuple is durably recorded.
|
||||
func RecordProviderAllocation(ctx context.Context, db *sql.DB, allocation domain.Allocation, now time.Time) (domain.Allocation, error) {
|
||||
request := domain.AllocationRequest{AllocationID: allocation.AllocationID, MatchID: allocation.MatchID, Region: allocation.Region, Build: allocation.Build, Protocol: allocation.Protocol, Transport: allocation.Transport}
|
||||
request := domain.AllocationRequest{AllocationID: allocation.AllocationID, MatchID: allocation.MatchID, Region: allocation.Region, Build: allocation.Build, Protocol: allocation.Protocol, ArenaPath: allocation.ArenaPath, Transport: allocation.Transport}
|
||||
if !validAllocationInput(db, request, now) || allocation.State != domain.ServerAllocated || allocation.ServerID == "" {
|
||||
return domain.Allocation{}, domain.ErrAllocationInput
|
||||
}
|
||||
@@ -106,7 +106,7 @@ func RecordProviderAllocation(ctx context.Context, db *sql.DB, allocation domain
|
||||
err := RunSerializable(ctx, db, DefaultSerializableAttempts, func(ctx context.Context, tx *sql.Tx) error {
|
||||
var prior domain.Allocation
|
||||
var priorDigest []byte
|
||||
err := tx.QueryRowContext(ctx, SelectAllocationSQL, allocation.AllocationID).Scan(&prior.AllocationID, &prior.MatchID, &prior.ServerID, &prior.Region, &prior.Build, &prior.Protocol, &prior.Transport, &prior.AllocatedAt, &priorDigest)
|
||||
err := tx.QueryRowContext(ctx, SelectAllocationSQL, allocation.AllocationID).Scan(&prior.AllocationID, &prior.MatchID, &prior.ServerID, &prior.Region, &prior.Build, &prior.Protocol, &prior.ArenaPath, &prior.Transport, &prior.AllocatedAt, &priorDigest)
|
||||
if err == nil {
|
||||
if !bytes.Equal(priorDigest, digest[:]) || prior.ServerID != allocation.ServerID {
|
||||
return domain.ErrConflict
|
||||
@@ -133,7 +133,7 @@ func RecordProviderAllocation(ctx context.Context, db *sql.DB, allocation domain
|
||||
}
|
||||
recorded = allocation
|
||||
recorded.AllocatedAt = now
|
||||
_, err = tx.ExecContext(ctx, InsertAllocationSQL, allocation.AllocationID, allocation.MatchID, serverID, allocation.Region, allocation.Build, allocation.Protocol, allocation.Transport, digest[:], now)
|
||||
_, err = tx.ExecContext(ctx, InsertAllocationSQL, allocation.AllocationID, allocation.MatchID, serverID, allocation.Region, allocation.Build, allocation.Protocol, allocation.ArenaPath, allocation.Transport, digest[:], now)
|
||||
return err
|
||||
})
|
||||
return recorded, err
|
||||
|
||||
Reference in New Issue
Block a user