Hi Shihao,

shihao zhong <[email protected]> wrote:
> The rewrite gets the error if those finish within deadlock_timeout,
> otherwise REPACK does.

That matches what I see with v3 on master (e27f3b2cad7), using the
same checks as for v2.  With Thom's sequence (a transaction open for
3 s, VACUUM FULL of the TOAST relation meanwhile) and the default
deadlock_timeout of 1 s, REPACK got "deadlock detected" in all 6 runs.
With deadlock_timeout = 5s, REPACK finished and the VACUUM FULL got
the error in all 3.  No update was lost in any run; master loses it
every time.

One case differs from v2: a transaction that already has an XID runs
REINDEX on the TOAST relation, with no rewrite, during the startup.
On master and with v2, REPACK completes; with v3 it gets "deadlock
detected", at both timeouts.  ALTER TABLE on the table itself
deadlocks the same way on master, as you said.

repack_toast fails without 0001 and passed 20 of 20 runs with it;
check and the isolation suite pass.  The script and output are
attached.

Regards,
Manu
v3 of the REPACK (CONCURRENTLY) TOAST fix, checked on master e27f3b2cad7
(--enable-cassert --enable-injection-points)
ctl3 = v3-0002 only (test without the fix), v3 = v3-0001 + v3-0002

===== v3_check.sh =====
#!/usr/bin/env bash
# The same checks as v2_check.sh, for v3 of the fix: REPACK (CONCURRENTLY)
# now locks the TOAST relation before the decoding worker starts, so a
# rewrite of it waits for REPACK.  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.
#
# New in v3_check.sh:
#   xidalter  a transaction that already has an XID runs ALTER TABLE on the
#             table itself while the worker waits for it (v3's commit
#             message says master already deadlocks here).
#
#   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 xidalter}
N=${N:-5}
SECS=${SECS:-20}
D=${D:-$HOME/pgprog/data_v3check}
P=${P:-55711}
LOG=$HOME/pgprog/v3check.log
OUT=$HOME/pgprog/v3check-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 = ${DT:-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)), deadlock_timeout ${DT:-1s}"
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|xidalter)
                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).  xidalter locks the 
table
                # itself instead.
                if [ $c = xidlock ]; then
                        stmt="REINDEX TABLE $TOAST"
                elif [ $c = xidalter ]; then
                        stmt="ALTER TABLE test ADD COLUMN extra int"
                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

===== v3_all.sh =====
#!/bin/bash
# Everything for the v3 review, in one run:
#   1. behaviour checks (v3_check.sh) on the control build (test only) and on v3
#   2. the injection_points tests of both: repack_toast should fail without the 
fix
#   3. repack_toast alone N times on v3 (it relies on locks and lock_timeout)
#   4. core regression + isolation on v3
set -u
A=$(cd "$(dirname "$0")" && pwd)
RUNS=${RUNS:-20}
for b in ctl3 v3; do
  bash $A/v3_check.sh $HOME/pgtoast-i-$b
  echo
done
for b in ctl3 v3; do
  echo "== injection_points check, $b"
  make -C $HOME/pgtoast-$b/src/test/modules/injection_points check 2>&1 \
    | grep -E '^(ok|not ok|# All|# [0-9]+ of)|tests (passed|failed)' | tail -30
done
echo "== repack_toast alone, $RUNS runs, v3"
pass=0; fail=0
for i in $(seq $RUNS); do
  if make -C $HOME/pgtoast-v3/src/test/modules/injection_points check \
       REGRESS= ISOLATION=repack_toast > /tmp/claude-1000/rt_$i.log 2>&1; then
    pass=$((pass + 1))
  else
    fail=$((fail + 1)); cp /tmp/claude-1000/rt_$i.log 
$A/v3_repack_toast_fail_$i.log
  fi
done
echo "repack_toast: $pass passed, $fail failed of $RUNS"
echo "== core regression + isolation, v3"
make -C $HOME/pgtoast-v3 -s check 2>&1 | grep -E '# All|# [0-9]+ of|not ok' | 
tail -5
make -C $HOME/pgtoast-v3/src/test/isolation -s check 2>&1 | grep -E '# All|# 
[0-9]+ of|not ok' | tail -5
echo V3-ALL-DONE

===== ./v3_all.sh (deadlock_timeout 1s) =====
== build: /home/manu/pgtoast-i-ctl3 (postgres (PostgreSQL) 20devel)
  race 1       REPACK ok                                                   2.5s 
  value oldoldold
  race 2       REPACK ok                                                   2.5s 
  value oldoldold
  race 3       REPACK ok                                                   2.7s 
  value oldoldold
  race 4       REPACK ok                                                   2.5s 
  value oldoldold
  race 5       REPACK ok                                                   2.5s 
  value oldoldold
  hammer 20s   REPACK ok                                                   3.5s 
  value oldoldold
  none 1       REPACK ok                                                   2.5s 
  value NEWNEWNEW
  none 2       REPACK ok                                                   2.5s 
  value NEWNEWNEW
  none 3       REPACK ok                                                   2.5s 
  value NEWNEWNEW
  xidlock      REPACK ok                                                   2.0s 
  value NEWNEWNEW,x
               session 1 (REINDEX TABLE pg_toast.pg_toast_23013): ok
  xidrewrite   REPACK ok                                                   2.0s 
  value oldoldold,x
               session 1 (CLUSTER pg_toast.pg_toast_23041 USING 
pg_toast_23041_index): ok
  xidalter     REPACK deadlock detected                                    1.0s 
  value NEWNEWNEW,x
               DETAIL:  Process 433771 waits for ShareLock on transaction 2346; 
blocked by process 433611.
               HINT:  See server log for query details.
               session 1 (ALTER TABLE test ADD COLUMN extra int): ok
  log: 2026-09-25 03:04:28.970 -03 [433771] ERROR:  deadlock detected
  log: 2026-09-25 03:04:28.973 -03 [433754] ERROR:  deadlock detected

== build: /home/manu/pgtoast-i-v3 (postgres (PostgreSQL) 20devel)
  race 1       REPACK deadlock detected                                    3.7s 
  value NEWNEWNEW
               DETAIL:  Process 434893 waits for ShareLock on transaction 668; 
blocked by process 435170.
               HINT:  See server log for query details.
  race 2       REPACK deadlock detected                                    3.7s 
  value NEWNEWNEW
               DETAIL:  Process 436301 waits for ShareLock on transaction 675; 
blocked by process 436572.
               HINT:  See server log for query details.
  race 3       REPACK deadlock detected                                    3.6s 
  value NEWNEWNEW
               DETAIL:  Process 437506 waits for ShareLock on transaction 682; 
blocked by process 437794.
               HINT:  See server log for query details.
  race 4       REPACK deadlock detected                                    3.7s 
  value NEWNEWNEW
               DETAIL:  Process 438838 waits for ShareLock on transaction 689; 
blocked by process 439118.
               HINT:  See server log for query details.
  race 5       REPACK deadlock detected                                    3.6s 
  value NEWNEWNEW
               DETAIL:  Process 440083 waits for ShareLock on transaction 696; 
blocked by process 440416.
               HINT:  See server log for query details.
  hammer 20s   REPACK deadlock detected                                    4.5s 
  value NEWNEWNEW
               DETAIL:  Process 441414 waits for ShareLock on transaction 704; 
blocked by process 441416.
               HINT:  See server log for query details.
  none 1       REPACK ok                                                   2.5s 
  value NEWNEWNEW
  none 2       REPACK ok                                                   2.5s 
  value NEWNEWNEW
  none 3       REPACK ok                                                   2.5s 
  value NEWNEWNEW
  xidlock      REPACK deadlock detected                                    1.0s 
  value NEWNEWNEW,x
               DETAIL:  Process 454261 waits for ShareLock on transaction 2307; 
blocked by process 454120.
               HINT:  See server log for query details.
               session 1 (REINDEX TABLE pg_toast.pg_toast_22881): ok
  xidrewrite   REPACK deadlock detected                                    1.0s 
  value NEWNEWNEW,x
               DETAIL:  Process 455050 waits for ShareLock on transaction 2313; 
blocked by process 454885.
               HINT:  See server log for query details.
               session 1 (CLUSTER pg_toast.pg_toast_22896 USING 
pg_toast_22896_index): ok
  xidalter     REPACK deadlock detected                                    1.0s 
  value NEWNEWNEW,x
               DETAIL:  Process 455852 waits for ShareLock on transaction 2319; 
blocked by process 455689.
               HINT:  See server log for query details.
               session 1 (ALTER TABLE test ADD COLUMN extra int): ok
  log: 2026-09-25 03:04:34.927 -03 [434893] ERROR:  deadlock detected
  log: 2026-09-25 03:04:35.076 -03 [434892] ERROR:  deadlock detected
  log: 2026-09-25 03:04:39.335 -03 [436301] ERROR:  deadlock detected

== injection_points check, ctl3
ok 1         - injection_points                           72 ms
ok 2         - hashagg                                    17 ms
ok 3         - reindex_conc                               16 ms
ok 4         - vacuum                                     15 ms
# All 4 tests passed.
ok 1         - basic                                      81 ms
ok 2         - inplace                                   837 ms
ok 3         - reindex_concurrently_deferred             111 ms
ok 4         - repack                                    181 ms
ok 5         - repack_commit_race                        511 ms
ok 6         - repack_decode                             267 ms
ok 7         - repack_temporal                           151 ms
ok 8         - repack_temporal_multirange                156 ms
not ok 9     - repack_toast                              537 ms
ok 10        - ri_fastpath_reindex                       341 ms
ok 11        - ri_fastpath_snapshot                       67 ms
ok 12        - syscache-update-pruned                    658 ms
ok 13        - wait_cleanup                               79 ms
ok 14        - heap_lock_update                          134 ms
ok 15        - on_conflict_probe_window                  283 ms
# 1 of 15 tests failed.
== injection_points check, v3
ok 1         - injection_points                           73 ms
ok 2         - hashagg                                    15 ms
ok 3         - reindex_conc                               18 ms
ok 4         - vacuum                                     20 ms
# All 4 tests passed.
ok 1         - basic                                      91 ms
ok 2         - inplace                                   817 ms
ok 3         - reindex_concurrently_deferred             112 ms
ok 4         - repack                                    180 ms
ok 5         - repack_commit_race                        497 ms
ok 6         - repack_decode                             249 ms
ok 7         - repack_temporal                           141 ms
ok 8         - repack_temporal_multirange                142 ms
ok 9         - repack_toast                              521 ms
ok 10        - ri_fastpath_reindex                       343 ms
ok 11        - ri_fastpath_snapshot                       62 ms
ok 12        - syscache-update-pruned                    563 ms
ok 13        - wait_cleanup                               69 ms
ok 14        - heap_lock_update                          126 ms
ok 15        - on_conflict_probe_window                  288 ms
# All 15 tests passed.
== repack_toast alone, 20 runs, v3
repack_toast: 20 passed, 0 failed of 20
== core regression + isolation, v3
# All 239 tests passed.
# All 133 tests passed.
V3-ALL-DONE

===== DT=5s N=3 ./v3_check.sh <v3> race xidlock xidalter =====
== build: /home/manu/pgtoast-i-v3 (postgres (PostgreSQL) 20devel), 
deadlock_timeout 5s
  race 1       REPACK ok                                                   6.0s 
  value NEWNEWNEW
  race 2       REPACK ok                                                   6.0s 
  value NEWNEWNEW
  race 3       REPACK ok                                                   6.0s 
  value NEWNEWNEW
  xidlock      REPACK deadlock detected                                    5.0s 
  value NEWNEWNEW,x
               DETAIL:  Process 531395 waits for ShareLock on transaction 691; 
blocked by process 531224.
               HINT:  See server log for query details.
               session 1 (REINDEX TABLE pg_toast.pg_toast_16465): ok
  xidalter     REPACK deadlock detected                                    5.0s 
  value NEWNEWNEW,x
               DETAIL:  Process 533699 waits for ShareLock on transaction 697; 
blocked by process 533548.
               HINT:  See server log for query details.
               session 1 (ALTER TABLE test ADD COLUMN extra int): ok
  log: 2026-09-25 03:08:05.832 -03 [523132] ERROR:  deadlock detected
  log: 2026-09-25 03:08:13.400 -03 [525785] ERROR:  deadlock detected
  log: 2026-09-25 03:08:20.968 -03 [528631] ERROR:  deadlock detected

log excerpt, first deadlock with deadlock_timeout 5s:
2026-09-25 03:08:05.832 -03 [523132] ERROR:  deadlock detected
2026-09-25 03:08:05.832 -03 [523132] DETAIL:  Process 523132 waits for 
AccessExclusiveLock on relation 16388 of database 5; blocked by process 522701.
        Process 522701 waits for ShareLock on transaction 668; blocked by 
process 523132.
        Process 523132: VACUUM FULL pg_toast.pg_toast_16384
        Process 522701: <command string not enabled>
2026-09-25 03:08:05.832 -03 [523132] HINT:  See server log for query details.
2026-09-25 03:08:05.832 -03 [523132] STATEMENT:  VACUUM FULL 
pg_toast.pg_toast_16384

Reply via email to