Skip to content

Commit 90c5e8a

Browse files
dzhelezovclaude
andcommitted
harness(chaos): commit the Postgres-tier chaos kill/resume harness (issue #52)
Adds the native-Postgres, crash-durable chaos tier that closes the attributable-resume-from-partial-persisted-state gap the Tier-0 PGlite harness scopes out (issue #50 first-commit granularity). The Postgres backend (fsync on) is crash-durable, so a SIGKILL mid-write is recovered by WAL replay while row content is identical — store identity is therefore a deterministic LOGICAL digest over ponder_sync row content, not a byte diff of the datadir. Tools (all env-parameterized; no absolute paths baked in): - pg-digest.mjs logical-digest store identity. Digests md5(to_jsonb(row) - surrogate-serial-id) per table under a natural-key total order, so a sequence-driven surrogate-id shift across a killed+resumed flush is invisible while any content change or row duplication still diverges the digest. - snapshot-coverage.mjs the shared pure coverage-classification core. - snapshot-coverage-pg.mjs / check-intervals-pg.mjs the pg probes (partial/empty/complete coverage; intervals tile the window exactly), reusing that core so the pg and PGlite backends share one verdict. - verify-resume-pg.sh the pg acceptance gate (kill floor + baseline-metadata match + logical-digest identity + exact tiling). - chaos-pg-driver.sh the campaign loop (Poisson kills, per-kill coverage snapshots, single-writer DB-boundary gate, resume-from- aggregate). Runnable selftest mode built in. - build-baseline-pg.sh / pg-ctl-chaos.sh / pg-chaos.conf baseline build (with a digest-determinism proof across a pg_ctl restart) and the throwaway crash-durable cluster lifecycle. Tests (picked up by the existing harness CI glob, node --test harness/chaos/*.test.mjs): pg-digest.test.mjs, check-intervals-pg.test.mjs, snapshot-coverage-pg.test.mjs — the pure cores (digest fold order/count/ name binding + surrogate-id exclusion contract; pg closed-upper tiling; partial-coverage classification). The DB-level id-shift-invariance / mutation / duplication proofs are exercised by the driver's built-in selftest against a live cluster. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3deb8b5 commit 90c5e8a

12 files changed

Lines changed: 2979 additions & 0 deletions

harness/chaos/build-baseline-pg.sh

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#!/usr/bin/env bash
2+
# build-baseline-pg.sh — build the fresh Postgres-tier baseline for the chaos kill/resume campaign
3+
# (issue #52) on the crash-durable native Postgres cluster. A clean, UNKILLED, COMPLETE store built
4+
# with the SAME small-fixed-chunk params the campaign runs under, so the logical-digest identity check
5+
# (pg-digest.mjs) is against a same-parameterization, same-backend baseline. Writes standard metadata
6+
# via the repo's chaos-meta.mjs (so verify-resume-pg's `match` accepts it) plus a `backend`/`digest`/
7+
# `tier1` block. Proves digest DETERMINISM: digests the completed store twice AND once more after a
8+
# pg_ctl restart — all three must be identical.
9+
#
10+
# Env:
11+
# SQD_PONDER_TARBALL (required) path to the @subsquid/ponder tarball under test
12+
# CHAOS_APP (required) path to a Postgres-backed ponder app dir (installs the tarball,
13+
# reads its store URL from $CHAOS_PG_URL — see the app's ponder.config)
14+
# CHAOS_META_MJS (required) path to harness/chaos/chaos-meta.mjs (reused unchanged)
15+
# CHAOS_PORTAL / CHAOS_RPC Portal dataset URL / RPC URL (public endpoints; defaulted)
16+
# CHAOS_CHAIN_ID / CHAOS_FACTORY chain id / factory address (defaulted to the eth Euler factory)
17+
# CHAOS_FROM / CHAOS_TO block range (defaulted)
18+
# CHAOS_CHUNK_BLOCKS / CHAOS_CHUNK_FIXED / CHAOS_READAHEAD small-fixed-chunk params (2000/1/1)
19+
# CHAOS_PGPORT / CHAOS_PGSOCK cluster TCP port / socket dir (must match pg-ctl-chaos.sh)
20+
# CHAOS_BASELINE_DBNAME baseline DB name (default: chaos_baseline_t1)
21+
# CHAOS_BASELINE_META output metadata path (default: baseline-pg.meta.json next to this)
22+
# CHAOS_PSQL / CHAOS_CREATEDB / CHAOS_DROPDB client bins (default: psql/createdb/dropdb on PATH)
23+
set -uo pipefail
24+
25+
CDIR="$(cd "$(dirname "$0")" && pwd)"
26+
27+
TARBALL="${SQD_PONDER_TARBALL:?SQD_PONDER_TARBALL required (path to the @subsquid/ponder tarball)}"
28+
APP="${CHAOS_APP:?CHAOS_APP required (a Postgres-backed ponder app dir)}"
29+
CHAOS_META_MJS="${CHAOS_META_MJS:?CHAOS_META_MJS required (path to harness/chaos/chaos-meta.mjs)}"
30+
31+
PORTAL="${CHAOS_PORTAL:-https://portal.sqd.dev/datasets/ethereum-mainnet}"
32+
RPC="${CHAOS_RPC:-https://ethereum-rpc.publicnode.com}"
33+
CHAIN_ID="${CHAOS_CHAIN_ID:-1}"
34+
FACTORY="${CHAOS_FACTORY:-0x29a56a1b8214D9Cf7c5561811750D5cBDb45CC8e}"
35+
FROM="${CHAOS_FROM:-20529207}"
36+
TO="${CHAOS_TO:-20579207}"
37+
PORT="${CHAOS_BASELINE_PORT:-44331}"
38+
39+
T1_CHUNK_BLOCKS="${CHAOS_CHUNK_BLOCKS:-2000}"
40+
T1_CHUNK_FIXED="${CHAOS_CHUNK_FIXED:-1}"
41+
T1_READAHEAD="${CHAOS_READAHEAD:-1}"
42+
43+
PGPORT="${CHAOS_PGPORT:-54329}"
44+
PGHOST_TCP="127.0.0.1"
45+
PGSOCK="${CHAOS_PGSOCK:-$CDIR/.chaos-pg/pgsock}"
46+
BASELINE_DBNAME="${CHAOS_BASELINE_DBNAME:-chaos_baseline_t1}"
47+
BASELINE_URL="postgres://postgres@$PGHOST_TCP:$PGPORT/$BASELINE_DBNAME"
48+
BASELINE_META="${CHAOS_BASELINE_META:-$CDIR/baseline-pg.meta.json}"
49+
50+
PSQL="${CHAOS_PSQL:-psql}"
51+
CREATEDB="${CHAOS_CREATEDB:-createdb}"
52+
DROPDB="${CHAOS_DROPDB:-dropdb}"
53+
54+
DIGEST_MJS="$CDIR/pg-digest.mjs"
55+
CHECK_INTERVALS_MJS="$CDIR/check-intervals-pg.mjs"
56+
ART="${CHAOS_ART:-$CDIR/.chaos-pg/artifacts}"
57+
LOG="$ART/baseline-pg-build.log"
58+
mkdir -p "$ART"
59+
60+
log () { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"; }
61+
62+
log "▶ building fresh Postgres-tier baseline → db=$BASELINE_DBNAME (chunk=$T1_CHUNK_BLOCKS fixed=$T1_CHUNK_FIXED readahead=$T1_READAHEAD)"
63+
[ -f "$TARBALL" ] || { log "✗ tarball not found: $TARBALL"; exit 2; }
64+
[ -d "$APP" ] || { log "✗ app not found: $APP"; exit 2; }
65+
[ -f "$CHAOS_META_MJS" ] || { log "✗ chaos-meta.mjs not found: $CHAOS_META_MJS"; exit 2; }
66+
67+
# cluster must be up
68+
bash "$CDIR/pg-ctl-chaos.sh" ensure || { log "✗ could not ensure pg cluster"; exit 1; }
69+
70+
export PGHOST="$PGSOCK" PGPORT="$PGPORT"
71+
# refuse to clobber an existing baseline (persistent evidence).
72+
if "$PSQL" -U postgres -Atqc "select 1 from pg_database where datname='$BASELINE_DBNAME'" postgres | grep -q 1; then
73+
log "✗ baseline DB $BASELINE_DBNAME already exists — refusing to overwrite (drop it first)"
74+
exit 2
75+
fi
76+
"$CREATEDB" -U postgres "$BASELINE_DBNAME" || { log "✗ createdb failed"; exit 1; }
77+
log "created baseline DB $BASELINE_DBNAME"
78+
79+
# install the app into a throwaway workspace
80+
WORK="$(mktemp -d)"
81+
NPM_CACHE="$(mktemp -d)"
82+
BASELINE_PIDFILE="$(mktemp)"
83+
cleanup () { rm -rf "$WORK" "$NPM_CACHE"; rm -f "$BASELINE_PIDFILE"; }
84+
trap cleanup EXIT INT TERM
85+
86+
cp -r "$APP/." "$WORK/"
87+
SQD_PONDER_TARBALL="$TARBALL" node -e "const p=require('$WORK/package.json');p.dependencies['@subsquid/ponder']='file:'+process.env.SQD_PONDER_TARBALL;require('fs').writeFileSync('$WORK/package.json',JSON.stringify(p,null,2))"
88+
( cd "$WORK" && npm install --no-audit --no-fund --silent --cache "$NPM_CACHE" ) || { log "✗ install failed"; exit 1; }
89+
[ -x "$WORK/node_modules/.bin/ponder" ] || { log "✗ ponder bin missing"; exit 1; }
90+
# copy the pg tools into the workspace so their `import 'pg'` resolves.
91+
cp "$DIGEST_MJS" "$CHECK_INTERVALS_MJS" "$WORK/"
92+
93+
: > "$LOG"
94+
( cd "$WORK" && \
95+
PONDER_START="$FROM" PONDER_END="$TO" \
96+
CHAOS_PG_URL="$BASELINE_URL" \
97+
PORTAL_URL_1="$PORTAL" PONDER_RPC_URL_1="$RPC" CHAIN_ID="$CHAIN_ID" EULER_FACTORY="$FACTORY" \
98+
PORTAL_CHECKS=strict CI=true \
99+
PORTAL_CHUNK_BLOCKS="$T1_CHUNK_BLOCKS" PORTAL_CHUNK_FIXED="$T1_CHUNK_FIXED" PORTAL_READAHEAD="$T1_READAHEAD" \
100+
setsid ./node_modules/.bin/ponder start --schema baseline --port "$PORT" >"$LOG" 2>&1 & echo $! > "$BASELINE_PIDFILE" )
101+
BP="$(cat "$BASELINE_PIDFILE")"
102+
103+
DONE=0
104+
for _ in $(seq 1 300); do
105+
grep -qiE 'Completed indexing across' "$LOG" && { DONE=1; break; }
106+
kill -0 "$BP" 2>/dev/null || break
107+
sleep 1
108+
done
109+
kill -9 -"$BP" 2>/dev/null
110+
STRAG="$(ss -H -tlnp "sport = :$PORT" 2>/dev/null | grep -oE 'pid=[0-9]+' | head -1 | cut -d= -f2)"
111+
[ -n "$STRAG" ] && kill -9 "$STRAG" 2>/dev/null
112+
wait "$BP" 2>/dev/null
113+
114+
if [ "$DONE" != 1 ]; then
115+
log "✗ baseline did not complete — tail:"; tail -6 "$LOG"
116+
"$DROPDB" -U postgres --if-exists "$BASELINE_DBNAME"
117+
exit 1
118+
fi
119+
log "✓ baseline backfill completed"
120+
121+
# ── verify complete: intervals tile exactly ──────────────────────────────────────────────────────
122+
if ! ( cd "$WORK" && node ./check-intervals-pg.mjs "$BASELINE_URL" "$FROM" "$TO" ); then
123+
log "✗ baseline intervals do NOT tile [$FROM,$TO] — refusing to accept an incomplete baseline"
124+
"$DROPDB" -U postgres --if-exists "$BASELINE_DBNAME"
125+
exit 1
126+
fi
127+
log "✓ baseline intervals tile [$FROM,$TO] exactly"
128+
129+
# ── digest DETERMINISM proof: twice on the same store, once more after a pg_ctl restart ───────────
130+
D1="$( ( cd "$WORK" && node ./pg-digest.mjs "$BASELINE_URL" ) )"
131+
D2="$( ( cd "$WORK" && node ./pg-digest.mjs "$BASELINE_URL" ) )"
132+
log "digest #1 = $D1"
133+
log "digest #2 (same store) = $D2"
134+
log "▶ restarting pg cluster to prove digest survives a WAL-durable restart identically"
135+
bash "$CDIR/pg-ctl-chaos.sh" restart || { log "✗ pg restart failed"; exit 1; }
136+
D3="$( ( cd "$WORK" && node ./pg-digest.mjs "$BASELINE_URL" ) )"
137+
log "digest #3 (after pg_ctl restart) = $D3"
138+
if [ -z "$D1" ] || [ "$D1" != "$D2" ] || [ "$D1" != "$D3" ]; then
139+
log "✗ digest NOT deterministic (#1=$D1 #2=$D2 #3=$D3) — refusing baseline"
140+
exit 1
141+
fi
142+
log "✓ digest deterministic across 2 reads + 1 pg_ctl restart: $D1"
143+
144+
# per-table detail for the metadata
145+
PERTABLE_JSON="$( ( cd "$WORK" && node ./pg-digest.mjs "$BASELINE_URL" --json ) )"
146+
147+
# ── standard metadata via the repo's chaos-meta.mjs (scenario=baseline) so `match` accepts it ─────
148+
CHAOS_META_APP="$APP" CHAOS_META_FROM="$FROM" CHAOS_META_TO="$TO" CHAOS_META_PORTAL="$PORTAL" \
149+
CHAOS_META_TARBALL="$TARBALL" CHAOS_META_CHAIN_ID="$CHAIN_ID" CHAOS_META_FACTORY="$FACTORY" \
150+
CHAOS_META_SCENARIO="baseline" CHAOS_META_KILLS="0" \
151+
node "$CHAOS_META_MJS" write "$BASELINE_META" || { log "✗ could not write baseline metadata"; exit 1; }
152+
153+
TARBALL_SHA="$(sha256sum "$TARBALL" | awk '{print $1}')"
154+
BASELINE_META="$BASELINE_META" DIGEST="$D1" PERTABLE_JSON="$PERTABLE_JSON" \
155+
T1_CHUNK_BLOCKS="$T1_CHUNK_BLOCKS" T1_CHUNK_FIXED="$T1_CHUNK_FIXED" T1_READAHEAD="$T1_READAHEAD" \
156+
BASELINE_DBNAME="$BASELINE_DBNAME" TARBALL_SHA="$TARBALL_SHA" PGPORT="$PGPORT" \
157+
node -e '
158+
const fs = require("fs");
159+
const f = process.env.BASELINE_META;
160+
const m = JSON.parse(fs.readFileSync(f, "utf8"));
161+
m.backend = "postgres16-fsync-on";
162+
m.databaseName = process.env.BASELINE_DBNAME;
163+
m.pgPort = Number(process.env.PGPORT);
164+
m.driver = "pg";
165+
m.tarballSha256 = process.env.TARBALL_SHA;
166+
m.tier1 = {
167+
chunkBlocks: Number(process.env.T1_CHUNK_BLOCKS),
168+
chunkFixed: Number(process.env.T1_CHUNK_FIXED),
169+
readahead: Number(process.env.T1_READAHEAD),
170+
};
171+
let pertable = [];
172+
try { pertable = JSON.parse(process.env.PERTABLE_JSON).perTable.map((t)=>({table:t.table,digest:t.digest,rows:t.rows})); } catch {}
173+
m.digest = {
174+
algorithm: "md5-of-sorted-per-table-md5(order-by-natural-key of md5((to_jsonb(row) - surrogate-id)::text))",
175+
store: process.env.DIGEST,
176+
deterministic: true,
177+
determinismProof: "digested twice on the same store + once after a pg_ctl restart; all identical",
178+
tables: pertable,
179+
};
180+
fs.writeFileSync(f, JSON.stringify(m, null, 2) + "\n");
181+
' || { log "✗ could not augment baseline metadata"; exit 1; }
182+
183+
log "✓ baseline built + metadata written → $BASELINE_META"
184+
cat "$BASELINE_META"
185+
log "▶ baseline DB size:"
186+
"$PSQL" -U postgres -Atc "select pg_size_pretty(pg_database_size('$BASELINE_DBNAME'))" postgres

0 commit comments

Comments
 (0)