mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-16 14:22:03 +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)
|
websocket_status_changed.emit(status)
|
||||||
if status == "CONNECTED":
|
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]:
|
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():
|
if _operation.is_empty():
|
||||||
_run_resync(resource_id)
|
_run_resync(resource_id)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func _process(delta: float) -> void:
|
|||||||
_recovery_poll_seconds += delta
|
_recovery_poll_seconds += delta
|
||||||
if _recovery_poll_seconds >= RECOVERY_POLL_SECONDS:
|
if _recovery_poll_seconds >= RECOVERY_POLL_SECONDS:
|
||||||
_recovery_poll_seconds = 0.0
|
_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:
|
if recovery_err != OK and recovery_err != ERR_BUSY:
|
||||||
_on_local_error("State recovery unavailable: %s" % error_string(recovery_err))
|
_on_local_error("State recovery unavailable: %s" % error_string(recovery_err))
|
||||||
if ControlPlaneClient.state.phase == MatchmakingState.QUEUED and _heartbeat_seconds >= HEARTBEAT_SECONDS:
|
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
|
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:
|
func waited_seconds(now_unix: int) -> int:
|
||||||
if enqueued_at_unix <= 0:
|
if enqueued_at_unix <= 0:
|
||||||
return 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")
|
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:
|
func test_assignment_lifecycle_has_explicit_connecting_and_live_states() -> void:
|
||||||
var state := MatchmakingState.new()
|
var state := MatchmakingState.new()
|
||||||
state.begin_queue("ticket-1", "ranked")
|
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.
|
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.
|
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