@kedem/okdb 1.8.17 → 1.9.0
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/bin/okdb.js +1 -1
- package/docs/change-log.md +16 -16
- package/docs/embeddings.md +1 -1
- package/docs/functions.md +26 -25
- package/docs/http-cluster.md +157 -0
- package/docs/index.md +1 -0
- package/docs/manifest.json +5 -1
- package/docs/process-registry.md +185 -0
- package/docs/processors.md +61 -65
- package/docs/queue.md +23 -13
- package/docs/subscriptions.md +230 -0
- package/docs/worker-fleet.md +139 -0
- package/okdb-functions-runner-child.js +1 -1
- package/okdb-functions-sandbox-worker.js +1 -0
- package/okdb-http-worker-child.js +1 -0
- package/okdb-queue-load-handler.js +1 -0
- package/okdb-queue-spawn-child.js +1 -1
- package/okdb-views-bootstrap-worker.js +1 -1
- package/okdb-worker-child.js +1 -0
- package/okdb.js +1 -1
- package/package.json +1 -1
- package/public/sections/db/parts/db-overview.ok.js +1 -1
- package/public/sections/embeddings/parts/pipeline-create-panel.ok.js +1 -1
- package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
- package/public/sections/system/index.ok.html +1 -1
- package/public/sections/system/modals/data-ops-modal.ok.js +1 -1
- package/public/sections/system/parts/system-process-panel.ok.js +1 -0
- package/public/sections/system/parts/system-processing-panel.ok.js +1 -0
- package/public/sections/system/parts/system-runtime-overview.ok.js +1 -0
- package/public/sections/system/parts/system-workers-panel.ok.js +1 -0
- package/types/environment.d.ts +21 -4
- package/types/features/embeddings.d.ts +11 -2
- package/types/features/queue.d.ts +27 -5
- package/types/features/views.d.ts +10 -10
- package/types/index.d.ts +54 -6
- package/types/options.d.ts +12 -0
- package/okdb-fts-handler.js +0 -1
- package/okdb-processor-worker.js +0 -1
- package/public/sections/processors/index.ok.html +0 -1
package/docs/change-log.md
CHANGED
|
@@ -115,22 +115,22 @@ stop();
|
|
|
115
115
|
|
|
116
116
|
### Register options reference
|
|
117
117
|
|
|
118
|
-
| Option | Type | Default
|
|
119
|
-
| -------------------- | ---------- |
|
|
120
|
-
| `handler` | `function` | required
|
|
121
|
-
| `mode` | `string` | `'
|
|
122
|
-
| `bootstrap` | `string` | `'log'`
|
|
123
|
-
| `originMode` | `string` | `'self'`
|
|
124
|
-
| `batchSize` | `number` | `256`
|
|
125
|
-
| `hydrateValues` | `boolean` | `true`
|
|
126
|
-
| `cursorKey` | `string` | `null`
|
|
127
|
-
| `lockMode` | `string` | `null`
|
|
128
|
-
| `failOnHandlerError` | `boolean` | `false`
|
|
129
|
-
| `flushDebounce` | `number` | `0`
|
|
130
|
-
| `flushInterval` | `number` | `null`
|
|
131
|
-
| `name` | `string` | `null`
|
|
132
|
-
| `meta` | `object` | `{}`
|
|
133
|
-
| `leaseTtlMs` | `number` | `30000`
|
|
118
|
+
| Option | Type | Default | Description |
|
|
119
|
+
| -------------------- | ---------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `handler` | `function` | required | Async callback receiving `(changes[], info)` |
|
|
121
|
+
| `mode` | `string` | `'fanout'` | `'fanout'` (N-of-N) \| `'single'` (1-of-N) \| `'inline'` (every writer). Old names `'async'`→`'fanout'` and `'worker'`→`'single'` are accepted as deprecated aliases. |
|
|
122
|
+
| `bootstrap` | `string` | `'log'` | `'snapshot'` \| `'log'` — how the initial backfill runs |
|
|
123
|
+
| `originMode` | `string` | `'self'` | `'all'` \| `'self'` \| `'remote'` |
|
|
124
|
+
| `batchSize` | `number` | `256` | Max changes delivered per handler call |
|
|
125
|
+
| `hydrateValues` | `boolean` | `true` | Attach current document value to each change |
|
|
126
|
+
| `cursorKey` | `string` | `null` | Persist the cursor under this key so progress survives restarts |
|
|
127
|
+
| `lockMode` | `string` | `null` | `'exclusive'` — serialise concurrent flushes for this processor |
|
|
128
|
+
| `failOnHandlerError` | `boolean` | `false` | Put the processor into error state on handler exception |
|
|
129
|
+
| `flushDebounce` | `number` | `0` | Trailing-edge debounce in ms. Rapid write bursts coalesce into a single flush after `flushDebounce` ms of quiet. `0` = flush immediately (default). |
|
|
130
|
+
| `flushInterval` | `number` | `null` | Start a `setInterval` that ticks `_scheduleFlush` every `flushInterval` ms after bootstrap. Useful for polling-style processors or `originMode: 'remote'` subscriptions where local writes don't trigger `_onAfterCommit`. `null` = disabled. |
|
|
131
|
+
| `name` | `string` | `null` | Human-readable label shown in admin UI and status |
|
|
132
|
+
| `meta` | `object` | `{}` | Arbitrary metadata attached to status objects |
|
|
133
|
+
| `leaseTtlMs` | `number` | `30000` | Lease TTL for cross-process exclusive execution |
|
|
134
134
|
|
|
135
135
|
`stop.status()` (alias `stop.getStatus()`) returns the live processor state including `flushDebounce` and `flushInterval`. `stop.pause()` clears both timers; `stop.resume()` restarts the interval if configured.
|
|
136
136
|
|
package/docs/embeddings.md
CHANGED
|
@@ -237,7 +237,7 @@ await okdb.embeddings.createPipeline('articles', {
|
|
|
237
237
|
|
|
238
238
|
### Queue mode
|
|
239
239
|
|
|
240
|
-
For high-throughput or external embedder services, the indexer enqueues jobs and
|
|
240
|
+
For high-throughput or external embedder services, the indexer enqueues jobs and the worker population claims and processes them concurrently:
|
|
241
241
|
|
|
242
242
|
```javascript
|
|
243
243
|
await okdb.embeddings.createPipeline('articles', {
|
package/docs/functions.md
CHANGED
|
@@ -110,31 +110,32 @@ Use `ctx.log(...)` instead of `console`.
|
|
|
110
110
|
|
|
111
111
|
## Runtime model
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
This makes
|
|
113
|
+
A function invocation is a **durable work unit**, never a closure: `functions.run()` writes a
|
|
114
|
+
`~fn:requests` row and the response comes back as a `~fn:responses` row (the UDP bus carries a
|
|
115
|
+
wake hint so dispatch is ~ms). Because the stored function's script lives in the database, the
|
|
116
|
+
work is fully portable — whoever claims the row can run it. Execution **never blocks the
|
|
117
|
+
caller's main loop**; untrusted user code always runs off-loop.
|
|
118
|
+
|
|
119
|
+
Where it runs depends on whether a [worker population](worker-fleet.md) exists:
|
|
120
|
+
|
|
121
|
+
- **With workers** — a worker claims the request row and executes the user code in its
|
|
122
|
+
**sandbox thread**: a dedicated `worker_thread` with its own okdb instance, heap-capped from
|
|
123
|
+
`runtime.memoryMb` via `resourceLimits`, and `terminate()` as the timeout/wedge watchdog.
|
|
124
|
+
One sandbox per claimer; invocations run concurrently in it, capped. Containment is
|
|
125
|
+
thread-level — a wedged function is `terminate()`d without taking the worker's other claims
|
|
126
|
+
down.
|
|
127
|
+
- **No workers (embedded / scale 0)** — the instance claims its own requests and runs them in a
|
|
128
|
+
**fork pool** of child-process runners (warm between calls, idle-reaped, one run at a time per
|
|
129
|
+
runner). This is the zero-config default and the **memory fence**: a thread heap cap is
|
|
130
|
+
process-fatal on OOM, so only a separate process can contain a memory-hog function
|
|
131
|
+
(`FUNCTION_OOM`) without crashing the host. Embedders who prefer thread-cheapness can opt into
|
|
132
|
+
the sandbox locally; the fork pool can be forced with `OKDB_FN_LEGACY_POOL=1`.
|
|
133
|
+
|
|
134
|
+
The **transport is identical** either way (durable `~fn` rows + bus fast path) — only the
|
|
135
|
+
execution site differs (sandbox thread vs. runner process). Both: scripts are validated and
|
|
136
|
+
compile-checked before storage; timeouts kill/replace the executor; memory is capped; every run
|
|
137
|
+
is written to a run ledger. This makes functions suitable for operational entry points without
|
|
138
|
+
paying spawn cost on every call.
|
|
138
139
|
|
|
139
140
|
---
|
|
140
141
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# HTTP Clustering
|
|
2
|
+
|
|
3
|
+
By default, the `okdb` CLI runs HTTP serving across **multiple worker processes** so that
|
|
4
|
+
request/response latency is never starved by background work (indexing, view rebuilds,
|
|
5
|
+
compaction, async catch-up). The processing tier runs in a single **primary** process; HTTP +
|
|
6
|
+
SSE are served by **N passive worker processes** that share the listen socket.
|
|
7
|
+
|
|
8
|
+
This is a **CLI-only default**. The embedded library (`new OKDB(...).http.listen(port)`) stays
|
|
9
|
+
single-process unless you opt in — see [Embedded clustering](#embedded-clustering).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Process model
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
┌──────────────────────────────────────────────┐
|
|
17
|
+
│ primary (full role — engines, async │
|
|
18
|
+
│ processors, compaction:'active', bus)│
|
|
19
|
+
│ • runs ALL processing / indexing / compaction │
|
|
20
|
+
│ • HTTP-SILENT (never serves requests itself) │
|
|
21
|
+
│ • opens its OKDB BEFORE forking (seeds the │
|
|
22
|
+
│ shared __tokenSecret so workers agree) │
|
|
23
|
+
└───────────────┬──────────────────────────────┘
|
|
24
|
+
│ forks N
|
|
25
|
+
┌───────────────┬───────┴───────┬───────────────┐
|
|
26
|
+
▼ ▼ ▼ ▼
|
|
27
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
28
|
+
│ worker │ │ worker │ ... │ worker │ │ worker │ passive role:
|
|
29
|
+
│ HTTP+SSE│ │ HTTP+SSE│ │ HTTP+SSE│ │ HTTP+SSE│ engines:false
|
|
30
|
+
└─────────┘ └─────────┘ └─────────┘ └─────────┘ asyncProcessors:false
|
|
31
|
+
└───────────────┴── shared listen socket ──┴───────────┘ compaction:'passive'
|
|
32
|
+
bus:true
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- **Primary = processor.** Exactly one process runs the processing pool, async processors, and
|
|
36
|
+
compaction. It is HTTP-silent; it never serves requests. (The role flag is the belt; the
|
|
37
|
+
`OKDBLock` proc-lease is the backstop — no worker ever processes.)
|
|
38
|
+
- **Workers = passive HTTP servers.** Each forked worker opens a **passive** OKDB
|
|
39
|
+
(`engines:false`, `asyncProcessors:false`, `compaction:'passive'`, `bus:true`) and serves
|
|
40
|
+
HTTP + SSE on the shared socket. Workers do real reads/writes — inline index/schema/FK/TTL
|
|
41
|
+
updates run synchronously inside the writer regardless of role — but never run background
|
|
42
|
+
processing. The primary's async processors pick up the changelog via the UDP bus poke.
|
|
43
|
+
- **`workers:1`** (or `--no-cluster`) is byte-identical to the pre-cluster single-process path.
|
|
44
|
+
|
|
45
|
+
Worker writes update indexes correctly; the primary's processors then index FTS / refresh
|
|
46
|
+
views / run the materializer / etc. from the shared changelog.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## The `workers` knob
|
|
51
|
+
|
|
52
|
+
The CLI resolves the worker count from the first source present, highest priority first:
|
|
53
|
+
|
|
54
|
+
| Priority | Source | Notes |
|
|
55
|
+
| -------- | ------------------- | ---------------------------------------------- |
|
|
56
|
+
| 1 | `--no-cluster` | Forces `workers:1` (single-process escape). |
|
|
57
|
+
| 2 | `--workers N` | Explicit flag. |
|
|
58
|
+
| 3 | `OKDB_HTTP_WORKERS` | Environment variable. |
|
|
59
|
+
| 4 | `http.workers` | `.kdbconfig` config file. |
|
|
60
|
+
| 5 | _default_ | `max(1, cores − 1)` where `cores = os.cpus()`. |
|
|
61
|
+
|
|
62
|
+
A non-integer or `< 1` explicit value clamps to `1` with a warning. The default reserves one
|
|
63
|
+
core for the processing primary and serves HTTP from the rest; on a single-core box it collapses
|
|
64
|
+
to one process.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
okdb # default: max(1, cores − 1) HTTP workers
|
|
68
|
+
okdb --workers 4 # 4 workers
|
|
69
|
+
okdb --no-cluster # single process (back-compat)
|
|
70
|
+
OKDB_HTTP_WORKERS=3 okdb # 3 workers
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
// .kdbconfig
|
|
75
|
+
{ "http": { "workers": 4 } }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## shmbuf requirement (hard gate)
|
|
81
|
+
|
|
82
|
+
Multi-process clustering is **only safe with the native `shmbuf` binding**. The per-env Hybrid
|
|
83
|
+
Logical Clock and changelog clock counter live in a **cross-process shared-memory segment**
|
|
84
|
+
(`shmbuf`). Without the native addon, `shmbuf` falls back to an in-process `SharedArrayBuffer`
|
|
85
|
+
that cannot coordinate clocks across forked workers — two processes could mint the same HLC /
|
|
86
|
+
clock value and corrupt causal ordering.
|
|
87
|
+
|
|
88
|
+
Therefore, **if `shmbuf` is in fallback mode at startup, the CLI forces `workers:1`** regardless
|
|
89
|
+
of any flag, env var, or config, and logs a warning. This gate is non-negotiable.
|
|
90
|
+
|
|
91
|
+
Check readiness with `okdb doctor`:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
✓ cluster: ready (7 workers) # native present
|
|
95
|
+
✓ cluster: unavailable (shmbuf native missing) — running single-process # fallback
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## SSE / log coherence boundary
|
|
101
|
+
|
|
102
|
+
The cluster exposes two distinct event domains. Knowing which is which explains what an SSE
|
|
103
|
+
client on a given worker will and won't see:
|
|
104
|
+
|
|
105
|
+
- **Data changes are cluster-wide.** Each worker runs a **change-feed**: it subscribes to the
|
|
106
|
+
UDP bus `SYSTEM_POKE` signal and, per poke, reads the shared changelog (`getChanges`) to
|
|
107
|
+
reconstruct data-change events (item put/remove, index, view, type). A write committed on
|
|
108
|
+
worker A surfaces in worker B's SSE within the poke window. The bus carries the **signal**;
|
|
109
|
+
the **data** is read from shared LMDB — the changelog is the single source of truth.
|
|
110
|
+
- **Processing progress is primary-owned, forwarded over IPC.** Ephemeral processing events
|
|
111
|
+
(view-rebuild progress, FTS/index lifecycle, processor lifecycle) originate only in the
|
|
112
|
+
primary (the sole processor). The primary forwards a curated set to all workers over
|
|
113
|
+
**cluster IPC**, plus a workers / processing status snapshot. Workers serve
|
|
114
|
+
`/api/processing/status` and `/api/cluster/status` from that snapshot.
|
|
115
|
+
- **Worker-local logs stay local.** A log line emitted inside worker A is **not** replicated to
|
|
116
|
+
worker B's SSE. Logs are per-process; only data changes (via the changelog) and primary
|
|
117
|
+
progress (via IPC) are coherent across the cluster.
|
|
118
|
+
|
|
119
|
+
`/api/cluster/status` returns the cluster shape (primary pid, worker pids, uptime).
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Lifecycle
|
|
124
|
+
|
|
125
|
+
- **Worker death → primary respawns** it with bounded backoff (`min(4000, 250·2^failures)` ms)
|
|
126
|
+
and a crash-loop guard (≥ 5 deaths / 10 s per slot stops respawning that slot).
|
|
127
|
+
- **Primary death → workers exit** (no orphan listeners or locks), via `cluster` disconnect plus
|
|
128
|
+
a POSIX `ppid===1` backstop.
|
|
129
|
+
- **Shutdown** drains in-flight requests, then closes envs, with a 3000 ms hard-exit backstop.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Embedded clustering
|
|
134
|
+
|
|
135
|
+
The **library default is single-process** and unchanged. `new OKDB(...).http.listen(port)` with
|
|
136
|
+
no `workers` option returns a plain single-process `http.Server`. To opt an embedded app into
|
|
137
|
+
clustering, pass `workers` (and a passive-worker OKDB factory) to `http.listen`:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
okdb.http.listen(port, {
|
|
141
|
+
workers: 4,
|
|
142
|
+
primaryOkdb: okdb, // full-role processor, opened before listen() (open-before-fork)
|
|
143
|
+
makeOkdb: async () => {
|
|
144
|
+
const w = new OKDB(dbPath, {
|
|
145
|
+
engines: false,
|
|
146
|
+
asyncProcessors: false,
|
|
147
|
+
compaction: 'passive',
|
|
148
|
+
bus: true,
|
|
149
|
+
});
|
|
150
|
+
await w.open();
|
|
151
|
+
return w;
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The same shmbuf requirement applies — only cluster an embedded app on a host with the native
|
|
157
|
+
`shmbuf` binding.
|
package/docs/index.md
CHANGED
|
@@ -108,6 +108,7 @@ await okdb.close();
|
|
|
108
108
|
- [Full-Text Search](./fts.md) — FTS index, tokenizer, query
|
|
109
109
|
- [Embeddings](./embeddings.md) — vector pipeline, semantic search
|
|
110
110
|
- [Queue](./queue.md) — durable job queue
|
|
111
|
+
- [Live Subscriptions](./subscriptions.md) — signal-SSE change streams with durable sessions
|
|
111
112
|
- [Files](./files.md) — blob / file storage
|
|
112
113
|
- [Custom Functions](./functions.md) — sandboxed stored JS functions, env/global scope, HTTP execution
|
|
113
114
|
- [HTTP API](./http-api.md) — built-in REST API
|
package/docs/manifest.json
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
{ "file": "fts.md", "label": "Full-Text Search" },
|
|
19
19
|
{ "file": "embeddings.md", "label": "Embeddings" },
|
|
20
20
|
{ "file": "views.md", "label": "Materialized Views" },
|
|
21
|
+
{ "file": "subscriptions.md", "label": "Live Subscriptions" },
|
|
21
22
|
{ "file": "pipelines.md", "label": "Pipelines" },
|
|
22
23
|
{ "file": "queue.md", "label": "Queue" },
|
|
23
24
|
{ "file": "files.md", "label": "Files" },
|
|
@@ -31,8 +32,11 @@
|
|
|
31
32
|
{ "file": "auth-and-sync.md", "label": "Auth & Sync" },
|
|
32
33
|
{ "file": "auth-permissions.md", "label": "Auth Permissions" },
|
|
33
34
|
{ "file": "http-api.md", "label": "HTTP API" },
|
|
35
|
+
{ "file": "http-cluster.md", "label": "HTTP Clustering" },
|
|
36
|
+
{ "file": "process-registry.md", "label": "Process Registry" },
|
|
34
37
|
{ "file": "plugins.md", "label": "Plugins" },
|
|
35
|
-
{ "file": "processors.md", "label": "Processors (
|
|
38
|
+
{ "file": "processors.md", "label": "Processors (modes & registration)" },
|
|
39
|
+
{ "file": "worker-fleet.md", "label": "Workers (population & placement)" },
|
|
36
40
|
{ "file": "logging.md", "label": "Logging" },
|
|
37
41
|
{ "file": "migration.md", "label": "Migration" }
|
|
38
42
|
]
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Process Registry
|
|
2
|
+
|
|
3
|
+
The process registry is a root-wide **task manager** for every okdb process sharing one data
|
|
4
|
+
directory: the HTTP-cluster primary and its workers, queue/function child processes, a standalone
|
|
5
|
+
CLI server, and opted-in embedded nodes. It gives the admin UI (System → **Processes**) and the HTTP API
|
|
6
|
+
a single, live view of the whole process tree — and a durable, safe way to control it (pause/resume,
|
|
7
|
+
decommission, retry/reset, hard-kill).
|
|
8
|
+
|
|
9
|
+
It generalizes the same **durable-state-as-coordination** model used by live subscriptions: the
|
|
10
|
+
authority for every action is a durable record; the UDP bus only carries a lossy "go re-read it now"
|
|
11
|
+
hint. A forged or dropped bus packet can therefore neither trigger nor lose a control action.
|
|
12
|
+
|
|
13
|
+
> **Opt-in.** The registry is off by default so embedders and tests are unaffected. `bin/okdb` and the
|
|
14
|
+
> HTTP cluster turn it on automatically; a library embedder opts in with `new OKDB(path, { processes: … })`.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The model
|
|
19
|
+
|
|
20
|
+
### A process is a `node`; processors are a facet
|
|
21
|
+
|
|
22
|
+
The primary entity is the **process**, not the processor. Each long-lived process publishes one
|
|
23
|
+
`node` row:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
{
|
|
27
|
+
nodeKey, // identity: `${pid}:${startedAt}` (wall-clock startedAt — same-host, see Liveness)
|
|
28
|
+
kind, // primary | http-server | queue-worker | fn-runner | cli | standalone | embedded
|
|
29
|
+
pid, ppid, host,
|
|
30
|
+
supervisorKey, // nodeKey of the supervising process (null = a root) → builds the tree
|
|
31
|
+
slot, slotKey, // supervised position; slotKey is the STABLE durable control key (see below)
|
|
32
|
+
role, // { engines, asyncProcessors, compaction, http }
|
|
33
|
+
listenAddr,
|
|
34
|
+
caps, // { controllable, decommissionable, killable, killConfirm? } — see Capabilities
|
|
35
|
+
lifecycle, // { state: 'running' | 'decommissioning', desiredVersionApplied }
|
|
36
|
+
processing, // FACET (owners only): { pools[], processors[], cap, policy }
|
|
37
|
+
heartbeatAt, expiresAt,
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A processor (FTS, views, materializer, time-machine, …) is **not** a separate entity — it is summarized
|
|
42
|
+
on the owner's `processing` facet. The same is true of process-backend pool workers: they are real OS
|
|
43
|
+
processes but are summarized on `processing.pools[]`, not given their own rows.
|
|
44
|
+
|
|
45
|
+
### Two stores, split by lifetime
|
|
46
|
+
|
|
47
|
+
| Store | Where | Lifetime / durability | Holds |
|
|
48
|
+
| ------------------------- | --------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------- |
|
|
49
|
+
| **Status** (`~processes`) | a dedicated env (`sync:false`, `durability:'fast'`) | **disposable** — crash-loss _is_ the liveness mechanism | one self-refreshed `node` row per live process |
|
|
50
|
+
| **Desired** (`~system`) | `~processes:desired/*`, `~processes:cmd/*` sub-DBs | **durable** — the authority for every control action | pause/decommission/kill intent + retry/reset epochs |
|
|
51
|
+
|
|
52
|
+
`~processes` is off the change-feed (the `~` env name) and is read by key/range, never tailed. Status
|
|
53
|
+
writes emit **no** signal — the panel polls. Only desired-state/command writes emit a `PROC` hint.
|
|
54
|
+
|
|
55
|
+
### Supervisor-published registration
|
|
56
|
+
|
|
57
|
+
Children (http-servers, queue/fn runners) lack a stable cross-restart identity and some run with the
|
|
58
|
+
bus off, so they do **not** self-register. Their **supervisor** — which already tracks each child's
|
|
59
|
+
pid/slot and heartbeats it — writes the child's `node` row (`publishChildRow`) and removes it on exit.
|
|
60
|
+
Roots (primary/cli/standalone) and opted-in embedded nodes self-publish via their heartbeat.
|
|
61
|
+
|
|
62
|
+
### Liveness — wall-clock, read-time staleness
|
|
63
|
+
|
|
64
|
+
Each writer refreshes its rows every `OKDB_WORKERS_HEARTBEAT_MS` (default 10000); a row's
|
|
65
|
+
`expiresAt = now + 3×heartbeat`. Readers drop any row whose `expiresAt` is in the past
|
|
66
|
+
(`listNodes` filters at read time) — so a crashed publisher simply ages out; nothing has to notice the
|
|
67
|
+
death. Liveness deliberately uses **wall-clock `Date.now()`**, not the HLC: it is a same-host TTL
|
|
68
|
+
deadline, not a causal stamp, and there is no clock skew between processes on one host.
|
|
69
|
+
|
|
70
|
+
### `PROC` — a reconcile hint, never a command
|
|
71
|
+
|
|
72
|
+
A control write emits `BUS_EVENTS.PROC` = `{ kind: 'reconcile', scope, key }` on the UDP bus. The bus
|
|
73
|
+
framing is a shared-secret prefix, **not** an authenticated MAC, so the payload carries no command and
|
|
74
|
+
no authority — only "re-read the durable `~system` record for `scope`/`key` now." The owner applies the
|
|
75
|
+
durable record on the hint (fast path) **and** on its heartbeat (backstop), so a dropped hint
|
|
76
|
+
self-heals within one beat.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## HTTP API
|
|
81
|
+
|
|
82
|
+
All routes require `system:read` (views) or `system:write` (control).
|
|
83
|
+
|
|
84
|
+
> **Naming.** The canonical surface is **`processes`** — `okdb.processes` and
|
|
85
|
+
> `GET /api/processes[/tree]`. `/api/processes*` is the route surface (the former `okdb.fleet` accessor alias was removed). The tables
|
|
86
|
+
> below still use `/api/processes*`; `/api/processes*` is equivalent for the read views.
|
|
87
|
+
|
|
88
|
+
| Method & path | Purpose |
|
|
89
|
+
| ---------------------------------------------------- | -------------------------------------------------------------- |
|
|
90
|
+
| `GET /api/processes` (or `/api/processes`) | flat list of live processes (grouped, each with `instances[]`) |
|
|
91
|
+
| `GET /api/processes/tree` (or `/api/processes/tree`) | nested process → instance → work tree (`supervisorKey`) |
|
|
92
|
+
| `POST /api/processors/:id/pause`/`resume` | durable pause/resume of a processor (desired-state) |
|
|
93
|
+
| `POST /api/processors/:id/retry` | retry an errored processor (durable command epoch) |
|
|
94
|
+
| `POST /api/processors/cursor/reset` | reset a processor cursor to 0 (durable + command epoch) |
|
|
95
|
+
| `POST /api/processes/:nodeKey/decommission` | gracefully stop a supervised process and keep it down |
|
|
96
|
+
| `POST /api/processes/:nodeKey/recommission` | reverse a decommission/kill — the supervisor re-forks the slot |
|
|
97
|
+
| `POST /api/processes/:nodeKey/kill?force=1` | hard-kill (guarded; see Control tiers) |
|
|
98
|
+
|
|
99
|
+
When the registry is off (single-process / embedders), `/api/processes[/tree]` returns a degenerate
|
|
100
|
+
single-root node so the API still answers.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Control tiers
|
|
105
|
+
|
|
106
|
+
All three tiers write a durable record behind HTTP auth, then emit a `PROC` hint. The owner (or the
|
|
107
|
+
target's supervisor) reconciles it.
|
|
108
|
+
|
|
109
|
+
1. **Soft — pause/resume** (`controllable` kinds). Durable `~processes:desired/{proc,pool}/<key>` with a
|
|
110
|
+
monotonic `version`; the owner applies it when `version > applied`. Rapid toggles converge to the
|
|
111
|
+
last version (no flip-flop); concurrent writes from two nodes are last-writer-wins by `version`.
|
|
112
|
+
2. **Imperative — retry/reset** (`~processes:cmd/<logicalKey>`, monotonic `epoch`). The owner re-bootstraps
|
|
113
|
+
the processor via `processor.restart`; `reset` also zeroes the persisted `~proc:state` cursor (so it
|
|
114
|
+
takes effect on the owner's next start even while it is offline). `epoch > applied` guards against
|
|
115
|
+
replay.
|
|
116
|
+
3. **Lifecycle — decommission / hard-kill** (routed through the **supervisor**, not by fighting the
|
|
117
|
+
respawn loop):
|
|
118
|
+
- **Decommission** (`decommissionable` kinds): the supervisor marks the slot dead (no respawn) and
|
|
119
|
+
cleanly `disconnect()`s the child to drain in-flight work. Keyed by the **stable** `slotKey`
|
|
120
|
+
(`cluster:<port>/<slot>`), so it **survives a supervisor restart** — a restarted primary reloads
|
|
121
|
+
the dead slots before its fork loop and does not revive them. **Recommission** clears the record
|
|
122
|
+
and re-forks.
|
|
123
|
+
- **Hard-kill** (`POST …/kill`): the last resort, guarded. Requires explicit `?force=1`. Rejected
|
|
124
|
+
for non-killable kinds. Killing a **primary** additionally requires `?confirm=1` because it tears
|
|
125
|
+
down the whole cluster (its workers self-exit on the primary's death). The supervisor `SIGKILL`s
|
|
126
|
+
its own child; a root reads its own durable kill record and self-terminates.
|
|
127
|
+
|
|
128
|
+
Every decommission / recommission / kill writes a structured **audit** entry to `~log` (feature
|
|
129
|
+
`workers`: actor, target `nodeKey`+`kind`, action, result), surfaced in System → Logs.
|
|
130
|
+
|
|
131
|
+
### Capabilities
|
|
132
|
+
|
|
133
|
+
`caps` is published per kind and **enforced by the routes**:
|
|
134
|
+
|
|
135
|
+
| kind | controllable | decommissionable | killable | kill confirm |
|
|
136
|
+
| -------------------------------------------- | ------------ | ---------------- | -------- | ------------ |
|
|
137
|
+
| `primary` | if owner | no (root) | yes | cluster-wide |
|
|
138
|
+
| `cli` / `standalone` | if owner | no (root) | yes | confirm |
|
|
139
|
+
| `http-server` / `queue-worker` / `fn-runner` | no | yes | yes | force |
|
|
140
|
+
| `embedded` | no | no | **no** | — |
|
|
141
|
+
|
|
142
|
+
Roots have no supervisor to route a decommission through — stop a root via its own shutdown or a
|
|
143
|
+
confirmed kill. `embedded` is **observe-only**: okdb must never `process.exit` a host application, so an
|
|
144
|
+
embedded node is never killable and never self-terminates.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Scope: same-host processes vs sync peers
|
|
149
|
+
|
|
150
|
+
The registry covers **same-host, same-root** processes — they share the LMDB files, the UDP bus, and
|
|
151
|
+
the `EnvSharedState` shmbuf. **Cross-machine sync-replication peers are a separate axis** (no shared
|
|
152
|
+
LMDB; "kill" there would be a sync-protocol RPC) and are surfaced by the Sync view, not the process registry.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Embedding
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
// Observe-only: appear in the registry, expose nothing controllable.
|
|
160
|
+
const db = new OKDB('./data', { processes: { register: true, kind: 'embedded' } });
|
|
161
|
+
|
|
162
|
+
// A library server that wants the full registry (and is the processor owner):
|
|
163
|
+
const db = new OKDB('./data', { processes: { kind: 'standalone', listenAddr: ':8484' } });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`register: true` makes a process publish its own row even when it is not an `asyncProcessors` owner.
|
|
167
|
+
`bin/okdb` sets `kind` automatically (`primary` / `http-server`); embedders pass it explicitly.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Operational notes
|
|
172
|
+
|
|
173
|
+
- **Bus secret & kill safety.** The `PROC` hint is only a nudge, so a weak/default bus secret cannot
|
|
174
|
+
forge a control action — the durable `~system` record (written behind HTTP auth) is the sole
|
|
175
|
+
authority. Still, set `OKDB_BUS_SECRET` in production so unrelated processes don't cross-poke.
|
|
176
|
+
- **Hard-kill kill-switch.** The irreversible hard-kill tier is enabled by default but can be locked
|
|
177
|
+
down — `OKDB_PROCESS_KILL=off` or `new OKDB(path, { processes: { kill: false } })`. **Recommended in
|
|
178
|
+
production.** When off, every node's `caps.killable` is `false`, so the UI hides the Kill button and
|
|
179
|
+
the route rejects it (409). The graceful, reversible decommission/recommission tier is unaffected.
|
|
180
|
+
- **`okdb doctor`** reports a `processes: N (M supervised)` line when a server is already running on the
|
|
181
|
+
data directory (read as a passive reader; skipped otherwise).
|
|
182
|
+
- **Heartbeat tuning.** `OKDB_WORKERS_HEARTBEAT_MS` (default 10000) sets the publish cadence and, ×3, the
|
|
183
|
+
liveness TTL. Lower it for snappier death detection at the cost of more status writes.
|
|
184
|
+
|
|
185
|
+
See also: [HTTP Clustering](http-cluster.md), [Processors](processors.md).
|