From 88c6c10f9f9750500fcc3feb4b72d00a9bf9623c Mon Sep 17 00:00:00 2001 From: Vaijayanti Bharadwaj Date: Tue, 27 Oct 2026 22:07:23 +0530 Subject: [PATCH v2 2/2] Add test for concurrent abort of a prepared transaction's subtransaction Add an injection point in the change loop of ReorderBufferProcessTXN() and an isolation test using it. The test decodes a prepared transaction whose changes all belong to a subtransaction, holds decoding at the first change, and meanwhile rolls the transaction back and updates the catalog row that its ALTER TABLE had updated. The concurrent abort must be detected and decoding of the transaction must stop, so no changes are returned. The test needs max_prepared_transactions, so add that to the module's extra.conf. Backpatch-through: 17 --- .../replication/logical/reorderbuffer.c | 3 + src/test/modules/injection_points/Makefile | 3 +- .../prepared_subxact_concurrent_abort.out | 49 +++++++++++++ src/test/modules/injection_points/extra.conf | 1 + src/test/modules/injection_points/meson.build | 1 + .../prepared_subxact_concurrent_abort.spec | 71 +++++++++++++++++++ 6 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/injection_points/expected/prepared_subxact_concurrent_abort.out create mode 100644 src/test/modules/injection_points/specs/prepared_subxact_concurrent_abort.spec diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c index b8b3efed946..3d0f6917ef1 100644 --- a/src/backend/replication/logical/reorderbuffer.c +++ b/src/backend/replication/logical/reorderbuffer.c @@ -109,6 +109,7 @@ #include "storage/procarray.h" #include "storage/sinval.h" #include "utils/builtins.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/rel.h" @@ -2311,6 +2312,8 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, SetupCheckXidLive(curtxn->xid); } + INJECTION_POINT("reorderbuffer-process-change", NULL); + switch (change->action) { case REORDER_BUFFER_CHANGE_INTERNAL_SPEC_CONFIRM: diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index 775ad37860d..34c8190c22e 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -36,7 +36,8 @@ ISOLATION = basic \ $(WAIT_CLEANUP_TST) \ heap_lock_update \ on_conflict_probe_window \ - multixact-aborted-updater + multixact-aborted-updater \ + prepared_subxact_concurrent_abort # some isolation tests require wal_level=replica ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf diff --git a/src/test/modules/injection_points/expected/prepared_subxact_concurrent_abort.out b/src/test/modules/injection_points/expected/prepared_subxact_concurrent_abort.out new file mode 100644 index 00000000000..cde18918e23 --- /dev/null +++ b/src/test/modules/injection_points/expected/prepared_subxact_concurrent_abort.out @@ -0,0 +1,49 @@ +Parsed test spec with 2 sessions + +starting permutation: s2_prepare s1_decode s2_rollback_prepared s2_update_catalog s2_detach s2_wakeup +?column? +-------- +init +(1 row) + +injection_points_attach +----------------------- + +(1 row) + +step s2_prepare: + BEGIN; + SAVEPOINT sp; + ALTER TABLE t ADD COLUMN b int; + INSERT INTO t VALUES (1, 1); + RELEASE SAVEPOINT sp; + PREPARE TRANSACTION 'x'; + +step s1_decode: + SELECT data FROM pg_logical_slot_get_changes('s', NULL, NULL, + 'skip-empty-xacts', '1', 'include-xids', '0'); + +step s2_rollback_prepared: ROLLBACK PREPARED 'x'; +step s2_update_catalog: ALTER TABLE t SET (fillfactor = 90); +step s2_detach: SELECT injection_points_detach('reorderbuffer-process-change'); +injection_points_detach +----------------------- + +(1 row) + +step s2_wakeup: SELECT injection_points_wakeup('reorderbuffer-process-change'); +injection_points_wakeup +----------------------- + +(1 row) + +step s1_decode: <... completed> +data +---- +(0 rows) + +pg_drop_replication_slot +------------------------ + +(1 row) + diff --git a/src/test/modules/injection_points/extra.conf b/src/test/modules/injection_points/extra.conf index 010abb193a8..50aaa646bf1 100644 --- a/src/test/modules/injection_points/extra.conf +++ b/src/test/modules/injection_points/extra.conf @@ -1 +1,2 @@ wal_level=replica +max_prepared_transactions=5 diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index 60148f96fda..78cd6aa5001 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -44,6 +44,7 @@ injection_points_isolation = [ 'heap_lock_update', 'on_conflict_probe_window', 'multixact-aborted-updater', + 'prepared_subxact_concurrent_abort', ] # wait_cleanup terminates another backend, whose FATAL message can be lost diff --git a/src/test/modules/injection_points/specs/prepared_subxact_concurrent_abort.spec b/src/test/modules/injection_points/specs/prepared_subxact_concurrent_abort.spec new file mode 100644 index 00000000000..b4f521a93e8 --- /dev/null +++ b/src/test/modules/injection_points/specs/prepared_subxact_concurrent_abort.spec @@ -0,0 +1,71 @@ +# Concurrent abort of a prepared transaction whose changes all belong to a +# subtransaction. +# +# Decoding a prepared transaction at PREPARE time must notice if the +# transaction is rolled back while it is being decoded, because the catalog +# versions it sees can no longer be trusted. ReorderBufferProcessTXN arms +# that check by calling SetupCheckXidLive() for each change. Here the +# transaction's catalog change and the DML after it are made inside a +# subtransaction, with no change of the top-level transaction, so the check +# has to be armed for the subtransaction's changes. +# +# s1 decodes the prepared transaction and is held at its first change by an +# injection point. Meanwhile s2 rolls the transaction back and updates the +# same pg_class row that the aborted ALTER TABLE had updated, which +# overwrites the xmax of the old row version left by the aborted transaction. + +setup +{ + CREATE EXTENSION injection_points; + CREATE TABLE t (a int); +} + +# Slot creation cannot share a transaction with writes. +setup +{ + SELECT 'init' FROM pg_create_logical_replication_slot('s', 'test_decoding', false, true); +} + +teardown +{ + DROP TABLE t; + DROP EXTENSION injection_points; + SELECT pg_drop_replication_slot('s'); +} + +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('reorderbuffer-process-change', 'wait'); +} +step s1_decode +{ + SELECT data FROM pg_logical_slot_get_changes('s', NULL, NULL, + 'skip-empty-xacts', '1', 'include-xids', '0'); +} + +session s2 +step s2_prepare +{ + BEGIN; + SAVEPOINT sp; + ALTER TABLE t ADD COLUMN b int; + INSERT INTO t VALUES (1, 1); + RELEASE SAVEPOINT sp; + PREPARE TRANSACTION 'x'; +} +step s2_rollback_prepared { ROLLBACK PREPARED 'x'; } +# Update the pg_class row of t that the aborted transaction had updated. +step s2_update_catalog { ALTER TABLE t SET (fillfactor = 90); } +# Detach first so that decoding does not wait again at its next change. +step s2_detach { SELECT injection_points_detach('reorderbuffer-process-change'); } +step s2_wakeup { SELECT injection_points_wakeup('reorderbuffer-process-change'); } + +permutation + s2_prepare + s1_decode + s2_rollback_prepared + s2_update_catalog + s2_detach + s2_wakeup -- 2.55.0