A Go-based matchmaking service with REST and WebSocket APIs, designed to pair players into matches based on level, with configurable timeouts and real-time notifications. Built with Docker and tested using k6 for load testing and Go unit tests.
The project is the result of collaboration between Grok AI and myself.
-
Go Server:
- Core Logic: Located in
internal/matchmaking/
:matchmaker.go
: Manages the matchmaking queue, processes players every 1s (ticker), and sends results via channels.matcher.go
: Implements the matching algorithm, pairing 10 players within a level range.config.go
: Defines timeouts (MAX_WAIT_TIME
,RESULT_TIMEOUT
) loaded from.env
.types.go
: DefinesPlayer
,Match
, andMatchResult
structs.
- Server: In
internal/server/server.go
:- REST API:
POST /player
,GET /match/{matchID}
,GET /player/match/{playerID}
. - WebSocket:
/ws?player_id={playerID}
for real-time results.
- REST API:
- Entry Point:
cmd/matchmaking/main.go
loads.env
and starts the server.
- Core Logic: Located in
-
Docker:
Dockerfile
: Builds the Go server (matchmaking-server:dev
).Dockerfile.test
: Runs Go unit tests (matchmaking-unit-tests:dev
).Dockerfile.k6
: Runs k6 load tests (matchmaking-k6-tests:dev
).docker-compose.yml
: Deploys the server.docker-compose.unit.yml
&docker-compose.k6.yml
: Run tests.
-
Testing:
- Unit Tests:
internal/matchmaking/matchmaker_test.go
verifies core logic. - Load Tests:
tests/load_test.js
uses k6 to test"timeout"
and"success"
scenarios.
- Unit Tests:
- Player Addition: Client sends
POST /player
withid
andlevel
. - Matchmaking:
Matchmaker
queues players and attempts matches every 1s:- Success: 10 players within level range →
{"match":{...},"reason":"success"}
. - Timeout: After
MAX_WAIT_TIME
(default 2s) →{"match":null,"reason":"timeout"}
.
- Success: 10 players within level range →
- Notification: WebSocket (
/ws
) delivers results; channels stay open until consumed or server stops. - Query: REST endpoints (
/match/{matchID}
,/player/match/{playerID}
) retrieve completed matches.
.env
: Shared between server and k6:MAX_WAIT_TIME=2s RESULT_TIMEOUT=5s
- Docker and Docker Compose installed.
- .env file in the root directory with MAX_WAIT_TIME and RESULT_TIMEOUT.
- Build and run Server:
make server
- Starts
matchmaking-server:dev
athttp://localhost:8080
.
- Run Unit Tests:
make unit-tests
- Run Load Tests:
make k6-tests
- Stop Server:
make stop
- Add Player:
curl -X POST http://localhost:8080/player -H "Content-Type: application/json" -d '{"id":"player1","level":10}'
- WebSocket (using wscat):
npm install -g wscat
wscat -c "ws://localhost:8080/ws?player_id=player1"
- Query Match:
curl http://localhost:8080/player/match/player1
I did brainstorming with Grok to get some ideas on a simple matchmaking scenario and a skeleton project. Most of subsequent discussions were around confirming ideas and proposing changes to the project.
Initial requirements:
- Implement gaming MMR in Go. The only metric is player level. For example, a match needs 10 players. They should be picked based solely on player level.
- There also needs to be expansion rules based on time elapsed so that larger level gap can be tolerated if players have been waiting for a set number of seconds.
- There is also a timeout so that the matchmaking fails if wait time exceeds a limit.
- "For a single node without distributed caching, how well does this scale?"
Impact: Introduced optimizations like segmented level buckets and sync.RWMutex to enhance single-node scalability without external caching. - "Does removed player not need notification?"
Fix: Added explicit timeout notifications via MatchResult, ensuring clients are informed when players are removed from the queue. - "Does tryMatch and FindMatch both locking on 'mu' cause a deadlock?"
- Provided test code that proves deadlock in FindMatch.
Bug Fix: Removed redundant RLock() in FindMatch, resolving a self-deadlock when MAX_WAIT_TIME triggered before WebSocket connections.
Further requirements to Grok (now that the main logic of matchmaking is working as expected):
- Add endpoints that can be load tested via k6.
- The matches created right now stay forever which is OK. I need k6 tests to be able to create matches and verify players who failed to join due to timeout, and verify successful matches too. Websocket may be needed so that k6 tests can be notified of player timeout. The Go service, making use of the core matchmaking logic implemented so far, should also provide endpoints for querying matches.
- "What if a match is found before the client connects via WebSocket?"
Design Change: Added ResultTimeout (5s) to keep channels open longer for late WebSocket connections, later simplified by removing it. - "modularise the project"
Impact: project restructure with Go code broken into logical packages. - "Somehow, the load test doesn't receive timeout reason even after adjusting MaxWaitTime?"
Bug Fix: Removed a goroutine in handleAddPlayer that consumed channel results prematurely, ensuring WebSocket delivery of "timeout". - "Is it possible to share config between load tests and Go server?"
Design Change: Implemented .env file to share MAX_WAIT_TIME and RESULT_TIMEOUT, ensuring consistency across server and k6 tests. - "The first two stages are defined the same. So how will the results be different?"
Test Redesign: Switched to k6 scenarios with distinct functions (testTimeout, testSuccess), fixing overlap and ensuring unique outcomes. - "Is 'no_result' redundant?"
Design Simplification: Removed "no_result" response, reducing outcomes to "timeout" and "success", simplifying the API and client expectations. - "If I comment out one scenario, it passes, but together one timeout fails?"
Bug Fix: Used shared-iterations executor for timeout scenario with iterations: 1, preventing interference from success scenario’s players.