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
name: CI
on:
push:
branches:
# Every branch, not just `main`. The work ships as a stack, so every
# PR but the bottom one targets another PR's branch; restricting the
# trigger to `main` meant CI never ran on any of them.
pull_request:
schedule:
# Nightly at 07:00 UTC - drives the long-running stress/soak jobs.
- cron: '0 7 * * *'
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
# The profile written for CI: a slower, noisier host gets a looser
# slow-timeout than a workstation, and one retry so a genuinely flaky
# test is reported as flaky rather than as a failure. See
# `.config/nextest.toml`.
NEXTEST_PROFILE: ci
RUSTFLAGS: -D warnings
# Every `run` step goes through `bash -eo pipefail` rather than
# GitHub's default `bash -e`. Without `pipefail` a step like
# `just cov-summary | tee summary.txt` reports `tee`'s exit status,
# so a recipe that died with "command not found" still concluded
# green and published an empty report.
defaults:
run:
shell: bash
jobs:
# All three desktop and server hosts. Windows is not a formality here:
# it is the only one of the three without `rustix`, so its file
# locking, directory sync and positioned reads take different code in
# `src/env/std_env.rs`, and nothing else in the matrix exercises them.
test:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
profile: ci
- os: macos-latest
profile: ci
# Windows runs its own nextest profile, which drops the long
# concurrency and corruption soaks. See `.config/nextest.toml`
# for what it drops and why: the soaks test engine logic that
# is identical on every platform, while this leg exists for the
# code only Windows runs.
- os: windows-latest
profile: windows
runs-on: ${{ matrix.os }}
env:
NEXTEST_PROFILE: ${{ matrix.profile }}
# Windows is the slow one: it serves an fsync about two orders of
# magnitude slower than a Linux tmpfs, and the `ci` nextest profile
# retries a failure once, so one slow test can cost twice. Exceeding
# this reports as a cancelled job, which reads like someone pressed a
# button rather than like a budget being hit.
timeout-minutes: 75
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@nextest
- uses: extractions/setup-just@v2
- run: just test
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- uses: extractions/setup-just@v2
- run: just lint
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- uses: extractions/setup-just@v2
- run: just fmt
doc:
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -D warnings
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: extractions/setup-just@v2
- run: just doc
alloc-budget:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# Exits non-zero the moment a hot path allocates more per
# operation than its budget.
- run: cargo bench --bench allocs
# Model-checks the arena memtable's publication protocol, the flush
# and compaction version handoffs, and the engine's read horizon under
# every interleaving loom can reach. The models live in
# `src/engine/loom_model/`; `--cfg loom` is what swaps the memtable's
# atomics for loom's instrumented ones. Cheap enough to run on every
# pull request: both profiles together are under four minutes.
#
# Both profiles run, and neither is redundant. Release explores the
# larger search on the biggest model. Debug is where the skip list's
# single-writer guard (invariant S2) exists at all, because it is a
# `debug_assert`, so the calibration that proves the guard fires on an
# unserialized insert only runs there.
# Model-checks the arena memtable's publication protocol, the flush
# and compaction version handoffs, and the engine's read horizon under
# every interleaving loom can reach. The models live in
# `src/engine/loom_model/`; `--cfg loom` is what swaps the memtable's
# atomics for loom's instrumented ones. Cheap enough to run on every
# pull request: both profiles together are under four minutes.
#
# Both profiles run, and neither is redundant. Release explores the
# larger search on the biggest model. Debug is where the skip list's
# single-writer guard (invariant S2) exists at all, because it is a
# `debug_assert`, so the calibration that proves the guard fires on an
# unserialized insert only runs there.
msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.90
- uses: Swatinem/rust-cache@v2
- uses: extractions/setup-just@v2
- run: just msrv
# wasm32-wasip1 is a supported target, not a target that merely
# compiles. This job builds the probe and runs the whole database
# lifecycle under wasmtime with a host preopen - open, put, get,
# delete, batch, scan, snapshot, iterate, compact, close, reopen,
# read back - and the probe exits non-zero on the first byte that
# does not match what it wrote. The sustained phase then 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.
deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check
coverage:
runs-on: ubuntu-latest
# Instrumented builds run several times slower than the gate, so the
# job carries its own ceiling rather than inheriting the workflow's.
timeout-minutes: 45
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v2
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest
- name: Run coverage
run: just cov-summary | tee coverage-summary.txt
- name: Export coverage report
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: cargo llvm-cov report --lcov --output-path lcov.info --ignore-filename-regex '(^|/)tools/'
- name: Upload coverage report
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
disable_search: true
fail_ci_if_error: true
files: lcov.info
use_oidc: true
- name: Publish coverage summary
run: |
{
echo "## Coverage"
echo ""
echo '```'
cat coverage-summary.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Soft-warn when line coverage drops below 90%
run: |
# Pulls the percentage off the TOTAL row of `cargo llvm-cov`'s
# summary table. The line-coverage column is the 10th field
# (Filename, Regions, Missed, Cover, Functions, Missed, Cover,
# Lines, Missed, Cover). Soft gate - emits an annotation but
# never fails the job.
total=$(awk '/^TOTAL/ {gsub("%", "", $10); print $10}' coverage-summary.txt)
if [ -z "$total" ]; then
echo "::warning::could not parse line-coverage percentage from coverage-summary.txt"
exit 0
fi
awk -v t="$total" 'BEGIN { exit !(t+0 < 90) }' \
&& echo "::warning::line coverage $total% is below the 90% soft floor" \
|| echo "line coverage $total% ≥ 90% ✓"
# Miri over the modules that carry the crate's `unsafe`: the arena's
# chunk layout and bump arithmetic, the skip list's node layout and
# publication, the memtable's arena-backed slices, and the block
# decoder. Interpreted execution is orders of magnitude slower than
# native, so this runs on the nightly schedule rather than per pull
# request. Two things are scaled down under `cfg(miri)` in the tests
# themselves, with every assertion left intact: the two spinning-reader
# stress tests are `#[ignore]`d in favour of an interpreter-sized
# equivalent, and the arena's multi-megabyte fills use a smaller
# budget.
# Long-running stress, soak, and fault-injection tests. Gated behind
# `#[ignore]` in the main crate so `cargo test` stays fast for PRs;
# this job unlocks them via `--ignored`. Runs on the nightly schedule,
# manual dispatch, or direct pushes to `main`, never on PRs.
nightly-stress:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v2
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@nextest
- name: Run ignored tests (stress / soak / concurrency)
run: just test-slow
# Durability, split out from `test` because it fails for different
# reasons and is worth reading on its own: crash recovery, power loss,
# the fault shim, the lifecycle sweep and the MVCC invariants.
# Fuzz targets, smoke-run for a bounded time so a harness that stopped
# building is caught. Finding new crashes is the nightly job's work,
# not a gate on a PR.
fuzz:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
target:
- fuzz_wal_decoder
- fuzz_sst_decoder
- fuzz_block_decoder
- fuzz_manifest_decoder
- fuzz_range_tombstone_decoder
- fuzz_writebatch
- fuzz_api_ops
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v2
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
- run: cargo install cargo-fuzz --locked
- name: Fuzz ${{ matrix.target }}
run: just fuzz ${{ matrix.target }} 300
# Elle consistency checking. Its own job because it needs a JVM and the
# elle-cli distribution, and because it fails for a reason nothing else
# does: a history whose dependency graph has a cycle.
#
# elle-cli is not vendored (37 MB), so the job fetches a pinned release.
# The performance artifact: one self-contained HTML page per commit on
# main, keyed by its sha and downloadable from the run.
#
# Last because it depends on everything above passing: publishing a
# perf page for a build whose tests failed invites reading numbers off
# a broken engine. Retention is 30 runs' worth of days rather than a
# count, which is the only retention GitHub offers; the prune step
# below deletes older ones explicitly so the count is what was asked
# for rather than what the calendar happened to leave.
# Each family on its own runner. A sweep that runs every benchmark in
# one job takes as long as the sum of them and reports nothing until
# the slowest finishes; split, the wall clock is the slowest single
# family and a failure names which one.
bench:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
family:
- point_read
- write_buffered
- write_durable
- scan
- scan_stream
- batch
- large_value
- transaction
- memory
- size
- allocs
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: extractions/setup-just@v2
- name: Refuse to measure a busy runner
id: guard
run: just loadguard
continue-on-error: true
- name: Run ${{ matrix.family }}
env:
REGOLITH_BENCH_OUT: ${{ github.workspace }}/bench-${{ matrix.family }}.jsonl
run: cargo bench --bench ${{ matrix.family }}
- uses: actions/upload-artifact@v4
with:
name: bench-${{ matrix.family }}
path: bench-${{ matrix.family }}.jsonl
if-no-files-found: error
retention-days: 7
# Assemble every family into one run document, file it alongside every
# earlier run, and publish the dashboard that reads the whole set.
#
# The run documents are the artifact, not a rendered page. A page
# regenerated per run shows one run and loses the comparison the
# moment it is replaced; keeping the documents is what lets the
# dashboard plot a metric across every run ever recorded.
perf-site:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: actions/download-artifact@v4
with:
pattern: bench-*
path: bench-artifacts
merge-multiple: true
- name: Assemble the run document
run: |
set -euo pipefail
cat bench-artifacts/*.jsonl > bench-out.jsonl
REGOLITH_BENCH_OUT="$PWD/bench-out.jsonl" \
cargo bench --bench collect -- \
--out "run-${GITHUB_SHA::12}.json" \
--commit "${GITHUB_SHA}" --label main
- name: Fetch the runs already published
run: |
set -euo pipefail
mkdir -p site
# `keep_files` on the publish step preserves what is already
# there, so only this run's document has to be produced here.
cp tools/perf-site/index.html site/index.html
- name: Fetch the existing history
continue-on-error: true
run: |
set -euo pipefail
git fetch origin gh-pages --depth=1
git --work-tree=site checkout origin/gh-pages -- runs 2>/dev/null || true
- name: File this run
run: python3 tools/perf-site/publish.py "run-${GITHUB_SHA::12}.json" site
# Refuse to publish a dashboard that would drop a family it
# collected. A blank section is not an error to the browser, so
# without this the job stays green and the site goes quiet.
- name: Check the page renders every collected family
run: node tools/perf-site/render_check.mjs site
- name: Publish
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
publish_branch: gh-pages
keep_files: true
commit_message: "perf: record run ${{ github.sha }}"
- name: Link
run: |
{
echo "## Performance dashboard"
echo ""
echo "https://${GITHUB_REPOSITORY%%/*}.github.io/${GITHUB_REPOSITORY##*/}/"
} >> "$GITHUB_STEP_SUMMARY"