@kedem/okdb 1.9.2 → 2.0.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.
Files changed (48) hide show
  1. package/README.md +3 -2
  2. package/bin/okdb.js +1 -1
  3. package/docs/deployment.md +155 -0
  4. package/docs/diagnostics.md +115 -0
  5. package/docs/embeddings.md +1 -1
  6. package/docs/functions.md +30 -27
  7. package/docs/getting-started.md +14 -3
  8. package/docs/http-api.md +3 -3
  9. package/docs/http-cluster.md +93 -60
  10. package/docs/index.md +13 -1
  11. package/docs/manifest.json +6 -3
  12. package/docs/pipelines.md +1 -1
  13. package/docs/process-registry.md +5 -5
  14. package/docs/processors.md +35 -18
  15. package/docs/queue.md +112 -42
  16. package/docs/sync.md +10 -7
  17. package/docs/ttl.md +1 -1
  18. package/docs/upgrade-2.0.md +290 -0
  19. package/okdb-functions-sandbox-worker.js +1 -1
  20. package/okdb-http-worker-child.js +1 -1
  21. package/okdb-views-bootstrap-worker.js +1 -1
  22. package/okdb.js +1 -1
  23. package/package.json +1 -1
  24. package/public/layouts/_default.ok.html +1 -1
  25. package/public/sections/db/modals/create-env-modal.ok.js +1 -1
  26. package/public/sections/db/modals/demo-env-modal.ok.js +1 -1
  27. package/public/sections/db/parts/db-overview.ok.js +1 -1
  28. package/public/sections/engines/engine-ui-utils.js +1 -1
  29. package/public/sections/engines/parts/engine-declaration-editor.ok.js +1 -1
  30. package/public/sections/engines/parts/generic-engine-panel.ok.js +1 -1
  31. package/public/sections/queue/parts/code-panel.ok.js +1 -1
  32. package/public/sections/queue/parts/job-log-panel.ok.js +1 -1
  33. package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
  34. package/public/sections/sync/parts/sync-topology.ok.js +1 -1
  35. package/public/sections/system/index.ok.html +1 -1
  36. package/public/sections/system/parts/system-process-panel.ok.js +1 -1
  37. package/public/sections/system/parts/system-processing-panel.ok.js +1 -1
  38. package/public/sections/system/parts/system-runtime-overview.ok.js +1 -1
  39. package/public/setup-app.js +1 -1
  40. package/public/setup.html +116 -55
  41. package/types/index.d.ts +33 -3
  42. package/types/options.d.ts +19 -10
  43. package/docs/worker-fleet.md +0 -139
  44. package/okdb-functions-runner-child.js +0 -1
  45. package/okdb-queue-load-handler.js +0 -1
  46. package/okdb-queue-spawn-child.js +0 -1
  47. package/okdb-worker-child.js +0 -1
  48. package/public/sections/system/parts/system-workers-panel.ok.js +0 -1
package/public/setup.html CHANGED
@@ -44,6 +44,9 @@
44
44
  --accent2:#16a34a;
45
45
  }
46
46
  * { box-sizing:border-box; }
47
+ /* Author rule for .field/.check sets display, which beats the UA [hidden]
48
+ rule — so make hidden authoritative for everything on this page. */
49
+ [hidden] { display:none !important; }
47
50
  body {
48
51
  margin:0;
49
52
  min-height:100vh;
@@ -59,13 +62,13 @@
59
62
  background: radial-gradient(circle at top, #c8dff5 0%, var(--bg0) 45%, #e8f2ff 100%);
60
63
  }
61
64
  .shell {
62
- width:min(1000px, 100%);
65
+ width:min(440px, 100%);
63
66
  display:flex;
64
67
  flex-direction:column;
65
68
  gap:0;
66
69
  }
67
70
  .header {
68
- margin-bottom:28px;
71
+ margin-bottom:24px;
69
72
  text-align:center;
70
73
  }
71
74
  .eyebrow {
@@ -73,13 +76,8 @@
73
76
  background:rgba(79,193,255,.12); color:var(--accent); font-size:12px; letter-spacing:.08em; text-transform:uppercase;
74
77
  border:1px solid rgba(79,193,255,.18);
75
78
  }
76
- h1 { font-size:32px; line-height:1.1; margin:14px 0 6px; }
79
+ h1 { font-size:30px; line-height:1.1; margin:14px 0 6px; }
77
80
  .subtitle { color:var(--muted); font-size:15px; margin:0; }
78
- .panels {
79
- display:grid;
80
- grid-template-columns:1fr 1fr;
81
- gap:16px;
82
- }
83
81
  .panel {
84
82
  background:rgba(8,16,26,.86);
85
83
  border:1px solid rgba(79,193,255,.16);
@@ -111,6 +109,24 @@
111
109
  background:linear-gradient(135deg, var(--accent2), #a8f0b4);
112
110
  box-shadow:0 10px 30px rgba(126,231,135,.18);
113
111
  }
112
+ /* Secondary, low-emphasis action (e.g. the "Join a cluster" entry point) */
113
+ .button.ghost {
114
+ background:transparent; color:var(--muted); border:1px solid rgba(255,255,255,.12);
115
+ box-shadow:none; font-weight:600; font-size:14px; padding:12px 16px;
116
+ }
117
+ .button.ghost:hover { filter:none; color:var(--text); border-color:rgba(79,193,255,.4); }
118
+ [data-theme='light'] .button.ghost { border-color:var(--line); }
119
+ .divider {
120
+ display:flex; align-items:center; gap:12px; color:var(--muted); font-size:12px;
121
+ text-transform:uppercase; letter-spacing:.08em;
122
+ }
123
+ .divider::before, .divider::after { content:''; flex:1; height:1px; background:rgba(255,255,255,.1); }
124
+ [data-theme='light'] .divider::before, [data-theme='light'] .divider::after { background:var(--line); }
125
+ .back {
126
+ align-self:flex-start; background:none; border:none; cursor:pointer; font-family:inherit;
127
+ color:var(--muted); font-size:13px; padding:0; margin-bottom:10px;
128
+ }
129
+ .back:hover { color:var(--text); }
114
130
  .error {
115
131
  padding:10px 12px; border-radius:10px;
116
132
  background:rgba(248,113,113,.12); border:1px solid rgba(248,113,113,.25); color:#fecaca;
@@ -121,11 +137,24 @@
121
137
  background:rgba(126,231,135,.1); border:1px solid rgba(126,231,135,.25); color:#a8f0b4;
122
138
  font-size:13px;
123
139
  }
124
- @media (max-width:720px) {
125
- .panels { grid-template-columns:1fr; }
126
- }
127
140
  [data-theme='light'] .panel { background: rgba(255,255,255,.92); border-color: rgba(26,122,184,.2); }
128
141
  [data-theme='light'] .field input { background: var(--bg2); border-color: var(--line); }
142
+
143
+ /* Segmented credential toggle */
144
+ .segmented {
145
+ display:flex; border:1px solid rgba(255,255,255,.1); border-radius:12px; overflow:hidden;
146
+ }
147
+ .segmented button {
148
+ flex:1; background:transparent; color:var(--muted); border:none; cursor:pointer;
149
+ font-family:inherit; font-size:13px; font-weight:600; padding:10px 8px; transition:color .15s, background .15s;
150
+ }
151
+ .segmented button + button { border-left:1px solid rgba(255,255,255,.08); }
152
+ .segmented button.seg-active { background:#0b1420; color:var(--text); box-shadow:inset 0 0 0 1px rgba(79,193,255,.35); }
153
+ [data-theme='light'] .segmented { border-color:var(--line); }
154
+ [data-theme='light'] .segmented button.seg-active { background:var(--bg2); }
155
+ .check { display:flex; align-items:flex-start; gap:9px; font-size:13px; color:var(--muted); cursor:pointer; line-height:1.45; }
156
+ .check input { margin-top:2px; cursor:pointer; accent-color:var(--accent2); }
157
+ .hint { font-size:12px; color:var(--muted); line-height:1.5; margin:0; }
129
158
  </style>
130
159
  </head>
131
160
  <body>
@@ -133,57 +162,89 @@
133
162
  <header class="header">
134
163
  <div class="eyebrow">OKDB First-Time Setup</div>
135
164
  <h1>Welcome to OKDB</h1>
136
- <p class="subtitle">This node has no credentials yet. Create an admin token or join an existing cluster.</p>
165
+ <p class="subtitle">This node has no credentials yet. Create an admin to get started.</p>
137
166
  </header>
138
167
 
139
- <div class="panels">
140
- <section class="panel" id="createPanel">
141
- <div>
142
- <p class="panel-title">Create first admin token</p>
143
- <p class="panel-desc">Set up credentials for this node. You'll log in with these after setup.</p>
168
+ <!-- Create admin (default view) -->
169
+ <section class="panel" id="createView">
170
+ <div>
171
+ <p class="panel-title">Create your first admin</p>
172
+ <p class="panel-desc">Set up credentials for this node. You'll log in with these after setup.</p>
173
+ </div>
174
+ <form id="createForm" class="stack">
175
+ <div class="field">
176
+ <label for="createName">Username</label>
177
+ <input id="createName" name="name" value="admin" autocomplete="username" placeholder="admin">
144
178
  </div>
145
- <form id="createForm" class="stack">
146
- <div class="field">
147
- <label for="createName">Username</label>
148
- <input id="createName" name="name" value="admin" autocomplete="username" placeholder="admin">
149
- </div>
150
- <div class="field">
151
- <label for="createPassword">Password</label>
152
- <input id="createPassword" name="password" type="password" autocomplete="new-password" placeholder="••••••••">
153
- </div>
154
- <div class="field">
155
- <label for="createConfirm">Confirm password</label>
156
- <input id="createConfirm" name="confirm" type="password" autocomplete="new-password" placeholder="••••••••">
157
- </div>
158
- <div class="field">
159
- <label for="createLabel">Label <span style="color:var(--muted)">(optional)</span></label>
160
- <input id="createLabel" name="label" value="Initial admin" placeholder="Initial admin">
161
- </div>
162
- <div id="createError" class="error" hidden></div>
163
- <button type="submit" class="button">Create admin and continue</button>
164
- </form>
165
- </section>
179
+ <div class="field">
180
+ <label for="createPassword">Password</label>
181
+ <input id="createPassword" name="password" type="password" autocomplete="new-password" placeholder="••••••••">
182
+ </div>
183
+ <div class="field">
184
+ <label for="createConfirm">Confirm password</label>
185
+ <input id="createConfirm" name="confirm" type="password" autocomplete="new-password" placeholder="••••••••">
186
+ </div>
187
+ <div class="field">
188
+ <label for="createLabel">Label <span style="color:var(--muted)">(optional)</span></label>
189
+ <input id="createLabel" name="label" value="Initial admin" placeholder="Initial admin">
190
+ </div>
191
+ <div id="createError" class="error" hidden></div>
192
+ <button type="submit" class="button">Create admin and continue</button>
193
+ </form>
194
+
195
+ <div class="divider"><span>or</span></div>
196
+ <button type="button" id="showJoin" class="button ghost">Join an existing cluster</button>
197
+ </section>
166
198
 
167
- <section class="panel" id="joinPanel">
168
- <div>
169
- <p class="panel-title">Join an existing cluster</p>
170
- <p class="panel-desc">Connect to a running OKDB node. The cluster admin provides the sync token out-of-band.</p>
199
+ <!-- Join cluster (revealed on demand) -->
200
+ <section class="panel" id="joinView" hidden>
201
+ <div>
202
+ <button type="button" id="backToCreate" class="back">← Back</button>
203
+ <p class="panel-title">Join an existing cluster</p>
204
+ <p class="panel-desc">Connect to a running OKDB node. The credential is used once for the handshake — after that, sync authenticates with Ed25519 keys.</p>
205
+ </div>
206
+ <form id="joinForm" class="stack">
207
+ <div class="field">
208
+ <label for="joinAddress">Remote node address</label>
209
+ <input id="joinAddress" name="address" placeholder="http://node-a.example.com:8484">
171
210
  </div>
172
- <form id="joinForm" class="stack">
173
- <div class="field">
174
- <label for="joinAddress">Remote node address</label>
175
- <input id="joinAddress" name="address" placeholder="https://node-a.example.com:8484">
176
- </div>
177
- <div class="field">
178
- <label for="joinToken">Cluster sync token</label>
179
- <input id="joinToken" name="token" type="password" placeholder="cluster sync token">
211
+
212
+ <div class="field">
213
+ <label>Authentication</label>
214
+ <div class="segmented" id="joinAuthSeg">
215
+ <button type="button" data-method="token" class="seg-active">Token</button>
216
+ <button type="button" data-method="login">Login</button>
217
+ <button type="button" data-method="none">None</button>
180
218
  </div>
181
- <div id="joinError" class="error" hidden></div>
182
- <button type="submit" class="button green">Join cluster</button>
183
- </form>
184
- </section>
185
- </div>
219
+ </div>
220
+
221
+ <div class="field" data-auth="token">
222
+ <label for="joinToken">Cluster sync token</label>
223
+ <input id="joinToken" name="token" type="password" placeholder="Bearer token from the cluster admin">
224
+ </div>
225
+
226
+ <div class="field" data-auth="login" hidden>
227
+ <label for="joinUsername">Username</label>
228
+ <input id="joinUsername" name="username" autocomplete="username" placeholder="Username on the remote node">
229
+ <label for="joinPassword" style="margin-top:6px;">Password</label>
230
+ <input id="joinPassword" name="password" type="password" autocomplete="current-password" placeholder="••••••••">
231
+ </div>
232
+
233
+ <div class="field" data-auth="none" hidden>
234
+ <p class="hint">No credential is sent. This only works when the remote node is running in open mode.</p>
235
+ </div>
236
+
237
+ <label class="check">
238
+ <input type="checkbox" id="joinDataLink" checked>
239
+ <span>Sync data immediately <span style="opacity:.6">— unchecked links topology only</span></span>
240
+ </label>
241
+
242
+ <div id="joinError" class="error" hidden></div>
243
+ <button type="submit" class="button green">Join cluster</button>
244
+ </form>
245
+ </section>
186
246
  </main>
247
+
187
248
  <script type="module" src="setup-app.js"></script>
188
249
  </body>
189
250
  </html>
package/types/index.d.ts CHANGED
@@ -74,7 +74,12 @@ export declare class OKDB {
74
74
  readonly events: EventEmitter;
75
75
 
76
76
  // ── Subsystems ────────────────────────────────────────────────────────────
77
- readonly http: unknown;
77
+ readonly http: {
78
+ /** Start the built-in HTTP server (REST API + admin UI). Single-process only —
79
+ * a `{ workers }` second argument throws HTTP_CLUSTER_REMOVED (okdb 2.0). */
80
+ listen(port: number, host?: string): unknown;
81
+ [key: string]: unknown;
82
+ };
78
83
  readonly auth: OKDBAuth;
79
84
  readonly log: unknown;
80
85
  readonly bus: unknown;
@@ -100,12 +105,37 @@ export declare class OKDB {
100
105
  // ── Lifecycle ─────────────────────────────────────────────────────────────
101
106
  open(): Promise<void>;
102
107
  close(): Promise<void>;
108
+ /** Resolved role flags. `engines`/`compaction` are fixed for the instance's
109
+ * lifetime; `processors` reflects CURRENT participation (see processors.start/stop). */
110
+ readonly role: { readonly processors: boolean; readonly engines: boolean; readonly compaction: boolean };
111
+ /** Composite load signal for this node (cached 250 ms; safe to poll per claim).
112
+ * score ≥ 1 ≈ at the documented distress limits — the standard admission policy is
113
+ * `() => db.pressure().score < 1`. queuePending is workload context, not in the score. */
114
+ pressure(): {
115
+ writerStallMs: number;
116
+ writerDepth: number;
117
+ maxDurableLag: number;
118
+ queuePending: number;
119
+ loopLagMs: number;
120
+ score: number;
121
+ };
122
+ /** Processing participation control + status.
123
+ * start(): begin participating — un-gate lease claiming; the first call also runs
124
+ * the per-store view boot that open() skips on a non-participating node.
125
+ * stop(): cease participating — finish in-flight quanta, release leases (peers
126
+ * fail over), park registrations. Both idempotent; resolve true on state change. */
127
+ readonly processors: {
128
+ start(): Promise<boolean>;
129
+ stop(): Promise<boolean>;
130
+ status(): { participate: boolean; processors: unknown[] };
131
+ [key: string]: unknown;
132
+ };
103
133
 
104
134
  // ── Environment management ────────────────────────────────────────────────
105
135
  readonly default: OKDBEnvironment;
106
136
  env(name?: string): OKDBEnvironment;
107
- /** Open a registered env on demand (companion to the `envs` constructor
108
- * option). Idempotent; throws ENV_NOT_FOUND for an unregistered name. */
137
+ /** Open a registered env on demand (envs always open lazily in okdb 2.0).
138
+ * Idempotent; throws ENV_NOT_FOUND for an unregistered name. */
109
139
  openEnv(name: string): Promise<OKDBEnvironment>;
110
140
  createEnvironment(name: string, config?: OKDBEnvironmentConfig): Promise<OKDBEnvironment>;
111
141
  removeEnvironment(name: string, options?: { keepSubEnvs?: string[] }): Promise<OKDBRemoveEnvironmentResult>;
@@ -18,7 +18,6 @@ export interface OKDBOptions {
18
18
  auth?: OKDBAuthOptions;
19
19
  sync?: OKDBSyncOptions;
20
20
  queue?: OKDBQueueOptions;
21
- bus?: boolean | Record<string, unknown>;
22
21
  functions?: Record<string, unknown>;
23
22
  timeMachine?: OKDBTimeMachineOptions;
24
23
  runtimeDeclarations?: boolean;
@@ -28,14 +27,25 @@ export interface OKDBOptions {
28
27
  * Defaults preserve full-role behavior; passive instances serve reads/writes
29
28
  * but skip engines / async processors / compaction scheduling. */
30
29
  engines?: boolean;
31
- asyncProcessors?: boolean;
32
- compaction?: 'active' | 'passive';
33
- http?: boolean;
34
- /** Which registered envs open() eagerly opens: 'all' (default), 'core'
35
- * (~system/~log/default only), or an array of names (core + listed —
36
- * an unknown listed name makes open() throw). Use openEnv(name) to open
37
- * more on demand. */
38
- envs?: 'all' | 'core' | string[];
30
+ /** okdb 2.0: INITIAL processing participation (claim every unclaimed `single` lease
31
+ * and drain derived work — FTS, views, time-machine, embeddings). Default true.
32
+ * false = not participating: claim nothing; reads/writes + inline processors still
33
+ * run. Participation is dynamic — db.processors.start()/stop() flip it at runtime
34
+ * (open with false + start() later = fast startup). Pass false for ephemeral
35
+ * short-lived processes. */
36
+ processors?: boolean;
37
+ /** okdb 2.0: true = eligible to claim the per-env compaction lease,
38
+ * false = passive (explicit env.compact() still works). */
39
+ compaction?: boolean;
40
+ /** Process-registry participation. false = invisible non-participant;
41
+ * { name } labels this process in the admin's process list. */
42
+ processes?: boolean | { name?: string; kind?: string; register?: boolean; listenAddr?: string | null };
43
+ /** Auto-compaction thresholds; { enabled: false } (or OKDB_AUTO_COMPACT=0) disables. */
44
+ autoCompact?: { enabled?: boolean; [key: string]: unknown };
45
+ /** HTTP API options (e.g. defaultEnv for routes that omit :env). */
46
+ api?: { defaultEnv?: string; [key: string]: unknown };
47
+ /** Admin UI feature; false disables it entirely. */
48
+ admin?: boolean | Record<string, unknown>;
39
49
  }
40
50
 
41
51
  export interface OKDBAuthOptions {
@@ -90,7 +100,6 @@ export interface OKDBTimeMachineOptions {
90
100
  }
91
101
 
92
102
  export interface OKDBEnvironmentConfig {
93
- sync?: boolean;
94
103
  durability?: 'strict' | 'balanced' | 'fast' | 'custom';
95
104
  lmdb?: Record<string, unknown>;
96
105
  compression?: boolean;
@@ -1,139 +0,0 @@
1
- # Workers — background-work placement over the root
2
-
3
- The worker population is OKDB's **inter-process placement layer**: a set of
4
- generic worker processes forked over one LMDB root that claim and execute
5
- background work — processor drains, queue jobs, function runs, and (FTS)
6
- snapshot bootstraps. There is no intra-process executor (the thread/process
7
- pool was removed in the runtime-model rework, P4): whoever holds a claim
8
- drains it **on its own loop in bounded quanta**, yielding between chunks.
9
-
10
- > **See also:** [Processors](processors.md) for the change-subscription **modes**
11
- > (`single`/`fanout`/`inline`), registration, and bootstrap — the _guarantee_ each
12
- > processor declares. This doc covers _where_ that work runs (population & placement);
13
- > processors.md covers _what_ a processor is.
14
-
15
- ## Quick start
16
-
17
- ```javascript
18
- const db = new OKDB('./data');
19
- await db.open();
20
-
21
- // Declare workers desired state. Root-keyed and idempotent: any instance may call
22
- // it any number of times; one population results.
23
- db.workers.ensure({ scale: 'auto' }); // default: min 0, backlog-driven
24
-
25
- // Tear down: scale:0 persists across restarts.
26
- await db.workers.destroy();
27
- ```
28
-
29
- **Zero-config embedded behavior is unchanged** — no `ensure()` call means the
30
- instance self-processes exactly as before (end-of-tick async on its own loop,
31
- leases held uncontended).
32
-
33
- ## `workers.ensure({scale})`
34
-
35
- | `scale` value | Meaning |
36
- | ------------------ | --------------------------------------------------------------------------------------- |
37
- | `'auto'` (default) | Supervisor runs; starts at 0 workers; forks on backlog, retires to 0 when idle |
38
- | `{ min, max }` | Supervisor runs; always keeps at least `min` workers; elastic ceiling `max` |
39
- | `0` | Hard off — no supervisor, no workers (serverless, test runners, PID-limited containers) |
40
-
41
- ```javascript
42
- // Auto-scaling (recommended default)
43
- db.workers.ensure({ scale: 'auto' });
44
-
45
- // Fixed floor with elastic ceiling
46
- db.workers.ensure({ scale: { min: 2, max: 8 } });
47
-
48
- // Hard off
49
- db.workers.ensure({ scale: 0 });
50
- ```
51
-
52
- Workers are **homogeneous and anonymous**: every worker handles every job class
53
- (processing drains, queue definitions, function runs, bootstrap jobs). Any
54
- worker may claim any work unit — the claim IS the dispatcher.
55
-
56
- ## Deployment shapes
57
-
58
- | shape | how |
59
- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
- | embedded, self-process | default — nothing to configure |
61
- | embedded + workers | `db.workers.ensure({scale:'auto'})` — instance stops self-claiming when workers are up; resumes when they retire |
62
- | embedded + HTTP workers | `db.http.listen(port, { workers: N })` after `open()` — forks okdb's own passive HTTP worker entry (child-entry mode; your app never re-executes; built-in routes only) |
63
- | CLI server | HTTP cluster (primary + N HTTP workers) is the `bin/okdb` default; add workers alongside |
64
-
65
- ## How placement works
66
-
67
- - **`processing: 'auto'`** (the default) resolves against the workers
68
- desired-state:
69
- - `scale:0` or no workers → self-process (today's behavior)
70
- - `scale:'auto'` at 0 live workers → self-process; forks first worker → stop claiming; population retires to 0 → resume self-claiming
71
- - `scale:{min,max}` → stop claiming immediately (workers are starting up)
72
- A configured-but-dead population is **re-forked** by any `'auto'` instance
73
- (recovery restores separation rather than absorbing the work).
74
- - Explicit `processing: 'main'` self-processes regardless of workers;
75
- `'none'` never processes. The legacy `'threads'`/`'processes'` values warn
76
- once and map to `'auto'` (the executor they selected was removed in P4).
77
- - **Supervisor**: a per-root `OKDBLock` lease elects one forker. The holder
78
- forks/retires children to match desired-state, respawns crashes (backoff
79
- 250 ms→4 s, crash-loop guard 5-in-10 s), and recycles wedged workers (stats
80
- heartbeat silent > 3× interval → SIGKILL + respawn). Supervisor death → its
81
- children die with it (parent-death watch); any surviving instance acquires the
82
- lease and re-forks from durable state.
83
- - **Shmbuf hard gate**: `workers.ensure({ scale: non-zero })` throws
84
- `WORKERS_SHMBUF_REQUIRED` when the shmbuf native binding is missing (the
85
- fallback shim is process-local; a multi-process write topology would corrupt
86
- HLC/clock ordering). A persisted desired > 0 read by a fallback-mode instance
87
- is treated as 0 + a warning.
88
-
89
- ## Job classes
90
-
91
- | class | mechanism |
92
- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
- | `processing` | the existing per-(env,type) lease + retry; bus `POKE {env,type}` wakes the holder |
94
- | `queue` | the verb is the placement: `queue.process(type, handler)` — in-process consumer; `queue.worker(type, module)` — the SHARED POOL (durable definition, declarative: no workers yet → jobs wait until `workers.ensure()`); `queue.spawn(type, module)` — dedicated forked child |
95
- | `functions` | durable request/response rows (`~fn:requests`/`~fn:responses`) + PROC bus fast path (`OKDB_FN_LEGACY_POOL=1` forces the legacy runner pool) |
96
- | `bootstrap` | snapshot bootstraps route as one-shot worker jobs (v1: FTS only — durable per-index meta); ineligible/closure bootstraps run owner-local |
97
-
98
- ## Admission & elasticity
99
-
100
- Per worker: `maxLeases` (default 32, `OKDB_WORKERS_MAX_LEASES`), loop-lag gate —
101
- p99 > 200 ms (`OKDB_WORKERS_LAG_GATE_MS`) for 2 windows → stop claiming (resume
102
- below half the gate for 2 windows).
103
-
104
- The supervisor probes every `OKDB_WORKERS_SUPERVISE_MS` (default 10 s) and
105
- scales on four backlog signals, **all read from durable state** (never this
106
- instance's in-memory processor lag, which is self-cancelling on a self-claiming
107
- supervisor and inflates on a claim-gated one):
108
-
109
- - changelog lag — per-type heads vs the persisted `~proc:state` cursors
110
- (high `OKDB_WORKERS_LAG_HIGH` default 10 000, low `OKDB_WORKERS_LAG_LOW`
111
- default 1 000)
112
- - queue depth (`OKDB_WORKERS_QUEUE_HIGH` default 100, `_LOW` default 10)
113
- - function backlog — pending `~fn:requests` rows (`OKDB_WORKERS_FN_HIGH`
114
- default 10)
115
- - pending routed-bootstrap job rows (any pending row counts as backlog)
116
-
117
- Scale-up respects `maxWorkers` (for `'auto'`: cores−1 or
118
- `OKDB_WORKERS_AUTO_MAX`) and holds at +1 until the last-forked worker comes
119
- online; scale-down floor is `minWorkers` (0 for `'auto'` — the population fully
120
- retires when idle). Cooldown: `OKDB_WORKERS_SCALE_COOLDOWN_MS` default 5 000 ms.
121
-
122
- ## Ops
123
-
124
- - Workers are registry citizens: `kind:'worker'` rows under their supervisor,
125
- with memory/CPU stats **and their processor states** (reported over the
126
- stats IPC channel) — the registry's `processorsView()` shows the
127
- claim-holder's row as the truth for each 1-of-N key, so the UI says
128
- "online on worker pid N", not "paused". Decommission/recommission/kill route
129
- through the supervisor's reconcile loop (durable slot records).
130
- - Status honesty: a `processing:'auto'` instance that released its claims
131
- reports `claimGated: true` (rendered as `delegated`, never `paused`) and
132
- `heldBy: {pid, hostname}` from the live lease record; a worker-routed
133
- bootstrap surfaces `progress: {phase:'bootstrap', placement:'worker',
134
- state, claimedBy}`.
135
- - Per-drain deadlines (`drainDeadlineMs`, default 60 s) mark a slow processor
136
- errored (lease retained, in-place retry); a truly wedged worker (silent stats)
137
- is recycled by the supervisor.
138
- - Runtime mode switching: `processor.setMode(logicalKey, mode)` /
139
- `POST /api/processors/:logicalKey/mode` — see `docs/processors.md`.