mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
feat(multiplayer): add idempotent client mutation retries
This commit is contained in:
@@ -27,6 +27,8 @@ var assignment: AssignmentState
|
||||
var _request: HTTPRequest
|
||||
var _operation := ""
|
||||
var _last_queue_create: Dictionary = {}
|
||||
var _last_mutation: Dictionary = {}
|
||||
var _last_mutation_retryable := false
|
||||
var _websocket: WebSocketPeer
|
||||
var _websocket_status := "DISCONNECTED"
|
||||
var _websocket_retry_seconds := 0.0
|
||||
@@ -159,6 +161,17 @@ 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 retry_last_mutation() -> Error:
|
||||
if not can_retry_last_mutation():
|
||||
return ERR_INVALID_DATA
|
||||
var request := _last_mutation.duplicate(true)
|
||||
return _start_request(String(request["operation"]), int(request["method"]), String(request["path"]), request["payload"], String(request["key"]), int(request["expected_revision"]))
|
||||
|
||||
|
||||
func can_retry_last_mutation() -> bool:
|
||||
return _last_mutation_retryable and not _last_mutation.is_empty() and _operation.is_empty() and not auth_expired and is_valid_access_token(access_token)
|
||||
|
||||
|
||||
func recover_queue(ticket_id: String) -> Error:
|
||||
if ticket_id.is_empty():
|
||||
return ERR_INVALID_PARAMETER
|
||||
@@ -256,6 +269,10 @@ static func is_valid_access_token(token: String) -> bool:
|
||||
return separator > 0 and separator < token.length() - 1 and token.length() <= 4096 and not token.contains("\r") and not token.contains("\n")
|
||||
|
||||
|
||||
static func is_retryable_mutation_response(response_code: int) -> bool:
|
||||
return response_code == 0 or response_code == HTTPClient.RESPONSE_REQUEST_TIMEOUT or response_code == HTTPClient.RESPONSE_TOO_MANY_REQUESTS or response_code >= 500
|
||||
|
||||
|
||||
static func normalize_ticket(payload: Dictionary) -> Dictionary:
|
||||
var result := payload.duplicate(true)
|
||||
if result.has("expires_at") and result["expires_at"] is String:
|
||||
@@ -281,6 +298,9 @@ func _start_request(operation: String, method: HTTPClient.Method, path: String,
|
||||
if err != OK:
|
||||
_operation = ""
|
||||
return err
|
||||
if not idempotency_key.is_empty():
|
||||
_last_mutation = {"operation": operation, "method": method, "path": path, "payload": payload.duplicate(true), "key": idempotency_key, "expected_revision": expected_revision}
|
||||
_last_mutation_retryable = false
|
||||
return OK
|
||||
|
||||
|
||||
@@ -288,6 +308,7 @@ func _on_request_completed(result: HTTPRequest.Result, response_code: int, _head
|
||||
var operation := _operation
|
||||
_operation = ""
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
_last_mutation_retryable = _last_mutation.get("operation", "") == operation
|
||||
if operation == "ranked_profile":
|
||||
ranked_profile.set_error("Ranked profile request failed")
|
||||
elif operation == "queue_create" or operation == "queue_recover" or operation == "proposal_recover":
|
||||
@@ -298,6 +319,7 @@ func _on_request_completed(result: HTTPRequest.Result, response_code: int, _head
|
||||
return
|
||||
var parsed = JSON.parse_string(body.get_string_from_utf8())
|
||||
if not parsed is Dictionary:
|
||||
_last_mutation_retryable = _last_mutation.get("operation", "") == operation
|
||||
if operation == "ranked_profile":
|
||||
ranked_profile.set_error("Ranked profile returned invalid JSON")
|
||||
elif operation == "queue_create" or operation == "queue_recover" or operation == "proposal_recover":
|
||||
@@ -307,6 +329,7 @@ func _on_request_completed(result: HTTPRequest.Result, response_code: int, _head
|
||||
request_failed.emit(operation, response_code, "invalid JSON")
|
||||
return
|
||||
if response_code < 200 or response_code >= 300:
|
||||
_last_mutation_retryable = _last_mutation.get("operation", "") == operation and is_retryable_mutation_response(response_code)
|
||||
var detail := String(parsed.get("error", "request rejected"))
|
||||
var recover_proposal_after_conflict := response_code == HTTPClient.RESPONSE_CONFLICT and (operation == "proposal_accept" or operation == "proposal_decline") and not state.proposal_id.is_empty()
|
||||
if response_code == HTTPClient.RESPONSE_UNAUTHORIZED:
|
||||
@@ -334,6 +357,7 @@ func _on_request_completed(result: HTTPRequest.Result, response_code: int, _head
|
||||
call_deferred("_run_pending_resync")
|
||||
return
|
||||
var payload: Dictionary = parsed
|
||||
_last_mutation_retryable = false
|
||||
if operation == "steam_session":
|
||||
var returned_token := String(payload.get("access_token", ""))
|
||||
var returned_player_id := String(payload.get("player_id", ""))
|
||||
|
||||
Reference in New Issue
Block a user