mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
test(multiplayer): fence conflicting result races
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -889,6 +890,69 @@ func TestPostgreSQLResultCompletionAndOutboxAreAtomicAndReplayable(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPostgreSQLConcurrentConflictingResultSubmissionsKeepOneReceipt
|
||||
// exercises the other side of the result race: retries with different payloads
|
||||
// must not let the winner's durable receipt be overwritten or create a second
|
||||
// completion event.
|
||||
func TestPostgreSQLConcurrentConflictingResultSubmissionsKeepOneReceipt(t *testing.T) {
|
||||
db := openIntegrationPostgres(t)
|
||||
applyIntegrationMigrations(t, db)
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
ctx := context.Background()
|
||||
for _, player := range []string{"result-conflict-a", "result-conflict-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 matches (match_id, playlist, state, region, protocol_version, server_id) VALUES ('result-conflict-match', 'casual', 'RESULT_PENDING', 'NA', 1, 'result-conflict-server')`); 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 ('result-conflict-ticket-a', 'result-conflict-a', 'casual', 'LIVE', 'build-1', 1, $1, $2), ('result-conflict-ticket-b', 'result-conflict-b', 'casual', 'LIVE', 'build-1', 1, $1, $2)`, now, now.Add(time.Minute)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO match_participants (match_id, player_id, ticket_id, slot, team) VALUES ('result-conflict-match', 'result-conflict-a', 'result-conflict-ticket-a', 0, 0), ('result-conflict-match', 'result-conflict-b', 'result-conflict-ticket-b', 1, 1)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
base := domain.MatchResult{MatchID: "result-conflict-match", ServerID: "result-conflict-server", IntegrityState: domain.IntegritySuppressed}
|
||||
results := []domain.MatchResult{
|
||||
{MatchID: base.MatchID, ServerID: base.ServerID, ResultNonce: "result-conflict-nonce-a-123456", Team0Score: 2, Team1Score: 1, IntegrityState: base.IntegrityState},
|
||||
{MatchID: base.MatchID, ServerID: base.ServerID, ResultNonce: "result-conflict-nonce-b-123456", Team0Score: 1, Team1Score: 2, IntegrityState: base.IntegrityState},
|
||||
}
|
||||
errs := make([]error, 2)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
for i := range results {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
digest := domain.ResultDigest(results[i])
|
||||
receipt := domain.ResultReceipt{ResultID: fmt.Sprintf("result-conflict-receipt-%d", i), MatchID: results[i].MatchID, ResultNonce: results[i].ResultNonce, PayloadDigest: digest, IntegrityState: results[i].IntegrityState, ReceivedAt: now}
|
||||
errs[i] = CompleteResult(ctx, db, receipt, results[i].ServerID, fmt.Sprintf("result-conflict-event-%d", i), []byte(fmt.Sprintf(`{"nonce":%q}`, results[i].ResultNonce)), now)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
wins := 0
|
||||
for _, err := range errs {
|
||||
if err == nil {
|
||||
wins++
|
||||
} else if !strings.Contains(err.Error(), "conflict") {
|
||||
t.Fatalf("non-conflict error in conflicting race: %v", err)
|
||||
}
|
||||
}
|
||||
if wins != 1 {
|
||||
t.Fatalf("successful conflicting submissions = %d, want exactly one; errors=%v", wins, errs)
|
||||
}
|
||||
var receipts, events int
|
||||
if err := db.QueryRow(`SELECT count(*) FROM result_receipts WHERE match_id = 'result-conflict-match'`).Scan(&receipts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.QueryRow(`SELECT count(*) FROM outbox WHERE aggregate_id = 'result-conflict-match' AND event_type = 'match_completed'`).Scan(&events); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if receipts != 1 || events != 1 {
|
||||
t.Fatalf("durable conflict race left receipts=%d events=%d, want one of each", receipts, events)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPostgreSQLConcurrentIdenticalResultSubmissionAppliesRatingsExactlyOnce
|
||||
// races real concurrent duplicate result submissions -- the scenario behind
|
||||
// task 8.25's "identical duplicates idempotent" claim, which every other
|
||||
|
||||
Reference in New Issue
Block a user