mirror of https://github.com/postgres/postgres
Commit a8fd13cab0
added support for prepared transactions to built-in
logical replication via a new option "two_phase" for a subscription. The
"two_phase" option was not allowed with the existing streaming option.
This commit permits the combination of "streaming" and "two_phase"
subscription options. It extends the pgoutput plugin and the subscriber
side code to add the prepare API for streaming transactions which will
apply the changes accumulated in the spool-file at prepare time.
Author: Peter Smith and Ajin Cherian
Reviewed-by: Vignesh C, Amit Kapila, Greg Nancarrow
Tested-By: Haiying Tang
Discussion: https://postgr.es/m/02DA5F5E-CECE-4D9C-8B4B-418077E2C010@postgrespro.ru
Discussion: https://postgr.es/m/CAMGcDxeqEpWj3fTXwqhSwBdXd2RS9jzwWscO-XbeCfso6ts3+Q@mail.gmail.com
pull/68/head
parent
6424337073
commit
63cf61cdeb
@ -0,0 +1,284 @@ |
|||||||
|
|
||||||
|
# Copyright (c) 2021, PostgreSQL Global Development Group |
||||||
|
|
||||||
|
# Test logical replication of 2PC with streaming. |
||||||
|
use strict; |
||||||
|
use warnings; |
||||||
|
use PostgresNode; |
||||||
|
use TestLib; |
||||||
|
use Test::More tests => 18; |
||||||
|
|
||||||
|
############################### |
||||||
|
# Setup |
||||||
|
############################### |
||||||
|
|
||||||
|
# Initialize publisher node |
||||||
|
my $node_publisher = PostgresNode->new('publisher'); |
||||||
|
$node_publisher->init(allows_streaming => 'logical'); |
||||||
|
$node_publisher->append_conf('postgresql.conf', qq( |
||||||
|
max_prepared_transactions = 10 |
||||||
|
logical_decoding_work_mem = 64kB |
||||||
|
)); |
||||||
|
$node_publisher->start; |
||||||
|
|
||||||
|
# Create subscriber node |
||||||
|
my $node_subscriber = PostgresNode->new('subscriber'); |
||||||
|
$node_subscriber->init(allows_streaming => 'logical'); |
||||||
|
$node_subscriber->append_conf('postgresql.conf', qq( |
||||||
|
max_prepared_transactions = 10 |
||||||
|
)); |
||||||
|
$node_subscriber->start; |
||||||
|
|
||||||
|
# Create some pre-existing content on publisher |
||||||
|
$node_publisher->safe_psql('postgres', "CREATE TABLE test_tab (a int primary key, b varchar)"); |
||||||
|
$node_publisher->safe_psql('postgres', "INSERT INTO test_tab VALUES (1, 'foo'), (2, 'bar')"); |
||||||
|
|
||||||
|
# Setup structure on subscriber (columns a and b are compatible with same table name on publisher) |
||||||
|
$node_subscriber->safe_psql('postgres', |
||||||
|
"CREATE TABLE test_tab (a int primary key, b text, c timestamptz DEFAULT now(), d bigint DEFAULT 999)"); |
||||||
|
|
||||||
|
# Setup logical replication (streaming = on) |
||||||
|
my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; |
||||||
|
$node_publisher->safe_psql('postgres', "CREATE PUBLICATION tap_pub FOR TABLE test_tab"); |
||||||
|
|
||||||
|
my $appname = 'tap_sub'; |
||||||
|
$node_subscriber->safe_psql('postgres', " |
||||||
|
CREATE SUBSCRIPTION tap_sub |
||||||
|
CONNECTION '$publisher_connstr application_name=$appname' |
||||||
|
PUBLICATION tap_pub |
||||||
|
WITH (streaming = on, two_phase = on)"); |
||||||
|
|
||||||
|
# Wait for subscriber to finish initialization |
||||||
|
$node_publisher->wait_for_catchup($appname); |
||||||
|
|
||||||
|
# Also wait for initial table sync to finish |
||||||
|
my $synced_query = |
||||||
|
"SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');"; |
||||||
|
$node_subscriber->poll_query_until('postgres', $synced_query) |
||||||
|
or die "Timed out while waiting for subscriber to synchronize data"; |
||||||
|
|
||||||
|
# Also wait for two-phase to be enabled |
||||||
|
my $twophase_query = |
||||||
|
"SELECT count(1) = 0 FROM pg_subscription WHERE subtwophasestate NOT IN ('e');"; |
||||||
|
$node_subscriber->poll_query_until('postgres', $twophase_query) |
||||||
|
or die "Timed out while waiting for subscriber to enable twophase"; |
||||||
|
|
||||||
|
############################### |
||||||
|
# Check initial data was copied to subscriber |
||||||
|
############################### |
||||||
|
my $result = $node_subscriber->safe_psql('postgres', "SELECT count(*), count(c), count(d = 999) FROM test_tab"); |
||||||
|
is($result, qq(2|2|2), 'check initial data was copied to subscriber'); |
||||||
|
|
||||||
|
############################### |
||||||
|
# Test 2PC PREPARE / COMMIT PREPARED. |
||||||
|
# 1. Data is streamed as a 2PC transaction. |
||||||
|
# 2. Then do commit prepared. |
||||||
|
# |
||||||
|
# Expect all data is replicated on subscriber side after the commit. |
||||||
|
############################### |
||||||
|
|
||||||
|
# check that 2PC gets replicated to subscriber |
||||||
|
# Insert, update and delete enough rows to exceed the 64kB limit. |
||||||
|
$node_publisher->safe_psql('postgres', q{ |
||||||
|
BEGIN; |
||||||
|
INSERT INTO test_tab SELECT i, md5(i::text) FROM generate_series(3, 5000) s(i); |
||||||
|
UPDATE test_tab SET b = md5(b) WHERE mod(a,2) = 0; |
||||||
|
DELETE FROM test_tab WHERE mod(a,3) = 0; |
||||||
|
PREPARE TRANSACTION 'test_prepared_tab';}); |
||||||
|
|
||||||
|
$node_publisher->wait_for_catchup($appname); |
||||||
|
|
||||||
|
# check that transaction is in prepared state on subscriber |
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM pg_prepared_xacts;"); |
||||||
|
is($result, qq(1), 'transaction is prepared on subscriber'); |
||||||
|
|
||||||
|
# 2PC transaction gets committed |
||||||
|
$node_publisher->safe_psql('postgres', "COMMIT PREPARED 'test_prepared_tab';"); |
||||||
|
|
||||||
|
$node_publisher->wait_for_catchup($appname); |
||||||
|
|
||||||
|
# check that transaction is committed on subscriber |
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*), count(c), count(d = 999) FROM test_tab"); |
||||||
|
is($result, qq(3334|3334|3334), 'Rows inserted by 2PC have committed on subscriber, and extra columns contain local defaults'); |
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM pg_prepared_xacts;"); |
||||||
|
is($result, qq(0), 'transaction is committed on subscriber'); |
||||||
|
|
||||||
|
############################### |
||||||
|
# Test 2PC PREPARE / ROLLBACK PREPARED. |
||||||
|
# 1. Table is deleted back to 2 rows which are replicated on subscriber. |
||||||
|
# 2. Data is streamed using 2PC. |
||||||
|
# 3. Do rollback prepared. |
||||||
|
# |
||||||
|
# Expect data rolls back leaving only the original 2 rows. |
||||||
|
############################### |
||||||
|
|
||||||
|
# First, delete the data except for 2 rows (will be replicated) |
||||||
|
$node_publisher->safe_psql('postgres', "DELETE FROM test_tab WHERE a > 2;"); |
||||||
|
|
||||||
|
# Then insert, update and delete enough rows to exceed the 64kB limit. |
||||||
|
$node_publisher->safe_psql('postgres', q{ |
||||||
|
BEGIN; |
||||||
|
INSERT INTO test_tab SELECT i, md5(i::text) FROM generate_series(3, 5000) s(i); |
||||||
|
UPDATE test_tab SET b = md5(b) WHERE mod(a,2) = 0; |
||||||
|
DELETE FROM test_tab WHERE mod(a,3) = 0; |
||||||
|
PREPARE TRANSACTION 'test_prepared_tab';}); |
||||||
|
|
||||||
|
$node_publisher->wait_for_catchup($appname); |
||||||
|
|
||||||
|
# check that transaction is in prepared state on subscriber |
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM pg_prepared_xacts;"); |
||||||
|
is($result, qq(1), 'transaction is prepared on subscriber'); |
||||||
|
|
||||||
|
# 2PC transaction gets aborted |
||||||
|
$node_publisher->safe_psql('postgres', "ROLLBACK PREPARED 'test_prepared_tab';"); |
||||||
|
|
||||||
|
$node_publisher->wait_for_catchup($appname); |
||||||
|
|
||||||
|
# check that transaction is aborted on subscriber |
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*), count(c), count(d = 999) FROM test_tab"); |
||||||
|
is($result, qq(2|2|2), 'Rows inserted by 2PC are rolled back, leaving only the original 2 rows'); |
||||||
|
|
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM pg_prepared_xacts;"); |
||||||
|
is($result, qq(0), 'transaction is aborted on subscriber'); |
||||||
|
|
||||||
|
############################### |
||||||
|
# Check that 2PC COMMIT PREPARED is decoded properly on crash restart. |
||||||
|
# 1. insert, update and delete enough rows to exceed the 64kB limit. |
||||||
|
# 2. Then server crashes before the 2PC transaction is committed. |
||||||
|
# 3. After servers are restarted the pending transaction is committed. |
||||||
|
# |
||||||
|
# Expect all data is replicated on subscriber side after the commit. |
||||||
|
# Note: both publisher and subscriber do crash/restart. |
||||||
|
############################### |
||||||
|
|
||||||
|
$node_publisher->safe_psql('postgres', q{ |
||||||
|
BEGIN; |
||||||
|
INSERT INTO test_tab SELECT i, md5(i::text) FROM generate_series(3, 5000) s(i); |
||||||
|
UPDATE test_tab SET b = md5(b) WHERE mod(a,2) = 0; |
||||||
|
DELETE FROM test_tab WHERE mod(a,3) = 0; |
||||||
|
PREPARE TRANSACTION 'test_prepared_tab';}); |
||||||
|
|
||||||
|
$node_subscriber->stop('immediate'); |
||||||
|
$node_publisher->stop('immediate'); |
||||||
|
|
||||||
|
$node_publisher->start; |
||||||
|
$node_subscriber->start; |
||||||
|
|
||||||
|
# commit post the restart |
||||||
|
$node_publisher->safe_psql('postgres', "COMMIT PREPARED 'test_prepared_tab';"); |
||||||
|
$node_publisher->wait_for_catchup($appname); |
||||||
|
|
||||||
|
# check inserts are visible |
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*), count(c), count(d = 999) FROM test_tab"); |
||||||
|
is($result, qq(3334|3334|3334), 'Rows inserted by 2PC have committed on subscriber, and extra columns contain local defaults'); |
||||||
|
|
||||||
|
############################### |
||||||
|
# Do INSERT after the PREPARE but before ROLLBACK PREPARED. |
||||||
|
# 1. Table is deleted back to 2 rows which are replicated on subscriber. |
||||||
|
# 2. Data is streamed using 2PC. |
||||||
|
# 3. A single row INSERT is done which is after the PREPARE. |
||||||
|
# 4. Then do a ROLLBACK PREPARED. |
||||||
|
# |
||||||
|
# Expect the 2PC data rolls back leaving only 3 rows on the subscriber |
||||||
|
# (the original 2 + inserted 1). |
||||||
|
############################### |
||||||
|
|
||||||
|
# First, delete the data except for 2 rows (will be replicated) |
||||||
|
$node_publisher->safe_psql('postgres', "DELETE FROM test_tab WHERE a > 2;"); |
||||||
|
|
||||||
|
# Then insert, update and delete enough rows to exceed the 64kB limit. |
||||||
|
$node_publisher->safe_psql('postgres', q{ |
||||||
|
BEGIN; |
||||||
|
INSERT INTO test_tab SELECT i, md5(i::text) FROM generate_series(3, 5000) s(i); |
||||||
|
UPDATE test_tab SET b = md5(b) WHERE mod(a,2) = 0; |
||||||
|
DELETE FROM test_tab WHERE mod(a,3) = 0; |
||||||
|
PREPARE TRANSACTION 'test_prepared_tab';}); |
||||||
|
|
||||||
|
$node_publisher->wait_for_catchup($appname); |
||||||
|
|
||||||
|
# check that transaction is in prepared state on subscriber |
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM pg_prepared_xacts;"); |
||||||
|
is($result, qq(1), 'transaction is prepared on subscriber'); |
||||||
|
|
||||||
|
# Insert a different record (now we are outside of the 2PC transaction) |
||||||
|
# Note: the 2PC transaction still holds row locks so make sure this insert is for a separate primary key |
||||||
|
$node_publisher->safe_psql('postgres', "INSERT INTO test_tab VALUES (99999, 'foobar')"); |
||||||
|
|
||||||
|
# 2PC transaction gets aborted |
||||||
|
$node_publisher->safe_psql('postgres', "ROLLBACK PREPARED 'test_prepared_tab';"); |
||||||
|
|
||||||
|
$node_publisher->wait_for_catchup($appname); |
||||||
|
|
||||||
|
# check that transaction is aborted on subscriber, |
||||||
|
# but the extra INSERT outside of the 2PC still was replicated |
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*), count(c), count(d = 999) FROM test_tab"); |
||||||
|
is($result, qq(3|3|3), 'check the outside insert was copied to subscriber'); |
||||||
|
|
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM pg_prepared_xacts;"); |
||||||
|
is($result, qq(0), 'transaction is aborted on subscriber'); |
||||||
|
|
||||||
|
############################### |
||||||
|
# Do INSERT after the PREPARE but before COMMIT PREPARED. |
||||||
|
# 1. Table is deleted back to 2 rows which are replicated on subscriber. |
||||||
|
# 2. Data is streamed using 2PC. |
||||||
|
# 3. A single row INSERT is done which is after the PREPARE. |
||||||
|
# 4. Then do a COMMIT PREPARED. |
||||||
|
# |
||||||
|
# Expect 2PC data + the extra row are on the subscriber |
||||||
|
# (the 3334 + inserted 1 = 3335). |
||||||
|
############################### |
||||||
|
|
||||||
|
# First, delete the data except for 2 rows (will be replicated) |
||||||
|
$node_publisher->safe_psql('postgres', "DELETE FROM test_tab WHERE a > 2;"); |
||||||
|
|
||||||
|
# Then insert, update and delete enough rows to exceed the 64kB limit. |
||||||
|
$node_publisher->safe_psql('postgres', q{ |
||||||
|
BEGIN; |
||||||
|
INSERT INTO test_tab SELECT i, md5(i::text) FROM generate_series(3, 5000) s(i); |
||||||
|
UPDATE test_tab SET b = md5(b) WHERE mod(a,2) = 0; |
||||||
|
DELETE FROM test_tab WHERE mod(a,3) = 0; |
||||||
|
PREPARE TRANSACTION 'test_prepared_tab';}); |
||||||
|
|
||||||
|
$node_publisher->wait_for_catchup($appname); |
||||||
|
|
||||||
|
# check that transaction is in prepared state on subscriber |
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM pg_prepared_xacts;"); |
||||||
|
is($result, qq(1), 'transaction is prepared on subscriber'); |
||||||
|
|
||||||
|
# Insert a different record (now we are outside of the 2PC transaction) |
||||||
|
# Note: the 2PC transaction still holds row locks so make sure this insert is for a separare primary key |
||||||
|
$node_publisher->safe_psql('postgres', "INSERT INTO test_tab VALUES (99999, 'foobar')"); |
||||||
|
|
||||||
|
# 2PC transaction gets committed |
||||||
|
$node_publisher->safe_psql('postgres', "COMMIT PREPARED 'test_prepared_tab';"); |
||||||
|
|
||||||
|
$node_publisher->wait_for_catchup($appname); |
||||||
|
|
||||||
|
# check that transaction is committed on subscriber |
||||||
|
$result = $node_subscriber->safe_psql('postgres', |
||||||
|
"SELECT count(*), count(c), count(d = 999) FROM test_tab"); |
||||||
|
is($result, qq(3335|3335|3335), 'Rows inserted by 2PC (as well as outside insert) have committed on subscriber, and extra columns contain local defaults'); |
||||||
|
|
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM pg_prepared_xacts;"); |
||||||
|
is($result, qq(0), 'transaction is committed on subscriber'); |
||||||
|
|
||||||
|
############################### |
||||||
|
# check all the cleanup |
||||||
|
############################### |
||||||
|
|
||||||
|
$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub"); |
||||||
|
|
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM pg_subscription"); |
||||||
|
is($result, qq(0), 'check subscription was dropped on subscriber'); |
||||||
|
|
||||||
|
$result = $node_publisher->safe_psql('postgres', "SELECT count(*) FROM pg_replication_slots"); |
||||||
|
is($result, qq(0), 'check replication slot was dropped on publisher'); |
||||||
|
|
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM pg_subscription_rel"); |
||||||
|
is($result, qq(0), 'check subscription relation status was dropped on subscriber'); |
||||||
|
|
||||||
|
$result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM pg_replication_origin"); |
||||||
|
is($result, qq(0), 'check replication origin was dropped on subscriber'); |
||||||
|
|
||||||
|
$node_subscriber->stop('fast'); |
||||||
|
$node_publisher->stop('fast'); |
Loading…
Reference in new issue