|
| 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