rollbridge 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,55 @@
1
+ # Rollbridge contributor guidance
2
+
3
+ Rollbridge is the process supervisor and local traffic switcher. Keep deployment
4
+ tool concerns outside this repository: Rampway owns activation transactions,
5
+ deploy locks, release-retention metadata, and on-disk cleanup; Rollbridge owns
6
+ process lifecycle, retained generations, ports, and recovery state.
7
+
8
+ ## Background-jobs lifecycle invariant
9
+
10
+ - One runtime generation is one release-scoped `background-jobs-main` plus its
11
+ worker pool. Start the complete candidate generation before activation.
12
+ - After activation, retire the old generation as one unit. Its main stops
13
+ schedules, new dispatch, and new ordinary worker handoffs; its workers stop
14
+ accepting handoffs. The old main remains running with those workers and owns
15
+ their connections, lease fencing, report acceptance and acknowledgement, and
16
+ durable store transitions. The worker/reporting side durably retries terminal
17
+ reports, tracks outstanding report promises, enforces per-job execution
18
+ timeouts, and owns and reaps child runners. Main and workers remain one release
19
+ generation until every accepted handoff settles.
20
+ - Returned or retried work may be dispatched by the new active generation. A
21
+ retired main never dispatches it again. Old workers never reconnect or hand
22
+ over to the new main during a normal deploy.
23
+ - Jobs generations may overlap for hours on release-scoped endpoints. Beacon may
24
+ remain shared on `7330`; jobs-main must use a per-release port range.
25
+ - Deploy success is candidate activation and health. Deploy completion must not
26
+ wait for retired jobs generations, workers, jobs, HTTP/WebSocket connections,
27
+ or other retained services. HTTP and jobs drains are independent; an HTTP
28
+ drain finishing or timing out must not stop a live jobs generation.
29
+ - Persist and recover retired-generation ownership across later deploys and
30
+ supervisor/host recovery. Multiple generations may drain concurrently. Report
31
+ release references so the deployment tool can keep them pinned until no
32
+ retained process uses them.
33
+ - Runtime-owner replacement transfers or preserves this durable supervision and
34
+ returns once the replacement is healthy. It is never a synchronous full
35
+ shutdown. Per-job timeouts remain valid, but a normal worker-shutdown timeout
36
+ is not the deploy solution; legitimate multi-hour drains are valid.
37
+
38
+ Documentation must distinguish required architecture from behavior not yet
39
+ implemented. Do not claim production compliance when source/config still uses a
40
+ fixed jobs-main, worker adoption by a new main, destructive orphan recovery, or
41
+ synchronous cleanup.
42
+
43
+ ## Validation and publication
44
+
45
+ The project is ESM JavaScript with JSDoc type checking. Package scripts are the
46
+ source of truth: `npm run typecheck`, `npm run lint`, `npm test`, and the combined
47
+ `npm run all-checks`. Run focused checks for the files changed; documentation-only
48
+ work requires at least `git diff --check` plus any existing repository-owned
49
+ Markdown/link check, without installing dependencies.
50
+
51
+ Work on a feature branch and open a normal pull request against `master`; never
52
+ push feature work directly to `master`. Releases are maintainer-only and use
53
+ `npm run release:patch` from an up-to-date clean default branch after
54
+ `npm run all-checks`; do not edit package versions or publish during ordinary PR
55
+ work. See `docs/releasing.md` for the complete release checklist.
package/README.md CHANGED
@@ -2,7 +2,22 @@
2
2
 
3
3
  Rollbridge is a Node.js process supervisor and local traffic switcher for zero-downtime deploys.
4
4
 
5
- Nginx points at one stable Rollbridge proxy port. Deploy tooling asks Rollbridge to start a new release, health-check it, switch new traffic to it, and drain old HTTP/WebSocket connections before stopping the previous release.
5
+ Nginx points at one stable Rollbridge proxy port. Deploy tooling asks Rollbridge to start a new release, health-check it, and switch new traffic to it. Retirement then continues asynchronously: HTTP/WebSocket connections and retained process generations drain independently after the deploy command returns.
6
+
7
+ > **Required jobs-generation contract:** a release-scoped background-jobs
8
+ > runtime is its own `background-jobs-main` plus worker pool. Rollbridge starts a
9
+ > complete candidate generation before activation. After activation the retired
10
+ > main stops schedules, new dispatch, and new worker handoffs, but remains with
11
+ > its old workers to supervise every handoff it already made until all jobs and
12
+ > workers settle. Old workers never move to the new main. Generations may overlap
13
+ > for hours on separate ports, and neither deploy completion nor HTTP drain
14
+ > completion waits for or kills them. See [Background-job worker
15
+ > deployment](docs/workers.md) and the [Velocious deployment
16
+ > guide](docs/velocious.md).
17
+
18
+ This is the required architecture, not evidence that every released runtime or
19
+ consumer config already implements durable recovery and release-reference
20
+ reporting; verify source and config before claiming compliance.
6
21
 
7
22
  ## Install
8
23
 
@@ -47,23 +62,28 @@ export default {
47
62
  processes: [
48
63
  {
49
64
  id: "beacon",
50
- policy: "companion",
65
+ policy: "service",
51
66
  cwd: "{{releasePath}}",
52
67
  command: "env VELOCIOUS_BEACON_PORT={{port}} npx velocious beacon",
53
- port: {from: 17330, to: 17399}
68
+ port: 7330
54
69
  },
55
70
  {
56
71
  id: "background-jobs-worker",
57
72
  policy: "companion",
58
73
  cwd: "{{releasePath}}",
74
+ env: {VELOCIOUS_BACKGROUND_JOBS_PORT: "{{ports.background-jobs-main}}"},
59
75
  command: "npx velocious background-jobs-worker",
76
+ nonBlockingDrain: true,
77
+ gracefulStopMs: "indefinite",
60
78
  outputLines: 200
61
79
  },
62
80
  {
63
81
  id: "background-jobs-main",
64
82
  policy: "service",
83
+ deployStrategy: "handoff",
65
84
  cwd: "{{releasePath}}",
66
- command: "npx velocious background-jobs-main"
85
+ command: "env VELOCIOUS_BACKGROUND_JOBS_PORT={{port}} npx velocious background-jobs-main",
86
+ port: {from: 7331, to: 7399}
67
87
  },
68
88
  {
69
89
  id: "web",
@@ -118,10 +138,11 @@ See [`docs/config.md`](docs/config.md#processesmemory).
118
138
  memory: {limitBytes: 536870912, warnBytes: 402653184, checkIntervalMs: 5000}
119
139
  ```
120
140
 
121
- Set a process's `stopSignal` (default `"SIGTERM"`) to the signal it quiets on, so
122
- a worker finishes its in-flight work before exiting. Rollbridge sends `stopSignal`
123
- to gracefully stop the process and `SIGKILL`s it only if it hasn't exited within
124
- `gracefulStopMs`. For example, a job worker that drains on `SIGINT`:
141
+ Set a process's `stopSignal` (default `"SIGTERM"`) to the signal it quiets on.
142
+ This is a process-level stop mechanism, not the primary deploy-completion
143
+ mechanism for a release-scoped jobs generation. A retired generation may remain
144
+ for hours, and its old jobs-main must stay available until its workers finish.
145
+ For example, a generic worker that drains on `SIGINT`:
125
146
 
126
147
  ```js
127
148
  {id: "worker", policy: "companion", command: "…", stopSignal: "SIGINT", gracefulStopMs: 60000}
@@ -138,18 +159,19 @@ queue. See [`docs/config.md`](docs/config.md#processesreplicas).
138
159
  {id: "worker", policy: "companion", command: "npx velocious background-jobs-worker", replicas: 4}
139
160
  ```
140
161
 
141
- For workers that quiesce or drain via a command, set a `lifecycle` block —
162
+ For generic workers that quiesce or drain via a command, set a `lifecycle` block —
142
163
  Rollbridge runs `quietCommand`, then drains (`drainCommand`/`drainTimeoutMs`),
143
164
  then `stopCommand`/`stopSignal`, then `SIGKILL` after `gracefulStopMs` when
144
165
  gracefully stopping the process. Each hook is bounded so it can't wedge a stop.
145
166
 
146
- Set `nonBlockingDrain: true` on a worker companion to start its graceful stop the
147
- moment its release is retired in parallel with the proxied connection drain,
148
- not after it so new workers handle new work while the old workers finish theirs.
167
+ Set `nonBlockingDrain: true` on a jobs worker companion so it stops accepting new
168
+ handoffs as soon as its release retires, independently of the proxied connection
169
+ drain. Its release-scoped handoff jobs-main remains running to supervise existing
170
+ handoffs and exits only after the worker pool has drained.
149
171
 
150
- See [`docs/workers.md`](docs/workers.md) for the full safe background-job worker
151
- deployment pattern companion policy, `replicas`, and finishing in-flight jobs
152
- on deploy with `stopSignal`/`lifecycle` + `gracefulStopMs`.
172
+ See [`docs/workers.md`](docs/workers.md) for the full release-generation
173
+ deployment pattern: a handoff `background-jobs-main`, its companion worker pool,
174
+ independent quiescence, and durable supervision while retained generations drain.
153
175
 
154
176
  Set `releaseRetention` to bound how many stopped (drained) releases the daemon
155
177
  keeps in memory and reports in `status`. `keep` (default `10`) retains the most
@@ -270,8 +292,9 @@ A release-scoped helper (for example a background worker bound to one release).
270
292
  It starts **before** the proxied process in the same release, so release-local
271
293
  dependencies are ready before the health check, and it is auto-restarted while
272
294
  its release is active. Each release gets its own companions; a release's
273
- companions stop when that release is drained and retired after a newer release
274
- takes over.
295
+ companions normally stop when that release is drained and retired after a newer
296
+ release takes over. A jobs worker configured with `nonBlockingDrain` instead
297
+ quiesces at retirement and drains independently of HTTP/WebSocket connections.
275
298
 
276
299
  ```js
277
300
  {
@@ -279,7 +302,8 @@ takes over.
279
302
  policy: "companion",
280
303
  cwd: "{{releasePath}}",
281
304
  command: "npx velocious background-jobs-worker",
282
- gracefulStopMs: 60000
305
+ nonBlockingDrain: true,
306
+ gracefulStopMs: "indefinite"
283
307
  }
284
308
  ```
285
309
 
@@ -301,36 +325,36 @@ new copies simultaneously during a deploy would be unsafe.
301
325
 
302
326
  ### `service`
303
327
 
304
- A daemon-wide broker that should outlive individual releases — for example
305
- Velocious Beacon or `background-jobs-main`. Rollbridge starts it once (before
306
- release processes that depend on it), keeps it running across deploys, and gives
307
- it a stable port that does not change between releases. After each successful
308
- deploy its restart template is refreshed to the latest release, so if it crashes
309
- it restarts from the newest good release. It keeps restarting until the daemon
310
- shuts down.
328
+ A service can be daemon-wide and persistent, or release-scoped with
329
+ `deployStrategy: "handoff"`. Velocious Beacon is normally persistent on a stable
330
+ port. `background-jobs-main` is not: it must be a handoff service with one port
331
+ per release so old workers keep their old coordinator while new workers use the
332
+ candidate coordinator.
311
333
 
312
334
  ```js
313
335
  {
314
336
  id: "background-jobs-main",
315
337
  policy: "service",
338
+ deployStrategy: "handoff",
316
339
  cwd: "{{releasePath}}",
317
340
  command: "npx velocious background-jobs-main",
318
- port: 7331
341
+ port: {from: 7331, to: 7399}
319
342
  }
320
343
  ```
321
344
 
322
345
  ### Deploy ordering
323
346
 
324
- On `rollbridge deploy`, Rollbridge:
347
+ On `rollbridge deploy`, the required ordering is:
325
348
 
326
- 1. starts any `service` that is not already running;
349
+ 1. starts any missing persistent service and the candidate's handoff services;
327
350
  2. starts the new release's `companion`s, then its `proxied` process, and
328
351
  health-checks the proxied process;
329
352
  3. switches new traffic to the new release;
330
- 4. refreshes each `service`'s restart template to the new release;
353
+ 4. marks the previous jobs-main and worker pool retired as one generation;
331
354
  5. replaces `singleton`s (stops the old one, then starts the new one);
332
- 6. drains the previous release's connections, then stops its `proxied` and
333
- `companion` processes.
355
+ 6. returns success without waiting for the previous generation or its independent
356
+ HTTP/WebSocket drain; Rollbridge supervises all retained drains in the
357
+ background and reaps each generation only after its handoffs and workers end.
334
358
 
335
359
  If the new release fails to start or health-check, the previous release stays
336
360
  active and any service started during this deploy is rolled back.
@@ -413,13 +437,13 @@ rollbridge daemon --config /srv/ticket-server/rollbridge.js \
413
437
  ```
414
438
 
415
439
  External supervisors that need to replace a foreground owner without waiting
416
- for its workers to drain add `--takeover-owner`. The candidate starts and
417
- health-checks the exact release first. Only then does it retire the accepted
418
- owner's proxy/control listeners; the retired daemon keeps its already-accepted
419
- workers until their ordinary drain completes while the attested replacement
420
- binds the stable listeners. Candidate bootstrap failure leaves the accepted
421
- owner untouched. This is opt-in; ordinary daemon bootstrap and `shutdown` keep
422
- their existing behavior.
440
+ for retained generations to drain add `--takeover-owner`. The candidate starts
441
+ and health-checks the exact release first. Only then does it retire the accepted
442
+ owner's proxy/control listeners. Durable supervision of old jobs-main/worker
443
+ generations must be preserved or transferred while the attested replacement
444
+ binds the stable listeners; replacement does not mean full synchronous shutdown.
445
+ Candidate bootstrap failure leaves the accepted owner untouched. This is opt-in;
446
+ ordinary daemon bootstrap and `shutdown` keep their existing behavior.
423
447
 
424
448
  The four bootstrap inputs are all-or-nothing and use absolute config/release
425
449
  paths. Rollbridge binds its proxy, activates the release through the normal
@@ -600,7 +624,7 @@ daemon holds its log file open, so logrotate needs `copytruncate`.
600
624
 
601
625
  ## Deployment Notes
602
626
 
603
- Run migrations before `rollbridge deploy`, and keep migrations backwards-compatible while old and new web releases overlap. For stable local brokers such as Velocious Beacon or `background-jobs-main`, use `service` when the process should survive deploys and restart from the latest successful release if it crashes.
627
+ Run migrations before `rollbridge deploy`, and keep migrations backwards-compatible while old and new web and jobs generations overlap. Velocious Beacon may be a persistent fixed-port `service`; configure `background-jobs-main` as a release-scoped handoff service on a port range. A normal deploy may leave several retired generations draining concurrently.
604
628
 
605
629
  See [`docs/deploy-recipes.md`](docs/deploy-recipes.md) for ready-to-use shell, CI, and Capistrano recipes that drive Rollbridge through its CLI, and [`docs/troubleshooting.md`](docs/troubleshooting.md) for diagnosing health-check failures, port conflicts, stale sockets, crash loops, and stuck draining releases.
606
630
 
package/TODO.md CHANGED
@@ -30,11 +30,12 @@ This roadmap tracks planned Rollbridge features and documentation. Rollbridge sh
30
30
  - [x] Distinguish crash restarts, deploy replacements, manual restarts, and memory restarts in status/events. (Per-process `lastStartReason` + a `reason` on the `process started` event; the `memory` reason is wired and fires once memory supervision restarts a process.)
31
31
  - [x] Add a `restart` CLI command for a single process, a policy group, or all non-proxied workers.
32
32
  - [x] Keep restart behavior safe for job workers by using lifecycle hooks before termination. (Manual restart, memory restart, and deploy-drain stops all run the `lifecycle` hooks via `stop()`.)
33
- - [x] Graceful job-worker lifecycle.
33
+ - [x] Graceful process-stop controls for job workers.
34
34
  - [x] Add generic lifecycle hooks such as `quietCommand`, `drainCommand`, `drainTimeoutMs`, and `stopCommand` (per-process `lifecycle`).
35
35
  - [x] Support signal-only lifecycle steps for workers that can quiet on a Unix signal. (Per-process `stopSignal`; sent before the `SIGKILL`-after-`gracefulStopMs` fallback.)
36
- - [x] Add a non-blocking drain mode so new workers can start while old workers finish running jobs (per-process `nonBlockingDrain`; drains the worker in parallel with the connection drain).
37
- - [x] Document a Velocious background-jobs-worker recipe once the lifecycle contract is implemented (`docs/velocious.md` Worker recipe).
36
+ - [x] Add a non-blocking drain mode so a worker can quiesce at release retirement independently of the HTTP/WebSocket connection drain (`nonBlockingDrain`). This control alone does not provide durable retired-generation supervision.
37
+ - [x] Document the required Velocious release-generation contract (`docs/velocious.md` and `docs/workers.md`) without treating documentation as proof that the runtime implements it.
38
+ - [ ] Implement and verify durable release-scoped jobs-main retirement: owned-handoff supervision, recovery across daemon/host replacement, multiple concurrent retired generations, and release-reference reporting for cleanup pins.
38
39
  - [x] Replicas and stable worker indexes. (Supported on port-less `companion` processes; `proxied`/`singleton`/ported processes stay single.)
39
40
  - [x] Allow one process config to start multiple replicas (`replicas`, companion-only for now).
40
41
  - [x] Expose `ROLLBRIDGE_REPLICA_INDEX`, replica count, and per-replica template context (`{{replicaIndex}}`/`{{replicaCount}}`).
@@ -91,7 +92,7 @@ This roadmap tracks planned Rollbridge features and documentation. Rollbridge sh
91
92
  - [x] Write a CLI reference for `daemon`, `ensure-daemon`, `deploy`, `status`, `stop`, `shutdown`, and future commands (`docs/cli.md`).
92
93
  - [x] Expand process policy docs with deployment examples for `proxied`, `companion`, `singleton`, and `service`.
93
94
  - [x] Document memory checks and auto-restart behavior after the feature lands (`docs/config.md` → `processes[].memory`).
94
- - [x] Document safe background-job deployment patterns (`docs/workers.md`: companion + `replicas` + `stopSignal` + `gracefulStopMs`, old/new worker overlap).
95
+ - [x] Document the required background-job generation pattern (`docs/workers.md`: release-scoped jobs-main + worker pool, independent quiescence, durable retained supervision, and deploy completion that does not wait for drains).
95
96
  - [x] Document worker lifecycle hooks (`docs/config.md` → `processes[].lifecycle`, `docs/workers.md`).
96
97
  - [x] Add a Velocious deployment guide with Beacon, background-jobs-main, background-jobs-worker, and web process examples (`docs/velocious.md`).
97
98
  - [x] Add an Nginx guide with WebSocket headers, timeouts, and common failure modes (`docs/nginx.md`).
package/docs/cli.md CHANGED
@@ -66,10 +66,16 @@ and waits for control-socket deployments.
66
66
 
67
67
  `--takeover-owner` requires the complete bootstrap tuple. It bootstraps and
68
68
  health-checks the replacement before sending the current daemon the private
69
- retirement command. Retirement stops stable listeners and new work promptly,
70
- but does not wait for owned workers' normal drain before the replacement binds
71
- the proxy and control socket. A bootstrap failure occurs before retirement, so
72
- the previously accepted owner remains available.
69
+ retirement command. The current `performOwnerRetirement` path quiesces every
70
+ service, singleton, starting release, and retained release, releases the stable
71
+ listeners, and starts asynchronous `stop()` calls for all of them. It neither
72
+ preserves nor transfers retained-generation supervision to the replacement.
73
+ The replacement can bind before those stops finish. A bootstrap failure occurs
74
+ before retirement, so the previously accepted owner remains available.
75
+
76
+ A compliant future owner handoff must instead preserve or transfer durable
77
+ supervision of retained generations without handing old workers to a new
78
+ jobs-main. Current `--takeover-owner` does not provide that behavior.
73
79
 
74
80
  ## `ensure-daemon`
75
81
 
@@ -120,12 +126,26 @@ rollbridge deploy --release-path <path>
120
126
  [--daemon-start-timeout-ms <ms>]
121
127
  ```
122
128
 
123
- Starts the prepared release, health-checks the proxied process, switches new
124
- traffic to it, then drains and stops the previous release. Prints
129
+ Starts the complete prepared release generation, health-checks the proxied
130
+ process, and switches new traffic to it. The current daemon then starts
131
+ retirement of the previous release asynchronously, so the command does not wait
132
+ for old workers, jobs, or HTTP/WebSocket connections to finish. Prints
125
133
  `{"status": "success", "activeReleaseId": "...", "previousReleaseId": "..."}`.
126
134
  If the new release fails to start or health-check, the previous release stays
127
135
  active and the command errors.
128
136
 
137
+ After candidate activation, `Daemon.deploy()` synchronously waits for singleton
138
+ replacement before starting `drainAndPrune` and returning. A replacement failure
139
+ can therefore return a non-zero result while the candidate remains active, and a
140
+ slow replacement delays both the response and retirement of the old release.
141
+
142
+ This is a process-lifetime non-blocking drain, not durable supervision across a
143
+ daemon or host restart. It continues across later deploys only while the same
144
+ daemon remains alive. After a restart, surviving PIDs from persisted state are
145
+ advisory orphans that Rollbridge cannot re-adopt; explicit `recover --force`
146
+ stops them. Restart-surviving retained-generation ownership and recovery remain
147
+ required future behavior.
148
+
129
149
  Before each deploy, the daemon reloads the config path it was started with.
130
150
  Compatible process and lifecycle changes apply to the new release and govern
131
151
  how the previous release retires, including updated `nonBlockingDrain`,
package/docs/config.md CHANGED
@@ -75,7 +75,7 @@ to let a deploy group talk to the daemon.
75
75
  | `proxy.upstreamHost` | string | `proxy.host`, or `"127.0.0.1"` when `proxy.host` is `0.0.0.0`/`::` | Host Rollbridge uses for release health checks and proxy targets. |
76
76
  | `proxy.healthPath` | string | `"/ping"` | Default health-check path for proxied processes. |
77
77
  | `proxy.healthTimeoutMs` | number | `30000` | Default health-check timeout for proxied processes. |
78
- | `proxy.drainTimeoutMs` | number | `60000` | How long to drain open connections from a retired release before stopping it. |
78
+ | `proxy.drainTimeoutMs` | number | `60000` | How long to drain HTTP/WebSocket connections before stopping the retired proxied process. Expiry never stops an independently draining jobs generation. |
79
79
  | `proxy.forceStopTimeoutMs` | number | `10000` | Default per-process graceful-stop timeout (`SIGTERM`, then `SIGKILL`). |
80
80
 
81
81
  ## `releaseRetention`
@@ -212,6 +212,20 @@ range** so old and new instances can run at the same time:
212
212
 
213
213
  Reference it from same-release processes with `{{ports.background-jobs-main}}`.
214
214
  During a deploy, old workers keep the old port and new workers get the new port.
215
+ For background jobs, the required compliant architecture makes the handoff
216
+ service and its workers one release generation. Immediately after activation,
217
+ retirement must quiesce the old jobs-main's scheduling, dispatch, and new worker
218
+ handoffs while keeping it with its workers until their accepted work settles.
219
+ Workers are not adopted by the new service.
220
+
221
+ That immediate old-main quiescence is **required future compliance behavior**,
222
+ not current `deployStrategy: "handoff"` behavior. Today the release group starts
223
+ `stop()` for `nonBlockingDrain` companions at retirement but sends no retirement
224
+ or quiescence notice to the handoff service. It waits for the connection drain,
225
+ stops other dependent processes, waits for the non-blocking companion stops, and
226
+ only then stops the handoff service. Consequently, old and new jobs-main
227
+ instances can overlap scheduling and dispatch ownership. Do not treat the
228
+ configuration above alone as compliance with the background-jobs contract.
215
229
 
216
230
  ### `processes[].lifecycle`
217
231
 
@@ -220,6 +234,11 @@ deploy's drain, a `rollbridge restart`, a memory restart, or shutdown. They let
220
234
  job worker quiesce and finish in-flight work before it is terminated. Omit
221
235
  `lifecycle` for the default behavior (just `stopSignal` then `SIGKILL`).
222
236
 
237
+ These hooks describe an individual process stop. They do not define when a
238
+ deploy completes and must not impose a short normal-drain deadline on a jobs
239
+ generation. Per-job timeouts remain the correct bound for genuinely hung jobs;
240
+ legitimate hours-long generation drains are valid.
241
+
223
242
  | Field | Type | Default | Description |
224
243
  | --- | --- | --- | --- |
225
244
  | `lifecycle.quietCommand` | string | unset | Run first to tell the process to stop accepting new work. |
@@ -253,15 +272,16 @@ That keeps a worker alive in case the draining web process still depends on it
253
272
  but it also holds a background worker open for the whole connection drain.
254
273
 
255
274
  Set `nonBlockingDrain: true` on a `companion` whose work is independent of the
256
- proxied process (a job worker on a shared queue). Its graceful stop — `lifecycle`
257
- hooks, or `stopSignal` then `SIGKILL` after `gracefulStopMs` then starts **as
258
- soon as the release is retired**, in parallel with the connection drain, rather
259
- than after it. The new release's workers (started before traffic switches) handle
260
- new work while the retired release's workers finish their in-flight jobs. The
261
- whole drain stays non-blocking the deploy returns immediately.
275
+ proxied process (a job worker on a shared queue). Rollbridge starts that
276
+ companion's configured stop sequence **as soon as the release is retired**, in
277
+ parallel with the connection drain, rather than after it. Its quiet command or
278
+ signal must make the worker stop accepting new handoffs. The asynchronous
279
+ release drain continues after the deploy response. As described above, current
280
+ Rollbridge does not simultaneously quiesce the handoff service, so this setting
281
+ alone does not prevent overlapping jobs-main scheduling or dispatch ownership.
262
282
 
263
283
  ```js
264
- {id: "worker", policy: "companion", command: "…", nonBlockingDrain: true, stopSignal: "SIGINT", gracefulStopMs: 60000}
284
+ {id: "worker", policy: "companion", command: "…", nonBlockingDrain: true, stopSignal: "SIGINT", gracefulStopMs: "indefinite"}
265
285
  ```
266
286
 
267
287
  ### `processes[].restart`
package/docs/nginx.md CHANGED
@@ -67,10 +67,11 @@ Related Rollbridge timeouts (configured in `rollbridge.js`, not Nginx):
67
67
 
68
68
  - `proxy.healthTimeoutMs` gates how long a new release has to become healthy
69
69
  before a deploy aborts — it does not affect request timeouts.
70
- - `proxy.drainTimeoutMs` is how long Rollbridge keeps an old release alive for
71
- in-flight connections during a deploy. Keep Nginx's `proxy_read_timeout` for
72
- WebSocket locations comfortably above it so the front end doesn't cut
73
- connections Rollbridge is still draining.
70
+ - `proxy.drainTimeoutMs` is how long Rollbridge keeps an old proxied web process
71
+ alive for in-flight connections during a deploy. Keep Nginx's
72
+ `proxy_read_timeout` for WebSocket locations comfortably above it so the front
73
+ end doesn't cut connections Rollbridge is still draining. Retained jobs
74
+ generations drain independently and are not stopped when this timeout expires.
74
75
 
75
76
  ## Forwarded headers
76
77
 
@@ -1,129 +1,95 @@
1
- # TensorBuzz production runbook
1
+ # TensorBuzz Rollbridge runbook
2
2
 
3
- Operating the TensorBuzz backend under Rollbridge. The production config lives at
4
- [`examples/tensorbuzz.com.js`](../examples/tensorbuzz.com.js); this runbook
5
- assumes it is deployed to a stable path (`/etc/rollbridge/tensorbuzz.com.js`
6
- below) and the daemon runs as a systemd service (see
7
- [Running under systemd](../README.md#running-under-systemd)). For the general
8
- Velocious topology and the worker recipe, see [`docs/velocious.md`](velocious.md).
3
+ This runbook defines the required TensorBuzz backend topology. The example at
4
+ [`examples/tensorbuzz.com.js`](../examples/tensorbuzz.com.js) is illustrative;
5
+ verify the deployed consumer config and Rollbridge implementation before
6
+ asserting production compliance.
9
7
 
10
- ## Ports
8
+ ## Ports and generations
11
9
 
12
- | Port | Process | Notes |
10
+ | Port | Process | Contract |
13
11
  | --- | --- | --- |
14
- | `4500` | Rollbridge proxy | The stable public port. **Nginx proxies the backend host to `127.0.0.1:4500`** — never to a release's web port. |
15
- | `7330` | `beacon` (`service`) | Fixed; the shared broker every release connects to. |
16
- | `7331` | `background-jobs-main` (`service`) | Fixed; the job coordinator. |
17
- | `14500`–`14599` | `web` (`proxied`) | One port per release, allocated per deploy; Rollbridge forwards `4500` here. |
18
- | (none) | `background-jobs-worker` (`companion`) | A per-release worker; no listening port. |
19
-
20
- Control socket: `/tmp/rollbridge-tensorbuzz.sock`.
21
-
22
- ## Process topology
23
-
24
- - **`beacon`** and **`background-jobs-main`** are `service`s: one daemon-wide
25
- instance each, on their fixed ports, surviving deploys.
26
- - **`background-jobs-worker`** is a `companion`: a fresh worker per release,
27
- running that release's code, with `gracefulStopMs: 60000` so an in-flight job
28
- finishes before `SIGKILL`.
29
- - **`web`** is the one `proxied` process, health-checked at `/ping` before
30
- traffic switches.
31
-
32
- Each process waits for its dependencies with `wait-for-it` (`beacon` →
33
- `background-jobs-main` `worker`/`web`), so nothing starts talking to Beacon or
34
- the job coordinator before they listen.
35
-
36
- ## External services
37
-
38
- Rollbridge manages **only the four processes above**. Everything else the
39
- Velocious app depends on the database and any other backing services — is
40
- **provisioned and operated outside Rollbridge**: Rollbridge does not start, stop,
41
- health-check, or know about them. Configure those connections through the app's
42
- own environment/config. When such a dependency is down, the `web` process's
43
- `/ping` health check is what gates a deploy (a release that can't reach its
44
- database won't pass health and won't go live).
45
-
46
- ## Deploying
47
-
48
- Drive deploys through the CLI (see [`docs/deploy-recipes.md`](deploy-recipes.md)).
49
- Run **backwards-compatible** migrations before switching traffic, because the old
50
- and new releases overlap during the drain:
51
-
52
- ```bash
53
- release_path=/srv/tensorbuzz/releases/<timestamp> # prepared by your pipeline
54
- (cd "$release_path/backend" && npx velocious db:migrate)
55
-
56
- rollbridge deploy \
57
- --ensure-daemon \
58
- --config /etc/rollbridge/tensorbuzz.com.js \
59
- --release-path "$release_path" \
60
- --revision "$(git -C "$release_path/backend" rev-parse HEAD)"
61
- ```
62
-
63
- ### Deploy ordering
64
-
65
- On `rollbridge deploy`, Rollbridge:
66
-
67
- 1. starts any missing `service` (`beacon`, `background-jobs-main`);
68
- 2. starts the new release's `background-jobs-worker`, then its `web` process, and
69
- health-checks `web` on its `{{port}}`/`/ping`;
70
- 3. switches new traffic to the new `web`;
71
- 4. refreshes the services' restart templates to the new release;
72
- 5. drains the previous release's connections, then stops its `web` and worker.
73
-
74
- If the new release fails to start or health-check, **the previous release stays
75
- active** and the command exits non-zero so a failed deploy never takes the site
76
- down.
77
-
78
- ## Rollback
79
-
80
- ```bash
81
- rollbridge rollback --config /etc/rollbridge/tensorbuzz.com.js
82
- # or a specific retained release:
83
- rollbridge rollback --config /etc/rollbridge/tensorbuzz.com.js --release-id <id>
84
- ```
85
-
86
- Rollback re-runs the deploy flow on a retained release, health-checks it, and
87
- switches traffic back. Constraints:
88
-
89
- - **Migrations are not reverted.** Rollback only manages processes; if a release
90
- bumped the schema, rolling code back requires that the old code still works
91
- against the new schema keep migrations backwards-compatible (the same rule as
92
- deploys).
93
- - The target release's on-disk directory must still exist (don't prune it from
94
- disk before you might roll back to it).
95
- - Only releases Rollbridge still retains (`releaseRetention`) can be targeted.
96
-
97
- ## Day-to-day operations
98
-
99
- ```bash
100
- C=/etc/rollbridge/tensorbuzz.com.js
101
-
102
- rollbridge status --config "$C" # active release, ports, per-process state
103
- rollbridge logs --config "$C" --process web # recent stdout/stderr of a process
104
- rollbridge events --config "$C" # deploys, switches, crashes, restarts
105
- rollbridge doctor --config "$C" # pre-flight: socket, proxy port, state
106
- rollbridge restart --config "$C" --process background-jobs-worker # bounce the worker
107
- ```
108
-
109
- Restarting `beacon` or `background-jobs-main` bounces a shared broker and briefly
110
- disrupts everything that depends on it; prefer `deploy`/`rollback` for code
111
- changes. See [`docs/troubleshooting.md`](troubleshooting.md) for health-check
112
- failures, port conflicts, stale sockets, crash loops, and stuck draining
113
- releases.
114
-
115
- ## Crash recovery
116
-
117
- Set [`statePath`](config.md#statepath) in the config to have the daemon persist
118
- its state. After a daemon crash or reboot, `rollbridge doctor` reports any
119
- **orphaned** processes still alive from the previous daemon. To clean them up
120
- before restarting the daemon, run `rollbridge recover` (a dry run that lists
121
- them), then `rollbridge recover --force` to stop them:
122
-
123
- ```bash
124
- rollbridge recover --config /etc/rollbridge/tensorbuzz.com.js # list leftovers
125
- rollbridge recover --config /etc/rollbridge/tensorbuzz.com.js --force # stop them
126
- ```
127
-
128
- A machine reboot kills every process, so there are usually no orphans afterward —
129
- the daemon just starts fresh.
12
+ | `4500` | Rollbridge proxy | Stable public upstream for Nginx. |
13
+ | `7330` | Beacon | Shared persistent service; fixed port is allowed. |
14
+ | `7331`–`7399` | `background-jobs-main` | One allocated endpoint per release generation; never one fixed persistent coordinator. |
15
+ | `14500`–`14599` | web | One proxied endpoint per release. |
16
+ | none | workers | Same-release companions connected only to their generation's jobs-main. |
17
+
18
+ Each jobs generation contains one jobs-main and its complete worker pool, all
19
+ running the same release code. Several old generations may continue draining
20
+ while a newer generation is active.
21
+
22
+ ## Required deploy order
23
+
24
+ 1. Prepare the candidate release and run backwards-compatible migrations.
25
+ 2. Start the candidate jobs-main on a new port, then its worker pool and web
26
+ process. Health-check web before activation.
27
+ 3. Activate the candidate release and switch new traffic.
28
+ 4. Retire the previous jobs-main and workers as one generation. Jobs-main stops
29
+ schedule ownership, new dispatch, and new handoffs; workers stop accepting
30
+ handoffs.
31
+ 5. Return deploy success and release the deploy lock. Do not wait for old jobs,
32
+ workers, jobs-main, HTTP/WebSocket connections, or other retained services.
33
+
34
+ The retired jobs-main remains running on its old endpoint with its old workers.
35
+ For accepted handoffs it owns worker connections and heartbeats, lease fencing,
36
+ terminal-report acceptance and acknowledgement, and durable store transitions.
37
+ The old worker/reporting side durably retries terminal reports, tracks
38
+ outstanding report promises, enforces per-job execution timeouts, and owns and
39
+ reaps child runners. Returned or retried work becomes eligible for the new active
40
+ generation and is never redispatched by the retired main. Old workers do not
41
+ reconnect to or transfer their handoffs to the new main.
42
+
43
+ Old main and workers remain one release generation until every accepted handoff
44
+ settles. Only then, after every old worker exits, may jobs-main exit and
45
+ Rollbridge reap the generation. The referenced release directory stays pinned
46
+ against Rampway cleanup until that point.
47
+
48
+ ## Independent drains
49
+
50
+ Set the worker companion to `nonBlockingDrain: true` so it quiesces when its
51
+ generation retires. HTTP/WebSocket connection drain continues independently.
52
+ Finishing or timing out the HTTP drain must never stop a still-draining jobs
53
+ generation. A legitimate multi-hour job and generation drain are valid.
54
+
55
+ Per-job timeouts remain the backstop for genuinely hung jobs. Do not use a short
56
+ worker-shutdown or supervisor timeout as the primary deploy solution; deployment
57
+ has already completed after candidate activation and health.
58
+
59
+ ## Runtime-owner and recovery requirements
60
+
61
+ Required compliant behavior durably supervises every retired generation after
62
+ the deploy command returns, across later deploys and supervisor/host recovery.
63
+ Runtime-owner or version handoff must preserve or transfer that supervision and
64
+ return once the replacement is healthy. It must not perform full synchronous
65
+ shutdown, kill retained generations, or make the new jobs-main adopt old workers.
66
+
67
+ Recovery must reconstruct retained generation ownership, endpoints, release
68
+ paths, and process references. Do not treat them as generic orphans to force-stop
69
+ merely because a supervisor restarted. Cleanup becomes eligible only after the
70
+ last retained process exits.
71
+
72
+ Current Rollbridge does not yet meet those recovery and owner-handoff
73
+ requirements. Its non-blocking release drains last only for the current daemon's
74
+ lifetime; after restart it reports surviving PIDs as advisory, non-adoptable
75
+ orphans, and forced recovery stops them. `--takeover-owner` quiesces and starts
76
+ asynchronous stops for every managed process instead of transferring retained
77
+ generations.
78
+
79
+ ## Operator checks
80
+
81
+ Use `rollbridge status`, logs, and events to confirm:
82
+
83
+ - the active jobs generation uses the active release and a unique jobs-main port;
84
+ - every retired generation retains its own jobs-main, workers, endpoint, and
85
+ pinned release path;
86
+ - the deploy command has returned while long drains continue;
87
+ - no retired jobs-main is dispatching new ordinary queued work;
88
+ - HTTP drain completion or timeout did not stop a jobs generation; and
89
+ - completed generations are reaped and Rampway is told their release references
90
+ ended so it can release the pins.
91
+
92
+ Do not restart Beacon or a jobs-main casually. Never use `shutdown`, forced
93
+ orphan recovery, or process signals as a substitute for the normal retained-
94
+ generation lifecycle. See [`docs/velocious.md`](velocious.md) and
95
+ [`docs/workers.md`](workers.md) for the architecture details.
@@ -109,12 +109,21 @@ stays active.
109
109
  in `state: "draining"` with non-zero `connections` (often `websocket`).
110
110
 
111
111
  **Diagnose.** Long-lived connections (WebSockets, SSE, streaming responses) keep
112
- the retired release alive until they close or `proxy.drainTimeoutMs` elapses.
113
- `status` shows the release's `connections.http`/`connections.websocket` and
114
- `drainStartedAt`.
115
-
116
- **Fix.** Draining ends automatically when those connections close, or after
117
- `proxy.drainTimeoutMs` (then the release is stopped regardless). Lower
118
- `proxy.drainTimeoutMs` to force-stop sooner, or make clients reconnect (for
119
- example, have the front end close idle WebSockets on deploy). Once stopped, the
120
- release is pruned per `releaseRetention`.
112
+ the retired proxied web process alive until they close or
113
+ `proxy.drainTimeoutMs` elapses. `status` shows the release's
114
+ `connections.http`/`connections.websocket` and `drainStartedAt`. A retained jobs
115
+ generation has an independent lifecycle and may remain after the web drain ends.
116
+
117
+ **Fix.** The connection drain ends automatically when those connections close,
118
+ or after `proxy.drainTimeoutMs`. Rollbridge then stops the retired proxied web
119
+ process and other connection-dependent processes, including ordinary companions
120
+ with `nonBlockingDrain: false`. Lower the timeout only to shorten
121
+ HTTP/WebSocket retention, or make clients reconnect (for example, have the front
122
+ end close idle WebSockets on deploy). In the documented compliant jobs topology,
123
+ jobs companions use `nonBlockingDrain: true`, so timeout expiry affects only the
124
+ web side and must not stop a still-draining jobs generation.
125
+
126
+ Reporting release references to Rampway and pinning release directories against
127
+ on-disk cleanup are required future behavior, not implemented today. Current
128
+ Rollbridge `status` and `releaseRetention` govern only its in-memory release
129
+ records and do not fence Rampway cleanup.
package/docs/velocious.md CHANGED
@@ -1,37 +1,35 @@
1
1
  # Velocious deployment guide
2
2
 
3
- A Velocious backend typically runs four kinds of process: **Beacon** (the
4
- message broker other processes connect to), **background-jobs-main** (the job
5
- coordinator), **background-jobs-worker** (runs the jobs), and the **web/API**
6
- server. This guide maps each to a Rollbridge process policy, shows a complete
7
- `rollbridge.js`, and explains startup ordering and what happens on a deploy.
3
+ A Velocious backend normally runs Beacon, `background-jobs-main`, a
4
+ `background-jobs-worker` pool, and the web/API server. For deploy lifecycle
5
+ purposes, jobs-main and its workers are one release-scoped **jobs generation**.
6
+ This topology is required; a single persistent fixed-port jobs-main is not a
7
+ safe coordinator for workers that may drain across releases.
8
8
 
9
- A production version of this config lives at
10
- [`examples/tensorbuzz.com.js`](../examples/tensorbuzz.com.js).
9
+ This page states the architecture contract. It does not by itself assert that a
10
+ particular Rollbridge release or consumer production config implements durable
11
+ retired-generation recovery; verify source and config before claiming compliance.
11
12
 
12
13
  ## Process mapping
13
14
 
14
- | Velocious process | Policy | Why |
15
+ | Velocious process | Rollbridge policy | Lifecycle |
15
16
  | --- | --- | --- |
16
- | `beacon` | `service` | A shared broker the other processes connect to. It should survive deploys and keep a **stable port**, so workers and the web process always reach the same Beacon. |
17
- | `background-jobs-main` | `service` with `deployStrategy: "handoff"` | The job coordinator. Run it as a handoff service so each release's workers and web process use a same-release coordinator while old releases drain (see [Choosing the jobs-main policy](#choosing-the-jobs-main-policy)). |
18
- | `background-jobs-worker` | `companion` | Release-scoped: one set of workers per active release, started before the web process and running that release's code. |
19
- | `web` | `proxied` | Receives external HTTP/WebSocket traffic, is health-checked before traffic switches, and is drained on the next deploy. Exactly one process is `proxied`. |
20
-
21
- See [README → Process Policies](../README.md#process-policies) for the full
22
- semantics of each policy and [`docs/config.md`](config.md) for every field.
17
+ | `beacon` | persistent `service` | Shared broker; it may remain daemon-wide on fixed port `7330`. |
18
+ | `background-jobs-main` | `service` with `deployStrategy: "handoff"` | One endpoint per release; owns worker connections, lease fencing, report acceptance/acknowledgement, and durable store transitions. |
19
+ | `background-jobs-worker` | `companion` with `nonBlockingDrain: true` | Release-scoped pool; executes accepted work, owns child runners and execution timeouts, and durably retries terminal reports while tracking their promises. |
20
+ | `web` | `proxied` | Health-gated active HTTP/WebSocket target with a per-release port. |
23
21
 
24
22
  ## Example `rollbridge.js`
25
23
 
26
24
  ```js
27
- // rollbridge.js
28
25
  export default {
29
26
  application: "tensorbuzz",
30
27
  control: {path: "/tmp/rollbridge-tensorbuzz.sock"},
28
+ statePath: "/var/lib/rollbridge/tensorbuzz.json",
31
29
 
32
30
  proxy: {
33
31
  host: "127.0.0.1",
34
- port: 4500, // the stable port Nginx points at
32
+ port: 4500,
35
33
  healthPath: "/ping",
36
34
  healthTimeoutMs: 30000,
37
35
  drainTimeoutMs: 60000,
@@ -39,7 +37,6 @@ export default {
39
37
  },
40
38
 
41
39
  processes: [
42
- // Shared broker — one daemon-wide instance on a stable port.
43
40
  {
44
41
  id: "beacon",
45
42
  policy: "service",
@@ -48,8 +45,6 @@ export default {
48
45
  command: "npx velocious beacon",
49
46
  port: 7330
50
47
  },
51
-
52
- // Job coordinator — one release-scoped service instance per deploy.
53
48
  {
54
49
  id: "background-jobs-main",
55
50
  policy: "service",
@@ -63,23 +58,20 @@ export default {
63
58
  command: "wait-for-it 127.0.0.1:{{ports.beacon}} --strict -- npx velocious background-jobs-main",
64
59
  port: {from: 7331, to: 7399}
65
60
  },
66
-
67
- // Workers — one set per release; raise gracefulStopMs to let in-flight
68
- // jobs finish during a deploy.
69
61
  {
70
62
  id: "background-jobs-worker",
71
63
  policy: "companion",
64
+ nonBlockingDrain: true,
72
65
  cwd: "{{releasePath}}/backend",
73
66
  env: {
74
67
  NODE_ENV: "production",
75
68
  VELOCIOUS_BEACON_PORT: "{{ports.beacon}}",
76
69
  VELOCIOUS_BACKGROUND_JOBS_PORT: "{{ports.background-jobs-main}}"
77
70
  },
78
- command: "wait-for-it 127.0.0.1:{{ports.beacon}} --strict -- wait-for-it 127.0.0.1:{{ports.background-jobs-main}} --strict -- npx velocious background-jobs-worker",
71
+ command: "wait-for-it 127.0.0.1:{{ports.background-jobs-main}} --strict -- npx velocious background-jobs-worker",
72
+ replicas: 4,
79
73
  gracefulStopMs: "indefinite"
80
74
  },
81
-
82
- // Web/API — the one proxied process.
83
75
  {
84
76
  id: "web",
85
77
  policy: "proxied",
@@ -89,7 +81,7 @@ export default {
89
81
  VELOCIOUS_BEACON_PORT: "{{ports.beacon}}",
90
82
  VELOCIOUS_BACKGROUND_JOBS_PORT: "{{ports.background-jobs-main}}"
91
83
  },
92
- command: "wait-for-it 127.0.0.1:{{ports.beacon}} --strict -- wait-for-it 127.0.0.1:{{ports.background-jobs-main}} --strict -- npx velocious server --host 127.0.0.1 --port {{port}}",
84
+ command: "wait-for-it 127.0.0.1:{{ports.background-jobs-main}} --strict -- npx velocious server --host 127.0.0.1 --port {{port}}",
93
85
  port: {from: 14500, to: 14599},
94
86
  health: {path: "/ping", timeoutMs: 30000, intervalMs: 500}
95
87
  }
@@ -97,146 +89,77 @@ export default {
97
89
  }
98
90
  ```
99
91
 
100
- ## Wiring processes together
101
-
102
- Beacon gets a **fixed** port (`7330`) because it is a persistent `service` — a
103
- stable port lets every release's processes find the shared broker.
104
- `background-jobs-main` gets a **range** (`{from: 7331, to: 7399}`) because it is a
105
- handoff service: Rollbridge allocates a new port per release so old workers keep
106
- talking to the old coordinator while new workers and web use the new one. The
107
- proxied `web` process also gets a **range** (`{from: 14500, to: 14599}`) so old
108
- and new web releases can run side by side during the drain.
109
-
110
- Cross-reference ports with `{{ports.<id>}}` and pass them to Velocious through
111
- `env`. Rollbridge also injects `ROLLBRIDGE_<ID>_PORT` for every process (e.g.
112
- `ROLLBRIDGE_BACKGROUND_JOBS_MAIN_PORT`), so you can read ports from the
113
- environment instead of templating if you prefer see
114
- [`docs/config.md`](config.md#injected-environment-variables).
115
-
116
- ### Startup ordering
117
-
118
- Only the `proxied` process is health-checked, so dependent processes must wait
119
- for their dependencies themselves. Two mechanisms combine:
120
-
121
- 1. **Policy ordering.** On each deploy Rollbridge starts handoff `service`s
122
- first, then the release's `companion`s, then the `proxied` process (see
123
- [README Deploy ordering](../README.md#deploy-ordering)).
124
- 2. **Readiness gating.** `wait-for-it 127.0.0.1:{{ports.beacon}} --strict -- …`
125
- blocks the command until Beacon's port accepts connections, so
126
- `background-jobs-main`, the worker, and `web` don't start talking to Beacon
127
- before it is listening. `wait-for-it` is a small standalone script (install it
128
- on the host); any equivalent port-wait works.
129
-
130
- ## Deploying
131
-
132
- Drive deploys through the Rollbridge CLI Rollbridge ships no deploy-tool
133
- plugins (see [`docs/deploy-recipes.md`](deploy-recipes.md) for shell/CI/Capistrano
134
- recipes). The minimal step after a release directory is prepared:
135
-
136
- ```bash
137
- release_path=/srv/tensorbuzz/releases/20260523120000 # prepared by your pipeline
138
-
139
- # Run backwards-compatible migrations BEFORE switching traffic: the old and new
140
- # web releases overlap during the drain.
141
- (cd "$release_path/backend" && npx velocious db:migrate)
142
-
143
- rollbridge deploy \
144
- --ensure-daemon \
145
- --config /etc/rollbridge/rollbridge.js \
146
- --release-path "$release_path" \
147
- --revision "$(git -C "$release_path/backend" rev-parse HEAD)"
148
- ```
149
-
150
- `rollbridge deploy` starts the new release's worker and web process,
151
- health-checks `web` on its `{{port}}`/`/ping`, switches traffic, then drains and
152
- stops the previous release. It exits non-zero (leaving the previous release
153
- active) if the new release fails to start or health-check, so a failed deploy
154
- never promotes a broken release.
155
-
156
- ## Background jobs across a deploy
157
-
158
- The worker is a `companion`, so each release runs its own workers:
159
-
160
- - On deploy, the **new** release's workers start (running the new code) before
161
- traffic switches; the **old** release's workers are stopped when that release
162
- is drained and retired — the worker's `stopSignal`, then `SIGKILL` after
163
- `gracefulStopMs`.
164
- - Set `stopSignal` to the signal your worker drains on and `gracefulStopMs` to at
165
- least your longest in-flight job. Use `gracefulStopMs: "indefinite"` when the
166
- worker can safely drain until it exits on its own. Set `replicas` to run a pool
167
- of workers.
168
-
169
- See [`docs/workers.md`](workers.md) for the full safe background-job deployment
170
- pattern (companion + `replicas` + `stopSignal`/`lifecycle` hooks +
171
- `gracefulStopMs`), the old/new worker overlap, and `nonBlockingDrain` to start the
172
- old workers' drain immediately when a release is retired.
173
-
174
- ### Worker recipe
175
-
176
- A complete `background-jobs-worker` entry that runs a pool and finishes in-flight
177
- jobs across a deploy:
178
-
179
- ```js
180
- {
181
- id: "background-jobs-worker",
182
- policy: "companion",
183
- cwd: "{{releasePath}}/backend",
184
- env: {
185
- NODE_ENV: "production",
186
- VELOCIOUS_ENV: "production",
187
- VELOCIOUS_BEACON_PORT: "{{ports.beacon}}",
188
- VELOCIOUS_BACKGROUND_JOBS_PORT: "{{ports.background-jobs-main}}"
189
- },
190
- command: "wait-for-it 127.0.0.1:{{ports.beacon}} --strict -- wait-for-it 127.0.0.1:{{ports.background-jobs-main}} --strict -- npx velocious background-jobs-worker",
191
- replicas: 4,
192
- gracefulStopMs: "indefinite"
193
- }
194
- ```
195
-
196
- - `replicas: 4` runs four worker instances (`background-jobs-worker#0` … `#3`),
197
- each with `ROLLBRIDGE_REPLICA_INDEX`/`ROLLBRIDGE_REPLICA_COUNT` if you shard work.
198
- - On deploy the new release's workers start before traffic switches; the old
199
- release's workers receive `SIGTERM` (the default `stopSignal`) when the old
200
- release is retired, then wait to exit. With `gracefulStopMs: "indefinite"`,
201
- Rollbridge does not send a `SIGKILL` fallback.
202
-
203
- If your worker quiesces on a command or a non-default signal, add a `lifecycle`
204
- block — Rollbridge runs `quietCommand`, drains for up to `drainTimeoutMs`, then
205
- stops. For example, send a quiet signal to the worker's process group before the
206
- drain:
207
-
208
- ```js
209
- lifecycle: {quietCommand: "kill -TSTP -$ROLLBRIDGE_PID", drainTimeoutMs: 60000}
210
- ```
211
-
212
- ### Choosing the jobs-main policy
213
-
214
- `background-jobs-main` coordinates workers, so choose its lifecycle deliberately:
215
-
216
- - **`service` with `deployStrategy: "handoff"`** — starts one coordinator per
217
- release on a port from a range. New workers and web get the new release's port;
218
- old workers keep the old release's port while they drain. This is the safest
219
- default when the coordinator should run the same code version as its workers.
220
- - **`service` with the default `deployStrategy: "persistent"`** — keeps one
221
- daemon-wide coordinator on a stable port. Workers from every release talk to the
222
- same coordinator, but it keeps running the release it was started from and only
223
- adopts the latest template if it restarts later.
224
- - **`singleton`** — stops the old instance and then starts the new one on each
225
- deploy, so it always runs the latest release's code and two copies never
226
- overlap. The trade-off: a brief coordination gap while it restarts.
227
-
228
- Beacon is a broker rather than code that changes per release, so `service` is
229
- almost always right for it.
230
-
231
- ## Verifying
232
-
233
- After a deploy, `rollbridge status` should show `beacon` as a long-lived service
234
- with an unchanged port, `background-jobs-main` as the active release's handoff
235
- service, one `background-jobs-worker` for the active release, and the `web`
236
- process `proxied` with its connection counts. Use
237
- [`rollbridge logs --process <id>`](cli.md) to read recent output from any
238
- process, and [`docs/troubleshooting.md`](troubleshooting.md) for health-check,
239
- port, and draining problems.
240
-
241
- For the front end, point Nginx at the stable `proxy.port` (here `4500`), never at
242
- a release's web port — see [`docs/nginx.md`](nginx.md).
92
+ Beacon keeps its fixed port because it is intentionally shared. Jobs-main uses a
93
+ range because every release gets its own coordinator. Same-release
94
+ `{{ports.background-jobs-main}}` expansion ensures that old workers retain the
95
+ old endpoint while candidate workers use the candidate endpoint.
96
+
97
+ ## Deploy and activation
98
+
99
+ Run backwards-compatible migrations before activation, then invoke
100
+ `rollbridge deploy` with the prepared release. Rollbridge starts the candidate
101
+ jobs-main, its complete worker pool, and the web process before health gating and
102
+ activation. A candidate startup or health failure leaves the previous release
103
+ active.
104
+
105
+ After successful activation, the deploy returns without waiting for any retired
106
+ generation or HTTP/WebSocket connection to finish. The old and new release code
107
+ may therefore overlap for hours. Keep schema, queue payloads, and external side
108
+ effects compatible across that window.
109
+
110
+ ## Retired jobs-generation contract
111
+
112
+ Retire jobs-main and its workers as one unit:
113
+
114
+ - jobs-main relinquishes recurring schedule ownership and stops dispatching
115
+ queued work or making new worker handoffs;
116
+ - workers stop advertising or accepting new handoffs;
117
+ - jobs-main remains running on the old endpoint and owns worker connections and
118
+ heartbeats, lease fencing, terminal-report acceptance and acknowledgement, and
119
+ durable store transitions for its accepted handoffs;
120
+ - the old worker/reporting side durably retries terminal reports, tracks
121
+ outstanding report promises, enforces per-job execution timeouts, and owns and
122
+ reaps child runners;
123
+ - a job returned or retried to the shared queue becomes eligible for the new
124
+ active generation, and the retired main never dispatches it again;
125
+ - old workers never reconnect to or transfer their handoffs to the new jobs-main;
126
+ - old main and workers remain one release generation until all accepted work
127
+ settles; jobs-main exits only after that and after all workers drain and exit,
128
+ after which Rollbridge may reap the generation.
129
+
130
+ HTTP/WebSocket and jobs drains are independent. `proxy.drainTimeoutMs` bounds the
131
+ connection drain only; reaching it must not stop a still-draining jobs generation.
132
+ `nonBlockingDrain: true` starts worker quiescence at retirement rather than after
133
+ the HTTP drain.
134
+
135
+ The process supervisor must retain multiple old generations concurrently,
136
+ persist their ownership across later deploys and supervisor/host recovery, and
137
+ report every referenced release directory so Rampway can pin it against cleanup.
138
+ A runtime owner/version handoff preserves or transfers that supervision and
139
+ returns after the replacement is healthy; it is not a full synchronous shutdown.
140
+
141
+ These are target requirements. Current Rollbridge drains releases
142
+ asynchronously only while the same daemon remains alive, cannot re-adopt
143
+ surviving PIDs after restart, and stops rather than transfers all managed
144
+ processes during `--takeover-owner`; see [`docs/cli.md`](cli.md#daemon).
145
+
146
+ ## Timeouts
147
+
148
+ Velocious per-job timeouts remain responsible for genuinely hung work. Rollbridge
149
+ stop signals, lifecycle hooks, and graceful-stop bounds remain emergency/process
150
+ controls. Do not use a short normal worker-shutdown timeout to make deployment
151
+ complete: deployment is already complete after healthy activation, and a
152
+ legitimate hours-long job makes an hours-long generation drain valid.
153
+
154
+ ## Verification
155
+
156
+ After a deploy, status must be able to show the active generation and every
157
+ retired generation still draining, including each jobs-main endpoint, worker
158
+ pool, release path, and retention reference. Beacon may keep `7330`; active and
159
+ retired jobs-main instances must use different ports. Confirm that the deploy
160
+ command has returned even while retained generations remain and that an HTTP
161
+ drain timeout does not terminate them.
162
+
163
+ See [`docs/workers.md`](workers.md) for the focused lifecycle,
164
+ [`docs/config.md`](config.md) for configuration fields, and
165
+ [`docs/tensorbuzz-runbook.md`](tensorbuzz-runbook.md) for the consumer runbook.
package/docs/workers.md CHANGED
@@ -1,117 +1,95 @@
1
- # Background-job worker deployment
1
+ # Background-job generation deployment
2
2
 
3
- This guide covers deploying background-job workers (or any non-HTTP worker pool)
4
- with Rollbridge so that in-flight jobs finish across a deploy. It uses features
5
- that exist today; the command-based lifecycle hooks mentioned at the end are
6
- still on the roadmap.
3
+ This is the required Rollbridge lifecycle for a background-jobs runtime. A
4
+ generation is release-scoped and contains its own `background-jobs-main` plus
5
+ its worker pool. It is not just a set of workers attached to one persistent
6
+ coordinator.
7
7
 
8
- ## Run workers as a `companion`
8
+ ## Process topology
9
9
 
10
- Give each worker the `companion` policy. Companions are **release-scoped**: every
11
- release starts its own workers running that release's code, and a release's
12
- workers are stopped only when that release is retired (drained) after a newer
13
- release takes over. They start **before** the `proxied` web process, so they're
14
- ready before traffic switches.
10
+ Configure jobs-main as a handoff `service` on a multi-port range and the workers
11
+ as same-release `companion`s. Beacon may remain a shared persistent service on a
12
+ fixed port such as `7330`.
15
13
 
16
14
  ```js
17
15
  {
18
- id: "worker",
19
- policy: "companion",
20
- cwd: "{{releasePath}}",
21
- command: "npx velocious background-jobs-worker"
22
- }
23
- ```
24
-
25
- ## Scale the pool with `replicas`
26
-
27
- Set `replicas` to run several identical workers (a port-less companion only).
28
- Each instance runs as `worker#0`, `worker#1`, … and gets
29
- `ROLLBRIDGE_REPLICA_INDEX` / `ROLLBRIDGE_REPLICA_COUNT` (and `{{replicaIndex}}` /
30
- `{{replicaCount}}`), so an instance can claim a distinct shard, queue, or lock:
31
-
32
- ```js
33
- {id: "worker", policy: "companion", command: "npx velocious background-jobs-worker", replicas: 4}
34
- ```
35
-
36
- Restart the pool with `rollbridge restart --process worker` (all replicas) or a
37
- single instance with `rollbridge restart --process worker#0`.
38
-
39
- ## Finish in-flight jobs on stop (`stopSignal` + `gracefulStopMs`)
40
-
41
- When Rollbridge stops a worker — during a deploy's drain, a `rollbridge restart`,
42
- or shutdown — it sends the worker's **`stopSignal`** (default `SIGTERM`), waits up
43
- to **`gracefulStopMs`**, then `SIGKILL`s it if it hasn't exited. That window is
44
- the worker's chance to finish its current job and exit cleanly.
45
-
46
- - Set `stopSignal` to the signal your worker quiets/drains on. Many job runners
47
- finish the current job and exit on `SIGTERM` (the default); some use `SIGINT`
48
- or `SIGQUIT`. Use the one your worker treats as "drain and exit".
49
- - Set `gracefulStopMs` to at least your longest job's duration, so a job in
50
- progress is not cut off by the `SIGKILL` fallback. Use `"indefinite"` only for
51
- workers that are safe to leave draining until they exit on their own.
52
-
53
- ```js
16
+ id: "background-jobs-main",
17
+ policy: "service",
18
+ deployStrategy: "handoff",
19
+ command: "npx velocious background-jobs-main",
20
+ port: {from: 7331, to: 7399}
21
+ },
54
22
  {
55
- id: "worker",
23
+ id: "background-jobs-worker",
56
24
  policy: "companion",
25
+ env: {VELOCIOUS_BACKGROUND_JOBS_PORT: "{{ports.background-jobs-main}}"},
57
26
  command: "npx velocious background-jobs-worker",
58
27
  replicas: 4,
59
- stopSignal: "SIGTERM",
28
+ nonBlockingDrain: true,
60
29
  gracefulStopMs: "indefinite"
61
30
  }
62
31
  ```
63
32
 
64
- ## What happens across a deploy
65
-
66
- 1. The new release's workers start (running the **new** code) before traffic
67
- switches to the new web process.
68
- 2. Both old and new workers run while the previous release drains, so **both
69
- code versions consume the shared queue at once.** Keep job code
70
- backwards-compatible across a deploy — the same rule as database migrations.
71
- 3. When the previous release is retired (its HTTP/WebSocket connections close or
72
- `proxy.drainTimeoutMs` elapses), its workers are stopped: `stopSignal`, then
73
- `SIGKILL` after `gracefulStopMs`.
74
-
75
- Because old workers are retired on the release's **connection** drain (not on
76
- their own job queue draining), a job still running when the release is retired
77
- gets only the `gracefulStopMs` window to finish, unless `gracefulStopMs` is
78
- `"indefinite"`. Keep jobs **idempotent and safe to retry** so a job interrupted
79
- at a finite `SIGKILL` fallback can run again.
80
-
81
- ## Command-based lifecycle hooks
82
-
83
- For workers that quiesce or drain via a command rather than a single signal, set
84
- a `lifecycle` block. When Rollbridge gracefully stops the worker it runs
85
- `quietCommand` (stop accepting new work), then drains (`drainCommand`, or waits up
86
- to `drainTimeoutMs` for the worker to exit), then `stopCommand` or `stopSignal`,
87
- then `SIGKILL` after `gracefulStopMs`. Each hook gets `ROLLBRIDGE_PID` and is
88
- bounded by a timeout, so a slow hook can't wedge a deploy.
89
-
90
- ```js
91
- {
92
- id: "worker",
93
- policy: "companion",
94
- command: "npx velocious background-jobs-worker",
95
- replicas: 4,
96
- lifecycle: {quietCommand: "kill -TSTP -$ROLLBRIDGE_PID", drainTimeoutMs: 60000}
97
- }
98
- ```
99
-
100
- See [`docs/config.md`](config.md#processeslifecycle) for the hook reference.
101
-
102
- ## Non-blocking drain
103
-
104
- By default a retired release's workers are stopped only after the proxied
105
- process's connections have drained. Set `nonBlockingDrain: true` on a worker
106
- companion whose work is independent of the web process (a job worker on a shared
107
- queue) to start its graceful stop **immediately** when the release is retired
108
- in parallel with the connection drain. The new release's workers handle new work
109
- while the old workers finish their in-flight jobs:
110
-
111
- ```js
112
- {id: "worker", policy: "companion", command: "…", nonBlockingDrain: true, gracefulStopMs: "indefinite"}
113
- ```
114
-
115
- See [`docs/config.md`](config.md) for `stopSignal`, `replicas`, and
116
- `gracefulStopMs`, and [`docs/velocious.md`](velocious.md) for a full Velocious
117
- deployment (Beacon, jobs-main, workers, web) example.
33
+ Each worker receives its generation's jobs-main port. Old workers keep that port
34
+ for their entire lifetime; normal deploy draining never hands them to, or lets
35
+ them reconnect to, the new jobs-main. `replicas` scales the pool as
36
+ `background-jobs-worker#0`, `#1`, and so on.
37
+
38
+ ## Deploy and retirement sequence
39
+
40
+ 1. Before activation, Rollbridge starts the candidate release's jobs-main and
41
+ complete worker pool, then starts and health-checks the candidate web process.
42
+ 2. Activation switches new web traffic and makes the candidate jobs generation
43
+ active.
44
+ 3. The previous jobs generation retires as one unit. Its jobs-main stops schedule
45
+ ownership, new queue dispatch, and new ordinary worker handoffs. Its workers
46
+ stop accepting handoffs.
47
+ 4. The old jobs-main stays running with its old workers. It continues owning
48
+ their connections and heartbeats, lease fencing, terminal-report acceptance
49
+ and acknowledgement, and durable store transitions. The old worker/reporting
50
+ side durably retries terminal reports, tracks outstanding report promises,
51
+ enforces per-job execution timeouts, and owns and reaps child runners.
52
+ 5. Work returned or retried to the shared queue becomes eligible for the new
53
+ active generation. The retired main never dispatches it again.
54
+ 6. The old main and workers remain one release generation until every accepted
55
+ handoff settles. Only then, after every old worker drains and exits, may the
56
+ old jobs-main exit. Rollbridge then reaps the generation and reports that its
57
+ release reference ended so Rampway can release the retention pin.
58
+
59
+ Old and new generations may overlap for hours, each running its own release code
60
+ and jobs-main endpoint. Multiple retired generations may drain concurrently.
61
+
62
+ ## Deploy completion is independent
63
+
64
+ The deploy succeeds when the candidate release is activated and healthy. The
65
+ command and deploy lock do not wait for old jobs generations, workers, jobs,
66
+ HTTP/WebSocket connections, or other retained services to finish. The required
67
+ supervisor contract durably retains generations after the command returns and
68
+ across later deploys and supervisor/host recovery. Every referenced release
69
+ directory must be reported to Rampway and stays pinned against cleanup until the
70
+ last retained process exits. Current Rollbridge provides asynchronous draining
71
+ only for the lifetime of the current daemon; see the current-behavior notes in
72
+ [`docs/config.md`](config.md#processesdeploystrategy) and
73
+ [`docs/cli.md`](cli.md#deploy).
74
+
75
+ HTTP/WebSocket drain and jobs drain are independent. Set `nonBlockingDrain: true`
76
+ so workers stop accepting new handoffs when retirement starts rather than after
77
+ the connection drain. Closing or timing out HTTP connections must never kill a
78
+ still-draining jobs-main or worker pool.
79
+
80
+ ## Timeouts and failures
81
+
82
+ `stopSignal`, `lifecycle`, and `gracefulStopMs` remain useful process-stop tools,
83
+ and the jobs framework should enforce per-job timeouts for genuinely hung work.
84
+ They are not the primary deployment solution. A legitimate multi-hour job makes
85
+ a multi-hour generation drain valid; do not turn a normal worker-shutdown timeout
86
+ into the deploy deadline.
87
+
88
+ If a worker connection is actually lost, its old jobs-main applies lease fencing
89
+ and the durable store transitions that make work eligible to return or retry.
90
+ Returned work may then run in the active generation. Normal retirement does not
91
+ simulate a disconnect and does not make a new jobs-main adopt the old worker's
92
+ handoffs.
93
+
94
+ See [`docs/velocious.md`](velocious.md) for the complete topology and
95
+ [`docs/config.md`](config.md#processesdeploystrategy) for handoff-service fields.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rollbridge",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Zero-downtime process supervisor and local traffic switcher for deploy-managed apps.",
5
5
  "keywords": [
6
6
  "deploy",