mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): fence recovered arena identity
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, migration 0009 retains it on provider allocations, domain/store/provider boundaries and recovery lookups recheck 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, domain/store/provider boundaries and recovery lookups recheck 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 recovery worker also rejects a provider-recovered allocation whose arena differs from the durable request before recording or binding it.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ func (w Worker) RunOnce(ctx context.Context) (bool, error) {
|
||||
|
||||
func validateRecoveredAllocation(request domain.AllocationRequest, result agones.AllocatedServer) error {
|
||||
allocation := result.Allocation
|
||||
if result.Endpoint == "" || allocation.State != domain.ServerAllocated || allocation.AllocationID != request.AllocationID || allocation.MatchID != request.MatchID || allocation.ServerID == "" || allocation.Region != request.Region || allocation.Build != request.Build || allocation.Protocol != request.Protocol || allocation.Transport != request.Transport {
|
||||
if result.Endpoint == "" || allocation.State != domain.ServerAllocated || allocation.AllocationID != request.AllocationID || allocation.MatchID != request.MatchID || allocation.ServerID == "" || allocation.Region != request.Region || allocation.Build != request.Build || allocation.Protocol != request.Protocol || allocation.Transport != request.Transport || allocation.ArenaPath != request.ArenaPath {
|
||||
return fmt.Errorf("recovered allocation does not match request")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -105,6 +105,18 @@ func TestWorkerRejectsRecoveredAllocationForDifferentCompatibility(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerRejectsRecoveredAllocationForDifferentArena(t *testing.T) {
|
||||
request := domain.AllocationRequest{AllocationID: "allocation-1", MatchID: "match-1", ArenaPath: "res://scenes/arena_01.tscn", Region: "EU", Build: "build-1", Protocol: 1, Transport: "enet"}
|
||||
claims := &matchClaimSpy{request: request, found: true}
|
||||
provider := &recoverableProviderSpy{recovered: agones.AllocatedServer{Allocation: domain.Allocation{AllocationID: request.AllocationID, MatchID: request.MatchID, ServerID: "server-1", ArenaPath: "res://scenes/arena_02.tscn", Region: request.Region, Build: request.Build, Protocol: request.Protocol, Transport: request.Transport, State: domain.ServerAllocated}, Endpoint: "127.0.0.1:31001"}, found: true}
|
||||
durable := &durableSpy{}
|
||||
worker := Worker{Claims: claims, Service: Service{Provider: provider, Durable: durable, Now: func() time.Time { return time.Unix(1_000, 0) }}, Now: func() time.Time { return time.Unix(1_000, 0) }}
|
||||
processed, err := worker.RunOnce(context.Background())
|
||||
if err == nil || !processed || durable.calls != 0 || claims.bound != (domain.Allocation{}) {
|
||||
t.Fatalf("processed=%t err=%v durable_calls=%d bound=%+v", processed, err, durable.calls, claims.bound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerDoesNothingWhenNoDurableMatchIsAvailable(t *testing.T) {
|
||||
claims := &matchClaimSpy{}
|
||||
worker := Worker{Claims: claims, Now: func() time.Time { return time.Unix(1_000, 0) }}
|
||||
|
||||
Reference in New Issue
Block a user