Warning
This is not authoritative documentation. It describes a plan of work that is not yet implemented and will change as it lands.
Task 1: slatedb-capi shim, container image, and spike¶
Repos: https://opendev.org/drizzle/drizzle (plugin/slatedb/capi/,
container) and drizzle-test (fixtures). Goal: de-risk the FFI and the
dependency before any engine code exists, and land the second
source-built engine dependency under the composition decision recorded
in the spec.
Part 1: the shim crate¶
plugin/slatedb/capi/: a Cargo crate, crate-type = ["cdylib"],
pinning slatedb = "=0.14.1" (or current at execution — record the
pin and why) with a committed Cargo.lock and
rust-toolchain.toml (1.91.1, matching upstream’s own pin). The
dependency enables the non-default compaction_filters cargo
feature from this first commit — task 7 needs it, and a feature flip is
not a thing to discover at the end of the series. One hand-written
header, slatedb_capi.h, with a SLATEDB_CAPI_VERSION integer the
plugin checks at init.
Surface (complete list — resist additions). Every function returns an
integer status and takes a trailing slatedb_error_t **out_err; on
any non-OK status the shim writes an owned error object there
and the caller frees it with slatedb_error_free:
slatedb_open(url, path, settings_toml_or_null, out_db, out_err)— builds the object store viaDb::resolve_object_store, spawns the tokio runtime, opens the Db.slatedb_close(db, out_err).slatedb_flush(db, out_err).slatedb_txn_begin(db, out_txn, out_err)(Snapshot isolation),slatedb_txn_commit(txn, out_err),slatedb_txn_rollback(txn, out_err).slatedb_txn_get(txn, key, keylen, out_value, out_err)withslatedb_value_free;slatedb_txn_put(txn, k, klen, v, vlen, out_err);slatedb_txn_delete(txn, k, klen, out_err).slatedb_txn_scan(txn, start, startlen, end, endlen, descending, out_scan, out_err),slatedb_scan_next(scan, out_k, out_v, out_err)(NOT_FOUND= exhausted),slatedb_scan_seek,slatedb_scan_close.slatedb_error_code(err),slatedb_error_message(err) -> const char*(borrowed until the free),slatedb_error_free(err).
There is no ``slatedb_last_error``. Per-handle error strings are
rejected in the spec’s FFI section, and the header’s comments must
carry the reason because it is the thing an implementer would
“simplify” back: DbTransaction::commit and rollback take
self by value in 0.14.1 (db_transaction.rs), so a failing
commit has no handle left to hold a message — and
slatedb_scan_close has the same shape. Hoisting the string onto the
Db handle would make it shared mutable state across every session
thread. Owned errors, uniformly, no exemptions.
The lifetime contract, stated in the header and asserted by the spike:
slatedb_txn_commitandslatedb_txn_rollbackalways consume the transaction handle, on success and on failure alike. On return the pointer is dangling.No separate free is required or permitted: there is no
slatedb_txn_free, and a second call on the same pointer is a use-after-free, not a leak.The shim moves the
DbTransactionout of its box and drops the box. It deliberately does not keep anOption<DbTransaction>to make double calls survivable — a shim that quietly tolerates use-after-free teaches the C++ side to write it.Consequently the C++ engine clears its session slot before it inspects the returned status; the error object outlives the handle by construction, which is the whole point of owning it.
Handles with a plain destructor (
slatedb_close,slatedb_scan_close,slatedb_value_free,slatedb_error_free) keep the ordinary paired registry pattern.
Status codes: OK, NOT_FOUND, CONFLICT, FENCED,
INVALID, IO. Every Rust Error maps into these in one
impl, which also builds the owned error object carrying code and
message.
Panics never cross the boundary: every entry point is wrapped in
catch_unwind returning IO.
Part 2: the container image¶
plugin/slatedb/container/Containerfile, same lineage as
WiredTiger’s: FROM quay.io/drizzle/libdrizzle:trixie AS build
installs the pinned Rust toolchain (rustup pinned by version and
checksum, or the distro toolchain if trixie’s rustc satisfies the pin —
decide once, record why), runs cargo build --release --locked,
installs libslatedb_capi.so + slatedb_capi.h into
/install/usr/local; final stage copies the payload onto
libdrizzle. Comment discipline matching the WiredTiger
Containerfile (every non-obvious choice explained in place).
Also in this task, per the composition decision in the spec: the
drizzle builder image gains the COPY --from= composition of
wiredtiger + slatedb-capi payloads and the payload-disjointness check
(find /install -type f | sort manifests compared with
uniq -d). This is the moment the one-off wiredtiger layering
becomes the pattern.
Part 3: the spike program¶
A standalone C++ program (plain Makefile, explicit flags, inside the image) exercising, in order:
Open against
file:///tmp/spike-db; put/get/delete round trips.A transaction: begin, put N keys, scan-before-commit sees them (read-your-own-writes), commit, reopen, scan sees them.
Two transactions racing a write to one key: loser’s commit returns
CONFLICT. The error path is under test, not incidental: the loser’sslatedb_error_t*is retrieved, its message asserted non-empty and logged, and freed — after the commit that produced it already consumed the transaction handle. Then the loser’s dangling handle is not touched again, and the spike asserts (under ASan, step 8) that no double-free or leak resulted.Descending scan and seek behave as documented.
Kill -9 the process mid-write-load with
await_durableon; reopen; verify every acknowledged commit survived.Against MinIO (fixture below): repeat 1–2 and measure commit latency at default
flush_interval; record the numbers in the README.Fencing: open the same path from a second process; verify the first process’s next write fails
FENCED, and that a commit from the fenced writer likewise fails with a retrievable owned error.The whole spike rebuilt with the C++ side under
-fsanitize=address -fno-omit-frame-pointer(ASAN_OPTIONS=detect_leaks=1) and run again, including the failing commit from step 3 and the fenced commit from step 7. The practical limit, stated rather than glossed:cargo -Zsanitizer=addressis nightly-only and this crate is pinned to stable 1.91.1, so the Rust side is not instrumented. It is built withRUSTFLAGS="-Cforce-frame-pointers=yes -Cdebuginfo=2"so ASan can symbolize through it, and the instrumented C++ links against that ordinary releasecdylib. What this catches is the class of bug that actually threatens this design — C++ use-after-free and double-free of consumed transaction handles, and leaked owned errors, values, and scans — not memory errors inside SlateDB. The README says exactly that, so a clean ASan run is never read as blessing the Rust.
Fixtures (drizzle-test): a MinIO image pinned by digest plus a kube
yaml + Justfile in the established shape (just images, just
up, just spike, just down). Local-filesystem mode means most
CI needs no MinIO at all; the MinIO job exists to keep S3 semantics
honest.
README deliverables: exact pins and why (including the
compaction_filters feature); commit-latency numbers from
steps 5–6; the ASan caveat from step 8; the cargo license transitive inventory with the
GPLv2-compat note per the spec’s license posture; any API gaps found.
Gate: if steps 2, 3, or 5 fail against the pinned release, stop and report — the transaction and durability semantics are the engine’s foundation and the plan re-sequences around what the library actually does. Step 8 is equally blocking: an owned-error contract that leaks is strictly worse than the per-handle string it replaced.
Verification¶
podman buildgreen for the capi image and the composed builder.Spike green under both
file://and MinIO, run by a non-voting Zuul job invouchedusing the identicaljusttargets.Header + crate reviewed as the FFI contract;
SLATEDB_CAPI_VERSIONstarts at 1. The consuming-call and owned-error rules are reviewed as the contract’s load-bearing clauses, not as comments.