OxygenHub is one process today: pages, API, the `oxy` wire protocol, Flow's
8
scheduler and every background task share one runtime and one data
9
directory. That is the single-node edition, and it stays one command. This
10
document says how the same binary runs as several processes, each taking
11
some of the work (its **roles**), so the web tier can scale out and the
12
background work runs exactly once, without a second product to maintain.
13
14
##Principles
15
16
1.**One binary, many roles.**`oxygenhub serve --role web,worker` (all
17
roles when none is given). A cluster runs the roles apart; a small site
18
runs them together.
19
2.**Seams before services.** Each piece of shared state goes behind a
20
trait inside the process first, with today's behaviour as one
21
implementation. Splitting into processes is then a deployment choice.
22
3.**Postgres before new infrastructure.** Leader election (advisory
23
locks), the job queue (`FOR UPDATE SKIP LOCKED`) and the event bus
24
(`LISTEN/NOTIFY`) run on the Postgres the hub already uses (#211). NATS
25
or JetStream come only when a measurement asks for them.
26
4.**Lean on Oxygen's model.** Changes are immutable and content
27
addressed; only bookmark and tag moves are mutable, and they move by
28
compare-and-swap of the operation head. Two processes writing one
29
repository are already safe at the storage layer: that is what makes
30
web nodes cheap to add.
31
32
##The roles
33
34
| Role | Runs | Instances |
35
|---|---|---|
36
|**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 |
37
|**worker**| the background tasks below; one instance of each singleton at a time, chosen by a leader lock | one or more |
38
|**edge** (later) | the wire protocol and large uploads, routed to the node that holds a repository | many |
39
|**store** (later) | repositories on its disks, behind a service interface | several, by repository |
40
41
Tonight's slices are **web** and **worker**. Edge and store are the
42
repository service (a later phase): until then every node reaches every
43
repository either through one shared data directory (one machine) or
44
through bucket-backed repositories (oxygenhub#96), which any node can open.
45
46
##What runs where today, and what each needs
47
48
Background tasks started in `lib.rs::open`:
49
50
| Task | What it does | Role | Singleton? | Needs for several processes |
|`hooks::worker`| delivers webhooks from the `deliveries` table, nudged by the bus's `deliveries` topic | worker | yes today; many once deliveries are claimed with SKIP LOCKED | the nudge over the event bus (built, #273) |
- The transparency log's appends in `after_push` ("logged before the push
72
is answered"): **one writer only**. The log's files live in the data
73
directory; two processes appending would corrupt them. Needs either an
74
advisory lock around each append (one machine, shared directory), or a
75
log role that the web nodes call (a cluster).
76
77
State kept in the process:
78
79
| State | Today | In a cluster |
80
|---|---|---|
81
| state cache, summaries | per process, content-addressed | stays per node (safe: keys are hashes) |
82
| 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 |
83
| Flow placement hints (`CACHES`, `HELD`) | per process | per node is acceptable (a hint); the runner's own claim facts are the truth |
`crates/hub/src/bus.rs`: `app.bus.publish(event)` and
170
`app.bus.subscribe(topic)`, a `tokio::sync::broadcast::Receiver<Event>`.
171
Nothing to configure: a hub built for PostgreSQL carries events between
172
its processes over `LISTEN/NOTIFY`; on SQLite (one process) the bus is
173
in process only.
174
175
| Topic | Event | Published by | Heard by |
176
|---|---|---|---|
177
|`deliveries`|`delivery_waiting`|`hooks::emit` after queueing deliveries, a ping, a redelivery | the webhook worker (`hooks.worker`), wherever it leads |
178
|`proposals`|`proposal {repo, event}` (a check recorded on a state, with the check) | the checks API, Flow's checks | the proposal page's check stream (`…/proposal/{n}/events`) |