mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 17:33:43 +00:00
fc2faf9723
Closes the last named item on task 8.28 (Health-reclaim): nothing
currently detects or cleans up a match stuck in
ALLOCATING/PROCESS_READY/ASSIGNMENT_READY forever because its server
crashed or was reclaimed by Agones as unhealthy before ever
registering -- players would wait indefinitely for a match that was
never coming.
The design question this was blocked on -- does an abandoned match
auto-requeue its players, or fail and make them re-queue -- isn't
actually open: task 8.50's own stated acceptance criterion already
answers it ("infrastructure-caused cases cannot penalise affected
players"). A server-side crash/reclaim is exactly that, not player
behaviour, so store.ExpireStalledAllocations fails the match but
requeues every participant's ticket to QUEUED with a fresh expiry
(matching the ordinary 30s queue window), releases their
match_participants row (participation_active = false, so they're
matchable again immediately), all inside one FOR UPDATE SKIP LOCKED
pass so a second maintenance replica continues past whatever a
concurrent one is already reclaiming.
Wired into cmd/maintenance alongside the existing season-rollover
sweep: --stalled-allocation-deadline (default 2m) and
--stalled-allocation-batch (default 100).
Covered by a SQL-fragment test and a real PostgreSQL integration test:
two matches (one genuinely stalled, one recent), confirming the
deadline boundary is respected (recent match untouched), both
stranded participants' tickets requeue with a refreshed expiry, the
match_participants row releases, and a second pass doesn't reprocess
an already-FAILED match. Verified clean across 5 runs, plus the full
integration and unit suites.
42 lines
1.3 KiB
Go
42 lines
1.3 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestExpireStalledAllocationsSQLFencesAndRequeuesWithoutPenalty(t *testing.T) {
|
|
for _, fragment := range []string{
|
|
"ALLOCATING", "PROCESS_READY", "ASSIGNMENT_READY",
|
|
"FOR UPDATE SKIP LOCKED",
|
|
"SET state = 'FAILED'",
|
|
"SET participation_active = FALSE",
|
|
"SET state = 'QUEUED'",
|
|
} {
|
|
if !strings.Contains(ExpireStalledAllocationsSQL, fragment) {
|
|
t.Fatalf("ExpireStalledAllocationsSQL missing fragment %q:\n%s", fragment, ExpireStalledAllocationsSQL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExpireStalledAllocationsRejectsInvalidArgumentsWithoutDatabase(t *testing.T) {
|
|
now := time.Unix(1000, 0).UTC()
|
|
if _, err := ExpireStalledAllocations(context.Background(), nil, now, time.Minute, 10); err == nil {
|
|
t.Fatal("nil database accepted")
|
|
}
|
|
if _, err := ExpireStalledAllocations(context.Background(), nil, time.Time{}, time.Minute, 10); err == nil {
|
|
t.Fatal("zero time accepted")
|
|
}
|
|
if _, err := ExpireStalledAllocations(context.Background(), nil, now, 0, 10); err == nil {
|
|
t.Fatal("non-positive deadline accepted")
|
|
}
|
|
if _, err := ExpireStalledAllocations(context.Background(), nil, now, time.Minute, 0); err == nil {
|
|
t.Fatal("zero limit accepted")
|
|
}
|
|
if _, err := ExpireStalledAllocations(context.Background(), nil, now, time.Minute, 1001); err == nil {
|
|
t.Fatal("oversized limit accepted")
|
|
}
|
|
}
|