mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 00:14:00 +00:00
fix(multiplayer): fence client queue cancellation states
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -42,8 +43,12 @@ RETURNING ticket_id, player_id, playlist, state, client_build, protocol_version,
|
||||
QueueTicketCancelSQL = `UPDATE queue_tickets SET state = 'CANCELLED',
|
||||
revision = revision + 1, expires_at = $4
|
||||
WHERE ticket_id = $1 AND player_id = $2 AND revision = $3
|
||||
AND state NOT IN ('COMPLETED', 'CANCELLED', 'EXPIRED', 'FAILED')
|
||||
AND state IN ('QUEUED', 'PROPOSED')
|
||||
RETURNING ticket_id, player_id, playlist, state, client_build, protocol_version, enqueued_at, expires_at, revision, predicted_rtt`
|
||||
QueueMutationFailureSQL = `SELECT player_id, state, revision, expires_at
|
||||
FROM queue_tickets
|
||||
WHERE ticket_id = $1
|
||||
FOR UPDATE`
|
||||
QueueCooldownSelectSQL = `SELECT ends_at
|
||||
FROM penalties
|
||||
WHERE player_id = $1 AND playlist = $2
|
||||
@@ -294,7 +299,10 @@ func mutateQueueTicket(ctx context.Context, db *sql.DB, playerID, ticketID, idem
|
||||
var record queueTicketRecord
|
||||
var predictedRTT []byte
|
||||
if err := tx.QueryRowContext(ctx, mutationSQL, ticketID, playerID, expectedRevision, now).Scan(&record.TicketID, &record.PlayerID, &record.Playlist, &record.State, &record.ClientBuild, &record.ProtocolVersion, &record.EnqueuedAt, &record.ExpiresAt, &record.Revision, &predictedRTT); err != nil {
|
||||
return fmt.Errorf("queue mutation rejected: %w", err)
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("queue mutation rejected: %w", err)
|
||||
}
|
||||
return classifyQueueMutationFailure(ctx, tx, playerID, ticketID, expectedRevision, now)
|
||||
}
|
||||
if err := json.Unmarshal(predictedRTT, &record.PredictedRTT); err != nil {
|
||||
return fmt.Errorf("decode queue RTT: %w", err)
|
||||
@@ -315,6 +323,29 @@ func mutateQueueTicket(ctx context.Context, db *sql.DB, playerID, ticketID, idem
|
||||
return ticket, err
|
||||
}
|
||||
|
||||
func classifyQueueMutationFailure(ctx context.Context, tx *sql.Tx, playerID, ticketID string, expectedRevision uint64, now time.Time) error {
|
||||
var owner, state string
|
||||
var revision uint64
|
||||
var expiresAt time.Time
|
||||
err := tx.QueryRowContext(ctx, QueueMutationFailureSQL, ticketID).Scan(&owner, &state, &revision, &expiresAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.ErrTicketNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if owner != playerID {
|
||||
return domain.ErrNotTicketOwner
|
||||
}
|
||||
if (state == string(domain.Queued) || state == string(domain.Proposed)) && !now.Before(expiresAt) {
|
||||
return domain.ErrTicketExpired
|
||||
}
|
||||
if revision != expectedRevision {
|
||||
return domain.ErrStaleRevision
|
||||
}
|
||||
return fmt.Errorf("%w: %s in %s", domain.ErrConflict, "queue mutation", state)
|
||||
}
|
||||
|
||||
func queueTicketRecordFromDomain(ticket domain.QueueTicket) queueTicketRecord {
|
||||
return queueTicketRecord{TicketID: ticket.TicketID, PlayerID: ticket.PlayerID, ProposalID: ticket.ProposalID, 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}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user