regolith 0.1.1

ACID, performance oriented, embedded key-value database engine for edge systems
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434


default:
    @just --list

# Everything CI enforces on every push, in the order it fails fastest.
gate: fmt lint doc test deny

fmt:
    cargo fmt --all -- --check

fmt-fix:
    cargo fmt --all

lint:
    cargo clippy --workspace --all-targets -- -D warnings

doc:
    RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps

# nextest, not `cargo test`: each test gets its own process, so a
# wedged test fails on the profile's slow-timeout instead of holding the
# whole binary. See `.config/nextest.toml`.

test:
    cargo nextest run --workspace
    cargo test --workspace --doc

deny:
    cargo deny check

msrv:
    cargo "+$(grep -m1 '^rust-version' Cargo.toml | cut -d'"' -f2)" check --workspace

# The summary line CI annotates a build with.
# Through nextest, like the gate: one process per test, so a wedged
# test fails on the profile's slow-timeout instead of holding the whole
# instrumented run open until the job's ceiling.
cov-summary:
    # The `ci` profile, not the default: instrumented code runs several
    # times slower, and the default profile's 60s slow-timeout turns the
    # heavier soaks into timeouts that say nothing about the code.
    cargo llvm-cov nextest --summary-only --workspace --profile ci

# The browsable HTML report, for reading locally.
cov:
    cargo llvm-cov --workspace --html
    @echo "report: target/llvm-cov/html/index.html"

# ── suites CI runs as their own jobs, split by mechanism ─────

test-fault:
    cargo test --test fault_smoke

# The `#[ignore]`d fault tests: each spawns child processes and simulates a
# power cut. Measured at well under a second each, but kept out of the
# default run so `cargo test` stays quick.

test-fault-slow:
    cargo nextest run --test fault_smoke

# The `#[ignore]`d resource-exhaustion and extremes tests: six-figure key
# counts, a 64 MiB value, megabyte keys, a six-level cascade, and a real
# ENOSPC on a tmpfs mounted in a private user namespace. About 21 s in
# total on a debug build. `--test-threads=1` is required, not cosmetic:
# each test resets the kernel's peak-RSS counter to report its own memory
# high-water mark, and a concurrent test would inflate that number.

test-crash:
    cargo test --test crash_recovery

# Handle lifecycle: open, close, reopen under changed Options, locking,
# and every misuse of a closed or read-only handle. Measured at 0.4s, so
# every test in the file also runs in the default `cargo test`; nothing
# here is `#[ignore]`d.

test-power:
    cargo test --test power_loss -- --skip crash_child --nocapture

# Process-crash recovery: kill -9 at every point in the write path. Every
# test in the file is in the default run already; this recipe just runs the
# file on its own. Measured at 0.6s, spawning 41 child processes.

test-corruption-slow:
    cargo nextest run --test corruption_exhaustive

# The power-loss durability tests, with output shown so the measured cost of
# the default DurabilityMode::Eventual is visible. Every test here spawns a
# child process, crashes it at a byte-exact point and simulates a power cut.
# Measured at 0.6s in total, so it also runs in the default `cargo test`.

test-durability-slow:
    cargo nextest run --test proptest_durability

# Every ignored test in the workspace, including the scheduled stress runs.

test-extremes:
    cargo nextest run --test resource_limits --no-capture --test-threads 1

# The `#[ignore]`d durability property test: 128 randomized operation
# sequences, each run in a child process that is killed part way through
# and then power-cut by discarding every byte it never fsynced. Measured
# at 1.1 s. Needs the LD_PRELOAD fault shim, so it is Linux only.

test-lifecycle:
    cargo test --test lifecycle -- --skip crash_child

# MVCC and concurrency invariants: snapshot stability under concurrent
# writers and compaction, WriteBatch atomicity seen by concurrent readers,
# monotonic reads, version integrity across delete/compact/reopen, and
# iterators pinned across compactions that unlink their files. Measured at
# 0.5s, so every test in the fast set also runs in the default `cargo test`.

test-slow:
    cargo nextest run --workspace --release --no-capture

# Rebuild the LD_PRELOAD fault shim from scratch by dropping its cache.

fault-shim-clean:
    rm -rf target/tmp/regolith-fault

# The `#[ignore]`d exhaustive corruption sweeps: every byte offset and
# every single-bit flip of a WAL, an SSTable and a MANIFEST. 14,265
# trials, measured at 1.2s of wall time.

mvcc:
    cargo test --test mvcc_invariants -- --skip crash_child

# The `#[ignore]`d full-scale MVCC soaks: 120,000 writes racing a snapshot
# that pins every version of them, 30,000 WriteBatch generations checked by
# four readers, 1.9M monotonic point reads, and the focused gate for the
# user-thread `compact_range` read race. Measured at 13.8s + 9.3s + 4.5s +
# 26s on a debug build.
#
# This recipe is RED today, and that is the point: the focused gate finds a
# real read-path defect. See the doc comment on
# `a_user_thread_compact_range_never_makes_a_read_travel_backwards`.

mvcc-slow:
    cargo nextest run --test mvcc_invariants --no-capture

set shell := ["bash", "-uc"]

gains_dir := justfile_directory() / "../regolithgains"

label     := env_var_or_default("LABEL", "wip")

py        := env_var_or_default("REGOLITHGAINS_PY", "python3")

# NOTE: the commit sha is resolved INSIDE the recipe, never as a top-level
# `sha := `git ...`` assignment. just evaluates those eagerly at parse time, so a
# top-level backtick makes every `just --list` fail outside a git checkout.

# ---------- the gate ----------

fuzz target time="300":
    cargo +nightly fuzz run {{target}} -- -max_total_time={{time}}

# ---------- benchmark gating ----------

# Exit 1 when the host is too busy to trust a measurement. A dependency, not advice.

loadguard:
    {{py}} {{gains_dir}}/loadguard.py

# ---------- benchmarks ----------

# Capture the reference baseline. Run ONCE, before any stack code lands.

bench-baseline: loadguard
    cargo bench --bench point_read --bench write_durable --bench write_buffered \
                --bench scan --bench batch --bench transaction \
                --bench large_value -- --save-baseline pre

# Compare against the `pre` baseline. This is what a PR pastes into its body.

bench: loadguard
    cargo bench -- --baseline pre

# The sweep the CI perf artifact is built from: every family, into the
# JSON Lines file `collect` assembles a run file out of.
bench-collect: loadguard
    cargo bench --bench point_read --bench write_durable --bench write_buffered \
                --bench scan --bench batch --bench transaction \
                --bench large_value --bench memory --bench size

bench-one name: loadguard
    cargo bench --bench {{name}} -- --baseline pre

# RSS soak. Default 360s; pass seconds and an Options variant tag.

soak secs="360" wb="64" cache="64" shard_bits="6" tag="default": loadguard
    cargo bench --bench soak -- {{secs}} {{wb}} {{cache}} {{shard_bits}} {{tag}}

# The two soak variants compared. Deterministic: no loadguard needed.

soak-pair:
    just soak 360 64 64 6 default
    just soak 360 64 64 0 cache-budgeted

# Binary size, native and both wasm targets, against the budget.

size:
    cargo bench --bench size

# The memory table in the README: every profile, on both hosts, one
# workload. The wasm column is the reproducible one because linear
# memory only ever grows; RSS moves between runs.

wasm-budget puts="20000":
    #!/usr/bin/env bash
    set -euo pipefail
    cargo build --release --example embedded_profile --target wasm32-wasip1
    for profile in embedded wasm default; do
        echo "== $profile, x86_64 Linux =="
        d=$(mktemp -d)
        cargo run --release --quiet --example embedded_profile -- "$d" "$profile" {{puts}}
        rm -rf "$d"
        echo "== $profile, wasm32-wasip1 under wasmtime =="
        d=$(mktemp -d)
        wasmtime run --dir="$d::/data" \
            target/wasm32-wasip1/release/examples/embedded_profile.wasm \
            /data "$profile" {{puts}}
        rm -rf "$d"
    done

# Point memory probes.

mem:
    cargo bench --bench memory

# The MVCC regression probes. Must stay at zero violations.

ycsb workload="a" records="1000000" ops="1000000": loadguard
    cargo run --release -p regolith-ycsb -- \
        --workload {{workload}} --records {{records}} --operations {{ops}}

ycsb-all: loadguard
    for w in a b c d e f; do just ycsb $w; done

stress secs="600":
    cargo run --release -p regolith-stress -- --duration {{secs}}

# ---------- model checking ----------

# Loom model checks for the arena memtable, read horizon and version
# handoffs. `--cfg loom` swaps the primitives in `src/engine/sync.rs`
# for loom's instrumented ones; without it the whole target compiles
# away, so an ordinary `cargo test` neither builds loom nor runs these.
#
# | recipe        | models | profile | measured |
# |---------------|--------|---------|----------|
# | `loom`        | 14     | release | 19.7s    |
# | `loom-debug`  | 15     | debug   | 133s     |
# | `loom-all`    | both   | both    | ~153s    |
#
# The debug run carries one extra calibration: the skip list's
# single-writer guard (S2) is a `debug_assert`, so the model proving it
# fires is compiled out of a release build. Six of the models are
# `should_panic` calibrations that deliberately get the ordering wrong;
# they are what make the passes mean anything.

loom:
    RUSTFLAGS="--cfg loom" cargo test --release --test loom_memtable

loom-debug:
    RUSTFLAGS="--cfg loom" cargo test --test loom_memtable

loom-all: loom loom-debug

# The read-view chaos workload at full size: 6 instances x 2 rounds x 400
# versions. Measured at over 20 minutes wall and 4h of CPU unoptimized,
# which is why `cargo test` runs a smaller default and this recipe
# carries the full one. Release, because debug is where the cost is.
#
# Sized to finish, not to be maximal. Cost is roughly
# instances x rounds x versions x compaction passes, and the compaction
# passes each rewrite a database that grows with the version count, so
# raising `versions` raises the run time faster than linearly: 400 does
# not complete inside seven minutes, 120 completes in seconds. What the
# workload is hunting is overlap between a compaction and a read, and
# the overlap count is already in the thousands per instance here.

chaos instances="4" rounds="2" versions="120" min_rounds="20":
    REGOLITH_CHAOS_INSTANCES={{instances}} REGOLITH_CHAOS_ROUNDS={{rounds}} \
    REGOLITH_CHAOS_VERSIONS={{versions}} REGOLITH_CHAOS_MIN_ROUNDS={{min_rounds}} \
        cargo test --release --test read_view_chaos_workload -- --nocapture

# ---------- consistency ----------

# Elle consistency checking. `model` is the workload (list-append or
# rw-register); `level` is the consistency model to check it against.
#
# The two are separate axes and elle-cli spells both `--model`-ish, which
# is easy to get wrong: passing an isolation level as --model throws
# "No matching clause". Hence the explicit --consistency-models here.
elle model="list-append" level="snapshot-isolation" isolation="repeatable-read":
    cargo run --release --manifest-path harness/elle/Cargo.toml --bin elle-gen -- \
        --model {{model}} --isolation {{isolation}} \
        --threads 8 --txns 50 --keys 4 \
        --out /tmp/regolith-history.json --dir /tmp/regolith-elle-db
    java -jar harness/elle/elle-cli.jar --model {{model}} \
        --consistency-models {{level}} /tmp/regolith-history.json

# The same, with the fault injection the harness supports.
elle-fault model="list-append" level="snapshot-isolation":
    cargo run --release --manifest-path harness/elle/Cargo.toml --bin elle-gen -- \
        --model {{model}} --isolation repeatable-read --faults \
        --threads 8 --txns 50 --keys 4 \
        --out /tmp/regolith-history-fault.json --dir /tmp/regolith-elle-fault-db
    java -jar harness/elle/elle-cli.jar --model {{model}} \
        --consistency-models {{level}} /tmp/regolith-history-fault.json

# Every level regolith claims, checked in one go.
#
# Each line prints `true` or `false`; a `false` on a level regolith claims is
# a real failure and the recipe exits non-zero.
elle-matrix:
    #!/usr/bin/env bash
    set -uo pipefail
    cd harness/elle
    cargo build --release --bin elle-gen
    fail=0
    check() {
        local name="$1" model="$2" level="$3"; shift 3
        ./target/release/elle-gen --model "$model" "$@" \
            --out "/tmp/elle-$name.json" --dir "/tmp/elle-db-$name" >/dev/null
        # elle-cli prints "<path>\t<true|false>"; take the last field and
        # strip surrounding whitespace, so a stray tab cannot read as a
        # failure on a history that actually passed.
        local v
        v=$(java -jar elle-cli.jar --model "$model" \
            --consistency-models "$level" "/tmp/elle-$name.json" \
            | tail -1 | awk '{print $NF}')
        printf '  %-42s %s\n' "$name [$level]" "$v"
        if [ "$v" != "true" ]; then fail=1; fi
    }
    # Optimistic transactions are snapshot isolation. That is the level
    # regolith claims, so a false here is a defect.
    check optimistic-si       list-append snapshot-isolation --isolation repeatable-read --threads 8 --txns 50 --keys 4 --seed 4
    check optimistic-rw       rw-register snapshot-isolation --isolation repeatable-read --threads 8 --txns 50 --keys 4 --seed 5
    # Pessimistic transactions are checked at the level they request.
    check pessimistic-rc      list-append read-committed     --isolation read-committed --threads 8 --txns 50 --keys 4 --seed 2
    check pessimistic-hotkey  list-append read-committed     --isolation read-committed --threads 8 --txns 50 --keys 1 --seed 1
    # Serializable validates the whole read set, so the strongest model
    # Elle offers must hold.
    check serializable        list-append strict-serializable --isolation serializable --threads 8 --txns 60 --keys 4 --seed 11
    check serializable-rw     rw-register strict-serializable --isolation serializable --threads 8 --txns 60 --keys 4 --seed 12
    exit $fail

# ---------- portability ----------

# Full wasm32-wasip1 lifecycle under wasmtime. Non-zero on the first wrong byte.

wasm records="5000" sustained="20000":
    #!/usr/bin/env bash
    set -euo pipefail
    # open, put, get, delete, batch, scan, snapshot, iterate, compact,
    # close, REOPEN, read back - then `--sustained` writes past the L0
    # stop trigger with a 32 KiB memtable and no explicit compaction,
    # which is the case that wedges when nothing compacts on the
    # calling thread.
    cargo build --release -p regolith-wasm-probe --target wasm32-wasip1
    # Both shipped profiles a wasm module can open with, on the real
    # target: `embedded` runs with no block cache, `wasm` with one.
    for profile in embedded wasm; do
        echo "== profile $profile =="
        d=$(mktemp -d)
        wasmtime run --dir="$d::/data" \
            target/wasm32-wasip1/release/regolith-wasm-probe.wasm -- \
            --profile "$profile" --records {{records}} --sustained {{sustained}} \
            --probe-host --report-memory
        rm -rf "$d"
    done

# The same lifecycle natively, to tell a regolith bug apart from a wasm one.

wasm-native records="5000" sustained="20000":
    #!/usr/bin/env bash
    set -euo pipefail
    for profile in embedded wasm; do
        echo "== profile $profile =="
        cargo run --release -p regolith-wasm-probe -- \
            --profile "$profile" --records {{records}} --sustained {{sustained}} \
            --probe-host --report-memory
    done

# The OPFS contract against a real browser. `wasm-pack test` cannot
# drive these: it appends `--tests`, which builds every target in
# `tests/`, and all but the three `wasm_opfs*` files are native-only.
# The runner is named per target instead, so only the named test
# binaries are built.
wasm-browser:
    #!/usr/bin/env bash
    set -euo pipefail
    export CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner
    # wasm-bindgen's default per-test budget is 20s, which a headless
    # browser on a shared runner can exceed on a test that mounts OPFS
    # and writes through real sync access handles. Exceeding it kills
    # the whole driver, so nine passing tests report as one failure with
    # no attribution.
    export WASM_BINDGEN_TEST_TIMEOUT=180
    for suite in wasm_opfs wasm_opfs_main wasm_opfs_memory; do
        echo "== $suite =="
        cargo test --target wasm32-unknown-unknown --test "$suite"
    done

embedded:
    cargo bench --bench memory -- --profile embedded

# ---------- the gains figures ----------

# Collect every family into ONE run file and re-render. A PR runs this, then pastes.

gains: loadguard
    #!/usr/bin/env bash
    set -euo pipefail
    sha=$(git rev-parse --short HEAD)
    cargo bench -- --baseline pre --save-baseline "$sha"
    cargo bench --bench collect -- \
        --out {{gains_dir}}/runs/"$sha"-{{label}}.json \
        --commit "$sha" --label {{label}}
    just gains-render

gains-render:
    {{py}} {{gains_dir}}/render.py
    {{py}} {{gains_dir}}/render_rss.py

gains-diff base current:
    {{py}} {{gains_dir}}/render.py --baseline {{base}} --current {{current}}
    {{py}} {{gains_dir}}/render_rss.py --baseline {{base}} --current {{current}}

gains-list:
    {{py}} {{gains_dir}}/runs.py --list