test(multiplayer): verify allocator worker lifecycle

This commit is contained in:
Josh Creek
2026-09-01 15:34:51 +01:00
parent dad26a164c
commit 207ab47866
3 changed files with 150 additions and 1 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
container_name="cosmic-clash-allocator-integration"
database="cosmic_clash_test"
user="cosmic_clash_test"
password="cosmic_clash_test"
cleanup() {
docker rm -f "$container_name" >/dev/null 2>&1 || true
}
trap cleanup EXIT
cleanup
docker run --rm -d --name "$container_name" \
-e POSTGRES_DB="$database" \
-e POSTGRES_USER="$user" \
-e POSTGRES_PASSWORD="$password" \
-p 55436:5432 postgres:17-alpine >/dev/null
for attempt in $(seq 1 30); do
if docker exec "$container_name" pg_isready -U "$user" -d "$database" >/dev/null 2>&1; then
break
fi
if [ "$attempt" = 30 ]; then
echo "PostgreSQL did not become ready" >&2
exit 1
fi
sleep 1
done
cd "$repo_root/server"
COSMIC_CLASH_POSTGRES_DSN="postgres://${user}:${password}@127.0.0.1:55436/${database}?sslmode=disable" \
go test -tags integration ./allocator -count=1
@@ -0,0 +1,114 @@
//go:build integration
package allocator
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"github.com/cosmic-clash/cosmic-clash/server/agones"
"github.com/cosmic-clash/cosmic-clash/server/migrations"
"github.com/cosmic-clash/cosmic-clash/server/store"
_ "github.com/jackc/pgx/v5/stdlib"
)
func TestRealAllocatorWorkerReconcilesAgonesAllocationAndBindsMatch(t *testing.T) {
dsn := os.Getenv("COSMIC_CLASH_POSTGRES_DSN")
if dsn == "" {
t.Skip("COSMIC_CLASH_POSTGRES_DSN is not set")
}
db, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
ctx := context.Background()
if err := db.PingContext(ctx); err != nil {
t.Fatal(err)
}
if _, err := db.ExecContext(ctx, `DROP TABLE IF EXISTS schema_migrations, assignments, audit_events, outbox, result_receipts, ranked_season_rollovers, penalties, seasons, ratings, match_participants, matches, proposal_participants, proposals, allocations, game_servers, queue_tickets, idempotency_keys, sessions, identities CASCADE`); err != nil {
t.Fatal(err)
}
if err := migrations.Apply(ctx, db, filepath.Join("..", "migrations")); err != nil {
t.Fatal(err)
}
now := time.Now().UTC().Truncate(time.Microsecond)
for index, player := range []string{"allocator-worker-a", "allocator-worker-b"} {
if _, err := db.ExecContext(ctx, `INSERT INTO identities (player_id, steam_id) VALUES ($1, $1)`, player); err != nil {
t.Fatal(err)
}
if _, err := db.ExecContext(ctx, `INSERT INTO queue_tickets (ticket_id, player_id, playlist, state, client_build, protocol_version, enqueued_at, expires_at) VALUES ($1, $2, 'casual', 'ACCEPTED', 'build-1', 1, $3, $4)`, fmt.Sprintf("allocator-worker-ticket-%d", index), player, now, now.Add(time.Minute)); err != nil {
t.Fatal(err)
}
}
if _, err := db.ExecContext(ctx, `INSERT INTO matches (match_id, playlist, state, region, protocol_version) VALUES ('allocator-worker-match', 'casual', 'ALLOCATING', 'EU', 1)`); err != nil {
t.Fatal(err)
}
for index, player := range []string{"allocator-worker-a", "allocator-worker-b"} {
if _, err := db.ExecContext(ctx, `INSERT INTO match_participants (match_id, player_id, ticket_id, slot, team) VALUES ('allocator-worker-match', $1, $2, $3, $4)`, player, fmt.Sprintf("allocator-worker-ticket-%d", index), index, index); err != nil {
t.Fatal(err)
}
}
var allocationCalls int
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
_, _ = w.Write([]byte(`{"items":[{"metadata":{"name":"agones-ready-1","labels":{"cosmic-clash.io/region":"EU","cosmic-clash.io/build":"build-1","cosmic-clash.io/protocol":"1","cosmic-clash.io/transport":"enet"}},"status":{"state":"Ready"}}]}`))
return
}
if r.Method != http.MethodPost {
t.Fatalf("provider method = %s", r.Method)
}
allocationCalls++
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body["kind"] != "GameServerAllocation" {
t.Fatalf("provider body kind = %v", body["kind"])
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":{"state":"Allocated","gameServerName":"agones-ready-1","address":"127.0.0.1","ports":[{"name":"default","port":7777}]}}`))
}))
defer provider.Close()
agonesClient := agones.Client{BaseURL: provider.URL, Namespace: "games", HTTP: provider.Client()}
ready, err := agonesClient.ListReadyServers(ctx)
if err != nil || len(ready) != 1 {
t.Fatalf("ready projection = %+v, err=%v", ready, err)
}
if err := store.RegisterReadyServer(ctx, db, ready[0], now); err != nil {
t.Fatal(err)
}
worker := Worker{
Claims: store.AllocatingMatchClaims{DB: db, Transport: "enet"},
Service: Service{Provider: agonesClient, Durable: store.AllocationRegistry{DB: db}, Now: func() time.Time { return now }},
Now: func() time.Time { return now },
}
processed, err := worker.RunOnce(ctx)
if err != nil || !processed || allocationCalls != 1 {
t.Fatalf("worker processed=%t err=%v provider calls=%d", processed, err, allocationCalls)
}
var serverID, matchState, ticketState string
if err := db.QueryRowContext(ctx, `SELECT server_id, state FROM matches WHERE match_id = 'allocator-worker-match'`).Scan(&serverID, &matchState); err != nil {
t.Fatal(err)
}
if err := db.QueryRowContext(ctx, `SELECT state FROM queue_tickets WHERE ticket_id = 'allocator-worker-ticket-0'`).Scan(&ticketState); err != nil {
t.Fatal(err)
}
if serverID != "agones-ready-1" || matchState != "ALLOCATING" || ticketState != "ALLOCATING" {
t.Fatalf("durable lifecycle server=%q match=%q ticket=%q", serverID, matchState, ticketState)
}
var recorded int
if err := db.QueryRowContext(ctx, `SELECT count(*) FROM allocations WHERE allocation_id = 'allocation-allocator-worker-match' AND server_id = 'agones-ready-1' AND state = 'ALLOCATED'`).Scan(&recorded); err != nil || recorded != 1 {
t.Fatalf("recorded allocations=%d err=%v", recorded, err)
}
}