mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
fix(multiplayer): recover requeued tickets
This commit is contained in:
@@ -474,7 +474,7 @@ func _set_websocket_status(status: String) -> void:
|
||||
websocket_status_changed.emit(status)
|
||||
if status == "CONNECTED":
|
||||
if not state.ticket_id.is_empty() and state.phase not in [MatchmakingState.CANCELLED, MatchmakingState.EXPIRED, MatchmakingState.FAILED, MatchmakingState.LIVE, MatchmakingState.COMPLETED]:
|
||||
var resource_id := state.proposal_id if not state.proposal_id.is_empty() else state.ticket_id
|
||||
var resource_id := state.proposal_id if state.has_open_proposal() else state.ticket_id
|
||||
if _operation.is_empty():
|
||||
_run_resync(resource_id)
|
||||
else:
|
||||
|
||||
@@ -41,7 +41,7 @@ func _process(delta: float) -> void:
|
||||
_recovery_poll_seconds += delta
|
||||
if _recovery_poll_seconds >= RECOVERY_POLL_SECONDS:
|
||||
_recovery_poll_seconds = 0.0
|
||||
var recovery_err := ControlPlaneClient.recover_proposal(ControlPlaneClient.state.proposal_id) if not ControlPlaneClient.state.proposal_id.is_empty() else ControlPlaneClient.recover_queue(ControlPlaneClient.state.ticket_id)
|
||||
var recovery_err := ControlPlaneClient.recover_proposal(ControlPlaneClient.state.proposal_id) if ControlPlaneClient.state.has_open_proposal() else ControlPlaneClient.recover_queue(ControlPlaneClient.state.ticket_id)
|
||||
if recovery_err != OK and recovery_err != ERR_BUSY:
|
||||
_on_local_error("State recovery unavailable: %s" % error_string(recovery_err))
|
||||
if ControlPlaneClient.state.phase == MatchmakingState.QUEUED and _heartbeat_seconds >= HEARTBEAT_SECONDS:
|
||||
|
||||
@@ -217,6 +217,10 @@ func can_cancel() -> bool:
|
||||
return phase == QUEUED or phase == PROPOSED or phase == ALLOCATING
|
||||
|
||||
|
||||
func has_open_proposal() -> bool:
|
||||
return not proposal_id.is_empty() and proposal_state == "OPEN"
|
||||
|
||||
|
||||
func waited_seconds(now_unix: int) -> int:
|
||||
if enqueued_at_unix <= 0:
|
||||
return 0
|
||||
|
||||
@@ -129,6 +129,15 @@ func test_proposal_projection_rejects_illegal_higher_revision_transitions() -> v
|
||||
assert_true(not declined.apply_proposal_update({"proposal_id": "proposal-declined", "revision": 3, "state": "ACCEPTED"}), "declined proposal cannot accept")
|
||||
|
||||
|
||||
func test_terminal_proposal_is_not_an_active_recovery_target() -> void:
|
||||
var state := MatchmakingState.new()
|
||||
state.begin_queue("ticket-terminal-proposal", "casual")
|
||||
assert_true(state.apply_proposal_update({"proposal_id": "proposal-terminal", "revision": 1, "state": "OPEN"}), "proposal opens")
|
||||
assert_true(state.has_open_proposal(), "open proposal is an active recovery target")
|
||||
assert_true(state.apply_proposal_update({"proposal_id": "proposal-terminal", "revision": 2, "state": "EXPIRED"}), "proposal expires")
|
||||
assert_true(not state.has_open_proposal(), "terminal proposal uses ticket recovery instead")
|
||||
|
||||
|
||||
func test_assignment_lifecycle_has_explicit_connecting_and_live_states() -> void:
|
||||
var state := MatchmakingState.new()
|
||||
state.begin_queue("ticket-1", "ranked")
|
||||
|
||||
@@ -1549,4 +1549,6 @@ Proposal updates now enforce the documented `OPEN → ACCEPTED/DECLINED/EXPIRED/
|
||||
|
||||
Proposal decline/expiry/cancellation now leaves a still-proposed ticket in `QUEUED`, matching the durable server requeue transaction; the proposal’s terminal message remains visible without making the ticket terminal. A cancelled ticket is never resurrected by a later proposal event, covered by adversarial cross-aggregate tests.
|
||||
|
||||
Recovery targeting now follows the same boundary: only an `OPEN` proposal is polled as a proposal; terminal proposal outcomes fall back to the ticket recovery endpoint. This prevents repeated reads of a finished proposal from starving recovery of the requeued ticket.
|
||||
|
||||
Ticket projections now validate playlist metadata on every update, rejecting unknown values before either phase or playlist state can mutate. An adversarial higher-revision update test covers this boundary.
|
||||
|
||||
Reference in New Issue
Block a user