mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix: retry queue creation idempotently
This commit is contained in:
@@ -17,6 +17,7 @@ var ranked_profile: RankedProfileState
|
||||
|
||||
var _request: HTTPRequest
|
||||
var _operation := ""
|
||||
var _last_queue_create: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -45,12 +46,31 @@ func queue_create(ticket_id: String, playlist: String, client_build: String, pro
|
||||
return ERR_INVALID_PARAMETER
|
||||
if not state.begin_queue(ticket_id, playlist):
|
||||
return ERR_INVALID_PARAMETER
|
||||
var err := _start_request("queue_create", HTTPClient.METHOD_POST, "/v1/queue", {"ticket_id": ticket_id, "playlist": playlist, "client_build": client_build, "protocol_version": protocol_version}, _idempotency_key("queue"))
|
||||
var key := _idempotency_key("queue")
|
||||
_last_queue_create = {"ticket_id": ticket_id, "playlist": playlist, "client_build": client_build, "protocol_version": protocol_version, "key": key}
|
||||
var err := _start_request("queue_create", HTTPClient.METHOD_POST, "/v1/queue", {"ticket_id": ticket_id, "playlist": playlist, "client_build": client_build, "protocol_version": protocol_version}, key)
|
||||
if err != OK:
|
||||
state.fail("Could not start matchmaking: %s" % error_string(err))
|
||||
return err
|
||||
|
||||
|
||||
func retry_queue_create() -> Error:
|
||||
if _last_queue_create.is_empty() or not _last_queue_create.has("ticket_id"):
|
||||
return ERR_INVALID_DATA
|
||||
var ticket_id := String(_last_queue_create["ticket_id"])
|
||||
var playlist := String(_last_queue_create["playlist"])
|
||||
if not state.begin_queue(ticket_id, playlist):
|
||||
return ERR_INVALID_PARAMETER
|
||||
var err := _start_request("queue_create", HTTPClient.METHOD_POST, "/v1/queue", {"ticket_id": ticket_id, "playlist": playlist, "client_build": String(_last_queue_create["client_build"]), "protocol_version": int(_last_queue_create["protocol_version"])}, String(_last_queue_create["key"]))
|
||||
if err != OK:
|
||||
state.fail("Could not retry matchmaking: %s" % error_string(err))
|
||||
return err
|
||||
|
||||
|
||||
func can_retry_queue_create() -> bool:
|
||||
return not _last_queue_create.is_empty() and state.phase == MatchmakingState.FAILED and String(_last_queue_create.get("ticket_id", "")) == state.ticket_id
|
||||
|
||||
|
||||
func recover_queue(ticket_id: String) -> Error:
|
||||
if ticket_id.is_empty():
|
||||
return ERR_INVALID_PARAMETER
|
||||
|
||||
@@ -52,6 +52,11 @@ func _process(delta: float) -> void:
|
||||
|
||||
|
||||
func _on_queue_pressed() -> void:
|
||||
if ControlPlaneClient.can_retry_queue_create():
|
||||
var retry_err := ControlPlaneClient.retry_queue_create()
|
||||
if retry_err != OK:
|
||||
_on_local_error("Could not retry matchmaking: %s" % error_string(retry_err))
|
||||
return
|
||||
if not _can_start_new_search(ControlPlaneClient.state.phase):
|
||||
return
|
||||
_elapsed_seconds = 0.0
|
||||
@@ -166,7 +171,8 @@ func _render(snapshot: Dictionary) -> void:
|
||||
cancel_button.visible = ControlPlaneClient.state.can_cancel()
|
||||
accept_button.visible = phase == MatchmakingState.PROPOSED
|
||||
decline_button.visible = phase == MatchmakingState.PROPOSED
|
||||
queue_button.disabled = not _can_start_new_search(phase)
|
||||
queue_button.disabled = not (_can_start_new_search(phase) or ControlPlaneClient.can_retry_queue_create())
|
||||
queue_button.text = "Retry Search" if ControlPlaneClient.can_retry_queue_create() else "Search"
|
||||
|
||||
|
||||
static func _is_terminal(phase: String) -> bool:
|
||||
|
||||
+1
-1
@@ -1227,7 +1227,7 @@ the local/CI/community transport, not a silent production fallback.
|
||||
| # | Task | Acceptance |
|
||||
|---|---|---|
|
||||
| 8.39 `[D:8.3,8.14,8.17]` | **IN PROGRESS.** `MatchmakingState` now projects queue → proposal → allocation/process-ready/assignment-ready/connect/live plus terminal failure states; autoload `ControlPlaneClient` provides authenticated queue create/recovery/heartbeat/cancel and proposal response requests with idempotency/revision headers; `matchmaking.tscn`/`matchmaking.gd` expose the state and authoritative actions from the main menu; authenticated proposal recovery now reconciles missed proposal events and expires them at read time | `test_matchmaking_state.gd`, `test_control_plane_client.gd`, `test_matchmaking_ui.gd` and `TestProposalRecoveryIsParticipantScopedAndExpiresAtReadBoundary` reject stale/gapped/conflicting updates, validate endpoint/token/payload normalization, preserve idempotent duplicates, and guarantee visible phase/terminal copy; server-pushed allocation events, wait/latency explanations and Godot runtime verification remain |
|
||||
| 8.40 `[D:8.3,8.14]` | **IN PROGRESS.** Pure Go revisioned replica reducer rejects gaps for REST resync, makes duplicate/out-of-order events idempotent, and resumes from the authoritative snapshot revision; authenticated queue-ticket recovery now has an owner-checked REST read; Godot client projection persists non-secret ticket/proposal state and forces authoritative recovery after restart | `server/domain/sync.go`, `server/api/service.go`, `matchmaking_state.gd` and `control_plane_client.gd` cover gap, snapshot, replay, same-revision conflict, owner-only ticket recovery, expired-ticket terminal handling and malformed restart snapshots; authenticated WebSocket transport and duplicate-ticket integration remain |
|
||||
| 8.40 `[D:8.3,8.14]` | **IN PROGRESS.** Pure Go revisioned replica reducer rejects gaps for REST resync, makes duplicate/out-of-order events idempotent, and resumes from the authoritative snapshot revision; authenticated queue-ticket recovery now has an owner-checked REST read; Godot client projection persists non-secret ticket/proposal state, forces authoritative recovery after restart, and can replay a lost queue-create response with the original ticket/idempotency key | `server/domain/sync.go`, `server/api/service.go`, `matchmaking_state.gd` and `control_plane_client.gd` cover gap, snapshot, replay, same-revision conflict, owner-only ticket recovery, expired-ticket terminal handling, malformed restart snapshots and duplicate-create retry identity; authenticated WebSocket transport and live Godot verification remain |
|
||||
| 8.41 `[D:7.8,8.9,8.31,8.40]` | After assignment-ready, install SDR relay ticket before connect and send match-scoped join authorisation in `hello`; retain ENet assignments locally | Production connects/reconnects/fences old generation through SDR, never before assignment-ready; allocated/direct ENet and community flows remain compatible |
|
||||
| 8.42 `[D:8.22,8.23,8.24,8.40]` | **IN PROGRESS.** `RankedProfileState` and `ControlPlaneClient.fetch_ranked_profile()` expose the backend-authoritative rating/RD/volatility/games/tier/provisional/season view; matchmaking UI displays provisional/tier status without client-side rating math | `test_control_plane_client.gd` validates profile shape, numeric safety and provisional display; ranked profile fetch/display, committed revision after reconnect, abandon status and season countdown remain dependent on live auth/backend events and Godot runtime verification |
|
||||
| 8.43 `[D:8.39,8.40,8.41]` | **IN PROGRESS.** Matchmaking client now distinguishes expired queue recovery, session expiry, missing records and retryable control-plane outages; terminal messages remain visible and active searches are not falsely failed on transient errors | `MatchmakingState` and `ControlPlaneClient` tests cover explicit expiry and the existing terminal/retry-safe state paths; decline, version mismatch, regional outage retry UI, failed reconnect, duplicate-action recovery and live Godot verification remain |
|
||||
|
||||
Reference in New Issue
Block a user