Multiple services & topology: one process, a whole service map
services[] doesn't have to hold just one. Real deployments often mix "an HTTP site farm + WebSocket push + a TCP gateway + UDP heartbeats" — all sharing one engine, one set of script capabilities and one graceful shutdown.
Overview
11.1 Intro
The naive way to deploy multiple services is "one process per kind" — the cost is process sprawl, chaotic port planning and scattered logs. XServer's topology model is multiple servers in one process: one xs.json describes every endpoint, one process hosts every service — sharing the engine thread pool, the script runtime capabilities and a single graceful shutdown. The production home repository runs exactly this shape: one XServer process, 14 vhosts, http and static sites mixed.
11.2 One process, many protocols
{
"engine": { "workers": 8 },
"services": [
{ "class": "http", "name": "web", "port": 80, "tls": true, "port_tls": 443,
"host_default": { "name": "app", "path": "wwwroot", "devlang": "c", "devfile": "main.c" } },
{ "class": "ws", "name": "push", "port": 8081,
"host_default": { "name": "app", "devlang": "c", "devfile": "push_main.c" } },
{ "class": "tcp", "name": "gw", "port": 9000, "tls": true, "port_tls": 9443,
"devlang": "c", "devfile": "gw_main.c" },
{ "class": "udp", "name": "beat", "port": 9010,
"devlang": "c", "devfile": "beat_main.c" }
]
}
What just happened: four services, four scripts; at assembly every candidate must compile before any bind happens — a single compile failure refuses the whole startup (the same discipline as hot reload's "compile before swap"). Each service swaps generations independently (reload publishes per server) without affecting the others.
11.3 engine.workers
- The one process-level knob: 0–1024, 0 = xrt default (12 on this machine in the demo)
- Every server's callbacks share this one pool — HTTP requests, WS messages, TCP data and UDP datagrams all run on the workers
- Immutable across hot reload: a reload intent changing workers is rejected (thread count is a process-level fact)
- Capacity estimate: callbacks are short event handlers; workers ≈ CPU cores is a sane start; long blocking work belongs on its own thread (chapter 41), not in bigger workers
11.4 Runtime topology queries
/* enumerate all services from the push service's script */
static bool PrintTopo(XS_ServerInfo* pServer, void* pUserData)
{
printf("- %s (%s) :%u%s\n", pServer->Name, pServer->Class,
pServer->Port, pServer->TLS ? " +tls" : "");
return true;
}
xsEnumServers(PrintTopo, NULL);
/* take a retained lease by name (must Release) */
XS_ServerInfo* pWeb = xsServerFind("web");
if ( pWeb != NULL ) {
XS_HostInfo* pHost = xsHostFind(pWeb, "app");
/* ... read topology info across services ... */
xsServerRelease(pWeb);
}
The read side of the topology is guarded by an RWLock (many reads, few writes) and the Accept path enters the read side every time — queries are cheap. Enumeration arguments are borrowed inside the callback; lease discipline lives in the reference.
11.5 Contract
- Assembly: with multiple services, every candidate compiles before any bind; any failure rejects the whole startup.
- Swap: reload publishes per server; xsReloadAllSubmit is a batch transaction (all succeed or all roll back).
- Queries: xsServerFind returns a retained lease; enumeration arguments are borrowed for the callback — Retain before storing.
Common mistakes
- Symptom
- The reload intent FAILS; Message says workers is immutable.
- Cause
- The thread pool is process-level; rebuilding it at runtime would change the concurrency semantics of every callback — rejected outright rather than risked.
- Symptom
- After reloading the web service, an XS_ServerInfo* cached in the push service becomes a wild pointer.
- Cause
- Pointers from enumeration callbacks are borrowed for one round; keeping them across generations/services requires Retain into a lease.
Exercises
Run the 11.2 topology (scripts may export nothing each) and draw the topology table from the boot log.
Add a /topo route to the web service and emit the JSON topology via xsEnumServers.
Break the gw script's compile on purpose and start; observe. Then fix gw and break web's script instead.
Summary
| Point | Conclusion |
|---|---|
| Topology | Multiple servers per process mixing five protocols, sharing the engine and shutdown |
| Assembly | All candidates compile before bind; any failure rejects the whole startup |
| workers | Process-level, shared by callbacks, immutable across reload; start at ≈CPU cores |
| Queries | xsServerFind (lease) / xsEnumServers (borrowed) |
- xrt reference · executor — the work-stealing model behind the workers pool