Artifact eba5565471b1c23139c69fa80131a1c106d5cfef744fe99daec66c4cc108145d:
- Executable file
scripts/soak.sh
— part of check-in
[fa78377389]
at
2026-09-18 12:31:12
on branch trunk
— soak: a nightly deep DST and fuzz run
scripts/soak.sh passed the optimize flag this Zig rejects, so it has been unrunnable since the toolchain moved; it takes --release=safe now. A scheduled workflow runs it at 03:00 UTC on the x64 runner with the script's nightly budgets, overridable from the dispatch form, keeps the log and any reproduce line as an artifact, and is pinned to the ci toolchain by ci_test like the other workflows. (user: dmestas, size: 10117) [annotate] [blame] [check-ins using]
#!/usr/bin/env bash # # soak.sh — out-of-band deep DST + fuzz soak runner. # Requires bash (uses the SECONDS builtin for the per-target wall-clock budget). # # The pre-commit gate and CI run these harnesses shallow, for REGRESSION # DETECTION: `zig build dst` sweeps a few seeds x a few thousand steps, and CI # fuzzes one seed x 100 rounds. Neither is a bug hunt. This script is the bug # hunt: the same harnesses at nightly depth, on a ReleaseSafe build (asserts # ON), so machine-hours — not new invariants — do the finding. Every harness # comment in the tree that defers depth to "an out-of-band runner" means this. # # It NEVER auto-files and NEVER auto-fixes. On any failure it appends the # harness's own verbatim `reproduce with seed=...` line (reshaped into a # runnable command) to a dated log, drops a flag file the operator's next # session sees, and exits non-zero. Triage is the operator's; a soak finding # is a P6 GO/NO-GO input, not a footnote. # # --------------------------------------------------------------------------- # Usage # scripts/soak.sh # full nightly (see defaults below) # DST_SEEDS=3 DST_STEPS=500 \ # FUZZ_TARGET_BUDGET=6 FUZZ_SEEDS=3 scripts/soak.sh # bounded smoke # # Config (all env-overridable; defaults are the nightly budget): # DST_SEEDS=200 seeds swept by the repostate+sync DST # DST_STEPS=10000 steps per DST seed # FUZZ_TARGET_BUDGET=300 wall-clock seconds granted to EACH fuzz target # FUZZ_SEEDS=20 fresh-seed invocations per target (sets the slice: # slice = FUZZ_TARGET_BUDGET / FUZZ_SEEDS seconds) # FUZZ_BASE_ROUNDS=16 starting --rounds; auto-grows (x2) to fill a slice # FUZZ_MAX_ROUNDS=100000000 growth cap (one invocation never exceeds it) # ZIT_SOAK_DIR=$HOME/.zit-soak log + flag directory (kept OUT of the repo # so a soak run never shows up in `fossil status`) # # Depth-by-time, not by flat rounds: a flat --rounds would run # artifact_parse_garbage in seconds while logstore_pack_same_path_round_trip # (~276x heavier per round, see src/fuzz_tests.zig:63-78) ran for hours. Each # target instead gets an equal wall-clock budget; --rounds is grown per target # from measured speed to fill each slice, so cheap targets sweep millions of # rounds and heavy targets sweep thousands — the cost ratios honor themselves, # with no second per-target table to drift. The target LIST comes from the # runner's own usage output (the src/.github/workflows/ci.yml:146-159 pattern), # so it cannot drift either. # # --------------------------------------------------------------------------- # Operator install (NOT done by this script — installing the schedule and # running the first full nightly is an operator action): # # macOS launchd (the mac-mini runner) — write to # ~/Library/LaunchAgents/group.zit.soak.plist then `launchctl load` it: # # <?xml version="1.0" encoding="UTF-8"?> # <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" # "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> # <plist version="1.0"><dict> # <key>Label</key><string>group.zit.soak</string> # <key>ProgramArguments</key> # <array> # <string>/Users/dmestas/zit/scripts/soak.sh</string> # </array> # <key>StartCalendarInterval</key> # <dict><key>Hour</key><integer>3</integer> # <key>Minute</key><integer>0</integer></dict> # <key>StandardOutPath</key><string>/tmp/zit-soak.out</string> # <key>StandardErrorPath</key><string>/tmp/zit-soak.err</string> # </dict></plist> # # launchctl load ~/Library/LaunchAgents/group.zit.soak.plist # # cron equivalent (3am daily): # 0 3 * * * cd /Users/dmestas/zit && ./scripts/soak.sh # # After a run, check for the flag file printed on failure; triage, then delete # the flag once the finding is dispositioned. A clean run leaves any prior flag # untouched — the soak never clears a failure the operator has not yet seen. # set -eu # --- locate the repo root (parent of this script's dir) and build there ------ script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) repo_root=$(CDPATH= cd -- "${script_dir}/.." && pwd) cd "${repo_root}" # --- config ------------------------------------------------------------------ DST_SEEDS=${DST_SEEDS:-200} DST_STEPS=${DST_STEPS:-10000} FUZZ_TARGET_BUDGET=${FUZZ_TARGET_BUDGET:-300} FUZZ_SEEDS=${FUZZ_SEEDS:-20} FUZZ_BASE_ROUNDS=${FUZZ_BASE_ROUNDS:-16} FUZZ_MAX_ROUNDS=${FUZZ_MAX_ROUNDS:-100000000} ZIT_SOAK_DIR=${ZIT_SOAK_DIR:-${HOME}/.zit-soak} # ReleaseSafe: the whole point is asserts ON at depth. Never change this. OPT=--release=safe # per-target slice: an equal share of the target budget across FUZZ_SEEDS # fresh-seed invocations, floored at 1s so integer SECONDS math stays sane. slice=$(( FUZZ_TARGET_BUDGET / FUZZ_SEEDS )) [ "${slice}" -lt 1 ] && slice=1 mkdir -p "${ZIT_SOAK_DIR}" stamp=$(date +%Y%m%d) log="${ZIT_SOAK_DIR}/soak-${stamp}.log" flag="${ZIT_SOAK_DIR}/SOAK-FAILED" run_stderr="${ZIT_SOAK_DIR}/.soak-run.stderr" started=$(date -u +%Y-%m-%dT%H:%M:%SZ) printf '=== soak start %s (dst %sx%s, fuzz %ss/target x27) ===\n' \ "${started}" "${DST_SEEDS}" "${DST_STEPS}" "${FUZZ_TARGET_BUDGET}" >>"${log}" # fail PHASE REPRODUCE_CMD — append the verbatim reproduce line, raise the flag, # and exit non-zero. Never files, never fixes. fail() { _phase=$1 _cmd=$2 _when=$(date -u +%Y-%m-%dT%H:%M:%SZ) { printf 'FAIL %s [%s]\n' "${_phase}" "${_when}" printf 'reproduce: %s\n' "${_cmd}" printf -- '--- captured harness output (tail) ---\n' tail -n 20 "${run_stderr}" 2>/dev/null || true printf -- '---\n' } >>"${log}" { printf 'zit soak FAILED %s in %s\n' "${_when}" "${_phase}" printf 'reproduce: %s\n' "${_cmd}" printf 'log: %s\n' "${log}" } >>"${flag}" printf 'SOAK FAILED (%s). reproduce logged to %s ; flag at %s\n' \ "${_phase}" "${log}" "${flag}" >&2 exit 1 } # locate_exe NAME — echo the newest built executable NAME under the cache. # The soak runs the built binaries DIRECTLY, not through `zig build ... --`, # for two reasons: `zig build` captures and DISCARDS a failed run's child # stderr (so the harness's `reproduce with seed=...` line would be lost, the one # thing this runner exists to record), and a direct exec skips the per-round # build-graph check. `zig build` is used ONLY to compile. locate_exe() { find .zig-cache/o -type f -name "$1" -perm -u+x 2>/dev/null -print0 \ | xargs -0 ls -t 2>/dev/null | head -n 1 } # --- phase 1: the DST sweep -------------------------------------------------- printf 'soak: dst --seeds %s --steps %s (ReleaseSafe)\n' "${DST_SEEDS}" "${DST_STEPS}" >&2 # Compile the ReleaseSafe dst exe (the trivial run may exit non-zero if an # invariant is already broken; the COMPILE is what we need, so ignore its exit). zig build dst "${OPT}" -- --seeds 1 --steps 1 >/dev/null 2>&1 || true dst_exe=$(locate_exe dst) [ -z "${dst_exe}" ] && fail "dst-build" "zig build dst ${OPT} # compile failed" if "${dst_exe}" --seeds "${DST_SEEDS}" --steps "${DST_STEPS}" \ >/dev/null 2>"${run_stderr}"; then printf 'soak: dst clean (%s seeds x %s steps)\n' "${DST_SEEDS}" "${DST_STEPS}" >>"${log}" else # dst prints `reproduce with seed=N` (decimal) for both the repostate and the # sync sweep. The replay needs the same --steps, which the line omits. seed=$(grep -o 'reproduce with seed=[0-9]*' "${run_stderr}" | tail -n 1 | sed 's/.*=//') [ -z "${seed}" ] && seed='<unknown>' fail "dst" "zig build dst ${OPT} -- --seed ${seed} --steps ${DST_STEPS}" fi # --- phase 2: the fuzz sweep ------------------------------------------------- # Enumerate from the runner's own usage output (ci.yml:146-159 pattern), which # also compiles the ReleaseSafe fuzz exe. targets=$( (zig build fuzz "${OPT}" 2>&1 || true) \ | sed -n '/^targets:/,$p' | grep -E '^ [a-z0-9_]+$' | tr -d ' ' ) if [ -z "${targets}" ]; then fail "fuzz-enumerate" "zig build fuzz ${OPT} # usage format changed; sweep is vacuous" fi fuzz_exe=$(locate_exe fuzz) [ -z "${fuzz_exe}" ] && fail "fuzz-build" "zig build fuzz ${OPT} # compile failed" for target in ${targets}; do printf 'soak: fuzz %s (budget %ss, slice %ss)\n' "${target}" "${FUZZ_TARGET_BUDGET}" "${slice}" >&2 rounds=${FUZZ_BASE_ROUNDS} t_start=${SECONDS} while [ $(( SECONDS - t_start )) -lt "${FUZZ_TARGET_BUDGET}" ]; do i_start=${SECONDS} # A fresh explicit seed per invocation, drawn here (8 random bytes -> u64), # NOT left to the runner's csprng. The runner prints `reproduce with # seed=0x...` before a run, but its buffered stderr is DROPPED on the error # return path (Zig 0.16 Io): a crashing target emits only `error: X`, not # its seed. Owning the seed makes the logged reproduce line exact anyway. seed="0x$(od -An -N8 -tx1 /dev/urandom | tr -d ' \n')" if "${fuzz_exe}" "${target}" --seed "${seed}" --rounds "${rounds}" \ >/dev/null 2>"${run_stderr}"; then : else fail "fuzz:${target}" "zig build fuzz ${OPT} -- ${target} --seed ${seed} --rounds ${rounds}" fi i_dt=$(( SECONDS - i_start )) # Grow rounds (x2) toward filling a slice while an invocation still finishes # under it; measured, not guessed — the same discipline the harness uses. # Start small (FUZZ_BASE_ROUNDS) so even the heaviest target's first # invocation fits a slice, then settle where i_dt ~= slice. There is no # mid-invocation kill, so a settled invocation may overshoot the slice by up # to itself; the target budget is therefore a soft bound, honest for a soak. if [ "${i_dt}" -lt "${slice}" ] && [ "${rounds}" -lt "${FUZZ_MAX_ROUNDS}" ]; then rounds=$(( rounds * 2 )) [ "${rounds}" -gt "${FUZZ_MAX_ROUNDS}" ] && rounds=${FUZZ_MAX_ROUNDS} fi done printf 'soak: fuzz %s clean (final rounds=%s)\n' "${target}" "${rounds}" >>"${log}" done finished=$(date -u +%Y-%m-%dT%H:%M:%SZ) printf '=== soak CLEAN %s ===\n' "${finished}" >>"${log}" printf 'soak: CLEAN. log: %s\n' "${log}" >&2