mirror of
https://github.com/postgres/postgres.git
synced 2026-10-23 13:29:09 +03:00
A two-phase transaction that is assigned an XID but produces no change
to be decoded -- for example, one that only acquires row locks via
SELECT ... FOR SHARE -- has no base snapshot in the reorder
buffer. ReorderBufferReplay() already skips such a transaction at
PREPARE time and never invokes the begin_prepare/change/prepare
callbacks for it, but ReorderBufferFinishPrepared() still called the
commit_prepared (or rollback_prepared) callback. As a result a
spurious COMMIT/ROLLBACK PREPARED was sent to the output plugin with
no preceding PREPARE. For the built-in subscriber this breaks
replication (the apply worker fails to find the prepared transaction),
and test_decoding could even crash.
Fix this by detecting an empty transaction (base_snapshot == NULL) in
ReorderBufferFinishPrepared() and cleaning it up without invoking the
commit/rollback prepared callbacks, mirroring the existing empty
transaction handling in ReorderBufferReplay().
On v18 and newer versions, commit 072ee847ad changed
ReorderBufferPrepare() to send the prepare whenever it had not already
been sent, which also fires for empty transactions and emits a
spurious PREPARE. On those branches ReorderBufferPrepare() is
therefore additionally guarded with base_snapshot != NULL. This guard
and the Assert(!rbtxn_sent_prepare()) added in
ReorderBufferFinishPrepared(), are not necessary on v17 and older
versions: there ReorderBufferPrepare() only sends a prepare for
concurrently-aborted transactions (which never applies to an empty
transaction) and the RBTXN_SENT_PREPARE flag does not exist.
Back-patch to v14, where decoding of two-phase transactions was
introduced.
Bug: #19556
Reported-by: Alexander Kozhemyakin <a.kozhemyakin@postgrespro.ru>
Reviewed-by: Amit Kapila <amit.kapila16@gmail.com>
Discussion: https://postgr.es/m/19556-daa6d7ea65054d48@postgresql.org
Backpatch-through: 14
146 lines
6.7 KiB
PL/PgSQL
146 lines
6.7 KiB
PL/PgSQL
-- Test prepared transactions. When two-phase-commit is enabled, transactions are
|
|
-- decoded at PREPARE time rather than at COMMIT PREPARED time.
|
|
SET synchronous_commit = on;
|
|
SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding', false, true);
|
|
|
|
CREATE TABLE test_prepared1(id integer primary key);
|
|
CREATE TABLE test_prepared2(id integer primary key);
|
|
|
|
-- Test that decoding happens at PREPARE time when two-phase-commit is enabled.
|
|
-- Decoding after COMMIT PREPARED must have all the commands in the transaction.
|
|
BEGIN;
|
|
INSERT INTO test_prepared1 VALUES (1);
|
|
INSERT INTO test_prepared1 VALUES (2);
|
|
-- should show nothing because the xact has not been prepared yet.
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
PREPARE TRANSACTION 'test_prepared#1';
|
|
-- should show both the above inserts and the PREPARE TRANSACTION.
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
COMMIT PREPARED 'test_prepared#1';
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
|
|
-- Test that rollback of a prepared xact is decoded.
|
|
BEGIN;
|
|
INSERT INTO test_prepared1 VALUES (3);
|
|
PREPARE TRANSACTION 'test_prepared#2';
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
ROLLBACK PREPARED 'test_prepared#2';
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
|
|
-- Test prepare of a xact containing ddl. Leaving xact uncommitted for next test.
|
|
BEGIN;
|
|
ALTER TABLE test_prepared1 ADD COLUMN data text;
|
|
INSERT INTO test_prepared1 VALUES (4, 'frakbar');
|
|
PREPARE TRANSACTION 'test_prepared#3';
|
|
-- confirm that exclusive lock from the ALTER command is held on test_prepared1 table
|
|
SELECT 'test_prepared_1' AS relation, locktype, mode
|
|
FROM pg_locks
|
|
WHERE locktype = 'relation'
|
|
AND relation = 'test_prepared1'::regclass;
|
|
-- The insert should show the newly altered column but not the DDL.
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
|
|
-- Test that we decode correctly while an uncommitted prepared xact
|
|
-- with ddl exists.
|
|
--
|
|
-- Use a separate table for the concurrent transaction because the lock from
|
|
-- the ALTER will stop us inserting into the other one.
|
|
--
|
|
INSERT INTO test_prepared2 VALUES (5);
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
|
|
COMMIT PREPARED 'test_prepared#3';
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
-- make sure stuff still works
|
|
INSERT INTO test_prepared1 VALUES (6);
|
|
INSERT INTO test_prepared2 VALUES (7);
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
|
|
-- Check 'CLUSTER' (as operation that hold exclusive lock) doesn't block
|
|
-- logical decoding.
|
|
BEGIN;
|
|
INSERT INTO test_prepared1 VALUES (8, 'othercol');
|
|
CLUSTER test_prepared1 USING test_prepared1_pkey;
|
|
INSERT INTO test_prepared1 VALUES (9, 'othercol2');
|
|
PREPARE TRANSACTION 'test_prepared_lock';
|
|
|
|
SELECT 'test_prepared1' AS relation, locktype, mode
|
|
FROM pg_locks
|
|
WHERE locktype = 'relation'
|
|
AND relation = 'test_prepared1'::regclass;
|
|
-- The above CLUSTER command shouldn't cause a timeout on 2pc decoding.
|
|
SET statement_timeout = '180s';
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
RESET statement_timeout;
|
|
COMMIT PREPARED 'test_prepared_lock';
|
|
-- consume the commit
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
|
|
-- Test savepoints and sub-xacts. Creating savepoints will create
|
|
-- sub-xacts implicitly.
|
|
BEGIN;
|
|
CREATE TABLE test_prepared_savepoint (a int);
|
|
INSERT INTO test_prepared_savepoint VALUES (1);
|
|
SAVEPOINT test_savepoint;
|
|
INSERT INTO test_prepared_savepoint VALUES (2);
|
|
ROLLBACK TO SAVEPOINT test_savepoint;
|
|
PREPARE TRANSACTION 'test_prepared_savepoint';
|
|
-- should show only 1, not 2
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
COMMIT PREPARED 'test_prepared_savepoint';
|
|
-- consume the commit
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
|
|
-- Test that a GID containing "_nodecode" gets decoded at commit prepared time.
|
|
BEGIN;
|
|
INSERT INTO test_prepared1 VALUES (20);
|
|
PREPARE TRANSACTION 'test_prepared_nodecode';
|
|
-- should show nothing
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
COMMIT PREPARED 'test_prepared_nodecode';
|
|
-- should be decoded now
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
|
|
-- Test that accessing a TOAST table is permitted during the decoding of a
|
|
-- prepared transaction.
|
|
|
|
-- Create a table with a column that uses a TOASTed default value.
|
|
-- (temporarily hide query, to avoid the long CREATE TABLE stmt)
|
|
\set ECHO none
|
|
SELECT 'CREATE TABLE test_tab (a text DEFAULT ''' || string_agg('toast value', '') || ''');' FROM generate_series(1, 4000)
|
|
\gexec
|
|
\set ECHO all
|
|
|
|
BEGIN;
|
|
INSERT INTO test_tab VALUES('test');
|
|
PREPARE TRANSACTION 'test_toast_table_access';
|
|
|
|
SELECT count(*) FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1', 'stream-changes', '1');
|
|
|
|
COMMIT PREPARED 'test_toast_table_access';
|
|
|
|
-- consume commit prepared
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1', 'stream-changes', '1');
|
|
|
|
-- Test that an empty prepared transaction should not be decoded, whether it
|
|
-- is committed or rolled back.
|
|
BEGIN;
|
|
SELECT * FROM test_prepared1 WHERE id = 1 FOR SHARE;
|
|
PREPARE TRANSACTION 'test_empty_transaction';
|
|
COMMIT PREPARED 'test_empty_transaction';
|
|
BEGIN;
|
|
SELECT * FROM test_prepared1 WHERE id = 1 FOR SHARE;
|
|
PREPARE TRANSACTION 'test_empty_transaction';
|
|
ROLLBACK PREPARED 'test_empty_transaction';
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
|
|
-- Test 8:
|
|
-- cleanup and make sure results are also empty
|
|
DROP TABLE test_prepared1;
|
|
DROP TABLE test_prepared2;
|
|
DROP TABLE test_tab;
|
|
-- show results. There should be nothing to show
|
|
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
|
|
|
|
SELECT pg_drop_replication_slot('regression_slot');
|