#!/bin/bash # repro-case3-aimed.sh: VACUUM FULL pg_class loses the pg_class row of a committed table when # the subtransaction that updated that row aborts during the rewrite. No patch, no debugger. # # s1 updates victim's pg_class row in a subtransaction, then busy-waits on the progress of the # VACUUM FULL pg_class that s2 starts (phase "seq scanning heap" and heap_blks_scanned past the # block of the old version A). At that point the rewrite # has already read A while the updater was running. s1 runs ROLLBACK TO and reads the progress # again, which shows that the scan has not reached the block of the new version B yet (B goes # to the last block of the heap). Each round uses a fresh victim, created before the fillers. # # Works on master (progress command 'REPACK') and on REL_14_STABLE..REL_18_STABLE ('CLUSTER'). # The parameter slots differ; pg_stat_get_progress_info() returns pid, datid, relid and then # st_progress_param[i] as param(i+1), so (src/include/commands/progress.h of each branch): # master, REPACK: PROGRESS_REPACK_PHASE 1 -> param2 # PROGRESS_REPACK_TOTAL_HEAP_BLKS 7 -> param8, # PROGRESS_REPACK_HEAP_BLKS_SCANNED 8 -> param9 # 14..18, CLUSTER: PROGRESS_CLUSTER_PHASE 1 -> param2 # PROGRESS_CLUSTER_TOTAL_HEAP_BLKS 5 -> param6, # PROGRESS_CLUSTER_HEAP_BLKS_SCANNED 6 -> param7 # In both, phase 1 is "seq scanning heap" (PROGRESS_*_PHASE_SEQ_SCAN_HEAP). # # Usage: repro-case3-aimed.sh [port] [ntables] [rounds] # is removed and recreated; it holds a scratch cluster. set -uo pipefail BIN=$1; W=$2; PORT=${3:-61340}; NTABLES=${4:-20000}; ROUNDS=${5:-5} D=$W/data; LOG=$W/server.log export PGHOST=$W PGPORT=$PORT PGDATABASE=postgres PSQL="$BIN/psql -X -q -At -v ON_ERROR_STOP=1" rm -rf "$W"; mkdir -p "$W" "$BIN/initdb" -D "$D" -N -A trust > "$W/initdb.log" 2>&1 || { echo "initdb failed"; exit 2; } cat >> "$D/postgresql.conf" < /dev/null || { echo "start failed"; exit 2; } trap '"$BIN/pg_ctl" -D "$D" -m immediate stop > /dev/null 2>&1' EXIT # (count(*) so that the function really runs; the old branches reject 'REPACK' with an ERROR) if $PSQL -c "SELECT count(*) FROM pg_stat_get_progress_info('REPACK')" > /dev/null 2>&1; then CMD=REPACK; TOT=param8; SCAN=param9 else CMD=CLUSTER; TOT=param6; SCAN=param7 fi echo "server: $($PSQL -c 'SELECT version()' | cut -d, -f1); progress: '$CMD', phase param2," \ "heap_blks_total $TOT, heap_blks_scanned $SCAN" for r in $(seq 1 "$ROUNDS"); do $PSQL -c "CREATE TABLE victim_$r (a int, b text)" || exit 2; done for ((i = 1; i <= NTABLES; i += 1000)); do $PSQL -c "DO \$\$ BEGIN FOR i IN $i..$((i + 999 < NTABLES ? i + 999 : NTABLES)) LOOP EXECUTE format('CREATE TABLE filler_%s ()', i); END LOOP; END \$\$" || exit 2 done $PSQL -c "VACUUM FULL pg_class" || exit 2 echo "setup: $ROUNDS victims, $NTABLES filler tables" PROG="SELECT 'phase ' || param2 || ', heap_blks_scanned ' || $SCAN || ' of ' || $TOT FROM pg_stat_get_progress_info('$CMD') WHERE relid = 1259" lost=0 for r in $(seq 1 "$ROUNDS"); do rm -f "$W/altered" VOID=$($PSQL -c "SELECT oid FROM pg_class WHERE relname = 'victim_$r'") read -r BLK < <($PSQL -c "SELECT (ctid::text::point)[0]::int FROM pg_class WHERE oid = $VOID") NB=$($PSQL -c "SELECT pg_relation_size('pg_class') / 8192") echo "round $r: victim_$r oid $VOID, xmin $($PSQL -c "SELECT xmin FROM pg_class WHERE oid = $VOID")," \ "A in block $BLK of $NB (B will go to the last block)" $PSQL > "$W/s1-$r.out" 2>&1 < $BLK; n := n + 1; IF n > 20000000 THEN RAISE EXCEPTION 'scan never passed block $BLK'; END IF; END LOOP; END \$\$; SELECT pg_stat_clear_snapshot(); SELECT 's1: before ROLLBACK TO: ' || ($PROG); ROLLBACK TO sp; SELECT pg_stat_clear_snapshot(); SELECT 's1: after ROLLBACK TO: ' || coalesce(($PROG), 'VACUUM FULL already finished'); COMMIT; SQL S1=$! while [ ! -f "$W/altered" ]; do kill -0 $S1 2> /dev/null || { echo "s1 failed:"; cat "$W/s1-$r.out"; exit 2; } sleep 0.01 done $PSQL -c "VACUUM FULL pg_class" 2>&1 wait $S1 R=$($PSQL -c "SELECT count(*) FROM pg_class WHERE oid = $VOID") grep '^s1:' "$W/s1-$r.out" | sed 's/^s1: / /' echo " victim_$r rows in pg_class after VACUUM FULL: $R" grep -v '^s1:\|^$\|^phase' "$W/s1-$r.out" | grep . | sed 's/^/ s1 says: /' [ "$R" = 1 ] || lost=$((lost + 1)) done echo "RESULT: pg_class row lost in $lost of $ROUNDS rounds"