feat(multiplayer): persist connection generation leases

This commit is contained in:
Josh Creek
2026-09-03 13:38:13 +01:00
parent 2e9da3032c
commit 3e0022ce9c
10 changed files with 286 additions and 80 deletions
+19 -4
View File
@@ -606,13 +606,13 @@ func TestPostgreSQLConnectionReceiptsStartCompleteRelaxedCasualRoster(t *testing
}
}
binding := domain.WorkloadBinding{AllocationID: "connect-allocation", MatchID: "connect-match", ServerID: "connect-server"}
if err := RecordPlayerConnected(ctx, db, binding, "connect-player-0", "connect-receipt-key-0000", now); err != nil {
if generation, err := ClaimPlayerConnection(ctx, db, binding, "connect-player-0", 0, "connect-receipt-key-0000", now); err != nil || generation != 1 {
t.Fatalf("first receipt: %v", err)
}
if err := RecordPlayerConnected(ctx, db, domain.WorkloadBinding{AllocationID: "forged-allocation", MatchID: "connect-match", ServerID: "connect-server"}, "connect-player-1", "connect-receipt-key-forged", now); !errors.Is(err, domain.ErrConflict) {
if _, err := ClaimPlayerConnection(ctx, db, domain.WorkloadBinding{AllocationID: "forged-allocation", MatchID: "connect-match", ServerID: "connect-server"}, "connect-player-1", 0, "connect-receipt-key-forged", now); !errors.Is(err, domain.ErrConflict) {
t.Fatalf("forged binding err=%v, want conflict", err)
}
if err := RecordPlayerConnected(ctx, db, binding, "connect-player-1", "connect-receipt-key-0001", now); err != nil {
if generation, err := ClaimPlayerConnection(ctx, db, binding, "connect-player-1", 0, "connect-receipt-key-0001", now); err != nil || generation != 1 {
t.Fatalf("second receipt: %v", err)
}
reconciled, err := ReconcileInitialConnect(ctx, db, now.Add(time.Second), 10)
@@ -629,9 +629,24 @@ func TestPostgreSQLConnectionReceiptsStartCompleteRelaxedCasualRoster(t *testing
}
// A lost 204 can be retried after assignment expiry because the exact
// durable receipt is replayed before checking the now-expired assignment.
if err := RecordPlayerConnected(ctx, db, binding, "connect-player-0", "connect-receipt-key-0000", now.Add(2*time.Minute)); err != nil {
if generation, err := ClaimPlayerConnection(ctx, db, binding, "connect-player-0", 0, "connect-receipt-key-0000", now.Add(2*time.Minute)); err != nil || generation != 1 {
t.Fatalf("durable receipt replay: %v", err)
}
if _, err := ClaimPlayerConnection(ctx, db, binding, "connect-player-0", 1, "connect-active-duplicate", now.Add(2*time.Second)); !errors.Is(err, domain.ErrConflict) {
t.Fatalf("active duplicate err=%v, want conflict", err)
}
if err := RecordPlayerDisconnected(ctx, db, binding, "connect-player-0", 1, "disconnect-receipt-0000", now.Add(3*time.Second)); err != nil {
t.Fatalf("disconnect receipt: %v", err)
}
if _, err := ClaimPlayerConnection(ctx, db, binding, "connect-player-0", 0, "connect-receipt-key-0000", now.Add(4*time.Second)); !errors.Is(err, domain.ErrConflict) {
t.Fatalf("stale connect replay err=%v, want conflict", err)
}
if generation, err := ClaimPlayerConnection(ctx, db, binding, "connect-player-0", 1, "reconnect-receipt-0000", now.Add(63*time.Second)); err != nil || generation != 2 {
t.Fatalf("grace-boundary reconnect generation=%d err=%v", generation, err)
}
if err := RecordPlayerDisconnected(ctx, db, binding, "connect-player-0", 1, "stale-disconnect-0000", now.Add(64*time.Second)); !errors.Is(err, domain.ErrConflict) {
t.Fatalf("stale disconnect err=%v, want conflict", err)
}
}
func TestPostgreSQLProposalClaimAndResponseAreAtomic(t *testing.T) {