Hi Shihao,

> Agreed. v2 attached. REPACK now fails if the TOAST table was
> rewritten, and the user can run it again.

I ran the same checks as for v1 against v2, on cff329240ba with
--enable-cassert --enable-injection-points, next to a control build
with only v2-0002.  Both patches apply cleanly and build with no
warnings.

1. The race without an injection point (VACUUM FULL of the TOAST
relation while the worker waits, UPDATE right after), 5 runs each:

  control:  REPACK succeeds, update lost in 5 of 5
  v2:       REPACK fails in 5 of 5, update kept in 5 of 5

  ERROR:  could not execute REPACK (CONCURRENTLY) on relation "test"
  DETAIL:  The TOAST relation was rewritten concurrently.
  HINT:  The transaction might succeed if retried.

2. VACUUM FULL of the TOAST relation in a loop for 20 s, over the
whole startup: v2 fails after 3.6 s with the same error, and the
update is kept.  With v1 the same run took about 20 s and 166 worker
restarts, so the unbounded wait I mentioned for v1 is gone.

3. No rewrite at all: REPACK succeeds in 3 of 3, 2.5-2.6 s, the same
as the control.

4. Thom's deadlock case, where a transaction that already has an XID
locks the TOAST relation while the worker waits for it:

  REINDEX TABLE of the TOAST relation (lock, no rewrite)
    v2:       REPACK succeeds, no deadlock, update kept
  CLUSTER of the TOAST relation (lock and rewrite)
    control:  REPACK succeeds, update lost
    v2:       REPACK fails with the error above, update kept

So taking the lock after the worker's setup does what the commit
message says: no deadlock, and a clear error when the rewrite does
happen.

5. Tests: repack_toast fails on the control and passes with v2.  With
v2 all injection_points tests pass (4 regress, 14 isolation), and so
do make check (239) and src/test/isolation (133).

The script is attached (.txt, so the cfbot keeps testing your
patches).

Regards,
Manu
#!/usr/bin/env bash
# v2 of the fix: REPACK (CONCURRENTLY) now errors out if the TOAST relation
# was rewritten while the decoding worker was starting, instead of retrying.
# Every case runs against a build and reports REPACK's own outcome (ok or
# its error), how long it took, and the final value of the updated rows.
#
#   race      Thom's case without an injection point: an open transaction
#             keeps the worker waiting, VACUUM FULL rewrites the TOAST
#             relation meanwhile, an UPDATE of the TOASTed column commits
#             right after the transaction ends.  N attempts.
#   hammer    VACUUM FULL of the TOAST relation in a loop for SECS seconds,
#             over the whole startup of REPACK (the v1 retry loop spun here).
#   none      the same, with no rewrite at all: the normal path.
#   xidlock   a transaction that already has an XID locks the TOAST
#             relation (REINDEX of it) while the worker waits for it: taking
#             the TOAST lock before starting the worker deadlocks here.
#   xidrewrite  the same, but the transaction rewrites the TOAST relation
#             (CLUSTER of it) before committing.
#
#   v2_check.sh <install dir> [case ...]
set -u
B=$1; shift
CASES=${*:-race hammer none xidlock xidrewrite}
N=${N:-5}
SECS=${SECS:-20}
D=${D:-$HOME/pgprog/data_v2check}
P=${P:-55711}
LOG=$HOME/pgprog/v2check.log
OUT=$HOME/pgprog/v2check-repack.out

"$B/bin/pg_ctl" -D "$D" -m immediate -w stop >/dev/null 2>&1
rm -rf "$D" "$LOG"
"$B/bin/initdb" -D "$D" -U postgres --no-sync -A trust >/dev/null 2>&1
cat >> "$D/postgresql.conf" <<'EOF'
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
deadlock_timeout = 1s
EOF
"$B/bin/pg_ctl" -D "$D" -o "-p $P" -l "$LOG" -w start >/dev/null 2>&1

q() { "$B/bin/psql" -p $P -U postgres -qtAX -c "$1" 2>&1; }

fresh() {  # a new table with three TOASTed rows; sets TOAST
        q "DROP TABLE IF EXISTS test" >/dev/null
        q "CREATE TABLE test (id int PRIMARY KEY, big text)" >/dev/null
        q "ALTER TABLE test ALTER COLUMN big SET STORAGE EXTERNAL" >/dev/null
        q "INSERT INTO test SELECT g, repeat('old', 3000) FROM 
generate_series(1,3) g" >/dev/null
        TOAST=$(q "SELECT 'pg_toast.' || c2.relname FROM pg_class c1 JOIN 
pg_class c2 ON c2.oid = c1.reltoastrelid WHERE c1.relname='test'")
}

start_repack() {  # in the background; sets REPACK and T0
        T0=$(date +%s.%N)
        ( q "REPACK (CONCURRENTLY) test" > "$OUT" 2>&1; date +%s.%N > 
"$OUT.end" ) &
        REPACK=$!
}

report() {  # label
        local limit=$((SECS + 60)) waited=0
        while kill -0 $REPACK 2>/dev/null && [ $waited -lt $limit ]; do sleep 
1; waited=$((waited+1)); done
        wait 2>/dev/null
        local secs=$(echo "$(cat "$OUT.end" 2>/dev/null || date +%s.%N) - $T0" 
| bc)
        local repack=$(grep -m1 -E 'ERROR|FATAL' "$OUT" | sed 
's/^.*\(ERROR\|FATAL\): *//')
        local value=$(q "SELECT string_agg(DISTINCT left(big, 9), ',') FROM 
test")
        printf '  %-12s REPACK %-50s %5.1fs   value %s\n' "$1" "${repack:-ok}" 
"$secs" "$value"
        [ -z "$repack" ] || grep -E '^(DETAIL|HINT):' "$OUT" | sed 's/^/        
       /'
}

open_xact() {  # seconds: a transaction with an XID, closed after the sleep
        ( "$B/bin/psql" -p $P -U postgres -qtAX \
                -c "BEGIN" -c "SELECT pg_current_xact_id()" -c "SELECT 
pg_sleep($1)" -c "COMMIT" >/dev/null 2>&1 ) &
        XACT=$!
}

update_new() {
        q "UPDATE test SET big = repeat('NEW', 4000) WHERE id IN (1,2,3)" 
>/dev/null
}

echo "== build: $B ($("$B/bin/postgres" --version))"
for c in $CASES; do
        case $c in
        race)
                for i in $(seq 1 $N); do
                        fresh; open_xact 3; sleep 0.5
                        start_repack; sleep 1
                        q "VACUUM FULL $TOAST" >/dev/null
                        wait $XACT; update_new
                        report "race $i"
                done ;;
        hammer)
                fresh; open_xact 4; sleep 0.5
                ( until_t=$((SECONDS + SECS))
                  while [ $SECONDS -lt $until_t ]; do q "VACUUM FULL $TOAST" 
>/dev/null; done ) &
                HAMMER=$!
                start_repack
                wait $XACT; update_new
                report "hammer ${SECS}s"
                wait $HAMMER 2>/dev/null ;;
        none)
                for i in $(seq 1 3); do
                        fresh; open_xact 3; sleep 0.5
                        start_repack
                        wait $XACT; update_new
                        report "none $i"
                done ;;
        xidlock|xidrewrite)
                fresh
                # LOCK TABLE is refused on a TOAST relation, so use commands 
that
                # lock it for real: REINDEX takes ShareLock on it without a 
rewrite,
                # CLUSTER rewrites it (new relfilenumber).
                if [ $c = xidlock ]; then
                        stmt="REINDEX TABLE $TOAST"
                else
                        idx=$(q "SELECT c.relname FROM pg_index i JOIN pg_class 
c ON c.oid = i.indexrelid WHERE i.indrelid = '$TOAST'::regclass")
                        stmt="CLUSTER $TOAST USING $idx"
                fi
                ( "$B/bin/psql" -p $P -U postgres -qtAX \
                        -c "BEGIN" -c "INSERT INTO test VALUES (100, 'x')" -c 
"SELECT pg_sleep(1.5)" \
                        -c "$stmt" -c "SELECT pg_sleep(1)" -c "COMMIT" > 
"$OUT.s1" 2>&1 ) &
                S1=$!
                sleep 0.5; start_repack
                wait $S1
                s1=$(grep -m1 -E 'ERROR' "$OUT.s1" | sed 's/^.*ERROR: *//')
                update_new
                report "$c"
                echo "               session 1 ($stmt): ${s1:-ok}" ;;
        esac
done

grep -E 'deadlock detected' "$LOG" | head -3 | sed 's/^/  log: /'
"$B/bin/pg_ctl" -D "$D" -m immediate -w stop >/dev/null 2>&1

Reply via email to