From 5c6fdcf847ea97bcfa7fce081c597b500d82d5f5 Mon Sep 17 00:00:00 2001 From: Rui Zhao Date: Sun, 13 Sep 2026 00:59:09 +0800 Subject: [PATCH 2/3] Test the initial decoding snapshot against a commit that is not in CLOG yet The snapshot builder counts a transaction as committed once it has decoded its commit record, but the transaction updates CLOG only after writing that record. An initial snapshot built in between and converted to a regular MVCC snapshot makes HeapTupleSatisfiesMVCC() consult CLOG about a transaction it takes as not running, and the transaction comes out as aborted. Add an injection point between the flush of the commit record and the CLOG update, and an isolation test that stops a transaction there while REPACK (CONCURRENTLY) builds its snapshot. Without a fix the repacked table lacks the changes of that transaction. --- src/backend/access/transam/xact.c | 10 ++ src/test/modules/injection_points/Makefile | 1 + .../expected/repack_commit_race.out | 64 +++++++++++++ .../injection_points/injection_points.c | 11 ++- src/test/modules/injection_points/meson.build | 1 + .../specs/repack_commit_race.spec | 96 +++++++++++++++++++ 6 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 src/test/modules/injection_points/expected/repack_commit_race.out create mode 100644 src/test/modules/injection_points/specs/repack_commit_race.spec diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index aca92507eb..514b6a0079 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -65,6 +65,7 @@ #include "utils/builtins.h" #include "utils/combocid.h" #include "utils/guc.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/relmapper.h" @@ -1377,6 +1378,9 @@ RecordTransactionCommit(void) &RelcacheInitFileInval); wrote_xlog = (XactLastRecEnd != 0); + /* Load the injection point before entering the critical section */ + INJECTION_POINT_LOAD("commit-before-clog-update"); + /* * If we haven't been assigned an XID yet, we neither can, nor do we want * to write a COMMIT record. @@ -1543,6 +1547,12 @@ RecordTransactionCommit(void) { XLogFlush(XactLastRecEnd); + /* + * The commit record is on disk, but not in CLOG yet. A test can stop + * here to see what others make of the transaction meanwhile. + */ + INJECTION_POINT_CACHED("commit-before-clog-update", NULL); + /* * Now we may update the CLOG, if we wrote a COMMIT record above */ diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index 408a35c3c2..0c00edc61e 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -18,6 +18,7 @@ ISOLATION = basic \ inplace \ reindex_concurrently_deferred \ repack \ + repack_commit_race \ repack_decode \ repack_temporal \ repack_temporal_multirange \ diff --git a/src/test/modules/injection_points/expected/repack_commit_race.out b/src/test/modules/injection_points/expected/repack_commit_race.out new file mode 100644 index 0000000000..4d0286029a --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_commit_race.out @@ -0,0 +1,64 @@ +Parsed test spec with 5 sessions + +starting permutation: s2_begin s1_repack s3_begin s2_rollback s4_changes s3_rollback s5_wakeup s1_check +injection_points_attach +----------------------- + +(1 row) + +step s2_begin: + BEGIN; + SELECT pg_current_xact_id() IS NOT NULL; + +?column? +-------- +t +(1 row) + +step s1_repack: + REPACK (CONCURRENTLY) repack_race; + +step s3_begin: + BEGIN; + SELECT pg_current_xact_id() IS NOT NULL; + +?column? +-------- +t +(1 row) + +step s2_rollback: + ROLLBACK; + +step s4_changes: + INSERT INTO repack_race(i, j) VALUES (3, 3); + UPDATE repack_race SET j = j + 1 WHERE i = 1; + DELETE FROM repack_race WHERE i = 2; + +step s3_rollback: + ROLLBACK; + +step s5_wakeup: + SELECT injection_points_wakeup('commit-before-clog-update'); + +injection_points_wakeup +----------------------- + +(1 row) + +step s1_repack: <... completed> +step s4_changes: <... completed> +step s1_check: + SELECT i, j FROM repack_race ORDER BY i; + +i|j +-+- +1|2 +3|3 +(2 rows) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/injection_points.c b/src/test/modules/injection_points/injection_points.c index 66d8158d0c..5e1bbc2f5c 100644 --- a/src/test/modules/injection_points/injection_points.c +++ b/src/test/modules/injection_points/injection_points.c @@ -246,12 +246,17 @@ injection_wait(const char *name, const void *private_data, void *arg) char *argstr = arg; int delay_us = 0; - if (inj_state == NULL) - injection_init_shmem(); - + /* + * Check the condition before attaching to the shared state: attaching + * allocates memory, which a process that is not meant to wait here must + * not do if the injection point is in a critical section. + */ if (!injection_point_allowed(condition, argstr)) return; + if (inj_state == NULL) + injection_init_shmem(); + /* * Use the injection point name for this custom wait event. Note that * this custom wait event name is not released, but we don't care much for diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index a7b40e084f..dda61f1756 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -47,6 +47,7 @@ tests += { 'inplace', 'reindex_concurrently_deferred', 'repack', + 'repack_commit_race', 'repack_decode', 'repack_temporal', 'repack_temporal_multirange', diff --git a/src/test/modules/injection_points/specs/repack_commit_race.spec b/src/test/modules/injection_points/specs/repack_commit_race.spec new file mode 100644 index 0000000000..a961d1c001 --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_commit_race.spec @@ -0,0 +1,96 @@ +# REPACK (CONCURRENTLY) takes its initial snapshot from the logical decoding +# snapshot builder, which counts a transaction as committed as soon as it has +# decoded its commit record. The transaction itself may still be between +# writing that record and updating CLOG. The snapshot must not be used before +# the transaction has finished committing, or the copy of the table takes it +# as aborted and its changes are lost: decoding starts after its commit record. +setup +{ + CREATE EXTENSION injection_points; + + CREATE TABLE repack_race(i int PRIMARY KEY, j int); + INSERT INTO repack_race(i, j) VALUES (1, 1), (2, 2); +} + +teardown +{ + DROP TABLE repack_race; + DROP EXTENSION injection_points; +} + +session s1 +step s1_repack +{ + REPACK (CONCURRENTLY) repack_race; +} +step s1_check +{ + SELECT i, j FROM repack_race ORDER BY i; +} + +# s2 and s3 keep a transaction with an XID open, so that the snapshot builder +# has to go through its BUILDING_SNAPSHOT and FULL_SNAPSHOT states instead of +# becoming consistent right away. +session s2 +step s2_begin +{ + BEGIN; + SELECT pg_current_xact_id() IS NOT NULL; +} +step s2_rollback +{ + ROLLBACK; +} + +session s3 +step s3_begin +{ + BEGIN; + SELECT pg_current_xact_id() IS NOT NULL; +} +step s3_rollback +{ + ROLLBACK; +} + +# s4 changes the table and stops after writing its commit record, before +# updating CLOG. +session s4 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('commit-before-clog-update', 'wait'); +} +step s4_changes +{ + INSERT INTO repack_race(i, j) VALUES (3, 3); + UPDATE repack_race SET j = j + 1 WHERE i = 1; + DELETE FROM repack_race WHERE i = 2; +} +teardown +{ + SELECT injection_points_detach('commit-before-clog-update'); +} + +session s5 +step s5_wakeup +{ + SELECT injection_points_wakeup('commit-before-clog-update'); +} + +# The snapshot builder waits for s2, then for s3. While it waits for s3, s4 +# writes its commit record: the builder will count s4 as committed and start +# decoding after it, but CLOG does not know about s4 yet. REPACK must not use +# its snapshot before s4 has finished committing. +# +# s4 cannot finish before s5 wakes it up, and s1 cannot finish before s4 does; +# the marker on s4_changes keeps the reporting order stable. +permutation + s2_begin + s1_repack + s3_begin + s2_rollback + s4_changes(s1_repack) + s3_rollback + s5_wakeup + s1_check -- 2.43.7