test(multiplayer): fence conflicting result races

This commit is contained in:
Josh Creek
2026-09-01 15:44:47 +01:00
parent f3b56f70e3
commit 802e5fc96f
2 changed files with 65 additions and 1 deletions
+64
View File
@@ -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