From 496463b1d4b4e7d0685f03144ed23c7c3b24a7a0 Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Wed, 10 Jul 2024 21:43:43 +0530 Subject: [PATCH v5] Fix data loss during initial sync in logical replication. Previously, when adding tables to a publication in PostgreSQL, they were locked using ShareUpdateExclusiveLock mode. This mode allowed the lock to succeed even if there were ongoing DML transactions on that table. As a consequence, the ALTER PUBLICATION command could be completed before these transactions, leading to a scenario where the catalog snapshot used for replication did not include changes from transactions initiated before the alteration. To fix this issue, tables are now locked using ShareRowExclusiveLock mode during the addition to a publication. This change ensures that the ALTER PUBLICATION command waits for any ongoing transactions on the tables (to be added to the publication) to be completed before proceeding. As a result, transactions initiated before the publication alteration are correctly included in the replication process. Reported-by: Tomas Vondra Diagnosed-by: Andres Freund Author: Vignesh C, Tomas Vondra Reviewed-by: Amit Kapila Backpatch-through: 12 Discussion: https://postgr.es/m/de52b282-1166-1180-45a2-8d8917ca74c6@enterprisedb.com --- src/backend/commands/publicationcmds.c | 16 ++- src/test/subscription/t/100_bugs.pl | 161 ++++++++++++++++++++++++- 2 files changed, 171 insertions(+), 6 deletions(-) diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c index 7ee8825522..8135db2cc0 100644 --- a/src/backend/commands/publicationcmds.c +++ b/src/backend/commands/publicationcmds.c @@ -548,8 +548,14 @@ RemovePublicationRelById(Oid proid) /* * Open relations specified by a RangeVar list. - * The returned tables are locked in ShareUpdateExclusiveLock mode in order to - * add them to a publication. + * + * The returned tables are locked in ShareRowExclusiveLock mode to add them + * to a publication. The table needs to be locked in ShareRowExclusiveLock + * mode to ensure that any ongoing transactions involving that table are + * completed before adding it to the publication. Otherwise, the transaction + * initiated before the alteration of the publication will continue to use a + * catalog snapshot predating the publication change, leading to + * non-replication of these transaction changes. */ static List * OpenTableList(List *tables) @@ -571,7 +577,7 @@ OpenTableList(List *tables) /* Allow query cancel in case this takes a long time */ CHECK_FOR_INTERRUPTS(); - rel = table_openrv(rv, ShareUpdateExclusiveLock); + rel = table_openrv(rv, ShareRowExclusiveLock); myrelid = RelationGetRelid(rel); /* @@ -583,7 +589,7 @@ OpenTableList(List *tables) */ if (list_member_oid(relids, myrelid)) { - table_close(rel, ShareUpdateExclusiveLock); + table_close(rel, ShareRowExclusiveLock); continue; } @@ -601,7 +607,7 @@ OpenTableList(List *tables) List *children; ListCell *child; - children = find_all_inheritors(myrelid, ShareUpdateExclusiveLock, + children = find_all_inheritors(myrelid, ShareRowExclusiveLock, NULL); foreach(child, children) diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index 7ebb97bbcf..bdbacef33c 100644 --- a/src/test/subscription/t/100_bugs.pl +++ b/src/test/subscription/t/100_bugs.pl @@ -3,7 +3,7 @@ use strict; use warnings; use PostgresNode; use TestLib; -use Test::More tests => 9; +use Test::More tests => 13; # Bug #15114 @@ -291,3 +291,162 @@ is( $node_subscriber_d_cols->safe_psql( $node_publisher_d_cols->stop('fast'); $node_subscriber_d_cols->stop('fast'); + +# The bug was that the incremental data synchronization was being skipped when +# a new table is added to the publication in presence of a concurrent active +# transaction performing the DML on the same table. +my $node_publisher1 = get_new_node('node_publisher1'); +$node_publisher1->init(allows_streaming => 'logical'); +$node_publisher1->start; + +my $node_subscriber1 = get_new_node('node_subscriber1'); +$node_subscriber1->init(allows_streaming => 'logical'); +$node_subscriber1->start; + +$publisher_connstr = $node_publisher1->connstr . ' dbname=postgres'; +$node_publisher1->safe_psql('postgres', + "CREATE PUBLICATION pub1"); +$node_subscriber1->safe_psql('postgres', + "CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1" +); + +# Create tables in publisher and subscriber. +$node_publisher1->safe_psql( + 'postgres', qq( + CREATE TABLE tab_conc(a int); + CREATE TABLE tab1_conc(a int); + CREATE TABLE tab1_conc_child() inherits (tab1_conc); +)); + +$node_subscriber1->safe_psql( + 'postgres', qq( + CREATE TABLE tab_conc(a int); + CREATE TABLE tab1_conc(a int); + CREATE TABLE tab1_conc_child() inherits (tab1_conc); +)); + +# Bump the query timeout to avoid false negatives on slow test systems. +my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default; + +# Initiate a background session that keeps a transaction active. +my $background_psql1 = $node_publisher1->background_psql( + 'postgres', + on_error_stop => 0, + timeout => $psql_timeout_secs); + +# Maintain an active transaction with the table. +$background_psql1->set_query_timer_restart(); +$background_psql1->query_safe( + qq[ + BEGIN; + INSERT INTO tab_conc VALUES (1); +]); + +# Add the table to the publication using background_psql, as the alter +# publication operation will wait for the lock and can only be completed after +# the previous open transaction is committed. +my $background_psql2 = $node_publisher1->background_psql( + 'postgres', + on_error_stop => 0, + timeout => $psql_timeout_secs); + +$background_psql2->set_query_timer_restart(); + +# This operation will wait because there is an open transaction holding a lock. +$background_psql2->query_until(qr//, + "ALTER PUBLICATION pub1 ADD TABLE tab_conc;\n"); + +# Verify that the table addition is waiting to acquire a ShareRowExclusiveLock. +$node_publisher1->poll_query_until('postgres', + "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab_conc'::regclass AND mode = 'ShareRowExclusiveLock';" + ) + or die + "Timed out while waiting for alter publication tries to wait on ShareRowExclusiveLock"; + +# Complete the old transaction. +$background_psql1->query_safe(qq[COMMIT]); + +# Maintain an active transaction with inheritance table. +$background_psql1->query_safe( + qq[ + BEGIN; + INSERT INTO tab1_conc_child VALUES (1); +]); + +# Add an inheritance table to the publication, this operation will wait because +# there is an open transaction holding a lock. +$background_psql2->query_until(qr//, + "ALTER PUBLICATION pub1 ADD TABLE tab1_conc;\n"); + +# Verify that the child table addition is waiting to acquire a +# ShareRowExclusiveLock. +$node_publisher1->poll_query_until('postgres', + "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab1_conc_child'::regclass AND mode = 'ShareRowExclusiveLock';" + ) + or die + "Timed out while waiting for alter publication tries to wait on ShareRowExclusiveLock"; + +# Complete the old transaction. +$background_psql1->query_safe(qq[COMMIT]); +$background_psql1->quit; + +# Wait till the tables are added to the publication. +$node_publisher1->poll_query_until('postgres', + "SELECT COUNT(1) = 2 FROM pg_publication_rel WHERE prrelid IN ('tab_conc'::regclass, 'tab1_conc'::regclass);" + ) + or die + "Timed out while waiting for alter publication to add the table to the publication"; + +$background_psql1->quit; + +$node_publisher1->safe_psql( + 'postgres', qq( + INSERT INTO tab_conc VALUES (2); + INSERT INTO tab1_conc_child VALUES (2); +)); + +# Refresh the publication. +$node_subscriber1->safe_psql('postgres', + 'ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION'); + +$node_subscriber1->wait_for_subscription_sync($node_publisher1, 'sub1'); + +my $result = $node_subscriber1->safe_psql('postgres', "SELECT * FROM tab_conc"); +is( $result, qq(1 +2), + 'Ensure that the data from the tab_conc table is synchronized to the subscriber after the subscription is refreshed' +); + +$result = + $node_subscriber1->safe_psql('postgres', "SELECT * FROM tab1_conc_child"); +is( $result, qq(1 +2), + 'Ensure that the data from the tab1_conc_child table is synchronized to the subscriber after the subscription is refreshed' +); + +# Perform an insert. +$node_publisher1->safe_psql( + 'postgres', qq( + INSERT INTO tab_conc VALUES (3); + INSERT INTO tab1_conc_child VALUES (3); +)); +$node_publisher1->wait_for_catchup('sub1'); + +# Verify that the insert is replicated to the subscriber. +$result = $node_subscriber1->safe_psql('postgres', "SELECT * FROM tab_conc"); +is( $result, qq(1 +2 +3), + 'Verify that the incremental data for table tab_conc added after table synchronization is replicated to the subscriber' +); + +$result = + $node_subscriber1->safe_psql('postgres', "SELECT * FROM tab1_conc_child"); +is( $result, qq(1 +2 +3), + 'Verify that the incremental data for table tab1_conc_child added after table synchronization is replicated to the subscriber' +); + +$node_publisher1->stop('fast'); +$node_subscriber1->stop('fast'); -- 2.34.1