@kedem/okdb 1.9.1 → 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.
- package/README.md +3 -2
- package/bin/okdb.js +1 -1
- package/docs/deployment.md +155 -0
- package/docs/diagnostics.md +115 -0
- package/docs/embeddings.md +1 -1
- package/docs/functions.md +30 -27
- package/docs/getting-started.md +14 -3
- package/docs/http-api.md +3 -3
- package/docs/http-cluster.md +93 -60
- package/docs/index.md +13 -1
- package/docs/manifest.json +6 -3
- package/docs/pipelines.md +1 -1
- package/docs/process-registry.md +5 -5
- package/docs/processors.md +35 -18
- package/docs/queue.md +112 -42
- package/docs/sync.md +10 -7
- package/docs/ttl.md +1 -1
- package/docs/upgrade-2.0.md +290 -0
- package/okdb-functions-sandbox-worker.js +1 -1
- package/okdb-http-worker-child.js +1 -1
- package/okdb-views-bootstrap-worker.js +1 -1
- package/okdb.js +1 -1
- package/package.json +1 -1
- package/public/layouts/_default.ok.html +1 -1
- package/public/sections/db/modals/create-env-modal.ok.js +1 -1
- package/public/sections/db/modals/demo-env-modal.ok.js +1 -1
- package/public/sections/db/parts/db-overview.ok.js +1 -1
- package/public/sections/engines/engine-ui-utils.js +1 -1
- package/public/sections/engines/parts/engine-declaration-editor.ok.js +1 -1
- package/public/sections/engines/parts/generic-engine-panel.ok.js +1 -1
- package/public/sections/queue/parts/code-panel.ok.js +1 -1
- package/public/sections/queue/parts/job-log-panel.ok.js +1 -1
- package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
- package/public/sections/sync/parts/sync-topology.ok.js +1 -1
- package/public/sections/system/index.ok.html +1 -1
- package/public/sections/system/parts/system-process-panel.ok.js +1 -1
- package/public/sections/system/parts/system-processing-panel.ok.js +1 -1
- package/public/sections/system/parts/system-runtime-overview.ok.js +1 -1
- package/public/setup-app.js +1 -1
- package/public/setup.html +116 -55
- package/types/index.d.ts +33 -3
- package/types/options.d.ts +19 -10
- package/docs/worker-fleet.md +0 -139
- package/okdb-functions-runner-child.js +0 -1
- package/okdb-queue-load-handler.js +0 -1
- package/okdb-queue-spawn-child.js +0 -1
- package/okdb-worker-child.js +0 -1
- package/public/sections/system/parts/system-workers-panel.ok.js +0 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Roles & Deployment
|
|
2
|
+
|
|
3
|
+
okdb 2.0 has one runtime model: **the process is the control unit**. Every process that opens
|
|
4
|
+
a data path declares — at construction — which background work it runs, and okdb never forks
|
|
5
|
+
or supervises processes for you. Scale-out is always the same move: **run more okdb processes
|
|
6
|
+
on the same path**; the per-processor 1-of-N lease distributes the work automatically, with no
|
|
7
|
+
coordinator.
|
|
8
|
+
|
|
9
|
+
If you only ever run one process, you can stop reading: `new OKDB(path)` is a full
|
|
10
|
+
do-everything node and the defaults are correct.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## The role flags
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
new OKDB(path, {
|
|
18
|
+
processors: true, // claim processor leases + drain derived work (default true)
|
|
19
|
+
engines: true, // run embeddings / vector-search engines (default true)
|
|
20
|
+
compaction: true, // eligible to claim the per-env compaction lease (default true)
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Flag | `true` (default) | `false` (passive) |
|
|
25
|
+
| ------------ | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `processors` | Claim every unclaimed `single` processor lease; drain FTS / views / time-machine / materializer / embeddings on this loop | Claim nothing. Reads, writes, and **inline** processors (indexes, schema, FK, TTL) still run |
|
|
27
|
+
| `engines` | Run queue worker engines, pipeline workers, embeddings engines | None of those start here |
|
|
28
|
+
| `compaction` | Eligible to claim the per-env compaction lease | Never auto-compacts (`env.compact()` still works when called explicitly) |
|
|
29
|
+
|
|
30
|
+
Three things to internalize:
|
|
31
|
+
|
|
32
|
+
- **Roles are policy, not placement.** A passive node is a full read/write citizen — its writes
|
|
33
|
+
land in the changelog and are indexed by whichever participating node holds the lease. Inline
|
|
34
|
+
processors are mandatory on every writer regardless of role.
|
|
35
|
+
- **The lease is the load balancer.** With N participating nodes, each `single` processor is
|
|
36
|
+
claimed by exactly one; a dead node's leases lapse and survivors re-claim. Under sustained
|
|
37
|
+
backlog holders rotate work in cooperative hold windows. See [Processors](./processors.md).
|
|
38
|
+
- **Ephemeral processes should be passive.** A CLI one-shot or short-lived script that grabs a
|
|
39
|
+
1-of-N lease and exits mid-quantum just thrashes it — pass `processors: false`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Topologies
|
|
44
|
+
|
|
45
|
+
### 1. One embedded process (the default)
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const db = new OKDB('./data');
|
|
49
|
+
await db.open();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Serves your app's reads/writes and does all derived work on its own loop, in bounded quanta.
|
|
53
|
+
This is crash-safe and correct on its own; you add processes when you decide you need them.
|
|
54
|
+
|
|
55
|
+
### 2. Embedded + HTTP
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
db.http.listen(8080); // returns a plain http.Server
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Same single process, plus the REST API and admin UI. The embedded server is always
|
|
62
|
+
single-process — `http.listen(port, { workers })` throws `HTTP_CLUSTER_REMOVED`.
|
|
63
|
+
|
|
64
|
+
### 3. N capable nodes (the CLI's shape)
|
|
65
|
+
|
|
66
|
+
Run N identical full-role processes on the same path — either via `bin/okdb` (shared listen
|
|
67
|
+
socket, thin passive supervisor; see [HTTP Clustering](./http-cluster.md)) or as N independent
|
|
68
|
+
`new OKDB(path)` + `http.listen()` processes behind your own load balancer. Each node serves
|
|
69
|
+
**and** processes; the leases spread the processors across them and fail over when a node dies.
|
|
70
|
+
|
|
71
|
+
### 4. Dedicated workers + a passive serving node
|
|
72
|
+
|
|
73
|
+
When you want the serving loop insulated from heavy backfill/indexing work:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
// serving process — loop only serves; never claims, never compacts
|
|
77
|
+
const front = new OKDB(path, { processors: false, engines: false, compaction: false });
|
|
78
|
+
front.http.listen(8080);
|
|
79
|
+
|
|
80
|
+
// N worker processes — headless, do all the processing
|
|
81
|
+
const worker = new OKDB(path, { processors: true }); // engines/compaction default true
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Workers need no ports: coordination is the shared LMDB + the UDP bus. Placement and respawn
|
|
85
|
+
are your launcher's job (systemd, pm2, docker, k8s, a PowerShell script — anything that can
|
|
86
|
+
start N processes).
|
|
87
|
+
|
|
88
|
+
**Boot order tip:** start processes one at a time — wait until a node has fully opened (e.g.
|
|
89
|
+
it bound its port, or touched a ready-file of your choosing) before starting the next, so
|
|
90
|
+
concurrent first-opens of the same envs never race.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Dynamic participation: `processors.start()` / `processors.stop()`
|
|
95
|
+
|
|
96
|
+
The `processors` constructor option is only the **initial** state — participation is
|
|
97
|
+
controlled at runtime:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
db.processors.start(); // begin participating: un-gate claiming, run the deferred view boot
|
|
101
|
+
db.processors.stop(); // cease: finish in-flight quanta, release leases (peers fail over)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Both are idempotent and resolve `true` when the state actually changed. `stop()` never
|
|
105
|
+
touches reads, writes, or inline processors — the node stays a full read/write citizen.
|
|
106
|
+
|
|
107
|
+
**Fast startup** falls out of this: open without participating, start once your service is up.
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
const db = new OKDB(path, { processors: false });
|
|
111
|
+
await db.open(); // fast — no view boot, no FTS/TM drains competing with your startup
|
|
112
|
+
|
|
113
|
+
app.listen(PORT, async () => {
|
|
114
|
+
await db.processors.start(); // now begin claiming + draining
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Maintenance windows** too: `stop()` during a heavy backfill to keep this node's loop free,
|
|
119
|
+
`start()` after — the leases redistribute to peers and back via normal failover.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Controlling processors at runtime
|
|
124
|
+
|
|
125
|
+
Static role flags decide _participation_; the running system is controlled per processor
|
|
126
|
+
through durable desired-state (applies across every process, no IPC):
|
|
127
|
+
|
|
128
|
+
- **Node-level participation** — `db.processors.start()` / `stop()` (see above); this-process,
|
|
129
|
+
in-memory.
|
|
130
|
+
- **Pause / resume / retry / cursor reset** — per processor, from the admin UI's Processors
|
|
131
|
+
view or `stop.pause()` / `stop.resume()` on the registration handle. A durable pause
|
|
132
|
+
survives restarts and re-claims (cross-process, unlike node-level start/stop).
|
|
133
|
+
- **Mode switching** — `setMode(logicalKey, mode)` flips a processor between sync/async at a
|
|
134
|
+
clock boundary. See [Processors](./processors.md).
|
|
135
|
+
- **Observability** — `GET /api/processors/status` (per-processor durable lag — the honest
|
|
136
|
+
backlog signal from any node), `GET /api/processes/tree` (the census of OS processes on the
|
|
137
|
+
root), and the per-env Write queue (`depth`, `oldestPendingMs` — the stall tell). These are
|
|
138
|
+
the signals an external autoscaler would watch; the scaling _policy_ lives with your
|
|
139
|
+
launcher, not inside okdb.
|
|
140
|
+
- **`db.pressure()`** — the composite of those signals for THIS node (writer stall/depth, max
|
|
141
|
+
durable lag, queue backlog, loop lag, and a normalized `score`), cached 250 ms. Feed it to a
|
|
142
|
+
queue consumer's `admission` option (`() => db.pressure().score < 1`) for load-aware
|
|
143
|
+
consumption, or poll it from your orchestrator. See docs/queue.md → "Load-aware consumers".
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Liveness guarantees (what you can rely on)
|
|
148
|
+
|
|
149
|
+
- Cross-process wakes ride the UDP bus; because UDP is lossy, every participating node also
|
|
150
|
+
runs an idle **catch-up tick** (`OKDB_PROCESSOR_CATCHUP_MS`, default 15 s) that drains any
|
|
151
|
+
backlog a lost poke left behind — a write from a short-lived process is picked up within
|
|
152
|
+
one tick, not "whenever the next write happens".
|
|
153
|
+
- The **changelog is never pruned past the slowest processor cursor**, so a lagging drain
|
|
154
|
+
finds its entries intact instead of silently skipping writes. A durably paused processor
|
|
155
|
+
therefore pins changelog growth — visible and recoverable, by design.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Diagnostics — finding crashes, hangs, and stalls
|
|
2
|
+
|
|
3
|
+
okdb's hardest failures are timing races on LMDB's native memory map: a write or read that
|
|
4
|
+
touches an env after it was closed/unmapped for a compaction swap or `removeEnvironment`. They
|
|
5
|
+
surface as:
|
|
6
|
+
|
|
7
|
+
- **`The environment is already closed`** — an uncaught JS error thrown from inside lmdb's own
|
|
8
|
+
deferred callback (no okdb stack at throw time). The "crash".
|
|
9
|
+
- **`0xC0000005` / access violation** (Windows) / **SIGSEGV** (POSIX) — a native read/write
|
|
10
|
+
after unmap. The "hard crash".
|
|
11
|
+
- **A hang / "halt"** — a compaction swap that never completes because something pins the env
|
|
12
|
+
across the swap (a busy slot, a wedged commit).
|
|
13
|
+
|
|
14
|
+
These are rare and timing-dependent, so the fix is **resident, opt-in instrumentation** you can
|
|
15
|
+
turn on in the failing deployment and leave on until it fires. Three independent tools, each a
|
|
16
|
+
single env var. **None has any cost when off.**
|
|
17
|
+
|
|
18
|
+
> **Before anything else: are you running current code?** These races have been fixed
|
|
19
|
+
> incrementally. okdb ships as a **build** (`dist/` / `release/` / npm package), not `src/`. If
|
|
20
|
+
> your deployment's build predates the fix, you are chasing a ghost. Rebuild
|
|
21
|
+
> (`npm run build` / `npm run build:release`) and redeploy, then reproduce. Check the build date
|
|
22
|
+
> against the relevant commits before spending time on instrumentation.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 1. `OKDB_DIAG` — the write-orphan ring + drain-stall watchdog
|
|
27
|
+
|
|
28
|
+
The primary tool for the "already closed" orphan and the compaction "halt". JS-level, works on
|
|
29
|
+
the main process **and** slot/worker isolates.
|
|
30
|
+
|
|
31
|
+
| Env var | Default | Effect |
|
|
32
|
+
| ---------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `OKDB_DIAG=1` | off | Enable the write-origin ring, the orphan crash-dump handler, and the drain-stall watchdog. |
|
|
34
|
+
| `OKDB_DIAG_STACK=1` | off | Capture a JS stack **at each write**, so a dump names the exact call site (doc commit / cursor save / view-fts-tm replay). A few µs/write — fine for a debugging run. |
|
|
35
|
+
| `OKDB_DIAG_DIR=<path>` | stderr only | Also append every dump to `<path>/okdb-diag.<pid>.log` (survives the crash). |
|
|
36
|
+
| `OKDB_DIAG_RING=N` | 256 | Size of the recent-writes ring. |
|
|
37
|
+
| `OKDB_DIAG_STALL_MS=N` | 8000 | A **swap** drain held longer than this is dumped as a stall (re-dumped every `N` ms while it persists). |
|
|
38
|
+
| `OKDB_DIAG_WATCH_MS=N` | 2000 | Stall-watchdog poll interval. |
|
|
39
|
+
| `OKDB_DIAG_SURVIVE=1` | off | After dumping an orphan, **do not exit** — ride through it (the orphaned write is lost, but the process keeps running). Use to gather more than one dump per run. |
|
|
40
|
+
|
|
41
|
+
**On an orphan** (`already closed` / invalid-read-txn signature) you get the recent write
|
|
42
|
+
origins, newest first — the orphaning call site is named (with `OKDB_DIAG_STACK=1`):
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
=== OKDB_DIAG ORPHAN (env-closed write) @ ... pid=19932 ===
|
|
46
|
+
error: Error: The environment is already closed
|
|
47
|
+
recent writes (newest first):
|
|
48
|
+
#4821 -3ms env=default raw-put
|
|
49
|
+
at OKDBProcessor._saveCursor (.../okdb-processor.js:...)
|
|
50
|
+
...
|
|
51
|
+
#4820 -4ms env=default commit
|
|
52
|
+
...
|
|
53
|
+
drain state (envs with a non-zero drain):
|
|
54
|
+
env=default phase=SWAP drainAge=120ms activeWriters=1 writer{depth=1 oldestPendingMs=118 ...}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**On a stall** the watchdog dumps _why_ the swap can't finish — which writer is in flight and
|
|
58
|
+
which slots are pinning the env:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
=== OKDB_DIAG STALL: swap drain held 8400ms on env=default @ ... ===
|
|
62
|
+
env=default phase=SWAP drainAge=8400ms activeWriters=0 writer{depth=0 oldestPendingMs=0 parkedNow=0}
|
|
63
|
+
slots PINNING this env: #3/fts/fts:default:Order
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Live, no crash needed:** `db.diag()` / `env.diag()` return the same snapshot
|
|
67
|
+
(`{recentWrites, drain, ...}`) for an ad-hoc tap or an admin endpoint.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 2. `OKDB_FATAL_REPORT` — native fault report
|
|
72
|
+
|
|
73
|
+
For the **native** crash (`0xC0000005` / SIGSEGV) that `OKDB_DIAG` can't catch (it's not a JS
|
|
74
|
+
exception). Makes Node write a diagnostic report naming the **faulting thread's** JS + native
|
|
75
|
+
stack, env, and thread id.
|
|
76
|
+
|
|
77
|
+
| Env var | Effect |
|
|
78
|
+
| ------------------------- | ------------------------------------------------------------------------ |
|
|
79
|
+
| `OKDB_FATAL_REPORT=1` | Write a Node report on a fatal native error. |
|
|
80
|
+
| `OKDB_REPORT_DIR=<path>` | Where to write reports (created if missing). |
|
|
81
|
+
| `OKDB_FATAL_REPORT_ENV=1` | Include env vars in the report (off by default — they may hold secrets). |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 3. `OKDB_NATIVE_OP_LOG` — the unmap timeline
|
|
86
|
+
|
|
87
|
+
For an **open-vs-close race** on lmdb-js's shared (process-global, refcounted) native env: a
|
|
88
|
+
synchronous breadcrumb right before each risky native op that unmaps/remaps memory
|
|
89
|
+
(`db.close`, env swap/rename, `removeEnvironment`, residency evict, reopen, slot stop) **and**
|
|
90
|
+
the matching open. Pairs with `OKDB_FATAL_REPORT`: the report gives the faulting stack, this
|
|
91
|
+
gives the cross-thread open/close timeline.
|
|
92
|
+
|
|
93
|
+
| Env var | Effect |
|
|
94
|
+
| --------------------------- | ----------------------------------------------------------------- |
|
|
95
|
+
| `OKDB_NATIVE_OP_LOG=<path>` | Append a one-line breadcrumb per risky native op. |
|
|
96
|
+
| `OKDB_NATIVE_OP_STACK=1` | Also append the JS stack of each op (names the exact close path). |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Triage workflow
|
|
101
|
+
|
|
102
|
+
1. **Confirm the build is current** (see the note above). Most "still crashing after a fix"
|
|
103
|
+
reports are a stale build.
|
|
104
|
+
2. Reproduce with **all three** on:
|
|
105
|
+
`OKDB_DIAG=1 OKDB_DIAG_STACK=1 OKDB_FATAL_REPORT=1 OKDB_NATIVE_OP_LOG=ops.log OKDB_NATIVE_OP_STACK=1 OKDB_DIAG_DIR=diag/ OKDB_REPORT_DIR=reports/`
|
|
106
|
+
3. On a crash:
|
|
107
|
+
- `already closed` → `OKDB_DIAG` dump names the write site. The fix is to route that write
|
|
108
|
+
through `OKDBWriter` with a **synchronous** `putSync`/`removeSync` inside the txn (an async
|
|
109
|
+
`db.put` inside a writer `childTransaction` escapes into lmdb's deferred event-turn-batch and
|
|
110
|
+
orphans on close).
|
|
111
|
+
- `0xC0000005` → cross-reference the `OKDB_FATAL_REPORT` faulting stack with the
|
|
112
|
+
`OKDB_NATIVE_OP_LOG` timeline to find the close that raced the read/write.
|
|
113
|
+
4. On a hang: the `OKDB_DIAG` **stall** dump names the writer/slot pinning the swap.
|
|
114
|
+
5. For a deterministic local repro, `OKDB_SLOTS_INLINE=1` runs slot quanta inline on the owner
|
|
115
|
+
loop — it makes several of these races reproduce 100% instead of intermittently.
|
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 the worker
|
|
240
|
+
For high-throughput or external embedder services, the indexer enqueues jobs and queue consumers (the `embed-worker`, a [queue](queue.md) consumer) claim and process them concurrently across whatever nodes run the embeddings engine:
|
|
241
241
|
|
|
242
242
|
```javascript
|
|
243
243
|
await okdb.embeddings.createPipeline('articles', {
|
package/docs/functions.md
CHANGED
|
@@ -9,7 +9,7 @@ They are designed for:
|
|
|
9
9
|
- stable HTTP entry points
|
|
10
10
|
- MCP-accessible orchestration routines
|
|
11
11
|
|
|
12
|
-
Custom functions are **stored**, **versioned**, **validated**, **audited**, and executed in **
|
|
12
|
+
Custom functions are **stored**, **versioned**, **validated**, **audited**, and executed in a per-process **sandbox thread**.
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -110,32 +110,35 @@ Use `ctx.log(...)` instead of `console`.
|
|
|
110
110
|
|
|
111
111
|
## Runtime model
|
|
112
112
|
|
|
113
|
-
A function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
caller's main loop
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
113
|
+
A function runs on **the node it was asked on**, in that node's per-process **sandbox thread**:
|
|
114
|
+
a dedicated `worker_thread` with its own okdb instance, heap-capped from `runtime.memoryMb` via
|
|
115
|
+
`resourceLimits`, with `terminate()` as the timeout/wedge watchdog. One sandbox per node,
|
|
116
|
+
lazy-spawned and idle-reaped; invocations run concurrently in it, capped. Execution **never
|
|
117
|
+
blocks the caller's main loop** — untrusted user code always runs off-loop — and containment is
|
|
118
|
+
thread-level: a wedged function is `terminate()`d without taking the node's other work down.
|
|
119
|
+
This is what makes "run the function locally" safe, so it no longer needs a separate worker
|
|
120
|
+
process.
|
|
121
|
+
|
|
122
|
+
Sandbox concurrency is capped **per process** (`OKDB_FN_SANDBOX_CONCURRENCY`, default 4):
|
|
123
|
+
invocations beyond the cap queue at the claim layer. Host-level CPU budgeting is the operator's
|
|
124
|
+
job — size the number of okdb processes (and their sandbox caps) to the machine.
|
|
125
|
+
|
|
126
|
+
Two invocation modes:
|
|
127
|
+
|
|
128
|
+
- **Synchronous** (`functions.run()` / HTTP execution) — routed straight to the local sandbox,
|
|
129
|
+
no `~fn:requests` round-trip, lowest latency.
|
|
130
|
+
- **Durable / run-eventually** (survive the caller, store the result) — a [queue](queue.md) job
|
|
131
|
+
on a designated node. The `~fn:requests` / `~fn:responses` substrate is retained as the
|
|
132
|
+
durable invocation mode (the UDP bus carries a wake hint so dispatch is ~ms).
|
|
133
|
+
|
|
134
|
+
Either way: scripts are validated and compile-checked before storage; timeouts terminate and
|
|
135
|
+
replace the executor; memory is capped; every run is written to a run ledger. This makes
|
|
136
|
+
functions suitable for operational entry points without paying spawn cost on every call.
|
|
137
|
+
|
|
138
|
+
> **Removed in 2.0:** the anonymous worker-population dispatch and the child-process **fork
|
|
139
|
+
> pool** (`OKDB_FN_LEGACY_POOL`). Functions execute in the asking node's local sandbox. See
|
|
140
|
+
> [Upgrading to 2.0](upgrade-2.0.md). If you don't want every HTTP node carrying a sandbox,
|
|
141
|
+
> designate which nodes serve functions — placement is yours.
|
|
139
142
|
|
|
140
143
|
---
|
|
141
144
|
|
package/docs/getting-started.md
CHANGED
|
@@ -17,7 +17,7 @@ Dependencies that OKDB needs internally (`lmdb`, `sift`, `hnswlib-node`, etc.) a
|
|
|
17
17
|
## Opening a database
|
|
18
18
|
|
|
19
19
|
```javascript
|
|
20
|
-
const OKDB = require('
|
|
20
|
+
const OKDB = require('@kedem/okdb');
|
|
21
21
|
|
|
22
22
|
const okdb = new OKDB('./mydb', options);
|
|
23
23
|
await okdb.open();
|
|
@@ -63,9 +63,20 @@ const okdb = new OKDB('./mydb', {
|
|
|
63
63
|
token: 'cluster-secret',
|
|
64
64
|
address: 'http://localhost:8080', // this node's public address
|
|
65
65
|
},
|
|
66
|
+
|
|
67
|
+
// Role flags (okdb 2.0) — what background work THIS process runs.
|
|
68
|
+
// All default to true; a plain `new OKDB(path)` is a full do-everything node.
|
|
69
|
+
// `processors` is the INITIAL participation only — okdb.processors.start()/stop()
|
|
70
|
+
// flip it at runtime (open with false + start() later = fast startup).
|
|
71
|
+
processors: true, // claim processor leases and drain derived work (FTS, views, …)
|
|
72
|
+
engines: true, // run embeddings / vector-search engines
|
|
73
|
+
compaction: true, // eligible to claim the per-env compaction lease
|
|
66
74
|
});
|
|
67
75
|
```
|
|
68
76
|
|
|
77
|
+
See [Roles & Deployment](./deployment.md) for what the role flags mean and how to run
|
|
78
|
+
multi-process topologies (N capable nodes, dedicated workers + passive serving nodes).
|
|
79
|
+
|
|
69
80
|
---
|
|
70
81
|
|
|
71
82
|
## Your first type
|
|
@@ -76,7 +87,7 @@ A **type** is OKDB's equivalent of a collection or table. Records in a type shar
|
|
|
76
87
|
await okdb.registerType('users');
|
|
77
88
|
```
|
|
78
89
|
|
|
79
|
-
|
|
90
|
+
`registerType` throws if the type already exists. Use `ensureType` for idempotent setup:
|
|
80
91
|
|
|
81
92
|
```javascript
|
|
82
93
|
await okdb.ensureType('users', {
|
|
@@ -197,7 +208,7 @@ process.on('SIGTERM', () => okdb.close().then(() => process.exit(0)));
|
|
|
197
208
|
|
|
198
209
|
```javascript
|
|
199
210
|
'use strict';
|
|
200
|
-
const OKDB = require('
|
|
211
|
+
const OKDB = require('@kedem/okdb');
|
|
201
212
|
|
|
202
213
|
async function main() {
|
|
203
214
|
const okdb = new OKDB('./demo');
|
package/docs/http-api.md
CHANGED
|
@@ -600,7 +600,7 @@ OKDB's HTTP layer is **framework-agnostic**. `okdb.http.listen()` is entirely op
|
|
|
600
600
|
|
|
601
601
|
```javascript
|
|
602
602
|
const express = require('express');
|
|
603
|
-
const OKDB = require('
|
|
603
|
+
const OKDB = require('@kedem/okdb');
|
|
604
604
|
|
|
605
605
|
const app = express();
|
|
606
606
|
const okdb = new OKDB('./mydb');
|
|
@@ -643,7 +643,7 @@ app.listen(3000);
|
|
|
643
643
|
|
|
644
644
|
```javascript
|
|
645
645
|
const fastify = require('fastify')({ logger: false });
|
|
646
|
-
const OKDB = require('
|
|
646
|
+
const OKDB = require('@kedem/okdb');
|
|
647
647
|
|
|
648
648
|
const okdb = new OKDB('./mydb');
|
|
649
649
|
await okdb.open();
|
|
@@ -684,7 +684,7 @@ await fastify.listen({ port: 3000 });
|
|
|
684
684
|
|
|
685
685
|
```javascript
|
|
686
686
|
const http = require('http');
|
|
687
|
-
const OKDB = require('
|
|
687
|
+
const OKDB = require('@kedem/okdb');
|
|
688
688
|
|
|
689
689
|
const okdb = new OKDB('./mydb');
|
|
690
690
|
await okdb.open();
|
package/docs/http-cluster.md
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
# HTTP Clustering
|
|
2
2
|
|
|
3
|
-
By default, the `okdb` CLI runs
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
By default, the `okdb` CLI runs across **N identical capable worker processes** that share
|
|
4
|
+
the listen socket: each one serves HTTP + SSE **and** participates in processing. There is **no
|
|
5
|
+
privileged processor** — the 1-of-N `OKDBLock` lease distributes claims across the workers
|
|
6
|
+
(cooperative hold-window rotation load-shares under sustained backlog), and each worker drains
|
|
7
|
+
its claims on its own loop in bounded quanta, so a heavy drain on one worker never blocks the
|
|
8
|
+
others. A dead worker's claims fail over to the survivors. The Node-cluster primary is a thin
|
|
9
|
+
**passive supervisor** (it forks/respawns the workers and seeds the shared token secret; it
|
|
10
|
+
does not serve or process).
|
|
11
|
+
|
|
12
|
+
This is a **CLI-only feature** (`okdb` / `bin/okdb`). The embedded library
|
|
13
|
+
(`new OKDB(...).http.listen(port)`) is always single-process — `http.listen({workers})` was
|
|
14
|
+
removed in 2.0. For multi-process embedding, run **N independent full nodes** (each its own
|
|
15
|
+
`new OKDB(path).http.listen(port)`) on the same data path behind your own load balancer: they
|
|
16
|
+
already coordinate through the shared LMDB + bus + the 1-of-N lease exactly like the CLI's
|
|
17
|
+
workers (this is the same "N capable nodes" shape, just supervised by your balancer instead of
|
|
18
|
+
`bin/okdb`). See [Embedded clustering](#embedded-clustering).
|
|
10
19
|
|
|
11
20
|
---
|
|
12
21
|
|
|
@@ -14,36 +23,40 @@ single-process unless you opt in — see [Embedded clustering](#embedded-cluster
|
|
|
14
23
|
|
|
15
24
|
```
|
|
16
25
|
┌──────────────────────────────────────────────┐
|
|
17
|
-
│
|
|
18
|
-
│
|
|
19
|
-
│ •
|
|
20
|
-
│ •
|
|
26
|
+
│ supervisor (PASSIVE — processors:false, │
|
|
27
|
+
│ engines:false, compaction:passive) │
|
|
28
|
+
│ • forks/respawns/decommissions the workers │
|
|
29
|
+
│ • owns the ~processes registry (publishes a │
|
|
30
|
+
│ row per worker; workers are registry-passive)│
|
|
21
31
|
│ • opens its OKDB BEFORE forking (seeds the │
|
|
22
32
|
│ shared __tokenSecret so workers agree) │
|
|
33
|
+
│ • HTTP-SILENT; does NOT process │
|
|
23
34
|
└───────────────┬──────────────────────────────┘
|
|
24
35
|
│ forks N
|
|
25
36
|
┌───────────────┬───────┴───────┬───────────────┐
|
|
26
37
|
▼ ▼ ▼ ▼
|
|
27
38
|
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
|
28
|
-
│ worker │ │ worker │ ... │ worker │ │ worker │
|
|
29
|
-
│
|
|
30
|
-
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
|
31
|
-
└───────────────┴── shared listen socket ──┴───────────┘
|
|
32
|
-
bus:true
|
|
39
|
+
│ worker │ │ worker │ ... │ worker │ │ worker │ CAPABLE role:
|
|
40
|
+
│HTTP+proc│ │HTTP+proc│ │HTTP+proc│ │HTTP+proc│ processors:true
|
|
41
|
+
└─────────┘ └─────────┘ └─────────┘ └─────────┘ (serve + process +
|
|
42
|
+
└───────────────┴── shared listen socket ──┴───────────┘ compact)
|
|
33
43
|
```
|
|
34
44
|
|
|
35
|
-
- **
|
|
36
|
-
|
|
37
|
-
`OKDBLock`
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
- **Workers = N identical capable nodes.** Each forked worker opens a **full-role** OKDB
|
|
46
|
+
(`processors:true`) and both serves HTTP + SSE on the shared socket **and** processes — it
|
|
47
|
+
claims the 1-of-N `OKDBLock` lease, builds/indexes FTS + views, and drains its claims on its
|
|
48
|
+
own loop in bounded quanta. Claims **distribute** across the workers (no
|
|
49
|
+
privileged processor); a dead worker's leases lapse and the survivors re-claim. Workers are
|
|
50
|
+
**registry-passive** (the supervisor publishes their `~processes` rows; a capable worker
|
|
51
|
+
reports its processing facet to the supervisor over IPC so cluster processing stays visible).
|
|
52
|
+
- **Supervisor = passive.** It opens a passive OKDB (`processors:false`, `engines:false`,
|
|
53
|
+
`compaction:false`) only to seed the shared `__tokenSecret` before forking and to own the
|
|
54
|
+
registry. It is HTTP-silent and never processes.
|
|
55
|
+
- **`workers:1`** (or `--no-cluster`) is byte-identical to the single-process path: one full
|
|
56
|
+
capable process serves + processes.
|
|
57
|
+
|
|
58
|
+
A write on any worker is indexed by whichever worker holds the lease (cross-process POKE + the
|
|
59
|
+
shared changelog); bounded quanta keep that indexing from monopolizing the holder's loop.
|
|
47
60
|
|
|
48
61
|
---
|
|
49
62
|
|
|
@@ -59,9 +72,9 @@ The CLI resolves the worker count from the first source present, highest priorit
|
|
|
59
72
|
| 4 | `http.workers` | `.kdbconfig` config file. |
|
|
60
73
|
| 5 | _default_ | `max(1, cores − 1)` where `cores = os.cpus()`. |
|
|
61
74
|
|
|
62
|
-
A non-integer or `< 1` explicit value clamps to `1` with a warning. The default
|
|
63
|
-
|
|
64
|
-
to one process.
|
|
75
|
+
A non-integer or `< 1` explicit value clamps to `1` with a warning. The default leaves one core
|
|
76
|
+
for the OS / the thin passive supervisor and runs N capable workers on the rest (each serves HTTP
|
|
77
|
+
_and_ processes); on a single-core box it collapses to one process.
|
|
65
78
|
|
|
66
79
|
```bash
|
|
67
80
|
okdb # default: max(1, cores − 1) HTTP workers
|
|
@@ -107,16 +120,21 @@ client on a given worker will and won't see:
|
|
|
107
120
|
reconstruct data-change events (item put/remove, index, view, type). A write committed on
|
|
108
121
|
worker A surfaces in worker B's SSE within the poke window. The bus carries the **signal**;
|
|
109
122
|
the **data** is read from shared LMDB — the changelog is the single source of truth.
|
|
110
|
-
- **Processing progress is
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
- **Processing progress is per-worker; the cluster-wide view is the durable registry.** There is
|
|
124
|
+
no privileged processor — each capable worker processes its own claimed leases and emits its own
|
|
125
|
+
ephemeral processing events (view-rebuild progress, FTS/index lifecycle, processor lifecycle) to
|
|
126
|
+
**its own** SSE. The cluster-wide picture is **read** from the durable `~processes` registry:
|
|
127
|
+
each worker reports its processing facet to the supervisor over **cluster IPC**, the supervisor
|
|
128
|
+
stamps it onto the worker's registry row, and any node serves `/api/processing/status`,
|
|
129
|
+
`/api/processors/status`, and `/api/cluster/status` from that shared registry. So a processing
|
|
130
|
+
event on worker A is not mirrored into worker B's live SSE, but A's processor **state** is
|
|
131
|
+
visible cluster-wide via the registry-sourced status routes.
|
|
132
|
+
- **Worker logs are forwarded to the supervisor's log pipeline** over cluster IPC (tagged per
|
|
133
|
+
worker) so the operator log is unified; they are **not** mirrored into other workers' SSE
|
|
134
|
+
streams (SSE log frames are per-process). Only data changes (via the changelog) are coherent
|
|
135
|
+
across every worker's SSE.
|
|
136
|
+
|
|
137
|
+
`/api/cluster/status` returns the cluster shape (supervisor pid, worker pids, uptime).
|
|
120
138
|
|
|
121
139
|
---
|
|
122
140
|
|
|
@@ -132,26 +150,41 @@ client on a given worker will and won't see:
|
|
|
132
150
|
|
|
133
151
|
## Embedded clustering
|
|
134
152
|
|
|
135
|
-
The **library default is single-process** and unchanged. `new OKDB(...).http.listen(port)`
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
The **library default is single-process** and unchanged. `new OKDB(...).http.listen(port)`
|
|
154
|
+
returns a plain single-process `http.Server`.
|
|
155
|
+
|
|
156
|
+
> **Removed in 2.0:** the embedded `http.listen(port, { workers, makeOkdb, primaryOkdb })` API
|
|
157
|
+
> (it **throws `HTTP_CLUSTER_REMOVED`**). A library node never forks — HTTP clustering is a
|
|
158
|
+
> **launcher** concern. See [Upgrading to 2.0](upgrade-2.0.md).
|
|
159
|
+
|
|
160
|
+
Two ways to cluster an embedded app — both are the same **"N identical capable nodes"** shape
|
|
161
|
+
(every node serves _and_ processes; the 1-of-N lease distributes the work; no privileged processor):
|
|
162
|
+
|
|
163
|
+
1. **N independent nodes behind your balancer** (simplest). Run your entry as N separate processes,
|
|
164
|
+
each a full `new OKDB(path).http.listen(port_i)`, and put a load balancer in front. They
|
|
165
|
+
coordinate through the shared LMDB + bus + the lease — no Node `cluster`, no supervisor needed.
|
|
166
|
+
2. **Plain Node `cluster`** (one listen port, shared socket). Fork your entry; each worker opens a
|
|
167
|
+
**capable** OKDB and calls `http.listen(port)` — a cluster worker's `listen` shares the socket
|
|
168
|
+
natively, with no okdb involvement.
|
|
138
169
|
|
|
139
170
|
```js
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
171
|
+
const cluster = require('node:cluster');
|
|
172
|
+
|
|
173
|
+
if (cluster.isPrimary) {
|
|
174
|
+
// Thin supervisor: open a passive node BEFORE forking to seed the shared __tokenSecret,
|
|
175
|
+
// then fork N capable workers. It does not serve or process.
|
|
176
|
+
const supervisor = new OKDB(dbPath, { processors: false, compaction: false, http: false });
|
|
177
|
+
await supervisor.open();
|
|
178
|
+
for (let i = 0; i < 4; i++) cluster.fork();
|
|
179
|
+
} else {
|
|
180
|
+
// Each worker = an identical CAPABLE node: serves HTTP AND processes (claims leases,
|
|
181
|
+
// pool-drains heavy work off its loop). The lease distributes processing across them.
|
|
182
|
+
const w = new OKDB(dbPath, { processors: true });
|
|
183
|
+
await w.open();
|
|
184
|
+
w.http.listen(port); // shared socket via Node cluster; bus auto-derives from the shared path
|
|
185
|
+
}
|
|
154
186
|
```
|
|
155
187
|
|
|
156
|
-
The same shmbuf requirement applies — only cluster
|
|
157
|
-
`
|
|
188
|
+
The same shmbuf requirement applies — only cluster on a host with the native `shmbuf` binding.
|
|
189
|
+
For a turnkey cluster (supervisor, respawn, registry rows, log forwarding), `bin/okdb` does all of
|
|
190
|
+
this for you.
|