mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-16 09:22:21 +00:00
fix(multiplayer): recover assignment handoff
This commit is contained in:
@@ -306,6 +306,10 @@ func TestPostgreSQLAllocationMatchClaimLeaseAndBindFence(t *testing.T) {
|
||||
if err := BindAllocatedMatch(ctx, db, allocation); err != nil {
|
||||
t.Fatalf("bind allocation: %v", err)
|
||||
}
|
||||
recoveredTicket, err := GetQueueTicket(ctx, db, "allocation-match-a", "allocation-match-ticket-0", now.Add(2*time.Second))
|
||||
if err != nil || recoveredTicket.MatchID != "allocation-match" {
|
||||
t.Fatalf("recovered ticket match=%q err=%v", recoveredTicket.MatchID, err)
|
||||
}
|
||||
var allocatingTickets int
|
||||
if err := db.QueryRowContext(ctx, `SELECT count(*) FROM queue_tickets WHERE ticket_id LIKE 'allocation-match-ticket-%' AND state = 'ALLOCATING'`).Scan(&allocatingTickets); err != nil || allocatingTickets != 2 {
|
||||
t.Fatalf("allocating tickets=%d err=%v", allocatingTickets, err)
|
||||
|
||||
@@ -21,10 +21,14 @@ ON CONFLICT (scope, idempotency_key) DO NOTHING`
|
||||
FROM idempotency_keys
|
||||
WHERE scope = $1 AND idempotency_key = $2
|
||||
FOR UPDATE`
|
||||
QueueTicketSelectSQL = `SELECT ticket_id, player_id, playlist, state, client_build,
|
||||
protocol_version, enqueued_at, expires_at, revision, predicted_rtt
|
||||
FROM queue_tickets
|
||||
WHERE ticket_id = $1 AND player_id = $2`
|
||||
QueueTicketSelectSQL = `SELECT q.ticket_id, q.player_id, q.playlist, q.state, q.client_build,
|
||||
q.protocol_version, q.enqueued_at, q.expires_at, q.revision, q.predicted_rtt,
|
||||
COALESCE((SELECT mp.match_id FROM match_participants mp
|
||||
WHERE mp.ticket_id = q.ticket_id AND mp.player_id = q.player_id
|
||||
AND mp.participation_active
|
||||
LIMIT 1), '')
|
||||
FROM queue_tickets q
|
||||
WHERE q.ticket_id = $1 AND q.player_id = $2`
|
||||
QueueTicketHeartbeatSQL = `UPDATE queue_tickets SET revision = revision + 1,
|
||||
expires_at = $4 + INTERVAL '30 seconds'
|
||||
WHERE ticket_id = $1 AND player_id = $2 AND revision = $3
|
||||
@@ -173,6 +177,7 @@ func CreateQueueTicket(ctx context.Context, db *sql.DB, ticketID, playerID, idem
|
||||
type queueTicketRecord struct {
|
||||
TicketID string `json:"ticket_id"`
|
||||
PlayerID string `json:"player_id"`
|
||||
MatchID string `json:"match_id,omitempty"`
|
||||
Playlist string `json:"playlist"`
|
||||
State string `json:"state"`
|
||||
ClientBuild string `json:"client_build"`
|
||||
@@ -230,7 +235,7 @@ func GetQueueTicket(ctx context.Context, db *sql.DB, playerID, ticketID string,
|
||||
}
|
||||
var record queueTicketRecord
|
||||
var predictedRTT []byte
|
||||
if err := db.QueryRowContext(ctx, QueueTicketSelectSQL, ticketID, playerID).Scan(&record.TicketID, &record.PlayerID, &record.Playlist, &record.State, &record.ClientBuild, &record.ProtocolVersion, &record.EnqueuedAt, &record.ExpiresAt, &record.Revision, &predictedRTT); err != nil {
|
||||
if err := db.QueryRowContext(ctx, QueueTicketSelectSQL, ticketID, playerID).Scan(&record.TicketID, &record.PlayerID, &record.Playlist, &record.State, &record.ClientBuild, &record.ProtocolVersion, &record.EnqueuedAt, &record.ExpiresAt, &record.Revision, &predictedRTT, &record.MatchID); err != nil {
|
||||
return domain.QueueTicket{}, err
|
||||
}
|
||||
if err := json.Unmarshal(predictedRTT, &record.PredictedRTT); err != nil {
|
||||
@@ -305,9 +310,9 @@ func mutateQueueTicket(ctx context.Context, db *sql.DB, playerID, ticketID, idem
|
||||
}
|
||||
|
||||
func queueTicketRecordFromDomain(ticket domain.QueueTicket) queueTicketRecord {
|
||||
return queueTicketRecord{TicketID: ticket.TicketID, PlayerID: ticket.PlayerID, Playlist: string(ticket.Playlist), State: string(ticket.State), ClientBuild: ticket.Candidate.ClientBuild, ProtocolVersion: ticket.Candidate.ProtocolVersion, EnqueuedAt: ticket.EnqueuedAt, ExpiresAt: ticket.ExpiresAt, Revision: ticket.Revision, PredictedRTT: ticket.Candidate.PredictedRTT}
|
||||
return queueTicketRecord{TicketID: ticket.TicketID, PlayerID: ticket.PlayerID, MatchID: ticket.MatchID, Playlist: string(ticket.Playlist), State: string(ticket.State), ClientBuild: ticket.Candidate.ClientBuild, ProtocolVersion: ticket.Candidate.ProtocolVersion, EnqueuedAt: ticket.EnqueuedAt, ExpiresAt: ticket.ExpiresAt, Revision: ticket.Revision, PredictedRTT: ticket.Candidate.PredictedRTT}
|
||||
}
|
||||
func queueTicketRecordToDomain(record queueTicketRecord) domain.QueueTicket {
|
||||
candidate := domain.Candidate{TicketID: record.TicketID, PlayerID: record.PlayerID, Playlist: domain.Playlist(record.Playlist), ClientBuild: record.ClientBuild, ProtocolVersion: record.ProtocolVersion, EnqueuedAt: record.EnqueuedAt, PredictedRTT: record.PredictedRTT}
|
||||
return domain.QueueTicket{TicketID: record.TicketID, PlayerID: record.PlayerID, Candidate: candidate, Playlist: domain.Playlist(record.Playlist), State: domain.State(record.State), Revision: record.Revision, EnqueuedAt: record.EnqueuedAt, ExpiresAt: record.ExpiresAt}
|
||||
return domain.QueueTicket{TicketID: record.TicketID, PlayerID: record.PlayerID, MatchID: record.MatchID, Candidate: candidate, Playlist: domain.Playlist(record.Playlist), State: domain.State(record.State), Revision: record.Revision, EnqueuedAt: record.EnqueuedAt, ExpiresAt: record.ExpiresAt}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ func TestQueueSQLUsesDurableIdempotencyAndOwnerScopedRecovery(t *testing.T) {
|
||||
for query, fragments := range map[string][]string{
|
||||
QueueIdempotencyInsertSQL: {"idempotency_keys", "ON CONFLICT (scope, idempotency_key) DO NOTHING", "payload_digest"},
|
||||
QueueIdempotencySelectSQL: {"scope = $1", "idempotency_key = $2", "FOR UPDATE"},
|
||||
QueueTicketSelectSQL: {"ticket_id = $1", "player_id = $2"},
|
||||
QueueTicketSelectSQL: {"q.ticket_id = $1", "q.player_id = $2", "match_participants", "participation_active"},
|
||||
QueueTicketInsertSQL: {"player_id", "playlist", "client_build", "protocol_version"},
|
||||
QueueTicketHeartbeatSQL: {"player_id = $2", "revision = $3", "expires_at > $4", "RETURNING"},
|
||||
QueueTicketCancelSQL: {"player_id = $2", "revision = $3", "state NOT IN", "RETURNING"},
|
||||
@@ -27,6 +27,16 @@ func TestQueueSQLUsesDurableIdempotencyAndOwnerScopedRecovery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueTicketRecordPreservesRecoveredMatchIdentity(t *testing.T) {
|
||||
ticket := queueTicketRecordToDomain(queueTicketRecord{TicketID: "ticket-1", PlayerID: "player-1", MatchID: "match-1", Playlist: string(domain.Casual), State: string(domain.AssignmentReady)})
|
||||
if ticket.MatchID != "match-1" {
|
||||
t.Fatalf("recovered match ID = %q", ticket.MatchID)
|
||||
}
|
||||
if got := queueTicketRecordFromDomain(ticket).MatchID; got != "match-1" {
|
||||
t.Fatalf("stored match ID = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRankedParticipantsRejectsNonSixPlayerLookupsWithoutDatabase(t *testing.T) {
|
||||
if _, err := LoadRankedParticipants(nil, nil, []string{"player-1"}); err == nil {
|
||||
t.Fatal("partial ranked identity lookup was accepted")
|
||||
|
||||
Reference in New Issue
Block a user