Skip to content

Overview

The arena service is the dedicated execution host for the adversarial test type’s head-to-head play: quick matches (pit two controllers, get a replay back immediately) and tournaments (run every pair in a field, streaming live per-match progress). Running those matches is CPU-bound, in-process wasm — fast but heavy — so it lives in its own service rather than the single-replica control-plane backend.

It is a deliberate data-plane peer, kept separate from the backend the same way the artifact service is: the backend owns the data (controller inputs, published tournaments, stored replays); the arena owns the execution. A console POSTs a match or tournament to the arena and streams a tournament’s live progress from it; arena reads (published tournaments + per-match replays) stay on the backend. The backend reports the arena’s public base URL (TCAB_ARENA_PUBLIC_URL) via GET /config, and the console fetches it for those run actions.

The arena holds no database and no disk. It fetches every controller input from the backend over HTTP — resolve a test-case version, a baseline’s references/<id>.wasm, a pushed run’s controller.wasm, and the pushed-controller listing — and persists a finished tournament and its per-match replays back to the backend. Only two controller kinds are resolvable in this topology: committed baselines and pushed-run controllers. A run-local controller (one resolved from a host’s own run output dir) has no home in a stateless service, so it is rejected with a 400; the desktop app, which runs the same engine in-process against its local worker, is the only place those resolve.

Because the in-flight tournament registry and its live progress channel are in-memory and per-pod, the arena runs as a single replica. Scale its throughput with its CPU and the concurrency cap, not the replica count.

The arena is the CPU-bound pod of the topology, so it bounds concurrent work hard: a semaphore (TCAB_ARENA_MAX_CONCURRENT, default 2) caps how many matches/tournaments run their wasm at once. At capacity it rejects with 503 (and a warn log) rather than queueing — a match holds one permit for its single blocking execution; a tournament holds one for its whole background drive. Its Kubernetes Deployment carries real CPU requests/limits to match.

The arena’s run endpoints (POST /matches, POST /tournaments, GET /matches/controllers, GET /tournaments/{id}/events) are unauthenticated behind the private-network boundary — faithful to the worker the console still posts them token-less to. It has no Kubernetes API access; it only talks HTTP to the backend.

The arena service is implemented as the test-cabinet-arena crate (crates/arena), an Axum server reusing the shared match_play engine (and, through it, the foray-host wasm sandbox). Its configuration is entirely environment variables, documented on its config.rs. It binds all interfaces by default (0.0.0.0:8791) because the console reaches it over the cluster network; the deployment fronts it with the same private-network boundary as the other services. It is deployed as a single-replica Deployment + Service + its own ServiceAccount (no API access, no PVC — it is stateless) — see Kubernetes: staging & prod.