Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ object needs a private one, so a process holds one open durable object at a
time and no ordinary `Session` beside it — fan-out goes across worker
processes.

The local backend answers to both `file:` and `local:`, because a namespace URL
gets shared between services in different languages and Python spells it
`local:` while Go takes either.

Recovery on another machine needs the object to live somewhere neither machine
owns, so `chdb/durable/s3` provides an S3-compatible backend — AWS S3,
Cloudflare R2 and MinIO through one implementation. It sits behind its own
Expand Down
97 changes: 86 additions & 11 deletions docs/design/durable-control-plane.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,19 @@ can plant it can rewrite the objects directly — and check-then-use cannot be
made atomic without `openat`, which Node does not expose. Anything wanting that
property should not be on this backend.

It answers to two scheme names, `file:` and `local:`. That is not a
convenience — a namespace URL ends up in a config file, and that config file
gets shared between services in different languages. Python spells this
backend `local:`, Go registers both, and this binding took only `file:` until
it was pointed out, which made Node the one place a working configuration
stopped working. The two forms differ in how a path is written rather than
where it points: `file:` is a real URL, so a space is `%20`; `local:` is
opaque, so the path is closer to literal. Past ordinary paths the bindings do
not agree anyway — Python leaves `local:` undecoded, Go decodes it — so this
follows Go rather than inventing a third behaviour for a case none of them
share. A `local:` URL naming a host (`local://data/objects`) is refused
instead of silently resolving to `/objects`.

A local directory cannot be a remote authority. When the machine holding it is
gone, so is the object — so the local backend is what conformance and
development run on, not what makes a database recoverable somewhere else.
Expand Down Expand Up @@ -416,6 +429,31 @@ npm run test:durable:e2e # end-to-end against a real libchdb, under Bun
npm run test:durable:stack # the whole stack: Bun + libchdb + real object storage
```

Two more run by hand, because each needs something CI does not have. They
check the promises that only move when core releases, so the cost of running
them at adoption time rather than per-commit is low:

```sh
CHDB_LIBCHDB_OLD=…/26.7.2-rc.2/libchdb.so CHDB_LIBCHDB_NEW=…/26.7.3/libchdb.so \
npm run test:durable:cross-version # a newer engine restores an older one's archive

CHDB_PYTHON=/tmp/pyenv/bin/python CHDB_GO_REPO=/path/to/chdb-go \
npm run test:durable:cross-binding # Python and Go write, Node reads, and back
```

`cross-binding` skips a pairing whose runtime is absent and fails only when
neither is there, so it is worth running with just one installed. Go finds its
engine through `CHDB_LIB_PATH`, which the script points at this repository's
`libchdb.so` — worth knowing because the chdb-go checkout I ran against
declared `v26.7.3` while the library on disk was still `26.7.2-rc.2`, which
would have made this a cross-version test wearing a cross-binding label.

`cross-version` re-execs itself once per stage. A libchdb is `dlopen`ed once
per process — it has to be, since the engine binds one data path per process —
so a single-process version would load the first library and answer every
later request with it: both versions report the same number, the floor never
rises, and the script passes while proving nothing.

The last one is the arrangement a downstream actually deploys, and the only
place the other two suites' halves meet. Each of them covers one: a real engine
over a local directory, or a real bucket under a fake engine. Both can pass
Expand Down Expand Up @@ -453,7 +491,7 @@ CHDB_LIBCHDB_PATH=/path/to/chdb-core/buildlib/libchdb.so npm run test:durable:e2
`CHDB_LIBCHDB_PATH` is only needed when a platform package that predates the
durable ABI is installed. The resolver prefers `@chdb/lib-*` over anything in
the working tree, so a stale one shadows a local `chdb-core` build — and until
the RC platform packages are published, the newest on npm is
a Durable-capable platform package is published, the newest on npm is
`26.7.0-stable.1`, which exports none of the durable symbols. Either override
the path or `rm -rf node_modules/@chdb/lib-*`, which is what CI already does.
Running against a shadowing build fails with a message naming the missing ABI
Expand Down Expand Up @@ -497,12 +535,40 @@ Against the V1 conformance list, this binding's position:
| Single-writer races, fencing, heartbeat, failed and ambiguous commits | covered |
| Unknown-field round trips, size limits, secret redaction, close failure | covered |
| Full provider suite against a real object store | covered — AWS S3 and MinIO |
| Restoring full backups from *earlier* core releases | not applicable yet — `v26.7.2-rc.2` is the first release with the Durable ABI, so there is no earlier archive to restore |
| New-header/old-library and old-header/new-library ABI tests | not applicable yet — same reason; the promise starts here |
| Every writer's fixture read by two other bindings | pending — needs Python and Go to reach V1 |
| Restoring full backups from *earlier* core releases | covered — `npm run test:durable:cross-version` |
| Old-header/new-library ABI | covered — an addon compiled against `26.7.2-rc.2` headers runs the whole suite on `26.7.3` |
| New-header/old-library ABI | not covered — needs an addon built against headers newer than the engine it loads, which no released pair produces yet |
| Every writer's fixture read by two other bindings | covered — Python and Go, `npm run test:durable:cross-binding` |

The two "not applicable" rows become real on the second Durable-capable core
release, and the fixtures written now are what they will be tested against.
Two of these became testable when `26.7.3` shipped, since `26.7.2-rc.2` then
stopped being the only Durable-capable engine. Both scripts are run by hand
rather than in CI — one needs two engine downloads, the other a second
language runtime — and what they check moves only when core releases.

Measured on `26.7.2-rc.2` → `26.7.3`:

| Claim | Result |
| --- | --- |
| New engine restores a base archive the old engine wrote | holds |
| …and replays a WAL segment the old engine appended | holds |
| Writing raises `min_reader`, and the old engine is then refused `engine_incompatible` | holds |

Measured against Python `chdb` 4.4.0 and `chdb-go` (all three on the same
`26.7.3` engine, each through its own bindings, so a difference would be a
binding difference rather than a version one):

| Exchange | Python | Go |
| --- | --- | --- |
| Its base + WAL, restored and replayed by Node | holds | holds |
| Node appends to its object; it reads the append back | holds | holds |
| Node checkpoints; it RESTOREs from that archive with no WAL left | holds | holds |
| One `local:` URL string opened by both | holds | — |
| Also over a real object store (MinIO), not just a local directory | holds | — |

That covers the contract's requirement from this binding's side: an object Node
wrote is read by both other V1 implementations, and objects they wrote are read
by Node. The pairing the three still owe each other is Python ↔ Go, which is
not this repository's to run.

### Provider conformance

Expand Down Expand Up @@ -533,14 +599,21 @@ durable object written on one "machine" — one namespace instance, one engine,
one scratch tree — reopened on another that shares nothing but the bucket, for
both a WAL-only object and a checkpointed one.

Run against engine `26.7.2-rc.2`:
Run against engine `26.7.3`:

| Provider | Status |
| --- | --- |
| AWS S3 (`us-east-2`) | 11/11 conformance, plus 4/4 full stack |
| MinIO (`RELEASE.2025-09-07`) | 11/11 conformance, run repeatedly |
| MinIO (latest) | 11/11 conformance, plus 4/4 full stack, plus the cross-binding exchange |
| AWS S3 (`eu-central-1`) | 11/11 conformance, 4/4 full stack, and the cross-binding exchange, on `26.7.3` |
| Cloudflare R2 | not run — the code path is the same, but the claim is not made until measured |

One number is worth carrying: the 12 MiB large-object case takes about 0.4s
against a local MinIO and about a minute against a real bucket an ocean away,
so it carries its own 180s timeout rather than the suite's 20s. Without that
it is the one test that fails on AWS while the implementation is fine, and a
red suite meaning "your network is far away" is a suite people learn to
ignore.

The two that have run agree on every point, including the ETag behaviour above:
`If-None-Match: *` and `If-Match` are atomic on both, an eight-way race leaves
exactly one winner on both, and re-PUTting identical bytes advances the token on
Expand Down Expand Up @@ -664,5 +737,7 @@ this package:
which the loader prefers over a local build, and *that* is the case the
adapter refuses by name. Until a tag goes out, using `chdb/durable/node`
from a checkout means `npm run build` plus `rm -rf node_modules/@chdb/lib-*`.
2. Add the shared cross-binding fixtures from the chdb repository once they
exist, and read a Python-written object with them.
2. Adopt the shared cross-binding fixtures from the chdb repository once they
exist. `test:durable:cross-binding` proves Node, Python and Go agree as
live implementations, which is not quite the same claim — a fixture pins a
byte layout, so it catches three implementations drifting together.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@
"test:durable:node": "npm run build:ts && vitest run --config vitest.durable-node.config.ts",
"test:durable:e2e": "bun test --timeout 120000 test/durable/libchdb-e2e.bun.test.ts",
"test:durable:s3": "vitest run --config vitest.durable.config.ts test/durable/s3-backend.test.ts",
"test:durable:stack": "bun test --timeout 300000 test/durable/full-stack.bun.test.ts"
"test:durable:stack": "bun test --timeout 300000 test/durable/full-stack.bun.test.ts",
"test:durable:cross-version": "bun scripts/durable-cross-version.ts",
"test:durable:cross-binding": "npm run build:ts && node scripts/durable-cross-binding.mjs"
},
"author": {
"name": "chdb",
Expand Down
Loading
Loading