long-standing data loss bug in initial sync of logical replication
Hi,
It seems there's a long-standing data loss issue related to the initial
sync of tables in the built-in logical replication (publications etc.).
I can reproduce it fairly reliably, but I haven't figured out all the
details yet and I'm a bit out of ideas, so I'm sharing what I know with
the hope someone takes a look and either spots the issue or has some
other insight ...
On the pgsql-bugs, Depesz reported reported [1]/messages/by-id/ZTu8GTDajCkZVjMs@depesz.com cases where tables are
added to a publication but end up missing rows on the subscriber. I
didn't know what might be the issue, but given his experience I decided
to take a do some blind attempts to reproduce the issue.
I'm not going to repeat all the details from the pgsql-bugs thread, but
I ended up writing a script that does randomized stress test tablesync
under concurrent load. Attached are two scripts, where crash-test.sh
does the main work, while run.sh drives the test - executes
crash-test.sh in a loop and generates random parameters for it.
The run.sh generates number of tables, refresh interval (after how many
tables we refresh subscription) and how long to sleep between steps (to
allow pgbench to do more work).
The crash-test.sh then does this:
1) initializes two clusters (expects $PATH to have pg_ctl etc.)
2) configures them for logical replication (wal_level, ...)
3) creates publication and subscription on the nodes
4) creates some a bunch of tables
5) starts a pgbench that inserts data into the tables
6) adds the tables to the publication one by one, occasionally
refreshing the subscription
7) waits for tablesync of all the tables to complete (so that the
tables get into the 'r' state, thus replicating normally)
8) stops the pgbench
9) waits for the subscriber to fully catch up
10) compares that the tables on publisher/subscriber nodes
To run this, just make sure PATH includes pg, and do e.g.
./run.sh 10
which does 10 runs of crash-test.sh with random parameters. Each run can
take a couple minutes, depending on the parameters, hardware etc.
Obviously, we expect the tables to match on the two nodes, but the
script regularly detects cases where the subscriber is missing some of
the rows. The script dumps those tables, and the rows contain timestamps
and LSNs to allow "rough correlation" (imperfect thanks to concurrency).
Depesz reported "gaps" in the data, i.e. missing a chunk of data, but
then following rows seemingly replicated. I did see such cases too, but
most of the time I see a missing chunk of rows at the end (but maybe if
the test continued a bit longer, it'd replicate some rows).
The report talks about replication between pg12->pg14, but I don't think
the cross-version part is necessary - I'm able to reproduce the issue on
individual versions (e.g. 12->12) since 12 (I haven't tried 11, but I'd
be surprised if it wasn't affected too).
The rows include `pg_current_wal_lsn()` to roughly track the LSN where
the row is inserted, and the "gap" of missing rows for each table seems
to match pg_subscription_rel.srsublsn, i.e. the LSN up to which
tablesync copied data, and the table should be replicated as usual.
Another interesting observation is that the issue only happens for "bulk
insert" transactions, i.e.
BEGIN;
... INSERT into all tables ...
COMMIT;
but not when each insert is a separate transaction. A bit strange.
After quite a bit of debugging, I came to the conclusion this happens
because we fail to invalidate caches on the publisher, so it does not
realize it should start sending rows for that table.
In particular, we initially build RelationSyncEntry when the table is
not yet included in the publication, so we end up with pubinsert=false,
thus not replicating the inserts. Which makes sense, but we then seems
to fail to invalidate the entry after it's added to the publication.
The other problem is that even if we happen to invalidate the entry, we
call GetRelationPublications(). But even if it happens long after the
table gets added to the publication (both in time and LSN terms), it
still returns NIL as if the table had no publications. And we end up
with pubinsert=false, skipping the inserts again.
Attached are three patches against master. 0001 adds some debug logging
that I found useful when investigating the issue. 0002 illustrates the
issue by forcefully invalidating the entry for each change, and
implementing a non-syscache variant of the GetRelationPublication().
This makes the code unbearably slow, but with both changes in place I
can no longer reproduce the issue. Undoing either of the two changes
makes it reproducible again. (I'll talk about 0003 later.)
I suppose timing matters, so it's possible it gets "fixed" simply
because of that, but I find that unlikely given the number of runs I did
without observing any failure.
Overall, this looks, walks and quacks like a cache invalidation issue,
likely a missing invalidation somewhere in the ALTER PUBLICATION code.
If we fail to invalidate the pg_publication_rel syscache somewhere, that
obviously explain why GetRelationPublications() returns stale data, but
it would also explain why the RelationSyncEntry is not invalidated, as
that happens in a syscache callback.
But I tried to do various crazy things in the ALTER PUBLICATION code,
and none of that worked, so I'm a bit confused/lost.
However, while randomly poking at different things, I realized that if I
change the lock obtained on the relation in OpenTableList() from
ShareUpdateExclusiveLock to ShareRowExclusiveLock, the issue goes away.
I don't know why it works, and I don't even recall what exactly led me
to the idea of changing it.
This is what 0003 does - it reverts 0002 and changes the lock level.
AFAIK the logical decoding code doesn't actually acquire locks on the
decoded tables, so why would this change matter? The only place that
does lock the relation is the tablesync, which gets RowExclusiveLock on
it. And it's interesting that RowExclusiveLock does not conflict with
ShareUpdateExclusiveLock, but does with ShareRowExclusiveLock. But why
would this even matter, when the tablesync can only touch the table
after it gets added to the publication?
regards
[1]: /messages/by-id/ZTu8GTDajCkZVjMs@depesz.com
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Attachments:
0001-debug-logging.patchtext/x-patch; charset=UTF-8; name=0001-debug-logging.patchDownload
From 4253c364838daf26c056c56d693bc00b1e3e8f73 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Thu, 16 Nov 2023 17:46:14 +0100
Subject: [PATCH 1/3] debug logging
---
src/backend/catalog/pg_subscription.c | 8 ++++++
src/backend/replication/logical/worker.c | 11 ++++++++
src/backend/replication/pgoutput/pgoutput.c | 29 ++++++++++++++++++++-
3 files changed, 47 insertions(+), 1 deletion(-)
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index d6a978f1362..0a2a644c293 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -238,6 +238,8 @@ AddSubscriptionRelState(Oid subid, Oid relid, char state,
bool nulls[Natts_pg_subscription_rel];
Datum values[Natts_pg_subscription_rel];
+ elog(LOG, "AddSubscriptionRelState relid %d state %c LSN %X/%X", relid, state, LSN_FORMAT_ARGS(sublsn));
+
LockSharedObject(SubscriptionRelationId, subid, 0, AccessShareLock);
rel = table_open(SubscriptionRelRelationId, RowExclusiveLock);
@@ -285,6 +287,8 @@ UpdateSubscriptionRelState(Oid subid, Oid relid, char state,
Datum values[Natts_pg_subscription_rel];
bool replaces[Natts_pg_subscription_rel];
+ elog(LOG, "UpdateSubscriptionRelState relid %d state %c LSN %X/%X", relid, state, LSN_FORMAT_ARGS(sublsn));
+
LockSharedObject(SubscriptionRelationId, subid, 0, AccessShareLock);
rel = table_open(SubscriptionRelRelationId, RowExclusiveLock);
@@ -369,6 +373,8 @@ GetSubscriptionRelState(Oid subid, Oid relid, XLogRecPtr *sublsn)
table_close(rel, AccessShareLock);
+ elog(LOG, "GetSubscriptionRelState relid %d state %c LSN %X/%X", relid, substate, LSN_FORMAT_ARGS(*sublsn));
+
return substate;
}
@@ -531,6 +537,8 @@ GetSubscriptionRelations(Oid subid, bool not_ready)
else
relstate->lsn = DatumGetLSN(d);
+ elog(LOG, "GetSubscriptionRelations relid %d state %c LSN %X/%X", relstate->relid, relstate->state, LSN_FORMAT_ARGS(relstate->lsn));
+
res = lappend(res, relstate);
}
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 52a9f136ab9..42e5423172b 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -503,6 +503,10 @@ should_apply_changes_for_rel(LogicalRepRelMapEntry *rel)
return rel->state == SUBREL_STATE_READY;
case WORKERTYPE_APPLY:
+ elog(LOG, "should_apply_changes_for_rel relid %d return %d", RelationGetRelid(rel->localrel), (rel->state == SUBREL_STATE_READY ||
+ (rel->state == SUBREL_STATE_SYNCDONE &&
+ rel->statelsn <= remote_final_lsn)));
+
return (rel->state == SUBREL_STATE_READY ||
(rel->state == SUBREL_STATE_SYNCDONE &&
rel->statelsn <= remote_final_lsn));
@@ -2398,20 +2402,27 @@ apply_handle_insert(StringInfo s)
MemoryContext oldctx;
bool run_as_owner;
+ elog(LOG, "apply_handle_insert");
+
/*
* Quick return if we are skipping data modification changes or handling
* streamed transactions.
*/
if (is_skipping_changes() ||
handle_streamed_transaction(LOGICAL_REP_MSG_INSERT, s))
+ {
+ elog(LOG, "apply_handle_insert / skipping changes or streaming");
return;
+ }
begin_replication_step();
relid = logicalrep_read_insert(s, &newtup);
rel = logicalrep_rel_open(relid, RowExclusiveLock);
+ elog(LOG, "apply_handle_insert relid %d state %c lsn %X/%X final %X/%X", RelationGetRelid(rel->localrel), rel->state, LSN_FORMAT_ARGS(rel->statelsn), LSN_FORMAT_ARGS(remote_final_lsn));
if (!should_apply_changes_for_rel(rel))
{
+ elog(LOG, "apply_handle_insert relid %d skipping", RelationGetRelid(rel->localrel));
/*
* The relation can't become interesting in the middle of the
* transaction so it's safe to unlock it.
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index e8add5ee5d9..09f53bea0a0 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1409,9 +1409,12 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
ReorderBufferChangeType action = change->action;
TupleTableSlot *old_slot = NULL;
TupleTableSlot *new_slot = NULL;
-
+elog(LOG, "pgoutput_change relid %d LSN %X/%X", RelationGetRelid(relation), LSN_FORMAT_ARGS(change->lsn));
if (!is_publishable_relation(relation))
+ {
+ elog(LOG, "pgoutput_change relid %d LSN %X/%X not publishable", RelationGetRelid(relation), LSN_FORMAT_ARGS(change->lsn));
return;
+ }
/*
* Remember the xid for the change in streaming mode. We need to send xid
@@ -1429,7 +1432,10 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
{
case REORDER_BUFFER_CHANGE_INSERT:
if (!relentry->pubactions.pubinsert)
+ {
+ elog(LOG, "pgoutput_change relid %d LSN %X/%X pubinsert=false", RelationGetRelid(relation), LSN_FORMAT_ARGS(change->lsn));
return;
+ }
break;
case REORDER_BUFFER_CHANGE_UPDATE:
if (!relentry->pubactions.pubupdate)
@@ -1502,7 +1508,10 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
* of the row filter for old and new tuple.
*/
if (!pgoutput_row_filter(targetrel, old_slot, &new_slot, relentry, &action))
+ {
+ elog(LOG, "pgoutput_change relid %d LSN %X/%X pgoutput_row_filter", RelationGetRelid(relation), LSN_FORMAT_ARGS(change->lsn));
goto cleanup;
+ }
/*
* Send BEGIN if we haven't yet.
@@ -1522,10 +1531,13 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
OutputPluginPrepareWrite(ctx, true);
+ elog(LOG, "pgoutput_change relid %d LSN %X/%X sending data", RelationGetRelid(relation), LSN_FORMAT_ARGS(change->lsn));
+
/* Send the data */
switch (action)
{
case REORDER_BUFFER_CHANGE_INSERT:
+ elog(LOG, "pgoutput_change relid %d LSN %X/%X write insert message", RelationGetRelid(relation), LSN_FORMAT_ARGS(change->lsn));
logicalrep_write_insert(ctx->out, xid, targetrel, new_slot,
data->binary, relentry->columns);
break;
@@ -1974,6 +1986,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
/* initialize entry, if it's new */
if (!found)
{
+ elog(LOG, "get_rel_sync_entry relation %d not found", RelationGetRelid(relation));
entry->replicate_valid = false;
entry->schema_sent = false;
entry->streamed_txns = NIL;
@@ -1988,6 +2001,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
entry->attrmap = NULL;
}
+ elog(LOG, "get_rel_sync_entry relation %d replicate_valid %d", RelationGetRelid(relation), entry->replicate_valid);
+
/* Validate the entry */
if (!entry->replicate_valid)
{
@@ -2007,6 +2022,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
char relkind = get_rel_relkind(relid);
List *rel_publications = NIL;
+ elog(LOG, "GetRelationPublications relation %d publications %p %d", relid, pubids, list_length(pubids));
+
/* Reload publications if needed before use. */
if (!publications_valid)
{
@@ -2021,6 +2038,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
publications_valid = true;
}
+ elog(LOG, "get_rel_sync_entry relation %d publications %d", RelationGetRelid(relation), list_length(data->publications));
+
/*
* Reset schema_sent status as the relation definition may have
* changed. Also reset pubactions to empty in case rel was dropped
@@ -2097,6 +2116,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
}
}
+ elog(LOG, "get_rel_sync_entry relation %d publish %d (A)", RelationGetRelid(relation), publish);
+
if (!publish)
{
bool ancestor_published = false;
@@ -2128,12 +2149,16 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
}
}
+ elog(LOG, "get_rel_sync_entry relation %d relation pubs %d subscription pub %d", RelationGetRelid(relation), list_length(pubids), pub->oid);
+
if (list_member_oid(pubids, pub->oid) ||
list_member_oid(schemaPubids, pub->oid) ||
ancestor_published)
publish = true;
}
+ elog(LOG, "get_rel_sync_entry relation %d publish %d (B)", RelationGetRelid(relation), publish);
+
/*
* If the relation is to be published, determine actions to
* publish, and list of columns, if appropriate.
@@ -2210,6 +2235,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
entry->replicate_valid = true;
}
+ elog(LOG, "get_rel_sync_entry relation %d pubinsert %d", RelationGetRelid(relation), entry->pubactions.pubinsert);
+
return entry;
}
--
2.41.0
0002-experimental-fix.patchtext/x-patch; charset=UTF-8; name=0002-experimental-fix.patchDownload
From bc400e7c9c8ad2ac4a0c431dfab9ec2f78bd5870 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Thu, 16 Nov 2023 19:57:33 +0100
Subject: [PATCH 2/3] experimental fix
---
src/backend/replication/pgoutput/pgoutput.c | 54 ++++++++++++++++++++-
1 file changed, 53 insertions(+), 1 deletion(-)
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 09f53bea0a0..adcd5688045 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -12,6 +12,8 @@
*/
#include "postgres.h"
+#include "access/genam.h"
+#include "access/table.h"
#include "access/tupconvert.h"
#include "catalog/partition.h"
#include "catalog/pg_publication.h"
@@ -29,6 +31,7 @@
#include "replication/origin.h"
#include "replication/pgoutput.h"
#include "utils/builtins.h"
+#include "utils/fmgroids.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
@@ -1958,6 +1961,51 @@ set_schema_sent_in_streamed_txn(RelationSyncEntry *entry, TransactionId xid)
MemoryContextSwitchTo(oldctx);
}
+static List*
+GetRelationPublicationsRaw(Oid relid)
+{
+ List *result = NIL;
+ SysScanDesc scandesc;
+ Relation relation;
+ ScanKeyData key[1];
+ HeapTuple tup;
+
+ elog(LOG, "GetRelationPublicationsRaw relid %d / start", relid);
+
+ relation = table_open(PublicationRelRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_publication_rel_prrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+
+ scandesc = systable_beginscan(relation,
+ PublicationRelPrrelidPrpubidIndexId,
+ true,
+ NULL,
+ 1,
+ key);
+
+ while (HeapTupleIsValid(tup = systable_getnext(scandesc)))
+ {
+ Form_pg_publication_rel form;
+
+ form = (Form_pg_publication_rel) GETSTRUCT(tup);
+
+ elog(LOG, "GetRelationPublicationsRaw relid %d / tuple pub %d", relid, form->prpubid);
+
+ result = lappend_oid(result, form->prpubid);
+ }
+
+ systable_endscan(scandesc);
+
+ table_close(relation, AccessShareLock);
+
+ elog(LOG, "GetRelationPublicationsRaw relid %d / end", relid);
+
+ return result;
+}
+
/*
* Find or create entry in the relation schema cache.
*
@@ -2003,11 +2051,15 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
elog(LOG, "get_rel_sync_entry relation %d replicate_valid %d", RelationGetRelid(relation), entry->replicate_valid);
+ /* force refresh of the entry for each change */
+ entry->replicate_valid = false;
+
/* Validate the entry */
if (!entry->replicate_valid)
{
Oid schemaId = get_rel_namespace(relid);
- List *pubids = GetRelationPublications(relid);
+ List *pubids = GetRelationPublicationsRaw(relid);
+ // List *pubids = GetRelationPublications(relid);
/*
* We don't acquire a lock on the namespace system table as we build
--
2.41.0
0003-alternative-experimental-fix-lock.patchtext/x-patch; charset=UTF-8; name=0003-alternative-experimental-fix-lock.patchDownload
From a95b5fe1d8243e16fdf5e2db5a773495f4641829 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Fri, 17 Nov 2023 11:20:50 +0100
Subject: [PATCH 3/3] alternative experimental fix - lock
---
src/backend/commands/publicationcmds.c | 2 +-
src/backend/replication/pgoutput/pgoutput.c | 54 +--------------------
2 files changed, 2 insertions(+), 54 deletions(-)
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index f4ba572697a..7a4c315183a 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1575,7 +1575,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();
- rel = table_openrv(t->relation, ShareUpdateExclusiveLock);
+ rel = table_openrv(t->relation, ShareRowExclusiveLock);
myrelid = RelationGetRelid(rel);
/*
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index adcd5688045..09f53bea0a0 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -12,8 +12,6 @@
*/
#include "postgres.h"
-#include "access/genam.h"
-#include "access/table.h"
#include "access/tupconvert.h"
#include "catalog/partition.h"
#include "catalog/pg_publication.h"
@@ -31,7 +29,6 @@
#include "replication/origin.h"
#include "replication/pgoutput.h"
#include "utils/builtins.h"
-#include "utils/fmgroids.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
@@ -1961,51 +1958,6 @@ set_schema_sent_in_streamed_txn(RelationSyncEntry *entry, TransactionId xid)
MemoryContextSwitchTo(oldctx);
}
-static List*
-GetRelationPublicationsRaw(Oid relid)
-{
- List *result = NIL;
- SysScanDesc scandesc;
- Relation relation;
- ScanKeyData key[1];
- HeapTuple tup;
-
- elog(LOG, "GetRelationPublicationsRaw relid %d / start", relid);
-
- relation = table_open(PublicationRelRelationId, AccessShareLock);
-
- ScanKeyInit(&key[0],
- Anum_pg_publication_rel_prrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(relid));
-
- scandesc = systable_beginscan(relation,
- PublicationRelPrrelidPrpubidIndexId,
- true,
- NULL,
- 1,
- key);
-
- while (HeapTupleIsValid(tup = systable_getnext(scandesc)))
- {
- Form_pg_publication_rel form;
-
- form = (Form_pg_publication_rel) GETSTRUCT(tup);
-
- elog(LOG, "GetRelationPublicationsRaw relid %d / tuple pub %d", relid, form->prpubid);
-
- result = lappend_oid(result, form->prpubid);
- }
-
- systable_endscan(scandesc);
-
- table_close(relation, AccessShareLock);
-
- elog(LOG, "GetRelationPublicationsRaw relid %d / end", relid);
-
- return result;
-}
-
/*
* Find or create entry in the relation schema cache.
*
@@ -2051,15 +2003,11 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
elog(LOG, "get_rel_sync_entry relation %d replicate_valid %d", RelationGetRelid(relation), entry->replicate_valid);
- /* force refresh of the entry for each change */
- entry->replicate_valid = false;
-
/* Validate the entry */
if (!entry->replicate_valid)
{
Oid schemaId = get_rel_namespace(relid);
- List *pubids = GetRelationPublicationsRaw(relid);
- // List *pubids = GetRelationPublications(relid);
+ List *pubids = GetRelationPublications(relid);
/*
* We don't acquire a lock on the namespace system table as we build
--
2.41.0
Hi,
On 2023-11-17 15:36:25 +0100, Tomas Vondra wrote:
It seems there's a long-standing data loss issue related to the initial
sync of tables in the built-in logical replication (publications etc.).
:(
Overall, this looks, walks and quacks like a cache invalidation issue,
likely a missing invalidation somewhere in the ALTER PUBLICATION code.
It could also be be that pgoutput doesn't have sufficient invalidation
handling.
One thing that looks bogus on the DDL side is how the invalidation handling
interacts with locking.
For tables etc the invalidation handling works because we hold a lock on the
relation before modifying the catalog and don't release that lock until
transaction end. That part is crucial: We queue shared invalidations at
transaction commit, *after* the transaction is marked as visible, but *before*
locks are released. That guarantees that any backend processing invalidations
will see the new contents. However, if the lock on the modified object is
released before transaction commit, other backends can build and use a cache
entry that hasn't processed invalidations (invaliations are processed when
acquiring locks).
While there is such an object for publications, it seems to be acquired too
late to actually do much good in a number of paths. And not at all in others.
E.g.:
pubform = (Form_pg_publication) GETSTRUCT(tup);
/*
* If the publication doesn't publish changes via the root partitioned
* table, the partition's row filter and column list will be used. So
* disallow using WHERE clause and column lists on partitioned table in
* this case.
*/
if (!pubform->puballtables && publish_via_partition_root_given &&
!publish_via_partition_root)
{
/*
* Lock the publication so nobody else can do anything with it. This
* prevents concurrent alter to add partitioned table(s) with WHERE
* clause(s) and/or column lists which we don't allow when not
* publishing via root.
*/
LockDatabaseObject(PublicationRelationId, pubform->oid, 0,
AccessShareLock);
a) Another session could have modified the publication and made puballtables out-of-date
b) The LockDatabaseObject() uses AccessShareLock, so others can get past this
point as well
b) seems like a copy-paste bug or such?
I don't see any locking of the publication around RemovePublicationRelById(),
for example.
I might just be misunderstanding things the way publication locking is
intended to work.
However, while randomly poking at different things, I realized that if I
change the lock obtained on the relation in OpenTableList() from
ShareUpdateExclusiveLock to ShareRowExclusiveLock, the issue goes away.
That's odd. There's cases where changing the lock level can cause invalidation
processing to happen because there is no pre-existing lock for the "new" lock
level, but there was for the old. But OpenTableList() is used when altering
the publications, so I don't see how that connects.
Greetings,
Andres Freund
Hi,
On 2023-11-17 17:54:43 -0800, Andres Freund wrote:
On 2023-11-17 15:36:25 +0100, Tomas Vondra wrote:
Overall, this looks, walks and quacks like a cache invalidation issue,
likely a missing invalidation somewhere in the ALTER PUBLICATION code.
I can confirm that something is broken with invalidation handling.
To test this I just used pg_recvlogical to stdout. It's just interesting
whether something arrives, that's easy to discern even with binary output.
CREATE PUBLICATION pb;
src/bin/pg_basebackup/pg_recvlogical --plugin=pgoutput --start --slot test -d postgres -o proto_version=4 -o publication_names=pb -o messages=true -f -
S1: CREATE TABLE d(data text not null);
S1: INSERT INTO d VALUES('d1');
S2: BEGIN; INSERT INTO d VALUES('d2');
S1: ALTER PUBLICATION pb ADD TABLE d;
S2: COMMIT
S2: INSERT INTO d VALUES('d3');
S1: INSERT INTO d VALUES('d4');
RL: <nothing>
Without the 'd2' insert in an in-progress transaction, pgoutput *does* react
to the ALTER PUBLICATION.
I think the problem here is insufficient locking. The ALTER PUBLICATION pb ADD
TABLE d basically modifies the catalog state of 'd', without a lock preventing
other sessions from having a valid cache entry that they could continue to
use. Due to this, decoding S2's transactions that started before S2's commit,
will populate the cache entry with the state as of the time of S1's last
action, i.e. no need to output the change.
The reason this can happen is because OpenTableList() uses
ShareUpdateExclusiveLock. That allows the ALTER PUBLICATION to happen while
there's an ongoing INSERT.
I think this isn't just a logical decoding issue. S2's cache state just after
the ALTER PUBLICATION is going to be wrong - the table is already locked,
therefore further operations on the table don't trigger cache invalidation
processing - but the catalog state *has* changed. It's a bigger problem for
logical decoding though, as it's a bit more lazy about invalidation processing
than normal transactions, allowing the problem to persist for longer.
I guess it's not really feasible to just increase the lock level here though
:(. The use of ShareUpdateExclusiveLock isn't new, and suddenly using AEL
would perhaps lead to new deadlocks and such? But it also seems quite wrong.
We could brute force this in the logical decoding infrastructure, by
distributing invalidations from catalog modifying transactions to all
concurrent in-progress transactions (like already done for historic catalog
snapshot, c.f. SnapBuildDistributeNewCatalogSnapshot()). But I think that'd
be a fairly significant increase in overhead.
Greetings,
Andres Freund
On 11/18/23 02:54, Andres Freund wrote:
Hi,
On 2023-11-17 15:36:25 +0100, Tomas Vondra wrote:
It seems there's a long-standing data loss issue related to the initial
sync of tables in the built-in logical replication (publications etc.).:(
Yeah :-(
Overall, this looks, walks and quacks like a cache invalidation issue,
likely a missing invalidation somewhere in the ALTER PUBLICATION code.It could also be be that pgoutput doesn't have sufficient invalidation
handling.
I'm not sure about the details, but it can't be just about pgoutput
failing to react to some syscache invalidation. As described, just
resetting the RelationSyncEntry doesn't fix the issue - it's the
syscache that's not invalidated, IMO. But maybe that's what you mean.
One thing that looks bogus on the DDL side is how the invalidation handling
interacts with locking.For tables etc the invalidation handling works because we hold a lock on the
relation before modifying the catalog and don't release that lock until
transaction end. That part is crucial: We queue shared invalidations at
transaction commit, *after* the transaction is marked as visible, but *before*
locks are released. That guarantees that any backend processing invalidations
will see the new contents. However, if the lock on the modified object is
released before transaction commit, other backends can build and use a cache
entry that hasn't processed invalidations (invaliations are processed when
acquiring locks).
Right.
While there is such an object for publications, it seems to be acquired too
late to actually do much good in a number of paths. And not at all in others.E.g.:
pubform = (Form_pg_publication) GETSTRUCT(tup);
/*
* If the publication doesn't publish changes via the root partitioned
* table, the partition's row filter and column list will be used. So
* disallow using WHERE clause and column lists on partitioned table in
* this case.
*/
if (!pubform->puballtables && publish_via_partition_root_given &&
!publish_via_partition_root)
{
/*
* Lock the publication so nobody else can do anything with it. This
* prevents concurrent alter to add partitioned table(s) with WHERE
* clause(s) and/or column lists which we don't allow when not
* publishing via root.
*/
LockDatabaseObject(PublicationRelationId, pubform->oid, 0,
AccessShareLock);a) Another session could have modified the publication and made puballtables out-of-date
b) The LockDatabaseObject() uses AccessShareLock, so others can get past this
point as wellb) seems like a copy-paste bug or such?
I don't see any locking of the publication around RemovePublicationRelById(),
for example.I might just be misunderstanding things the way publication locking is
intended to work.
I've been asking similar questions while investigating this, but the
interactions with logical decoding (which kinda happens concurrently in
terms of WAL, but not concurrently in terms of time), historical
snapshots etc. make my head spin.
However, while randomly poking at different things, I realized that if I
change the lock obtained on the relation in OpenTableList() from
ShareUpdateExclusiveLock to ShareRowExclusiveLock, the issue goes away.That's odd. There's cases where changing the lock level can cause invalidation
processing to happen because there is no pre-existing lock for the "new" lock
level, but there was for the old. But OpenTableList() is used when altering
the publications, so I don't see how that connects.
Yeah, I had the idea that maybe the transaction already holds the lock
on the table, and changing this to ShareRowExclusiveLock makes it
different, possibly triggering a new invalidation or something. But I
did check with gdb, and if I set a breakpoint at OpenTableList, there
are no locks on the table.
But the effect is hard to deny - if I run the test 100 times, with the
SharedUpdateExclusiveLock I get maybe 80 failures. After changing it to
ShareRowExclusiveLock I get 0. Sure, there's some randomness for cases
like this, but this is pretty unlikely.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On 11/18/23 03:54, Andres Freund wrote:
Hi,
On 2023-11-17 17:54:43 -0800, Andres Freund wrote:
On 2023-11-17 15:36:25 +0100, Tomas Vondra wrote:
Overall, this looks, walks and quacks like a cache invalidation issue,
likely a missing invalidation somewhere in the ALTER PUBLICATION code.I can confirm that something is broken with invalidation handling.
To test this I just used pg_recvlogical to stdout. It's just interesting
whether something arrives, that's easy to discern even with binary output.CREATE PUBLICATION pb;
src/bin/pg_basebackup/pg_recvlogical --plugin=pgoutput --start --slot test -d postgres -o proto_version=4 -o publication_names=pb -o messages=true -f -S1: CREATE TABLE d(data text not null);
S1: INSERT INTO d VALUES('d1');
S2: BEGIN; INSERT INTO d VALUES('d2');
S1: ALTER PUBLICATION pb ADD TABLE d;
S2: COMMIT
S2: INSERT INTO d VALUES('d3');
S1: INSERT INTO d VALUES('d4');
RL: <nothing>Without the 'd2' insert in an in-progress transaction, pgoutput *does* react
to the ALTER PUBLICATION.I think the problem here is insufficient locking. The ALTER PUBLICATION pb ADD
TABLE d basically modifies the catalog state of 'd', without a lock preventing
other sessions from having a valid cache entry that they could continue to
use. Due to this, decoding S2's transactions that started before S2's commit,
will populate the cache entry with the state as of the time of S1's last
action, i.e. no need to output the change.The reason this can happen is because OpenTableList() uses
ShareUpdateExclusiveLock. That allows the ALTER PUBLICATION to happen while
there's an ongoing INSERT.
I guess this would also explain why changing the lock mode from
ShareUpdateExclusiveLock to ShareRowExclusiveLock changes the behavior.
INSERT acquires RowExclusiveLock, which doesn't conflict only with the
latter.
I think this isn't just a logical decoding issue. S2's cache state just after
the ALTER PUBLICATION is going to be wrong - the table is already locked,
therefore further operations on the table don't trigger cache invalidation
processing - but the catalog state *has* changed. It's a bigger problem for
logical decoding though, as it's a bit more lazy about invalidation processing
than normal transactions, allowing the problem to persist for longer.
Yeah. I'm wondering if there's some other operation acquiring a lock
weaker than RowExclusiveLock that might be affected by this. Because
then we'd need to get an even stronger lock ...
I guess it's not really feasible to just increase the lock level here though
:(. The use of ShareUpdateExclusiveLock isn't new, and suddenly using AEL
would perhaps lead to new deadlocks and such? But it also seems quite wrong.
If this really is about the lock being too weak, then I don't see why
would it be wrong? If it's required for correctness, it's not really
wrong, IMO. Sure, stronger locks are not great ...
I'm not sure about the risk of deadlocks. If you do
ALTER PUBLICATION ... ADD TABLE
it's not holding many other locks. It essentially gets a lock just a
lock on pg_publication catalog, and then the publication row. That's it.
If we increase the locks from ShareUpdateExclusive to ShareRowExclusive,
we're making it conflict with RowExclusive. Which is just DML, and I
think we need to do that.
So maybe that's fine? For me, a detected deadlock is better than
silently missing some of the data.
We could brute force this in the logical decoding infrastructure, by
distributing invalidations from catalog modifying transactions to all
concurrent in-progress transactions (like already done for historic catalog
snapshot, c.f. SnapBuildDistributeNewCatalogSnapshot()). But I think that'd
be a fairly significant increase in overhead.
I have no idea what the overhead would be - perhaps not too bad,
considering catalog changes are not too common (I'm sure there are
extreme cases). And maybe we could even restrict this only to
"interesting" catalogs, or something like that? (However I hate those
weird differences in behavior, it can easily lead to bugs.)
But it feels more like a band-aid than actually fixing the issue.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Hi,
On 2023-11-18 11:56:47 +0100, Tomas Vondra wrote:
I guess it's not really feasible to just increase the lock level here though
:(. The use of ShareUpdateExclusiveLock isn't new, and suddenly using AEL
would perhaps lead to new deadlocks and such? But it also seems quite wrong.If this really is about the lock being too weak, then I don't see why
would it be wrong?
Sorry, that was badly formulated. The wrong bit is the use of
ShareUpdateExclusiveLock.
If it's required for correctness, it's not really wrong, IMO. Sure, stronger
locks are not great ...I'm not sure about the risk of deadlocks. If you do
ALTER PUBLICATION ... ADD TABLE
it's not holding many other locks. It essentially gets a lock just a
lock on pg_publication catalog, and then the publication row. That's it.If we increase the locks from ShareUpdateExclusive to ShareRowExclusive,
we're making it conflict with RowExclusive. Which is just DML, and I
think we need to do that.
From what I can tell it needs to to be an AccessExlusiveLock. Completely
independent of logical decoding. The way the cache stays coherent is catalog
modifications conflicting with anything that builds cache entries. We have a
few cases where we do use lower level locks, but for those we have explicit
analysis for why that's ok (see e.g. reloptions.c) or we block until nobody
could have an old view of the catalog (various CONCURRENTLY) operations.
So maybe that's fine? For me, a detected deadlock is better than
silently missing some of the data.
That certainly is true.
We could brute force this in the logical decoding infrastructure, by
distributing invalidations from catalog modifying transactions to all
concurrent in-progress transactions (like already done for historic catalog
snapshot, c.f. SnapBuildDistributeNewCatalogSnapshot()). But I think that'd
be a fairly significant increase in overhead.I have no idea what the overhead would be - perhaps not too bad,
considering catalog changes are not too common (I'm sure there are
extreme cases). And maybe we could even restrict this only to
"interesting" catalogs, or something like that? (However I hate those
weird differences in behavior, it can easily lead to bugs.)But it feels more like a band-aid than actually fixing the issue.
Agreed.
Greetings,
Andres Freund
On 11/18/23 19:12, Andres Freund wrote:
Hi,
On 2023-11-18 11:56:47 +0100, Tomas Vondra wrote:
I guess it's not really feasible to just increase the lock level here though
:(. The use of ShareUpdateExclusiveLock isn't new, and suddenly using AEL
would perhaps lead to new deadlocks and such? But it also seems quite wrong.If this really is about the lock being too weak, then I don't see why
would it be wrong?Sorry, that was badly formulated. The wrong bit is the use of
ShareUpdateExclusiveLock.
Ah, you meant the current lock mode seems wrong, not that changing the
locks seems wrong. Yeah, true.
If it's required for correctness, it's not really wrong, IMO. Sure, stronger
locks are not great ...I'm not sure about the risk of deadlocks. If you do
ALTER PUBLICATION ... ADD TABLE
it's not holding many other locks. It essentially gets a lock just a
lock on pg_publication catalog, and then the publication row. That's it.If we increase the locks from ShareUpdateExclusive to ShareRowExclusive,
we're making it conflict with RowExclusive. Which is just DML, and I
think we need to do that.From what I can tell it needs to to be an AccessExlusiveLock. Completely
independent of logical decoding. The way the cache stays coherent is catalog
modifications conflicting with anything that builds cache entries. We have a
few cases where we do use lower level locks, but for those we have explicit
analysis for why that's ok (see e.g. reloptions.c) or we block until nobody
could have an old view of the catalog (various CONCURRENTLY) operations.
Yeah, I got too focused on the issue I triggered, which seems to be
fixed by using SRE (still don't understand why ...). But you're probably
right there may be other cases where SRE would not be sufficient, I
certainly can't prove it'd be safe.
So maybe that's fine? For me, a detected deadlock is better than
silently missing some of the data.That certainly is true.
We could brute force this in the logical decoding infrastructure, by
distributing invalidations from catalog modifying transactions to all
concurrent in-progress transactions (like already done for historic catalog
snapshot, c.f. SnapBuildDistributeNewCatalogSnapshot()). But I think that'd
be a fairly significant increase in overhead.I have no idea what the overhead would be - perhaps not too bad,
considering catalog changes are not too common (I'm sure there are
extreme cases). And maybe we could even restrict this only to
"interesting" catalogs, or something like that? (However I hate those
weird differences in behavior, it can easily lead to bugs.)But it feels more like a band-aid than actually fixing the issue.
Agreed.
... and it would no not fix the other places outside logical decoding.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Hi,
On 2023-11-18 21:45:35 +0100, Tomas Vondra wrote:
On 11/18/23 19:12, Andres Freund wrote:
If we increase the locks from ShareUpdateExclusive to ShareRowExclusive,
we're making it conflict with RowExclusive. Which is just DML, and I
think we need to do that.From what I can tell it needs to to be an AccessExlusiveLock. Completely
independent of logical decoding. The way the cache stays coherent is catalog
modifications conflicting with anything that builds cache entries. We have a
few cases where we do use lower level locks, but for those we have explicit
analysis for why that's ok (see e.g. reloptions.c) or we block until nobody
could have an old view of the catalog (various CONCURRENTLY) operations.Yeah, I got too focused on the issue I triggered, which seems to be
fixed by using SRE (still don't understand why ...). But you're probably
right there may be other cases where SRE would not be sufficient, I
certainly can't prove it'd be safe.
I think it makes sense here: SRE prevents the problematic "scheduling" in your
test - with SRE no DML started before ALTER PUB ... ADD can commit after.
I'm not sure there are any cases where using SRE instead of AE would cause
problems for logical decoding, but it seems very hard to prove. I'd be very
surprised if just using SRE would not lead to corrupted cache contents in some
situations. The cases where a lower lock level is ok are ones where we just
don't care that the cache is coherent in that moment.
In a way, the logical decoding cache-invalidation situation is a lot more
atomic than the "normal" situation. During normal operation locking is
strictly required to prevent incoherent states when building a cache entry
after a transaction committed, but before the sinval entries have been
queued. But in the logical decoding case that window doesn't exist.
Greetings,
Andres Freund
On 11/18/23 22:05, Andres Freund wrote:
Hi,
On 2023-11-18 21:45:35 +0100, Tomas Vondra wrote:
On 11/18/23 19:12, Andres Freund wrote:
If we increase the locks from ShareUpdateExclusive to ShareRowExclusive,
we're making it conflict with RowExclusive. Which is just DML, and I
think we need to do that.From what I can tell it needs to to be an AccessExlusiveLock. Completely
independent of logical decoding. The way the cache stays coherent is catalog
modifications conflicting with anything that builds cache entries. We have a
few cases where we do use lower level locks, but for those we have explicit
analysis for why that's ok (see e.g. reloptions.c) or we block until nobody
could have an old view of the catalog (various CONCURRENTLY) operations.Yeah, I got too focused on the issue I triggered, which seems to be
fixed by using SRE (still don't understand why ...). But you're probably
right there may be other cases where SRE would not be sufficient, I
certainly can't prove it'd be safe.I think it makes sense here: SRE prevents the problematic "scheduling" in your
test - with SRE no DML started before ALTER PUB ... ADD can commit after.
If understand correctly, with the current code (which only gets
ShareUpdateExclusiveLock), we may end up in a situation like this
(sessions A and B):
A: starts "ALTER PUBLICATION p ADD TABLE t" and gets the SUE lock
A: writes the invalidation message(s) into WAL
B: inserts into table "t"
B: commit
A: commit
With the stronger SRE lock, the commits would have to happen in the
opposite order, because as you say it prevents the bad ordering.
But why would this matter for logical decoding? We accumulate the the
invalidations and execute them at transaction commit, or did I miss
something?
So what I think should happen is we get to apply B first, which won't
see the table as part of the publication. It might even build the cache
entries (syscache+relsync), reflecting that. But then we get to execute
A, along with all the invalidations, and that should invalidate them.
I'm clearly missing something, because the SRE does change the behavior,
so there has to be a difference (and by my reasoning it shouldn't be).
Or maybe it's the other way around? Won't B get the invalidation, but
use a historical snapshot that doesn't yet see the table in publication?
I'm not sure there are any cases where using SRE instead of AE would cause
problems for logical decoding, but it seems very hard to prove. I'd be very
surprised if just using SRE would not lead to corrupted cache contents in some
situations. The cases where a lower lock level is ok are ones where we just
don't care that the cache is coherent in that moment.
Are you saying it might break cases that are not corrupted now? How
could obtaining a stronger lock have such effect?
In a way, the logical decoding cache-invalidation situation is a lot more
atomic than the "normal" situation. During normal operation locking is
strictly required to prevent incoherent states when building a cache entry
after a transaction committed, but before the sinval entries have been
queued. But in the logical decoding case that window doesn't exist.
Because we apply the invalidations at commit time, so it happens as a
single operation that can't interleave with other sessions?
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On 2023-11-19 02:15:33 +0100, Tomas Vondra wrote:
On 11/18/23 22:05, Andres Freund wrote:
Hi,
On 2023-11-18 21:45:35 +0100, Tomas Vondra wrote:
On 11/18/23 19:12, Andres Freund wrote:
If we increase the locks from ShareUpdateExclusive to ShareRowExclusive,
we're making it conflict with RowExclusive. Which is just DML, and I
think we need to do that.From what I can tell it needs to to be an AccessExlusiveLock. Completely
independent of logical decoding. The way the cache stays coherent is catalog
modifications conflicting with anything that builds cache entries. We have a
few cases where we do use lower level locks, but for those we have explicit
analysis for why that's ok (see e.g. reloptions.c) or we block until nobody
could have an old view of the catalog (various CONCURRENTLY) operations.Yeah, I got too focused on the issue I triggered, which seems to be
fixed by using SRE (still don't understand why ...). But you're probably
right there may be other cases where SRE would not be sufficient, I
certainly can't prove it'd be safe.I think it makes sense here: SRE prevents the problematic "scheduling" in your
test - with SRE no DML started before ALTER PUB ... ADD can commit after.If understand correctly, with the current code (which only gets
ShareUpdateExclusiveLock), we may end up in a situation like this
(sessions A and B):A: starts "ALTER PUBLICATION p ADD TABLE t" and gets the SUE lock
A: writes the invalidation message(s) into WAL
B: inserts into table "t"
B: commit
A: commit
I don't think this the problematic sequence - at least it's not what I had
reproed in
/messages/by-id/20231118025445.crhaeeuvoe2g5dv6@awork3.anarazel.de
Adding line numbers:
1) S1: CREATE TABLE d(data text not null);
2) S1: INSERT INTO d VALUES('d1');
3) S2: BEGIN; INSERT INTO d VALUES('d2');
4) S1: ALTER PUBLICATION pb ADD TABLE d;
5) S2: COMMIT
6) S2: INSERT INTO d VALUES('d3');
7) S1: INSERT INTO d VALUES('d4');
8) RL: <nothing>
The problem with the sequence is that the insert from 3) is decoded *after* 4)
and that to decode the insert (which happened before the ALTER) the catalog
snapshot and cache state is from *before* the ALTER TABLE. Because the
transaction started in 3) doesn't actually modify any catalogs, no
invalidations are executed after decoding it. The result is that the cache
looks like it did at 3), not like after 4). Undesirable timetravel...
It's worth noting that here the cache state is briefly correct, after 4), it's
just that after 5) it stays the old state.
If 4) instead uses a SRE lock, then S1 will be blocked until S2 commits, and
everything is fine.
I'm not sure there are any cases where using SRE instead of AE would cause
problems for logical decoding, but it seems very hard to prove. I'd be very
surprised if just using SRE would not lead to corrupted cache contents in some
situations. The cases where a lower lock level is ok are ones where we just
don't care that the cache is coherent in that moment.
Are you saying it might break cases that are not corrupted now? How
could obtaining a stronger lock have such effect?
No, I mean that I don't know if using SRE instead of AE would have negative
consequences for logical decoding. I.e. whether, from a logical decoding POV,
it'd suffice to increase the lock level to just SRE instead of AE.
Since I don't see how it'd be correct otherwise, it's kind of a moot question.
In a way, the logical decoding cache-invalidation situation is a lot more
atomic than the "normal" situation. During normal operation locking is
strictly required to prevent incoherent states when building a cache entry
after a transaction committed, but before the sinval entries have been
queued. But in the logical decoding case that window doesn't exist.Because we apply the invalidations at commit time, so it happens as a
single operation that can't interleave with other sessions?
Yea, the situation is much simpler during logical decoding than "originally" -
there's no concurrency.
Greetings,
Andres Freund
Hi,
On 19.11.2023 09:18, Andres Freund wrote:
Yea, the situation is much simpler during logical decoding than "originally" -
there's no concurrency.Greetings,
Andres Freund
We've encountered a similar error on our industrial server.
The case: After adding a table to logical replication, table
initialization proceeds normally, but new data from the publisher's
table does not appear on the subscriber server. After we added the
table, we checked and saw that the data was present on the subscriber
and everything was normal, we discovered the error after some time. I
have attached scripts to the email.
The patch from the first message also solves this problem.
--
Best regards,
Vadim Lakt
Attachments:
On Sun, Nov 19, 2023 at 7:48 AM Andres Freund <andres@anarazel.de> wrote:
On 2023-11-19 02:15:33 +0100, Tomas Vondra wrote:
If understand correctly, with the current code (which only gets
ShareUpdateExclusiveLock), we may end up in a situation like this
(sessions A and B):A: starts "ALTER PUBLICATION p ADD TABLE t" and gets the SUE lock
A: writes the invalidation message(s) into WAL
B: inserts into table "t"
B: commit
A: commitI don't think this the problematic sequence - at least it's not what I had
reproed in
/messages/by-id/20231118025445.crhaeeuvoe2g5dv6@awork3.anarazel.deAdding line numbers:
1) S1: CREATE TABLE d(data text not null);
2) S1: INSERT INTO d VALUES('d1');
3) S2: BEGIN; INSERT INTO d VALUES('d2');
4) S1: ALTER PUBLICATION pb ADD TABLE d;
5) S2: COMMIT
6) S2: INSERT INTO d VALUES('d3');
7) S1: INSERT INTO d VALUES('d4');
8) RL: <nothing>The problem with the sequence is that the insert from 3) is decoded *after* 4)
and that to decode the insert (which happened before the ALTER) the catalog
snapshot and cache state is from *before* the ALTER TABLE. Because the
transaction started in 3) doesn't actually modify any catalogs, no
invalidations are executed after decoding it. The result is that the cache
looks like it did at 3), not like after 4). Undesirable timetravel...It's worth noting that here the cache state is briefly correct, after 4), it's
just that after 5) it stays the old state.If 4) instead uses a SRE lock, then S1 will be blocked until S2 commits, and
everything is fine.
I agree, your analysis looks right to me.
I'm not sure there are any cases where using SRE instead of AE would cause
problems for logical decoding, but it seems very hard to prove. I'd be very
surprised if just using SRE would not lead to corrupted cache contents in some
situations. The cases where a lower lock level is ok are ones where we just
don't care that the cache is coherent in that moment.Are you saying it might break cases that are not corrupted now? How
could obtaining a stronger lock have such effect?No, I mean that I don't know if using SRE instead of AE would have negative
consequences for logical decoding. I.e. whether, from a logical decoding POV,
it'd suffice to increase the lock level to just SRE instead of AE.Since I don't see how it'd be correct otherwise, it's kind of a moot question.
We lost track of this thread and the bug is still open. IIUC, the
conclusion is to use SRE in OpenTableList() to fix the reported issue.
Andres, Tomas, please let me know if my understanding is wrong,
otherwise, let's proceed and fix this issue.
--
With Regards,
Amit Kapila.
On 6/24/24 12:54, Amit Kapila wrote:
...
I'm not sure there are any cases where using SRE instead of AE would cause
problems for logical decoding, but it seems very hard to prove. I'd be very
surprised if just using SRE would not lead to corrupted cache contents in some
situations. The cases where a lower lock level is ok are ones where we just
don't care that the cache is coherent in that moment.Are you saying it might break cases that are not corrupted now? How
could obtaining a stronger lock have such effect?No, I mean that I don't know if using SRE instead of AE would have negative
consequences for logical decoding. I.e. whether, from a logical decoding POV,
it'd suffice to increase the lock level to just SRE instead of AE.Since I don't see how it'd be correct otherwise, it's kind of a moot question.
We lost track of this thread and the bug is still open. IIUC, the
conclusion is to use SRE in OpenTableList() to fix the reported issue.
Andres, Tomas, please let me know if my understanding is wrong,
otherwise, let's proceed and fix this issue.
It's in the commitfest [https://commitfest.postgresql.org/48/4766/] so I
don't think we 'lost track' of it, but it's true we haven't done much
progress recently.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Mon, Jun 24, 2024 at 8:06 PM Tomas Vondra
<tomas.vondra@enterprisedb.com> wrote:
On 6/24/24 12:54, Amit Kapila wrote:
...
I'm not sure there are any cases where using SRE instead of AE would cause
problems for logical decoding, but it seems very hard to prove. I'd be very
surprised if just using SRE would not lead to corrupted cache contents in some
situations. The cases where a lower lock level is ok are ones where we just
don't care that the cache is coherent in that moment.Are you saying it might break cases that are not corrupted now? How
could obtaining a stronger lock have such effect?No, I mean that I don't know if using SRE instead of AE would have negative
consequences for logical decoding. I.e. whether, from a logical decoding POV,
it'd suffice to increase the lock level to just SRE instead of AE.Since I don't see how it'd be correct otherwise, it's kind of a moot question.
We lost track of this thread and the bug is still open. IIUC, the
conclusion is to use SRE in OpenTableList() to fix the reported issue.
Andres, Tomas, please let me know if my understanding is wrong,
otherwise, let's proceed and fix this issue.It's in the commitfest [https://commitfest.postgresql.org/48/4766/] so I
don't think we 'lost track' of it, but it's true we haven't done much
progress recently.
Okay, thanks for pointing to the CF entry. Would you like to take care
of this? Are you seeing anything more than the simple fix to use SRE
in OpenTableList()?
--
With Regards,
Amit Kapila.
On 6/25/24 07:04, Amit Kapila wrote:
On Mon, Jun 24, 2024 at 8:06 PM Tomas Vondra
<tomas.vondra@enterprisedb.com> wrote:On 6/24/24 12:54, Amit Kapila wrote:
...
I'm not sure there are any cases where using SRE instead of AE would cause
problems for logical decoding, but it seems very hard to prove. I'd be very
surprised if just using SRE would not lead to corrupted cache contents in some
situations. The cases where a lower lock level is ok are ones where we just
don't care that the cache is coherent in that moment.Are you saying it might break cases that are not corrupted now? How
could obtaining a stronger lock have such effect?No, I mean that I don't know if using SRE instead of AE would have negative
consequences for logical decoding. I.e. whether, from a logical decoding POV,
it'd suffice to increase the lock level to just SRE instead of AE.Since I don't see how it'd be correct otherwise, it's kind of a moot question.
We lost track of this thread and the bug is still open. IIUC, the
conclusion is to use SRE in OpenTableList() to fix the reported issue.
Andres, Tomas, please let me know if my understanding is wrong,
otherwise, let's proceed and fix this issue.It's in the commitfest [https://commitfest.postgresql.org/48/4766/] so I
don't think we 'lost track' of it, but it's true we haven't done much
progress recently.Okay, thanks for pointing to the CF entry. Would you like to take care
of this? Are you seeing anything more than the simple fix to use SRE
in OpenTableList()?
I did not find a simpler fix than adding the SRE, and I think pretty
much any other fix is guaranteed to be more complex. I don't remember
all the details without relearning all the details, but IIRC the main
challenge for me was to convince myself it's a sufficient and reliable
fix (and not working simply by chance).
I won't have time to look into this anytime soon, so feel free to take
care of this and push the fix.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Wed, Jun 26, 2024 at 4:57 PM Tomas Vondra
<tomas.vondra@enterprisedb.com> wrote:
On 6/25/24 07:04, Amit Kapila wrote:
On Mon, Jun 24, 2024 at 8:06 PM Tomas Vondra
<tomas.vondra@enterprisedb.com> wrote:On 6/24/24 12:54, Amit Kapila wrote:
...
I'm not sure there are any cases where using SRE instead of AE would cause
problems for logical decoding, but it seems very hard to prove. I'd be very
surprised if just using SRE would not lead to corrupted cache contents in some
situations. The cases where a lower lock level is ok are ones where we just
don't care that the cache is coherent in that moment.Are you saying it might break cases that are not corrupted now? How
could obtaining a stronger lock have such effect?No, I mean that I don't know if using SRE instead of AE would have negative
consequences for logical decoding. I.e. whether, from a logical decoding POV,
it'd suffice to increase the lock level to just SRE instead of AE.Since I don't see how it'd be correct otherwise, it's kind of a moot question.
We lost track of this thread and the bug is still open. IIUC, the
conclusion is to use SRE in OpenTableList() to fix the reported issue.
Andres, Tomas, please let me know if my understanding is wrong,
otherwise, let's proceed and fix this issue.It's in the commitfest [https://commitfest.postgresql.org/48/4766/] so I
don't think we 'lost track' of it, but it's true we haven't done much
progress recently.Okay, thanks for pointing to the CF entry. Would you like to take care
of this? Are you seeing anything more than the simple fix to use SRE
in OpenTableList()?I did not find a simpler fix than adding the SRE, and I think pretty
much any other fix is guaranteed to be more complex. I don't remember
all the details without relearning all the details, but IIRC the main
challenge for me was to convince myself it's a sufficient and reliable
fix (and not working simply by chance).I won't have time to look into this anytime soon, so feel free to take
care of this and push the fix.
Okay, I'll take care of this.
--
With Regards,
Amit Kapila.
On Thu, 27 Jun 2024 at 08:38, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jun 26, 2024 at 4:57 PM Tomas Vondra
<tomas.vondra@enterprisedb.com> wrote:On 6/25/24 07:04, Amit Kapila wrote:
On Mon, Jun 24, 2024 at 8:06 PM Tomas Vondra
<tomas.vondra@enterprisedb.com> wrote:On 6/24/24 12:54, Amit Kapila wrote:
...
I'm not sure there are any cases where using SRE instead of AE would cause
problems for logical decoding, but it seems very hard to prove. I'd be very
surprised if just using SRE would not lead to corrupted cache contents in some
situations. The cases where a lower lock level is ok are ones where we just
don't care that the cache is coherent in that moment.Are you saying it might break cases that are not corrupted now? How
could obtaining a stronger lock have such effect?No, I mean that I don't know if using SRE instead of AE would have negative
consequences for logical decoding. I.e. whether, from a logical decoding POV,
it'd suffice to increase the lock level to just SRE instead of AE.Since I don't see how it'd be correct otherwise, it's kind of a moot question.
We lost track of this thread and the bug is still open. IIUC, the
conclusion is to use SRE in OpenTableList() to fix the reported issue.
Andres, Tomas, please let me know if my understanding is wrong,
otherwise, let's proceed and fix this issue.It's in the commitfest [https://commitfest.postgresql.org/48/4766/] so I
don't think we 'lost track' of it, but it's true we haven't done much
progress recently.Okay, thanks for pointing to the CF entry. Would you like to take care
of this? Are you seeing anything more than the simple fix to use SRE
in OpenTableList()?I did not find a simpler fix than adding the SRE, and I think pretty
much any other fix is guaranteed to be more complex. I don't remember
all the details without relearning all the details, but IIRC the main
challenge for me was to convince myself it's a sufficient and reliable
fix (and not working simply by chance).I won't have time to look into this anytime soon, so feel free to take
care of this and push the fix.Okay, I'll take care of this.
This issue is present in all supported versions. I was able to
reproduce it using the steps recommended by Andres and Tomas's
scripts. I also conducted a small test through TAP tests to verify the
problem. Attached is the alternate_lock_HEAD.patch, which includes the
lock modification(Tomas's change) and the TAP test.
To reproduce the issue in the HEAD version, we cannot use the same
test as in the alternate_lock_HEAD patch because the behavior changes
slightly after the fix to wait for the lock until the open transaction
completes. The attached issue_reproduce_testcase_head.patch can be
used to reproduce the issue through TAP test in HEAD.
The changes made in the HEAD version do not directly apply to older
branches. For PG14, PG13, and PG12 branches, you can use the
alternate_lock_PG14.patch.
Regards,
Vignesh
Attachments:
alternate_lock_HEAD.patchtext/x-patch; charset=US-ASCII; name=alternate_lock_HEAD.patchDownload
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6ea709988e..f4e5745909 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1568,7 +1568,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();
- rel = table_openrv(t->relation, ShareUpdateExclusiveLock);
+ rel = table_openrv(t->relation, ShareRowExclusiveLock);
myrelid = RelationGetRelid(rel);
/*
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..3316e57ff5 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,78 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Incremental data synchronization skipped when a new table is added, if
+# there is a concurrent active transaction involving the same table.
+
+# Create table in publisher and subscriber.
+$node_publisher->safe_psql('postgres', "CREATE TABLE tab_conc(a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_conc(a int)");
+
+# 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_publisher->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_publisher->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_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab_conc'::regclass AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ 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;
+
+$background_psql1->query_safe(qq[INSERT INTO tab_conc VALUES (2)]);
+$background_psql1->quit;
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'sub1');
+
+$result = $node_subscriber->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');
+
+# Perform an insert.
+$node_publisher->safe_psql('postgres', "INSERT INTO tab_conc values(3)");
+$node_publisher->wait_for_catchup('sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3), 'Verify that the incremental data added after table synchronization is replicated to the subscriber');
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
issue_reproduce_testcase_head.patchtext/x-patch; charset=US-ASCII; name=issue_reproduce_testcase_head.patchDownload
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..9fde78e1b9 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,72 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Incremental data synchronization skipped when a new table is added, if
+# there is a concurrent active transaction involving the same table.
+
+# Create table in publisher and subscriber.
+$node_publisher->safe_psql('postgres', "CREATE TABLE tab_conc(a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_conc(a int)");
+
+# 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_publisher->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 from background_psql
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+# This will wait as the open transaction holding a lock.
+$background_psql2->query_until(qr//, "ALTER PUBLICATION pub1 ADD TABLE tab_conc;\n");
+
+$node_publisher->poll_query_until('postgres',
+"SELECT COUNT(1) = 1 FROM pg_publication_rel WHERE prrelid = 'tab_conc'::regclass;"
+ )
+ or die
+ "Timed out while waiting for the table tab_conc is added to pg_publication_rel";
+
+# Complete the old transaction.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql1->quit;
+
+$background_psql1->query_safe(qq[INSERT INTO tab_conc VALUES (2)]);
+$background_psql1->quit;
+
+# Refresh the publication
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'sub1');
+
+$result = $node_subscriber->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');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab_conc values(3)");
+$node_publisher->wait_for_catchup('sub1');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3), 'Verify that the incremental data added after table synchronization is replicated to the subscriber');
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
alternate_lock_PG14.patchtext/x-patch; charset=US-ASCII; name=alternate_lock_PG14.patchDownload
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 7ee8825522..55e8cbfdc9 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -571,7 +571,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);
/*
On Mon, Jul 1, 2024 at 10:51 AM vignesh C <vignesh21@gmail.com> wrote:
This issue is present in all supported versions. I was able to
reproduce it using the steps recommended by Andres and Tomas's
scripts. I also conducted a small test through TAP tests to verify the
problem. Attached is the alternate_lock_HEAD.patch, which includes the
lock modification(Tomas's change) and the TAP test.
@@ -1568,7 +1568,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();
- rel = table_openrv(t->relation, ShareUpdateExclusiveLock);
+ rel = table_openrv(t->relation, ShareRowExclusiveLock);
The comment just above this code ("Open, share-lock, and check all the
explicitly-specified relations") needs modification. It would be
better to explain the reason of why we would need SRE lock here.
To reproduce the issue in the HEAD version, we cannot use the same
test as in the alternate_lock_HEAD patch because the behavior changes
slightly after the fix to wait for the lock until the open transaction
completes.
But won't the test that reproduces the problem in HEAD be successful
after the code change? If so, can't we use the same test instead of
slight modification to verify the lock mode?
The attached issue_reproduce_testcase_head.patch can be
used to reproduce the issue through TAP test in HEAD.
The changes made in the HEAD version do not directly apply to older
branches. For PG14, PG13, and PG12 branches, you can use the
alternate_lock_PG14.patch.
Why didn't you include the test in the back branches? If it is due to
background psql stuff, then won't commit
(https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=187b8991f70fc3d2a13dc709edd408a8df0be055)
can address it?
--
With Regards,
Amit Kapila.
On Tue, 9 Jul 2024 at 17:05, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Mon, Jul 1, 2024 at 10:51 AM vignesh C <vignesh21@gmail.com> wrote:
This issue is present in all supported versions. I was able to
reproduce it using the steps recommended by Andres and Tomas's
scripts. I also conducted a small test through TAP tests to verify the
problem. Attached is the alternate_lock_HEAD.patch, which includes the
lock modification(Tomas's change) and the TAP test.@@ -1568,7 +1568,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();- rel = table_openrv(t->relation, ShareUpdateExclusiveLock); + rel = table_openrv(t->relation, ShareRowExclusiveLock);The comment just above this code ("Open, share-lock, and check all the
explicitly-specified relations") needs modification. It would be
better to explain the reason of why we would need SRE lock here.
Updated comments for the same.
To reproduce the issue in the HEAD version, we cannot use the same
test as in the alternate_lock_HEAD patch because the behavior changes
slightly after the fix to wait for the lock until the open transaction
completes.But won't the test that reproduces the problem in HEAD be successful
after the code change? If so, can't we use the same test instead of
slight modification to verify the lock mode?
Before the patch fix, the ALTER PUBLICATION command would succeed
immediately. Now, the ALTER PUBLICATION command waits until it
acquires the ShareRowExclusiveLock. This change means that in test
cases, previously we waited until the table was added to the
publication, whereas now, after applying the patch, we wait until the
ALTER PUBLICATION command is actively waiting for the
ShareRowExclusiveLock. This waiting step ensures consistent execution
and sequencing of tests each time.
The attached issue_reproduce_testcase_head.patch can be
used to reproduce the issue through TAP test in HEAD.
The changes made in the HEAD version do not directly apply to older
branches. For PG14, PG13, and PG12 branches, you can use the
alternate_lock_PG14.patch.Why didn't you include the test in the back branches? If it is due to
background psql stuff, then won't commit
(https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=187b8991f70fc3d2a13dc709edd408a8df0be055)
can address it?
Indeed, I initially believed it wasn't available. Currently, I haven't
incorporated the back branch patch, but I plan to include it in a
subsequent version once there are no review comments on the HEAD
patch.
The updated v2 version patch has the fix for the comments.
Regards,
Vignesh
Attachments:
v2-0001-Fix-random-data-loss-during-logical-replication.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Fix-random-data-loss-during-logical-replication.patchDownload
From be5eb6701e232dd475cdc2639c423bcbe638b068 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Tue, 9 Jul 2024 19:23:10 +0530
Subject: [PATCH v2] Fix random data loss during 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 transactions on the table. As a
consequence, the ALTER PUBLICATION command could complete 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, the locking mechanism has been revised. Tables are
now locked using ShareRowExclusiveLock mode during the addition to a
publication. This adjustment ensures that the ALTER PUBLICATION command
waits for any ongoing transactions on these tables to complete before
proceeding. As a result, transactions initiated before the publication
alteration are correctly included in the replication process, maintaining
data consistency across replicas.
---
src/backend/commands/publicationcmds.c | 12 +++--
src/test/subscription/t/100_bugs.pl | 72 ++++++++++++++++++++++++++
2 files changed, 81 insertions(+), 3 deletions(-)
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6ea709988e..be1fcf4f58 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1542,8 +1542,14 @@ RemovePublicationSchemaById(Oid psoid)
/*
* Open relations specified by a PublicationTable 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 in order to
+ * add them to a publication. The table needs to be locked in
+ * ShareRowExclusiveLock mode to ensure that any ongoing transactions involving
+ * the table are completed before adding it to the publication. Failing to do
+ * so means that transactions 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)
@@ -1568,7 +1574,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();
- rel = table_openrv(t->relation, ShareUpdateExclusiveLock);
+ rel = table_openrv(t->relation, ShareRowExclusiveLock);
myrelid = RelationGetRelid(rel);
/*
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..3316e57ff5 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,78 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Incremental data synchronization skipped when a new table is added, if
+# there is a concurrent active transaction involving the same table.
+
+# Create table in publisher and subscriber.
+$node_publisher->safe_psql('postgres', "CREATE TABLE tab_conc(a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_conc(a int)");
+
+# 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_publisher->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_publisher->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_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab_conc'::regclass AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ 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;
+
+$background_psql1->query_safe(qq[INSERT INTO tab_conc VALUES (2)]);
+$background_psql1->quit;
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'sub1');
+
+$result = $node_subscriber->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');
+
+# Perform an insert.
+$node_publisher->safe_psql('postgres', "INSERT INTO tab_conc values(3)");
+$node_publisher->wait_for_catchup('sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3), 'Verify that the incremental data added after table synchronization is replicated to the subscriber');
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
On Tue, Jul 9, 2024 at 8:14 PM vignesh C <vignesh21@gmail.com> wrote:
On Tue, 9 Jul 2024 at 17:05, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Mon, Jul 1, 2024 at 10:51 AM vignesh C <vignesh21@gmail.com> wrote:
This issue is present in all supported versions. I was able to
reproduce it using the steps recommended by Andres and Tomas's
scripts. I also conducted a small test through TAP tests to verify the
problem. Attached is the alternate_lock_HEAD.patch, which includes the
lock modification(Tomas's change) and the TAP test.@@ -1568,7 +1568,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();- rel = table_openrv(t->relation, ShareUpdateExclusiveLock); + rel = table_openrv(t->relation, ShareRowExclusiveLock);The comment just above this code ("Open, share-lock, and check all the
explicitly-specified relations") needs modification. It would be
better to explain the reason of why we would need SRE lock here.Updated comments for the same.
The patch missed to use the ShareRowExclusiveLock for partitions, see
attached. I haven't tested it but they should also face the same
problem. Apart from that, I have changed the comments in a few places
in the patch.
--
With Regards,
Amit Kapila.
Attachments:
v3-0001-Fix-data-loss-during-initial-sync-in-logical-repl.patchapplication/octet-stream; name=v3-0001-Fix-data-loss-during-initial-sync-in-logical-repl.patchDownload
From 92422b204345ea8078be1c444d3014debe583b65 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Tue, 9 Jul 2024 19:23:10 +0530
Subject: [PATCH v3] 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 | 73 ++++++++++++++++++++++++++
2 files changed, 84 insertions(+), 5 deletions(-)
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6ea709988e..341ea0318d 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1542,8 +1542,14 @@ RemovePublicationSchemaById(Oid psoid)
/*
* Open relations specified by a PublicationTable 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)
@@ -1568,7 +1574,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();
- rel = table_openrv(t->relation, ShareUpdateExclusiveLock);
+ rel = table_openrv(t->relation, ShareRowExclusiveLock);
myrelid = RelationGetRelid(rel);
/*
@@ -1594,7 +1600,7 @@ OpenTableList(List *tables)
errmsg("conflicting or redundant column lists for table \"%s\"",
RelationGetRelationName(rel))));
- table_close(rel, ShareUpdateExclusiveLock);
+ table_close(rel, ShareRowExclusiveLock);
continue;
}
@@ -1622,7 +1628,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 cb36ca7b16..01ae1edfb2 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,79 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# 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.
+
+# Create table in publisher and subscriber.
+$node_publisher->safe_psql('postgres', "CREATE TABLE tab_conc(a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_conc(a int)");
+
+# 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_publisher->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_publisher->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_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab_conc'::regclass AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ 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;
+
+$background_psql1->query_safe(qq[INSERT INTO tab_conc VALUES (2)]);
+$background_psql1->quit;
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'sub1');
+
+$result = $node_subscriber->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');
+
+# Perform an insert.
+$node_publisher->safe_psql('postgres', "INSERT INTO tab_conc values(3)");
+$node_publisher->wait_for_catchup('sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3), 'Verify that the incremental data added after table synchronization is replicated to the subscriber');
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.28.0.windows.1
On Wed, 10 Jul 2024 at 12:28, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 9, 2024 at 8:14 PM vignesh C <vignesh21@gmail.com> wrote:
On Tue, 9 Jul 2024 at 17:05, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Mon, Jul 1, 2024 at 10:51 AM vignesh C <vignesh21@gmail.com> wrote:
This issue is present in all supported versions. I was able to
reproduce it using the steps recommended by Andres and Tomas's
scripts. I also conducted a small test through TAP tests to verify the
problem. Attached is the alternate_lock_HEAD.patch, which includes the
lock modification(Tomas's change) and the TAP test.@@ -1568,7 +1568,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();- rel = table_openrv(t->relation, ShareUpdateExclusiveLock); + rel = table_openrv(t->relation, ShareRowExclusiveLock);The comment just above this code ("Open, share-lock, and check all the
explicitly-specified relations") needs modification. It would be
better to explain the reason of why we would need SRE lock here.Updated comments for the same.
The patch missed to use the ShareRowExclusiveLock for partitions, see
attached. I haven't tested it but they should also face the same
problem. Apart from that, I have changed the comments in a few places
in the patch.
I could not hit the updated ShareRowExclusiveLock changes through the
partition table, instead I could verify it using the inheritance
table. Added a test for the same and also attaching the backbranch
patch.
Regards,
Vignesh
Attachments:
v4-0001-Fix-data-loss-during-initial-sync-in-logical-repl_HEAD.patchtext/x-patch; charset=US-ASCII; name=v4-0001-Fix-data-loss-during-initial-sync-in-logical-repl_HEAD.patchDownload
From d300868b61c65a6b575078c29c0d20994acae1fa Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Tue, 9 Jul 2024 19:23:10 +0530
Subject: [PATCH v4] 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 | 142 +++++++++++++++++++++++++
2 files changed, 153 insertions(+), 5 deletions(-)
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6ea709988e..341ea0318d 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1542,8 +1542,14 @@ RemovePublicationSchemaById(Oid psoid)
/*
* Open relations specified by a PublicationTable 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)
@@ -1568,7 +1574,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();
- rel = table_openrv(t->relation, ShareUpdateExclusiveLock);
+ rel = table_openrv(t->relation, ShareRowExclusiveLock);
myrelid = RelationGetRelid(rel);
/*
@@ -1594,7 +1600,7 @@ OpenTableList(List *tables)
errmsg("conflicting or redundant column lists for table \"%s\"",
RelationGetRelationName(rel))));
- table_close(rel, ShareUpdateExclusiveLock);
+ table_close(rel, ShareRowExclusiveLock);
continue;
}
@@ -1622,7 +1628,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 cb36ca7b16..670c574547 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,148 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# 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.
+
+# Create tables in publisher and subscriber.
+$node_publisher->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_subscriber->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_publisher->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_publisher->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_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab_conc'::regclass AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ 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_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab1_conc_child'::regclass AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ 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_publisher->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_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO tab1_conc_child VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'sub1');
+
+$result = $node_subscriber->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_subscriber->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_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO tab1_conc_child VALUES (3);
+));
+$node_publisher->wait_for_catchup('sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->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_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
v4-0001-Fix-data-loss-during-initial-sync-in-logical-repl_PG14.patchtext/x-patch; charset=US-ASCII; name=v4-0001-Fix-data-loss-during-initial-sync-in-logical-repl_PG14.patchDownload
From 654c8d3f6f599889c628090194aa28639bf3430d Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
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 e288dd41cd..0215f3999f 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -547,8 +547,14 @@ RemovePublicationById(Oid pubid)
/*
* 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)
@@ -570,7 +576,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);
/*
@@ -582,7 +588,7 @@ OpenTableList(List *tables)
*/
if (list_member_oid(relids, myrelid))
{
- table_close(rel, ShareUpdateExclusiveLock);
+ table_close(rel, ShareRowExclusiveLock);
continue;
}
@@ -600,7 +606,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 cce91891ab..501b94a332 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -6,7 +6,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 9;
+use Test::More tests => 13;
# Bug #15114
@@ -362,3 +362,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' AND waitstart IS NOT NULL;"
+ )
+ 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' AND waitstart IS NOT NULL;"
+ )
+ 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
v4-0001-Fix-data-loss-during-initial-sync-in-logical-repl_PG12.patchtext/x-patch; charset=US-ASCII; name=v4-0001-Fix-data-loss-during-initial-sync-in-logical-repl_PG12.patchDownload
From 7b2d8e60eb2192c4a10408b40a49914b3e1b5019 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Wed, 10 Jul 2024 21:57:57 +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 | 15 ++-
src/test/subscription/t/100_bugs.pl | 161 ++++++++++++++++++++++++-
2 files changed, 171 insertions(+), 5 deletions(-)
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 4f70af07ba..bf9761779f 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -506,7 +506,14 @@ RemovePublicationRelById(Oid proid)
/*
* Open relations specified by a RangeVar list.
- * The returned tables are locked in ShareUpdateExclusiveLock mode.
+ *
+ * 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)
@@ -528,7 +535,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);
/*
@@ -540,7 +547,7 @@ OpenTableList(List *tables)
*/
if (list_member_oid(relids, myrelid))
{
- table_close(rel, ShareUpdateExclusiveLock);
+ table_close(rel, ShareRowExclusiveLock);
continue;
}
@@ -553,7 +560,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 f5968ffa97..ba11528750 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 => 7;
+use Test::More tests => 11;
# Bug #15114
@@ -244,3 +244,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
v4-0001-Fix-data-loss-during-initial-sync-in-logical-repl_PG13.patchtext/x-patch; charset=US-ASCII; name=v4-0001-Fix-data-loss-during-initial-sync-in-logical-repl_PG13.patchDownload
From 496463b1d4b4e7d0685f03144ed23c7c3b24a7a0 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
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
v4-0001-Fix-data-loss-during-initial-sync-in-logical-repl_PG16.patchtext/x-patch; charset=US-ASCII; name=v4-0001-Fix-data-loss-during-initial-sync-in-logical-repl_PG16.patchDownload
From acd506f960cb1851e47ed7c3966b71618d6d2182 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Wed, 10 Jul 2024 20:58:04 +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 | 142 +++++++++++++++++++++++++
2 files changed, 153 insertions(+), 5 deletions(-)
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index f4ba572697..4bd56edb9b 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1549,8 +1549,14 @@ RemovePublicationSchemaById(Oid psoid)
/*
* Open relations specified by a PublicationTable 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)
@@ -1575,7 +1581,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();
- rel = table_openrv(t->relation, ShareUpdateExclusiveLock);
+ rel = table_openrv(t->relation, ShareRowExclusiveLock);
myrelid = RelationGetRelid(rel);
/*
@@ -1601,7 +1607,7 @@ OpenTableList(List *tables)
errmsg("conflicting or redundant column lists for table \"%s\"",
RelationGetRelationName(rel))));
- table_close(rel, ShareUpdateExclusiveLock);
+ table_close(rel, ShareRowExclusiveLock);
continue;
}
@@ -1629,7 +1635,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 091da5a506..1d087a74c0 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -488,6 +488,148 @@ $result = $node_subscriber->safe_psql('postgres',
is($result, qq(2|f
3|t), 'check replicated update on subscriber');
+# 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.
+
+# Create tables in publisher and subscriber.
+$node_publisher->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_subscriber->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_publisher->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_publisher->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_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab_conc'::regclass AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ 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_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab1_conc_child'::regclass AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ 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_publisher->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_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO tab1_conc_child VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'sub1');
+
+$result = $node_subscriber->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_subscriber->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_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO tab1_conc_child VALUES (3);
+));
+$node_publisher->wait_for_catchup('sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->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_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
v2_issue_reproduce_testcase_head.patchtext/x-patch; charset=US-ASCII; name=v2_issue_reproduce_testcase_head.patchDownload
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..bd8c305c7d 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,95 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Incremental data synchronization skipped when a new table is added, if
+# there is a concurrent active transaction involving the same table.
+
+# Create table in publisher and subscriber.
+$node_publisher->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_subscriber->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_publisher->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);
+ INSERT INTO tab1_conc_child VALUES (1);
+]);
+
+# Add the table to the publication from background_psql
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+# This will wait as the open transaction holding a lock.
+$background_psql2->query_until(qr//, "ALTER PUBLICATION pub1 ADD TABLE tab_conc, tab1_conc;\n");
+
+$node_publisher->poll_query_until('postgres',
+"SELECT COUNT(1) = 2 FROM pg_publication_rel WHERE prrelid = 'tab_conc'::regclass OR prrelid = 'tab1_conc'::regclass;"
+ )
+ or die
+ "Timed out while waiting for the table tab_conc is added to pg_publication_rel";
+
+# Complete the old transaction.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql1->quit;
+
+$background_psql1->query_safe(qq[INSERT INTO tab_conc VALUES (2)]);
+$background_psql1->query_safe(qq[INSERT INTO tab1_conc_child VALUES (2)]);
+$background_psql1->quit;
+
+# Refresh the publication
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM tab1_conc_child");
+is( $result, qq(1
+2), 'Ensure that the data from the tab_conc table is synchronized to the subscriber after the subscription is refreshed');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab_conc values(3)");
+$node_publisher->safe_psql('postgres', "INSERT INTO tab1_conc_child values(3)");
+
+$node_publisher->wait_for_catchup('sub1');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3), 'Verify that the incremental data added after table synchronization is replicated to the subscriber');
+
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1_conc_child");
+is( $result, qq(1
+2
+3), 'Verify that the incremental data added after table synchronization is replicated to the subscriber');
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
On Wed, Jul 10, 2024 at 10:39 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 10 Jul 2024 at 12:28, Amit Kapila <amit.kapila16@gmail.com> wrote:
The patch missed to use the ShareRowExclusiveLock for partitions, see
attached. I haven't tested it but they should also face the same
problem. Apart from that, I have changed the comments in a few places
in the patch.I could not hit the updated ShareRowExclusiveLock changes through the
partition table, instead I could verify it using the inheritance
table. Added a test for the same and also attaching the backbranch
patch.
Hi,
I tested alternative-experimental-fix-lock.patch provided by Tomas
(replaces SUE with SRE in OpenTableList). I believe there are a couple
of scenarios the patch does not cover.
1. It doesn't handle the case of "ALTER PUBLICATION <pub> ADD TABLES
IN SCHEMA <schema>".
I took crash-test.sh provided by Tomas and modified it to add all
tables in the schema to publication using the following command :
ALTER PUBLICATION p ADD TABLES IN SCHEMA public
The modified script is attached (crash-test-with-schema.sh). With this
script, I can reproduce the issue even with the patch applied. This is
because the code path to add a schema to the publication doesn't go
through OpenTableList.
I have also attached a script run-test-with-schema.sh to run
crash-test-with-schema.sh in a loop with randomly generated parameters
(modified from run.sh provided by Tomas).
2. The second issue is a deadlock which happens when the alter
publication command is run for a comma separated list of tables.
I created another script create-test-tables-order-reverse.sh. This
script runs a command like the following :
ALTER PUBLICATION p ADD TABLE test_2,test_1
Running the above script, I was able to get a deadlock error (the
output is attached in deadlock.txt). In the alter publication command,
I added the tables in the reverse order to increase the probability of
the deadlock. But it should happen with any order of tables.
I am not sure if the deadlock is a major issue because detecting the
deadlock is better than data loss. The schema issue is probably more
important. I didn't test it out with the latest patches sent by
Vignesh but since the code changes in that patch are also in
OpenTableList, I think the schema scenario won't be covered by those.
Thanks & Regards,
Nitin Motiani
Google
Attachments:
On Wed, Jul 10, 2024 at 11:22 PM Nitin Motiani <nitinmotiani@google.com> wrote:
On Wed, Jul 10, 2024 at 10:39 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 10 Jul 2024 at 12:28, Amit Kapila <amit.kapila16@gmail.com> wrote:
The patch missed to use the ShareRowExclusiveLock for partitions, see
attached. I haven't tested it but they should also face the same
problem. Apart from that, I have changed the comments in a few places
in the patch.I could not hit the updated ShareRowExclusiveLock changes through the
partition table, instead I could verify it using the inheritance
table. Added a test for the same and also attaching the backbranch
patch.Hi,
I tested alternative-experimental-fix-lock.patch provided by Tomas
(replaces SUE with SRE in OpenTableList). I believe there are a couple
of scenarios the patch does not cover.1. It doesn't handle the case of "ALTER PUBLICATION <pub> ADD TABLES
IN SCHEMA <schema>".I took crash-test.sh provided by Tomas and modified it to add all
tables in the schema to publication using the following command :ALTER PUBLICATION p ADD TABLES IN SCHEMA public
The modified script is attached (crash-test-with-schema.sh). With this
script, I can reproduce the issue even with the patch applied. This is
because the code path to add a schema to the publication doesn't go
through OpenTableList.I have also attached a script run-test-with-schema.sh to run
crash-test-with-schema.sh in a loop with randomly generated parameters
(modified from run.sh provided by Tomas).2. The second issue is a deadlock which happens when the alter
publication command is run for a comma separated list of tables.I created another script create-test-tables-order-reverse.sh. This
script runs a command like the following :ALTER PUBLICATION p ADD TABLE test_2,test_1
Running the above script, I was able to get a deadlock error (the
output is attached in deadlock.txt). In the alter publication command,
I added the tables in the reverse order to increase the probability of
the deadlock. But it should happen with any order of tables.I am not sure if the deadlock is a major issue because detecting the
deadlock is better than data loss. The schema issue is probably more
important. I didn't test it out with the latest patches sent by
Vignesh but since the code changes in that patch are also in
OpenTableList, I think the schema scenario won't be covered by those.
Hi,
I looked further into the scenario of adding the tables in schema to
the publication. Since in that case, the entry is added to
pg_publication_namespace instead of pg_publication_rel, the codepaths
for 'add table' and 'add tables in schema' are different. And in the
'add tables in schema' scenario, the OpenTableList function is not
called to get the relation ids. Therefore even with the proposed
patch, the data loss issue still persists in that case.
To validate this idea, I tried locking all the affected tables in the
schema just before the invalidation for those relations (in
ShareRowExclusiveLock mode). I am attaching the small patch for that
(alter_pub_for_schema.patch) where the change is made in the function
publication_add_schema in pg_publication.c. I am not sure if this is
the best place to make this change or if it is the right fix. It is
conceptually similar to the proposed change in OpenTableList but here
we are not just changing the lockmode but taking locks which were not
taken before. But with this change, the data loss errors went away in
my test script.
Another issue which persists with this change is the deadlock. Since
multiple table locks are acquired, the test script detects deadlock a
few times. Therefore I'm also attaching another modified script which
does a few retries in case of deadlock. The script is
crash-test-with-retries-for-schema.sh. It runs the following command
in a retry loop :
ALTER PUBLICATION p ADD TABLES IN SCHEMA public
If the command fails, it sleeps for a random amount of time (upper
bound by a MAXWAIT parameter) and then retries the command. If it
fails to run the command in the max number of retries, the final
return value from the script is DEADLOCK as we can't do a consistency
check in this scenario. Also attached is another script
run-with-deadlock-detection.sh which can run the above script for
multiple iterations.
I tried the test scripts with and without alter_pub_for_schema.patch.
Without the patch, I get the final output ERROR majority of the time
which means that the publication was altered successfully but the data
was lost on the subscriber. When I run it with the patch, I get a mix
of OK (no data loss) and DEADLOCK (the publication was not altered)
but no ERROR. I think by changing the parameters of sleep time and
number of retries we can get different fractions of OK and DEADLOCK.
I am not sure if this is the right or a clean way to fix the issue but
I think conceptually this might be the right direction. Please let me
know if my understanding is wrong or if I'm missing something.
Thanks & Regards,
Nitin Motiani
Google
Attachments:
alter_pub_for_schema.patchapplication/octet-stream; name=alter_pub_for_schema.patchDownload
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 0602398a54..067336d02c 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -32,6 +32,7 @@
#include "catalog/pg_type.h"
#include "commands/publicationcmds.h"
#include "funcapi.h"
+#include "storage/lmgr.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/catcache.h"
@@ -614,6 +615,7 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
List *schemaRels = NIL;
ObjectAddress myself,
referenced;
+ ListCell* lc;
rel = table_open(PublicationNamespaceRelationId, RowExclusiveLock);
@@ -677,6 +679,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
*/
schemaRels = GetSchemaPublicationRelations(schemaid,
PUBLICATION_PART_ALL);
+ foreach(lc, schemaRels) {
+ LockRelationOid(lfirst_oid(lc), ShareRowExclusiveLock);
+ }
InvalidatePublicationRels(schemaRels);
return myself;
On Thu, Jul 11, 2024 at 6:19 PM Nitin Motiani <nitinmotiani@google.com> wrote:
On Wed, Jul 10, 2024 at 11:22 PM Nitin Motiani <nitinmotiani@google.com> wrote:
On Wed, Jul 10, 2024 at 10:39 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 10 Jul 2024 at 12:28, Amit Kapila <amit.kapila16@gmail.com> wrote:
The patch missed to use the ShareRowExclusiveLock for partitions, see
attached. I haven't tested it but they should also face the same
problem. Apart from that, I have changed the comments in a few places
in the patch.I could not hit the updated ShareRowExclusiveLock changes through the
partition table, instead I could verify it using the inheritance
table. Added a test for the same and also attaching the backbranch
patch.Hi,
I tested alternative-experimental-fix-lock.patch provided by Tomas
(replaces SUE with SRE in OpenTableList). I believe there are a couple
of scenarios the patch does not cover.1. It doesn't handle the case of "ALTER PUBLICATION <pub> ADD TABLES
IN SCHEMA <schema>".I took crash-test.sh provided by Tomas and modified it to add all
tables in the schema to publication using the following command :ALTER PUBLICATION p ADD TABLES IN SCHEMA public
The modified script is attached (crash-test-with-schema.sh). With this
script, I can reproduce the issue even with the patch applied. This is
because the code path to add a schema to the publication doesn't go
through OpenTableList.I have also attached a script run-test-with-schema.sh to run
crash-test-with-schema.sh in a loop with randomly generated parameters
(modified from run.sh provided by Tomas).2. The second issue is a deadlock which happens when the alter
publication command is run for a comma separated list of tables.I created another script create-test-tables-order-reverse.sh. This
script runs a command like the following :ALTER PUBLICATION p ADD TABLE test_2,test_1
Running the above script, I was able to get a deadlock error (the
output is attached in deadlock.txt). In the alter publication command,
I added the tables in the reverse order to increase the probability of
the deadlock. But it should happen with any order of tables.I am not sure if the deadlock is a major issue because detecting the
deadlock is better than data loss.
The deadlock reported in this case is an expected behavior. This is no
different that locking tables or rows in reverse order.
I looked further into the scenario of adding the tables in schema to
the publication. Since in that case, the entry is added to
pg_publication_namespace instead of pg_publication_rel, the codepaths
for 'add table' and 'add tables in schema' are different. And in the
'add tables in schema' scenario, the OpenTableList function is not
called to get the relation ids. Therefore even with the proposed
patch, the data loss issue still persists in that case.To validate this idea, I tried locking all the affected tables in the
schema just before the invalidation for those relations (in
ShareRowExclusiveLock mode).
This sounds like a reasonable approach to fix the issue. However, we
should check SET publication_object as well, especially the drop part
in it. It should not happen that we miss sending the data for ADD but
for DROP, we send data when we shouldn't have sent it.
--
With Regards,
Amit Kapila.
On Mon, 15 Jul 2024 at 15:31, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Thu, Jul 11, 2024 at 6:19 PM Nitin Motiani <nitinmotiani@google.com> wrote:
On Wed, Jul 10, 2024 at 11:22 PM Nitin Motiani <nitinmotiani@google.com> wrote:
On Wed, Jul 10, 2024 at 10:39 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 10 Jul 2024 at 12:28, Amit Kapila <amit.kapila16@gmail.com> wrote:
The patch missed to use the ShareRowExclusiveLock for partitions, see
attached. I haven't tested it but they should also face the same
problem. Apart from that, I have changed the comments in a few places
in the patch.I could not hit the updated ShareRowExclusiveLock changes through the
partition table, instead I could verify it using the inheritance
table. Added a test for the same and also attaching the backbranch
patch.Hi,
I tested alternative-experimental-fix-lock.patch provided by Tomas
(replaces SUE with SRE in OpenTableList). I believe there are a couple
of scenarios the patch does not cover.1. It doesn't handle the case of "ALTER PUBLICATION <pub> ADD TABLES
IN SCHEMA <schema>".I took crash-test.sh provided by Tomas and modified it to add all
tables in the schema to publication using the following command :ALTER PUBLICATION p ADD TABLES IN SCHEMA public
The modified script is attached (crash-test-with-schema.sh). With this
script, I can reproduce the issue even with the patch applied. This is
because the code path to add a schema to the publication doesn't go
through OpenTableList.I have also attached a script run-test-with-schema.sh to run
crash-test-with-schema.sh in a loop with randomly generated parameters
(modified from run.sh provided by Tomas).2. The second issue is a deadlock which happens when the alter
publication command is run for a comma separated list of tables.I created another script create-test-tables-order-reverse.sh. This
script runs a command like the following :ALTER PUBLICATION p ADD TABLE test_2,test_1
Running the above script, I was able to get a deadlock error (the
output is attached in deadlock.txt). In the alter publication command,
I added the tables in the reverse order to increase the probability of
the deadlock. But it should happen with any order of tables.I am not sure if the deadlock is a major issue because detecting the
deadlock is better than data loss.The deadlock reported in this case is an expected behavior. This is no
different that locking tables or rows in reverse order.I looked further into the scenario of adding the tables in schema to
the publication. Since in that case, the entry is added to
pg_publication_namespace instead of pg_publication_rel, the codepaths
for 'add table' and 'add tables in schema' are different. And in the
'add tables in schema' scenario, the OpenTableList function is not
called to get the relation ids. Therefore even with the proposed
patch, the data loss issue still persists in that case.To validate this idea, I tried locking all the affected tables in the
schema just before the invalidation for those relations (in
ShareRowExclusiveLock mode).This sounds like a reasonable approach to fix the issue. However, we
should check SET publication_object as well, especially the drop part
in it. It should not happen that we miss sending the data for ADD but
for DROP, we send data when we shouldn't have sent it.
There were few other scenarios, similar to the one you mentioned,
where the issue occurred. For example: a) When specifying a subset of
existing tables in the ALTER PUBLICATION ... SET TABLE command, the
tables that were supposed to be removed from the publication were not
locked in ShareRowExclusiveLock mode. b) The ALTER PUBLICATION ...
DROP TABLES IN SCHEMA command did not lock the relations that will be
removed from the publication in ShareRowExclusiveLock mode. Both of
these scenarios resulted in data inconsistency due to inadequate
locking. The attached patch addresses these issues.
Regards,
Vignesh
Attachments:
v5-0001-Fix-data-loss-during-initial-sync-in-logical-repl.patchtext/x-patch; charset=US-ASCII; name=v5-0001-Fix-data-loss-during-initial-sync-in-logical-repl.patchDownload
From e1e79bcf24cacf4f8291692f7815dd323e7b4ab5 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Tue, 9 Jul 2024 19:23:10 +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.
A similar problem occurred when adding tables in schema to a publication
for an ongoing DML transaction involving those tables in the schema, as
the tables were not locked during the ALTER PUBLICATION.
The issue has now been resolved by locking all the tables in the schema
with ShareRowExclusiveLock mode during their addition to the publication
which resolves the addition of tables in schema waits similarly to the
addition of tables.
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/catalog/pg_publication.c | 9 +
src/backend/commands/publicationcmds.c | 34 +-
src/test/subscription/t/100_bugs.pl | 481 +++++++++++++++++++++++++
3 files changed, 518 insertions(+), 6 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 0602398a54..f078b705d6 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -32,6 +32,7 @@
#include "catalog/pg_type.h"
#include "commands/publicationcmds.h"
#include "funcapi.h"
+#include "storage/lmgr.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/catcache.h"
@@ -677,6 +678,14 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
*/
schemaRels = GetSchemaPublicationRelations(schemaid,
PUBLICATION_PART_ALL);
+
+ /*
+ * Data loss due to concurrency issues are avoided by locking the
+ * relation in ShareRowExclusiveLock as described atop OpenTableList.
+ */
+ foreach_oid(schrelid, schemaRels)
+ LockRelationOid(schrelid, ShareRowExclusiveLock);
+
InvalidatePublicationRels(schemaRels);
return myself;
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6ea709988e..d5cd9e3820 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1219,8 +1219,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
oldrel = palloc(sizeof(PublicationRelInfo));
oldrel->whereClause = NULL;
oldrel->columns = NIL;
+
+ /*
+ * Data loss due to concurrency issues are avoided by locking
+ * the relation in ShareRowExclusiveLock as described atop
+ * OpenTableList.
+ */
oldrel->relation = table_open(oldrelid,
- ShareUpdateExclusiveLock);
+ ShareRowExclusiveLock);
delrels = lappend(delrels, oldrel);
}
}
@@ -1542,8 +1548,14 @@ RemovePublicationSchemaById(Oid psoid)
/*
* Open relations specified by a PublicationTable 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)
@@ -1568,7 +1580,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();
- rel = table_openrv(t->relation, ShareUpdateExclusiveLock);
+ rel = table_openrv(t->relation, ShareRowExclusiveLock);
myrelid = RelationGetRelid(rel);
/*
@@ -1594,7 +1606,7 @@ OpenTableList(List *tables)
errmsg("conflicting or redundant column lists for table \"%s\"",
RelationGetRelationName(rel))));
- table_close(rel, ShareUpdateExclusiveLock);
+ table_close(rel, ShareRowExclusiveLock);
continue;
}
@@ -1622,7 +1634,7 @@ OpenTableList(List *tables)
List *children;
ListCell *child;
- children = find_all_inheritors(myrelid, ShareUpdateExclusiveLock,
+ children = find_all_inheritors(myrelid, ShareRowExclusiveLock,
NULL);
foreach(child, children)
@@ -1860,6 +1872,16 @@ PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok)
foreach(lc, schemas)
{
Oid schemaid = lfirst_oid(lc);
+ List *schemaRels;
+
+ schemaRels = GetSchemaPublicationRelations(schemaid, PUBLICATION_PART_ALL);
+
+ /*
+ * Data loss due to concurrency issues are avoided by locking the
+ * relation in ShareRowExclusiveLock as described atop OpenTableList.
+ */
+ foreach_oid(schrelid, schemaRels)
+ LockRelationOid(schrelid, ShareRowExclusiveLock);
psid = GetSysCacheOid2(PUBLICATIONNAMESPACEMAP,
Anum_pg_publication_namespace_oid,
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..a087bc2d08 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,487 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# cleanpup
+$node_publisher->safe_psql('postgres', qq(DROP PUBLICATION pub1;));
+$node_subscriber->safe_psql('postgres', qq(DROP SUBSCRIPTION sub1;));
+
+# =============================================================================
+# 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.
+# =============================================================================
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE TABLE tab1_conc(a int);
+ CREATE TABLE tab1_conc_child() inherits (tab1_conc);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SCHEMA sch4;
+ CREATE TABLE sch4.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE TABLE tab1_conc(a int);
+ CREATE TABLE tab1_conc_child() inherits (tab1_conc);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SCHEMA sch4;
+ CREATE TABLE sch4.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+
+));
+
+# 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_publisher->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_publisher->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 regress_pub1 ADD TABLE tab_conc;\n");
+
+# Verify that the table addition is waiting to acquire a ShareRowExclusiveLock.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab_conc'::regclass AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ or die
+ "Timed out while waiting for alter publication tries to wait on ShareRowExclusiveLock";
+
+# Complete the old transaction.
+$background_psql1->query_safe(qq[COMMIT]);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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'
+);
+
+# =============================================================================
+# This bug is present with inheritance table as well.
+# =============================================================================
+
+# 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 regress_pub1 ADD TABLE tab1_conc;\n");
+
+# Verify that the child table addition is waiting to acquire a
+# ShareRowExclusiveLock.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab1_conc_child'::regclass AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ 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_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_publication_rel WHERE prrelid IN ('tab1_conc'::regclass);"
+ )
+ or die
+ "Timed out while waiting for alter publication to add the table to the publication";
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab1_conc_child VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result =
+ $node_subscriber->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_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab1_conc_child VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result =
+ $node_subscriber->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'
+);
+
+# =============================================================================
+# This bug is present with ALTER PUBLICATION ... SET TABLE.
+# Specify a subset of tables present in the publication, ShareRowExclusiveLock
+# was not taken for the tables that were dropped as part of SET TABLE operation.
+# =============================================================================
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab1_conc_child VALUES (4);
+]);
+
+# This operation will wait because an open transaction is holding a lock on the
+# publication's relation.
+$background_psql2->query_until(qr//,
+ "ALTER PUBLICATION regress_pub1 SET TABLE tab_conc;\n");
+
+# Check that the tab1_conc_child table, which is set to be removed from the
+# publication, is waiting to acquire a ShareRowExclusiveLock due to the open
+# transaction.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation IN ('tab1_conc_child'::regclass) AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ or die
+ "Timed out while waiting for alter publication tries to wait on ShareRowExclusiveLock";
+
+# Verify that ShareRowExclusiveLock lock is acquired for tab_conc.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation IN ('tab_conc'::regclass) AND mode = 'ShareRowExclusiveLock' AND waitstart IS NULL;"
+ )
+ 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 table is removed from the publication.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 0 FROM pg_publication_rel WHERE prrelid IN ('tab1_conc_child'::regclass);"
+ )
+ or die
+ "Timed out while waiting for alter publication to add the table to the publication";
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert before SET PUBLICATION is replicated to the subscriber.
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1_conc_child");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table tab1_conc_child before removing table from publication is replicated to the subscriber'
+);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab1_conc_child VALUES (5);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Confirm that the insertion following SET PUBLICATION, which will remove the
+# relation from the publication, will not be replicated to the subscriber.
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM tab1_conc_child");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table tab1_conc_child after removing table from publication is not replicated to the subscriber'
+);
+
+# =============================================================================
+# This bug is present with ALTER PUBLICATION ... DROP TABLE.
+# =============================================================================
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (4);
+]);
+
+# This operation will wait because there is an open transaction holding a lock.
+$background_psql2->query_until(qr//,
+ "ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc;\n");
+
+# Verify that the child table addition is waiting to acquire a
+# ShareRowExclusiveLock.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab_conc'::regclass AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ 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 dropped from the publication.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 0 FROM pg_publication_rel WHERE prrelid IN ('tab_conc'::regclass);"
+ )
+ or die
+ "Timed out while waiting for alter publication to add the table to the publication";
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert before drop table is replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table tab_conc before removing table from publication is replicated to the subscriber'
+);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (5);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert fter drop table is not replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table tab_conc after removing table from publication is not replicated to the subscriber'
+);
+
+# =============================================================================
+# This bug is present with ADD TABLES IN SCHEMA too.
+# =============================================================================
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (1);
+]);
+
+# Add this schema to the publication, this operation will wait because
+# there is an open transaction holding a lock.
+$background_psql2->query_until(qr//,
+ "ALTER PUBLICATION regress_pub1 ADD TABLES IN SCHEMA sch3, sch4;\n");
+
+# Verify that the schema addition is waiting to acquire a ShareRowExclusiveLock.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = (SELECT oid FROM pg_class WHERE relname = 'tab_conc' AND relnamespace = 'sch3'::regnamespace) AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ or die
+ "Timed out while waiting for alter publication tries to wait on ShareRowExclusiveLock";
+
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql1->quit;
+
+$node_publisher->safe_psql('postgres',
+ qq(INSERT INTO sch3.tab_conc VALUES (2);));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+# =============================================================================
+# This bug is present with SET TABLES IN SCHEMA too.
+# =============================================================================
+
+# Maintain an active transaction with a schema table that will be removed from
+# the publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch4.tab_conc VALUES (1);
+]);
+
+# Set a subset of schema to the publication.
+$background_psql2->query_until(qr//,
+ "ALTER PUBLICATION regress_pub1 SET TABLES IN SCHEMA sch3;\n");
+
+# Verify that the sch4.tab_conc table which will be removed from the
+# publication is waiting to acquire a ShareRowExclusiveLock because of the open
+# transaction.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = (SELECT oid FROM pg_class WHERE relname = 'tab_conc' AND relnamespace = 'sch4'::regnamespace) AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ 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 table is removed from the publication.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 0 FROM pg_publication_namespace WHERE pnnspid IN ('sch4'::regnamespace);"
+ )
+ or die
+ "Timed out while waiting for alter publication to add the table to the publication";
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert before SET TABLES IN SCHEMA is replicated to the subscriber.
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch4.tab_conc");
+is($result, qq(1),
+ 'Verify that the incremental data for table sch4.tab_conc before removing table from publication is replicated to the subscriber'
+);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO sch4.tab_conc VALUES (2);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert after SET TABLES IN SCHEMA is not replicated to the subscriber.
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch4.tab_conc");
+is($result, qq(1),
+ 'Verify that the incremental data for table sch4.tab_conc after removing table from publication is not replicated to the subscriber'
+);
+
+# =============================================================================
+# This bug is present with DROP TABLES IN SCHEMA too.
+# =============================================================================
+
+# Maintain an active transaction with a schema table that will be dropped from
+# the publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (4);
+]);
+
+# DROP this schema to the publication, this operation will wait because
+# there is an open transaction holding a lock.
+$background_psql2->query_until(qr//,
+ "ALTER PUBLICATION regress_pub1 DROP TABLES IN SCHEMA sch3;\n");
+
+# Verify that the sch3.tab_conc table which will be dropped from the
+# publicaiton is waiting to acquire a ShareRowExclusiveLock.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = (SELECT oid FROM pg_class WHERE relname = 'tab_conc' AND relnamespace = 'sch3'::regnamespace) AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ or die
+ "Timed out while waiting for alter publication tries to wait on ShareRowExclusiveLock";
+
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql1->quit;
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Ensure that the data from the sch3.tab_conc table is replicated to the subscriber before drop tables in schema from publication'
+);
+
+$node_publisher->safe_psql('postgres',
+ qq(INSERT INTO sch3.tab_conc VALUES (5);));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Ensure that the data from the sch3.tab_conc table is not replicated after drop tables in schema from the publication'
+);
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
On Mon, Jul 15, 2024 at 11:42 PM vignesh C <vignesh21@gmail.com> wrote:
On Mon, 15 Jul 2024 at 15:31, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Thu, Jul 11, 2024 at 6:19 PM Nitin Motiani <nitinmotiani@google.com> wrote:
I looked further into the scenario of adding the tables in schema to
the publication. Since in that case, the entry is added to
pg_publication_namespace instead of pg_publication_rel, the codepaths
for 'add table' and 'add tables in schema' are different. And in the
'add tables in schema' scenario, the OpenTableList function is not
called to get the relation ids. Therefore even with the proposed
patch, the data loss issue still persists in that case.To validate this idea, I tried locking all the affected tables in the
schema just before the invalidation for those relations (in
ShareRowExclusiveLock mode).This sounds like a reasonable approach to fix the issue. However, we
should check SET publication_object as well, especially the drop part
in it. It should not happen that we miss sending the data for ADD but
for DROP, we send data when we shouldn't have sent it.There were few other scenarios, similar to the one you mentioned,
where the issue occurred. For example: a) When specifying a subset of
existing tables in the ALTER PUBLICATION ... SET TABLE command, the
tables that were supposed to be removed from the publication were not
locked in ShareRowExclusiveLock mode. b) The ALTER PUBLICATION ...
DROP TABLES IN SCHEMA command did not lock the relations that will be
removed from the publication in ShareRowExclusiveLock mode. Both of
these scenarios resulted in data inconsistency due to inadequate
locking. The attached patch addresses these issues.
Hi,
A couple of questions on the latest patch :
1. I see there is this logic in PublicationDropSchemas to first check
if there is a valid entry for the schema in pg_publication_namespace
psid = GetSysCacheOid2(PUBLICATIONNAMESPACEMAP,
Anum_pg_publication_namespace_oid,
ObjectIdGetDatum(schemaid),
ObjectIdGetDatum(pubid));
if (!OidIsValid(psid))
{
if (missing_ok)
continue;
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("tables from schema
\"%s\" are not part of the publication",
get_namespace_name(schemaid))));
}
Your proposed change locks the schemaRels before this code block.
Would it be better to lock the schemaRels after the error check? So
that just in case, the publication on the schema is not valid anymore,
the lock is not held unnecessarily on all its tables.
2. The function publication_add_schema explicitly invalidates cache by
calling InvalidatePublicationRels(schemaRels). That is not present in
the current PublicationDropSchemas code. Is that something which
should be added in the drop scenario also? Please let me know if there
is some context that I'm missing regarding why this was not added
originally for the drop scenario.
Thanks & Regards,
Nitin Motiani
Google
On Tue, Jul 16, 2024 at 12:48 AM Nitin Motiani <nitinmotiani@google.com> wrote:
A couple of questions on the latest patch :
1. I see there is this logic in PublicationDropSchemas to first check
if there is a valid entry for the schema in pg_publication_namespacepsid = GetSysCacheOid2(PUBLICATIONNAMESPACEMAP,
Anum_pg_publication_namespace_oid,
ObjectIdGetDatum(schemaid),
ObjectIdGetDatum(pubid));
if (!OidIsValid(psid))
{
if (missing_ok)
continue;ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("tables from schema
\"%s\" are not part of the publication",get_namespace_name(schemaid))));
}Your proposed change locks the schemaRels before this code block.
Would it be better to lock the schemaRels after the error check? So
that just in case, the publication on the schema is not valid anymore,
the lock is not held unnecessarily on all its tables.
Good point. It is better to lock the relations in
RemovePublicationSchemaById() where we are invalidating relcache as
well. See the response to your next point as well.
2. The function publication_add_schema explicitly invalidates cache by
calling InvalidatePublicationRels(schemaRels). That is not present in
the current PublicationDropSchemas code. Is that something which
should be added in the drop scenario also? Please let me know if there
is some context that I'm missing regarding why this was not added
originally for the drop scenario.
The required invalidation happens in the function
RemovePublicationSchemaById(). So, we should lock in
RemovePublicationSchemaById() as that would avoid calling
GetSchemaPublicationRelations() multiple times.
One related comment:
@@ -1219,8 +1219,14 @@ AlterPublicationTables(AlterPublicationStmt
*stmt, HeapTuple tup,
oldrel = palloc(sizeof(PublicationRelInfo));
oldrel->whereClause = NULL;
oldrel->columns = NIL;
+
+ /*
+ * Data loss due to concurrency issues are avoided by locking
+ * the relation in ShareRowExclusiveLock as described atop
+ * OpenTableList.
+ */
oldrel->relation = table_open(oldrelid,
- ShareUpdateExclusiveLock);
+ ShareRowExclusiveLock);
Isn't it better to lock the required relations in RemovePublicationRelById()?
--
With Regards,
Amit Kapila.
On Tue, Jul 16, 2024 at 9:29 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
One related comment: @@ -1219,8 +1219,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, oldrel = palloc(sizeof(PublicationRelInfo)); oldrel->whereClause = NULL; oldrel->columns = NIL; + + /* + * Data loss due to concurrency issues are avoided by locking + * the relation in ShareRowExclusiveLock as described atop + * OpenTableList. + */ oldrel->relation = table_open(oldrelid, - ShareUpdateExclusiveLock); + ShareRowExclusiveLock);Isn't it better to lock the required relations in RemovePublicationRelById()?
On my CentOS VM, the test file '100_bugs.pl' takes ~11s without a
patch and ~13.3s with a patch. So, 2 to 2.3s additional time for newly
added tests. It isn't worth adding this much extra time for one bug
fix. Can we combine table and schema tests into one single test and
avoid inheritance table tests as the code for those will mostly follow
the same path as a regular table?
--
With Regards,
Amit Kapila.
On Tue, 16 Jul 2024 at 11:59, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 9:29 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
One related comment: @@ -1219,8 +1219,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, oldrel = palloc(sizeof(PublicationRelInfo)); oldrel->whereClause = NULL; oldrel->columns = NIL; + + /* + * Data loss due to concurrency issues are avoided by locking + * the relation in ShareRowExclusiveLock as described atop + * OpenTableList. + */ oldrel->relation = table_open(oldrelid, - ShareUpdateExclusiveLock); + ShareRowExclusiveLock);Isn't it better to lock the required relations in RemovePublicationRelById()?
On my CentOS VM, the test file '100_bugs.pl' takes ~11s without a
patch and ~13.3s with a patch. So, 2 to 2.3s additional time for newly
added tests. It isn't worth adding this much extra time for one bug
fix. Can we combine table and schema tests into one single test and
avoid inheritance table tests as the code for those will mostly follow
the same path as a regular table?
Yes, that is better. The attached v6 version patch has the changes for the same.
The patch also addresses the comments from [1]/messages/by-id/CAA4eK1LZDW2AVDYFZdZcvmsKVGajH2-gZmjXr9BsYiy8ct_fEw@mail.gmail.com.
[1]: /messages/by-id/CAA4eK1LZDW2AVDYFZdZcvmsKVGajH2-gZmjXr9BsYiy8ct_fEw@mail.gmail.com
Regards,
Vignesh
Attachments:
v6-0001-Fix-data-loss-during-initial-sync-in-logical-repl.patchtext/x-patch; charset=US-ASCII; name=v6-0001-Fix-data-loss-during-initial-sync-in-logical-repl.patchDownload
From f09ac0daf8914a264a710fb27983560086a97742 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Tue, 9 Jul 2024 19:23:10 +0530
Subject: [PATCH v6] 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.
The same issue arose with operations such as a) ALTER PUBLICATION ... DROP
TABLE, b) ALTER PUBLICATION ... SET TABLE, c) ALTER PUBLICATION ... ADD
TABLES IN SCHEMA, d) ALTER PUBLICATION ... SET TABLES IN SCHEMA and
e) ALTER PUBLICATION ... DROP TABLES IN SCHEMA. This occurred due to
tables not being locked during the ALTER PUBLICATION process.
To address this, the tables of the publication are now locked using
ShareRowExclusiveLock mode during the ALTER PUBLICATION command. This
modification ensures that the ALTER PUBLICATION command waits until
ongoing transactions are completed before proceeding.
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/catalog/pg_publication.c | 9 ++
src/backend/commands/publicationcmds.c | 31 ++++-
src/test/subscription/t/100_bugs.pl | 156 +++++++++++++++++++++++++
3 files changed, 191 insertions(+), 5 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 0602398a54..a7c257a994 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -32,6 +32,7 @@
#include "catalog/pg_type.h"
#include "commands/publicationcmds.h"
#include "funcapi.h"
+#include "storage/lmgr.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/catcache.h"
@@ -677,6 +678,14 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
*/
schemaRels = GetSchemaPublicationRelations(schemaid,
PUBLICATION_PART_ALL);
+
+ /*
+ * Data loss due to concurrency issues are avoided by locking the relation
+ * in ShareRowExclusiveLock as described atop OpenTableList.
+ */
+ foreach_oid(schrelid, schemaRels)
+ LockRelationOid(schrelid, ShareRowExclusiveLock);
+
InvalidatePublicationRels(schemaRels);
return myself;
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 6ea709988e..9d9b5f6af9 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1466,6 +1466,13 @@ RemovePublicationRelById(Oid proid)
relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_ALL,
pubrel->prrelid);
+ /*
+ * Data loss due to concurrency issues are avoided by locking the relation
+ * in ShareRowExclusiveLock as described atop OpenTableList.
+ */
+ foreach_oid(relid, relids)
+ LockRelationOid(relid, ShareRowExclusiveLock);
+
InvalidatePublicationRels(relids);
CatalogTupleDelete(rel, &tup->t_self);
@@ -1531,6 +1538,14 @@ RemovePublicationSchemaById(Oid psoid)
*/
schemaRels = GetSchemaPublicationRelations(pubsch->pnnspid,
PUBLICATION_PART_ALL);
+
+ /*
+ * Data loss due to concurrency issues are avoided by locking the relation
+ * in ShareRowExclusiveLock as described atop OpenTableList.
+ */
+ foreach_oid(schrelid, schemaRels)
+ LockRelationOid(schrelid, ShareRowExclusiveLock);
+
InvalidatePublicationRels(schemaRels);
CatalogTupleDelete(rel, &tup->t_self);
@@ -1542,8 +1557,14 @@ RemovePublicationSchemaById(Oid psoid)
/*
* Open relations specified by a PublicationTable 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)
@@ -1568,7 +1589,7 @@ OpenTableList(List *tables)
/* Allow query cancel in case this takes a long time */
CHECK_FOR_INTERRUPTS();
- rel = table_openrv(t->relation, ShareUpdateExclusiveLock);
+ rel = table_openrv(t->relation, ShareRowExclusiveLock);
myrelid = RelationGetRelid(rel);
/*
@@ -1594,7 +1615,7 @@ OpenTableList(List *tables)
errmsg("conflicting or redundant column lists for table \"%s\"",
RelationGetRelationName(rel))));
- table_close(rel, ShareUpdateExclusiveLock);
+ table_close(rel, ShareRowExclusiveLock);
continue;
}
@@ -1622,7 +1643,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 cb36ca7b16..04c75b7806 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,162 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', qq(DROP PUBLICATION pub1;));
+$node_subscriber->safe_psql('postgres', qq(DROP SUBSCRIPTION sub1;));
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SCHEMA sch4;
+ CREATE TABLE sch4.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SCHEMA sch4;
+ CREATE TABLE sch4.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+]);
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.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
+$background_psql3->query_until(qr//,
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch4, sch3;\n"
+);
+
+# Verify that the table addition is waiting to acquire a ShareRowExclusiveLock.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = 'tab_conc'::regclass AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ or die
+ "Timed out while waiting for alter publication tries to wait on ShareRowExclusiveLock";
+
+# Complete the transaction on the tables, so that ALTER PUBLICATION can proceed
+# further to acquire locks on the schema table.
+$background_psql1->query_safe(qq[COMMIT]);
+
+# Verify that ShareRowExclusiveLock is acquired on sch4.tab_conc for which
+# there is no on-going transaction.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = (SELECT oid FROM pg_class WHERE relname = 'tab_conc' AND relnamespace = 'sch4'::regnamespace) AND mode = 'ShareRowExclusiveLock' AND waitstart IS NULL;"
+ )
+ or die
+ "Timed out while waiting for alter publication tries to wait on ShareRowExclusiveLock";
+
+# Verify that the schema addition is waiting to acquire a ShareRowExclusiveLock
+# for the table tab_conc which has an on-going transaction.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 1 FROM pg_locks WHERE relation = (SELECT oid FROM pg_class WHERE relname = 'tab_conc' AND relnamespace = 'sch3'::regnamespace) AND mode = 'ShareRowExclusiveLock' AND waitstart IS NOT NULL;"
+ )
+ or die
+ "Timed out while waiting for alter publication tries to wait on ShareRowExclusiveLock";
+
+$background_psql2->query_safe(qq[COMMIT]);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
On Tue, 16 Jul 2024 at 11:59, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 9:29 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
One related comment: @@ -1219,8 +1219,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, oldrel = palloc(sizeof(PublicationRelInfo)); oldrel->whereClause = NULL; oldrel->columns = NIL; + + /* + * Data loss due to concurrency issues are avoided by locking + * the relation in ShareRowExclusiveLock as described atop + * OpenTableList. + */ oldrel->relation = table_open(oldrelid, - ShareUpdateExclusiveLock); + ShareRowExclusiveLock);Isn't it better to lock the required relations in RemovePublicationRelById()?
On my CentOS VM, the test file '100_bugs.pl' takes ~11s without a
patch and ~13.3s with a patch. So, 2 to 2.3s additional time for newly
added tests. It isn't worth adding this much extra time for one bug
fix. Can we combine table and schema tests into one single test and
avoid inheritance table tests as the code for those will mostly follow
the same path as a regular table?Yes, that is better. The attached v6 version patch has the changes for the same.
The patch also addresses the comments from [1].
Thanks, I don't see any noticeable difference in test timing with new
tests. I have slightly modified the comments in the attached diff
patch (please rename it to .patch).
BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.
--
With Regards,
Amit Kapila.
Attachments:
v6-topup-amit.patch.txttext/plain; charset=US-ASCII; name=v6-topup-amit.patch.txtDownload
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index a7c257a994..a274ec0f7e 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -680,8 +680,8 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
PUBLICATION_PART_ALL);
/*
- * Data loss due to concurrency issues are avoided by locking the relation
- * in ShareRowExclusiveLock as described atop OpenTableList.
+ * Lock the tables so that concurrent transactions don't miss replicating
+ * the changes. See comments atop OpenTableList for further details.
*/
foreach_oid(schrelid, schemaRels)
LockRelationOid(schrelid, ShareRowExclusiveLock);
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 9d9b5f6af9..95f83d5563 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -1467,8 +1467,8 @@ RemovePublicationRelById(Oid proid)
pubrel->prrelid);
/*
- * Data loss due to concurrency issues are avoided by locking the relation
- * in ShareRowExclusiveLock as described atop OpenTableList.
+ * Lock the tables to avoid concurrent transactions from replicating the
+ * changes. See comments atop OpenTableList for further details.
*/
foreach_oid(relid, relids)
LockRelationOid(relid, ShareRowExclusiveLock);
@@ -1540,8 +1540,8 @@ RemovePublicationSchemaById(Oid psoid)
PUBLICATION_PART_ALL);
/*
- * Data loss due to concurrency issues are avoided by locking the relation
- * in ShareRowExclusiveLock as described atop OpenTableList.
+ * Lock the tables to avoid concurrent transactions from replicating the
+ * changes. See comments atop OpenTableList for further details.
*/
foreach_oid(schrelid, schemaRels)
LockRelationOid(schrelid, ShareRowExclusiveLock);
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
On Tue, 16 Jul 2024 at 11:59, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 9:29 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
One related comment: @@ -1219,8 +1219,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, oldrel = palloc(sizeof(PublicationRelInfo)); oldrel->whereClause = NULL; oldrel->columns = NIL; + + /* + * Data loss due to concurrency issues are avoided by locking + * the relation in ShareRowExclusiveLock as described atop + * OpenTableList. + */ oldrel->relation = table_open(oldrelid, - ShareUpdateExclusiveLock); + ShareRowExclusiveLock);Isn't it better to lock the required relations in RemovePublicationRelById()?
On my CentOS VM, the test file '100_bugs.pl' takes ~11s without a
patch and ~13.3s with a patch. So, 2 to 2.3s additional time for newly
added tests. It isn't worth adding this much extra time for one bug
fix. Can we combine table and schema tests into one single test and
avoid inheritance table tests as the code for those will mostly follow
the same path as a regular table?Yes, that is better. The attached v6 version patch has the changes for the same.
The patch also addresses the comments from [1].Thanks, I don't see any noticeable difference in test timing with new
tests. I have slightly modified the comments in the attached diff
patch (please rename it to .patch).BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.
I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1
The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;
Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.
However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.
We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);
Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.
I don't think locking all tables is a viable solution in this case, as
it would require asking the user to refrain from performing any
operations on any of the tables in the database while creating a
publication.
Thoughts?
Regards,
Vignesh
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
On Tue, 16 Jul 2024 at 11:59, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 9:29 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
One related comment: @@ -1219,8 +1219,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, oldrel = palloc(sizeof(PublicationRelInfo)); oldrel->whereClause = NULL; oldrel->columns = NIL; + + /* + * Data loss due to concurrency issues are avoided by locking + * the relation in ShareRowExclusiveLock as described atop + * OpenTableList. + */ oldrel->relation = table_open(oldrelid, - ShareUpdateExclusiveLock); + ShareRowExclusiveLock);Isn't it better to lock the required relations in RemovePublicationRelById()?
On my CentOS VM, the test file '100_bugs.pl' takes ~11s without a
patch and ~13.3s with a patch. So, 2 to 2.3s additional time for newly
added tests. It isn't worth adding this much extra time for one bug
fix. Can we combine table and schema tests into one single test and
avoid inheritance table tests as the code for those will mostly follow
the same path as a regular table?Yes, that is better. The attached v6 version patch has the changes for the same.
The patch also addresses the comments from [1].Thanks, I don't see any noticeable difference in test timing with new
tests. I have slightly modified the comments in the attached diff
patch (please rename it to .patch).BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.
Hi,
I tried the 'DROP PUBLICATION' command even for a publication with a
single table. And there also the data continues to get replicated.
To test this, I did a similar experiment as the above but instead of
creating publication on all tables, I did it for one specific table.
Here are the steps :
1. Create table test_1 and test_2 on both the publisher and subscriber
instances.
2. Create publication p for table test_1 on the publisher.
3. Create a subscription s which subscribes to p.
4. On the publisher
4a) Session 1 : BEGIN; INSERT INTO test_1 VALUES(val1);
4b) Session 2 : DROP PUBLICATION p;
4c) Session 1 : Commit;
5. On the publisher : INSERT INTO test_1 VALUES(val2);
After these, when I check the subscriber, both val1 and val2 have been
replicated. I tried a few more inserts on publisher after this and
they all got replicated to the subscriber. Only after explicitly
creating a new publication p2 for test_1 on the publisher, the
replication stopped. Most likely because the create publication
command invalidated the cache.
My guess is that this issue probably comes from the fact that
RemoveObjects in dropcmds.c doesn't do any special handling or
invalidation for the object drop command.
Please let me know if I'm missing something in my setup or if my
understanding of the drop commands is wrong.
Thanks
On Thu, Jul 18, 2024 at 3:05 PM Nitin Motiani <nitinmotiani@google.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.Hi,
I tried the 'DROP PUBLICATION' command even for a publication with a
single table. And there also the data continues to get replicated.To test this, I did a similar experiment as the above but instead of
creating publication on all tables, I did it for one specific table.Here are the steps :
1. Create table test_1 and test_2 on both the publisher and subscriber
instances.
2. Create publication p for table test_1 on the publisher.
3. Create a subscription s which subscribes to p.
4. On the publisher
4a) Session 1 : BEGIN; INSERT INTO test_1 VALUES(val1);
4b) Session 2 : DROP PUBLICATION p;
4c) Session 1 : Commit;
5. On the publisher : INSERT INTO test_1 VALUES(val2);After these, when I check the subscriber, both val1 and val2 have been
replicated. I tried a few more inserts on publisher after this and
they all got replicated to the subscriber. Only after explicitly
creating a new publication p2 for test_1 on the publisher, the
replication stopped. Most likely because the create publication
command invalidated the cache.My guess is that this issue probably comes from the fact that
RemoveObjects in dropcmds.c doesn't do any special handling or
invalidation for the object drop command.
I checked further and I see that RemovePublicationById does do cache
invalidation but it is only done in the scenario when the publication
is on all tables. This is done without taking any locks. But for the
other cases (eg. publication on one table), I don't see any cache
invalidation in RemovePublicationById. That would explain why the
replication kept happening for multiple transactions after the drop
publication command in my example..
Thanks & Regards
Nitin Motiani
Google
On Thu, Jul 18, 2024 at 3:25 PM Nitin Motiani <nitinmotiani@google.com> wrote:
On Thu, Jul 18, 2024 at 3:05 PM Nitin Motiani <nitinmotiani@google.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.Hi,
I tried the 'DROP PUBLICATION' command even for a publication with a
single table. And there also the data continues to get replicated.To test this, I did a similar experiment as the above but instead of
creating publication on all tables, I did it for one specific table.Here are the steps :
1. Create table test_1 and test_2 on both the publisher and subscriber
instances.
2. Create publication p for table test_1 on the publisher.
3. Create a subscription s which subscribes to p.
4. On the publisher
4a) Session 1 : BEGIN; INSERT INTO test_1 VALUES(val1);
4b) Session 2 : DROP PUBLICATION p;
4c) Session 1 : Commit;
5. On the publisher : INSERT INTO test_1 VALUES(val2);After these, when I check the subscriber, both val1 and val2 have been
replicated. I tried a few more inserts on publisher after this and
they all got replicated to the subscriber. Only after explicitly
creating a new publication p2 for test_1 on the publisher, the
replication stopped. Most likely because the create publication
command invalidated the cache.My guess is that this issue probably comes from the fact that
RemoveObjects in dropcmds.c doesn't do any special handling or
invalidation for the object drop command.I checked further and I see that RemovePublicationById does do cache
invalidation but it is only done in the scenario when the publication
is on all tables. This is done without taking any locks. But for the
other cases (eg. publication on one table), I don't see any cache
invalidation in RemovePublicationById. That would explain why the
replication kept happening for multiple transactions after the drop
publication command in my example..
Sorry, I missed that for the individual table scenario, the
invalidation would happen in RemovePublicationRelById. That is
invalidating the cache for all relids. But this is also not taking any
locks. So that would explain why dropping the publication on a single
table doesn't invalidate the cache in an ongoing transaction. I'm not
sure why the replication kept happening even in subsequent
transactions.
Either way I think the SRE lock should be taken for all relids in that
function also before the invalidations.
Thanks & Regards
Nitin Motiani
Google
On Thu, Jul 18, 2024 at 3:30 PM Nitin Motiani <nitinmotiani@google.com> wrote:
On Thu, Jul 18, 2024 at 3:25 PM Nitin Motiani <nitinmotiani@google.com> wrote:
On Thu, Jul 18, 2024 at 3:05 PM Nitin Motiani <nitinmotiani@google.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.Hi,
I tried the 'DROP PUBLICATION' command even for a publication with a
single table. And there also the data continues to get replicated.To test this, I did a similar experiment as the above but instead of
creating publication on all tables, I did it for one specific table.Here are the steps :
1. Create table test_1 and test_2 on both the publisher and subscriber
instances.
2. Create publication p for table test_1 on the publisher.
3. Create a subscription s which subscribes to p.
4. On the publisher
4a) Session 1 : BEGIN; INSERT INTO test_1 VALUES(val1);
4b) Session 2 : DROP PUBLICATION p;
4c) Session 1 : Commit;
5. On the publisher : INSERT INTO test_1 VALUES(val2);After these, when I check the subscriber, both val1 and val2 have been
replicated. I tried a few more inserts on publisher after this and
they all got replicated to the subscriber. Only after explicitly
creating a new publication p2 for test_1 on the publisher, the
replication stopped. Most likely because the create publication
command invalidated the cache.My guess is that this issue probably comes from the fact that
RemoveObjects in dropcmds.c doesn't do any special handling or
invalidation for the object drop command.I checked further and I see that RemovePublicationById does do cache
invalidation but it is only done in the scenario when the publication
is on all tables. This is done without taking any locks. But for the
other cases (eg. publication on one table), I don't see any cache
invalidation in RemovePublicationById. That would explain why the
replication kept happening for multiple transactions after the drop
publication command in my example..Sorry, I missed that for the individual table scenario, the
invalidation would happen in RemovePublicationRelById. That is
invalidating the cache for all relids. But this is also not taking any
locks. So that would explain why dropping the publication on a single
table doesn't invalidate the cache in an ongoing transaction. I'm not
sure why the replication kept happening even in subsequent
transactions.Either way I think the SRE lock should be taken for all relids in that
function also before the invalidations.
My apologies. I wasn't testing with the latest patch. I see this has
already been done in the v6 patch file.
Thanks & Regards
Nitin Motiani
Google
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.I don't think locking all tables is a viable solution in this case, as
it would require asking the user to refrain from performing any
operations on any of the tables in the database while creating a
publication.
Indeed, locking all tables in the database to prevent concurrent DMLs
for this scenario also looks odd to me. The other alternative
previously suggested by Andres is to distribute catalog modifying
transactions to all concurrent in-progress transactions [1]/messages/by-id/20231118025445.crhaeeuvoe2g5dv6@awork3.anarazel.de but as
mentioned this could add an overhead. One possibility to reduce
overhead is that we selectively distribute invalidations for
catalogs-related publications but I haven't analyzed the feasibility.
We need more opinions to decide here, so let me summarize the problem
and solutions discussed. As explained with an example in an email [1]/messages/by-id/20231118025445.crhaeeuvoe2g5dv6@awork3.anarazel.de,
the problem related to logical decoding is that it doesn't process
invalidations corresponding to DDLs for the already in-progress
transactions. We discussed preventing DMLs in the first place when
concurrent DDLs like ALTER PUBLICATION ... ADD TABLE ... are in
progress. The solution discussed was to acquire
ShareUpdateExclusiveLock for all the tables being added via such
commands. Further analysis revealed that the same handling is required
for ALTER PUBLICATION ... ADD TABLES IN SCHEMA which means locking all
the tables in the specified schemas. Then DROP PUBLICATION also seems
to have similar symptoms which means in the worst case (where
publication is for ALL TABLES) we have to lock all the tables in the
database. We are not sure if that is good so the other alternative we
can pursue is to distribute invalidations in logical decoding
infrastructure [1]/messages/by-id/20231118025445.crhaeeuvoe2g5dv6@awork3.anarazel.de which has its downsides.
Thoughts?
[1]: /messages/by-id/20231118025445.crhaeeuvoe2g5dv6@awork3.anarazel.de
--
With Regards,
Amit Kapila.
On Wed, Jul 24, 2024 at 9:53 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.I don't think locking all tables is a viable solution in this case, as
it would require asking the user to refrain from performing any
operations on any of the tables in the database while creating a
publication.Indeed, locking all tables in the database to prevent concurrent DMLs
for this scenario also looks odd to me. The other alternative
previously suggested by Andres is to distribute catalog modifying
transactions to all concurrent in-progress transactions [1] but as
mentioned this could add an overhead. One possibility to reduce
overhead is that we selectively distribute invalidations for
catalogs-related publications but I haven't analyzed the feasibility.We need more opinions to decide here, so let me summarize the problem
and solutions discussed. As explained with an example in an email [1],
the problem related to logical decoding is that it doesn't process
invalidations corresponding to DDLs for the already in-progress
transactions. We discussed preventing DMLs in the first place when
concurrent DDLs like ALTER PUBLICATION ... ADD TABLE ... are in
progress. The solution discussed was to acquire
ShareUpdateExclusiveLock for all the tables being added via such
commands. Further analysis revealed that the same handling is required
for ALTER PUBLICATION ... ADD TABLES IN SCHEMA which means locking all
the tables in the specified schemas. Then DROP PUBLICATION also seems
to have similar symptoms which means in the worst case (where
publication is for ALL TABLES) we have to lock all the tables in the
database. We are not sure if that is good so the other alternative we
can pursue is to distribute invalidations in logical decoding
infrastructure [1] which has its downsides.Thoughts?
Thank you for summarizing the problem and solutions!
I think it's worth trying the idea of distributing invalidation
messages, and we will see if there could be overheads or any further
obstacles. IIUC this approach would resolve another issue we discussed
before too[1]/messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com.
Regards,
[1]: /messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
On Wed, Jul 31, 2024 at 3:27 AM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Wed, Jul 24, 2024 at 9:53 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.I don't think locking all tables is a viable solution in this case, as
it would require asking the user to refrain from performing any
operations on any of the tables in the database while creating a
publication.Indeed, locking all tables in the database to prevent concurrent DMLs
for this scenario also looks odd to me. The other alternative
previously suggested by Andres is to distribute catalog modifying
transactions to all concurrent in-progress transactions [1] but as
mentioned this could add an overhead. One possibility to reduce
overhead is that we selectively distribute invalidations for
catalogs-related publications but I haven't analyzed the feasibility.We need more opinions to decide here, so let me summarize the problem
and solutions discussed. As explained with an example in an email [1],
the problem related to logical decoding is that it doesn't process
invalidations corresponding to DDLs for the already in-progress
transactions. We discussed preventing DMLs in the first place when
concurrent DDLs like ALTER PUBLICATION ... ADD TABLE ... are in
progress. The solution discussed was to acquire
ShareUpdateExclusiveLock for all the tables being added via such
commands. Further analysis revealed that the same handling is required
for ALTER PUBLICATION ... ADD TABLES IN SCHEMA which means locking all
the tables in the specified schemas. Then DROP PUBLICATION also seems
to have similar symptoms which means in the worst case (where
publication is for ALL TABLES) we have to lock all the tables in the
database. We are not sure if that is good so the other alternative we
can pursue is to distribute invalidations in logical decoding
infrastructure [1] which has its downsides.Thoughts?
Thank you for summarizing the problem and solutions!
I think it's worth trying the idea of distributing invalidation
messages, and we will see if there could be overheads or any further
obstacles. IIUC this approach would resolve another issue we discussed
before too[1].
Yes, and we also discussed having a similar solution at the time when
that problem was reported. So, it is clear that even though locking
tables can work for commands alter ALTER PUBLICATION ... ADD TABLE
..., we need a solution for distributing invalidations to the
in-progress transactions during logical decoding for other cases as
reported by you previously.
Thanks for looking into this.
--
With Regards,
Amit Kapila.
On Wed, 31 Jul 2024 at 09:36, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 31, 2024 at 3:27 AM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Wed, Jul 24, 2024 at 9:53 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.I don't think locking all tables is a viable solution in this case, as
it would require asking the user to refrain from performing any
operations on any of the tables in the database while creating a
publication.Indeed, locking all tables in the database to prevent concurrent DMLs
for this scenario also looks odd to me. The other alternative
previously suggested by Andres is to distribute catalog modifying
transactions to all concurrent in-progress transactions [1] but as
mentioned this could add an overhead. One possibility to reduce
overhead is that we selectively distribute invalidations for
catalogs-related publications but I haven't analyzed the feasibility.We need more opinions to decide here, so let me summarize the problem
and solutions discussed. As explained with an example in an email [1],
the problem related to logical decoding is that it doesn't process
invalidations corresponding to DDLs for the already in-progress
transactions. We discussed preventing DMLs in the first place when
concurrent DDLs like ALTER PUBLICATION ... ADD TABLE ... are in
progress. The solution discussed was to acquire
ShareUpdateExclusiveLock for all the tables being added via such
commands. Further analysis revealed that the same handling is required
for ALTER PUBLICATION ... ADD TABLES IN SCHEMA which means locking all
the tables in the specified schemas. Then DROP PUBLICATION also seems
to have similar symptoms which means in the worst case (where
publication is for ALL TABLES) we have to lock all the tables in the
database. We are not sure if that is good so the other alternative we
can pursue is to distribute invalidations in logical decoding
infrastructure [1] which has its downsides.Thoughts?
Thank you for summarizing the problem and solutions!
I think it's worth trying the idea of distributing invalidation
messages, and we will see if there could be overheads or any further
obstacles. IIUC this approach would resolve another issue we discussed
before too[1].Yes, and we also discussed having a similar solution at the time when
that problem was reported. So, it is clear that even though locking
tables can work for commands alter ALTER PUBLICATION ... ADD TABLE
..., we need a solution for distributing invalidations to the
in-progress transactions during logical decoding for other cases as
reported by you previously.Thanks for looking into this.
Thanks, I am working on to implement a solution for distributing
invalidations. Will share a patch for the same.
Thanks and Regards,
Shlok Kyal
On Wed, 31 Jul 2024 at 11:17, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Wed, 31 Jul 2024 at 09:36, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 31, 2024 at 3:27 AM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Wed, Jul 24, 2024 at 9:53 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.I don't think locking all tables is a viable solution in this case, as
it would require asking the user to refrain from performing any
operations on any of the tables in the database while creating a
publication.Indeed, locking all tables in the database to prevent concurrent DMLs
for this scenario also looks odd to me. The other alternative
previously suggested by Andres is to distribute catalog modifying
transactions to all concurrent in-progress transactions [1] but as
mentioned this could add an overhead. One possibility to reduce
overhead is that we selectively distribute invalidations for
catalogs-related publications but I haven't analyzed the feasibility.We need more opinions to decide here, so let me summarize the problem
and solutions discussed. As explained with an example in an email [1],
the problem related to logical decoding is that it doesn't process
invalidations corresponding to DDLs for the already in-progress
transactions. We discussed preventing DMLs in the first place when
concurrent DDLs like ALTER PUBLICATION ... ADD TABLE ... are in
progress. The solution discussed was to acquire
ShareUpdateExclusiveLock for all the tables being added via such
commands. Further analysis revealed that the same handling is required
for ALTER PUBLICATION ... ADD TABLES IN SCHEMA which means locking all
the tables in the specified schemas. Then DROP PUBLICATION also seems
to have similar symptoms which means in the worst case (where
publication is for ALL TABLES) we have to lock all the tables in the
database. We are not sure if that is good so the other alternative we
can pursue is to distribute invalidations in logical decoding
infrastructure [1] which has its downsides.Thoughts?
Thank you for summarizing the problem and solutions!
I think it's worth trying the idea of distributing invalidation
messages, and we will see if there could be overheads or any further
obstacles. IIUC this approach would resolve another issue we discussed
before too[1].Yes, and we also discussed having a similar solution at the time when
that problem was reported. So, it is clear that even though locking
tables can work for commands alter ALTER PUBLICATION ... ADD TABLE
..., we need a solution for distributing invalidations to the
in-progress transactions during logical decoding for other cases as
reported by you previously.Thanks for looking into this.
Thanks, I am working on to implement a solution for distributing
invalidations. Will share a patch for the same.
Created a patch for distributing invalidations.
Here we collect the invalidation messages for the current transaction
and distribute it to all the inprogress transactions, whenever we are
distributing the snapshots..Thoughts?
Thanks and Regards,
Shlok Kyal
Attachments:
v7-0001-Distribute-invalidatons-if-change-in-catalog-tabl.patchapplication/octet-stream; name=v7-0001-Distribute-invalidatons-if-change-in-catalog-tabl.patchDownload
From 1f154f4d84a413a6fa490b28c6f2a67a8a697647 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 1 Aug 2024 12:16:24 +0530
Subject: [PATCH v7] Distribute invalidatons if change in catalog tables
Distribute invalidations to inprogress transactions if the current
committed transaction change any catalog table.
---
.../replication/logical/reorderbuffer.c | 36 +++++
src/backend/replication/logical/snapbuild.c | 26 +++-
src/include/replication/reorderbuffer.h | 7 +
src/test/subscription/t/100_bugs.pl | 131 ++++++++++++++++++
4 files changed, 197 insertions(+), 3 deletions(-)
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 00a8327e77..28694370ea 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -5308,3 +5308,39 @@ restart:
*cmax = ent->cmax;
return true;
}
+
+/*
+ * Get a list of invalidation messages in current committed transaction
+ */
+List *
+GetInvalidationMsg(ReorderBuffer *rb, XLogRecPtr lsn, TransactionId xid)
+{
+ List *invalmsgs = NIL;
+ ReorderBufferTXN *txn;
+ ReorderBufferIterTXNState *volatile iterstate = NULL;
+ ReorderBufferChange *change;
+
+ txn = ReorderBufferTXNByXid(rb, xid, false, NULL, InvalidXLogRecPtr, false);
+ ReorderBufferIterTXNInit(rb, txn, &iterstate);
+
+ while ((change = ReorderBufferIterTXNNext(rb, iterstate)) != NULL)
+ {
+ if (change->action == REORDER_BUFFER_CHANGE_INVALIDATION)
+ {
+ InvalidationMsg *invalmsg = (InvalidationMsg *) palloc(sizeof(InvalidationMsg));
+
+ invalmsg->nmsgs = change->data.inval.ninvalidations;
+ invalmsg->msgs = (SharedInvalidationMessage *) palloc(sizeof(SharedInvalidationMessage) * invalmsg->nmsgs);
+ memcpy(invalmsg->msgs, change->data.inval.invalidations, sizeof(SharedInvalidationMessage) * invalmsg->nmsgs);
+
+ invalmsgs = lappend(invalmsgs, invalmsg);
+ }
+ }
+
+ /* clean up the iterator */
+
+ ReorderBufferIterTXNFinish(rb, iterstate);
+ iterstate = NULL;
+
+ return invalmsgs;
+}
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index ae676145e6..d79b380699 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -300,7 +300,7 @@ static void SnapBuildFreeSnapshot(Snapshot snap);
static void SnapBuildSnapIncRefcount(Snapshot snap);
-static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn);
+static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid, List *invalmsgs);
static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
uint32 xinfo);
@@ -867,7 +867,7 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
* contents).
*/
static void
-SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
+SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid, List *invalidmsgs)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
@@ -913,6 +913,19 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
SnapBuildSnapIncRefcount(builder->snapshot);
ReorderBufferAddSnapshot(builder->reorder, txn->xid, lsn,
builder->snapshot);
+
+ /*
+ * Add invalidation messages to the reorder buffer of inprogress
+ * transactions except the current committed transaction
+ */
+ if (txn->xid != xid)
+ {
+ foreach_ptr(InvalidationMsg, invalmsg, invalidmsgs)
+ {
+ ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
+ invalmsg->nmsgs, invalmsg->msgs);
+ }
+ }
}
}
@@ -1156,6 +1169,8 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* if there's any reason to build a historic snapshot, do so now */
if (needs_snapshot)
{
+ List *invalmsgs;
+
/*
* If we haven't built a complete snapshot yet there's no need to hand
* it out, it wouldn't (and couldn't) be used anyway.
@@ -1184,8 +1199,13 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* refcount of the snapshot builder for the new snapshot */
SnapBuildSnapIncRefcount(builder->snapshot);
+ /* get invalidation messages from reorder buffer */
+ invalmsgs = GetInvalidationMsg(builder->reorder, lsn, xid);
+
/* add a new catalog snapshot to all currently running transactions */
- SnapBuildDistributeNewCatalogSnapshot(builder, lsn);
+ SnapBuildDistributeNewCatalogSnapshot(builder, lsn, xid, invalmsgs);
+
+ list_free_deep(invalmsgs);
}
}
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index 851a001c8b..7e2d5d9661 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -664,6 +664,11 @@ struct ReorderBuffer
int64 totalBytes; /* total amount of data decoded */
};
+typedef struct InvalidationMsg
+{
+ uint32 nmsgs;
+ SharedInvalidationMessage *msgs;
+} InvalidationMsg;
extern ReorderBuffer *ReorderBufferAllocate(void);
extern void ReorderBufferFree(ReorderBuffer *rb);
@@ -740,4 +745,6 @@ extern void ReorderBufferSetRestartPoint(ReorderBuffer *rb, XLogRecPtr ptr);
extern void StartupReorderBuffer(void);
+extern List *GetInvalidationMsg(ReorderBuffer *rb, XLogRecPtr lsn, TransactionId xid);
+
#endif
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..82497c9d11 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,137 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', qq(DROP PUBLICATION pub1;));
+$node_subscriber->safe_psql('postgres', qq(DROP SUBSCRIPTION sub1;));
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SCHEMA sch4;
+ CREATE TABLE sch4.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SCHEMA sch4;
+ CREATE TABLE sch4.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+]);
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.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
+$background_psql3->query_until(qr//,
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch4, sch3;\n"
+);
+
+# Complete the transaction on the tables, so that ALTER PUBLICATION can proceed
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
On Thu, 8 Aug 2024 at 16:24, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Wed, 31 Jul 2024 at 11:17, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Wed, 31 Jul 2024 at 09:36, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 31, 2024 at 3:27 AM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Wed, Jul 24, 2024 at 9:53 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.I don't think locking all tables is a viable solution in this case, as
it would require asking the user to refrain from performing any
operations on any of the tables in the database while creating a
publication.Indeed, locking all tables in the database to prevent concurrent DMLs
for this scenario also looks odd to me. The other alternative
previously suggested by Andres is to distribute catalog modifying
transactions to all concurrent in-progress transactions [1] but as
mentioned this could add an overhead. One possibility to reduce
overhead is that we selectively distribute invalidations for
catalogs-related publications but I haven't analyzed the feasibility.We need more opinions to decide here, so let me summarize the problem
and solutions discussed. As explained with an example in an email [1],
the problem related to logical decoding is that it doesn't process
invalidations corresponding to DDLs for the already in-progress
transactions. We discussed preventing DMLs in the first place when
concurrent DDLs like ALTER PUBLICATION ... ADD TABLE ... are in
progress. The solution discussed was to acquire
ShareUpdateExclusiveLock for all the tables being added via such
commands. Further analysis revealed that the same handling is required
for ALTER PUBLICATION ... ADD TABLES IN SCHEMA which means locking all
the tables in the specified schemas. Then DROP PUBLICATION also seems
to have similar symptoms which means in the worst case (where
publication is for ALL TABLES) we have to lock all the tables in the
database. We are not sure if that is good so the other alternative we
can pursue is to distribute invalidations in logical decoding
infrastructure [1] which has its downsides.Thoughts?
Thank you for summarizing the problem and solutions!
I think it's worth trying the idea of distributing invalidation
messages, and we will see if there could be overheads or any further
obstacles. IIUC this approach would resolve another issue we discussed
before too[1].Yes, and we also discussed having a similar solution at the time when
that problem was reported. So, it is clear that even though locking
tables can work for commands alter ALTER PUBLICATION ... ADD TABLE
..., we need a solution for distributing invalidations to the
in-progress transactions during logical decoding for other cases as
reported by you previously.Thanks for looking into this.
Thanks, I am working on to implement a solution for distributing
invalidations. Will share a patch for the same.Created a patch for distributing invalidations.
Here we collect the invalidation messages for the current transaction
and distribute it to all the inprogress transactions, whenever we are
distributing the snapshots..Thoughts?
In the v7 patch, I am looping through the reorder buffer of the
current committed transaction and storing all invalidation messages in
a list. Then I am distributing those invalidations.
But I found that for a transaction we already store all the
invalidation messages (see [1]https://github.com/postgres/postgres/blob/7da1bdc2c2f17038f2ae1900be90a0d7b5e361e0/src/include/replication/reorderbuffer.h#L384). So we don't need to loop through the
reorder buffer and store the invalidations.
I have modified the patch accordingly and attached the same.
Thanks and Regards,
Shlok Kyal
Attachments:
v8-0001-Distribute-invalidatons-if-change-in-catalog-tabl.patchapplication/octet-stream; name=v8-0001-Distribute-invalidatons-if-change-in-catalog-tabl.patchDownload
From 1e08c53164bc37736b5cdb87f367215cdbfbae84 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Thu, 1 Aug 2024 12:16:24 +0530
Subject: [PATCH v8] Distribute invalidatons if change in catalog tables
Distribute invalidations to inprogress transactions if the current
committed transaction change any catalog table.
---
.../replication/logical/reorderbuffer.c | 5 +-
src/backend/replication/logical/snapbuild.c | 34 +++--
src/include/replication/reorderbuffer.h | 4 +
src/test/subscription/t/100_bugs.pl | 131 ++++++++++++++++++
4 files changed, 160 insertions(+), 14 deletions(-)
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 00a8327e77..2028e081e3 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -221,9 +221,6 @@ int debug_logical_replication_streaming = DEBUG_LOGICAL_REP_STREAMING_BUFFERED
*/
static ReorderBufferTXN *ReorderBufferGetTXN(ReorderBuffer *rb);
static void ReorderBufferReturnTXN(ReorderBuffer *rb, ReorderBufferTXN *txn);
-static ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
- TransactionId xid, bool create, bool *is_new,
- XLogRecPtr lsn, bool create_as_top);
static void ReorderBufferTransferSnapToParent(ReorderBufferTXN *txn,
ReorderBufferTXN *subtxn);
@@ -619,7 +616,7 @@ ReorderBufferReturnRelids(ReorderBuffer *rb, Oid *relids)
* (with the given LSN, and as top transaction if that's specified);
* when this happens, is_new is set to true.
*/
-static ReorderBufferTXN *
+ReorderBufferTXN *
ReorderBufferTXNByXid(ReorderBuffer *rb, TransactionId xid, bool create,
bool *is_new, XLogRecPtr lsn, bool create_as_top)
{
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index ae676145e6..fa8871d3d0 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -300,7 +300,7 @@ static void SnapBuildFreeSnapshot(Snapshot snap);
static void SnapBuildSnapIncRefcount(Snapshot snap);
-static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn);
+static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
uint32 xinfo);
@@ -859,18 +859,21 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
}
/*
- * Add a new Snapshot to all transactions we're decoding that currently are
- * in-progress so they can see new catalog contents made by the transaction
- * that just committed. This is necessary because those in-progress
- * transactions will use the new catalog's contents from here on (at the very
- * least everything they do needs to be compatible with newer catalog
- * contents).
+ * Add a new Snapshot and invalidation messages to all transactions we're
+ * decoding that currently are in-progress so they can see new catalog contents
+ * made by the transaction that just committed. This is necessary because those
+ * in-progress transactions will use the new catalog's contents from here on
+ * (at the very least everything they do needs to be compatible with newer
+ * catalog contents).
*/
static void
-SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
+SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
+ ReorderBufferTXN *curr_txn;
+
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -913,6 +916,14 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
SnapBuildSnapIncRefcount(builder->snapshot);
ReorderBufferAddSnapshot(builder->reorder, txn->xid, lsn,
builder->snapshot);
+
+ /*
+ * Add invalidation messages to the reorder buffer of inprogress
+ * transactions except the current committed transaction
+ */
+ if (txn->xid != xid && curr_txn->ninvalidations > 0)
+ ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
+ curr_txn->ninvalidations, curr_txn->invalidations);
}
}
@@ -1184,8 +1195,11 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* refcount of the snapshot builder for the new snapshot */
SnapBuildSnapIncRefcount(builder->snapshot);
- /* add a new catalog snapshot to all currently running transactions */
- SnapBuildDistributeNewCatalogSnapshot(builder, lsn);
+ /*
+ * add a new catalog snapshot and invalidations messages to all
+ * currently running transactions
+ */
+ SnapBuildDistributeNewCatalogSnapshot(builder, lsn, xid);
}
}
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index 851a001c8b..95eda20128 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -738,6 +738,10 @@ extern TransactionId *ReorderBufferGetCatalogChangesXacts(ReorderBuffer *rb);
extern void ReorderBufferSetRestartPoint(ReorderBuffer *rb, XLogRecPtr ptr);
+extern ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
+ TransactionId xid, bool create, bool *is_new,
+ XLogRecPtr lsn, bool create_as_top);
+
extern void StartupReorderBuffer(void);
#endif
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..82497c9d11 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,137 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', qq(DROP PUBLICATION pub1;));
+$node_subscriber->safe_psql('postgres', qq(DROP SUBSCRIPTION sub1;));
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SCHEMA sch4;
+ CREATE TABLE sch4.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SCHEMA sch4;
+ CREATE TABLE sch4.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+]);
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.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
+$background_psql3->query_until(qr//,
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch4, sch3;\n"
+);
+
+# Complete the transaction on the tables, so that ALTER PUBLICATION can proceed
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
On Thu, 8 Aug 2024 at 16:24, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Wed, 31 Jul 2024 at 11:17, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Wed, 31 Jul 2024 at 09:36, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 31, 2024 at 3:27 AM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Wed, Jul 24, 2024 at 9:53 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.I don't think locking all tables is a viable solution in this case, as
it would require asking the user to refrain from performing any
operations on any of the tables in the database while creating a
publication.Indeed, locking all tables in the database to prevent concurrent DMLs
for this scenario also looks odd to me. The other alternative
previously suggested by Andres is to distribute catalog modifying
transactions to all concurrent in-progress transactions [1] but as
mentioned this could add an overhead. One possibility to reduce
overhead is that we selectively distribute invalidations for
catalogs-related publications but I haven't analyzed the feasibility.We need more opinions to decide here, so let me summarize the problem
and solutions discussed. As explained with an example in an email [1],
the problem related to logical decoding is that it doesn't process
invalidations corresponding to DDLs for the already in-progress
transactions. We discussed preventing DMLs in the first place when
concurrent DDLs like ALTER PUBLICATION ... ADD TABLE ... are in
progress. The solution discussed was to acquire
ShareUpdateExclusiveLock for all the tables being added via such
commands. Further analysis revealed that the same handling is required
for ALTER PUBLICATION ... ADD TABLES IN SCHEMA which means locking all
the tables in the specified schemas. Then DROP PUBLICATION also seems
to have similar symptoms which means in the worst case (where
publication is for ALL TABLES) we have to lock all the tables in the
database. We are not sure if that is good so the other alternative we
can pursue is to distribute invalidations in logical decoding
infrastructure [1] which has its downsides.Thoughts?
Thank you for summarizing the problem and solutions!
I think it's worth trying the idea of distributing invalidation
messages, and we will see if there could be overheads or any further
obstacles. IIUC this approach would resolve another issue we discussed
before too[1].Yes, and we also discussed having a similar solution at the time when
that problem was reported. So, it is clear that even though locking
tables can work for commands alter ALTER PUBLICATION ... ADD TABLE
..., we need a solution for distributing invalidations to the
in-progress transactions during logical decoding for other cases as
reported by you previously.Thanks for looking into this.
Thanks, I am working on to implement a solution for distributing
invalidations. Will share a patch for the same.Created a patch for distributing invalidations.
Here we collect the invalidation messages for the current transaction
and distribute it to all the inprogress transactions, whenever we are
distributing the snapshots..Thoughts?
Since we are applying invalidations to all in-progress transactions,
the publisher will only replicate half of the transaction data up to
the point of invalidation, while the remaining half will not be
replicated.
Ex:
Session1:
BEGIN;
INSERT INTO tab_conc VALUES (1);
Session2:
ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc;
Session1:
INSERT INTO tab_conc VALUES (2);
INSERT INTO tab_conc VALUES (3);
COMMIT;
After the above the subscriber data looks like:
postgres=# select * from tab_conc ;
a
---
1
(1 row)
You can reproduce the issue using the attached test.
I'm not sure if this behavior is ok. At present, we’ve replicated the
first record within the same transaction, but the second and third
records are being skipped. Would it be better to apply invalidations
after the transaction is underway?
Thoughts?
Regards,
Vignesh
Attachments:
test_issue_reproduce.patchtext/x-patch; charset=US-ASCII; name=test_issue_reproduce.patchDownload
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..b5d9749bdf 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,145 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# cleanpup
+$node_publisher->safe_psql('postgres', qq(DROP PUBLICATION pub1;));
+$node_subscriber->safe_psql('postgres', qq(DROP SUBSCRIPTION sub1;));
+
+# =============================================================================
+# 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.
+# =============================================================================
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+
+));
+
+# 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_publisher->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_publisher->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 regress_pub1 ADD TABLE tab_conc;\n");
+
+# Complete the old transaction.
+$background_psql1->query_safe(qq[COMMIT]);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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'
+);
+
+# =============================================================================
+# This bug is present with ALTER PUBLICATION ... DROP TABLE.
+# =============================================================================
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (4);
+]);
+
+# wait for WAL to be generated
+sleep(1);
+
+# This operation will wait because there is an open transaction holding a lock.
+$background_psql2->query_until(qr//,
+ "ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc;\n");
+
+# wait for WAL to be generated
+sleep(1);
+
+# Complete the old transaction.
+$background_psql1->query_safe(
+ qq[
+ INSERT INTO tab_conc VALUES (5);
+ INSERT INTO tab_conc VALUES (6);
+ COMMIT;
+]);
+#$background_psql1->query_safe(qq[COMMIT]);
+$background_psql1->quit;
+
+# Wait till the tables are dropped from the publication.
+$node_publisher->poll_query_until('postgres',
+ "SELECT COUNT(1) = 0 FROM pg_publication_rel WHERE prrelid IN ('tab_conc'::regclass);"
+ )
+ or die
+ "Timed out while waiting for alter publication to add the table to the publication";
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert before drop table is replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4
+5
+6),
+ 'Verify that the incremental data for table tab_conc before removing table from publication is replicated to the subscriber'
+);
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
On Thu, Aug 15, 2024 at 9:31 PM vignesh C <vignesh21@gmail.com> wrote:
On Thu, 8 Aug 2024 at 16:24, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Wed, 31 Jul 2024 at 11:17, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
Created a patch for distributing invalidations.
Here we collect the invalidation messages for the current transaction
and distribute it to all the inprogress transactions, whenever we are
distributing the snapshots..Thoughts?Since we are applying invalidations to all in-progress transactions,
the publisher will only replicate half of the transaction data up to
the point of invalidation, while the remaining half will not be
replicated.
Ex:
Session1:
BEGIN;
INSERT INTO tab_conc VALUES (1);Session2:
ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc;Session1:
INSERT INTO tab_conc VALUES (2);
INSERT INTO tab_conc VALUES (3);
COMMIT;After the above the subscriber data looks like:
postgres=# select * from tab_conc ;
a
---
1
(1 row)You can reproduce the issue using the attached test.
I'm not sure if this behavior is ok. At present, we’ve replicated the
first record within the same transaction, but the second and third
records are being skipped.
This can happen even without a concurrent DDL if some of the tables in
the database are part of the publication and others are not. In such a
case inserts for publicized tables will be replicated but other
inserts won't. Sending the partial data of the transaction isn't a
problem to me. Do you have any other concerns that I am missing?
Would it be better to apply invalidations
after the transaction is underway?
But that won't fix the problem reported by Sawada-san in an email [1]/messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com.
BTW, we should do some performance testing by having a mix of DML and
DDLs to see the performance impact of this patch.
[1]: /messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com
--
With Regards,
Amit Kapila.
On Tue, 20 Aug 2024 at 16:10, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Thu, Aug 15, 2024 at 9:31 PM vignesh C <vignesh21@gmail.com> wrote:
On Thu, 8 Aug 2024 at 16:24, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Wed, 31 Jul 2024 at 11:17, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
Created a patch for distributing invalidations.
Here we collect the invalidation messages for the current transaction
and distribute it to all the inprogress transactions, whenever we are
distributing the snapshots..Thoughts?Since we are applying invalidations to all in-progress transactions,
the publisher will only replicate half of the transaction data up to
the point of invalidation, while the remaining half will not be
replicated.
Ex:
Session1:
BEGIN;
INSERT INTO tab_conc VALUES (1);Session2:
ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc;Session1:
INSERT INTO tab_conc VALUES (2);
INSERT INTO tab_conc VALUES (3);
COMMIT;After the above the subscriber data looks like:
postgres=# select * from tab_conc ;
a
---
1
(1 row)You can reproduce the issue using the attached test.
I'm not sure if this behavior is ok. At present, we’ve replicated the
first record within the same transaction, but the second and third
records are being skipped.This can happen even without a concurrent DDL if some of the tables in
the database are part of the publication and others are not. In such a
case inserts for publicized tables will be replicated but other
inserts won't. Sending the partial data of the transaction isn't a
problem to me. Do you have any other concerns that I am missing?
My main concern was about sending only part of the data from a
transaction table and leaving out the rest. However, since this is
happening elsewhere as well, I'm okay with it.
Regards,
Vignesh
BTW, we should do some performance testing by having a mix of DML and
DDLs to see the performance impact of this patch.[1] - /messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com
I did some performance testing and I found some performance impact for
the following case:
1. Created a publisher, subscriber set up on a single table, say 'tab_conc1';
2. Created a second publisher, subscriber set on a single table say 'tp';
3. Created 'tcount' no. of tables. These tables are not part of any publication.
4. There are two sessions running in parallel, let's say S1 and S2.
5. Begin a transaction in S1.
6. Now in a loop (this loop runs 100 times):
S1: Insert a row in table 'tab_conc1'
S1: Insert a row in all 'tcount' tables.
S2: BEGIN; Alter publication for 2nd publication; COMMIT;
The current logic in the patch will call the function
'rel_sync_cache_publication_cb' during invalidation. This will
invalidate the cache for all the tables. So cache related to all the
tables i.e. table 'tab_conc1', 'tcount' tables will be invalidated.
7. COMMIT the transaction in S1.
The performance in this case is:
No. of tables | With patch (in ms) | With head (in ms)
-----------------------------------------------------------------------------
tcount = 100 | 101376.4 | 101357.8
tcount = 1000 | 994085.4 | 993471.4
For 100 tables the performance is slow by '0.018%' and for 1000 tables
performance is slow by '0.06%'.
These results are the average of 5 runs.
Other than this I tested the following cases but did not find any
performance impact:
1. with 'tcount = 10'. But I didn't find any performance impact.
2. with 'tcount = 0' and running the loop 1000 times. But I didn't
find any performance impact.
I have also attached the test script and the machine configurations on
which performance testing was done.
Next I am planning to test solely on the logical decoding side and
will share the results.
Thanks and Regards,
Shlok Kyal
On Fri, Aug 30, 2024 at 3:06 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
Next I am planning to test solely on the logical decoding side and
will share the results.
Thanks, the next set of proposed tests makes sense to me. It will also
be useful to generate some worst-case scenarios where the number of
invalidations is more to see the distribution cost in such cases. For
example, Truncate/Drop a table with 100 or 1000 partitions.
--
With Regards,
Amit Kapila.
On Tue, Aug 20, 2024 at 4:10 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Thu, Aug 15, 2024 at 9:31 PM vignesh C <vignesh21@gmail.com> wrote:
Since we are applying invalidations to all in-progress transactions,
the publisher will only replicate half of the transaction data up to
the point of invalidation, while the remaining half will not be
replicated.
Ex:
Session1:
BEGIN;
INSERT INTO tab_conc VALUES (1);Session2:
ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc;Session1:
INSERT INTO tab_conc VALUES (2);
INSERT INTO tab_conc VALUES (3);
COMMIT;After the above the subscriber data looks like:
postgres=# select * from tab_conc ;
a
---
1
(1 row)You can reproduce the issue using the attached test.
I'm not sure if this behavior is ok. At present, we’ve replicated the
first record within the same transaction, but the second and third
records are being skipped.This can happen even without a concurrent DDL if some of the tables in
the database are part of the publication and others are not. In such a
case inserts for publicized tables will be replicated but other
inserts won't. Sending the partial data of the transaction isn't a
problem to me. Do you have any other concerns that I am missing?
Hi,
I think that the partial data replication for one table is a bigger
issue than the case of data being sent for a subset of the tables in
the transaction. This can lead to inconsistent data if the same row is
updated multiple times or deleted in the same transaction. In such a
case if only the partial updates from the transaction are sent to the
subscriber, it might end up with the data which was never visible on
the publisher side.
Here is an example I tried with the patch v8-001 :
I created following 2 tables on the publisher and the subscriber :
CREATE TABLE delete_test(id int primary key, name varchar(100));
CREATE TABLE update_test(id int primary key, name varchar(100));
I added both the tables to the publication p on the publisher and
created a subscription s on the subscriber.
I run 2 sessions on the publisher and do the following :
Session 1 :
BEGIN;
INSERT INTO delete_test VALUES(0, 'Nitin');
Session 2 :
ALTER PUBLICATION p DROP TABLE delete_test;
Session 1 :
DELETE FROM delete_test WHERE id=0;
COMMIT;
After the commit there should be no new row created on the publisher.
But because the partial data was replicated, this is what the select
on the subscriber shows :
SELECT * FROM delete_test;
id | name
----+-----------
0 | Nitin
(1 row)
I don't think the above is a common use case. But this is still an
issue because the subscriber has the data which never existed on the
publisher.
Similar issue can be seen with an update command.
Session 1 :
BEGIN;
INSERT INTO update_test VALUES(1, 'Chiranjiv');
Session 2 :
ALTER PUBLICATION p DROP TABLE update_test;
Session 1:
UPDATE update_test SET name='Eeshan' where id=1;
COMMIT;
After the commit, this is the state on the publisher :
SELECT * FROM update_test;
1 | Eeshan
(1 row)
While this is the state on the subscriber :
SELECT * FROM update_test;
1 | Chiranjiv
(1 row)
I think the update during a transaction scenario might be more common
than deletion right after insertion. But both of these seem like real
issues to consider. Please let me know if I'm missing something.
Thanks & Regards
Nitin Motiani
Google
On Mon, Sep 2, 2024 at 9:19 PM Nitin Motiani <nitinmotiani@google.com> wrote:
I think that the partial data replication for one table is a bigger
issue than the case of data being sent for a subset of the tables in
the transaction. This can lead to inconsistent data if the same row is
updated multiple times or deleted in the same transaction. In such a
case if only the partial updates from the transaction are sent to the
subscriber, it might end up with the data which was never visible on
the publisher side.Here is an example I tried with the patch v8-001 :
I created following 2 tables on the publisher and the subscriber :
CREATE TABLE delete_test(id int primary key, name varchar(100));
CREATE TABLE update_test(id int primary key, name varchar(100));I added both the tables to the publication p on the publisher and
created a subscription s on the subscriber.I run 2 sessions on the publisher and do the following :
Session 1 :
BEGIN;
INSERT INTO delete_test VALUES(0, 'Nitin');Session 2 :
ALTER PUBLICATION p DROP TABLE delete_test;Session 1 :
DELETE FROM delete_test WHERE id=0;
COMMIT;After the commit there should be no new row created on the publisher.
But because the partial data was replicated, this is what the select
on the subscriber shows :SELECT * FROM delete_test;
id | name
----+-----------
0 | Nitin
(1 row)I don't think the above is a common use case. But this is still an
issue because the subscriber has the data which never existed on the
publisher.
I don't think that is the correct conclusion because the user has
intentionally avoided sending part of the transaction changes. This
can happen in various ways without the patch as well. For example, if
the user has performed the ALTER in the same transaction.
Publisher:
=========
BEGIN
postgres=*# Insert into delete_test values(0, 'Nitin');
INSERT 0 1
postgres=*# Alter Publication pub1 drop table delete_test;
ALTER PUBLICATION
postgres=*# Delete from delete_test where id=0;
DELETE 1
postgres=*# commit;
COMMIT
postgres=# select * from delete_test;
id | name
----+------
(0 rows)
Subscriber:
=========
postgres=# select * from delete_test;
id | name
----+-------
0 | Nitin
(1 row)
This can also happen when the user has published only 'inserts' but
not 'updates' or 'deletes'.
--
With Regards,
Amit Kapila.
On Mon, 2 Sept 2024 at 10:12, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Fri, Aug 30, 2024 at 3:06 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
Next I am planning to test solely on the logical decoding side and
will share the results.Thanks, the next set of proposed tests makes sense to me. It will also
be useful to generate some worst-case scenarios where the number of
invalidations is more to see the distribution cost in such cases. For
example, Truncate/Drop a table with 100 or 1000 partitions.--
With Regards,
Amit Kapila.
Hi,
I did some performance testing solely on the logical decoding side and
found some degradation in performance, for the following testcase:
1. Created a publisher on a single table, say 'tab_conc1';
2. Created a second publisher on a single table say 'tp';
4. two sessions are running in parallel, let's say S1 and S2.
5. Begin a transaction in S1.
6. Now in a loop (this loop runs 'count' times):
S1: Insert a row in table 'tab_conc1'
S2: BEGIN; Alter publication DROP/ ADD tp; COMMIT
7. COMMIT the transaction in S1.
8. run 'pg_logical_slot_get_binary_changes' to get the decoding changes.
Observation:
With fix a new entry is added in decoding. During debugging I found
that this entry only comes when we do a 'INSERT' in Session 1 after we
do 'ALTER PUBLICATION' in another session in parallel (or we can say
due to invalidation). Also, I observed that this new entry is related
to sending replica identity, attributes,etc as function
'logicalrep_write_rel' is called.
Performance:
We see a performance degradation as we are sending new entries during
logical decoding. Results are an average of 5 runs.
count | Head (sec) | Fix (sec) | Degradation (%)
------------------------------------------------------------------------------
10000 | 1.298 | 1.574 | 21.26348228
50000 | 22.892 | 24.997 | 9.195352088
100000 | 88.602 | 93.759 | 5.820410374
I have also attached the test script here.
Thanks and Regards,
Shlok Kyal
Attachments:
On Mon, 2 Sept 2024 at 10:12, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Fri, Aug 30, 2024 at 3:06 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
Next I am planning to test solely on the logical decoding side and
will share the results.Thanks, the next set of proposed tests makes sense to me. It will also
be useful to generate some worst-case scenarios where the number of
invalidations is more to see the distribution cost in such cases. For
example, Truncate/Drop a table with 100 or 1000 partitions.--
With Regards,
Amit Kapila.
Also, I did testing with a table with partitions. To test for the
scenario where the number of invalidations are more than distribution.
Following is the test case:
1. Created a publisher on a single table, say 'tconc_1';
2. Created a second publisher on a partition table say 'tp';
3. Created 'tcount' partitions for the table 'tp'.
4. two sessions are running in parallel, let's say S1 and S2.
5. Begin a transaction in S1.
6. S1: Insert a row in table 'tconc_1'
S2: BEGIN; TRUNCATE TABLE tp; COMMIT;
With patch, this will add 'tcount * 3' invalidation messages to
transaction in session 1.
S1: Insert a row in table 't_conc1'
7. COMMIT the transaction in S1.
8. run 'pg_logical_slot_get_binary_changes' to get the decoding changes.
Performance:
We see a degradation in performance. Results are an average of 5 runs.
count of partitions | Head (sec) | Fix (sec) | Degradation (%)
-------------------------------------------------------------------------------------
1000 | 0.114 | 0.118 | 3.50877193
5000 | 0.502 | 0.522 | 3.984063745
10000 | 1.012 | 1.024 | 1.185770751
I have also attached the test script here. And will also do further testing.
Thanks and Regards,
Shlok Kyal
Attachments:
On Friday, August 9, 2024 7:21 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
Hi,
In the v7 patch, I am looping through the reorder buffer of the current committed
transaction and storing all invalidation messages in a list. Then I am
distributing those invalidations.
But I found that for a transaction we already store all the invalidation messages
(see [1]). So we don't need to loop through the reorder buffer and store the
invalidations.I have modified the patch accordingly and attached the same.
I have tested this patch across various scenarios and did not find issues.
I confirmed that changes are correctly replicated after adding the table or
schema to the publication, and changes will not be replicated after removing
the table or schema from the publication. This behavior is consistent in both
streaming and non-streaming modes. Additionally, I verified that invalidations
occurring within subtransactions are appropriately distributed.
Please refer to the attached ISOLATION tests which tested the above cases.
This also inspires me if it would be cheaper to write an ISOLATION test for this
bug instead of building a real pub/sub cluster. But I am not against the current
tests in the V8 patch as that can check the replicated data in a visible way.
Best Regards,
Hou zj
Attachments:
0001-test-invalidation-distribution.patch.txttext/plain; name=0001-test-invalidation-distribution.patch.txtDownload
From 8f4e36c5fc65d4a88058467a73cbe423a5f0e91e Mon Sep 17 00:00:00 2001
From: Hou Zhijie <houzj.fnst@cn.fujitsu.com>
Date: Mon, 9 Sep 2024 19:56:18 +0800
Subject: [PATCH] test invalidation distribution
---
contrib/test_decoding/Makefile | 2 +-
.../expected/invalidation_distrubution.out | 173 ++++++++++++++++++
.../specs/invalidation_distrubution.spec | 56 ++++++
3 files changed, 230 insertions(+), 1 deletion(-)
create mode 100644 contrib/test_decoding/expected/invalidation_distrubution.out
create mode 100644 contrib/test_decoding/specs/invalidation_distrubution.spec
diff --git a/contrib/test_decoding/Makefile b/contrib/test_decoding/Makefile
index a4ba1a509a..eef7077067 100644
--- a/contrib/test_decoding/Makefile
+++ b/contrib/test_decoding/Makefile
@@ -9,7 +9,7 @@ REGRESS = ddl xact rewrite toast permissions decoding_in_xact \
ISOLATION = mxact delayed_startup ondisk_startup concurrent_ddl_dml \
oldest_xmin snapshot_transfer subxact_without_top concurrent_stream \
twophase_snapshot slot_creation_error catalog_change_snapshot \
- skip_snapshot_restore
+ skip_snapshot_restore invalidation_distrubution
REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/test_decoding/logical.conf
ISOLATION_OPTS = --temp-config $(top_srcdir)/contrib/test_decoding/logical.conf
diff --git a/contrib/test_decoding/expected/invalidation_distrubution.out b/contrib/test_decoding/expected/invalidation_distrubution.out
new file mode 100644
index 0000000000..cdc871d31d
--- /dev/null
+++ b/contrib/test_decoding/expected/invalidation_distrubution.out
@@ -0,0 +1,173 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_init s2_create_pub s1_insert_tbl1 s1_begin s1_insert_tbl1 s2_alter_pub_add_tbl s1_commit s1_insert_tbl1 s2_get_binary_changes s2_drop_pub
+step s1_init: SELECT 'init' FROM pg_create_logical_replication_slot('isolation_slot', 'pgoutput');
+?column?
+--------
+init
+(1 row)
+
+step s2_create_pub: CREATE PUBLICATION pub;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s1_begin: BEGIN;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s2_alter_pub_add_tbl: ALTER PUBLICATION pub ADD TABLE tbl1;
+step s1_commit: COMMIT;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s2_get_binary_changes: SELECT count(data) FROM pg_logical_slot_get_binary_changes('isolation_slot', NULL, NULL, 'proto_version', '4', 'publication_names', 'pub') WHERE get_byte(data, 0) = 73;
+count
+-----
+ 1
+(1 row)
+
+step s2_drop_pub: DROP PUBLICATION pub;
+?column?
+--------
+stop
+(1 row)
+
+
+starting permutation: s1_init s2_create_pub s1_insert_tbl1 s1_begin s1_insert_tbl1 s2_alter_pub_add_schema s1_commit s1_insert_tbl1 s2_get_binary_changes s2_drop_pub
+step s1_init: SELECT 'init' FROM pg_create_logical_replication_slot('isolation_slot', 'pgoutput');
+?column?
+--------
+init
+(1 row)
+
+step s2_create_pub: CREATE PUBLICATION pub;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s1_begin: BEGIN;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s2_alter_pub_add_schema: ALTER PUBLICATION pub ADD TABLES IN SCHEMA public;
+step s1_commit: COMMIT;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s2_get_binary_changes: SELECT count(data) FROM pg_logical_slot_get_binary_changes('isolation_slot', NULL, NULL, 'proto_version', '4', 'publication_names', 'pub') WHERE get_byte(data, 0) = 73;
+count
+-----
+ 1
+(1 row)
+
+step s2_drop_pub: DROP PUBLICATION pub;
+?column?
+--------
+stop
+(1 row)
+
+
+starting permutation: s1_init s2_create_pub s2_alter_pub_add_tbl s1_begin s1_insert_tbl1 s2_alter_pub_drop_tbl s1_commit s1_insert_tbl1 s2_get_binary_changes s2_drop_pub
+step s1_init: SELECT 'init' FROM pg_create_logical_replication_slot('isolation_slot', 'pgoutput');
+?column?
+--------
+init
+(1 row)
+
+step s2_create_pub: CREATE PUBLICATION pub;
+step s2_alter_pub_add_tbl: ALTER PUBLICATION pub ADD TABLE tbl1;
+step s1_begin: BEGIN;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s2_alter_pub_drop_tbl: ALTER PUBLICATION pub DROP TABLE tbl1;
+step s1_commit: COMMIT;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s2_get_binary_changes: SELECT count(data) FROM pg_logical_slot_get_binary_changes('isolation_slot', NULL, NULL, 'proto_version', '4', 'publication_names', 'pub') WHERE get_byte(data, 0) = 73;
+count
+-----
+ 1
+(1 row)
+
+step s2_drop_pub: DROP PUBLICATION pub;
+?column?
+--------
+stop
+(1 row)
+
+
+starting permutation: s1_init s2_create_pub s2_alter_pub_add_schema s1_begin s1_insert_tbl1 s2_alter_pub_drop_schema s1_commit s1_insert_tbl1 s2_get_binary_changes s2_drop_pub
+step s1_init: SELECT 'init' FROM pg_create_logical_replication_slot('isolation_slot', 'pgoutput');
+?column?
+--------
+init
+(1 row)
+
+step s2_create_pub: CREATE PUBLICATION pub;
+step s2_alter_pub_add_schema: ALTER PUBLICATION pub ADD TABLES IN SCHEMA public;
+step s1_begin: BEGIN;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s2_alter_pub_drop_schema: ALTER PUBLICATION pub DROP TABLES IN SCHEMA public;
+step s1_commit: COMMIT;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s2_get_binary_changes: SELECT count(data) FROM pg_logical_slot_get_binary_changes('isolation_slot', NULL, NULL, 'proto_version', '4', 'publication_names', 'pub') WHERE get_byte(data, 0) = 73;
+count
+-----
+ 1
+(1 row)
+
+step s2_drop_pub: DROP PUBLICATION pub;
+?column?
+--------
+stop
+(1 row)
+
+
+starting permutation: s1_init s2_create_pub s1_insert_tbl1 s1_begin s1_insert_tbl1 s2_begin s2_savepoint s2_alter_pub_add_tbl s2_commit s1_commit s1_insert_tbl1 s2_get_binary_changes s2_drop_pub
+step s1_init: SELECT 'init' FROM pg_create_logical_replication_slot('isolation_slot', 'pgoutput');
+?column?
+--------
+init
+(1 row)
+
+step s2_create_pub: CREATE PUBLICATION pub;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s1_begin: BEGIN;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s2_begin: BEGIN;
+step s2_savepoint: SAVEPOINT s1;
+step s2_alter_pub_add_tbl: ALTER PUBLICATION pub ADD TABLE tbl1;
+step s2_commit: COMMIT;
+step s1_commit: COMMIT;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s2_get_binary_changes: SELECT count(data) FROM pg_logical_slot_get_binary_changes('isolation_slot', NULL, NULL, 'proto_version', '4', 'publication_names', 'pub') WHERE get_byte(data, 0) = 73;
+count
+-----
+ 1
+(1 row)
+
+step s2_drop_pub: DROP PUBLICATION pub;
+?column?
+--------
+stop
+(1 row)
+
+
+starting permutation: s2_create_pub s1_init s1_insert_tbl1 s1_begin s1_insert_tbl1 s2_set_streaming_mode s2_alter_pub_add_tbl s2_get_binary_stream_changes s1_insert_tbl1 s1_commit s2_get_binary_stream_changes s2_drop_pub
+step s2_create_pub: CREATE PUBLICATION pub;
+step s1_init: SELECT 'init' FROM pg_create_logical_replication_slot('isolation_slot', 'pgoutput');
+?column?
+--------
+init
+(1 row)
+
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s1_begin: BEGIN;
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s2_set_streaming_mode: SET debug_logical_replication_streaming = immediate;
+step s2_alter_pub_add_tbl: ALTER PUBLICATION pub ADD TABLE tbl1;
+step s2_get_binary_stream_changes: SELECT count(data) FROM pg_logical_slot_get_binary_changes('isolation_slot', NULL, NULL, 'proto_version', '4', 'publication_names', 'pub', 'streaming', 'on') WHERE get_byte(data, 0) = 73;
+count
+-----
+ 0
+(1 row)
+
+step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1);
+step s1_commit: COMMIT;
+step s2_get_binary_stream_changes: SELECT count(data) FROM pg_logical_slot_get_binary_changes('isolation_slot', NULL, NULL, 'proto_version', '4', 'publication_names', 'pub', 'streaming', 'on') WHERE get_byte(data, 0) = 73;
+count
+-----
+ 1
+(1 row)
+
+step s2_drop_pub: DROP PUBLICATION pub;
+?column?
+--------
+stop
+(1 row)
+
diff --git a/contrib/test_decoding/specs/invalidation_distrubution.spec b/contrib/test_decoding/specs/invalidation_distrubution.spec
new file mode 100644
index 0000000000..0d3d328250
--- /dev/null
+++ b/contrib/test_decoding/specs/invalidation_distrubution.spec
@@ -0,0 +1,56 @@
+setup
+{
+ DROP TABLE IF EXISTS tbl1;
+ CREATE TABLE tbl1(val1 integer, val2 integer);
+}
+
+teardown
+{
+ DROP TABLE tbl1;
+ SELECT 'stop' FROM pg_drop_replication_slot('isolation_slot');
+}
+
+session "s1"
+setup { SET synchronous_commit=on; }
+
+step "s1_init" { SELECT 'init' FROM pg_create_logical_replication_slot('isolation_slot', 'pgoutput'); }
+step "s1_begin" { BEGIN; }
+step "s1_insert_tbl1" { INSERT INTO tbl1 (val1, val2) VALUES (1, 1); }
+step "s1_commit" { COMMIT; }
+
+session "s2"
+setup { SET synchronous_commit=on; }
+
+step "s2_begin" { BEGIN; }
+step "s2_savepoint" { SAVEPOINT s1; }
+step "s2_set_streaming_mode" { SET debug_logical_replication_streaming = immediate; }
+step "s2_create_pub" { CREATE PUBLICATION pub; }
+step "s2_alter_pub_add_tbl" { ALTER PUBLICATION pub ADD TABLE tbl1; }
+step "s2_alter_pub_drop_tbl" { ALTER PUBLICATION pub DROP TABLE tbl1; }
+step "s2_alter_pub_add_schema" { ALTER PUBLICATION pub ADD TABLES IN SCHEMA public; }
+step "s2_alter_pub_drop_schema" { ALTER PUBLICATION pub DROP TABLES IN SCHEMA public; }
+step "s2_drop_pub" { DROP PUBLICATION pub; }
+
+
+step "s2_get_binary_stream_changes" { SELECT count(data) FROM pg_logical_slot_get_binary_changes('isolation_slot', NULL, NULL, 'proto_version', '4', 'publication_names', 'pub', 'streaming', 'on') WHERE get_byte(data, 0) = 73; }
+step "s2_commit" { COMMIT; }
+
+step "s2_get_binary_changes" { SELECT count(data) FROM pg_logical_slot_get_binary_changes('isolation_slot', NULL, NULL, 'proto_version', '4', 'publication_names', 'pub') WHERE get_byte(data, 0) = 73; }
+
+# Expect to get one insert change. LOGICAL_REP_MSG_INSERT = 'I'
+permutation "s1_init" "s2_create_pub" "s1_insert_tbl1" "s1_begin" "s1_insert_tbl1" "s2_alter_pub_add_tbl" "s1_commit" "s1_insert_tbl1" "s2_get_binary_changes" "s2_drop_pub"
+
+# Expect to get one insert change. LOGICAL_REP_MSG_INSERT = 'I'
+permutation "s1_init" "s2_create_pub" "s1_insert_tbl1" "s1_begin" "s1_insert_tbl1" "s2_alter_pub_add_schema" "s1_commit" "s1_insert_tbl1" "s2_get_binary_changes" "s2_drop_pub"
+
+# Expect to get one insert change. LOGICAL_REP_MSG_INSERT = 'I'
+permutation "s1_init" "s2_create_pub" "s2_alter_pub_add_tbl" "s1_begin" "s1_insert_tbl1" "s2_alter_pub_drop_tbl" "s1_commit" "s1_insert_tbl1" "s2_get_binary_changes" "s2_drop_pub"
+
+# Expect to get one insert change. LOGICAL_REP_MSG_INSERT = 'I'
+permutation "s1_init" "s2_create_pub" "s2_alter_pub_add_schema" "s1_begin" "s1_insert_tbl1" "s2_alter_pub_drop_schema" "s1_commit" "s1_insert_tbl1" "s2_get_binary_changes" "s2_drop_pub"
+
+# Expect to get one insert change. LOGICAL_REP_MSG_INSERT = 'I'
+permutation "s1_init" "s2_create_pub" "s1_insert_tbl1" "s1_begin" "s1_insert_tbl1" "s2_begin" "s2_savepoint" "s2_alter_pub_add_tbl" "s2_commit" "s1_commit" "s1_insert_tbl1" "s2_get_binary_changes" "s2_drop_pub"
+
+# Expect to get one insert change. LOGICAL_REP_MSG_INSERT = 'I'
+permutation "s2_create_pub" "s1_init" "s1_insert_tbl1" "s1_begin" "s1_insert_tbl1" "s2_set_streaming_mode" "s2_alter_pub_add_tbl" "s2_get_binary_stream_changes" "s1_insert_tbl1" "s1_commit" "s2_get_binary_stream_changes" "s2_drop_pub"
--
2.30.0.windows.2
On Thu, Sep 5, 2024 at 4:04 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Mon, Sep 2, 2024 at 9:19 PM Nitin Motiani <nitinmotiani@google.com> wrote:
I think that the partial data replication for one table is a bigger
issue than the case of data being sent for a subset of the tables in
the transaction. This can lead to inconsistent data if the same row is
updated multiple times or deleted in the same transaction. In such a
case if only the partial updates from the transaction are sent to the
subscriber, it might end up with the data which was never visible on
the publisher side.Here is an example I tried with the patch v8-001 :
I created following 2 tables on the publisher and the subscriber :
CREATE TABLE delete_test(id int primary key, name varchar(100));
CREATE TABLE update_test(id int primary key, name varchar(100));I added both the tables to the publication p on the publisher and
created a subscription s on the subscriber.I run 2 sessions on the publisher and do the following :
Session 1 :
BEGIN;
INSERT INTO delete_test VALUES(0, 'Nitin');Session 2 :
ALTER PUBLICATION p DROP TABLE delete_test;Session 1 :
DELETE FROM delete_test WHERE id=0;
COMMIT;After the commit there should be no new row created on the publisher.
But because the partial data was replicated, this is what the select
on the subscriber shows :SELECT * FROM delete_test;
id | name
----+-----------
0 | Nitin
(1 row)I don't think the above is a common use case. But this is still an
issue because the subscriber has the data which never existed on the
publisher.I don't think that is the correct conclusion because the user has
intentionally avoided sending part of the transaction changes. This
can happen in various ways without the patch as well. For example, if
the user has performed the ALTER in the same transaction.Publisher:
=========
BEGIN
postgres=*# Insert into delete_test values(0, 'Nitin');
INSERT 0 1
postgres=*# Alter Publication pub1 drop table delete_test;
ALTER PUBLICATION
postgres=*# Delete from delete_test where id=0;
DELETE 1
postgres=*# commit;
COMMIT
postgres=# select * from delete_test;
id | name
----+------
(0 rows)Subscriber:
=========
postgres=# select * from delete_test;
id | name
----+-------
0 | Nitin
(1 row)This can also happen when the user has published only 'inserts' but
not 'updates' or 'deletes'.
Thanks for the clarification. I didn't think of this case. The change
seems fine if this can already happen.
Thanks & Regards
Nitin Motiani
Google
On Mon, 9 Sept 2024 at 10:41, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Mon, 2 Sept 2024 at 10:12, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Fri, Aug 30, 2024 at 3:06 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
Next I am planning to test solely on the logical decoding side and
will share the results.Thanks, the next set of proposed tests makes sense to me. It will also
be useful to generate some worst-case scenarios where the number of
invalidations is more to see the distribution cost in such cases. For
example, Truncate/Drop a table with 100 or 1000 partitions.--
With Regards,
Amit Kapila.Hi,
I did some performance testing solely on the logical decoding side and
found some degradation in performance, for the following testcase:
1. Created a publisher on a single table, say 'tab_conc1';
2. Created a second publisher on a single table say 'tp';
4. two sessions are running in parallel, let's say S1 and S2.
5. Begin a transaction in S1.
6. Now in a loop (this loop runs 'count' times):
S1: Insert a row in table 'tab_conc1'
S2: BEGIN; Alter publication DROP/ ADD tp; COMMIT
7. COMMIT the transaction in S1.
8. run 'pg_logical_slot_get_binary_changes' to get the decoding changes.Observation:
With fix a new entry is added in decoding. During debugging I found
that this entry only comes when we do a 'INSERT' in Session 1 after we
do 'ALTER PUBLICATION' in another session in parallel (or we can say
due to invalidation). Also, I observed that this new entry is related
to sending replica identity, attributes,etc as function
'logicalrep_write_rel' is called.Performance:
We see a performance degradation as we are sending new entries during
logical decoding. Results are an average of 5 runs.count | Head (sec) | Fix (sec) | Degradation (%)
------------------------------------------------------------------------------
10000 | 1.298 | 1.574 | 21.26348228
50000 | 22.892 | 24.997 | 9.195352088
100000 | 88.602 | 93.759 | 5.820410374I have also attached the test script here.
For the above case I tried to investigate the inconsistent degradation
and found out that Serialization was happening for a large number of
'count'. So, I tried adjusting 'logical_decoding_work_mem' to a large
value, so that we can avoid serialization here. I ran the above
performance test again and got the following results:
count | Head (sec) | Fix (sec) | Degradation (%)
-----------------------------------------------------------------------------------
10000 | 0.415446 | 0.53596167 | 29.00874482
50000 | 7.950266 | 10.37375567 | 30.48312685
75000 | 17.192372 | 22.246715 | 29.39875312
100000 | 30.555903 | 39.431542 | 29.04721552
These results are an average of 3 runs. Here the degradation is
consistent around ~30%.
Thanks and Regards,
Shlok Kyal
In the v7 patch, I am looping through the reorder buffer of the
current committed transaction and storing all invalidation messages in
a list. Then I am distributing those invalidations.
But I found that for a transaction we already store all the
invalidation messages (see [1]). So we don't need to loop through the
reorder buffer and store the invalidations.I have modified the patch accordingly and attached the same.
Hi,
I tried to add changes to selectively invalidate the cache to reduce
the performance degradation during the distribution of invalidations.
Here is the analysis for selective invalidation.
Observation:
Currently when there is a change in a publication, cache related to
all the tables is invalidated including the ones that are not part of
any publication and even tables of different publications. For
example, suppose pub1 includes tables t1 to t1000, while pub2 contains
just table t1001. If pub2 is altered, even though it only has t1001,
this change will also invalidate all the tables t1 through t1000 in
pub1.
Similarly for a namespace, whenever we alter a schema or we add/drop a
schema to the publication, cache related to all the tables is
invalidated including the ones that are on of different schema. For
example, suppose pub1 includes tables t1 to t1000 in schema sc1, while
pub2 contains just table t1001 in schema sc2. If schema ‘sc2’ is
changed or if it is dropped from publication ‘pub2’ even though it
only has t1001, this change will invalidate all the tables t1 through
t1000 in schema sc1.
‘rel_sync_cache_publication_cb’ function is called during the
execution of invalidation in both above cases. And
‘rel_sync_cache_publication_cb’ invalidates all the tables in the
cache.
Solution:
1. When we alter a publication using commands like ‘ALTER PUBLICATION
pub_name DROP TABLE table_name’, first all tables in the publications
are invalidated using the function ‘rel_sync_cache_relation_cb’. Then
again ‘rel_sync_cache_publication_cb’ function is called which
invalidates all the tables. This happens because of the following
callback registered:
CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
rel_sync_cache_publication_cb, (Datum) 0);
So, I feel this second function call can be avoided. And I have
included changes for the same in the patch. Now the behavior will be
as:
suppose pub1 includes tables t1 to t1000, while pub2 contains just
table t1001. If pub2 is altered, it will only invalidate t1001.
2. When we add/drop a schema to/from a publication using command like
‘ALTER PUBLICATION pub_name ADD TABLES in SCHEMA schema_name’, first
all tables in that schema are invalidated using
‘rel_sync_cache_relation_cb’ and then again
‘rel_sync_cache_publication_cb’ function is called which invalidates
all the tables. This happens because of the following callback
registered:
CacheRegisterSyscacheCallback(PUBLICATIONNAMESPACEMAP,
rel_sync_cache_publication_cb, (Datum) 0);
So, I feel this second function call can be avoided. And I have
included changes for the same in the patch. Now the behavior will be
as:
suppose pub1 includes tables t1 to t1000 in schema sc1, while pub2
contains just table t1001 in schema sc2. If schema ‘sc2’ dropped from
publication ‘pub2’, it will only invalidate table t1001.
3. When we alter a namespace using command like ‘ALTER SCHEMA
schema_name RENAME to new_schema_name’ all the table in cache are
invalidated as ‘rel_sync_cache_publication_cb’ is called due to the
following registered callback:
CacheRegisterSyscacheCallback(NAMESPACEOID,
rel_sync_cache_publication_cb, (Datum) 0);
So, we added a new callback function ‘rel_sync_cache_namespacerel_cb’
will be called instead of function ‘rel_sync_cache_publication_cb’ ,
which invalidates only the cache of the tables which are part of that
particular namespace. For the new function the ‘namespace id’ is added
in the Invalidation message.
For example, if namespace ‘sc1’ has table t1 and t2 and a namespace
‘sc2’ has table t3. Then if we rename namespace ‘sc1’ to ‘sc_new’.
Only tables in sc1 i.e. tables t1 and table t2 are invalidated.
Performance Comparison:
I have run the same tests as shared in [1]/messages/by-id/CANhcyEW4pq6+PO_eFn2q=23sgV1budN3y4SxpYBaKMJNADSDuA@mail.gmail.com and observed a significant
decrease in the degradation with the new changes. With selective
invalidation degradation is around ~5%. This results are an average of
3 runs.
count | Head (sec) | Fix (sec) | Degradation (%)
-----------------------------------------------------------------------------------------
10000 | 0.38842567 | 0.405057 | 4.281727827
50000 | 7.22018834 | 7.605011334 | 5.329819333
75000 | 15.627181 | 16.38659034 | 4.859541462
100000 | 27.37910867 | 28.8636873 | 5.422304458
I have attached the patch for the same
v9-0001 : distribute invalidation to inprogress transaction
v9-0002: Selective invalidation
[1]: /messages/by-id/CANhcyEW4pq6+PO_eFn2q=23sgV1budN3y4SxpYBaKMJNADSDuA@mail.gmail.com
Thanks and Regards,
Shlok Kyal
Attachments:
v9-0001-Distribute-invalidatons-if-change-in-catalog-tabl.patchapplication/x-patch; name=v9-0001-Distribute-invalidatons-if-change-in-catalog-tabl.patchDownload
From 4222dca86e4892fbae6698ed7a6135f61d499d8f Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 23 Aug 2024 14:02:20 +0530
Subject: [PATCH v9 1/2] Distribute invalidatons if change in catalog tables
Distribute invalidations to inprogress transactions if the current
committed transaction change any catalog table.
---
.../replication/logical/reorderbuffer.c | 5 +-
src/backend/replication/logical/snapbuild.c | 34 +++--
src/include/replication/reorderbuffer.h | 4 +
src/test/subscription/t/100_bugs.pl | 128 ++++++++++++++++++
4 files changed, 157 insertions(+), 14 deletions(-)
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 22bcf171ff..c5dfc1ab06 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -221,9 +221,6 @@ int debug_logical_replication_streaming = DEBUG_LOGICAL_REP_STREAMING_BUFFERED
*/
static ReorderBufferTXN *ReorderBufferGetTXN(ReorderBuffer *rb);
static void ReorderBufferReturnTXN(ReorderBuffer *rb, ReorderBufferTXN *txn);
-static ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
- TransactionId xid, bool create, bool *is_new,
- XLogRecPtr lsn, bool create_as_top);
static void ReorderBufferTransferSnapToParent(ReorderBufferTXN *txn,
ReorderBufferTXN *subtxn);
@@ -622,7 +619,7 @@ ReorderBufferReturnRelids(ReorderBuffer *rb, Oid *relids)
* (with the given LSN, and as top transaction if that's specified);
* when this happens, is_new is set to true.
*/
-static ReorderBufferTXN *
+ReorderBufferTXN *
ReorderBufferTXNByXid(ReorderBuffer *rb, TransactionId xid, bool create,
bool *is_new, XLogRecPtr lsn, bool create_as_top)
{
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 0450f94ba8..42c947651b 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -300,7 +300,7 @@ static void SnapBuildFreeSnapshot(Snapshot snap);
static void SnapBuildSnapIncRefcount(Snapshot snap);
-static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn);
+static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
uint32 xinfo);
@@ -859,18 +859,21 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
}
/*
- * Add a new Snapshot to all transactions we're decoding that currently are
- * in-progress so they can see new catalog contents made by the transaction
- * that just committed. This is necessary because those in-progress
- * transactions will use the new catalog's contents from here on (at the very
- * least everything they do needs to be compatible with newer catalog
- * contents).
+ * Add a new Snapshot and invalidation messages to all transactions we're
+ * decoding that currently are in-progress so they can see new catalog contents
+ * made by the transaction that just committed. This is necessary because those
+ * in-progress transactions will use the new catalog's contents from here on
+ * (at the very least everything they do needs to be compatible with newer
+ * catalog contents).
*/
static void
-SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
+SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
+ ReorderBufferTXN *curr_txn;
+
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -913,6 +916,14 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
SnapBuildSnapIncRefcount(builder->snapshot);
ReorderBufferAddSnapshot(builder->reorder, txn->xid, lsn,
builder->snapshot);
+
+ /*
+ * Add invalidation messages to the reorder buffer of inprogress
+ * transactions except the current committed transaction
+ */
+ if (txn->xid != xid && curr_txn->ninvalidations > 0)
+ ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
+ curr_txn->ninvalidations, curr_txn->invalidations);
}
}
@@ -1184,8 +1195,11 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* refcount of the snapshot builder for the new snapshot */
SnapBuildSnapIncRefcount(builder->snapshot);
- /* add a new catalog snapshot to all currently running transactions */
- SnapBuildDistributeNewCatalogSnapshot(builder, lsn);
+ /*
+ * add a new catalog snapshot and invalidations messages to all
+ * currently running transactions
+ */
+ SnapBuildDistributeNewCatalogSnapshot(builder, lsn, xid);
}
}
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index e332635f70..093d21213a 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -743,6 +743,10 @@ extern TransactionId *ReorderBufferGetCatalogChangesXacts(ReorderBuffer *rb);
extern void ReorderBufferSetRestartPoint(ReorderBuffer *rb, XLogRecPtr ptr);
+extern ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
+ TransactionId xid, bool create, bool *is_new,
+ XLogRecPtr lsn, bool create_as_top);
+
extern void StartupReorderBuffer(void);
#endif
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..85d5c0d016 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,134 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', qq(DROP PUBLICATION pub1;));
+$node_subscriber->safe_psql('postgres', qq(DROP SUBSCRIPTION sub1;));
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+]);
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.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
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Complete the transaction on the tables, so that ALTER PUBLICATION can proceed
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
v9-0002-Add-Selective-Invalidation-of-Cache.patchapplication/x-patch; name=v9-0002-Add-Selective-Invalidation-of-Cache.patchDownload
From 7ce19b7dafdf659a9689856211e81c501dfe498f Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 25 Sep 2024 11:41:42 +0530
Subject: [PATCH v9 2/2] Add Selective Invalidation of Cache
When we alter a publication, add/drop namespace to/from publication,
alter a namespace all the cache for all the tables are invalidated. With
this patch for the above operationns we will invalidate the cache of
only the desired tables.
---
src/backend/replication/pgoutput/pgoutput.c | 52 ++++----
src/backend/utils/cache/inval.c | 127 +++++++++++++++++++-
src/include/storage/sinval.h | 9 ++
src/include/utils/inval.h | 4 +
src/test/subscription/t/100_bugs.pl | 91 ++++++++++++++
5 files changed, 253 insertions(+), 30 deletions(-)
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 00e7024563..ba480e7e48 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -126,6 +126,8 @@ typedef struct RelationSyncEntry
{
Oid relid; /* relation oid */
+ Oid schemaid; /* schema oid */
+
bool replicate_valid; /* overall validity flag for entry */
bool schema_sent;
@@ -216,6 +218,7 @@ static RelationSyncEntry *get_rel_sync_entry(PGOutputData *data,
static void rel_sync_cache_relation_cb(Datum arg, Oid relid);
static void rel_sync_cache_publication_cb(Datum arg, int cacheid,
uint32 hashvalue);
+static void rel_sync_cache_namespacerel_cb(Datum arg, int nspid);
static void set_schema_sent_in_streamed_txn(RelationSyncEntry *entry,
TransactionId xid);
static bool get_schema_sent_in_streamed_txn(RelationSyncEntry *entry,
@@ -1739,12 +1742,6 @@ static void
publication_invalidation_cb(Datum arg, int cacheid, uint32 hashvalue)
{
publications_valid = false;
-
- /*
- * Also invalidate per-relation cache so that next time the filtering info
- * is checked it will be updated with the new publication settings.
- */
- rel_sync_cache_publication_cb(arg, cacheid, hashvalue);
}
/*
@@ -1911,26 +1908,7 @@ init_rel_sync_cache(MemoryContext cachectx)
/* We must update the cache entry for a relation after a relcache flush */
CacheRegisterRelcacheCallback(rel_sync_cache_relation_cb, (Datum) 0);
-
- /*
- * Flush all cache entries after a pg_namespace change, in case it was a
- * schema rename affecting a relation being replicated.
- */
- CacheRegisterSyscacheCallback(NAMESPACEOID,
- rel_sync_cache_publication_cb,
- (Datum) 0);
-
- /*
- * Flush all cache entries after any publication changes. (We need no
- * callback entry for pg_publication, because publication_invalidation_cb
- * will take care of it.)
- */
- CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
- CacheRegisterSyscacheCallback(PUBLICATIONNAMESPACEMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
+ CacheRegisterNspcacheCallback(rel_sync_cache_namespacerel_cb, (Datum) 0);
relation_callbacks_registered = true;
}
@@ -2076,6 +2054,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
entry->estate = NULL;
memset(entry->exprstate, 0, sizeof(entry->exprstate));
+ entry->schemaid = schemaId;
+
/*
* Build publication cache. We can't use one provided by relcache as
* relcache considers all publications that the given relation is in,
@@ -2349,6 +2329,26 @@ rel_sync_cache_publication_cb(Datum arg, int cacheid, uint32 hashvalue)
}
}
+/*
+ * Namespace invalidation callback
+ */
+static void
+rel_sync_cache_namespacerel_cb(Datum arg, int nspid)
+{
+ HASH_SEQ_STATUS status;
+ RelationSyncEntry *entry;
+
+ if (RelationSyncCache == NULL)
+ return;
+
+ hash_seq_init(&status, RelationSyncCache);
+ while ((entry = (RelationSyncEntry *) hash_seq_search(&status)) != NULL)
+ {
+ if (entry->replicate_valid && entry->schemaid == nspid)
+ entry->replicate_valid = false;
+ }
+}
+
/* Send Replication origin */
static void
send_repl_origin(LogicalDecodingContext *ctx, RepOriginId origin_id,
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c
index 603aa4157b..fc0d91aec9 100644
--- a/src/backend/utils/cache/inval.c
+++ b/src/backend/utils/cache/inval.c
@@ -114,6 +114,7 @@
#include "access/xact.h"
#include "access/xloginsert.h"
#include "catalog/catalog.h"
+#include "catalog/pg_namespace.h"
#include "catalog/pg_constraint.h"
#include "miscadmin.h"
#include "storage/sinval.h"
@@ -160,6 +161,9 @@
*/
#define CatCacheMsgs 0
#define RelCacheMsgs 1
+#define NspCacheMsgs 2
+
+#define NumberofCache 3
/* Pointers to main arrays in TopTransactionContext */
typedef struct InvalMessageArray
@@ -168,13 +172,13 @@ typedef struct InvalMessageArray
int maxmsgs; /* current allocated size of array */
} InvalMessageArray;
-static InvalMessageArray InvalMessageArrays[2];
+static InvalMessageArray InvalMessageArrays[NumberofCache];
/* Control information for one logical group of messages */
typedef struct InvalidationMsgsGroup
{
- int firstmsg[2]; /* first index in relevant array */
- int nextmsg[2]; /* last+1 index */
+ int firstmsg[NumberofCache]; /* first index in relevant array */
+ int nextmsg[NumberofCache]; /* last+1 index */
} InvalidationMsgsGroup;
/* Macros to help preserve InvalidationMsgsGroup abstraction */
@@ -189,6 +193,7 @@ typedef struct InvalidationMsgsGroup
do { \
SetSubGroupToFollow(targetgroup, priorgroup, CatCacheMsgs); \
SetSubGroupToFollow(targetgroup, priorgroup, RelCacheMsgs); \
+ SetSubGroupToFollow(targetgroup, priorgroup, NspCacheMsgs); \
} while (0)
#define NumMessagesInSubGroup(group, subgroup) \
@@ -196,7 +201,8 @@ typedef struct InvalidationMsgsGroup
#define NumMessagesInGroup(group) \
(NumMessagesInSubGroup(group, CatCacheMsgs) + \
- NumMessagesInSubGroup(group, RelCacheMsgs))
+ NumMessagesInSubGroup(group, RelCacheMsgs) + \
+ NumMessagesInSubGroup(group, NspCacheMsgs))
/*----------------
@@ -251,6 +257,7 @@ int debug_discard_caches = 0;
#define MAX_SYSCACHE_CALLBACKS 64
#define MAX_RELCACHE_CALLBACKS 10
+#define MAX_NSPCACHE_CALLBACKS 10
static struct SYSCACHECALLBACK
{
@@ -270,7 +277,14 @@ static struct RELCACHECALLBACK
Datum arg;
} relcache_callback_list[MAX_RELCACHE_CALLBACKS];
+static struct NSPCACHECALLBACK
+{
+ NspcacheCallbackFunction function;
+ Datum arg;
+} nspcache_callback_list[MAX_NSPCACHE_CALLBACKS];
+
static int relcache_callback_count = 0;
+static int nspcache_callback_count = 0;
/* ----------------------------------------------------------------
* Invalidation subgroup support functions
@@ -464,6 +478,35 @@ AddRelcacheInvalidationMessage(InvalidationMsgsGroup *group,
AddInvalidationMessage(group, RelCacheMsgs, &msg);
}
+static void
+AddNspcacheInvalidationMessage(InvalidationMsgsGroup *group,
+ Oid dbId, Oid nspId)
+{
+ SharedInvalidationMessage msg;
+
+ /*
+ * Don't add a duplicate item. We assume dbId need not be checked because
+ * it will never change. InvalidOid for relId means all relations so we
+ * don't need to add individual ones when it is present.
+ */
+
+ ProcessMessageSubGroup(group, NspCacheMsgs,
+ if (msg->nc.id == SHAREDINVALNSPCACHE_ID &&
+ (msg->nc.nspId == nspId ||
+ msg->nc.nspId == InvalidOid))
+ return);
+
+
+ /* OK, add the item */
+ msg.nc.id = SHAREDINVALNSPCACHE_ID;
+ msg.nc.dbId = dbId;
+ msg.nc.nspId = nspId;
+ /* check AddCatcacheInvalidationMessage() for an explanation */
+ VALGRIND_MAKE_MEM_DEFINED(&msg, sizeof(msg));
+
+ AddInvalidationMessage(group, NspCacheMsgs, &msg);
+}
+
/*
* Add a snapshot inval entry
*
@@ -502,6 +545,7 @@ AppendInvalidationMessages(InvalidationMsgsGroup *dest,
{
AppendInvalidationMessageSubGroup(dest, src, CatCacheMsgs);
AppendInvalidationMessageSubGroup(dest, src, RelCacheMsgs);
+ AppendInvalidationMessageSubGroup(dest, src, NspCacheMsgs);
}
/*
@@ -516,6 +560,7 @@ ProcessInvalidationMessages(InvalidationMsgsGroup *group,
{
ProcessMessageSubGroup(group, CatCacheMsgs, func(msg));
ProcessMessageSubGroup(group, RelCacheMsgs, func(msg));
+ ProcessMessageSubGroup(group, NspCacheMsgs, func(msg));
}
/*
@@ -528,6 +573,7 @@ ProcessInvalidationMessagesMulti(InvalidationMsgsGroup *group,
{
ProcessMessageSubGroupMulti(group, CatCacheMsgs, func(msgs, n));
ProcessMessageSubGroupMulti(group, RelCacheMsgs, func(msgs, n));
+ ProcessMessageSubGroupMulti(group, NspCacheMsgs, func(msgs, n));
}
/* ----------------------------------------------------------------
@@ -590,6 +636,18 @@ RegisterRelcacheInvalidation(Oid dbId, Oid relId)
transInvalInfo->RelcacheInitFileInval = true;
}
+/*
+ * RegisterNspcacheInvalidation
+ *
+ * As above, but register a namespace invalidation event.
+ */
+static void
+RegisterNspcacheInvalidation(Oid dbId, Oid nspId)
+{
+ AddNspcacheInvalidationMessage(&transInvalInfo->CurrentCmdInvalidMsgs,
+ dbId, nspId);
+}
+
/*
* RegisterSnapshotInvalidation
*
@@ -660,6 +718,8 @@ PrepareInvalidationState(void)
InvalMessageArrays[CatCacheMsgs].maxmsgs = 0;
InvalMessageArrays[RelCacheMsgs].msgs = NULL;
InvalMessageArrays[RelCacheMsgs].maxmsgs = 0;
+ InvalMessageArrays[NspCacheMsgs].msgs = NULL;
+ InvalMessageArrays[NspCacheMsgs].maxmsgs = 0;
}
transInvalInfo = myInfo;
@@ -773,6 +833,20 @@ LocalExecuteInvalidationMessage(SharedInvalidationMessage *msg)
else if (msg->sn.dbId == MyDatabaseId)
InvalidateCatalogSnapshot();
}
+ else if (msg->id == SHAREDINVALNSPCACHE_ID)
+ {
+ if (msg->nc.dbId == MyDatabaseId || msg->nc.dbId == InvalidOid)
+ {
+ int i;
+
+ for (i = 0; i < nspcache_callback_count; i++)
+ {
+ struct NSPCACHECALLBACK *ncitem = nspcache_callback_list + i;
+
+ ncitem->function(ncitem->arg, msg->nc.nspId);
+ }
+ }
+ }
else
elog(FATAL, "unrecognized SI message ID: %d", msg->id);
}
@@ -944,6 +1018,18 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
msgs,
n * sizeof(SharedInvalidationMessage)),
nmsgs += n));
+ ProcessMessageSubGroupMulti(&transInvalInfo->PriorCmdInvalidMsgs,
+ NspCacheMsgs,
+ (memcpy(msgarray + nmsgs,
+ msgs,
+ n * sizeof(SharedInvalidationMessage)),
+ nmsgs += n));
+ ProcessMessageSubGroupMulti(&transInvalInfo->CurrentCmdInvalidMsgs,
+ NspCacheMsgs,
+ (memcpy(msgarray + nmsgs,
+ msgs,
+ n * sizeof(SharedInvalidationMessage)),
+ nmsgs += n));
Assert(nmsgs == nummsgs);
return nmsgs;
@@ -1312,6 +1398,17 @@ CacheInvalidateHeapTuple(Relation relation,
else
return;
}
+ else if (tupleRelId == NamespaceRelationId)
+ {
+ Form_pg_namespace nsptup = (Form_pg_namespace) GETSTRUCT(tuple);
+
+ /* get namespace id */
+ relationId = nsptup->oid;
+ databaseId = MyDatabaseId;
+
+ RegisterNspcacheInvalidation(databaseId, relationId);
+ return;
+ }
else
return;
@@ -1567,6 +1664,25 @@ CacheRegisterRelcacheCallback(RelcacheCallbackFunction func,
++relcache_callback_count;
}
+/*
+ * CacheRegisterNspcacheCallback
+ * Register the specified function to be called for all future
+ * namespace invalidation events. The OID of the namespace being
+ * invalidated will be passed to the function.
+ */
+void
+CacheRegisterNspcacheCallback(NspcacheCallbackFunction func,
+ Datum arg)
+{
+ if (nspcache_callback_count >= MAX_NSPCACHE_CALLBACKS)
+ elog(FATAL, "out of nspcache_callback_list slots");
+
+ nspcache_callback_list[nspcache_callback_count].function = func;
+ nspcache_callback_list[nspcache_callback_count].arg = arg;
+
+ ++nspcache_callback_count;
+}
+
/*
* CallSyscacheCallbacks
*
@@ -1629,6 +1745,9 @@ LogLogicalInvalidations(void)
ProcessMessageSubGroupMulti(group, RelCacheMsgs,
XLogRegisterData((char *) msgs,
n * sizeof(SharedInvalidationMessage)));
+ ProcessMessageSubGroupMulti(group, NspCacheMsgs,
+ XLogRegisterData((char *) msgs,
+ n * sizeof(SharedInvalidationMessage)));
XLogInsert(RM_XACT_ID, XLOG_XACT_INVALIDATIONS);
}
}
diff --git a/src/include/storage/sinval.h b/src/include/storage/sinval.h
index 8f5744b21b..4c53012528 100644
--- a/src/include/storage/sinval.h
+++ b/src/include/storage/sinval.h
@@ -110,6 +110,14 @@ typedef struct
Oid relId; /* relation ID */
} SharedInvalSnapshotMsg;
+#define SHAREDINVALNSPCACHE_ID (-6)
+typedef struct
+{
+ int8 id; /* type field --- must be first */
+ Oid dbId; /* database ID, or 0 if a shared relation */
+ Oid nspId; /* namespace ID */
+} SharedInvalNspcacheMsg;
+
typedef union
{
int8 id; /* type field --- must be first */
@@ -119,6 +127,7 @@ typedef union
SharedInvalSmgrMsg sm;
SharedInvalRelmapMsg rm;
SharedInvalSnapshotMsg sn;
+ SharedInvalNspcacheMsg nc;
} SharedInvalidationMessage;
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.h
index 24695facf2..99a0c90b6d 100644
--- a/src/include/utils/inval.h
+++ b/src/include/utils/inval.h
@@ -22,6 +22,7 @@ extern PGDLLIMPORT int debug_discard_caches;
typedef void (*SyscacheCallbackFunction) (Datum arg, int cacheid, uint32 hashvalue);
typedef void (*RelcacheCallbackFunction) (Datum arg, Oid relid);
+typedef void (*NspcacheCallbackFunction) (Datum arg, Oid nspid);
extern void AcceptInvalidationMessages(void);
@@ -59,6 +60,9 @@ extern void CacheRegisterSyscacheCallback(int cacheid,
extern void CacheRegisterRelcacheCallback(RelcacheCallbackFunction func,
Datum arg);
+extern void CacheRegisterNspcacheCallback(NspcacheCallbackFunction func,
+ Datum arg);
+
extern void CallSyscacheCallbacks(int cacheid, uint32 hashvalue);
extern void InvalidateSystemCaches(void);
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index 85d5c0d016..e038dd8a87 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -611,6 +611,97 @@ is( $result, qq(1
'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
);
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (4);
+]);
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (4);
+]);
+
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (5);
+ INSERT INTO sch3.tab_conc VALUES (5);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (6);
+]);
+
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (6);
+]);
+
+$background_psql3->query_safe(
+ qq[
+ DROP PUBLICATION regress_pub1;
+]);
+
+$background_psql1->query_safe(
+ qq[
+ INSERT INTO tab_conc VALUES (7);
+]);
+
+$background_psql2->query_safe(
+ qq[
+ INSERT INTO sch3.tab_conc VALUES (7);
+]);
+
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+my $offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/,
+ $offset);
+
+$node_subscriber->safe_psql('postgres',
+ 'DROP SUBSCRIPTION regress_sub1;');
+
$background_psql1->quit;
$background_psql2->quit;
$background_psql3->quit;
--
2.34.1
Dear Shlok,
Hi,
I tried to add changes to selectively invalidate the cache to reduce
the performance degradation during the distribution of invalidations.
Thanks for improving the patch!
...
Solution:
1. When we alter a publication using commands like ‘ALTER PUBLICATION
pub_name DROP TABLE table_name’, first all tables in the publications
are invalidated using the function ‘rel_sync_cache_relation_cb’. Then
again ‘rel_sync_cache_publication_cb’ function is called which
invalidates all the tables.
On my environment, rel_sync_cache_publication_cb() was called first and invalidate
all the entries, then rel_sync_cache_relation_cb() was called and the specified
entry is invalidated - hence second is NO-OP.
This happens because of the following
callback registered:
CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
rel_sync_cache_publication_cb, (Datum) 0);
But even in this case, I could understand that you want to remove the
rel_sync_cache_publication_cb() callback.
2. When we add/drop a schema to/from a publication using command like
‘ALTER PUBLICATION pub_name ADD TABLES in SCHEMA schema_name’, first
all tables in that schema are invalidated using
‘rel_sync_cache_relation_cb’ and then again
‘rel_sync_cache_publication_cb’ function is called which invalidates
all the tables.
Even in this case, rel_sync_cache_publication_cb() was called first and then
rel_sync_cache_relation_cb().
3. When we alter a namespace using command like ‘ALTER SCHEMA
schema_name RENAME to new_schema_name’ all the table in cache are
invalidated as ‘rel_sync_cache_publication_cb’ is called due to the
following registered callback:
CacheRegisterSyscacheCallback(NAMESPACEOID,
rel_sync_cache_publication_cb, (Datum) 0);So, we added a new callback function ‘rel_sync_cache_namespacerel_cb’
will be called instead of function ‘rel_sync_cache_publication_cb’ ,
which invalidates only the cache of the tables which are part of that
particular namespace. For the new function the ‘namespace id’ is added
in the Invalidation message.
Hmm, I feel this fix is too much. Unlike ALTER PUBLICATION statements, I think
ALTER SCHEMA is rarely executed at the production stage. However, this approach
requires adding a new cache callback system, which affects the entire postgres
system; this is not very beneficial compared to the outcome. It should be discussed
on another thread to involve more people, and then we can add the improvement
after being accepted.
Performance Comparison:
I have run the same tests as shared in [1] and observed a significant
decrease in the degradation with the new changes. With selective
invalidation degradation is around ~5%. This results are an average of
3 runs.
IIUC, the executed workload did not contain ALTER SCHEMA command, so
third improvement did not contribute this improvement.
Best regards,
Hayato Kuroda
FUJITSU LIMITED
Hi Kuroda-san,
Thanks for reviewing the patch.
Solution:
1. When we alter a publication using commands like ‘ALTER PUBLICATION
pub_name DROP TABLE table_name’, first all tables in the publications
are invalidated using the function ‘rel_sync_cache_relation_cb’. Then
again ‘rel_sync_cache_publication_cb’ function is called which
invalidates all the tables.On my environment, rel_sync_cache_publication_cb() was called first and invalidate
all the entries, then rel_sync_cache_relation_cb() was called and the specified
entry is invalidated - hence second is NO-OP.
You are correct. I made a silly mistake while writing the write-up.
rel_sync_cache_publication_cb() is called first and invalidate all the
entries, then rel_sync_cache_relation_cb() is called and the specified
entry is invalidated
This happens because of the following
callback registered:
CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
rel_sync_cache_publication_cb, (Datum) 0);But even in this case, I could understand that you want to remove the
rel_sync_cache_publication_cb() callback.
Yes, I think rel_sync_cache_publication_cb() callback can be removed,
as it is invalidating all the other tables as well (which are not in
this publication).
2. When we add/drop a schema to/from a publication using command like
‘ALTER PUBLICATION pub_name ADD TABLES in SCHEMA schema_name’, first
all tables in that schema are invalidated using
‘rel_sync_cache_relation_cb’ and then again
‘rel_sync_cache_publication_cb’ function is called which invalidates
all the tables.Even in this case, rel_sync_cache_publication_cb() was called first and then
rel_sync_cache_relation_cb().
Yes, your observation is correct. rel_sync_cache_publication_cb() is
called first and then rel_sync_cache_relation_cb().
3. When we alter a namespace using command like ‘ALTER SCHEMA
schema_name RENAME to new_schema_name’ all the table in cache are
invalidated as ‘rel_sync_cache_publication_cb’ is called due to the
following registered callback:
CacheRegisterSyscacheCallback(NAMESPACEOID,
rel_sync_cache_publication_cb, (Datum) 0);So, we added a new callback function ‘rel_sync_cache_namespacerel_cb’
will be called instead of function ‘rel_sync_cache_publication_cb’ ,
which invalidates only the cache of the tables which are part of that
particular namespace. For the new function the ‘namespace id’ is added
in the Invalidation message.Hmm, I feel this fix is too much. Unlike ALTER PUBLICATION statements, I think
ALTER SCHEMA is rarely executed at the production stage. However, this approach
requires adding a new cache callback system, which affects the entire postgres
system; this is not very beneficial compared to the outcome. It should be discussed
on another thread to involve more people, and then we can add the improvement
after being accepted.
Yes, I also agree with you. I have removed the changes in the updated patch.
Performance Comparison:
I have run the same tests as shared in [1] and observed a significant
decrease in the degradation with the new changes. With selective
invalidation degradation is around ~5%. This results are an average of
3 runs.IIUC, the executed workload did not contain ALTER SCHEMA command, so
third improvement did not contribute this improvement.
I have removed the changes corresponding to the third improvement.
I have addressed the comment for 0002 patch and attached the patches.
Also, I have moved the tests in the 0002 to 0001 patch.
Thanks and Regards,
Shlok Kyal
Attachments:
v10-0002-Selective-Invalidation-of-Cache.patchapplication/octet-stream; name=v10-0002-Selective-Invalidation-of-Cache.patchDownload
From f671f13607ef87c413cecefc26f2eb5b6a81faef Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 27 Sep 2024 16:04:54 +0530
Subject: [PATCH v10 2/2] Selective Invalidation of Cache
When we alter a publication, add/drop namespace to/from publication
all the cache for all the tables are invalidated.
With this patch for the above operationns we will invalidate the
cache of only the desired tables.
---
src/backend/replication/pgoutput/pgoutput.c | 18 ------------------
1 file changed, 18 deletions(-)
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 00e7024563..b8429be8cf 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1739,12 +1739,6 @@ static void
publication_invalidation_cb(Datum arg, int cacheid, uint32 hashvalue)
{
publications_valid = false;
-
- /*
- * Also invalidate per-relation cache so that next time the filtering info
- * is checked it will be updated with the new publication settings.
- */
- rel_sync_cache_publication_cb(arg, cacheid, hashvalue);
}
/*
@@ -1920,18 +1914,6 @@ init_rel_sync_cache(MemoryContext cachectx)
rel_sync_cache_publication_cb,
(Datum) 0);
- /*
- * Flush all cache entries after any publication changes. (We need no
- * callback entry for pg_publication, because publication_invalidation_cb
- * will take care of it.)
- */
- CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
- CacheRegisterSyscacheCallback(PUBLICATIONNAMESPACEMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
-
relation_callbacks_registered = true;
}
--
2.34.1
v10-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchapplication/octet-stream; name=v10-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchDownload
From 3f4ddd0be632a736b672c3ea4d2e572ba1d1b4ef Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 23 Aug 2024 14:02:20 +0530
Subject: [PATCH v10 1/2] Distribute invalidatons if change in catalog tables
Distribute invalidations to inprogress transactions if the current
committed transaction change any catalog table.
---
.../replication/logical/reorderbuffer.c | 5 +-
src/backend/replication/logical/snapbuild.c | 34 ++-
src/include/replication/reorderbuffer.h | 4 +
src/test/subscription/t/100_bugs.pl | 236 ++++++++++++++++++
4 files changed, 265 insertions(+), 14 deletions(-)
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 22bcf171ff..c5dfc1ab06 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -221,9 +221,6 @@ int debug_logical_replication_streaming = DEBUG_LOGICAL_REP_STREAMING_BUFFERED
*/
static ReorderBufferTXN *ReorderBufferGetTXN(ReorderBuffer *rb);
static void ReorderBufferReturnTXN(ReorderBuffer *rb, ReorderBufferTXN *txn);
-static ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
- TransactionId xid, bool create, bool *is_new,
- XLogRecPtr lsn, bool create_as_top);
static void ReorderBufferTransferSnapToParent(ReorderBufferTXN *txn,
ReorderBufferTXN *subtxn);
@@ -622,7 +619,7 @@ ReorderBufferReturnRelids(ReorderBuffer *rb, Oid *relids)
* (with the given LSN, and as top transaction if that's specified);
* when this happens, is_new is set to true.
*/
-static ReorderBufferTXN *
+ReorderBufferTXN *
ReorderBufferTXNByXid(ReorderBuffer *rb, TransactionId xid, bool create,
bool *is_new, XLogRecPtr lsn, bool create_as_top)
{
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 0450f94ba8..42c947651b 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -300,7 +300,7 @@ static void SnapBuildFreeSnapshot(Snapshot snap);
static void SnapBuildSnapIncRefcount(Snapshot snap);
-static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn);
+static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
uint32 xinfo);
@@ -859,18 +859,21 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
}
/*
- * Add a new Snapshot to all transactions we're decoding that currently are
- * in-progress so they can see new catalog contents made by the transaction
- * that just committed. This is necessary because those in-progress
- * transactions will use the new catalog's contents from here on (at the very
- * least everything they do needs to be compatible with newer catalog
- * contents).
+ * Add a new Snapshot and invalidation messages to all transactions we're
+ * decoding that currently are in-progress so they can see new catalog contents
+ * made by the transaction that just committed. This is necessary because those
+ * in-progress transactions will use the new catalog's contents from here on
+ * (at the very least everything they do needs to be compatible with newer
+ * catalog contents).
*/
static void
-SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
+SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
+ ReorderBufferTXN *curr_txn;
+
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -913,6 +916,14 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
SnapBuildSnapIncRefcount(builder->snapshot);
ReorderBufferAddSnapshot(builder->reorder, txn->xid, lsn,
builder->snapshot);
+
+ /*
+ * Add invalidation messages to the reorder buffer of inprogress
+ * transactions except the current committed transaction
+ */
+ if (txn->xid != xid && curr_txn->ninvalidations > 0)
+ ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
+ curr_txn->ninvalidations, curr_txn->invalidations);
}
}
@@ -1184,8 +1195,11 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* refcount of the snapshot builder for the new snapshot */
SnapBuildSnapIncRefcount(builder->snapshot);
- /* add a new catalog snapshot to all currently running transactions */
- SnapBuildDistributeNewCatalogSnapshot(builder, lsn);
+ /*
+ * add a new catalog snapshot and invalidations messages to all
+ * currently running transactions
+ */
+ SnapBuildDistributeNewCatalogSnapshot(builder, lsn, xid);
}
}
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index e332635f70..093d21213a 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -743,6 +743,10 @@ extern TransactionId *ReorderBufferGetCatalogChangesXacts(ReorderBuffer *rb);
extern void ReorderBufferSetRestartPoint(ReorderBuffer *rb, XLogRecPtr ptr);
+extern ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
+ TransactionId xid, bool create, bool *is_new,
+ XLogRecPtr lsn, bool create_as_top);
+
extern void StartupReorderBuffer(void);
#endif
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..77e8a24497 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,242 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', qq(DROP PUBLICATION pub1;));
+$node_subscriber->safe_psql('postgres', qq(DROP SUBSCRIPTION sub1;));
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+]);
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (1);
+]);
+
+# Add the table to the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even when
+# tables are dropped from the publication in presence of a concurrent active
+# transaction performing the DML on the same table.
+
+# Maintain an active transaction with the table that will be dropped from the
+# publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (4);
+]);
+
+# Maintain an active transaction with a schema table that will be dropped from the
+# publication.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (4);
+]);
+
+# Drop the table from the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (5);
+ INSERT INTO sch3.tab_conc VALUES (5);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is not replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that data for table tab_conc are not replicated to subscriber');
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table sch3.tab_conc are not replicated to subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is dropped in a concurrent active transaction.
+
+# Add tables to the publication.
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (6);
+]);
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (6);
+]);
+
+# Drop publication.
+$background_psql3->query_safe(
+ qq[
+ DROP PUBLICATION regress_pub1;
+]);
+
+# Perform an insert.
+$background_psql1->query_safe(
+ qq[
+ INSERT INTO tab_conc VALUES (7);
+]);
+
+$background_psql2->query_safe(
+ qq[
+ INSERT INTO sch3.tab_conc VALUES (7);
+]);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+# ERROR should appear on subscriber.
+my $offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION regress_sub1;');
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
On Thu, 26 Sept 2024 at 11:39, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
In the v7 patch, I am looping through the reorder buffer of the
current committed transaction and storing all invalidation messages in
a list. Then I am distributing those invalidations.
But I found that for a transaction we already store all the
invalidation messages (see [1]). So we don't need to loop through the
reorder buffer and store the invalidations.I have modified the patch accordingly and attached the same.
Hi,
I tried to add changes to selectively invalidate the cache to reduce
the performance degradation during the distribution of invalidations.Here is the analysis for selective invalidation.
Observation:
Currently when there is a change in a publication, cache related to
all the tables is invalidated including the ones that are not part of
any publication and even tables of different publications. For
example, suppose pub1 includes tables t1 to t1000, while pub2 contains
just table t1001. If pub2 is altered, even though it only has t1001,
this change will also invalidate all the tables t1 through t1000 in
pub1.
Similarly for a namespace, whenever we alter a schema or we add/drop a
schema to the publication, cache related to all the tables is
invalidated including the ones that are on of different schema. For
example, suppose pub1 includes tables t1 to t1000 in schema sc1, while
pub2 contains just table t1001 in schema sc2. If schema ‘sc2’ is
changed or if it is dropped from publication ‘pub2’ even though it
only has t1001, this change will invalidate all the tables t1 through
t1000 in schema sc1.
‘rel_sync_cache_publication_cb’ function is called during the
execution of invalidation in both above cases. And
‘rel_sync_cache_publication_cb’ invalidates all the tables in the
cache.Solution:
1. When we alter a publication using commands like ‘ALTER PUBLICATION
pub_name DROP TABLE table_name’, first all tables in the publications
are invalidated using the function ‘rel_sync_cache_relation_cb’. Then
again ‘rel_sync_cache_publication_cb’ function is called which
invalidates all the tables. This happens because of the following
callback registered:
CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
rel_sync_cache_publication_cb, (Datum) 0);So, I feel this second function call can be avoided. And I have
included changes for the same in the patch. Now the behavior will be
as:
suppose pub1 includes tables t1 to t1000, while pub2 contains just
table t1001. If pub2 is altered, it will only invalidate t1001.2. When we add/drop a schema to/from a publication using command like
‘ALTER PUBLICATION pub_name ADD TABLES in SCHEMA schema_name’, first
all tables in that schema are invalidated using
‘rel_sync_cache_relation_cb’ and then again
‘rel_sync_cache_publication_cb’ function is called which invalidates
all the tables. This happens because of the following callback
registered:
CacheRegisterSyscacheCallback(PUBLICATIONNAMESPACEMAP,
rel_sync_cache_publication_cb, (Datum) 0);So, I feel this second function call can be avoided. And I have
included changes for the same in the patch. Now the behavior will be
as:
suppose pub1 includes tables t1 to t1000 in schema sc1, while pub2
contains just table t1001 in schema sc2. If schema ‘sc2’ dropped from
publication ‘pub2’, it will only invalidate table t1001.3. When we alter a namespace using command like ‘ALTER SCHEMA
schema_name RENAME to new_schema_name’ all the table in cache are
invalidated as ‘rel_sync_cache_publication_cb’ is called due to the
following registered callback:
CacheRegisterSyscacheCallback(NAMESPACEOID,
rel_sync_cache_publication_cb, (Datum) 0);So, we added a new callback function ‘rel_sync_cache_namespacerel_cb’
will be called instead of function ‘rel_sync_cache_publication_cb’ ,
which invalidates only the cache of the tables which are part of that
particular namespace. For the new function the ‘namespace id’ is added
in the Invalidation message.For example, if namespace ‘sc1’ has table t1 and t2 and a namespace
‘sc2’ has table t3. Then if we rename namespace ‘sc1’ to ‘sc_new’.
Only tables in sc1 i.e. tables t1 and table t2 are invalidated.Performance Comparison:
I have run the same tests as shared in [1] and observed a significant
decrease in the degradation with the new changes. With selective
invalidation degradation is around ~5%. This results are an average of
3 runs.count | Head (sec) | Fix (sec) | Degradation (%)
-----------------------------------------------------------------------------------------
10000 | 0.38842567 | 0.405057 | 4.281727827
50000 | 7.22018834 | 7.605011334 | 5.329819333
75000 | 15.627181 | 16.38659034 | 4.859541462
100000 | 27.37910867 | 28.8636873 | 5.422304458I have attached the patch for the same
v9-0001 : distribute invalidation to inprogress transaction
v9-0002: Selective invalidation[1]:/messages/by-id/CANhcyEW4pq6+PO_eFn2q=23sgV1budN3y4SxpYBaKMJNADSDuA@mail.gmail.com
I have also prepared a bar chart for performance comparison between
HEAD, 0001 patch and (0001+0002) patch and attached here.
Thanks and Regards,
Shlok Kyal
Attachments:
Dear Shlok,
I have addressed the comment for 0002 patch and attached the patches.
Also, I have moved the tests in the 0002 to 0001 patch.
Thanks for updating the patch. 0002 patch seems to remove cache invalidations
from publication_invalidation_cb(). Related with it, I found an issue and had a concern.
1.
The replication continues even after ALTER PUBLICATION RENAME is executed.
For example - assuming that a subscriber subscribes only "pub":
```
pub=# INSERT INTO tab values (1);
INSERT 0 1
pub=# ALTER PUBLICATION pub RENAME TO pub1;
ALTER PUBLICATION
pub=# INSERT INTO tab values (2);
INSERT 0 1
sub=# SELECT * FROM tab ; -- (2) should not be replicated however...
a
---
1
2
(2 rows)
```
This happens because 1) ALTER PUBLICATION RENAME statement won't be invalidate the
relation cache, and 2) publications are reloaded only when invalid RelationSyncEntry
is found. In given example, the first INSERT creates the valid cache and second
INSERT reuses it. Therefore, the pubname-check is skipped.
For now, the actual renaming is done at AlterObjectRename_internal(), a generic
function. I think we must implement a dedecated function to publication and must
invalidate relcaches there.
2.
Similarly with above, the relcache won't be invalidated when ALTER PUBLICATION
OWNER TO is executed. This means that privilage checks may be ignored if the entry
is still valid. Not sure, but is there a possibility this causes an inconsistency?
Best regards,
Hayato Kuroda
FUJITSU LIMITED
2.
Similarly with above, the relcache won't be invalidated when ALTER
PUBLICATION
OWNER TO is executed. This means that privilage checks may be ignored if the
entry
is still valid. Not sure, but is there a possibility this causes an inconsistency?
Hmm, IIUC, the attribute pubowner is not used for now. The paragpargh
"There are currently no privileges on publications...." [1]https://www.postgresql.org/docs/devel/logical-replication-security.html may show the current
status. However, to keep the current behavior, I suggest to invalidate the relcache
of pubrelations when the owner is altered.
[1]: https://www.postgresql.org/docs/devel/logical-replication-security.html
Best regards,
Hayato Kuroda
FUJITSU LIMITED
I have addressed the comment for 0002 patch and attached the patches.
Also, I have moved the tests in the 0002 to 0001 patch.Thanks for updating the patch. 0002 patch seems to remove cache invalidations
from publication_invalidation_cb(). Related with it, I found an issue and had a concern.1.
The replication continues even after ALTER PUBLICATION RENAME is executed.
For example - assuming that a subscriber subscribes only "pub":```
pub=# INSERT INTO tab values (1);
INSERT 0 1
pub=# ALTER PUBLICATION pub RENAME TO pub1;
ALTER PUBLICATION
pub=# INSERT INTO tab values (2);
INSERT 0 1sub=# SELECT * FROM tab ; -- (2) should not be replicated however...
a
---
1
2
(2 rows)
```This happens because 1) ALTER PUBLICATION RENAME statement won't be invalidate the
relation cache, and 2) publications are reloaded only when invalid RelationSyncEntry
is found. In given example, the first INSERT creates the valid cache and second
INSERT reuses it. Therefore, the pubname-check is skipped.For now, the actual renaming is done at AlterObjectRename_internal(), a generic
function. I think we must implement a dedecated function to publication and must
invalidate relcaches there.2.
Similarly with above, the relcache won't be invalidated when ALTER PUBLICATION
OWNER TO is executed. This means that privilage checks may be ignored if the entry
is still valid. Not sure, but is there a possibility this causes an inconsistency?
Hi Kuroda-san,
Thanks for testing the patch. I have fixed the comments and attached
the updated patch.
I have added a callback function rel_sync_cache_publicationrel_cb().
This callback invalidates the cache of tables in a particular
publication.
This callback is called when there is some modification in
pg_publication catalog.
I have tested the two cases 'ALTER PUBLICATION ... RENAME TO ...' and
'ALTER PUBLICATION ... OWNER TO ...' and debugged it. The newly
added callback is called and it invalidates the cache of tables
present in that particular publication.
I have also added a test related to 'ALTER PUBLICATION ... RENAME TO
...' to 0001 patch.
Thanks and Regards,
Shlok Kyal
Attachments:
v11-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchapplication/octet-stream; name=v11-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchDownload
From 7eacfcd36cf86b35c443d32f4e7392a0f5a274dc Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 23 Aug 2024 14:02:20 +0530
Subject: [PATCH v11 1/2] Distribute invalidatons if change in catalog tables
Distribute invalidations to inprogress transactions if the current
committed transaction change any catalog table.
---
.../replication/logical/reorderbuffer.c | 5 +-
src/backend/replication/logical/snapbuild.c | 34 +-
src/include/replication/reorderbuffer.h | 4 +
src/test/subscription/t/100_bugs.pl | 291 ++++++++++++++++++
4 files changed, 320 insertions(+), 14 deletions(-)
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 22bcf171ff..c5dfc1ab06 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -221,9 +221,6 @@ int debug_logical_replication_streaming = DEBUG_LOGICAL_REP_STREAMING_BUFFERED
*/
static ReorderBufferTXN *ReorderBufferGetTXN(ReorderBuffer *rb);
static void ReorderBufferReturnTXN(ReorderBuffer *rb, ReorderBufferTXN *txn);
-static ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
- TransactionId xid, bool create, bool *is_new,
- XLogRecPtr lsn, bool create_as_top);
static void ReorderBufferTransferSnapToParent(ReorderBufferTXN *txn,
ReorderBufferTXN *subtxn);
@@ -622,7 +619,7 @@ ReorderBufferReturnRelids(ReorderBuffer *rb, Oid *relids)
* (with the given LSN, and as top transaction if that's specified);
* when this happens, is_new is set to true.
*/
-static ReorderBufferTXN *
+ReorderBufferTXN *
ReorderBufferTXNByXid(ReorderBuffer *rb, TransactionId xid, bool create,
bool *is_new, XLogRecPtr lsn, bool create_as_top)
{
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 0450f94ba8..42c947651b 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -300,7 +300,7 @@ static void SnapBuildFreeSnapshot(Snapshot snap);
static void SnapBuildSnapIncRefcount(Snapshot snap);
-static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn);
+static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
uint32 xinfo);
@@ -859,18 +859,21 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
}
/*
- * Add a new Snapshot to all transactions we're decoding that currently are
- * in-progress so they can see new catalog contents made by the transaction
- * that just committed. This is necessary because those in-progress
- * transactions will use the new catalog's contents from here on (at the very
- * least everything they do needs to be compatible with newer catalog
- * contents).
+ * Add a new Snapshot and invalidation messages to all transactions we're
+ * decoding that currently are in-progress so they can see new catalog contents
+ * made by the transaction that just committed. This is necessary because those
+ * in-progress transactions will use the new catalog's contents from here on
+ * (at the very least everything they do needs to be compatible with newer
+ * catalog contents).
*/
static void
-SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
+SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
+ ReorderBufferTXN *curr_txn;
+
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -913,6 +916,14 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
SnapBuildSnapIncRefcount(builder->snapshot);
ReorderBufferAddSnapshot(builder->reorder, txn->xid, lsn,
builder->snapshot);
+
+ /*
+ * Add invalidation messages to the reorder buffer of inprogress
+ * transactions except the current committed transaction
+ */
+ if (txn->xid != xid && curr_txn->ninvalidations > 0)
+ ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
+ curr_txn->ninvalidations, curr_txn->invalidations);
}
}
@@ -1184,8 +1195,11 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* refcount of the snapshot builder for the new snapshot */
SnapBuildSnapIncRefcount(builder->snapshot);
- /* add a new catalog snapshot to all currently running transactions */
- SnapBuildDistributeNewCatalogSnapshot(builder, lsn);
+ /*
+ * add a new catalog snapshot and invalidations messages to all
+ * currently running transactions
+ */
+ SnapBuildDistributeNewCatalogSnapshot(builder, lsn, xid);
}
}
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index e332635f70..093d21213a 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -743,6 +743,10 @@ extern TransactionId *ReorderBufferGetCatalogChangesXacts(ReorderBuffer *rb);
extern void ReorderBufferSetRestartPoint(ReorderBuffer *rb, XLogRecPtr ptr);
+extern ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
+ TransactionId xid, bool create, bool *is_new,
+ XLogRecPtr lsn, bool create_as_top);
+
extern void StartupReorderBuffer(void);
#endif
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..bdabe53e42 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,297 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', qq(DROP PUBLICATION pub1;));
+$node_subscriber->safe_psql('postgres', qq(DROP SUBSCRIPTION sub1;));
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+]);
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (1);
+]);
+
+# Add the table to the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even when
+# tables are dropped from the publication in presence of a concurrent active
+# transaction performing the DML on the same table.
+
+# Maintain an active transaction with the table that will be dropped from the
+# publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (4);
+]);
+
+# Maintain an active transaction with a schema table that will be dropped from the
+# publication.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (4);
+]);
+
+# Drop the table from the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (5);
+ INSERT INTO sch3.tab_conc VALUES (5);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is not replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that data for table tab_conc are not replicated to subscriber');
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table sch3.tab_conc are not replicated to subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is dropped in a concurrent active transaction.
+
+# Add tables to the publication.
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (6);
+]);
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (6);
+]);
+
+# Drop publication.
+$background_psql3->query_safe(
+ qq[
+ DROP PUBLICATION regress_pub1;
+]);
+
+# Perform an insert.
+$background_psql1->query_safe(
+ qq[
+ INSERT INTO tab_conc VALUES (7);
+]);
+
+$background_psql2->query_safe(
+ qq[
+ INSERT INTO sch3.tab_conc VALUES (7);
+]);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+# ERROR should appear on subscriber.
+my $offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION regress_sub1;');
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is renamed in a concurrent active transaction.
+
+# Create publication.
+$background_psql3->query_safe(
+ qq[
+ CREATE PUBLICATION regress_pub1 FOR TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Create subscription.
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (8);
+]);
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (8);
+]);
+
+# Rename publication.
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 RENAME TO regress_pub1_rename;
+]);
+
+# Perform an insert.
+$background_psql1->query_safe(
+ qq[
+ INSERT INTO tab_conc VALUES (9);
+]);
+
+$background_psql2->query_safe(
+ qq[
+ INSERT INTO sch3.tab_conc VALUES (9);
+]);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+# ERROR should appear on subscriber.
+$offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
v11-0002-Selective-Invalidation-of-Cache.patchapplication/octet-stream; name=v11-0002-Selective-Invalidation-of-Cache.patchDownload
From bda601d01c2cba6a6544e36d51fc62b79d8a3f53 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 27 Sep 2024 16:04:54 +0530
Subject: [PATCH v11 2/2] Selective Invalidation of Cache
When we alter a publication, add/drop namespace to/from publication
all the cache for all the tables are invalidated.
With this patch for the above operationns we will invalidate the
cache of only the desired tables.
Added a new callback function 'rel_sync_cache_publicationrel_cb' which
is called when there is any change in pg_publication catalog and it
invalidates the tables present in the publication modified.
---
src/backend/replication/pgoutput/pgoutput.c | 58 ++++++---
src/backend/utils/cache/inval.c | 130 +++++++++++++++++++-
src/include/storage/sinval.h | 9 ++
src/include/utils/inval.h | 4 +
4 files changed, 179 insertions(+), 22 deletions(-)
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 00e7024563..1d80d27d0f 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -132,6 +132,8 @@ typedef struct RelationSyncEntry
List *streamed_txns; /* streamed toplevel transactions with this
* schema */
+ List *pub_ids;
+
/* are we publishing this rel? */
PublicationActions pubactions;
@@ -216,6 +218,7 @@ static RelationSyncEntry *get_rel_sync_entry(PGOutputData *data,
static void rel_sync_cache_relation_cb(Datum arg, Oid relid);
static void rel_sync_cache_publication_cb(Datum arg, int cacheid,
uint32 hashvalue);
+static void rel_sync_cache_publicationrel_cb(Datum arg, Oid pubid);
static void set_schema_sent_in_streamed_txn(RelationSyncEntry *entry,
TransactionId xid);
static bool get_schema_sent_in_streamed_txn(RelationSyncEntry *entry,
@@ -1134,7 +1137,7 @@ init_tuple_slot(PGOutputData *data, Relation relation,
TupleDesc oldtupdesc;
TupleDesc newtupdesc;
- oldctx = MemoryContextSwitchTo(data->cachectx);
+ oldctx = MemoryContextSwitchTo(CacheMemoryContext);
/*
* Create tuple table slots. Create a copy of the TupleDesc as it needs to
@@ -1739,12 +1742,6 @@ static void
publication_invalidation_cb(Datum arg, int cacheid, uint32 hashvalue)
{
publications_valid = false;
-
- /*
- * Also invalidate per-relation cache so that next time the filtering info
- * is checked it will be updated with the new publication settings.
- */
- rel_sync_cache_publication_cb(arg, cacheid, hashvalue);
}
/*
@@ -1920,17 +1917,7 @@ init_rel_sync_cache(MemoryContext cachectx)
rel_sync_cache_publication_cb,
(Datum) 0);
- /*
- * Flush all cache entries after any publication changes. (We need no
- * callback entry for pg_publication, because publication_invalidation_cb
- * will take care of it.)
- */
- CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
- CacheRegisterSyscacheCallback(PUBLICATIONNAMESPACEMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
+ CacheRegisterPubcacheCallback(rel_sync_cache_publicationrel_cb, (Datum) 0);
relation_callbacks_registered = true;
}
@@ -2000,6 +1987,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
entry->publish_as_relid = InvalidOid;
entry->columns = NULL;
entry->attrmap = NULL;
+ entry->pub_ids = NIL;
}
/* Validate the entry */
@@ -2044,6 +2032,8 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
entry->schema_sent = false;
list_free(entry->streamed_txns);
entry->streamed_txns = NIL;
+ list_free(entry->pub_ids);
+ entry->pub_ids = NIL;
bms_free(entry->columns);
entry->columns = NULL;
entry->pubactions.pubinsert = false;
@@ -2108,6 +2098,10 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
pub_relid = llast_oid(ancestors);
ancestor_level = list_length(ancestors);
+
+ oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+ entry->pub_ids = lappend_oid(entry->pub_ids, pub->oid);
+ MemoryContextSwitchTo(oldctx);
}
}
@@ -2145,7 +2139,12 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
if (list_member_oid(pubids, pub->oid) ||
list_member_oid(schemaPubids, pub->oid) ||
ancestor_published)
+ {
publish = true;
+ oldctx = MemoryContextSwitchTo(CacheMemoryContext);
+ entry->pub_ids = lappend_oid(entry->pub_ids, pub->oid);
+ MemoryContextSwitchTo(oldctx);
+ }
}
/*
@@ -2318,6 +2317,29 @@ rel_sync_cache_relation_cb(Datum arg, Oid relid)
}
}
+/*
+ * Publication invalidation callback
+ */
+static void
+rel_sync_cache_publicationrel_cb(Datum arg, Oid pubid)
+{
+ HASH_SEQ_STATUS status;
+ RelationSyncEntry *entry;
+
+ if (RelationSyncCache == NULL)
+ return;
+
+ hash_seq_init(&status, RelationSyncCache);
+ while ((entry = (RelationSyncEntry *) hash_seq_search(&status)) != NULL)
+ {
+ if (entry->replicate_valid && list_member_oid(entry->pub_ids, pubid))
+ {
+ entry->replicate_valid = false;
+ entry->pub_ids = NIL;
+ }
+ }
+}
+
/*
* Publication relation/schema map syscache invalidation callback
*
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c
index 603aa4157b..a34be79ee6 100644
--- a/src/backend/utils/cache/inval.c
+++ b/src/backend/utils/cache/inval.c
@@ -160,6 +160,9 @@
*/
#define CatCacheMsgs 0
#define RelCacheMsgs 1
+#define PubCacheMsgs 2
+
+#define NumberofCache 3
/* Pointers to main arrays in TopTransactionContext */
typedef struct InvalMessageArray
@@ -168,13 +171,13 @@ typedef struct InvalMessageArray
int maxmsgs; /* current allocated size of array */
} InvalMessageArray;
-static InvalMessageArray InvalMessageArrays[2];
+static InvalMessageArray InvalMessageArrays[NumberofCache];
/* Control information for one logical group of messages */
typedef struct InvalidationMsgsGroup
{
- int firstmsg[2]; /* first index in relevant array */
- int nextmsg[2]; /* last+1 index */
+ int firstmsg[NumberofCache]; /* first index in relevant array */
+ int nextmsg[NumberofCache]; /* last+1 index */
} InvalidationMsgsGroup;
/* Macros to help preserve InvalidationMsgsGroup abstraction */
@@ -189,6 +192,7 @@ typedef struct InvalidationMsgsGroup
do { \
SetSubGroupToFollow(targetgroup, priorgroup, CatCacheMsgs); \
SetSubGroupToFollow(targetgroup, priorgroup, RelCacheMsgs); \
+ SetSubGroupToFollow(targetgroup, priorgroup, PubCacheMsgs); \
} while (0)
#define NumMessagesInSubGroup(group, subgroup) \
@@ -196,7 +200,8 @@ typedef struct InvalidationMsgsGroup
#define NumMessagesInGroup(group) \
(NumMessagesInSubGroup(group, CatCacheMsgs) + \
- NumMessagesInSubGroup(group, RelCacheMsgs))
+ NumMessagesInSubGroup(group, RelCacheMsgs) + \
+ NumMessagesInSubGroup(group, PubCacheMsgs))
/*----------------
@@ -251,6 +256,7 @@ int debug_discard_caches = 0;
#define MAX_SYSCACHE_CALLBACKS 64
#define MAX_RELCACHE_CALLBACKS 10
+#define MAX_PUBCACHE_CALLBACKS 10
static struct SYSCACHECALLBACK
{
@@ -272,6 +278,14 @@ static struct RELCACHECALLBACK
static int relcache_callback_count = 0;
+static struct PUBCACHECALLBACK
+{
+ PubcacheCallbackFunction function;
+ Datum arg;
+} pubcache_callback_list[MAX_PUBCACHE_CALLBACKS];
+
+static int pubcache_callback_count = 0;
+
/* ----------------------------------------------------------------
* Invalidation subgroup support functions
* ----------------------------------------------------------------
@@ -464,6 +478,38 @@ AddRelcacheInvalidationMessage(InvalidationMsgsGroup *group,
AddInvalidationMessage(group, RelCacheMsgs, &msg);
}
+/*
+ * Add a publication inval entry
+ */
+static void
+AddPubcacheInvalidationMessage(InvalidationMsgsGroup *group,
+ Oid dbId, Oid pubId)
+{
+ SharedInvalidationMessage msg;
+
+ /*
+ * Don't add a duplicate item. We assume dbId need not be checked because
+ * it will never change. InvalidOid for relId means all relations so we
+ * don't need to add individual ones when it is present.
+ */
+
+ ProcessMessageSubGroup(group, PubCacheMsgs,
+ if (msg->pc.id == SHAREDINVALPUBCACHE_ID &&
+ (msg->pc.pubId == pubId ||
+ msg->pc.pubId == InvalidOid))
+ return);
+
+
+ /* OK, add the item */
+ msg.pc.id = SHAREDINVALPUBCACHE_ID;
+ msg.pc.dbId = dbId;
+ msg.pc.pubId = pubId;
+ /* check AddCatcacheInvalidationMessage() for an explanation */
+ VALGRIND_MAKE_MEM_DEFINED(&msg, sizeof(msg));
+
+ AddInvalidationMessage(group, PubCacheMsgs, &msg);
+}
+
/*
* Add a snapshot inval entry
*
@@ -502,6 +548,7 @@ AppendInvalidationMessages(InvalidationMsgsGroup *dest,
{
AppendInvalidationMessageSubGroup(dest, src, CatCacheMsgs);
AppendInvalidationMessageSubGroup(dest, src, RelCacheMsgs);
+ AppendInvalidationMessageSubGroup(dest, src, PubCacheMsgs);
}
/*
@@ -516,6 +563,7 @@ ProcessInvalidationMessages(InvalidationMsgsGroup *group,
{
ProcessMessageSubGroup(group, CatCacheMsgs, func(msg));
ProcessMessageSubGroup(group, RelCacheMsgs, func(msg));
+ ProcessMessageSubGroup(group, PubCacheMsgs, func(msg));
}
/*
@@ -528,6 +576,7 @@ ProcessInvalidationMessagesMulti(InvalidationMsgsGroup *group,
{
ProcessMessageSubGroupMulti(group, CatCacheMsgs, func(msgs, n));
ProcessMessageSubGroupMulti(group, RelCacheMsgs, func(msgs, n));
+ ProcessMessageSubGroupMulti(group, PubCacheMsgs, func(msgs, n));
}
/* ----------------------------------------------------------------
@@ -590,6 +639,18 @@ RegisterRelcacheInvalidation(Oid dbId, Oid relId)
transInvalInfo->RelcacheInitFileInval = true;
}
+/*
+ * RegisterPubcacheInvalidation
+ *
+ * As above, but register a publication invalidation event.
+ */
+static void
+RegisterPubcacheInvalidation(Oid dbId, Oid pubId)
+{
+ AddPubcacheInvalidationMessage(&transInvalInfo->CurrentCmdInvalidMsgs,
+ dbId, pubId);
+}
+
/*
* RegisterSnapshotInvalidation
*
@@ -660,6 +721,8 @@ PrepareInvalidationState(void)
InvalMessageArrays[CatCacheMsgs].maxmsgs = 0;
InvalMessageArrays[RelCacheMsgs].msgs = NULL;
InvalMessageArrays[RelCacheMsgs].maxmsgs = 0;
+ InvalMessageArrays[PubCacheMsgs].msgs = NULL;
+ InvalMessageArrays[PubCacheMsgs].maxmsgs = 0;
}
transInvalInfo = myInfo;
@@ -773,6 +836,20 @@ LocalExecuteInvalidationMessage(SharedInvalidationMessage *msg)
else if (msg->sn.dbId == MyDatabaseId)
InvalidateCatalogSnapshot();
}
+ else if (msg->id == SHAREDINVALPUBCACHE_ID)
+ {
+ if (msg->pc.dbId == MyDatabaseId || msg->pc.dbId == InvalidOid)
+ {
+ int i;
+
+ for (i = 0; i < pubcache_callback_count; i++)
+ {
+ struct PUBCACHECALLBACK *pcitem = pubcache_callback_list + i;
+
+ pcitem->function(pcitem->arg, msg->pc.pubId);
+ }
+ }
+ }
else
elog(FATAL, "unrecognized SI message ID: %d", msg->id);
}
@@ -944,6 +1021,18 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
msgs,
n * sizeof(SharedInvalidationMessage)),
nmsgs += n));
+ ProcessMessageSubGroupMulti(&transInvalInfo->PriorCmdInvalidMsgs,
+ PubCacheMsgs,
+ (memcpy(msgarray + nmsgs,
+ msgs,
+ n * sizeof(SharedInvalidationMessage)),
+ nmsgs += n));
+ ProcessMessageSubGroupMulti(&transInvalInfo->CurrentCmdInvalidMsgs,
+ PubCacheMsgs,
+ (memcpy(msgarray + nmsgs,
+ msgs,
+ n * sizeof(SharedInvalidationMessage)),
+ nmsgs += n));
Assert(nmsgs == nummsgs);
return nmsgs;
@@ -1312,6 +1401,17 @@ CacheInvalidateHeapTuple(Relation relation,
else
return;
}
+ else if (tupleRelId == PublicationRelationId)
+ {
+ Form_pg_publication pubtup = (Form_pg_publication) GETSTRUCT(tuple);
+
+ /* get publication id */
+ relationId = pubtup->oid;
+ databaseId = MyDatabaseId;
+
+ RegisterPubcacheInvalidation(databaseId, relationId);
+ return;
+ }
else
return;
@@ -1567,6 +1667,25 @@ CacheRegisterRelcacheCallback(RelcacheCallbackFunction func,
++relcache_callback_count;
}
+/*
+ * CacheRegisterPubcacheCallback
+ * Register the specified function to be called for all future
+ * publication invalidation events. The OID of the publication being
+ * invalidated will be passed to the function.
+ */
+void
+CacheRegisterPubcacheCallback(PubcacheCallbackFunction func,
+ Datum arg)
+{
+ if (pubcache_callback_count >= MAX_PUBCACHE_CALLBACKS)
+ elog(FATAL, "out of pubcache_callback_list slots");
+
+ pubcache_callback_list[pubcache_callback_count].function = func;
+ pubcache_callback_list[pubcache_callback_count].arg = arg;
+
+ ++pubcache_callback_count;
+}
+
/*
* CallSyscacheCallbacks
*
@@ -1629,6 +1748,9 @@ LogLogicalInvalidations(void)
ProcessMessageSubGroupMulti(group, RelCacheMsgs,
XLogRegisterData((char *) msgs,
n * sizeof(SharedInvalidationMessage)));
+ ProcessMessageSubGroupMulti(group, PubCacheMsgs,
+ XLogRegisterData((char *) msgs,
+ n * sizeof(SharedInvalidationMessage)));
XLogInsert(RM_XACT_ID, XLOG_XACT_INVALIDATIONS);
}
}
diff --git a/src/include/storage/sinval.h b/src/include/storage/sinval.h
index 8f5744b21b..9a97268b0a 100644
--- a/src/include/storage/sinval.h
+++ b/src/include/storage/sinval.h
@@ -110,6 +110,14 @@ typedef struct
Oid relId; /* relation ID */
} SharedInvalSnapshotMsg;
+#define SHAREDINVALPUBCACHE_ID (-6)
+typedef struct
+{
+ int8 id; /* type field --- must be first */
+ Oid dbId; /* database ID, or 0 if a shared relation */
+ Oid pubId; /* publication ID */
+} SharedInvalPubcacheMsg;
+
typedef union
{
int8 id; /* type field --- must be first */
@@ -119,6 +127,7 @@ typedef union
SharedInvalSmgrMsg sm;
SharedInvalRelmapMsg rm;
SharedInvalSnapshotMsg sn;
+ SharedInvalPubcacheMsg pc;
} SharedInvalidationMessage;
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.h
index 24695facf2..66d27b8bee 100644
--- a/src/include/utils/inval.h
+++ b/src/include/utils/inval.h
@@ -22,6 +22,7 @@ extern PGDLLIMPORT int debug_discard_caches;
typedef void (*SyscacheCallbackFunction) (Datum arg, int cacheid, uint32 hashvalue);
typedef void (*RelcacheCallbackFunction) (Datum arg, Oid relid);
+typedef void (*PubcacheCallbackFunction) (Datum arg, Oid pubid);
extern void AcceptInvalidationMessages(void);
@@ -59,6 +60,9 @@ extern void CacheRegisterSyscacheCallback(int cacheid,
extern void CacheRegisterRelcacheCallback(RelcacheCallbackFunction func,
Datum arg);
+extern void CacheRegisterPubcacheCallback(PubcacheCallbackFunction func,
+ Datum arg);
+
extern void CallSyscacheCallbacks(int cacheid, uint32 hashvalue);
extern void InvalidateSystemCaches(void);
--
2.34.1
On Mon, 30 Sept 2024 at 16:56, Hayato Kuroda (Fujitsu)
<kuroda.hayato@fujitsu.com> wrote:
2.
Similarly with above, the relcache won't be invalidated when ALTER
PUBLICATION
OWNER TO is executed. This means that privilage checks may be ignored if the
entry
is still valid. Not sure, but is there a possibility this causes an inconsistency?Hmm, IIUC, the attribute pubowner is not used for now. The paragpargh
"There are currently no privileges on publications...." [1] may show the current
status. However, to keep the current behavior, I suggest to invalidate the relcache
of pubrelations when the owner is altered.[1]: https://www.postgresql.org/docs/devel/logical-replication-security.html
I have shared the updated patch [1]/messages/by-id/CANhcyEWEXL3rxvKH9-Xtx-DgGX0D62EktHpW+nG+MSSaMVUVig@mail.gmail.com.
So now, when 'ALTER .. PUBLICATION .. OWNER TO ..' is executed the
relcache is invalidated for that specific publication.
[1]: /messages/by-id/CANhcyEWEXL3rxvKH9-Xtx-DgGX0D62EktHpW+nG+MSSaMVUVig@mail.gmail.com
Thanks and Regards,
Shlok Kyal
Dear Shlok-san,
Thanks for updating the patch. Here are comments.
1.
I feel the name of SnapBuildDistributeNewCatalogSnapshot() should be updated because it
distributes two objects: catalog snapshot and invalidation messages. Do you have good one
in your mind? I considered "SnapBuildDistributeNewCatalogSnapshotAndInValidations" or
"SnapBuildDistributeItems" but seems not good :-(.
2.
Hmm, still, it is overengineering for me to add a new type of invalidation message
only for the publication. According to the ExecRenameStmt() we can implement an
arbitrary rename function like RenameConstraint() and RenameDatabase().
Regaring the ALTER PUBLICATION OWNER TO, I feel adding CacheInvalidateRelcacheAll()
and InvalidatePublicationRels() is enough.
I attached a PoC which implements above. It could pass tests on my env. Could you
please see it tell me how you think?
Best regards,
Hayato Kuroda
FUJITSU LIMITED
Attachments:
add_invalidations.diffsapplication/octet-stream; name=add_invalidations.diffsDownload
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 4f99ebb447..395fe530b3 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -399,6 +399,9 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TYPE:
return RenameType(stmt);
+ case OBJECT_PUBLICATION:
+ return RenamePublication(stmt->subname, stmt->newname);
+
case OBJECT_AGGREGATE:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
@@ -416,7 +419,6 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TSDICTIONARY:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
- case OBJECT_PUBLICATION:
case OBJECT_SUBSCRIPTION:
{
ObjectAddress address;
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index d6ffef374e..86b270d1cf 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -433,6 +433,85 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
return result;
}
+/*
+ * Execute ALTER PUBLICATION RENAME
+ */
+ObjectAddress
+RenamePublication(const char *oldname, const char *newname)
+{
+ Relation rel;
+ HeapTuple tup;
+ ObjectAddress address;
+ Form_pg_publication pubform;
+ bool replaces[Natts_pg_publication];
+ bool nulls[Natts_pg_publication];
+ Datum values[Natts_pg_publication];
+
+ rel = table_open(PublicationRelationId, RowExclusiveLock);
+
+ tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+ CStringGetDatum(oldname));
+
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("publication \"%s\" does not exist",
+ oldname)));
+
+ pubform = (Form_pg_publication) GETSTRUCT(tup);
+
+ /* must be owner */
+ if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+ NameStr(pubform->pubname));
+
+ /* Everything ok, form a new tuple. */
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));
+
+ /* Only update the pubname */
+ values[Anum_pg_publication_pubname - 1] =
+ DirectFunctionCall1(namein, CStringGetDatum(newname));
+ replaces[Anum_pg_publication_pubname - 1] = true;
+
+ tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+ replaces);
+
+ /* Invalidate the relcache. */
+ if (pubform->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * XXX: all tables in the tree is listed now, but this may be too much.
+ */
+ relids = GetPublicationRelations(pubform->oid,
+ PUBLICATION_PART_ALL);
+ schemarelids = GetAllSchemaPublicationRelations(pubform->oid,
+ PUBLICATION_PART_ALL);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ ObjectAddressSet(address, PublicationRelationId, pubform->oid);
+
+ heap_freetuple(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
/* check_functions_in_node callback */
static bool
contain_mutable_or_user_functions_checker(Oid func_id, void *context)
@@ -1920,6 +1999,30 @@ AlterPublicationOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
}
form->pubowner = newOwnerId;
+
+ /* Invalidate the relcache. */
+ if (form->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * XXX: all tables in the tree is listed now, but this may be too much.
+ */
+ relids = GetPublicationRelations(form->oid,
+ PUBLICATION_PART_ALL);
+ schemarelids = GetAllSchemaPublicationRelations(form->oid,
+ PUBLICATION_PART_ALL);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
CatalogTupleUpdate(rel, &tup->t_self, tup);
/* Update owner dependency reference */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4aa8646af7..ec10bfdd8c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9466,7 +9466,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_PUBLICATION;
- n->object = (Node *) makeString($3);
+ n->subname = $3;
n->newname = $6;
n->missing_ok = false;
$$ = (Node *) n;
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 1d80d27d0f..b280532a3a 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -218,7 +218,6 @@ static RelationSyncEntry *get_rel_sync_entry(PGOutputData *data,
static void rel_sync_cache_relation_cb(Datum arg, Oid relid);
static void rel_sync_cache_publication_cb(Datum arg, int cacheid,
uint32 hashvalue);
-static void rel_sync_cache_publicationrel_cb(Datum arg, Oid pubid);
static void set_schema_sent_in_streamed_txn(RelationSyncEntry *entry,
TransactionId xid);
static bool get_schema_sent_in_streamed_txn(RelationSyncEntry *entry,
@@ -1917,8 +1916,6 @@ init_rel_sync_cache(MemoryContext cachectx)
rel_sync_cache_publication_cb,
(Datum) 0);
- CacheRegisterPubcacheCallback(rel_sync_cache_publicationrel_cb, (Datum) 0);
-
relation_callbacks_registered = true;
}
@@ -2317,29 +2314,6 @@ rel_sync_cache_relation_cb(Datum arg, Oid relid)
}
}
-/*
- * Publication invalidation callback
- */
-static void
-rel_sync_cache_publicationrel_cb(Datum arg, Oid pubid)
-{
- HASH_SEQ_STATUS status;
- RelationSyncEntry *entry;
-
- if (RelationSyncCache == NULL)
- return;
-
- hash_seq_init(&status, RelationSyncCache);
- while ((entry = (RelationSyncEntry *) hash_seq_search(&status)) != NULL)
- {
- if (entry->replicate_valid && list_member_oid(entry->pub_ids, pubid))
- {
- entry->replicate_valid = false;
- entry->pub_ids = NIL;
- }
- }
-}
-
/*
* Publication relation/schema map syscache invalidation callback
*
diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c
index a34be79ee6..53f00dfd13 100644
--- a/src/backend/utils/cache/inval.c
+++ b/src/backend/utils/cache/inval.c
@@ -160,9 +160,6 @@
*/
#define CatCacheMsgs 0
#define RelCacheMsgs 1
-#define PubCacheMsgs 2
-
-#define NumberofCache 3
/* Pointers to main arrays in TopTransactionContext */
typedef struct InvalMessageArray
@@ -171,13 +168,13 @@ typedef struct InvalMessageArray
int maxmsgs; /* current allocated size of array */
} InvalMessageArray;
-static InvalMessageArray InvalMessageArrays[NumberofCache];
+static InvalMessageArray InvalMessageArrays[2];
/* Control information for one logical group of messages */
typedef struct InvalidationMsgsGroup
{
- int firstmsg[NumberofCache]; /* first index in relevant array */
- int nextmsg[NumberofCache]; /* last+1 index */
+ int firstmsg[2]; /* first index in relevant array */
+ int nextmsg[2]; /* last+1 index */
} InvalidationMsgsGroup;
/* Macros to help preserve InvalidationMsgsGroup abstraction */
@@ -192,7 +189,6 @@ typedef struct InvalidationMsgsGroup
do { \
SetSubGroupToFollow(targetgroup, priorgroup, CatCacheMsgs); \
SetSubGroupToFollow(targetgroup, priorgroup, RelCacheMsgs); \
- SetSubGroupToFollow(targetgroup, priorgroup, PubCacheMsgs); \
} while (0)
#define NumMessagesInSubGroup(group, subgroup) \
@@ -200,9 +196,7 @@ typedef struct InvalidationMsgsGroup
#define NumMessagesInGroup(group) \
(NumMessagesInSubGroup(group, CatCacheMsgs) + \
- NumMessagesInSubGroup(group, RelCacheMsgs) + \
- NumMessagesInSubGroup(group, PubCacheMsgs))
-
+ NumMessagesInSubGroup(group, RelCacheMsgs))
/*----------------
* Invalidation messages are divided into two groups:
@@ -256,7 +250,6 @@ int debug_discard_caches = 0;
#define MAX_SYSCACHE_CALLBACKS 64
#define MAX_RELCACHE_CALLBACKS 10
-#define MAX_PUBCACHE_CALLBACKS 10
static struct SYSCACHECALLBACK
{
@@ -278,14 +271,6 @@ static struct RELCACHECALLBACK
static int relcache_callback_count = 0;
-static struct PUBCACHECALLBACK
-{
- PubcacheCallbackFunction function;
- Datum arg;
-} pubcache_callback_list[MAX_PUBCACHE_CALLBACKS];
-
-static int pubcache_callback_count = 0;
-
/* ----------------------------------------------------------------
* Invalidation subgroup support functions
* ----------------------------------------------------------------
@@ -478,38 +463,6 @@ AddRelcacheInvalidationMessage(InvalidationMsgsGroup *group,
AddInvalidationMessage(group, RelCacheMsgs, &msg);
}
-/*
- * Add a publication inval entry
- */
-static void
-AddPubcacheInvalidationMessage(InvalidationMsgsGroup *group,
- Oid dbId, Oid pubId)
-{
- SharedInvalidationMessage msg;
-
- /*
- * Don't add a duplicate item. We assume dbId need not be checked because
- * it will never change. InvalidOid for relId means all relations so we
- * don't need to add individual ones when it is present.
- */
-
- ProcessMessageSubGroup(group, PubCacheMsgs,
- if (msg->pc.id == SHAREDINVALPUBCACHE_ID &&
- (msg->pc.pubId == pubId ||
- msg->pc.pubId == InvalidOid))
- return);
-
-
- /* OK, add the item */
- msg.pc.id = SHAREDINVALPUBCACHE_ID;
- msg.pc.dbId = dbId;
- msg.pc.pubId = pubId;
- /* check AddCatcacheInvalidationMessage() for an explanation */
- VALGRIND_MAKE_MEM_DEFINED(&msg, sizeof(msg));
-
- AddInvalidationMessage(group, PubCacheMsgs, &msg);
-}
-
/*
* Add a snapshot inval entry
*
@@ -548,7 +501,6 @@ AppendInvalidationMessages(InvalidationMsgsGroup *dest,
{
AppendInvalidationMessageSubGroup(dest, src, CatCacheMsgs);
AppendInvalidationMessageSubGroup(dest, src, RelCacheMsgs);
- AppendInvalidationMessageSubGroup(dest, src, PubCacheMsgs);
}
/*
@@ -563,7 +515,6 @@ ProcessInvalidationMessages(InvalidationMsgsGroup *group,
{
ProcessMessageSubGroup(group, CatCacheMsgs, func(msg));
ProcessMessageSubGroup(group, RelCacheMsgs, func(msg));
- ProcessMessageSubGroup(group, PubCacheMsgs, func(msg));
}
/*
@@ -576,7 +527,6 @@ ProcessInvalidationMessagesMulti(InvalidationMsgsGroup *group,
{
ProcessMessageSubGroupMulti(group, CatCacheMsgs, func(msgs, n));
ProcessMessageSubGroupMulti(group, RelCacheMsgs, func(msgs, n));
- ProcessMessageSubGroupMulti(group, PubCacheMsgs, func(msgs, n));
}
/* ----------------------------------------------------------------
@@ -639,18 +589,6 @@ RegisterRelcacheInvalidation(Oid dbId, Oid relId)
transInvalInfo->RelcacheInitFileInval = true;
}
-/*
- * RegisterPubcacheInvalidation
- *
- * As above, but register a publication invalidation event.
- */
-static void
-RegisterPubcacheInvalidation(Oid dbId, Oid pubId)
-{
- AddPubcacheInvalidationMessage(&transInvalInfo->CurrentCmdInvalidMsgs,
- dbId, pubId);
-}
-
/*
* RegisterSnapshotInvalidation
*
@@ -721,8 +659,6 @@ PrepareInvalidationState(void)
InvalMessageArrays[CatCacheMsgs].maxmsgs = 0;
InvalMessageArrays[RelCacheMsgs].msgs = NULL;
InvalMessageArrays[RelCacheMsgs].maxmsgs = 0;
- InvalMessageArrays[PubCacheMsgs].msgs = NULL;
- InvalMessageArrays[PubCacheMsgs].maxmsgs = 0;
}
transInvalInfo = myInfo;
@@ -836,20 +772,6 @@ LocalExecuteInvalidationMessage(SharedInvalidationMessage *msg)
else if (msg->sn.dbId == MyDatabaseId)
InvalidateCatalogSnapshot();
}
- else if (msg->id == SHAREDINVALPUBCACHE_ID)
- {
- if (msg->pc.dbId == MyDatabaseId || msg->pc.dbId == InvalidOid)
- {
- int i;
-
- for (i = 0; i < pubcache_callback_count; i++)
- {
- struct PUBCACHECALLBACK *pcitem = pubcache_callback_list + i;
-
- pcitem->function(pcitem->arg, msg->pc.pubId);
- }
- }
- }
else
elog(FATAL, "unrecognized SI message ID: %d", msg->id);
}
@@ -1021,18 +943,6 @@ xactGetCommittedInvalidationMessages(SharedInvalidationMessage **msgs,
msgs,
n * sizeof(SharedInvalidationMessage)),
nmsgs += n));
- ProcessMessageSubGroupMulti(&transInvalInfo->PriorCmdInvalidMsgs,
- PubCacheMsgs,
- (memcpy(msgarray + nmsgs,
- msgs,
- n * sizeof(SharedInvalidationMessage)),
- nmsgs += n));
- ProcessMessageSubGroupMulti(&transInvalInfo->CurrentCmdInvalidMsgs,
- PubCacheMsgs,
- (memcpy(msgarray + nmsgs,
- msgs,
- n * sizeof(SharedInvalidationMessage)),
- nmsgs += n));
Assert(nmsgs == nummsgs);
return nmsgs;
@@ -1401,17 +1311,6 @@ CacheInvalidateHeapTuple(Relation relation,
else
return;
}
- else if (tupleRelId == PublicationRelationId)
- {
- Form_pg_publication pubtup = (Form_pg_publication) GETSTRUCT(tuple);
-
- /* get publication id */
- relationId = pubtup->oid;
- databaseId = MyDatabaseId;
-
- RegisterPubcacheInvalidation(databaseId, relationId);
- return;
- }
else
return;
@@ -1667,25 +1566,6 @@ CacheRegisterRelcacheCallback(RelcacheCallbackFunction func,
++relcache_callback_count;
}
-/*
- * CacheRegisterPubcacheCallback
- * Register the specified function to be called for all future
- * publication invalidation events. The OID of the publication being
- * invalidated will be passed to the function.
- */
-void
-CacheRegisterPubcacheCallback(PubcacheCallbackFunction func,
- Datum arg)
-{
- if (pubcache_callback_count >= MAX_PUBCACHE_CALLBACKS)
- elog(FATAL, "out of pubcache_callback_list slots");
-
- pubcache_callback_list[pubcache_callback_count].function = func;
- pubcache_callback_list[pubcache_callback_count].arg = arg;
-
- ++pubcache_callback_count;
-}
-
/*
* CallSyscacheCallbacks
*
@@ -1748,9 +1628,6 @@ LogLogicalInvalidations(void)
ProcessMessageSubGroupMulti(group, RelCacheMsgs,
XLogRegisterData((char *) msgs,
n * sizeof(SharedInvalidationMessage)));
- ProcessMessageSubGroupMulti(group, PubCacheMsgs,
- XLogRegisterData((char *) msgs,
- n * sizeof(SharedInvalidationMessage)));
XLogInsert(RM_XACT_ID, XLOG_XACT_INVALIDATIONS);
}
}
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 5487c571f6..b953193812 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -35,5 +35,6 @@ extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
List *ancestors, bool pubviaroot);
extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
List *ancestors, bool pubviaroot);
+extern ObjectAddress RenamePublication(const char *oldname, const char *newname);
#endif /* PUBLICATIONCMDS_H */
diff --git a/src/include/storage/sinval.h b/src/include/storage/sinval.h
index 9a97268b0a..8f5744b21b 100644
--- a/src/include/storage/sinval.h
+++ b/src/include/storage/sinval.h
@@ -110,14 +110,6 @@ typedef struct
Oid relId; /* relation ID */
} SharedInvalSnapshotMsg;
-#define SHAREDINVALPUBCACHE_ID (-6)
-typedef struct
-{
- int8 id; /* type field --- must be first */
- Oid dbId; /* database ID, or 0 if a shared relation */
- Oid pubId; /* publication ID */
-} SharedInvalPubcacheMsg;
-
typedef union
{
int8 id; /* type field --- must be first */
@@ -127,7 +119,6 @@ typedef union
SharedInvalSmgrMsg sm;
SharedInvalRelmapMsg rm;
SharedInvalSnapshotMsg sn;
- SharedInvalPubcacheMsg pc;
} SharedInvalidationMessage;
diff --git a/src/include/utils/inval.h b/src/include/utils/inval.h
index 66d27b8bee..24695facf2 100644
--- a/src/include/utils/inval.h
+++ b/src/include/utils/inval.h
@@ -22,7 +22,6 @@ extern PGDLLIMPORT int debug_discard_caches;
typedef void (*SyscacheCallbackFunction) (Datum arg, int cacheid, uint32 hashvalue);
typedef void (*RelcacheCallbackFunction) (Datum arg, Oid relid);
-typedef void (*PubcacheCallbackFunction) (Datum arg, Oid pubid);
extern void AcceptInvalidationMessages(void);
@@ -60,9 +59,6 @@ extern void CacheRegisterSyscacheCallback(int cacheid,
extern void CacheRegisterRelcacheCallback(RelcacheCallbackFunction func,
Datum arg);
-extern void CacheRegisterPubcacheCallback(PubcacheCallbackFunction func,
- Datum arg);
-
extern void CallSyscacheCallbacks(int cacheid, uint32 hashvalue);
extern void InvalidateSystemCaches(void);
Hi Kuroda-san,
Thanks for reviewing the patch.
1.
I feel the name of SnapBuildDistributeNewCatalogSnapshot() should be updated because it
distributes two objects: catalog snapshot and invalidation messages. Do you have good one
in your mind? I considered "SnapBuildDistributeNewCatalogSnapshotAndInValidations" or
"SnapBuildDistributeItems" but seems not good :-(.
I have renamed the function to 'SnapBuildDistributeSnapshotAndInval'. Thoughts?
2.
Hmm, still, it is overengineering for me to add a new type of invalidation message
only for the publication. According to the ExecRenameStmt() we can implement an
arbitrary rename function like RenameConstraint() and RenameDatabase().
Regaring the ALTER PUBLICATION OWNER TO, I feel adding CacheInvalidateRelcacheAll()
and InvalidatePublicationRels() is enough.
I agree with you.
I attached a PoC which implements above. It could pass tests on my env. Could you
please see it tell me how you think?
I have tested the POC and it is working as expected. The changes look
fine to me. I have created a patch for the same.
Currently, we are passing 'PUBLICATION_PART_ALL' as an argument to
function 'GetPublicationRelations' and
'GetAllSchemaPublicationRelations'. Need to check if we can use
'PUBLICATION_PART_ROOT' or 'PUBLICATION_PART_LEAF' depending on the
'publish_via_partition_root' option. Will test and address this in the
next version of the patch. For now, I have added a TODO.
Thanks and Regards,
Shlok Kyal
Attachments:
v12-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchapplication/octet-stream; name=v12-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchDownload
From 2969ff336fbd1bd764770a9e8314fcb62a6f1eab Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 23 Aug 2024 14:02:20 +0530
Subject: [PATCH v12 1/2] Distribute invalidatons if change in catalog tables
Distribute invalidations to inprogress transactions if the current
committed transaction change any catalog table.
---
.../replication/logical/reorderbuffer.c | 5 +-
src/backend/replication/logical/snapbuild.c | 34 +-
src/include/replication/reorderbuffer.h | 4 +
src/test/subscription/t/100_bugs.pl | 291 ++++++++++++++++++
4 files changed, 320 insertions(+), 14 deletions(-)
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 22bcf171ff..c5dfc1ab06 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -221,9 +221,6 @@ int debug_logical_replication_streaming = DEBUG_LOGICAL_REP_STREAMING_BUFFERED
*/
static ReorderBufferTXN *ReorderBufferGetTXN(ReorderBuffer *rb);
static void ReorderBufferReturnTXN(ReorderBuffer *rb, ReorderBufferTXN *txn);
-static ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
- TransactionId xid, bool create, bool *is_new,
- XLogRecPtr lsn, bool create_as_top);
static void ReorderBufferTransferSnapToParent(ReorderBufferTXN *txn,
ReorderBufferTXN *subtxn);
@@ -622,7 +619,7 @@ ReorderBufferReturnRelids(ReorderBuffer *rb, Oid *relids)
* (with the given LSN, and as top transaction if that's specified);
* when this happens, is_new is set to true.
*/
-static ReorderBufferTXN *
+ReorderBufferTXN *
ReorderBufferTXNByXid(ReorderBuffer *rb, TransactionId xid, bool create,
bool *is_new, XLogRecPtr lsn, bool create_as_top)
{
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 0450f94ba8..1f7c24cad0 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -300,7 +300,7 @@ static void SnapBuildFreeSnapshot(Snapshot snap);
static void SnapBuildSnapIncRefcount(Snapshot snap);
-static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn);
+static void SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
uint32 xinfo);
@@ -859,18 +859,21 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
}
/*
- * Add a new Snapshot to all transactions we're decoding that currently are
- * in-progress so they can see new catalog contents made by the transaction
- * that just committed. This is necessary because those in-progress
- * transactions will use the new catalog's contents from here on (at the very
- * least everything they do needs to be compatible with newer catalog
- * contents).
+ * Add a new Snapshot and invalidation messages to all transactions we're
+ * decoding that currently are in-progress so they can see new catalog contents
+ * made by the transaction that just committed. This is necessary because those
+ * in-progress transactions will use the new catalog's contents from here on
+ * (at the very least everything they do needs to be compatible with newer
+ * catalog contents).
*/
static void
-SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
+SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
+ ReorderBufferTXN *curr_txn;
+
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -913,6 +916,14 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
SnapBuildSnapIncRefcount(builder->snapshot);
ReorderBufferAddSnapshot(builder->reorder, txn->xid, lsn,
builder->snapshot);
+
+ /*
+ * Add invalidation messages to the reorder buffer of inprogress
+ * transactions except the current committed transaction
+ */
+ if (txn->xid != xid && curr_txn->ninvalidations > 0)
+ ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
+ curr_txn->ninvalidations, curr_txn->invalidations);
}
}
@@ -1184,8 +1195,11 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* refcount of the snapshot builder for the new snapshot */
SnapBuildSnapIncRefcount(builder->snapshot);
- /* add a new catalog snapshot to all currently running transactions */
- SnapBuildDistributeNewCatalogSnapshot(builder, lsn);
+ /*
+ * add a new catalog snapshot and invalidations messages to all
+ * currently running transactions
+ */
+ SnapBuildDistributeSnapshotAndInval(builder, lsn, xid);
}
}
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index e332635f70..093d21213a 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -743,6 +743,10 @@ extern TransactionId *ReorderBufferGetCatalogChangesXacts(ReorderBuffer *rb);
extern void ReorderBufferSetRestartPoint(ReorderBuffer *rb, XLogRecPtr ptr);
+extern ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
+ TransactionId xid, bool create, bool *is_new,
+ XLogRecPtr lsn, bool create_as_top);
+
extern void StartupReorderBuffer(void);
#endif
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..bdabe53e42 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,297 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', qq(DROP PUBLICATION pub1;));
+$node_subscriber->safe_psql('postgres', qq(DROP SUBSCRIPTION sub1;));
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+]);
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (1);
+]);
+
+# Add the table to the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ 'ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION');
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even when
+# tables are dropped from the publication in presence of a concurrent active
+# transaction performing the DML on the same table.
+
+# Maintain an active transaction with the table that will be dropped from the
+# publication.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (4);
+]);
+
+# Maintain an active transaction with a schema table that will be dropped from the
+# publication.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (4);
+]);
+
+# Drop the table from the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (5);
+ INSERT INTO sch3.tab_conc VALUES (5);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is not replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that data for table tab_conc are not replicated to subscriber');
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table sch3.tab_conc are not replicated to subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is dropped in a concurrent active transaction.
+
+# Add tables to the publication.
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (6);
+]);
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (6);
+]);
+
+# Drop publication.
+$background_psql3->query_safe(
+ qq[
+ DROP PUBLICATION regress_pub1;
+]);
+
+# Perform an insert.
+$background_psql1->query_safe(
+ qq[
+ INSERT INTO tab_conc VALUES (7);
+]);
+
+$background_psql2->query_safe(
+ qq[
+ INSERT INTO sch3.tab_conc VALUES (7);
+]);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+# ERROR should appear on subscriber.
+my $offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION regress_sub1;');
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is renamed in a concurrent active transaction.
+
+# Create publication.
+$background_psql3->query_safe(
+ qq[
+ CREATE PUBLICATION regress_pub1 FOR TABLE tab_conc, TABLES IN SCHEMA sch3;
+]);
+
+# Create subscription.
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO tab_conc VALUES (8);
+]);
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq[
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (8);
+]);
+
+# Rename publication.
+$background_psql3->query_safe(
+ qq[
+ ALTER PUBLICATION regress_pub1 RENAME TO regress_pub1_rename;
+]);
+
+# Perform an insert.
+$background_psql1->query_safe(
+ qq[
+ INSERT INTO tab_conc VALUES (9);
+]);
+
+$background_psql2->query_safe(
+ qq[
+ INSERT INTO sch3.tab_conc VALUES (9);
+]);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe(qq[COMMIT]);
+$background_psql2->query_safe(qq[COMMIT]);
+
+# ERROR should appear on subscriber.
+$offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
v12-0002-Selective-Invalidation-of-Cache.patchapplication/octet-stream; name=v12-0002-Selective-Invalidation-of-Cache.patchDownload
From edd4564e5750623b2f965da0ed5130cc09ad5388 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 4 Oct 2024 12:25:31 +0530
Subject: [PATCH v12 2/2] Selective Invalidation of Cache
When we alter a publication, add/drop namespace to/from publication
all the cache for all the tables are invalidated.
With this patch for the above operationns we will invalidate the
cache of only the desired tables.
---
src/backend/commands/alter.c | 4 +-
src/backend/commands/publicationcmds.c | 105 ++++++++++++++++++++
src/backend/parser/gram.y | 2 +-
src/backend/replication/pgoutput/pgoutput.c | 18 ----
src/include/commands/publicationcmds.h | 1 +
5 files changed, 110 insertions(+), 20 deletions(-)
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 4f99ebb447..395fe530b3 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -399,6 +399,9 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TYPE:
return RenameType(stmt);
+ case OBJECT_PUBLICATION:
+ return RenamePublication(stmt->subname, stmt->newname);
+
case OBJECT_AGGREGATE:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
@@ -416,7 +419,6 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TSDICTIONARY:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
- case OBJECT_PUBLICATION:
case OBJECT_SUBSCRIPTION:
{
ObjectAddress address;
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index d6ffef374e..ae06aa254b 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -433,6 +433,86 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
return result;
}
+/*
+ * Execute ALTER PUBLICATION RENAME
+ */
+ObjectAddress
+RenamePublication(const char *oldname, const char *newname)
+{
+ Relation rel;
+ HeapTuple tup;
+ ObjectAddress address;
+ Form_pg_publication pubform;
+ bool replaces[Natts_pg_publication];
+ bool nulls[Natts_pg_publication];
+ Datum values[Natts_pg_publication];
+
+ rel = table_open(PublicationRelationId, RowExclusiveLock);
+
+ tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+ CStringGetDatum(oldname));
+
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("publication \"%s\" does not exist",
+ oldname)));
+
+ pubform = (Form_pg_publication) GETSTRUCT(tup);
+
+ /* must be owner */
+ if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+ NameStr(pubform->pubname));
+
+ /* Everything ok, form a new tuple. */
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));
+
+ /* Only update the pubname */
+ values[Anum_pg_publication_pubname - 1] =
+ DirectFunctionCall1(namein, CStringGetDatum(newname));
+ replaces[Anum_pg_publication_pubname - 1] = true;
+
+ tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+ replaces);
+
+ /* Invalidate the relcache. */
+ if (pubform->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * XXX: Should we pass different pub_partops depending on
+ * 'publish_via_partition_root'
+ */
+ relids = GetPublicationRelations(pubform->oid,
+ PUBLICATION_PART_ALL);
+ schemarelids = GetAllSchemaPublicationRelations(pubform->oid,
+ PUBLICATION_PART_ALL);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ ObjectAddressSet(address, PublicationRelationId, pubform->oid);
+
+ heap_freetuple(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
/* check_functions_in_node callback */
static bool
contain_mutable_or_user_functions_checker(Oid func_id, void *context)
@@ -1920,6 +2000,31 @@ AlterPublicationOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
}
form->pubowner = newOwnerId;
+
+ /* Invalidate the relcache. */
+ if (form->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * XXX: Should we pass different pub_partops depending on
+ * 'publish_via_partition_root'
+ */
+ relids = GetPublicationRelations(form->oid,
+ PUBLICATION_PART_ALL);
+ schemarelids = GetAllSchemaPublicationRelations(form->oid,
+ PUBLICATION_PART_ALL);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
CatalogTupleUpdate(rel, &tup->t_self, tup);
/* Update owner dependency reference */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4aa8646af7..ec10bfdd8c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9466,7 +9466,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_PUBLICATION;
- n->object = (Node *) makeString($3);
+ n->subname = $3;
n->newname = $6;
n->missing_ok = false;
$$ = (Node *) n;
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 00e7024563..b8429be8cf 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1739,12 +1739,6 @@ static void
publication_invalidation_cb(Datum arg, int cacheid, uint32 hashvalue)
{
publications_valid = false;
-
- /*
- * Also invalidate per-relation cache so that next time the filtering info
- * is checked it will be updated with the new publication settings.
- */
- rel_sync_cache_publication_cb(arg, cacheid, hashvalue);
}
/*
@@ -1920,18 +1914,6 @@ init_rel_sync_cache(MemoryContext cachectx)
rel_sync_cache_publication_cb,
(Datum) 0);
- /*
- * Flush all cache entries after any publication changes. (We need no
- * callback entry for pg_publication, because publication_invalidation_cb
- * will take care of it.)
- */
- CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
- CacheRegisterSyscacheCallback(PUBLICATIONNAMESPACEMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
-
relation_callbacks_registered = true;
}
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 5487c571f6..b953193812 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -35,5 +35,6 @@ extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
List *ancestors, bool pubviaroot);
extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
List *ancestors, bool pubviaroot);
+extern ObjectAddress RenamePublication(const char *oldname, const char *newname);
#endif /* PUBLICATIONCMDS_H */
--
2.34.1
On Fri, 4 Oct 2024 at 12:52, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
Hi Kuroda-san,
Thanks for reviewing the patch.
1.
I feel the name of SnapBuildDistributeNewCatalogSnapshot() should be updated because it
distributes two objects: catalog snapshot and invalidation messages. Do you have good one
in your mind? I considered "SnapBuildDistributeNewCatalogSnapshotAndInValidations" or
"SnapBuildDistributeItems" but seems not good :-(.I have renamed the function to 'SnapBuildDistributeSnapshotAndInval'. Thoughts?
2.
Hmm, still, it is overengineering for me to add a new type of invalidation message
only for the publication. According to the ExecRenameStmt() we can implement an
arbitrary rename function like RenameConstraint() and RenameDatabase().
Regaring the ALTER PUBLICATION OWNER TO, I feel adding CacheInvalidateRelcacheAll()
and InvalidatePublicationRels() is enough.I agree with you.
I attached a PoC which implements above. It could pass tests on my env. Could you
please see it tell me how you think?I have tested the POC and it is working as expected. The changes look
fine to me. I have created a patch for the same.
Currently, we are passing 'PUBLICATION_PART_ALL' as an argument to
function 'GetPublicationRelations' and
'GetAllSchemaPublicationRelations'. Need to check if we can use
'PUBLICATION_PART_ROOT' or 'PUBLICATION_PART_LEAF' depending on the
'publish_via_partition_root' option. Will test and address this in the
next version of the patch. For now, I have added a TODO.
I have tested this part. I observed that ,whenever we insert data in a
partition table, the function 'get_rel_sync_entry' is called and a
hash entry is created for the corresponding leaf node relid. So I feel
while invalidating here we can specify 'PUBLICATION_PART_LEAF' . I
have made the corresponding changes 0002 patch.
I have also modified the tests in 0001 patch. These changes are only
related to syntax of writing tests.
Thanks and Regards,
Shlok Kyal
Attachments:
v13-0002-Selective-Invalidation-of-Cache.patchapplication/octet-stream; name=v13-0002-Selective-Invalidation-of-Cache.patchDownload
From 0e422f51be8da9cc9f91233a5f7ff1321d515b40 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 4 Oct 2024 12:25:31 +0530
Subject: [PATCH v13 2/2] Selective Invalidation of Cache
When we alter a publication, add/drop namespace to/from publication
all the cache for all the tables are invalidated.
With this patch for the above operationns we will invalidate the
cache of only the desired tables.
---
src/backend/commands/alter.c | 4 +-
src/backend/commands/publicationcmds.c | 107 ++++++++++++++++++++
src/backend/parser/gram.y | 2 +-
src/backend/replication/pgoutput/pgoutput.c | 18 ----
src/include/commands/publicationcmds.h | 1 +
5 files changed, 112 insertions(+), 20 deletions(-)
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 4f99ebb447..395fe530b3 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -399,6 +399,9 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TYPE:
return RenameType(stmt);
+ case OBJECT_PUBLICATION:
+ return RenamePublication(stmt->subname, stmt->newname);
+
case OBJECT_AGGREGATE:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
@@ -416,7 +419,6 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TSDICTIONARY:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
- case OBJECT_PUBLICATION:
case OBJECT_SUBSCRIPTION:
{
ObjectAddress address;
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index d6ffef374e..b70091a3c6 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -433,6 +433,87 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
return result;
}
+/*
+ * Execute ALTER PUBLICATION RENAME
+ */
+ObjectAddress
+RenamePublication(const char *oldname, const char *newname)
+{
+ Relation rel;
+ HeapTuple tup;
+ ObjectAddress address;
+ Form_pg_publication pubform;
+ bool replaces[Natts_pg_publication];
+ bool nulls[Natts_pg_publication];
+ Datum values[Natts_pg_publication];
+
+ rel = table_open(PublicationRelationId, RowExclusiveLock);
+
+ tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+ CStringGetDatum(oldname));
+
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("publication \"%s\" does not exist",
+ oldname)));
+
+ pubform = (Form_pg_publication) GETSTRUCT(tup);
+
+ /* must be owner */
+ if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+ NameStr(pubform->pubname));
+
+ /* Everything ok, form a new tuple. */
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));
+
+ /* Only update the pubname */
+ values[Anum_pg_publication_pubname - 1] =
+ DirectFunctionCall1(namein, CStringGetDatum(newname));
+ replaces[Anum_pg_publication_pubname - 1] = true;
+
+ tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+ replaces);
+
+ /* Invalidate the relcache. */
+ if (pubform->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * For partition table, when we insert data, get_rel_sync_entry is called and
+ * a hash entry is created for the corresponding leaf table. So invalidating
+ * the leaf nodes would be sufficient here.
+ */
+ relids = GetPublicationRelations(pubform->oid,
+ PUBLICATION_PART_LEAF);
+ schemarelids = GetAllSchemaPublicationRelations(pubform->oid,
+ PUBLICATION_PART_LEAF);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ ObjectAddressSet(address, PublicationRelationId, pubform->oid);
+
+ heap_freetuple(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
/* check_functions_in_node callback */
static bool
contain_mutable_or_user_functions_checker(Oid func_id, void *context)
@@ -1920,6 +2001,32 @@ AlterPublicationOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
}
form->pubowner = newOwnerId;
+
+ /* Invalidate the relcache. */
+ if (form->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * For partition table, when we insert data, get_rel_sync_entry is called and
+ * a hash entry is created for the corresponding leaf table. So invalidating
+ * the leaf nodes would be sufficient here.
+ */
+ relids = GetPublicationRelations(form->oid,
+ PUBLICATION_PART_LEAF);
+ schemarelids = GetAllSchemaPublicationRelations(form->oid,
+ PUBLICATION_PART_LEAF);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
CatalogTupleUpdate(rel, &tup->t_self, tup);
/* Update owner dependency reference */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4aa8646af7..ec10bfdd8c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9466,7 +9466,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_PUBLICATION;
- n->object = (Node *) makeString($3);
+ n->subname = $3;
n->newname = $6;
n->missing_ok = false;
$$ = (Node *) n;
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 00e7024563..b8429be8cf 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1739,12 +1739,6 @@ static void
publication_invalidation_cb(Datum arg, int cacheid, uint32 hashvalue)
{
publications_valid = false;
-
- /*
- * Also invalidate per-relation cache so that next time the filtering info
- * is checked it will be updated with the new publication settings.
- */
- rel_sync_cache_publication_cb(arg, cacheid, hashvalue);
}
/*
@@ -1920,18 +1914,6 @@ init_rel_sync_cache(MemoryContext cachectx)
rel_sync_cache_publication_cb,
(Datum) 0);
- /*
- * Flush all cache entries after any publication changes. (We need no
- * callback entry for pg_publication, because publication_invalidation_cb
- * will take care of it.)
- */
- CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
- CacheRegisterSyscacheCallback(PUBLICATIONNAMESPACEMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
-
relation_callbacks_registered = true;
}
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 5487c571f6..b953193812 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -35,5 +35,6 @@ extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
List *ancestors, bool pubviaroot);
extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
List *ancestors, bool pubviaroot);
+extern ObjectAddress RenamePublication(const char *oldname, const char *newname);
#endif /* PUBLICATIONCMDS_H */
--
2.34.1
v13-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchapplication/octet-stream; name=v13-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchDownload
From 6c3c7500cf457bc71e66261c3569b150486d626f Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 23 Aug 2024 14:02:20 +0530
Subject: [PATCH v13 1/2] Distribute invalidatons if change in catalog tables
Distribute invalidations to inprogress transactions if the current
committed transaction change any catalog table.
---
.../replication/logical/reorderbuffer.c | 5 +-
src/backend/replication/logical/snapbuild.c | 34 ++-
src/include/replication/reorderbuffer.h | 4 +
src/test/subscription/t/100_bugs.pl | 267 ++++++++++++++++++
4 files changed, 296 insertions(+), 14 deletions(-)
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 22bcf171ff..c5dfc1ab06 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -221,9 +221,6 @@ int debug_logical_replication_streaming = DEBUG_LOGICAL_REP_STREAMING_BUFFERED
*/
static ReorderBufferTXN *ReorderBufferGetTXN(ReorderBuffer *rb);
static void ReorderBufferReturnTXN(ReorderBuffer *rb, ReorderBufferTXN *txn);
-static ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
- TransactionId xid, bool create, bool *is_new,
- XLogRecPtr lsn, bool create_as_top);
static void ReorderBufferTransferSnapToParent(ReorderBufferTXN *txn,
ReorderBufferTXN *subtxn);
@@ -622,7 +619,7 @@ ReorderBufferReturnRelids(ReorderBuffer *rb, Oid *relids)
* (with the given LSN, and as top transaction if that's specified);
* when this happens, is_new is set to true.
*/
-static ReorderBufferTXN *
+ReorderBufferTXN *
ReorderBufferTXNByXid(ReorderBuffer *rb, TransactionId xid, bool create,
bool *is_new, XLogRecPtr lsn, bool create_as_top)
{
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 0450f94ba8..1f7c24cad0 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -300,7 +300,7 @@ static void SnapBuildFreeSnapshot(Snapshot snap);
static void SnapBuildSnapIncRefcount(Snapshot snap);
-static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn);
+static void SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
uint32 xinfo);
@@ -859,18 +859,21 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
}
/*
- * Add a new Snapshot to all transactions we're decoding that currently are
- * in-progress so they can see new catalog contents made by the transaction
- * that just committed. This is necessary because those in-progress
- * transactions will use the new catalog's contents from here on (at the very
- * least everything they do needs to be compatible with newer catalog
- * contents).
+ * Add a new Snapshot and invalidation messages to all transactions we're
+ * decoding that currently are in-progress so they can see new catalog contents
+ * made by the transaction that just committed. This is necessary because those
+ * in-progress transactions will use the new catalog's contents from here on
+ * (at the very least everything they do needs to be compatible with newer
+ * catalog contents).
*/
static void
-SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
+SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
+ ReorderBufferTXN *curr_txn;
+
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -913,6 +916,14 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
SnapBuildSnapIncRefcount(builder->snapshot);
ReorderBufferAddSnapshot(builder->reorder, txn->xid, lsn,
builder->snapshot);
+
+ /*
+ * Add invalidation messages to the reorder buffer of inprogress
+ * transactions except the current committed transaction
+ */
+ if (txn->xid != xid && curr_txn->ninvalidations > 0)
+ ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
+ curr_txn->ninvalidations, curr_txn->invalidations);
}
}
@@ -1184,8 +1195,11 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* refcount of the snapshot builder for the new snapshot */
SnapBuildSnapIncRefcount(builder->snapshot);
- /* add a new catalog snapshot to all currently running transactions */
- SnapBuildDistributeNewCatalogSnapshot(builder, lsn);
+ /*
+ * add a new catalog snapshot and invalidations messages to all
+ * currently running transactions
+ */
+ SnapBuildDistributeSnapshotAndInval(builder, lsn, xid);
}
}
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index e332635f70..093d21213a 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -743,6 +743,10 @@ extern TransactionId *ReorderBufferGetCatalogChangesXacts(ReorderBuffer *rb);
extern void ReorderBufferSetRestartPoint(ReorderBuffer *rb, XLogRecPtr ptr);
+extern ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
+ TransactionId xid, bool create, bool *is_new,
+ XLogRecPtr lsn, bool create_as_top);
+
extern void StartupReorderBuffer(void);
#endif
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..c581e28261 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,273 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+));
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (1);
+));
+
+# Add the table to the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION");
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even when
+# tables are dropped from the publication in presence of a concurrent active
+# transaction performing the DML on the same table.
+
+# Maintain an active transaction with the table that will be dropped from the
+# publication.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (4);
+));
+
+# Maintain an active transaction with a schema table that will be dropped from the
+# publication.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (4);
+));
+
+# Drop the table from the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (5);
+ INSERT INTO sch3.tab_conc VALUES (5);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is not replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that data for table tab_conc are not replicated to subscriber');
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table sch3.tab_conc are not replicated to subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is dropped in a concurrent active transaction.
+
+# Add tables to the publication.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (6);
+));
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (6);
+));
+
+# Drop publication.
+$background_psql3->query_safe("DROP PUBLICATION regress_pub1");
+
+# Perform an insert.
+$background_psql1->query_safe("INSERT INTO tab_conc VALUES (7)");
+$background_psql2->query_safe("INSERT INTO sch3.tab_conc VALUES (7)");
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# ERROR should appear on subscriber.
+my $offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION regress_sub1");
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is renamed in a concurrent active transaction.
+
+# Create publication.
+$background_psql3->query_safe(
+ "CREATE PUBLICATION regress_pub1 FOR TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Create subscription.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1"
+);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (8);
+));
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (8);
+));
+
+# Rename publication.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 RENAME TO regress_pub1_rename");
+
+# Perform an insert.
+$background_psql1->query_safe("INSERT INTO tab_conc VALUES (9)");
+$background_psql2->query_safe("INSERT INTO sch3.tab_conc VALUES (9)");
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# ERROR should appear on subscriber.
+$offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
Dear Shlok,
I have tested this part. I observed that ,whenever we insert data in a
partition table, the function 'get_rel_sync_entry' is called and a
hash entry is created for the corresponding leaf node relid. So I feel
while invalidating here we can specify 'PUBLICATION_PART_LEAF' . I
have made the corresponding changes 0002 patch.
I also verified and it seems true. The root table is a virtual table and actual
changes are recorded in leaf ones. It is same for WAL layer. Logical decoding
obtains info from WAL records so leaf tables are passed to pgoutput layer as
"relation". I.e., I think it is enough to invalidate relcache of leaf.
I have also modified the tests in 0001 patch. These changes are only
related to syntax of writing tests.
LGTM. I found small improvements, please find the attached.
Best regards,
Hayato Kuroda
FUJITSU LIMITED
Attachments:
minor_fix.diffsapplication/octet-stream; name=minor_fix.diffsDownload
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index b70091a3c6..ab380c60be 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -489,9 +489,9 @@ RenamePublication(const char *oldname, const char *newname)
List *schemarelids = NIL;
/*
- * For partition table, when we insert data, get_rel_sync_entry is called and
- * a hash entry is created for the corresponding leaf table. So invalidating
- * the leaf nodes would be sufficient here.
+ * For partition table, when we insert data, get_rel_sync_entry is
+ * called and a hash entry is created for the corresponding leaf table.
+ * So invalidating the leaf nodes would be sufficient here.
*/
relids = GetPublicationRelations(pubform->oid,
PUBLICATION_PART_LEAF);
@@ -2013,9 +2013,9 @@ AlterPublicationOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
List *schemarelids = NIL;
/*
- * For partition table, when we insert data, get_rel_sync_entry is called and
- * a hash entry is created for the corresponding leaf table. So invalidating
- * the leaf nodes would be sufficient here.
+ * For partition table, when we insert data, get_rel_sync_entry is
+ * called and a hash entry is created for the corresponding leaf table.
+ * So invalidating the leaf nodes would be sufficient here.
*/
relids = GetPublicationRelations(form->oid,
PUBLICATION_PART_LEAF);
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 1f7c24cad0..d0a5e7d026 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -867,13 +867,15 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
* catalog contents).
*/
static void
-SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
+SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn,
+ TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
ReorderBufferTXN *curr_txn;
- curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL,
+ InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -923,7 +925,8 @@ SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, Transact
*/
if (txn->xid != xid && curr_txn->ninvalidations > 0)
ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
- curr_txn->ninvalidations, curr_txn->invalidations);
+ curr_txn->ninvalidations,
+ curr_txn->invalidations);
}
}
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index c581e28261..72aaaae272 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -488,8 +488,8 @@ is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
# Clean up
-$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
-$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
# 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
Hi Kuroda-san,
I have also modified the tests in 0001 patch. These changes are only
related to syntax of writing tests.LGTM. I found small improvements, please find the attached.
I have applied the changes and updated the patch.
Thanks & Regards,
Shlok Kyal
Attachments:
v14-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchapplication/x-patch; name=v14-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchDownload
From 07f94de76be177d0e39762cb2bd36a4bc04a7993 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 23 Aug 2024 14:02:20 +0530
Subject: [PATCH v14 1/2] Distribute invalidatons if change in catalog tables
Distribute invalidations to inprogress transactions if the current
committed transaction change any catalog table.
---
.../replication/logical/reorderbuffer.c | 5 +-
src/backend/replication/logical/snapbuild.c | 34 ++-
src/include/replication/reorderbuffer.h | 4 +
src/test/subscription/t/100_bugs.pl | 267 ++++++++++++++++++
4 files changed, 296 insertions(+), 14 deletions(-)
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 22bcf171ff..c5dfc1ab06 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -221,9 +221,6 @@ int debug_logical_replication_streaming = DEBUG_LOGICAL_REP_STREAMING_BUFFERED
*/
static ReorderBufferTXN *ReorderBufferGetTXN(ReorderBuffer *rb);
static void ReorderBufferReturnTXN(ReorderBuffer *rb, ReorderBufferTXN *txn);
-static ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
- TransactionId xid, bool create, bool *is_new,
- XLogRecPtr lsn, bool create_as_top);
static void ReorderBufferTransferSnapToParent(ReorderBufferTXN *txn,
ReorderBufferTXN *subtxn);
@@ -622,7 +619,7 @@ ReorderBufferReturnRelids(ReorderBuffer *rb, Oid *relids)
* (with the given LSN, and as top transaction if that's specified);
* when this happens, is_new is set to true.
*/
-static ReorderBufferTXN *
+ReorderBufferTXN *
ReorderBufferTXNByXid(ReorderBuffer *rb, TransactionId xid, bool create,
bool *is_new, XLogRecPtr lsn, bool create_as_top)
{
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 0450f94ba8..1f7c24cad0 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -300,7 +300,7 @@ static void SnapBuildFreeSnapshot(Snapshot snap);
static void SnapBuildSnapIncRefcount(Snapshot snap);
-static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn);
+static void SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
uint32 xinfo);
@@ -859,18 +859,21 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
}
/*
- * Add a new Snapshot to all transactions we're decoding that currently are
- * in-progress so they can see new catalog contents made by the transaction
- * that just committed. This is necessary because those in-progress
- * transactions will use the new catalog's contents from here on (at the very
- * least everything they do needs to be compatible with newer catalog
- * contents).
+ * Add a new Snapshot and invalidation messages to all transactions we're
+ * decoding that currently are in-progress so they can see new catalog contents
+ * made by the transaction that just committed. This is necessary because those
+ * in-progress transactions will use the new catalog's contents from here on
+ * (at the very least everything they do needs to be compatible with newer
+ * catalog contents).
*/
static void
-SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
+SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
+ ReorderBufferTXN *curr_txn;
+
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -913,6 +916,14 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
SnapBuildSnapIncRefcount(builder->snapshot);
ReorderBufferAddSnapshot(builder->reorder, txn->xid, lsn,
builder->snapshot);
+
+ /*
+ * Add invalidation messages to the reorder buffer of inprogress
+ * transactions except the current committed transaction
+ */
+ if (txn->xid != xid && curr_txn->ninvalidations > 0)
+ ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
+ curr_txn->ninvalidations, curr_txn->invalidations);
}
}
@@ -1184,8 +1195,11 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* refcount of the snapshot builder for the new snapshot */
SnapBuildSnapIncRefcount(builder->snapshot);
- /* add a new catalog snapshot to all currently running transactions */
- SnapBuildDistributeNewCatalogSnapshot(builder, lsn);
+ /*
+ * add a new catalog snapshot and invalidations messages to all
+ * currently running transactions
+ */
+ SnapBuildDistributeSnapshotAndInval(builder, lsn, xid);
}
}
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index e332635f70..093d21213a 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -743,6 +743,10 @@ extern TransactionId *ReorderBufferGetCatalogChangesXacts(ReorderBuffer *rb);
extern void ReorderBufferSetRestartPoint(ReorderBuffer *rb, XLogRecPtr ptr);
+extern ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
+ TransactionId xid, bool create, bool *is_new,
+ XLogRecPtr lsn, bool create_as_top);
+
extern void StartupReorderBuffer(void);
#endif
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index cb36ca7b16..72aaaae272 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -487,6 +487,273 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+));
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (1);
+));
+
+# Add the table to the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION");
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even when
+# tables are dropped from the publication in presence of a concurrent active
+# transaction performing the DML on the same table.
+
+# Maintain an active transaction with the table that will be dropped from the
+# publication.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (4);
+));
+
+# Maintain an active transaction with a schema table that will be dropped from the
+# publication.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (4);
+));
+
+# Drop the table from the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (5);
+ INSERT INTO sch3.tab_conc VALUES (5);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is not replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that data for table tab_conc are not replicated to subscriber');
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table sch3.tab_conc are not replicated to subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is dropped in a concurrent active transaction.
+
+# Add tables to the publication.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (6);
+));
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (6);
+));
+
+# Drop publication.
+$background_psql3->query_safe("DROP PUBLICATION regress_pub1");
+
+# Perform an insert.
+$background_psql1->query_safe("INSERT INTO tab_conc VALUES (7)");
+$background_psql2->query_safe("INSERT INTO sch3.tab_conc VALUES (7)");
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# ERROR should appear on subscriber.
+my $offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION regress_sub1");
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is renamed in a concurrent active transaction.
+
+# Create publication.
+$background_psql3->query_safe(
+ "CREATE PUBLICATION regress_pub1 FOR TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Create subscription.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1"
+);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (8);
+));
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (8);
+));
+
+# Rename publication.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 RENAME TO regress_pub1_rename");
+
+# Perform an insert.
+$background_psql1->query_safe("INSERT INTO tab_conc VALUES (9)");
+$background_psql2->query_safe("INSERT INTO sch3.tab_conc VALUES (9)");
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# ERROR should appear on subscriber.
+$offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
v14-0002-Selective-Invalidation-of-Cache.patchapplication/x-patch; name=v14-0002-Selective-Invalidation-of-Cache.patchDownload
From a134f762eec24dbacf1f9b94a8b777cfb58655c7 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 4 Oct 2024 12:25:31 +0530
Subject: [PATCH v14 2/2] Selective Invalidation of Cache
When we alter a publication, add/drop namespace to/from publication
all the cache for all the tables are invalidated.
With this patch for the above operationns we will invalidate the
cache of only the desired tables.
---
src/backend/commands/alter.c | 4 +-
src/backend/commands/publicationcmds.c | 107 ++++++++++++++++++++
src/backend/parser/gram.y | 2 +-
src/backend/replication/logical/snapbuild.c | 9 +-
src/backend/replication/pgoutput/pgoutput.c | 18 ----
src/include/commands/publicationcmds.h | 1 +
6 files changed, 118 insertions(+), 23 deletions(-)
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 4f99ebb447..395fe530b3 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -399,6 +399,9 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TYPE:
return RenameType(stmt);
+ case OBJECT_PUBLICATION:
+ return RenamePublication(stmt->subname, stmt->newname);
+
case OBJECT_AGGREGATE:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
@@ -416,7 +419,6 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TSDICTIONARY:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
- case OBJECT_PUBLICATION:
case OBJECT_SUBSCRIPTION:
{
ObjectAddress address;
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index d6ffef374e..ab380c60be 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -433,6 +433,87 @@ pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestor
return result;
}
+/*
+ * Execute ALTER PUBLICATION RENAME
+ */
+ObjectAddress
+RenamePublication(const char *oldname, const char *newname)
+{
+ Relation rel;
+ HeapTuple tup;
+ ObjectAddress address;
+ Form_pg_publication pubform;
+ bool replaces[Natts_pg_publication];
+ bool nulls[Natts_pg_publication];
+ Datum values[Natts_pg_publication];
+
+ rel = table_open(PublicationRelationId, RowExclusiveLock);
+
+ tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+ CStringGetDatum(oldname));
+
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("publication \"%s\" does not exist",
+ oldname)));
+
+ pubform = (Form_pg_publication) GETSTRUCT(tup);
+
+ /* must be owner */
+ if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+ NameStr(pubform->pubname));
+
+ /* Everything ok, form a new tuple. */
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));
+
+ /* Only update the pubname */
+ values[Anum_pg_publication_pubname - 1] =
+ DirectFunctionCall1(namein, CStringGetDatum(newname));
+ replaces[Anum_pg_publication_pubname - 1] = true;
+
+ tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+ replaces);
+
+ /* Invalidate the relcache. */
+ if (pubform->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * For partition table, when we insert data, get_rel_sync_entry is
+ * called and a hash entry is created for the corresponding leaf table.
+ * So invalidating the leaf nodes would be sufficient here.
+ */
+ relids = GetPublicationRelations(pubform->oid,
+ PUBLICATION_PART_LEAF);
+ schemarelids = GetAllSchemaPublicationRelations(pubform->oid,
+ PUBLICATION_PART_LEAF);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ ObjectAddressSet(address, PublicationRelationId, pubform->oid);
+
+ heap_freetuple(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
/* check_functions_in_node callback */
static bool
contain_mutable_or_user_functions_checker(Oid func_id, void *context)
@@ -1920,6 +2001,32 @@ AlterPublicationOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
}
form->pubowner = newOwnerId;
+
+ /* Invalidate the relcache. */
+ if (form->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * For partition table, when we insert data, get_rel_sync_entry is
+ * called and a hash entry is created for the corresponding leaf table.
+ * So invalidating the leaf nodes would be sufficient here.
+ */
+ relids = GetPublicationRelations(form->oid,
+ PUBLICATION_PART_LEAF);
+ schemarelids = GetAllSchemaPublicationRelations(form->oid,
+ PUBLICATION_PART_LEAF);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
CatalogTupleUpdate(rel, &tup->t_self, tup);
/* Update owner dependency reference */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4aa8646af7..ec10bfdd8c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9466,7 +9466,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_PUBLICATION;
- n->object = (Node *) makeString($3);
+ n->subname = $3;
n->newname = $6;
n->missing_ok = false;
$$ = (Node *) n;
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 1f7c24cad0..d0a5e7d026 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -867,13 +867,15 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
* catalog contents).
*/
static void
-SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
+SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn,
+ TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
ReorderBufferTXN *curr_txn;
- curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL,
+ InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -923,7 +925,8 @@ SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, Transact
*/
if (txn->xid != xid && curr_txn->ninvalidations > 0)
ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
- curr_txn->ninvalidations, curr_txn->invalidations);
+ curr_txn->ninvalidations,
+ curr_txn->invalidations);
}
}
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 00e7024563..b8429be8cf 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1739,12 +1739,6 @@ static void
publication_invalidation_cb(Datum arg, int cacheid, uint32 hashvalue)
{
publications_valid = false;
-
- /*
- * Also invalidate per-relation cache so that next time the filtering info
- * is checked it will be updated with the new publication settings.
- */
- rel_sync_cache_publication_cb(arg, cacheid, hashvalue);
}
/*
@@ -1920,18 +1914,6 @@ init_rel_sync_cache(MemoryContext cachectx)
rel_sync_cache_publication_cb,
(Datum) 0);
- /*
- * Flush all cache entries after any publication changes. (We need no
- * callback entry for pg_publication, because publication_invalidation_cb
- * will take care of it.)
- */
- CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
- CacheRegisterSyscacheCallback(PUBLICATIONNAMESPACEMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
-
relation_callbacks_registered = true;
}
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 5487c571f6..b953193812 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -35,5 +35,6 @@ extern bool pub_rf_contains_invalid_column(Oid pubid, Relation relation,
List *ancestors, bool pubviaroot);
extern bool pub_collist_contains_invalid_column(Oid pubid, Relation relation,
List *ancestors, bool pubviaroot);
+extern ObjectAddress RenamePublication(const char *oldname, const char *newname);
#endif /* PUBLICATIONCMDS_H */
--
2.34.1
On Wed, 31 Jul 2024 at 03:27, Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Wed, Jul 24, 2024 at 9:53 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.I don't think locking all tables is a viable solution in this case, as
it would require asking the user to refrain from performing any
operations on any of the tables in the database while creating a
publication.Indeed, locking all tables in the database to prevent concurrent DMLs
for this scenario also looks odd to me. The other alternative
previously suggested by Andres is to distribute catalog modifying
transactions to all concurrent in-progress transactions [1] but as
mentioned this could add an overhead. One possibility to reduce
overhead is that we selectively distribute invalidations for
catalogs-related publications but I haven't analyzed the feasibility.We need more opinions to decide here, so let me summarize the problem
and solutions discussed. As explained with an example in an email [1],
the problem related to logical decoding is that it doesn't process
invalidations corresponding to DDLs for the already in-progress
transactions. We discussed preventing DMLs in the first place when
concurrent DDLs like ALTER PUBLICATION ... ADD TABLE ... are in
progress. The solution discussed was to acquire
ShareUpdateExclusiveLock for all the tables being added via such
commands. Further analysis revealed that the same handling is required
for ALTER PUBLICATION ... ADD TABLES IN SCHEMA which means locking all
the tables in the specified schemas. Then DROP PUBLICATION also seems
to have similar symptoms which means in the worst case (where
publication is for ALL TABLES) we have to lock all the tables in the
database. We are not sure if that is good so the other alternative we
can pursue is to distribute invalidations in logical decoding
infrastructure [1] which has its downsides.Thoughts?
Thank you for summarizing the problem and solutions!
I think it's worth trying the idea of distributing invalidation
messages, and we will see if there could be overheads or any further
obstacles. IIUC this approach would resolve another issue we discussed
before too[1].Regards,
[1] /messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com
Hi Sawada-san,
I have tested the scenario shared by you on the thread [1]/messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com. And I
confirm that the latest patch [2]/messages/by-id/CANhcyEWfqdUvn2d2KOdvkhebBi5VO6O8J+C6+OwsPNwCTM=akQ@mail.gmail.com fixes this issue.
[1]: /messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com
[2]: /messages/by-id/CANhcyEWfqdUvn2d2KOdvkhebBi5VO6O8J+C6+OwsPNwCTM=akQ@mail.gmail.com
Thanks and Regards,
Shlok Kyal
On Tue, Oct 08, 2024 at 03:21:38PM +0530, Shlok Kyal wrote:
I have tested the scenario shared by you on the thread [1]. And I
confirm that the latest patch [2] fixes this issue.[1] /messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com
[2] /messages/by-id/CANhcyEWfqdUvn2d2KOdvkhebBi5VO6O8J+C6+OwsPNwCTM=akQ@mail.gmail.com
Sawada-san, are you planning to look at that? It looks like this
thread is waiting for your input.
--
Michael
On Mon, Dec 9, 2024 at 10:27 PM Michael Paquier <michael@paquier.xyz> wrote:
On Tue, Oct 08, 2024 at 03:21:38PM +0530, Shlok Kyal wrote:
I have tested the scenario shared by you on the thread [1]. And I
confirm that the latest patch [2] fixes this issue.[1] /messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com
[2] /messages/by-id/CANhcyEWfqdUvn2d2KOdvkhebBi5VO6O8J+C6+OwsPNwCTM=akQ@mail.gmail.comSawada-san, are you planning to look at that? It looks like this
thread is waiting for your input.
Sorry I lost track of this thread. I'll check the test results and patch soon.
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
On Tue, Dec 10, 2024 at 01:50:16PM -0800, Masahiko Sawada wrote:
Sorry I lost track of this thread. I'll check the test results and
patch soon.
Thanks.
--
Michael
On Tue, 8 Oct 2024 at 11:11, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
Hi Kuroda-san,
I have also modified the tests in 0001 patch. These changes are only
related to syntax of writing tests.LGTM. I found small improvements, please find the attached.
I have applied the changes and updated the patch.
Patches needed a rebase. Attached the rebased patch.
Thanks and Regards,
Shlok Kyal
Attachments:
v15-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchapplication/octet-stream; name=v15-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchDownload
From 8fa004c5e2e5ec985314870e0844daef50883f04 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 23 Aug 2024 14:02:20 +0530
Subject: [PATCH v15 1/2] Distribute invalidatons if change in catalog tables
Distribute invalidations to inprogress transactions if the current
committed transaction change any catalog table.
---
.../replication/logical/reorderbuffer.c | 5 +-
src/backend/replication/logical/snapbuild.c | 34 ++-
src/include/replication/reorderbuffer.h | 4 +
src/test/subscription/t/100_bugs.pl | 267 ++++++++++++++++++
4 files changed, 296 insertions(+), 14 deletions(-)
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index e3a5c7b660..c0ef3d429b 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -221,9 +221,6 @@ int debug_logical_replication_streaming = DEBUG_LOGICAL_REP_STREAMING_BUFFERED
*/
static ReorderBufferTXN *ReorderBufferGetTXN(ReorderBuffer *rb);
static void ReorderBufferReturnTXN(ReorderBuffer *rb, ReorderBufferTXN *txn);
-static ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
- TransactionId xid, bool create, bool *is_new,
- XLogRecPtr lsn, bool create_as_top);
static void ReorderBufferTransferSnapToParent(ReorderBufferTXN *txn,
ReorderBufferTXN *subtxn);
@@ -627,7 +624,7 @@ ReorderBufferReturnRelids(ReorderBuffer *rb, Oid *relids)
* (with the given LSN, and as top transaction if that's specified);
* when this happens, is_new is set to true.
*/
-static ReorderBufferTXN *
+ReorderBufferTXN *
ReorderBufferTXNByXid(ReorderBuffer *rb, TransactionId xid, bool create,
bool *is_new, XLogRecPtr lsn, bool create_as_top)
{
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index a6a4da3266..28a685dba0 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -161,7 +161,7 @@ static void SnapBuildFreeSnapshot(Snapshot snap);
static void SnapBuildSnapIncRefcount(Snapshot snap);
-static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn);
+static void SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
uint32 xinfo);
@@ -720,18 +720,21 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
}
/*
- * Add a new Snapshot to all transactions we're decoding that currently are
- * in-progress so they can see new catalog contents made by the transaction
- * that just committed. This is necessary because those in-progress
- * transactions will use the new catalog's contents from here on (at the very
- * least everything they do needs to be compatible with newer catalog
- * contents).
+ * Add a new Snapshot and invalidation messages to all transactions we're
+ * decoding that currently are in-progress so they can see new catalog contents
+ * made by the transaction that just committed. This is necessary because those
+ * in-progress transactions will use the new catalog's contents from here on
+ * (at the very least everything they do needs to be compatible with newer
+ * catalog contents).
*/
static void
-SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
+SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
+ ReorderBufferTXN *curr_txn;
+
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -774,6 +777,14 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
SnapBuildSnapIncRefcount(builder->snapshot);
ReorderBufferAddSnapshot(builder->reorder, txn->xid, lsn,
builder->snapshot);
+
+ /*
+ * Add invalidation messages to the reorder buffer of inprogress
+ * transactions except the current committed transaction
+ */
+ if (txn->xid != xid && curr_txn->ninvalidations > 0)
+ ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
+ curr_txn->ninvalidations, curr_txn->invalidations);
}
}
@@ -1045,8 +1056,11 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* refcount of the snapshot builder for the new snapshot */
SnapBuildSnapIncRefcount(builder->snapshot);
- /* add a new catalog snapshot to all currently running transactions */
- SnapBuildDistributeNewCatalogSnapshot(builder, lsn);
+ /*
+ * add a new catalog snapshot and invalidations messages to all
+ * currently running transactions
+ */
+ SnapBuildDistributeSnapshotAndInval(builder, lsn, xid);
}
}
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index 3bc365a7b0..12668849b7 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -733,6 +733,10 @@ extern TransactionId *ReorderBufferGetCatalogChangesXacts(ReorderBuffer *rb);
extern void ReorderBufferSetRestartPoint(ReorderBuffer *rb, XLogRecPtr ptr);
+extern ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
+ TransactionId xid, bool create, bool *is_new,
+ XLogRecPtr lsn, bool create_as_top);
+
extern void StartupReorderBuffer(void);
#endif
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index 794b928f50..24817128ea 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -477,6 +477,273 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+));
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (1);
+));
+
+# Add the table to the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION");
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even when
+# tables are dropped from the publication in presence of a concurrent active
+# transaction performing the DML on the same table.
+
+# Maintain an active transaction with the table that will be dropped from the
+# publication.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (4);
+));
+
+# Maintain an active transaction with a schema table that will be dropped from the
+# publication.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (4);
+));
+
+# Drop the table from the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (5);
+ INSERT INTO sch3.tab_conc VALUES (5);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is not replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that data for table tab_conc are not replicated to subscriber');
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table sch3.tab_conc are not replicated to subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is dropped in a concurrent active transaction.
+
+# Add tables to the publication.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (6);
+));
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (6);
+));
+
+# Drop publication.
+$background_psql3->query_safe("DROP PUBLICATION regress_pub1");
+
+# Perform an insert.
+$background_psql1->query_safe("INSERT INTO tab_conc VALUES (7)");
+$background_psql2->query_safe("INSERT INTO sch3.tab_conc VALUES (7)");
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# ERROR should appear on subscriber.
+my $offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION regress_sub1");
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is renamed in a concurrent active transaction.
+
+# Create publication.
+$background_psql3->query_safe(
+ "CREATE PUBLICATION regress_pub1 FOR TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Create subscription.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1"
+);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (8);
+));
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (8);
+));
+
+# Rename publication.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 RENAME TO regress_pub1_rename");
+
+# Perform an insert.
+$background_psql1->query_safe("INSERT INTO tab_conc VALUES (9)");
+$background_psql2->query_safe("INSERT INTO sch3.tab_conc VALUES (9)");
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# ERROR should appear on subscriber.
+$offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.34.1
v15-0002-Selective-Invalidation-of-Cache.patchapplication/octet-stream; name=v15-0002-Selective-Invalidation-of-Cache.patchDownload
From 46e5e3b85a528009a7235f2bcec670cd4fbfc47c Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 11 Dec 2024 08:48:55 +0530
Subject: [PATCH v15 2/2] Selective Invalidation of Cache
When we alter a publication, add/drop namespace to/from publication
all the cache for all the tables are invalidated.
With this patch for the above operationns we will invalidate the
cache of only the desired tables.
---
src/backend/commands/alter.c | 4 +-
src/backend/commands/publicationcmds.c | 107 ++++++++++++++++++++
src/backend/parser/gram.y | 2 +-
src/backend/replication/logical/snapbuild.c | 9 +-
src/backend/replication/pgoutput/pgoutput.c | 18 ----
src/include/commands/publicationcmds.h | 1 +
6 files changed, 118 insertions(+), 23 deletions(-)
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index a45f3bb6b8..79bd6b7cef 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -399,6 +399,9 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TYPE:
return RenameType(stmt);
+ case OBJECT_PUBLICATION:
+ return RenamePublication(stmt->subname, stmt->newname);
+
case OBJECT_AGGREGATE:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
@@ -416,7 +419,6 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TSDICTIONARY:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
- case OBJECT_PUBLICATION:
case OBJECT_SUBSCRIPTION:
{
ObjectAddress address;
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 5050057a7e..402cd44c5f 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -470,6 +470,87 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
return *invalid_column_list || *invalid_gen_col;
}
+/*
+ * Execute ALTER PUBLICATION RENAME
+ */
+ObjectAddress
+RenamePublication(const char *oldname, const char *newname)
+{
+ Relation rel;
+ HeapTuple tup;
+ ObjectAddress address;
+ Form_pg_publication pubform;
+ bool replaces[Natts_pg_publication];
+ bool nulls[Natts_pg_publication];
+ Datum values[Natts_pg_publication];
+
+ rel = table_open(PublicationRelationId, RowExclusiveLock);
+
+ tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+ CStringGetDatum(oldname));
+
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("publication \"%s\" does not exist",
+ oldname)));
+
+ pubform = (Form_pg_publication) GETSTRUCT(tup);
+
+ /* must be owner */
+ if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+ NameStr(pubform->pubname));
+
+ /* Everything ok, form a new tuple. */
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));
+
+ /* Only update the pubname */
+ values[Anum_pg_publication_pubname - 1] =
+ DirectFunctionCall1(namein, CStringGetDatum(newname));
+ replaces[Anum_pg_publication_pubname - 1] = true;
+
+ tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+ replaces);
+
+ /* Invalidate the relcache. */
+ if (pubform->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * For partition table, when we insert data, get_rel_sync_entry is
+ * called and a hash entry is created for the corresponding leaf table.
+ * So invalidating the leaf nodes would be sufficient here.
+ */
+ relids = GetPublicationRelations(pubform->oid,
+ PUBLICATION_PART_LEAF);
+ schemarelids = GetAllSchemaPublicationRelations(pubform->oid,
+ PUBLICATION_PART_LEAF);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ ObjectAddressSet(address, PublicationRelationId, pubform->oid);
+
+ heap_freetuple(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
/* check_functions_in_node callback */
static bool
contain_mutable_or_user_functions_checker(Oid func_id, void *context)
@@ -1973,6 +2054,32 @@ AlterPublicationOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
}
form->pubowner = newOwnerId;
+
+ /* Invalidate the relcache. */
+ if (form->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * For partition table, when we insert data, get_rel_sync_entry is
+ * called and a hash entry is created for the corresponding leaf table.
+ * So invalidating the leaf nodes would be sufficient here.
+ */
+ relids = GetPublicationRelations(form->oid,
+ PUBLICATION_PART_LEAF);
+ schemarelids = GetAllSchemaPublicationRelations(form->oid,
+ PUBLICATION_PART_LEAF);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
CatalogTupleUpdate(rel, &tup->t_self, tup);
/* Update owner dependency reference */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 67eb96396a..bc2da291ca 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9475,7 +9475,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_PUBLICATION;
- n->object = (Node *) makeString($3);
+ n->subname = $3;
n->newname = $6;
n->missing_ok = false;
$$ = (Node *) n;
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 28a685dba0..f00465b737 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -728,13 +728,15 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
* catalog contents).
*/
static void
-SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
+SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn,
+ TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
ReorderBufferTXN *curr_txn;
- curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL,
+ InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -784,7 +786,8 @@ SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, Transact
*/
if (txn->xid != xid && curr_txn->ninvalidations > 0)
ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
- curr_txn->ninvalidations, curr_txn->invalidations);
+ curr_txn->ninvalidations,
+ curr_txn->invalidations);
}
}
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index b50b3d62e3..c68955fa85 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1781,12 +1781,6 @@ static void
publication_invalidation_cb(Datum arg, int cacheid, uint32 hashvalue)
{
publications_valid = false;
-
- /*
- * Also invalidate per-relation cache so that next time the filtering info
- * is checked it will be updated with the new publication settings.
- */
- rel_sync_cache_publication_cb(arg, cacheid, hashvalue);
}
/*
@@ -1962,18 +1956,6 @@ init_rel_sync_cache(MemoryContext cachectx)
rel_sync_cache_publication_cb,
(Datum) 0);
- /*
- * Flush all cache entries after any publication changes. (We need no
- * callback entry for pg_publication, because publication_invalidation_cb
- * will take care of it.)
- */
- CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
- CacheRegisterSyscacheCallback(PUBLICATIONNAMESPACEMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
-
relation_callbacks_registered = true;
}
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index 19037518e8..b812d6d19f 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -38,5 +38,6 @@ extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
bool pubgencols,
bool *invalid_column_list,
bool *invalid_gen_col);
+extern ObjectAddress RenamePublication(const char *oldname, const char *newname);
#endif /* PUBLICATIONCMDS_H */
--
2.34.1
On Tue, Oct 8, 2024 at 2:51 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Wed, 31 Jul 2024 at 03:27, Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Wed, Jul 24, 2024 at 9:53 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.I don't think locking all tables is a viable solution in this case, as
it would require asking the user to refrain from performing any
operations on any of the tables in the database while creating a
publication.Indeed, locking all tables in the database to prevent concurrent DMLs
for this scenario also looks odd to me. The other alternative
previously suggested by Andres is to distribute catalog modifying
transactions to all concurrent in-progress transactions [1] but as
mentioned this could add an overhead. One possibility to reduce
overhead is that we selectively distribute invalidations for
catalogs-related publications but I haven't analyzed the feasibility.We need more opinions to decide here, so let me summarize the problem
and solutions discussed. As explained with an example in an email [1],
the problem related to logical decoding is that it doesn't process
invalidations corresponding to DDLs for the already in-progress
transactions. We discussed preventing DMLs in the first place when
concurrent DDLs like ALTER PUBLICATION ... ADD TABLE ... are in
progress. The solution discussed was to acquire
ShareUpdateExclusiveLock for all the tables being added via such
commands. Further analysis revealed that the same handling is required
for ALTER PUBLICATION ... ADD TABLES IN SCHEMA which means locking all
the tables in the specified schemas. Then DROP PUBLICATION also seems
to have similar symptoms which means in the worst case (where
publication is for ALL TABLES) we have to lock all the tables in the
database. We are not sure if that is good so the other alternative we
can pursue is to distribute invalidations in logical decoding
infrastructure [1] which has its downsides.Thoughts?
Thank you for summarizing the problem and solutions!
I think it's worth trying the idea of distributing invalidation
messages, and we will see if there could be overheads or any further
obstacles. IIUC this approach would resolve another issue we discussed
before too[1].Regards,
[1] /messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com
Hi Sawada-san,
I have tested the scenario shared by you on the thread [1]. And I
confirm that the latest patch [2] fixes this issue.
I confirmed that the proposed patch fixes these issues. I have one
question about the patch:
In the main loop in SnapBuildDistributeSnapshotAndInval(), we have the
following code:
/*
* If we don't have a base snapshot yet, there are no changes in this
* transaction which in turn implies we don't yet need a snapshot at
* all. We'll add a snapshot when the first change gets queued.
*
* NB: This works correctly even for subtransactions because
* ReorderBufferAssignChild() takes care to transfer the base snapshot
* to the top-level transaction, and while iterating the changequeue
* we'll get the change from the subtxn.
*/
if (!ReorderBufferXidHasBaseSnapshot(builder->reorder, txn->xid))
continue;
Is there any case where we need to distribute inval messages to
transactions that don't have the base snapshot yet but eventually need
the inval messages?
Overall, with this idea, we distribute invalidation messages to all
concurrent decoded transactions. It could introduce performance
regressions by several causes. For example, we could end up
invalidating RelationSyncCache entries in more cases. While this is
addressed by your selectively cache invalidation patch, there is still
5% regression. We might need to accept a certain amount of regressions
for making it correct but it would be better to figure out where these
regressions come from. Other than that, I think the performance
regression could happen due to the costs of distributing invalidation
messages. You've already observed there is 1~3% performance regression
in cases where we distribute a large amount of invalidation messages
to one concurrently decoded transaction[1]/messages/by-id/CANhcyEX+C3G68W51myHWfbpAdmSXDwHdMsWUa+zHBF_QKKvZMw@mail.gmail.com. I guess that the
selectively cache invalidation idea would not help this case. Also, I
think we might want to test other cases like where we distribute a
small amount of invalidation messages to many concurrently decoded
transactions.
Regards,
[1]: /messages/by-id/CANhcyEX+C3G68W51myHWfbpAdmSXDwHdMsWUa+zHBF_QKKvZMw@mail.gmail.com
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
On Wed, Dec 11, 2024 at 12:37 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
I confirmed that the proposed patch fixes these issues. I have one
question about the patch:In the main loop in SnapBuildDistributeSnapshotAndInval(), we have the
following code:/*
* If we don't have a base snapshot yet, there are no changes in this
* transaction which in turn implies we don't yet need a snapshot at
* all. We'll add a snapshot when the first change gets queued.
*
* NB: This works correctly even for subtransactions because
* ReorderBufferAssignChild() takes care to transfer the base snapshot
* to the top-level transaction, and while iterating the changequeue
* we'll get the change from the subtxn.
*/
if (!ReorderBufferXidHasBaseSnapshot(builder->reorder, txn->xid))
continue;Is there any case where we need to distribute inval messages to
transactions that don't have the base snapshot yet but eventually need
the inval messages?
Good point. It is mentioned that for snapshots: "We'll add a snapshot
when the first change gets queued.". I think we achieve this via
builder->committed.xip array such that when we set a base snapshot for
a transaction, we use that array to form a snapshot. However, I don't
see any such consideration for invalidations. Now, we could either
always add invalidations to xacts that don't have base_snapshot yet or
have a mechanism similar committed.xid array. But it is better to
first reproduce the problem.
Overall, with this idea, we distribute invalidation messages to all
concurrent decoded transactions. It could introduce performance
regressions by several causes. For example, we could end up
invalidating RelationSyncCache entries in more cases. While this is
addressed by your selectively cache invalidation patch, there is still
5% regression. We might need to accept a certain amount of regressions
for making it correct but it would be better to figure out where these
regressions come from. Other than that, I think the performance
regression could happen due to the costs of distributing invalidation
messages. You've already observed there is 1~3% performance regression
in cases where we distribute a large amount of invalidation messages
to one concurrently decoded transaction[1]. I guess that the
selectively cache invalidation idea would not help this case. Also, I
think we might want to test other cases like where we distribute a
small amount of invalidation messages to many concurrently decoded
transactions.
+1.
--
With Regards,
Amit Kapila.
On Wed, 11 Dec 2024 at 09:13, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Tue, 8 Oct 2024 at 11:11, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
Hi Kuroda-san,
I have also modified the tests in 0001 patch. These changes are only
related to syntax of writing tests.LGTM. I found small improvements, please find the attached.
I have applied the changes and updated the patch.
Patches needed a rebase. Attached the rebased patch.
Patches need a rebase. Attached the rebased patch.
Thanks and regards,
Shlok Kyal
Attachments:
v16-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchapplication/octet-stream; name=v16-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchDownload
From c6f01f4b335b724024292e6f69e7498d84055429 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 23 Aug 2024 14:02:20 +0530
Subject: [PATCH v16 1/2] Distribute invalidatons if change in catalog tables
Distribute invalidations to inprogress transactions if the current
committed transaction change any catalog table.
---
.../replication/logical/reorderbuffer.c | 5 +-
src/backend/replication/logical/snapbuild.c | 34 ++-
src/include/replication/reorderbuffer.h | 4 +
src/test/subscription/t/100_bugs.pl | 267 ++++++++++++++++++
4 files changed, 296 insertions(+), 14 deletions(-)
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index b42f4002ba8..340fdd50f8a 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -222,9 +222,6 @@ int debug_logical_replication_streaming = DEBUG_LOGICAL_REP_STREAMING_BUFFERED
*/
static ReorderBufferTXN *ReorderBufferGetTXN(ReorderBuffer *rb);
static void ReorderBufferReturnTXN(ReorderBuffer *rb, ReorderBufferTXN *txn);
-static ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
- TransactionId xid, bool create, bool *is_new,
- XLogRecPtr lsn, bool create_as_top);
static void ReorderBufferTransferSnapToParent(ReorderBufferTXN *txn,
ReorderBufferTXN *subtxn);
@@ -630,7 +627,7 @@ ReorderBufferReturnRelids(ReorderBuffer *rb, Oid *relids)
* (with the given LSN, and as top transaction if that's specified);
* when this happens, is_new is set to true.
*/
-static ReorderBufferTXN *
+ReorderBufferTXN *
ReorderBufferTXNByXid(ReorderBuffer *rb, TransactionId xid, bool create,
bool *is_new, XLogRecPtr lsn, bool create_as_top)
{
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index bd0680dcbe5..1716340799b 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -161,7 +161,7 @@ static void SnapBuildFreeSnapshot(Snapshot snap);
static void SnapBuildSnapIncRefcount(Snapshot snap);
-static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn);
+static void SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
uint32 xinfo);
@@ -720,18 +720,21 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
}
/*
- * Add a new Snapshot to all transactions we're decoding that currently are
- * in-progress so they can see new catalog contents made by the transaction
- * that just committed. This is necessary because those in-progress
- * transactions will use the new catalog's contents from here on (at the very
- * least everything they do needs to be compatible with newer catalog
- * contents).
+ * Add a new Snapshot and invalidation messages to all transactions we're
+ * decoding that currently are in-progress so they can see new catalog contents
+ * made by the transaction that just committed. This is necessary because those
+ * in-progress transactions will use the new catalog's contents from here on
+ * (at the very least everything they do needs to be compatible with newer
+ * catalog contents).
*/
static void
-SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
+SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
+ ReorderBufferTXN *curr_txn;
+
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -774,6 +777,14 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
SnapBuildSnapIncRefcount(builder->snapshot);
ReorderBufferAddSnapshot(builder->reorder, txn->xid, lsn,
builder->snapshot);
+
+ /*
+ * Add invalidation messages to the reorder buffer of inprogress
+ * transactions except the current committed transaction
+ */
+ if (txn->xid != xid && curr_txn->ninvalidations > 0)
+ ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
+ curr_txn->ninvalidations, curr_txn->invalidations);
}
}
@@ -1045,8 +1056,11 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* refcount of the snapshot builder for the new snapshot */
SnapBuildSnapIncRefcount(builder->snapshot);
- /* add a new catalog snapshot to all currently running transactions */
- SnapBuildDistributeNewCatalogSnapshot(builder, lsn);
+ /*
+ * add a new catalog snapshot and invalidations messages to all
+ * currently running transactions
+ */
+ SnapBuildDistributeSnapshotAndInval(builder, lsn, xid);
}
}
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index 517a8e3634f..4f3ceef0092 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -759,6 +759,10 @@ extern TransactionId *ReorderBufferGetCatalogChangesXacts(ReorderBuffer *rb);
extern void ReorderBufferSetRestartPoint(ReorderBuffer *rb, XLogRecPtr ptr);
+extern ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
+ TransactionId xid, bool create, bool *is_new,
+ XLogRecPtr lsn, bool create_as_top);
+
extern void StartupReorderBuffer(void);
#endif
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index 83120f1cb6f..d6dbeebed54 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -477,6 +477,273 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+));
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (1);
+));
+
+# Add the table to the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION");
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even when
+# tables are dropped from the publication in presence of a concurrent active
+# transaction performing the DML on the same table.
+
+# Maintain an active transaction with the table that will be dropped from the
+# publication.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (4);
+));
+
+# Maintain an active transaction with a schema table that will be dropped from the
+# publication.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (4);
+));
+
+# Drop the table from the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (5);
+ INSERT INTO sch3.tab_conc VALUES (5);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is not replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that data for table tab_conc are not replicated to subscriber');
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table sch3.tab_conc are not replicated to subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is dropped in a concurrent active transaction.
+
+# Add tables to the publication.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (6);
+));
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (6);
+));
+
+# Drop publication.
+$background_psql3->query_safe("DROP PUBLICATION regress_pub1");
+
+# Perform an insert.
+$background_psql1->query_safe("INSERT INTO tab_conc VALUES (7)");
+$background_psql2->query_safe("INSERT INTO sch3.tab_conc VALUES (7)");
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# ERROR should appear on subscriber.
+my $offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION regress_sub1");
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is renamed in a concurrent active transaction.
+
+# Create publication.
+$background_psql3->query_safe(
+ "CREATE PUBLICATION regress_pub1 FOR TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Create subscription.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1"
+);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (8);
+));
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (8);
+));
+
+# Rename publication.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 RENAME TO regress_pub1_rename");
+
+# Perform an insert.
+$background_psql1->query_safe("INSERT INTO tab_conc VALUES (9)");
+$background_psql2->query_safe("INSERT INTO sch3.tab_conc VALUES (9)");
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# ERROR should appear on subscriber.
+$offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.41.0.windows.3
v16-0002-Selective-Invalidation-of-Cache.patchapplication/octet-stream; name=v16-0002-Selective-Invalidation-of-Cache.patchDownload
From 65c97c89c984475c0e28a26796c983ec9acc5e46 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 24 Feb 2025 15:34:31 +0530
Subject: [PATCH v16 2/2] Selective Invalidation of Cache
When we alter a publication, add/drop namespace to/from publication
all the cache for all the tables are invalidated.
With this patch for the above operationns we will invalidate the
cache of only the desired tables.
---
src/backend/commands/alter.c | 4 +-
src/backend/commands/publicationcmds.c | 107 ++++++++++++++++++++
src/backend/parser/gram.y | 2 +-
src/backend/replication/pgoutput/pgoutput.c | 18 ----
src/include/commands/publicationcmds.h | 1 +
5 files changed, 112 insertions(+), 20 deletions(-)
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 78c1d4e1b84..a79329acc1f 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -400,6 +400,9 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TYPE:
return RenameType(stmt);
+ case OBJECT_PUBLICATION:
+ return RenamePublication(stmt->subname, stmt->newname);
+
case OBJECT_AGGREGATE:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
@@ -417,7 +420,6 @@ ExecRenameStmt(RenameStmt *stmt)
case OBJECT_TSDICTIONARY:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
- case OBJECT_PUBLICATION:
case OBJECT_SUBSCRIPTION:
{
ObjectAddress address;
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 150a768d16f..182d2187f1c 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -491,6 +491,87 @@ pub_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
return *invalid_column_list || *invalid_gen_col;
}
+/*
+ * Execute ALTER PUBLICATION RENAME
+ */
+ObjectAddress
+RenamePublication(const char *oldname, const char *newname)
+{
+ Relation rel;
+ HeapTuple tup;
+ ObjectAddress address;
+ Form_pg_publication pubform;
+ bool replaces[Natts_pg_publication];
+ bool nulls[Natts_pg_publication];
+ Datum values[Natts_pg_publication];
+
+ rel = table_open(PublicationRelationId, RowExclusiveLock);
+
+ tup = SearchSysCacheCopy1(PUBLICATIONNAME,
+ CStringGetDatum(oldname));
+
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("publication \"%s\" does not exist",
+ oldname)));
+
+ pubform = (Form_pg_publication) GETSTRUCT(tup);
+
+ /* must be owner */
+ if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
+ NameStr(pubform->pubname));
+
+ /* Everything ok, form a new tuple. */
+ memset(values, 0, sizeof(values));
+ memset(nulls, false, sizeof(nulls));
+ memset(replaces, false, sizeof(replaces));
+
+ /* Only update the pubname */
+ values[Anum_pg_publication_pubname - 1] =
+ DirectFunctionCall1(namein, CStringGetDatum(newname));
+ replaces[Anum_pg_publication_pubname - 1] = true;
+
+ tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
+ replaces);
+
+ /* Invalidate the relcache. */
+ if (pubform->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * For partition table, when we insert data, get_rel_sync_entry is
+ * called and a hash entry is created for the corresponding leaf table.
+ * So invalidating the leaf nodes would be sufficient here.
+ */
+ relids = GetPublicationRelations(pubform->oid,
+ PUBLICATION_PART_LEAF);
+ schemarelids = GetAllSchemaPublicationRelations(pubform->oid,
+ PUBLICATION_PART_LEAF);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ ObjectAddressSet(address, PublicationRelationId, pubform->oid);
+
+ heap_freetuple(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
/* check_functions_in_node callback */
static bool
contain_mutable_or_user_functions_checker(Oid func_id, void *context)
@@ -1996,6 +2077,32 @@ AlterPublicationOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
}
form->pubowner = newOwnerId;
+
+ /* Invalidate the relcache. */
+ if (form->puballtables)
+ {
+ CacheInvalidateRelcacheAll();
+ }
+ else
+ {
+ List *relids = NIL;
+ List *schemarelids = NIL;
+
+ /*
+ * For partition table, when we insert data, get_rel_sync_entry is
+ * called and a hash entry is created for the corresponding leaf table.
+ * So invalidating the leaf nodes would be sufficient here.
+ */
+ relids = GetPublicationRelations(form->oid,
+ PUBLICATION_PART_LEAF);
+ schemarelids = GetAllSchemaPublicationRelations(form->oid,
+ PUBLICATION_PART_LEAF);
+
+ relids = list_concat_unique_oid(relids, schemarelids);
+
+ InvalidatePublicationRels(relids);
+ }
+
CatalogTupleUpdate(rel, &tup->t_self, tup);
/* Update owner dependency reference */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7d99c9355c6..49fe0567c57 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -9513,7 +9513,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_PUBLICATION;
- n->object = (Node *) makeString($3);
+ n->subname = $3;
n->newname = $6;
n->missing_ok = false;
$$ = (Node *) n;
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 7d464f656aa..b28ce636d50 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1789,12 +1789,6 @@ static void
publication_invalidation_cb(Datum arg, int cacheid, uint32 hashvalue)
{
publications_valid = false;
-
- /*
- * Also invalidate per-relation cache so that next time the filtering info
- * is checked it will be updated with the new publication settings.
- */
- rel_sync_cache_publication_cb(arg, cacheid, hashvalue);
}
/*
@@ -1970,18 +1964,6 @@ init_rel_sync_cache(MemoryContext cachectx)
rel_sync_cache_publication_cb,
(Datum) 0);
- /*
- * Flush all cache entries after any publication changes. (We need no
- * callback entry for pg_publication, because publication_invalidation_cb
- * will take care of it.)
- */
- CacheRegisterSyscacheCallback(PUBLICATIONRELMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
- CacheRegisterSyscacheCallback(PUBLICATIONNAMESPACEMAP,
- rel_sync_cache_publication_cb,
- (Datum) 0);
-
relation_callbacks_registered = true;
}
diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h
index e11a942ea0f..3dfceef70f9 100644
--- a/src/include/commands/publicationcmds.h
+++ b/src/include/commands/publicationcmds.h
@@ -38,5 +38,6 @@ extern bool pub_contains_invalid_column(Oid pubid, Relation relation,
char pubgencols_type,
bool *invalid_column_list,
bool *invalid_gen_col);
+extern ObjectAddress RenamePublication(const char *oldname, const char *newname);
#endif /* PUBLICATIONCMDS_H */
--
2.41.0.windows.3
Hi,
After reading the thread and doing a bit of testing, the problem seems
significant and is still present. The fact that it's probably not well
known makes it more concerning, in my opinion. I was wondering what
could be done to help move this topic forward (given my limited abilities)?
--
Benoit Lobréau
Consultant
http://dalibo.com
On Tue, Feb 25, 2025 at 3:56 PM Benoit Lobréau
<benoit.lobreau@dalibo.com> wrote:
After reading the thread and doing a bit of testing, the problem seems
significant and is still present. The fact that it's probably not well
known makes it more concerning, in my opinion. I was wondering what
could be done to help move this topic forward (given my limited abilities)?
You can help with the review/test of the proposed patch. Also, help
with the performance impact of the patch, if possible. Shlok has done
some performance testing of the patch which you can perform
independently and then Sawada-San has asked for more performance tests
in his last email (1) which you can also help with.
(1) - /messages/by-id/CAD21AoDoWc8MWTyKtmNF_606bcW6J0gV==r=VmPXKUN-e3o9ew@mail.gmail.com
--
With Regards,
Amit Kapila.
On Wed, 11 Dec 2024 at 12:37, Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Tue, Oct 8, 2024 at 2:51 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Wed, 31 Jul 2024 at 03:27, Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Wed, Jul 24, 2024 at 9:53 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Jul 17, 2024 at 5:25 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, 17 Jul 2024 at 11:54, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Jul 16, 2024 at 6:54 PM vignesh C <vignesh21@gmail.com> wrote:
BTW, I noticed that we don't take any table-level locks for Create
Publication .. For ALL TABLES (and Drop Publication). Can that create
a similar problem? I haven't tested so not sure but even if there is a
problem for the Create case, it should lead to some ERROR like missing
publication.I tested these scenarios, and as you expected, it throws an error for
the create publication case:
2024-07-17 14:50:01.145 IST [481526] 481526 ERROR: could not receive
data from WAL stream: ERROR: publication "pub1" does not exist
CONTEXT: slot "sub1", output plugin "pgoutput", in the change
callback, associated LSN 0/1510CD8
2024-07-17 14:50:01.147 IST [481450] 481450 LOG: background worker
"logical replication apply worker" (PID 481526) exited with exit code
1The steps for this process are as follows:
1) Create tables in both the publisher and subscriber.
2) On the publisher: Create a replication slot.
3) On the subscriber: Create a subscription using the slot created by
the publisher.
4) On the publisher:
4.a) Session 1: BEGIN; INSERT INTO T1;
4.b) Session 2: CREATE PUBLICATION FOR ALL TABLES
4.c) Session 1: COMMIT;Since we are throwing out a "publication does not exist" error, there
is no inconsistency issue here.However, an issue persists with DROP ALL TABLES publication, where
data continues to replicate even after the publication is dropped.
This happens because the open transaction consumes the invalidation,
causing the publications to be revalidated using old snapshot. As a
result, both the open transactions and the subsequent transactions are
getting replicated.We can reproduce this issue by following these steps in a logical
replication setup with an "ALL TABLES" publication:
On the publisher:
Session 1: BEGIN; INSERT INTO T1 VALUES (val1);
In another session on the publisher:
Session 2: DROP PUBLICATION
Back in Session 1 on the publisher:
COMMIT;
Finally, in Session 1 on the publisher:
INSERT INTO T1 VALUES (val2);Even after dropping the publication, both val1 and val2 are still
being replicated to the subscriber. This means that both the
in-progress concurrent transaction and the subsequent transactions are
being replicated.I don't think locking all tables is a viable solution in this case, as
it would require asking the user to refrain from performing any
operations on any of the tables in the database while creating a
publication.Indeed, locking all tables in the database to prevent concurrent DMLs
for this scenario also looks odd to me. The other alternative
previously suggested by Andres is to distribute catalog modifying
transactions to all concurrent in-progress transactions [1] but as
mentioned this could add an overhead. One possibility to reduce
overhead is that we selectively distribute invalidations for
catalogs-related publications but I haven't analyzed the feasibility.We need more opinions to decide here, so let me summarize the problem
and solutions discussed. As explained with an example in an email [1],
the problem related to logical decoding is that it doesn't process
invalidations corresponding to DDLs for the already in-progress
transactions. We discussed preventing DMLs in the first place when
concurrent DDLs like ALTER PUBLICATION ... ADD TABLE ... are in
progress. The solution discussed was to acquire
ShareUpdateExclusiveLock for all the tables being added via such
commands. Further analysis revealed that the same handling is required
for ALTER PUBLICATION ... ADD TABLES IN SCHEMA which means locking all
the tables in the specified schemas. Then DROP PUBLICATION also seems
to have similar symptoms which means in the worst case (where
publication is for ALL TABLES) we have to lock all the tables in the
database. We are not sure if that is good so the other alternative we
can pursue is to distribute invalidations in logical decoding
infrastructure [1] which has its downsides.Thoughts?
Thank you for summarizing the problem and solutions!
I think it's worth trying the idea of distributing invalidation
messages, and we will see if there could be overheads or any further
obstacles. IIUC this approach would resolve another issue we discussed
before too[1].Regards,
[1] /messages/by-id/CAD21AoAenVqiMjpN-PvGHL1N9DWnHSq673bfgr6phmBUzx=kLQ@mail.gmail.com
Hi Sawada-san,
I have tested the scenario shared by you on the thread [1]. And I
confirm that the latest patch [2] fixes this issue.I confirmed that the proposed patch fixes these issues. I have one
question about the patch:In the main loop in SnapBuildDistributeSnapshotAndInval(), we have the
following code:/*
* If we don't have a base snapshot yet, there are no changes in this
* transaction which in turn implies we don't yet need a snapshot at
* all. We'll add a snapshot when the first change gets queued.
*
* NB: This works correctly even for subtransactions because
* ReorderBufferAssignChild() takes care to transfer the base snapshot
* to the top-level transaction, and while iterating the changequeue
* we'll get the change from the subtxn.
*/
if (!ReorderBufferXidHasBaseSnapshot(builder->reorder, txn->xid))
continue;Is there any case where we need to distribute inval messages to
transactions that don't have the base snapshot yet but eventually need
the inval messages?Overall, with this idea, we distribute invalidation messages to all
concurrent decoded transactions. It could introduce performance
regressions by several causes. For example, we could end up
invalidating RelationSyncCache entries in more cases. While this is
addressed by your selectively cache invalidation patch, there is still
5% regression. We might need to accept a certain amount of regressions
for making it correct but it would be better to figure out where these
regressions come from. Other than that, I think the performance
regression could happen due to the costs of distributing invalidation
messages. You've already observed there is 1~3% performance regression
in cases where we distribute a large amount of invalidation messages
to one concurrently decoded transaction[1]. I guess that the
selectively cache invalidation idea would not help this case. Also, I
think we might want to test other cases like where we distribute a
small amount of invalidation messages to many concurrently decoded
transactions.
Hi Sawada-san,
I have done the performance testing for cases where we distribute a
small amount of invalidation messages to many concurrently decoded
transactions.
Here are results:
Concurrent Txn | Head (sec) | Patch (sec) | Degradation in %
---------------------------------------------------------------------------------------------
50 | 0.2627734 | 0.2654608 | 1.022706256
100 | 0.4801048 | 0.4869254 | 1.420648158
500 | 2.2170336 | 2.2438656 | 1.210265825
1000 | 4.4957402 | 4.5282574 | 0.723289126
2000 | 9.2013082 | 9.21164 | 0.112286207
The steps I followed is:
1. Initially logical replication is setup.
2. Then we start 'n' number of concurrent transactions.
Each txn look like:
BEGIN;
Insert into t1 values(11);
3. Now we add two invalidation which will be distributed each
transaction by running command:
ALTER PUBLICATION regress_pub1 DROP TABLE t1
ALTER PUBLICATION regress_pub1 ADD TABLE t1
4. Then run an insert for each txn. It will build cache for relation
in each txn.
5. Commit Each transaction.
I have also attached the script.
Thanks and Regards,
Shlok Kyal
Attachments:
On Wed, Feb 26, 2025 at 9:21 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
I have done the performance testing for cases where we distribute a
small amount of invalidation messages to many concurrently decoded
transactions.
Here are results:Concurrent Txn | Head (sec) | Patch (sec) | Degradation in %
---------------------------------------------------------------------------------------------
50 | 0.2627734 | 0.2654608 | 1.022706256
100 | 0.4801048 | 0.4869254 | 1.420648158
500 | 2.2170336 | 2.2438656 | 1.210265825
1000 | 4.4957402 | 4.5282574 | 0.723289126
2000 | 9.2013082 | 9.21164 | 0.112286207The steps I followed is:
1. Initially logical replication is setup.
2. Then we start 'n' number of concurrent transactions.
Each txn look like:
BEGIN;
Insert into t1 values(11);
3. Now we add two invalidation which will be distributed each
transaction by running command:
ALTER PUBLICATION regress_pub1 DROP TABLE t1
ALTER PUBLICATION regress_pub1 ADD TABLE t1
4. Then run an insert for each txn. It will build cache for relation
in each txn.
5. Commit Each transaction.I have also attached the script.
The tests are done using pub-sub setup which has some overhead of
logical replication as well. Can we try this test by fetching changes
via SQL API using pgoutput as plugin to see the impact?
--
With Regards,
Amit Kapila.
On Monday, February 24, 2025 5:50 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Dec 11, 2024 at 12:37 PM Masahiko Sawada
<sawada.mshk@gmail.com> wrote:I confirmed that the proposed patch fixes these issues. I have one
question about the patch:In the main loop in SnapBuildDistributeSnapshotAndInval(), we have the
following code:/*
* If we don't have a base snapshot yet, there are no changes in this
* transaction which in turn implies we don't yet need a snapshot at
* all. We'll add a snapshot when the first change gets queued.
*
* NB: This works correctly even for subtransactions because
* ReorderBufferAssignChild() takes care to transfer the basesnapshot
* to the top-level transaction, and while iterating the changequeue
* we'll get the change from the subtxn.
*/
if (!ReorderBufferXidHasBaseSnapshot(builder->reorder, txn->xid))
continue;Is there any case where we need to distribute inval messages to
transactions that don't have the base snapshot yet but eventually need
the inval messages?Good point. It is mentioned that for snapshots: "We'll add a snapshot
when the first change gets queued.". I think we achieve this via
builder->committed.xip array such that when we set a base snapshot for
a transaction, we use that array to form a snapshot. However, I don't
see any such consideration for invalidations. Now, we could either
always add invalidations to xacts that don't have base_snapshot yet or
have a mechanism similar committed.xid array. But it is better to
first reproduce the problem.
I think distributing invalidations to a transaction that has not yet built a
base snapshot is un-necessary. This is because, during the process of building
its base snapshot, such a transaction will have already recorded the XID of the
transaction that altered the publication information into its array of
committed XIDs. Consequently, it will reflect the latest changes in the catalog
from the beginning. In the context of logical decoding, this scenario is
analogous to decoding a new transaction initiated after the catalog-change
transaction has been committed.
The original issue arises because the catalog cache was constructed using an
outdated snapshot that could not reflect the latest catalog changes. However,
this is not a problem in cases without a base snapshot. Since the existing
catalog cache should have been invalidated upon decoding the committed
catalog-change transaction, the subsequent transactions will construct a new
cache with the latest snapshot.
I also considered the scenario where only a sub-transaction has a base snapshot
that has not yet been transferred to its top-level transaction. However, I
think this is not problematic because a sub-transaction transfers its snapshot
immediately upon building it (see ReorderBufferSetBaseSnapshot). The only
exception is if the sub-transaction is independent (i.e., not yet associated
with its top-level transaction). In such a case, the sub-transaction is treated
as a top-level transaction, and invalidations will be distributed to this
sub-transaction after applying the patch which is sufficient to resolve the
issue.
Considering the complexity of this topic, I think it would be better to add some
comments like the attachment
Best Regards,
Hou zj
Attachments:
0001-add-comments-for-txns-without-base-snapshot.patchapplication/octet-stream; name=0001-add-comments-for-txns-without-base-snapshot.patchDownload
From 8e26f5f06124aba8f12d89e549e47ada7aa6329e Mon Sep 17 00:00:00 2001
From: Hou Zhijie <houzj.fnst@cn.fujitsu.com>
Date: Thu, 27 Feb 2025 16:06:11 +0800
Subject: [PATCH] add comments for txns without base snapshot
---
src/backend/replication/logical/snapbuild.c | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 1716340799b..6917ca27181 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -752,6 +752,15 @@ SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, Transact
* transaction which in turn implies we don't yet need a snapshot at
* all. We'll add a snapshot when the first change gets queued.
*
+ * Moreover, distributing invalidations to this transaction at this
+ * stage is unnecessary. Once a base snapshot is built, it will
+ * naturally include the xids of committed transactions that have
+ * modified the catalog, thus reflecting the new catalog contents. The
+ * existing catalog cache will have already been invalidated after
+ * processing the invalidations in the transaction that modified
+ * catalogs, ensuring that a fresh cache is constructed during
+ * decoding.
+ *
* NB: This works correctly even for subtransactions because
* ReorderBufferAssignChild() takes care to transfer the base snapshot
* to the top-level transaction, and while iterating the changequeue
--
2.30.0.windows.2
On Thu, Feb 27, 2025 at 12:14 AM Zhijie Hou (Fujitsu)
<houzj.fnst@fujitsu.com> wrote:
On Monday, February 24, 2025 5:50 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Dec 11, 2024 at 12:37 PM Masahiko Sawada
<sawada.mshk@gmail.com> wrote:I confirmed that the proposed patch fixes these issues. I have one
question about the patch:In the main loop in SnapBuildDistributeSnapshotAndInval(), we have the
following code:/*
* If we don't have a base snapshot yet, there are no changes in this
* transaction which in turn implies we don't yet need a snapshot at
* all. We'll add a snapshot when the first change gets queued.
*
* NB: This works correctly even for subtransactions because
* ReorderBufferAssignChild() takes care to transfer the basesnapshot
* to the top-level transaction, and while iterating the changequeue
* we'll get the change from the subtxn.
*/
if (!ReorderBufferXidHasBaseSnapshot(builder->reorder, txn->xid))
continue;Is there any case where we need to distribute inval messages to
transactions that don't have the base snapshot yet but eventually need
the inval messages?Good point. It is mentioned that for snapshots: "We'll add a snapshot
when the first change gets queued.". I think we achieve this via
builder->committed.xip array such that when we set a base snapshot for
a transaction, we use that array to form a snapshot. However, I don't
see any such consideration for invalidations. Now, we could either
always add invalidations to xacts that don't have base_snapshot yet or
have a mechanism similar committed.xid array. But it is better to
first reproduce the problem.I think distributing invalidations to a transaction that has not yet built a
base snapshot is un-necessary. This is because, during the process of building
its base snapshot, such a transaction will have already recorded the XID of the
transaction that altered the publication information into its array of
committed XIDs. Consequently, it will reflect the latest changes in the catalog
from the beginning. In the context of logical decoding, this scenario is
analogous to decoding a new transaction initiated after the catalog-change
transaction has been committed.The original issue arises because the catalog cache was constructed using an
outdated snapshot that could not reflect the latest catalog changes. However,
this is not a problem in cases without a base snapshot. Since the existing
catalog cache should have been invalidated upon decoding the committed
catalog-change transaction, the subsequent transactions will construct a new
cache with the latest snapshot.
I've also concluded it's not necessary but the reason and analysis
might be somewhat different. IIUC in the original issue (looking at
Andres's reproducer[1]/messages/by-id/20231118025445.crhaeeuvoe2g5dv6@awork3.anarazel.de), the fact that when replaying a
non-catalog-change transaction, the walsender constructed the snapshot
that doesn't reflect the catalog change is fine because the first
change of that transaction was made before the catalog change. The
problem is that the walsender process absorbed the invalidation
message when replaying the change that happened before the catalog
change, and ended up keeping replaying the subsequent changes with
that snapshot. That is why we concluded that we need to distribute the
invalidation messages to concurrently decoded transactions so that we
can invalidate the cache again at that point. As the comment
mentioned, the base snapshot is set before queuing any changes, so if
the transaction doesn't have the base snapshot yet, there must be no
queued change that happened before the catalog change. The
transactions that initiated after the catalog change don't have this
issue.
Regards,
[1]: /messages/by-id/20231118025445.crhaeeuvoe2g5dv6@awork3.anarazel.de
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
On Fri, Feb 28, 2025 at 6:15 AM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Thu, Feb 27, 2025 at 12:14 AM Zhijie Hou (Fujitsu)
<houzj.fnst@fujitsu.com> wrote:On Monday, February 24, 2025 5:50 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Dec 11, 2024 at 12:37 PM Masahiko Sawada
<sawada.mshk@gmail.com> wrote:I confirmed that the proposed patch fixes these issues. I have one
question about the patch:In the main loop in SnapBuildDistributeSnapshotAndInval(), we have the
following code:/*
* If we don't have a base snapshot yet, there are no changes in this
* transaction which in turn implies we don't yet need a snapshot at
* all. We'll add a snapshot when the first change gets queued.
*
* NB: This works correctly even for subtransactions because
* ReorderBufferAssignChild() takes care to transfer the basesnapshot
* to the top-level transaction, and while iterating the changequeue
* we'll get the change from the subtxn.
*/
if (!ReorderBufferXidHasBaseSnapshot(builder->reorder, txn->xid))
continue;Is there any case where we need to distribute inval messages to
transactions that don't have the base snapshot yet but eventually need
the inval messages?Good point. It is mentioned that for snapshots: "We'll add a snapshot
when the first change gets queued.". I think we achieve this via
builder->committed.xip array such that when we set a base snapshot for
a transaction, we use that array to form a snapshot. However, I don't
see any such consideration for invalidations. Now, we could either
always add invalidations to xacts that don't have base_snapshot yet or
have a mechanism similar committed.xid array. But it is better to
first reproduce the problem.I think distributing invalidations to a transaction that has not yet built a
base snapshot is un-necessary. This is because, during the process of building
its base snapshot, such a transaction will have already recorded the XID of the
transaction that altered the publication information into its array of
committed XIDs. Consequently, it will reflect the latest changes in the catalog
from the beginning. In the context of logical decoding, this scenario is
analogous to decoding a new transaction initiated after the catalog-change
transaction has been committed.The original issue arises because the catalog cache was constructed using an
outdated snapshot that could not reflect the latest catalog changes. However,
this is not a problem in cases without a base snapshot. Since the existing
catalog cache should have been invalidated upon decoding the committed
catalog-change transaction, the subsequent transactions will construct a new
cache with the latest snapshot.I've also concluded it's not necessary but the reason and analysis
might be somewhat different. IIUC in the original issue (looking at
Andres's reproducer[1]), the fact that when replaying a
non-catalog-change transaction, the walsender constructed the snapshot
that doesn't reflect the catalog change is fine because the first
change of that transaction was made before the catalog change. The
problem is that the walsender process absorbed the invalidation
message when replaying the change that happened before the catalog
change, and ended up keeping replaying the subsequent changes with
that snapshot. That is why we concluded that we need to distribute the
invalidation messages to concurrently decoded transactions so that we
can invalidate the cache again at that point. As the comment
mentioned, the base snapshot is set before queuing any changes, so if
the transaction doesn't have the base snapshot yet, there must be no
queued change that happened before the catalog change. The
transactions that initiated after the catalog change don't have this
issue.
I think both of you are saying the same thing with slightly different
words. Hou-San's explanation goes into more detail at the code level,
and you have said the same thing with a slightly higher-level view.
Additionally, for streaming transactions where we would have already
sent one or more streams, we don't need anything special since they
behave similarly to a transaction having a base snapshot because we
save the snapshot after sending each stream.
--
With Regards,
Amit Kapila.
On Fri, Feb 28, 2025 at 9:45 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Fri, Feb 28, 2025 at 6:15 AM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Thu, Feb 27, 2025 at 12:14 AM Zhijie Hou (Fujitsu)
I think distributing invalidations to a transaction that has not yet built a
base snapshot is un-necessary. This is because, during the process of building
its base snapshot, such a transaction will have already recorded the XID of the
transaction that altered the publication information into its array of
committed XIDs. Consequently, it will reflect the latest changes in the catalog
from the beginning. In the context of logical decoding, this scenario is
analogous to decoding a new transaction initiated after the catalog-change
transaction has been committed.The original issue arises because the catalog cache was constructed using an
outdated snapshot that could not reflect the latest catalog changes. However,
this is not a problem in cases without a base snapshot. Since the existing
catalog cache should have been invalidated upon decoding the committed
catalog-change transaction, the subsequent transactions will construct a new
cache with the latest snapshot.I've also concluded it's not necessary but the reason and analysis
might be somewhat different.
Based on the discussion on this point and Hou-San's proposed comment,
I have tried to add/edit a few comments in 0001 patch. See, if those
make sense to you, it is important to capture the reason and theory we
discussed here in the form of comments so that it will be easy to
remember the reason in the future.
--
With Regards,
Amit Kapila.
Attachments:
v17-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchapplication/octet-stream; name=v17-0001-Distribute-invalidatons-if-change-in-catalog-tab.patchDownload
From 80be7b27ca4f4b0435c3b0b0de41536baabbc82b Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 23 Aug 2024 14:02:20 +0530
Subject: [PATCH v17] Distribute invalidatons if change in catalog tables
Distribute invalidations to inprogress transactions if the current
committed transaction change any catalog table.
---
.../replication/logical/reorderbuffer.c | 5 +-
src/backend/replication/logical/snapbuild.c | 54 +++-
src/include/replication/reorderbuffer.h | 4 +
src/test/subscription/t/100_bugs.pl | 267 ++++++++++++++++++
4 files changed, 313 insertions(+), 17 deletions(-)
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 5186ad2a397..72fc74544c7 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -222,9 +222,6 @@ int debug_logical_replication_streaming = DEBUG_LOGICAL_REP_STREAMING_BUFFERED
*/
static ReorderBufferTXN *ReorderBufferGetTXN(ReorderBuffer *rb);
static void ReorderBufferReturnTXN(ReorderBuffer *rb, ReorderBufferTXN *txn);
-static ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
- TransactionId xid, bool create, bool *is_new,
- XLogRecPtr lsn, bool create_as_top);
static void ReorderBufferTransferSnapToParent(ReorderBufferTXN *txn,
ReorderBufferTXN *subtxn);
@@ -630,7 +627,7 @@ ReorderBufferReturnRelids(ReorderBuffer *rb, Oid *relids)
* (with the given LSN, and as top transaction if that's specified);
* when this happens, is_new is set to true.
*/
-static ReorderBufferTXN *
+ReorderBufferTXN *
ReorderBufferTXNByXid(ReorderBuffer *rb, TransactionId xid, bool create,
bool *is_new, XLogRecPtr lsn, bool create_as_top)
{
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index bd0680dcbe5..6f6039f4ef2 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -161,7 +161,7 @@ static void SnapBuildFreeSnapshot(Snapshot snap);
static void SnapBuildSnapIncRefcount(Snapshot snap);
-static void SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn);
+static void SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid);
static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid,
uint32 xinfo);
@@ -720,18 +720,21 @@ SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid,
}
/*
- * Add a new Snapshot to all transactions we're decoding that currently are
- * in-progress so they can see new catalog contents made by the transaction
- * that just committed. This is necessary because those in-progress
- * transactions will use the new catalog's contents from here on (at the very
- * least everything they do needs to be compatible with newer catalog
- * contents).
+ * Add a new Snapshot and invalidation messages to all transactions we're
+ * decoding that currently are in-progress so they can see new catalog contents
+ * made by the transaction that just committed. This is necessary because those
+ * in-progress transactions will use the new catalog's contents from here on
+ * (at the very least everything they do needs to be compatible with newer
+ * catalog contents).
*/
static void
-SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
+SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid)
{
dlist_iter txn_i;
ReorderBufferTXN *txn;
+ ReorderBufferTXN *curr_txn;
+
+ curr_txn = ReorderBufferTXNByXid(builder->reorder, xid, false, NULL, InvalidXLogRecPtr, false);
/*
* Iterate through all toplevel transactions. This can include
@@ -749,6 +752,14 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
* transaction which in turn implies we don't yet need a snapshot at
* all. We'll add a snapshot when the first change gets queued.
*
+ * Similarly, we don't need to add invalidations to a transaction whose
+ * base snapshot is not yet set. Once a base snapshot is built, it will
+ * include the xids of committed transactions that have modified the
+ * catalog, thus reflecting the new catalog contents. The existing
+ * catalog cache will have already been invalidated after processing
+ * the invalidations in the transaction that modified catalogs,
+ * ensuring that a fresh cache is constructed during decoding.
+ *
* NB: This works correctly even for subtransactions because
* ReorderBufferAssignChild() takes care to transfer the base snapshot
* to the top-level transaction, and while iterating the changequeue
@@ -758,13 +769,13 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
continue;
/*
- * We don't need to add snapshot to prepared transactions as they
- * should not see the new catalog contents.
+ * We don't need to add snapshot or invalidations to prepared
+ * transactions as they should not see the new catalog contents.
*/
if (rbtxn_is_prepared(txn))
continue;
- elog(DEBUG2, "adding a new snapshot to %u at %X/%X",
+ elog(DEBUG2, "adding a new snapshot and invalidations to %u at %X/%X",
txn->xid, LSN_FORMAT_ARGS(lsn));
/*
@@ -774,6 +785,20 @@ SnapBuildDistributeNewCatalogSnapshot(SnapBuild *builder, XLogRecPtr lsn)
SnapBuildSnapIncRefcount(builder->snapshot);
ReorderBufferAddSnapshot(builder->reorder, txn->xid, lsn,
builder->snapshot);
+
+ /*
+ * Add invalidation messages to the reorder buffer of inprogress
+ * transactions except the current committed transaction, for which we
+ * will execute invalidations at the end.
+ *
+ * It is required, otherwise, we will end up using the stale catcache
+ * contents built by the current transaction even after its decoding
+ * which should have been invalidated due to concurrent catalog
+ * changing transaction.
+ */
+ if (txn->xid != xid && curr_txn->ninvalidations > 0)
+ ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn,
+ curr_txn->ninvalidations, curr_txn->invalidations);
}
}
@@ -1045,8 +1070,11 @@ SnapBuildCommitTxn(SnapBuild *builder, XLogRecPtr lsn, TransactionId xid,
/* refcount of the snapshot builder for the new snapshot */
SnapBuildSnapIncRefcount(builder->snapshot);
- /* add a new catalog snapshot to all currently running transactions */
- SnapBuildDistributeNewCatalogSnapshot(builder, lsn);
+ /*
+ * add a new catalog snapshot and invalidations messages to all
+ * currently running transactions
+ */
+ SnapBuildDistributeSnapshotAndInval(builder, lsn, xid);
}
}
diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h
index 517a8e3634f..4f3ceef0092 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -759,6 +759,10 @@ extern TransactionId *ReorderBufferGetCatalogChangesXacts(ReorderBuffer *rb);
extern void ReorderBufferSetRestartPoint(ReorderBuffer *rb, XLogRecPtr ptr);
+extern ReorderBufferTXN *ReorderBufferTXNByXid(ReorderBuffer *rb,
+ TransactionId xid, bool create, bool *is_new,
+ XLogRecPtr lsn, bool create_as_top);
+
extern void StartupReorderBuffer(void);
#endif
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index 83120f1cb6f..d6dbeebed54 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -477,6 +477,273 @@ $result =
is( $result, qq(2|f
3|t), 'check replicated update on subscriber');
+# Clean up
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+
+# 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.
+
+# Initial setup.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE PUBLICATION regress_pub1;
+));
+
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ CREATE TABLE tab_conc(a int);
+ CREATE SCHEMA sch3;
+ CREATE TABLE sch3.tab_conc(a int);
+ CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1;
+));
+
+# Bump the query timeout to avoid false negatives on slow test systems.
+my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
+
+# Initiate 3 background sessions.
+my $background_psql1 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql1->set_query_timer_restart();
+
+my $background_psql2 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+
+$background_psql2->set_query_timer_restart();
+
+my $background_psql3 = $node_publisher->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $psql_timeout_secs);
+$background_psql3->set_query_timer_restart();
+
+# Maintain an active transaction with the table that will be added to the
+# publication.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (1);
+));
+
+# Maintain an active transaction with a schema table that will be added to the
+# publication.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (1);
+));
+
+# Add the table to the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (2);
+ INSERT INTO sch3.tab_conc VALUES (2);
+));
+
+# Refresh the publication.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION regress_sub1 REFRESH PUBLICATION");
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'regress_sub1');
+
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2),
+ 'Ensure that the data from the sch3.tab_conc table is synchronized to the subscriber after the subscription is refreshed'
+);
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (3);
+ INSERT INTO sch3.tab_conc VALUES (3);
+));
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is replicated to the subscriber.
+$result = $node_subscriber->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_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3),
+ 'Verify that the incremental data for table sch3.tab_conc added after table synchronization is replicated to the subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even when
+# tables are dropped from the publication in presence of a concurrent active
+# transaction performing the DML on the same table.
+
+# Maintain an active transaction with the table that will be dropped from the
+# publication.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (4);
+));
+
+# Maintain an active transaction with a schema table that will be dropped from the
+# publication.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (4);
+));
+
+# Drop the table from the publication using background_psql, as the alter
+# publication operation will distribute the invalidations to inprogress txns.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 DROP TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# Perform an insert.
+$node_publisher->safe_psql(
+ 'postgres', qq(
+ INSERT INTO tab_conc VALUES (5);
+ INSERT INTO sch3.tab_conc VALUES (5);
+));
+
+$node_publisher->wait_for_catchup('regress_sub1');
+
+# Verify that the insert is not replicated to the subscriber.
+$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that data for table tab_conc are not replicated to subscriber');
+
+$result =
+ $node_subscriber->safe_psql('postgres', "SELECT * FROM sch3.tab_conc");
+is( $result, qq(1
+2
+3
+4),
+ 'Verify that the incremental data for table sch3.tab_conc are not replicated to subscriber'
+);
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is dropped in a concurrent active transaction.
+
+# Add tables to the publication.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 ADD TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (6);
+));
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (6);
+));
+
+# Drop publication.
+$background_psql3->query_safe("DROP PUBLICATION regress_pub1");
+
+# Perform an insert.
+$background_psql1->query_safe("INSERT INTO tab_conc VALUES (7)");
+$background_psql2->query_safe("INSERT INTO sch3.tab_conc VALUES (7)");
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# ERROR should appear on subscriber.
+my $offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION regress_sub1");
+
+# The bug was that the incremental data synchronization was happening even after
+# publication is renamed in a concurrent active transaction.
+
+# Create publication.
+$background_psql3->query_safe(
+ "CREATE PUBLICATION regress_pub1 FOR TABLE tab_conc, TABLES IN SCHEMA sch3"
+);
+
+# Create subscription.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub1"
+);
+
+# Maintain an active transaction with the table.
+$background_psql1->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO tab_conc VALUES (8);
+));
+
+# Maintain an active transaction with a schema table.
+$background_psql2->query_safe(
+ qq(
+ BEGIN;
+ INSERT INTO sch3.tab_conc VALUES (8);
+));
+
+# Rename publication.
+$background_psql3->query_safe(
+ "ALTER PUBLICATION regress_pub1 RENAME TO regress_pub1_rename");
+
+# Perform an insert.
+$background_psql1->query_safe("INSERT INTO tab_conc VALUES (9)");
+$background_psql2->query_safe("INSERT INTO sch3.tab_conc VALUES (9)");
+
+# Complete the transaction on the tables.
+$background_psql1->query_safe("COMMIT");
+$background_psql2->query_safe("COMMIT");
+
+# ERROR should appear on subscriber.
+$offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+ qr/ERROR: publication "regress_pub1" does not exist/, $offset);
+
+$background_psql1->quit;
+$background_psql2->quit;
+$background_psql3->quit;
+
$node_publisher->stop('fast');
$node_subscriber->stop('fast');
--
2.28.0.windows.1
Hi,
It took me a while but I ran the test on my laptop with 20 runs per
test. I asked for a dedicated server and will re-run the tests if/when I
have it.
count of partitions | Head (sec) | Fix (sec) | Degradation (%)
----------------------------------------------------------------------
1000 | 0,0265 | 0,028 | 5,66037735849054
5000 | 0,091 | 0,0945 | 3,84615384615385
10000 | 0,1795 | 0,1815 | 1,11420612813371
Concurrent Txn | Head (sec) | Patch (sec) | Degradation in %
---------------------------------------------------------------------
50 | 0,1797647 | 0,1920949 | 6,85907744957
100 | 0,3693029 | 0,3823425 | 3,53086856344
500 | 1,62265755 | 1,91427485 | 17,97158617972
1000 | 3,01388635 | 3,57678295 | 18,67676928162
2000 | 7,0171877 | 6,4713304 | 8,43500897435
I'll try to run test2.pl later (right now it fails).
hope this helps.
--
Benoit Lobréau
Consultant
http://dalibo.com
Attachments:
cache_inval_bug_v1.odsapplication/vnd.oasis.opendocument.spreadsheet; name=cache_inval_bug_v1.odsDownload
PK �B\Z�l9�. . mimetypeapplication/vnd.oasis.opendocument.spreadsheetPK �B\Z Configurations2/accelerator/PK �B\Z Configurations2/images/Bitmaps/PK �B\Z Configurations2/toolpanel/PK �B\Z Configurations2/floater/PK �B\Z Configurations2/statusbar/PK �B\Z Configurations2/toolbar/PK �B\Z Configurations2/progressbar/PK �B\Z Configurations2/popupmenu/PK �B\Z Configurations2/menubar/PK �B\Z
styles.xml�[�r�����`�����`x�dkf=�Z��\��e;��[� �9��y�<U�$��79�X��Dvy��n��hz���g��4_���w�G4�y�r���
�r_���& ��2��.C� \���cFr�,�(P�9v,_R�1_�0C|)�%-PnE�]��R��#��b�n�(��w8f���������4 �Qx�;eqL��C��{�C���+q���W"X,��Z��fS�1# �Q.������j����P�M���u����MW�;(���wo-���Tm&�5}��R���;�~�,���� H(�hV@�7���0����@�4���p�%�4��Q���<5$�(�����b-v�(�8�A�p���Lg)��6�XZ�����X��?t,���[6f��M-��y)�����sRX�4�dz�-�YX����sl�)�z����8O�@c6������,+��K���2Qc�N�>X ���AQ���J�M)l����S�%o]��3��T���)�]�Alr����O0"6�e�z�P
2p.���&��3���t������\�F�("|�\�(��,���7�w4�������0�i��H"HQ��VnFc��K�E�]� >��������T@��0�=(���?����(0~�����C���>�=T)s^�����W����S��p���A��aS���������������&F �s��3����Z�2�2XlA�h����)7�Z�ssy�f�e�U���DWPH��j2���|~!��y��)Z� S����E��mO �0�M��Q*��Z�&ZM��yk)F�%�z�U,k8A���h�/�{��-nC(I~�2�'�S����2����}����k��P�r��/�^�����2����g�"�����nr
���[��I�� �����Q�K�)��<��<������e:��2
�NP^@��8FT�BRl��������lTxW.���edp���!x�r�`8#����y��%�)5������J������Y5��v����r�v�u��J�z!���j�
�{�p2�Z1�����,V���bu$b��\yN���cV��������cY���k��5C�$��jb���\�����18��r�A�J�a��J�g��T�z����k���-oX�=hH�)WV�v4���C]�TA\�����Av��B��b�������-���N���e�k�#$��DT����Q�$I"�Pd�)��E8�
�dZD�hI����|#-$�@��
o��eI�BP"����n�*�r��c�oCC[��1ws����n����CK_������
]���@�E����]W9iYU�L��
��CAO����c�2��^2E�8����@)����#�1����OU\��[��@�e���� �E8�F�������v�[j����vd�T.��)-T�]�n�
%v��9������������I�=�����$�<b���~�H�o������2X�'�<�����d�����T��*��il�&���H���P�gG�
BP�S#��O�P+���!i�p~
v+���{Ixa��zL���)� ��� ��$��z�:Q�i�v�]�j��.+���%}x��Ei��Q5�/s��� �48�������j����o���j����&�2we���2se/��Mv�'t�R_��L���SM���> |�k���q������)�!����ClJ��;�:�b�^��g�pTO�<�����s�|E�~���
�"�_�����vZw��, ���f2H/W���^�n
��w�o���<���+�]+[�u���{YL�.������.�(���o�X����,���s�U�ru�����>&l��v�z 7��}_nX,n�����3A:�>]E����@���^��(��1�~�+����D�Lh�Ntd_�P��7��o?t�W��H���l=M$�IU��T=�Q��q�y��8�v�}d���:j���������f���_�=�����n������)���xCv������n_Ak�=�w>����O[_��:�O����K*?:��������}��O@ ���Y"��!�e�\���M_in0h���[�a����\1d���y���������UecL]x����iuPU���
���i�.���� 8o��M�x��B�|����`�XD�N��O��a�1�m�:�H4*�PB��`p��� ^m�R�&xs��\��ku�P�
K8�q4|����U��"@�
���wE�=L�+��^����Y��*�XB�����'�
�z�?{vi�Nd��8�7*���:Es���ao`z����Gi��� 6�x�L�]�?����`vY������)�WV��5����95��u��c��������9�
�RC� /�(�|�A9����*�)p����-����o���/^<���f�h9�x�f��9/[�a�O�j7�@�f�i�����B��u�������p���:�9��w�F�v���J�e���!���{�Rg��?t|���R���"����~)����]�d�b��
���!����{����q��������o �/9�����������z�������^����x�_PK���x>
�<