(yhub) allow to configure yhub worker and server

We want to be able to configure both server and worker, the idea is to
be able to deploy separately the server and the worker and to scale
them.
This commit is contained in:
Manuel Raynaud
2026-09-22 16:00:56 +02:00
parent 6c3e9f53a1
commit dd85b6f321
9 changed files with 319 additions and 8 deletions
+30
View File
@@ -89,6 +89,36 @@ probes are not worth publishing either — kubelet calls them from inside — an
the helm chart's ingress lists what it routes rather than what it hides, so
they stay in-cluster on their own.
## Roles (`YHUB_ROLE`)
yhub is two halves that share the two stores and nothing else — no in-process
state, no ordering between them:
- the **server** accepts the websocket connections, serves the routes above,
and writes every update to the redis stream,
- the **worker** claims tasks from that stream, merges the updates and stores
the result in postgres, then trims what it persisted.
One process runs both, which is the default and what `YHUB_ROLE` unset means.
Setting it splits them, so each can be scaled on its own — the server with the
connected editors, the worker with the write throughput:
| `YHUB_ROLE` | websocket + routes | drains the stream |
| ----------- | ------------------ | ----------------- |
| unset, `all` | yes | yes |
| `server` | yes | no |
| `worker` | no | yes |
A `worker` process binds no port: no probes to give it and no service to put in
front of it. A `server` process claims no task, so a deployment of servers
alone accepts edits and never persists them — the two halves are split
together or not at all. Any other value is refused at startup rather than
guessed.
Redis consumer groups hand each task to exactly one worker, so the number of
workers is a throughput knob and nothing else: no leader, no partitioning, no
coordination between them.
## Container image
The `Dockerfile` has two final stages, like the other services of this
+31 -2
View File
@@ -39,6 +39,24 @@ const allowedOrigins = (
).split(',');
const Y_PROVIDER_API_KEY = secret('Y_PROVIDER_API_KEY', 'yprovider-api-key');
const ORG = process.env.YHUB_ORG || 'docs';
// Which halves of yhub this process runs. The server accepts the websocket
// connections and serves the REST routes; the worker drains the redis stream
// into postgres. They share the two stores and nothing else — no in-process
// state, no ordering between them — so one process can run both (the default)
// or a deployment can split them and scale each on its own: the server with
// the connected editors, the worker with the write throughput.
//
// A stream is only drained by the workers that are running: a deployment of
// `server` alone keeps accepting edits and never persists them, so the two
// halves are split together or not at all.
const ROLE = process.env.YHUB_ROLE || 'all';
if (!['all', 'server', 'worker'].includes(ROLE)) {
throw new Error(
`YHUB_ROLE must be one of "all", "server" or "worker" (got "${ROLE}")`,
);
}
const RUNS_SERVER = ROLE !== 'worker';
const RUNS_WORKER = ROLE !== 'server';
// Segment every route is mounted under (`server.apiPrefix` below), matching the
// URL scheme Docs already routes to the collaboration server. Hardcoded like
// the audiences: the backend builds its urls with the same prefix.
@@ -818,8 +836,19 @@ const yhub = await createYHub({
},
postgres: POSTGRES,
persistence: [], // blobs live in yhub's postgres
// Both halves are declared, and YHUB_ROLE decides which are built: a null
// server binds no port at all (a `worker` pod has no http surface, hence no
// probes and no service in front of it), a null worker claims no task.
//
// apiPrefix mounts every route — built-ins, our custom endpoints, and the
// websocket (/collaboration/ws/v1/{org}/{docid}) — under /collaboration/.
server: { port: PORT, auth, api, apiPrefix: API_PREFIX },
worker: { taskConcurrency: 5, events: workerEvents },
server: RUNS_SERVER
? { port: PORT, auth, api, apiPrefix: API_PREFIX }
: null,
worker: RUNS_WORKER ? { taskConcurrency: 5, events: workerEvents } : null,
});
logger.info(
{ role: ROLE, server: RUNS_SERVER, worker: RUNS_WORKER },
'yhub role',
);