feat(multiplayer): bind admissions to durable leases

This commit is contained in:
Josh Creek
2026-09-03 13:45:39 +01:00
parent 3e0022ce9c
commit aac81c89b6
9 changed files with 301 additions and 64 deletions
+21 -47
View File
@@ -4,6 +4,7 @@ const NetCodec = preload("res://scripts/net_codec.gd")
const ServerControlScript = preload("res://scripts/server_control.gd")
const AgonesSDKScript = preload("res://scripts/agones_sdk.gd")
const AssignmentState = preload("res://scripts/assignment_state.gd")
const ConnectionLeaseClientScript = preload("res://scripts/connection_lease_client.gd")
# Headless dedicated server entry point (task 1.6). Parses CLI args, hosts
# via NetworkManager, logs structured lines, and watches for physics-tick
@@ -32,9 +33,8 @@ var _watchdog_armed := false # skip the first _process(): engine startup sched
var _control: ServerControl = null
var _match_loop: ServerMatchLoop = null
var _agones = null
var _connection_leases = null
var _drain_requested := false
var _connection_reports_inflight: Dictionary = {}
var _connection_reports_complete: Dictionary = {}
func _ready() -> void:
@@ -105,6 +105,18 @@ func _ready() -> void:
get_tree().root.add_child.call_deferred(_agones)
if _agones.configure_from_environment():
_agones.start_health()
_connection_leases = ConnectionLeaseClientScript.new()
_connection_leases.name = "ConnectionLeases"
var lease_url := OS.get_environment("COSMIC_CLASH_CONTROL_PLANE_URL")
var lease_token := OS.get_environment("COSMIC_CLASH_WORKLOAD_TOKEN")
if _connection_leases.configure(lease_url, lease_token, String(config.get_value("match-id")), String(config.get_value("server-id"))):
_connection_leases.reconciliation_failed.connect(_on_connection_lease_reconciliation_failed)
get_tree().root.add_child.call_deferred(_connection_leases)
MatchNet.configure_connection_lease_callbacks(_connection_leases.claim, _connection_leases.record_disconnect)
else:
_connection_leases.queue_free()
_connection_leases = null
ServerLog.warn("connection_lease_backend_unavailable", {"reason": "invalid_or_missing_configuration"})
NetworkManager.client_connected.connect(_on_client_connected)
NetworkManager.client_disconnected.connect(_on_client_disconnected)
@@ -188,8 +200,6 @@ func _on_client_disconnected(peer_id: int) -> void:
func _on_player_joined(peer_id: int, player_name: String) -> void:
ServerLog.info("player_joined", {"peer_id": peer_id, "name": player_name, "roster": MatchNet.roster.size()})
if config != null and bool(config.get_value("allocated-mode")):
_report_player_connected(MatchNet.player_identity(peer_id))
func _on_player_left(peer_id: int) -> void:
@@ -209,52 +219,16 @@ func _on_initial_connect_ready() -> void:
ServerLog.info("initial_connect_window_started", {"match_id": String(config.get_value("match-id"))})
func _report_player_connected(player_id: String) -> void:
var base_url := OS.get_environment("COSMIC_CLASH_CONTROL_PLANE_URL").strip_edges().trim_suffix("/")
var workload_token := OS.get_environment("COSMIC_CLASH_WORKLOAD_TOKEN").strip_edges()
var server_id := String(config.get_value("server-id"))
var match_id := String(config.get_value("match-id"))
if not valid_connection_report_configuration(base_url, workload_token, match_id, server_id, player_id) or _connection_reports_inflight.has(player_id) or _connection_reports_complete.has(player_id):
return
_connection_reports_inflight[player_id] = true
var endpoint := "%s/v1/servers/%s/connect" % [base_url, server_id.uri_encode()]
# A player can join many matches. Scope the durable key to this match so a
# later valid report cannot conflict with an earlier match's stored digest.
var idempotency_key := "server-connect-" + (match_id + "\n" + player_id).sha256_text()
var payload := JSON.stringify({"player_id": player_id})
for attempt in range(5):
var request := HTTPRequest.new()
request.timeout = 5.0
add_child(request)
var start_error := request.request(endpoint, [
"Authorization: Bearer " + workload_token,
"Content-Type: application/json",
"Idempotency-Key: " + idempotency_key,
], HTTPClient.METHOD_POST, payload)
var response_code := 0
if start_error == OK:
var response: Array = await request.request_completed
response_code = int(response[1])
request.queue_free()
if response_code == 204:
_connection_reports_complete[player_id] = true
_connection_reports_inflight.erase(player_id)
ServerLog.debug("player_connection_recorded", {"player_id": player_id})
return
if response_code in [400, 401, 404, 409, 422]:
break
if attempt < 4 and is_inside_tree():
await get_tree().create_timer(1.0).timeout
_connection_reports_inflight.erase(player_id)
ServerLog.warn("player_connection_report_failed", {"player_id": player_id})
func _on_connection_lease_reconciliation_failed(reason: String) -> void:
# A durable/local divergence means this process can no longer prove that a
# future generation is globally current. Preserve the live match but close
# admission so it cannot mint additional ambiguous leases.
MatchNet.admissions_open = false
ServerLog.error("connection_lease_reconciliation_failed", {"reason": reason})
static func valid_connection_report_configuration(base_url: String, workload_token: String, match_id: String, server_id: String, player_id: String) -> bool:
if not (base_url.begins_with("http://") or base_url.begins_with("https://")) or base_url.contains("\n") or base_url.contains("\r") or base_url.contains("?") or base_url.contains("#"):
return false
if workload_token.is_empty() or workload_token.contains("\n") or workload_token.contains("\r"):
return false
return AssignmentState.is_valid_opaque_id(match_id) and AssignmentState.is_valid_opaque_id(server_id) and AssignmentState.is_valid_opaque_id(player_id)
return ConnectionLeaseClientScript.valid_configuration(base_url, workload_token, match_id, server_id) and AssignmentState.is_valid_opaque_id(player_id)
static func required_min_players(allocated: bool, roster_size: int, configured: int) -> int: