Lobby server
This page documents the purpose and design of a lobby server in the Celte architecture, explains how the provided Go example implements a lobby, and describes how to write a generic lobby that interacts with the Master API.
High-level purpose
- The lobby is the public-facing HTTP service for your game. Clients talk to the lobby to join the game, authenticate, and request to be placed in a running game instance.
- The lobby is responsible for session lifecycle (creating/cleaning sessions on the master), matchmaking/spawner selection, and requesting the master to create or link clients to server nodes.
- The master manages cluster-level primitives (namespaces, nodes, redis keys, process orchestration) and exposes small JSON endpoints; the lobby implements game-specific policies and calls the master when cluster-level actions are required.
Why have a separate lobby?
- Separation of concerns: the lobby contains game-specific logic (auth, matchmaking rules, player inventories, persistence), while the master handles cluster primitives and system-level orchestration.
- Security: the lobby can validate and authenticate clients before allowing them to interact with the master or cluster.
- Scalability: you can scale lobbies independently from master nodes and server nodes, and run multiple lobby instances behind a load balancer.
What the example Go lobby does (summary)
- Listens on port 5002 and exposes two client endpoints:
- POST /client/connect — client asks to connect to the cluster; returns cluster host/port and a SessionId.
- POST /client/link — client asks the lobby to link it to a server node (the lobby chooses a spawner and calls the Master API to link the client to the node responsible for that spawner).
- Reads configuration from a YAML file (
~/.celte.yaml
by default) and flattens keys into aConfig
struct (keys used:CELTE_MASTER_HOST
,CELTE_MASTER_PORT
,CELTE_PULSAR_HOST
,CELTE_PULSAR_PORT
). - Generates a random
SessionId
per lobby run and calls the Master API to create a session and create initial root nodes (SetupGameSession
). - Uses helper functions that call the master endpoints:
server/create
to create nodes (used during session initialization)server/create_session
to create the Pulsar namespaceclient/link
to tell the master to connect a client to the appropriate noderedis/clear
andserver/cleanup_session
on shutdown
Files to inspect in the example
main.go
— startup, config loading, session initialization and graceful shutdown.config.go
— YAML parsing andConfig
struct (generates a random SessionId).router.go
— HTTP routes registration using Gorilla Mux.handlers.go
— request parsing and the two client endpoints (connect
,link
).celteMasterAPI.go
— small helpers that issue HTTP POSTs to the Master API (server/create
,client/link
,server/create_session
,redis/clear
,server/cleanup_session
).gameSession.go
— builds an initial set of nodes by calling the Master API.
API contract (example lobby endpoints)
- POST /client/connect
Purpose: initial handshake for a client. The lobby can authenticate the client, then respond with cluster connection information.
Request body (JSON)
{ "clientId": "<client runtime id>" }
Response (200)
{
"clusterHost": "<pulsar or cluster host>",
"clusterPort": "<pulsar or cluster port>",
"SessionId": "<session id generated by lobby>"
}
Notes: the example lobby returns CELTE_PULSAR_HOST
and port values read from the YAML config so clients know how to reach the messaging layer.
- POST /client/link
Purpose: request the lobby to assign the client to a game node (spawner). The lobby chooses the spawner following its configured policy and calls the master (client/link
) to actually connect the client to the node.
Request body (JSON)
{ "clientId": "<client runtime id>" }
Response (200)
{ "status": "ok", "message": "Client <id> linked to node <spawner>" }
Notes: the example uses a simple round-robin over availableSpawners := ["DefaultSpawner","Spawner1","Spawner2"]
. Replace this with a real strategy (least-loaded, region-aware, user affinities, party placement) as needed.
How the lobby interacts with the master
- Lifecycle actions (done once per lobby run or per game session):
- Call
POST /server/create_session
to create a Pulsar namespace for the session. This is implemented inCelteMasterAPICreateSession
. - Create root server nodes for the session by calling
POST /server/create
withparentId
entries (seeSetupGameSession
ingameSession.go
). Eachserver/create
call includesSessionId
added by the helper.
- Call
- Client linking:
- When a client wants to play the lobby chooses a
spawnerId
and calls the masterPOST /client/link
(viaCelteMasterAPILinkClient
). The master then routes the client to the active node responsible for that spawner (or fails if the lookup fails).
- When a client wants to play the lobby chooses a
- Teardown:
- On shutdown the lobby calls
POST /redis/clear
to remove session keys andPOST /server/cleanup_session
to cleanup processes and delete the Pulsar namespace.
- On shutdown the lobby calls
Designing a generic lobby (guidelines)
- Keep game logic in the lobby, orchestration in the master
- The lobby should implement authentication, matchmaking, party handling, and policy decisions. The lobby should not embed cluster orchestration logic — call the master for creating nodes, namespaces and making cross-process connections.
- Stateliness and sessionId
- The example generates a new SessionId per lobby process run. You can change this to maintain long-lived sessions (persistent world) or generate per-match session ids for ephemeral matches. Put
SessionId
in the payloads when calling the master so all cluster state is properly namespaced.
- Spawner selection strategies
- Round-robin: simple, demonstrated in the example.
- Least-loaded: query a telemetry endpoint or keep in-memory counters of players per spawner.
- Affinity and parties: keep party members together by storing party/spawner mappings.
- Region-aware: pick a spawner close to the player's region.
- Fault handling and retries
- Master calls can fail; wrap them with retries and circuit-breaker logic. The example prints responses and proceeds; production requires robust error handling.
- Health checks and liveness
- Provide health endpoints for load balancers. Monitor master responses and degrade gracefully when master is unreachable.
- Security
- Authenticate clients in the lobby (tokens, OAuth, session cookies). Only allow trusted lobbies or admin tooling to call master-level APIs if applicable.
- Observability and metrics
- Export metrics (requests, latencies, errors) and logs so you can observe matchmaking and master interaction success rates.
Implementation checklist (for a new lobby)
- Read YAML config for master host/port and messaging hosts (or use environment variables).
- Implement /client/connect to authenticate and return cluster connection info + SessionId.
- Decide and implement a spawner selection policy for /client/link.
- Implement master helpers (create_session, create node, link client, clear redis, cleanup_session).
- Call create_session and create initial nodes when starting a match or lobby run.
- Add graceful shutdown logic to call
redis/clear
andcleanup_session
to avoid stale resources. - Add retries and error handling around master calls.
- Add health checks, metrics, and logging.
Example: mapping the example code to these responsibilities
main.go
— startup, config loading, call toSetupGameSession
, graceful shutdown that callsCelteMasterAPIClearRedis
andCelteMasterAPICleanupSession
.config.go
— flattens YAML to aConfig
struct withCelteMasterHost
,CelteMasterPort
,CeltePulsarHost
,CeltePulsarPort
and generates a randomSessionId
.gameSession.go
— callsCelteMasterAPICreateSession
and thenCelteMasterAPICreateServer
for a set of root nodes.handlers.go
—connectClientToCelteCluster
returns cluster host/port and session id;linkClientToNode
selects a spawner and callsCelteMasterAPILinkClient
.celteMasterAPI.go
— shows small helpers that perform POST requests to the master API endpoints withSessionId
added to the JSON body.
Further suggestions
- Replace the static
availableSpawners
with a dynamic registry (in-memory or persistent) that can be updated as game features change. - Add authentication hooks in
connectClientToCelteCluster
and an ACL for master-level operations. - Implement a small integration test that starts the master (or a mock) and the lobby, then runs through connect->link flows.