Status: design, 2026-10-19 (overnight loop 17). First slices: `--role`
4
with a leader lock (H2) and the event bus (H3).
5
6
OxygenHub is one process today: pages, API, the `oxy` wire protocol, Flow's
7
scheduler and every background task share one runtime and one data
8
directory. That is the single-node edition, and it stays one command. This
9
document says how the same binary runs as several processes, each taking
10
some of the work (its **roles**), so the web tier can scale out and the
11
background work runs exactly once, without a second product to maintain.
12
13
##Principles
14
15
1.**One binary, many roles.**`oxygenhub serve --role web,worker` (all
16
roles when none is given). A cluster runs the roles apart; a small site
17
runs them together.
18
2.**Seams before services.** Each piece of shared state goes behind a
19
trait inside the process first, with today's behaviour as one
20
implementation. Splitting into processes is then a deployment choice.
21
3.**Postgres before new infrastructure.** Leader election (advisory
22
locks), the job queue (`FOR UPDATE SKIP LOCKED`) and the event bus
23
(`LISTEN/NOTIFY`) run on the Postgres the hub already uses (#211). NATS
24
or JetStream come only when a measurement asks for them.
25
4.**Lean on Oxygen's model.** Changes are immutable and content
26
addressed; only bookmark and tag moves are mutable, and they move by
27
compare-and-swap of the operation head. Two processes writing one
28
repository are already safe at the storage layer: that is what makes
29
web nodes cheap to add.
30
31
##The roles
32
33
| Role | Runs | Instances |
34
|---|---|---|
35
|**web**| pages, the JSON API, the `oxy` wire protocol (fetch and push), live pages (SSE), the runner API (claims, logs, finish) | many, behind a load balancer |
36
|**worker**| the background tasks below; one instance of each singleton at a time, chosen by a leader lock | one or more |
37
|**edge** (later) | the wire protocol and large uploads, routed to the node that holds a repository | many |
38
|**store** (later) | repositories on its disks, behind a service interface | several, by repository |
39
40
Tonight's slices are **web** and **worker**. Edge and store are the
41
repository service (a later phase): until then every node reaches every
42
repository either through one shared data directory (one machine) or
43
through bucket-backed repositories (oxygenhub#96), which any node can open.
44
45
##What runs where today, and what each needs
46
47
Background tasks started in `lib.rs::open`:
48
49
| Task | What it does | Role | Singleton? | Needs for several processes |
50
|---|---|---|---|---|
51
|`hooks::worker`| delivers webhooks from the `deliveries` table, nudged by `events.deliveries`| worker | yes today; many once deliveries are claimed with SKIP LOCKED | the nudge over the event bus |
52
|`tlog::checkpointer`| signs checkpoints, gathers witness cosignatures | worker |**yes**| the log's single writer (below) |
|`diffusion::backfill`, `diffusion::reconciler`| the pin index and the bumps it drives | worker | yes | leader lock |
59
|`images::learn_archs`| reads kept images' architectures once | worker | no (idempotent) | none |
60
61
Work spawned by requests (runs on whichever web node took the request):
62
63
-`autoland::on_check_task` after a check is recorded: serialised today by
64
a process-wide `TURN` mutex, so two nodes could land one proposal twice.
65
Needs a cluster-wide lock (an advisory lock per proposal).
66
-`diffusion::propagate` / `try_land` / `catch_up` / `land_ready` after a
67
push: idempotent by design, but racing; the same per-proposal lock.
68
- The transparency log's appends in `after_push` ("logged before the push
69
is answered"): **one writer only**. The log's files live in the data
70
directory; two processes appending would corrupt them. Needs either an
71
advisory lock around each append (one machine, shared directory), or a
72
log role that the web nodes call (a cluster).
73
74
State kept in the process:
75
76
| State | Today | In a cluster |
77
|---|---|---|
78
| state cache, summaries | per process, content-addressed | stays per node (safe: keys are hashes) |
79
| a repository's writer lock (`repos::Locks`, #249) | per process | the operation head's compare-and-swap already arbitrates; the lock only saves retries, so per node is correct |
80
| Flow placement hints (`CACHES`, `HELD`) | per process | per node is acceptable (a hint); the runner's own claim facts are the truth |
81
| live pages (`events`, `rooms`) |`tokio::sync::broadcast`|**the event bus**|
82
| rate limits (`throttle`) | per process | per node (limits per node), or Postgres later |
83
| sessions, tokens, accounts | the database | unchanged |