Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1x9B47-00000001nXD-0GZR for pgsql-hackers@arkaria.postgresql.org; Wed, 23 Sep 2026 00:42:27 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.98.2) (envelope-from ) id 1x9B46-00000002Uu4-0Pw6 for pgsql-hackers@arkaria.postgresql.org; Wed, 23 Sep 2026 00:42:26 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1x9B45-00000002Utw-2OeZ for pgsql-hackers@lists.postgresql.org; Wed, 23 Sep 2026 00:42:25 +0000 Received: from mail-dy2-x10.google.com ([2607:f8b0:4864:36::10]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1x9B43-00000000lf5-0xvM for pgsql-hackers@lists.postgresql.org; Wed, 23 Sep 2026 00:42:24 +0000 Received: by mail-dy2-x10.google.com with SMTP id 5a478bee46e88-339795849f6so89655eec.1 for ; Tue, 22 Sep 2026 17:42:23 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1790124143; x=1790728943; darn=lists.postgresql.org; h=content-type:mime-version:references:in-reply-to:message-id:date :subject:cc:to:from:from:to:cc:subject:date:message-id:reply-to :content-type; bh=9SbnwcGKMNxr8IEZmflNzAdqDgAa4ACRFjiIQfT3w1U=; b=DkWAAYxyzEM3PbiquHZKDeJAguADaPcI3KS2qopOwkBc0mSPzDQ/z38JauXf3o/Wat I7aT3G9gBpn1kvRKpmshv3uvQu600XxABn0kuFl0SRxilK1ozQ8R293tHCfpuCh50YO1 k5nt+RtLHMz5vQe7Er91FBlfwMz76XXLT4+POTIDlB81DK7foglIJPSwzAYf12pxFSkt /Pq5UgBvl6I9a5Q9bKlwLl3SRZOKhRJyjVlheYTAfOeuk0h26bYk7QNAXe4D46hkEEfe e+WZLPshWE78SoqjTdH03MAgheNWD1u99ujrnqtK+FwKDbZiuq5JXTYW3sypTqcp/IIO O4pQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1790124143; x=1790728943; h=content-type:mime-version:references:in-reply-to:message-id:date :subject:cc:to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject :date:message-id:reply-to:content-type; bh=9SbnwcGKMNxr8IEZmflNzAdqDgAa4ACRFjiIQfT3w1U=; b=uHeykFTViHpOi/I10kUYR5mjM5t+jcj4TtcdyDd689OoKvEM7KkhqMnEtI/WQ+Gp91 PB7vNYtenekiZlF90gQcNnmKAk97RSyQOVFPbaIsKc9a0eIJyJoen9Ja9ceGwdtgg6lH XaDy04NF5SiPK49u3N2dPIIa3B8idSwlV0Pq5S+sprsBm8XqoD9nEbDYkAtFYqmvz+nU pFmYVgLqkowtNfQqA9qLTxq5swXGJs9YHryQFROS6T8Bc8l5olZUUMOubtELcHJYWVpu 9+8xn/ocFZwa1vLCmS3Uoncd2ShGFJkarQzQ6pGl+1w0vGS79AB/AZ8dYhMV3grTMu1n ArDQ== X-Gm-Message-State: AFuF++k0Kexyh3vZfiyTMLdSe+vTYwkA7KZeTCkpjk6LURutnCZ+lSl3 QF/YmrVIimeSu/BuJb0M4sgPCqpdE9INgAOHf4F/umo+yy9JKU2UxD5olazHveSfpew= X-Gm-Gg: AYBFou207vdnm5rA+Bs748e0a9Y4l0yWi6tnSJ8anv9mD4AhgvhVGasQlEx+MVjBTY3 QngzMITqe6RQ3wjKbQpyZtIZtw82SeRi0F3be29OMrj+LkME3rZC61oFhVRYNBLHxjScrSE/BhA qMoqcMX4fI0dJ8/nOTFJF8BZCjpY+fJdcWxawqHjGfYhEGW2+E55dt7QMVGvADjqJVF7oAR0HGu yLDqiblNj0z7MVeybwTzCj1qB0nwf/rni1MbmrzFDf72ar66fBybfeaVkCffeJH8wuDYEUCH+T8 FNtUZldtRTZoae8oNyChv1DCcbpNruOV0cg4dgujAxueG9ULbAlc6yMMSFFCVEFnL06DRO6phin 7HyWX8LF5FZd/gMiNoHkiqD7sf+3lS6mTFgfbuYxcRzei70zQhOzj3v5ghXpU5UfPYIXEILGYyF CFBi2LaLxm77hbPyw07oSuDT9wibJNwxshVkNuIbtMzCVw+H9bw8H7aWLoX0VL9SrCjRh1UFZr5 nVqSmgE76CCv/2n8MDF4x4hYWZnmX1kcpEO5YAbAnpqtsp5WTz3u6tov9BjpRIe7F27aS5+aXx0 At3jGsoyh7iG06w8KMkEUK/Z3llbQl0+lX3P X-Received: by 2002:a05:701b:2303:b0:12d:c389:ae54 with SMTP id a92af1059eb24-144f91d7039mr1701188c88.2.1790124142610; Tue, 22 Sep 2026 17:42:22 -0700 (PDT) Received: from 192.168.1.109 ([2803:3c90:cee:ea00:5c3c:7e70:29e3:7a55]) by smtp.gmail.com with ESMTPSA id a92af1059eb24-144f983d73fsm2469545c88.6.2026.09.22.17.42.20 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 22 Sep 2026 17:42:21 -0700 (PDT) From: Manu To: Thom Brown Cc: pgsql-hackers@lists.postgresql.org Subject: Re: REPACK (CONCURRENTLY) can silently lose updates when the toast table is rewritten Date: Tue, 22 Sep 2026 21:42:19 -0300 Message-ID: <179012413951.1850281.5077495683381671561@gmail.com> In-Reply-To: References: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="===============1338542497413467292==" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --===============1338542497413467292== Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 7bit Hi Thom, Reproduced here on master (07c73f45063), Linux, following your recipe exactly: the three rows read NEWNEWNEW/12000 before REPACK finishes and oldoldold/9000 after it, no error anywhere. One thing worth adding, because it changes how this reads: the injection point is not needed. It makes the race deterministic, but the window your analysis describes - between repack_setup_logical_decoding() taking the toast relfilenode and copy_table_data() locking the toast table - is wide enough on its own, because get_initial_snapshot() sits in it waiting for running XIDs. An ordinary open transaction is enough to hold it open. Without any injection point: session A BEGIN; SELECT pg_current_xact_id(); (sleeps 3s, commits) session B REPACK (CONCURRENTLY) test; -- waits for A session C VACUUM FULL pg_toast.pg_toast_; -- inside the gap session C UPDATE test SET big = ... ; -- right after A commits 5 of 5 runs lost the update. Script attached; it builds and drops the table on each iteration so the result is not an artifact of one particular relfilenode. So this does not need a debug build or a test-only feature to happen. It needs a long-running transaction, a REPACK (CONCURRENTLY), and someone rewriting that table's toast relation in the meantime - which is an odd thing to do by hand, but it is allowed, the parent is only held with ShareUpdateExclusiveLock as you say, and "run VACUUM FULL on the biggest toast tables" is the kind of thing maintenance scripts do. I have not looked for a fix yet. From your description the obvious question is whether repack_setup_logical_decoding() should hold the toast lock until copy_table_data() takes it, or whether the relfilenode should be re-checked after the snapshot is built; the second sounds cheaper but I have not read enough of that path to have an opinion worth posting. Regards, Manu --===============1338542497413467292== Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Content-Disposition: attachment; filename="nocfbot-repack-toast-race.sh.txt" MIME-Version: 1.0 #!/usr/bin/env bash # ?Hace falta el injection point, o la carrera se gana sola? # # Con el punto de inyeccion el bug sale siempre, porque REPACK queda detenido # justo antes de tomar el lock. Sin el, hay que meter el UPDATE en la ventana # que va desde que la transaccion vieja commitea hasta que REPACK copia la # tabla. Aca se intenta N veces, con el UPDATE disparado inmediatamente # despues del COMMIT, sin esperas. set -u B=3D/home/manu/pgprog/i-serie D=3D/home/manu/pgprog/data_carrera P=3D55592 N=3D${N:-5} "$B/bin/pg_ctl" -D "$D" -m immediate -w stop >/dev/null 2>&1 rm -rf "$D" "$B/bin/initdb" -D "$D" -U postgres --no-sync -A trust >/dev/null 2>&1 cat >> "$D/postgresql.conf" <<'EOF' wal_level =3D logical max_replication_slots =3D 10 max_wal_senders =3D 10 EOF "$B/bin/pg_ctl" -D "$D" -o "-p $P" -l /home/manu/pgprog/carrera.log -w start = >/dev/null 2>&1 q() { "$B/bin/psql" -p $P -U postgres -qtAX -c "$1" 2>&1; } ganadas=3D0 for i in $(seq 1 $N); do 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=3D$(q "SELECT 'pg_toast.' || c2.relname FROM pg_class c1 JOIN pg_class= c2 ON c2.oid =3D c1.reltoastrelid WHERE c1.relname=3D'test'") # A: transaccion abierta, se cierra sola a los 3s ( "$B/bin/psql" -p $P -U postgres -qtAX \ -c "BEGIN" -c "SELECT pg_current_xact_id()" -c "SELECT pg_sleep(3)" -c "COM= MIT" >/dev/null 2>&1 ) & sleep 0.5 # B: REPACK, que se va a quedar esperando a que A termine ( q "REPACK (CONCURRENTLY) test" >/dev/null 2>&1 ) & # C: reescribir la toast mientras REPACK espera sleep 1 q "VACUUM FULL $TOAST" >/dev/null 2>&1 # esperar a que A commitee y disparar el UPDATE lo antes posible wait %1 2>/dev/null q "UPDATE test SET big =3D repeat('NEW', 4000) WHERE id IN (1,2,3)" >/dev/nu= ll 2>&1 wait 2>/dev/null res=3D$(q "SELECT DISTINCT left(big,9) FROM test") if [ "$res" =3D "oldoldold" ]; then ganadas=3D$((ganadas+1)); echo " intento $i: UPDATE PERDIDO" else echo " intento $i: los updates sobrevivieron ($res)" fi done "$B/bin/pg_ctl" -D "$D" -m immediate -w stop >/dev/null 2>&1 echo echo "sin injection point: $ganadas de $N intentos perdieron el update" --===============1338542497413467292==--