test(db2-7): gate leg for WO_DATA=<file> — residency section 8, db-bench --wo-data-file
- residency-accept.sh section 8 (11 checks): file-form seed -> restart prints
the directory form's line; `find -mindepth 1` shows exactly app.db; missing
parent exits 2 naming path + parent, no mkdir -p; a fifo exits 2 "neither a
regular file nor a directory"; `d/` still writes d/shard-0.wal; `nodir/`
keeps the pre-7 "cannot open .../nodir//shard-0.wal" bytes; WO_EPHEMERAL=1
with the file exits 2 on the 6a conflict
- kill -9 battery against app.db: stdbuf -oL vehicle, asserts the kill landed
(rc 137) before verifying every acked row replays; forced compaction
(WO_CHECKPOINT_BYTES=1, WO_WAL_STATS proves >= 1 ran) leaves app.db the only
artifact and every row replays
- failing-first on the pre-7 wovm: 9 of 12 new checks red ("cannot open
.../app.db/shard-0.wal"); the two trailing-slash pins and the 6a conflict
pass by construction — they pin what must stay byte-identical
- db-bench.py --wo-data-file: restart proof + crash battery against
<tmp>/app.db, legs tagged .file, file form also asserts app.db is the only
artifact; no metric, bench/baseline.json untouched; quick run unchanged
without the flag (181 checks / 5 failures both ways, all five the known
residency.keys.fit rc 74)
- READMEs: db-bench env-knob row for WO_DATA=<path>.db + the driver flag;
residency run instructions name the file form
- gates: just residency 32/1 (the seed rc, pre-existing), make -C runtime
test 21 suites 8452/0, just oop-e2e 129/0
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit aaea6b2c0f818efd0cdf472ccbd9bdd09eac5554)
This commit is contained in:
parent
37c24e13bc
commit
58dcb1f969
4 changed files with 137 additions and 10 deletions
|
|
@ -37,6 +37,7 @@ strictly better. Recorded as a plan deviation.)
|
||||||
| `WO_SHARDS=<n>` | shard count. **`1` means every statement runs inline on shard 0 and group commit cannot engage** — batches form only where writes queue from other shards |
|
| `WO_SHARDS=<n>` | shard count. **`1` means every statement runs inline on shard 0 and group commit cannot engage** — batches form only where writes queue from other shards |
|
||||||
| `WO_CHECKPOINT_BYTES` / `WO_CHECKPOINT_RATIO` | **databasev2 3:** the checkpoint trigger — the log must exceed the floor AND exceed the ratio times the last compaction's own size. A tiny floor forces compaction in a few writes, which is how the gate tests the policy at all; an enormous one disables it, which is how the checkpoint leg measures the same workload with and without |
|
| `WO_CHECKPOINT_BYTES` / `WO_CHECKPOINT_RATIO` | **databasev2 3:** the checkpoint trigger — the log must exceed the floor AND exceed the ratio times the last compaction's own size. A tiny floor forces compaction in a few writes, which is how the gate tests the policy at all; an enormous one disables it, which is how the checkpoint leg measures the same workload with and without |
|
||||||
| `WO_WAL_STATS=1` | **databasev2 4:** print one line at exit — `walstats batches=… records=… peak_batch=… peak_staged=… compactions=… compact_us_max=… compact_us_total=… compacted_bytes=…`. Opt-in so it does not pollute every durable program's output. Mean batch is `records/batches`; **mean 1.0 means group commit is not engaging**, which is expected for a serial writer or `WO_SHARDS=1` and a bug anywhere else |
|
| `WO_WAL_STATS=1` | **databasev2 4:** print one line at exit — `walstats batches=… records=… peak_batch=… peak_staged=… compactions=… compact_us_max=… compact_us_total=… compacted_bytes=…`. Opt-in so it does not pollute every durable program's output. Mean batch is `records/batches`; **mean 1.0 means group commit is not engaging**, which is expected for a serial writer or `WO_SHARDS=1` and a bug anywhere else |
|
||||||
|
| `WO_DATA=<path>.db` | **databasev2 7:** the store as ONE file — the path IS the log (created if absent, its parent must exist; a directory or trailing `/` keeps the `<dir>/shard-0.wal` form). `scripts/db-bench.py --wo-data-file` runs the restart proof and the kill -9 battery against `<tmp>/app.db` instead of a directory; same acceptance, no metric, baseline untouched |
|
||||||
|
|
||||||
**Do not put `WO_DATA` on `/tmp`.** It is `tmpfs` on the reference machine,
|
**Do not put `WO_DATA` on `/tmp`.** It is `tmpfs` on the reference machine,
|
||||||
where `fdatasync` is free: the same `wmix` run measured **195 000 ops/s at p50
|
where `fdatasync` is free: the same `wmix` run measured **195 000 ops/s at p50
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ one data directory:
|
||||||
|
|
||||||
```
|
```
|
||||||
compiler/_build/default/bin/woc --emit docs/examples/residency/main.wo -o /tmp/residency.wob
|
compiler/_build/default/bin/woc --emit docs/examples/residency/main.wo -o /tmp/residency.wob
|
||||||
mkdir -p /tmp/residency-data # WO_DATA must exist; wovm will not create it
|
mkdir -p /tmp/residency-data # WO_DATA=<dir> must exist; wovm will not create it (or WO_DATA=<path>.db: one file, parent must exist)
|
||||||
WO_DATA=/tmp/residency-data runtime/wovm /tmp/residency.wob seed
|
WO_DATA=/tmp/residency-data runtime/wovm /tmp/residency.wob seed
|
||||||
WO_DATA=/tmp/residency-data runtime/wovm /tmp/residency.wob order
|
WO_DATA=/tmp/residency-data runtime/wovm /tmp/residency.wob order
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ into one results JSON, runs the durability legs (restart proof + kill -9
|
||||||
battery), samples RSS/fd during the mix phase (LW_SOAK discipline), and
|
battery), samples RSS/fd during the mix phase (LW_SOAK discipline), and
|
||||||
evaluates every metric against bench/baseline.json.
|
evaluates every metric against bench/baseline.json.
|
||||||
|
|
||||||
scripts/db-bench.py [--quick] [--write-baseline]
|
scripts/db-bench.py [--quick] [--write-baseline] [--wo-data-file]
|
||||||
|
|
||||||
Exit 0 = campaign green; 1 = gate breach or a durability leg failed.
|
Exit 0 = campaign green; 1 = gate breach or a durability leg failed.
|
||||||
Plan deviation, disclosed: one python driver instead of bash+python —
|
Plan deviation, disclosed: one python driver instead of bash+python —
|
||||||
|
|
@ -30,6 +30,16 @@ RESULTS_DIR = os.path.join(ROOT, "bench/results")
|
||||||
|
|
||||||
QUICK = "--quick" in sys.argv
|
QUICK = "--quick" in sys.argv
|
||||||
WRITE_BASELINE = "--write-baseline" in sys.argv
|
WRITE_BASELINE = "--write-baseline" in sys.argv
|
||||||
|
# databasev2 7: WO_DATA may name THE log file instead of a directory. The flag
|
||||||
|
# points the durability legs (restart proof, kill -9 battery) at <tmp>/app.db.
|
||||||
|
# Same wo_wal_* path underneath — a guard on main.c's resolution under the
|
||||||
|
# honest crash, not a new measurement: no metric, baseline untouched.
|
||||||
|
FILE_FORM = "--wo-data-file" in sys.argv
|
||||||
|
FORM = ".file" if FILE_FORM else ""
|
||||||
|
def store(d):
|
||||||
|
"""a durability leg's WO_DATA: the directory, or with --wo-data-file the
|
||||||
|
one file <d>/app.db (its parent must exist; wovm never runs mkdir -p)"""
|
||||||
|
return os.path.join(d, "app.db") if FILE_FORM else d
|
||||||
|
|
||||||
N = 2000 if QUICK else 20000
|
N = 2000 if QUICK else 20000
|
||||||
# databasev2 4: the write-concurrent leg. `mix` writes on one op in ten with
|
# databasev2 4: the write-concurrent leg. `mix` writes on one op in ten with
|
||||||
|
|
@ -227,19 +237,22 @@ def durability(metrics):
|
||||||
# restart proof
|
# restart proof
|
||||||
data = os.path.join(ROOT, "bench", f"tmp.{os.getpid()}.restart.{s}")
|
data = os.path.join(ROOT, "bench", f"tmp.{os.getpid()}.restart.{s}")
|
||||||
os.makedirs(data, exist_ok=True)
|
os.makedirs(data, exist_ok=True)
|
||||||
env["WO_DATA"] = data
|
env["WO_DATA"] = store(data)
|
||||||
rc1, _, _, _ = run(["seed", "3000"], env, 300)
|
rc1, _, _, _ = run(["seed", "3000"], env, 300)
|
||||||
rc2, lines, _, _ = run(["verify"], env, 300)
|
rc2, lines, _, _ = run(["verify"], env, 300)
|
||||||
if rc1 == 0 and rc2 == 0:
|
# file form: app.db must be the ONLY artifact (no shard-0.wal, no
|
||||||
ok(f"restart.{s}: seeded store replays byte-true")
|
# .compact temp); the directory form has nothing to assert here
|
||||||
|
arts = sorted(os.listdir(data))
|
||||||
|
if rc1 == 0 and rc2 == 0 and (not FILE_FORM or arts == ["app.db"]):
|
||||||
|
ok(f"restart.{s}{FORM}: seeded store replays byte-true")
|
||||||
else:
|
else:
|
||||||
bad(f"restart.{s}", f"seed rc={rc1} verify rc={rc2} {lines[-1:]}")
|
bad(f"restart.{s}{FORM}", f"seed rc={rc1} verify rc={rc2} {lines[-1:]} artifacts={arts}")
|
||||||
shutil.rmtree(data, ignore_errors=True)
|
shutil.rmtree(data, ignore_errors=True)
|
||||||
# crash battery: kill -9 mid-wal, verify every acked row
|
# crash battery: kill -9 mid-wal, verify every acked row
|
||||||
for rep in range(CRASH_REPS):
|
for rep in range(CRASH_REPS):
|
||||||
data = os.path.join(ROOT, "bench", f"tmp.{os.getpid()}.crash.{s}.{rep}")
|
data = os.path.join(ROOT, "bench", f"tmp.{os.getpid()}.crash.{s}.{rep}")
|
||||||
os.makedirs(data, exist_ok=True)
|
os.makedirs(data, exist_ok=True)
|
||||||
env["WO_DATA"] = data
|
env["WO_DATA"] = store(data)
|
||||||
e = dict(os.environ); e.update(env)
|
e = dict(os.environ); e.update(env)
|
||||||
p = subprocess.Popen([BIN, "wal", str(WAL_N)], stdout=subprocess.PIPE,
|
p = subprocess.Popen([BIN, "wal", str(WAL_N)], stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL, text=True, env=e)
|
stderr=subprocess.DEVNULL, text=True, env=e)
|
||||||
|
|
@ -252,11 +265,11 @@ def durability(metrics):
|
||||||
acked = int(l.split()[1])
|
acked = int(l.split()[1])
|
||||||
rc, lines, _, _ = run(["verify-acked", str(max(acked, 1))], env, 300)
|
rc, lines, _, _ = run(["verify-acked", str(max(acked, 1))], env, 300)
|
||||||
if acked > 0 and rc == 0:
|
if acked > 0 and rc == 0:
|
||||||
ok(f"crash.{s}.{rep}: {acked} acked rows all present after kill -9")
|
ok(f"crash.{s}.{rep}{FORM}: {acked} acked rows all present after kill -9")
|
||||||
elif acked == 0:
|
elif acked == 0:
|
||||||
ok(f"crash.{s}.{rep}: killed before first ack (nothing owed)")
|
ok(f"crash.{s}.{rep}{FORM}: killed before first ack (nothing owed)")
|
||||||
else:
|
else:
|
||||||
bad(f"crash.{s}.{rep}", f"acked={acked} verify rc={rc} {lines[-1:]}")
|
bad(f"crash.{s}.{rep}{FORM}", f"acked={acked} verify rc={rc} {lines[-1:]}")
|
||||||
shutil.rmtree(data, ignore_errors=True)
|
shutil.rmtree(data, ignore_errors=True)
|
||||||
|
|
||||||
def gate(metrics):
|
def gate(metrics):
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,119 @@ else
|
||||||
bad "resident:keys refusal fixture compiles" "$(head -1 "$WORK/e")"
|
bad "resident:keys refusal fixture compiles" "$(head -1 "$WORK/e")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ---- 8. WO_DATA=<file>: the store as one file ------------------------------
|
||||||
|
# databasev2 7: WO_DATA names either a directory (-> <dir>/shard-0.wal, as it
|
||||||
|
# always did) or THE log file. Underneath it is the same wo_wal_* path, so
|
||||||
|
# these legs are a regression guard on main.c's resolution and its two new
|
||||||
|
# refusals, plus proof that compaction's temp (<file>.compact) never leaves a
|
||||||
|
# second artifact beside the file. Refusal legs run under `timeout`: a wrong
|
||||||
|
# resolution that opened the fifo would block, not fail.
|
||||||
|
F8="$WORK/f8"
|
||||||
|
mkdir -p "$F8/a" "$F8/d" "$F8/c" "$F8/k"
|
||||||
|
if [[ -f "$WORK/mix.wob" ]]; then
|
||||||
|
# (i) seed -> restart prints exactly what the directory form printed in 1
|
||||||
|
WO_DATA="$F8/a/app.db" "$WOVM" "$WORK/mix.wob" seed >"$F8/seed.out" 2>&1; rc=$?
|
||||||
|
got="$(WO_DATA="$F8/a/app.db" "$WOVM" "$WORK/mix.wob" 2>&1)"
|
||||||
|
[[ $rc -eq 0 && "$got" == "kept=1 scratch=0" ]] \
|
||||||
|
&& ok "file form: seed rc 0, restart replays the durable row, volatile gone" \
|
||||||
|
|| bad "file form restart" "seed rc=$rc got: $got $(head -1 "$F8/seed.out")"
|
||||||
|
arts="$(find "$F8/a" -mindepth 1 -printf '%P\n' | sort | tr '\n' ' ')"
|
||||||
|
[[ "$arts" == "app.db " ]] \
|
||||||
|
&& ok "file form: app.db is the only artifact (no shard-0.wal, no .compact)" \
|
||||||
|
|| bad "file form single artifact" "found: '$arts'"
|
||||||
|
# (ii) missing parent: exit 2, name the path AND the parent, create nothing
|
||||||
|
out="$(WO_DATA="$F8/nope/app.db" timeout 10 "$WOVM" "$WORK/mix.wob" seed 2>&1)"; rc=$?
|
||||||
|
[[ $rc -eq 2 ]] && grep -Eq 'its parent .*/nope is not an existing directory' <<<"$out" \
|
||||||
|
&& grep -q 'nope/app.db' <<<"$out" && [[ ! -e "$F8/nope" ]] \
|
||||||
|
&& ok "file form: missing parent exits 2, names path + parent, runs no mkdir -p" \
|
||||||
|
|| bad "file form missing parent" "exit=$rc nope-exists=$([[ -e "$F8/nope" ]] && echo yes || echo no) got: $out"
|
||||||
|
# (iii) exists but is neither a regular file nor a directory
|
||||||
|
mkfifo "$F8/pipe.db"
|
||||||
|
out="$(WO_DATA="$F8/pipe.db" timeout 10 "$WOVM" "$WORK/mix.wob" seed 2>&1)"; rc=$?
|
||||||
|
[[ $rc -eq 2 ]] && grep -q 'neither a regular file nor a directory' <<<"$out" \
|
||||||
|
&& ok "file form: a fifo at WO_DATA exits 2 and says why" \
|
||||||
|
|| bad "file form fifo" "exit=$rc got: $out"
|
||||||
|
# (iv) a trailing slash keeps the directory form, present or missing
|
||||||
|
WO_DATA="$F8/d/" "$WOVM" "$WORK/mix.wob" seed >/dev/null 2>&1; rc=$?
|
||||||
|
[[ $rc -eq 0 && -f "$F8/d/shard-0.wal" ]] \
|
||||||
|
&& ok "trailing slash: WO_DATA=<dir>/ still writes <dir>/shard-0.wal" \
|
||||||
|
|| bad "trailing slash dir form" "exit=$rc in d: $(ls "$F8/d" 2>&1 | tr '\n' ' ')"
|
||||||
|
out="$(WO_DATA="$F8/nodir/" timeout 10 "$WOVM" "$WORK/mix.wob" seed 2>&1)"; rc=$?
|
||||||
|
[[ $rc -eq 2 ]] && grep -Eq 'cannot open .*/nodir//shard-0.wal' <<<"$out" \
|
||||||
|
&& ok "trailing slash on a missing dir: pre-7 'cannot open .../nodir//shard-0.wal' kept byte for byte" \
|
||||||
|
|| bad "trailing slash missing dir" "exit=$rc got: $out"
|
||||||
|
# (v) WO_EPHEMERAL=1 conflicts with the file form exactly as with a directory
|
||||||
|
out="$(WO_EPHEMERAL=1 WO_DATA="$F8/a/app.db" timeout 10 "$WOVM" "$WORK/mix.wob" 2>&1)"; rc=$?
|
||||||
|
[[ $rc -eq 2 ]] && grep -q 'incompatible with WO_DATA' <<<"$out" \
|
||||||
|
&& ok "file form: WO_EPHEMERAL=1 with WO_DATA=<file> exits 2, names the conflict" \
|
||||||
|
|| bad "file form ephemeral conflict" "exit=$rc got: $out"
|
||||||
|
else
|
||||||
|
bad "file form legs" "mix.wob missing (section 1 did not compile)"
|
||||||
|
fi
|
||||||
|
# `wal N` prints `acked i` AFTER each insert returns (the return is the ack);
|
||||||
|
# `verify M` wants rows 1..M present exactly once, rows past M allowed.
|
||||||
|
cat > "$WORK/onefile.wo" <<'EOF'
|
||||||
|
@table(name: "rows", index: [k])
|
||||||
|
class Row { k: Int }
|
||||||
|
fn main(args: multi Text) -> Int {
|
||||||
|
if len(args) < 2 { return 2; }
|
||||||
|
let n = parse_int(args[1]);
|
||||||
|
if n == nil or n < 1 { return 2; }
|
||||||
|
if args[0] == "wal" {
|
||||||
|
let i = 1;
|
||||||
|
while i <= n { insert Row { k: i }; print("acked ${i}"); i = i + 1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let i = 1;
|
||||||
|
while i <= n {
|
||||||
|
let hits = 0;
|
||||||
|
for r in from r in Row where r.k == i select r { hits = hits + 1; }
|
||||||
|
if hits != 1 { print("row ${i}: ${hits} hits"); return 3; }
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
print("verified ${n}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
if "$WOC" --emit "$WORK/onefile.wo" -o "$WORK/onefile.wob" 2>"$WORK/e"; then
|
||||||
|
# (vi) kill -9 mid-write against the file: every acked row replays. stdout
|
||||||
|
# is line-buffered (stdbuf) so the ack count is exact, not a flush boundary;
|
||||||
|
# N is far more than 0.5 s of inserts so the kill lands mid-run — a run that
|
||||||
|
# exited on its own (rc != 137) proves nothing and is reported as such.
|
||||||
|
WO_DATA="$F8/k/app.db" stdbuf -oL "$WOVM" "$WORK/onefile.wob" wal 1000000 >"$F8/k.out" 2>/dev/null &
|
||||||
|
kp=$!
|
||||||
|
sleep "0.$((RANDOM % 30 + 20))"
|
||||||
|
kill -KILL "$kp" 2>/dev/null; wait "$kp" 2>/dev/null; krc=$?
|
||||||
|
acked="$(grep -E '^acked [0-9]+$' "$F8/k.out" | tail -1 | cut -d' ' -f2)"
|
||||||
|
if [[ $krc -eq 137 && -n "$acked" && "$acked" -gt 0 ]]; then
|
||||||
|
out="$(WO_DATA="$F8/k/app.db" "$WOVM" "$WORK/onefile.wob" verify "$acked" 2>&1)"; rc=$?
|
||||||
|
[[ $rc -eq 0 ]] \
|
||||||
|
&& ok "file form: kill -9 after $acked acks, all $acked rows replay from app.db" \
|
||||||
|
|| bad "file form kill -9 replay" "acked=$acked verify rc=$rc got: $(tail -1 <<<"$out")"
|
||||||
|
else
|
||||||
|
bad "file form kill -9" "run rc=$krc acked=${acked:-0} -- the kill did not land mid-write, the leg proves nothing"
|
||||||
|
fi
|
||||||
|
# (vii) a forced compaction in the file form: the temp is <file>.compact
|
||||||
|
# beside the log; after the rename app.db must still be the only
|
||||||
|
# artifact and every row must replay. WO_WAL_STATS proves the
|
||||||
|
# compaction RAN — a trigger that never fired would prove nothing.
|
||||||
|
out="$(WO_DATA="$F8/c/app.db" WO_CHECKPOINT_BYTES=1 WO_WAL_STATS=1 "$WOVM" "$WORK/onefile.wob" wal 300 2>&1)"; rc=$?
|
||||||
|
comps="$(grep -o 'compactions=[0-9]*' <<<"$out" | cut -d= -f2)"
|
||||||
|
[[ $rc -eq 0 && "${comps:-0}" -ge 1 ]] \
|
||||||
|
&& ok "file form: WO_CHECKPOINT_BYTES=1 forced $comps compaction(s) over 300 inserts" \
|
||||||
|
|| bad "file form compaction binds" "exit=$rc compactions=${comps:-none} got: $(tail -1 <<<"$out")"
|
||||||
|
arts="$(find "$F8/c" -mindepth 1 -printf '%P\n' | sort | tr '\n' ' ')"
|
||||||
|
[[ "$arts" == "app.db " ]] \
|
||||||
|
&& ok "file form: app.db is still the only artifact after compaction (no .compact left)" \
|
||||||
|
|| bad "file form compaction artifacts" "found: '$arts'"
|
||||||
|
out="$(WO_DATA="$F8/c/app.db" "$WOVM" "$WORK/onefile.wob" verify 300 2>&1)"; rc=$?
|
||||||
|
[[ $rc -eq 0 ]] \
|
||||||
|
&& ok "file form: all 300 rows replay from the compacted app.db" \
|
||||||
|
|| bad "file form compacted replay" "exit=$rc got: $(tail -1 <<<"$out")"
|
||||||
|
else
|
||||||
|
bad "file form battery fixture compiles" "$(head -1 "$WORK/e")"
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "residency-accept: $((pass + fail)) checks, $fail failures"
|
echo "residency-accept: $((pass + fail)) checks, $fail failures"
|
||||||
[[ $fail -eq 0 ]] || exit 1
|
[[ $fail -eq 0 ]] || exit 1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue