Subject: Re: Add an option to skip loading missing publication to avoid logical replication failure

From: Masahiko Sawada <[email-masked]>
Date: Sat, 8 Mar 2025 19:29:49 -0800

On Tue, Mar 4, 2025 at 9:04 PM Amit Kapila <[email-masked]> wrote:
>
> On Tue, Mar 4, 2025 at 12:23 PM vignesh C <[email-masked]> wrote:
> >
> > There is almost negligible dip with the above suggested way, the test
> > results for the same is given below(execution time is in milli
> > seconds):
> > Brach/records  |  100     |  1000   |  10000    |  100000 |  1000000
> > Head               |   10.25  |  15.85  |   65.53    |  569.15  |  9194.19
> > Patch              |   10.25  |  15.84  |   65.91    |  571.75  |  9208.66
> > % diff              |      0.00 |    0.06  |    -0.58    |     -0.46  |    -0.16
> >
> > There is a performance dip in the range of 0 to 0.58 percent.
> > The attached patch has the changes for the same. The test script used
> > is also attached.
> >
>
> The patch still needs more review but the change has negligible
> performance impact. The next step is to get more opinions on whether
> we should add a new subscription option (say
> skip_not_existant_publication) for this work. See patch v1-0002-* in
> email [1]. The problem summary is explained in email [2] and in the
> commit message of the 0001 patch in this thread. But still, let me
> write briefly for the ease of others.
>
> The problem is that ALTER SUBSCRIPTION ... SET PUBLICATION ... will
> lead to restarting of apply worker, and after the restart, the apply
> worker will use the existing slot and replication origin corresponding
> to the subscription. Now, it is possible that before the restart, the
> origin has not been updated, and the WAL start location points to a
> location before where PUBLICATION pointed to by SET PUBLICATION
> exists. This leads to an error: "ERROR:  publication "pub1" does not
> exist". Once this error occurs, apply worker will never be able to
> proceed and will always return the same error. For users, this is a
> problem because they would have created a publication before executing
> ALTER SUBSCRIPTION ... SET PUBLICATION .. and now they have no way to
> proceed.
>
> The solution we came up with is to skip loading the publication if the
> publication does not exist. We load the publication later and update
> the relation entry when the publication gets created.
>
> The two main concerns with this idea, as shared in email [3], are
> performance implications of this change and the possibility of current
> behaviour expectations from the users.
>
> We came up with a solution where the performance impact is negligible,
> as shown in the tests [4]. For that, we won't try to reload the
> skipped/missing publication for each change but will attempt it only
> when any new publication is created/dropped for a valid relation entry
> in RelationSyncCache (maintained by pgoutput).

Thank you for summarizing the issue. That helps catch up a lot.

>
> The new option skip_not_existant_publication is to address the second
> concern "Imagine you have a subscriber using two publications p1 and
> p2, and someone comes around and drops p1 by mistake. With the
> proposed patch, the subscription will notice this, but it'll continue
> sending data ignoring the missing publication. Yes, it will continue
> working, but it's quite possible this breaks the subscriber and it's
> be better to fail and stop replicating.".

I think that in this particular situation the current behavior would
be likely to miss more changes than the patch'ed behavior case.

After the logical replication stops, the user would have to alter the
subscription to subscribe to only p1 by executing 'ALTER SUBSCRIPTION
... SET PUBLICATION p1' in order to resume the logical replication. In
any case, the publisher might be receiving further changes but the
subscriber would end up missing changes for tables associated with p2
generated while p2 doesn't exist. Even if the user re-creates the
publication p2 after that, it would be hard for users to re-alter the
subscription to get changes for tables associated with p1 and p2 from
the exact point of p2 being created. Therefore, the subscriber could
end up missing some changes that happened between 'CREATE PUBLICATION
p2' and 'ALTER SUBSCRIPTION ... SET PUBLICATION p1, p2'.

On the other hand, with the patch, the publication can send the
changes to tables associated with p1 and p2 as soon as it decodes the
WAL record of re-CREATE PUBLICATION p2.

>
> I see the point of adding such an option to avoid breaking the current
> applications (if there are any) that are relying on current behaviour.
> But OTOH, I am not sure if users expect us to fail explicitly in such
> scenarios.

On the side note, with the patch since we ignore missing publications
we will be able to create a subscription with whatever publications
regardless their existence like:

CREATE SUBSCRIPTION ... PUBLICATION pub1, pub2, pub3, pub4, pub5, ..., pub1000;

The walsender corresponding to such subscriber can stream changes as
soon as the listed publications are created on the publisher and
REFRESH PUBLICATION is executed.

>
> This is a long-standing behaviour for which we get reports from time
> to time, and once analyzing a failure, Tom also looked at it and
> agreed that we don't have much choice to avoid skipping non-existent
> publications [5]. But we never concluded as to whether skipping should
> be a default behavior or an optional one. So, we need more opinions on
> it.

I'm leaning toward making the skipping behavior a default as I could
not find a good benefit for the current behavior (i.e., stopping
logical replication due to missing publications).

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com

---

From: Amit Kapila <[email-masked]>
Date: Mon, 10 Mar 2025 09:03:54 +0530

> REFRESH PUBLICATION is executed.

Right, but OTOH, one can expect that the data should start replicating
as soon as one creates a publication on the publisher. However, the
data for tables that are part of the publication will start
replicating from the point when the decoding process will process the
WAL corresponding to Create Publication. I suggest to add something
for this in docs unless it is already explained.

> logical replication due to missing publications).

Sounds reasonable. We can always add the option at a later point if
required. Thanks for your input. We can continue reviewing and
committing the current patch.

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Mon, 10 Mar 2025 09:32:56 +0530

On Tue, Mar 4, 2025 at 6:54 PM vignesh C <[email-masked]> wrote:
>
> On further thinking, I felt the use of publications_updated variable
> is not required we can use publications_valid itself which will be set
> if the publication system table is invalidated. Here is a patch for
> the same.
>

The patch relies on the fact that whenever a publication's data is
invalidated, it will also invalidate all the RelSyncEntires as per
publication_invalidation_cb. But note that we are discussing removing
that inefficiency in the thread  [1]. So, we should try to rebuild the
entry when we have skipped the required publication previously.

Apart from this, please consider updating the docs, as mentioned in my
response to Sawada-San's email.

BTW, I am planning to commit this only on HEAD as this is a behavior
change. Please let me know if you guys think otherwise.

[1] - https://www.postgresql.org/message-id/OSCPR01MB14966C09AA201EFFA706576A7F5C92%40OSCPR01MB14966.jpnprd01.prod.outlook.com

-- 
With Regards,
Amit Kapila.

---

From: Dilip Kumar <[email-masked]>
Date: Mon, 10 Mar 2025 10:14:58 +0530

> response to Sawada-San's email.

I'm not sure I fully understand it, but based on your previous email and
the initial email from Vignesh, if IIUC, the issue occurs when a
publication is created after a certain LSN. When ALTER SUBSCRIPTION ... SET
PUBLICATION is executed, the subscriber workers restart and request the
changes based on restart_lsn, which is at an earlier LSN in the WAL than
the LSN at which the publication was created. This leads to an error, and
we are addressing this behavior as part of the fix by skipping the changes
which are between the restart_lsn of subscriber and the lsn at which
publication is created and this behavior looks fine.

> change. Please let me know if you guys think otherwise.

Somehow this looks like a bug fix which should be backported no?  Am I
missing something?

-- 
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com

---

From: Amit Kapila <[email-masked]>
Date: Mon, 10 Mar 2025 10:54:06 +0530

On Mon, Mar 10, 2025 at 10:15 AM Dilip Kumar <[email-masked]> wrote:
> I'm not sure I fully understand it, but based on your previous email and the initial email from Vignesh, if IIUC, the issue occurs when a publication is created after a certain LSN. When ALTER SUBSCRIPTION ... SET PUBLICATION is executed, the subscriber workers restart and request the changes based on restart_lsn, which is at an earlier LSN in the WAL than the LSN at which the publication was created. This leads to an error, and we are addressing this behavior as part of the fix by skipping the changes which are between the restart_lsn of subscriber and the lsn at which publication is created and this behavior looks fine.
>

Yes, your understanding is correct, but note that as such, the patch
is simply skipping the missing publication. The skipped changes are
because those were on the table that is not part of any publication
w.r.t historic snapshot we have at the point of time.

> Somehow this looks like a bug fix which should be backported no?  Am I missing something?
>

We can consider this a bug-fix and backpatch it, but I am afraid that
because this is a behavior change, some users may not like it. Also, I
don't remember seeing public reports for this behavior; that is
probably because it is hard to hit. FYI, we found this via BF
failures. So, I thought it would be better to get this field tested
via HEAD only at this point in time.

-- 
With Regards,
Amit Kapila.

---

From: Dilip Kumar <[email-masked]>
Date: Tue, 11 Mar 2025 09:47:52 +0530

> w.r.t historic snapshot we have at the point of time.

So, it will skip loading the missing publication up to the LSN where
the publication is created and then load it from there, correct? Do we
have a test case for this? I couldn't find one in the latest patch or
in the email thread to demonstrate this behavior.

> via HEAD only at this point in time.

At the moment, I don't have a strong opinion on this. Since no one has
encountered or reported this issue, it might be the case that it's not
affecting anyone, and we could simply backpatch without causing any
dissatisfaction. However, I'm fine with whatever others decide.

---

From: vignesh C <[email-masked]>
Date: Tue, 11 Mar 2025 16:00:45 +0530

> response to Sawada-San's email.

The create subscription documentation already has "We allow
non-existent publications to be specified so that users can add those
later. This means pg_subscription can have non-existent publications."
and should be enough at [1]. Let me know if we need to add more
documentation.

Apart from this I have changed the log level that logs "skipped
loading publication" to WARNING as we log a warning "WARNING:
publications "pub2", "pub3" do not exist on the publisher" in case of
CREATE SUBSCRIPTION and looked similar to this. I can change it to a
different log level in case you feel this is not the right level.

Also I have added a test case for dilip's comment from [2].
The attached v7 version patch has the changes for the same.

[1] - https://www.postgresql.org/docs/devel/sql-createsubscription.html
[2] - https://www.postgresql.org/message-id/CAFiTN-tgUR6QLSs3UHK7gx4VP7cURGNkufA_xkrQLw9eCnbGQw%40mail.gmail.com

Regards,
Vignesh

---

From: Amit Kapila <[email-masked]>
Date: Wed, 12 Mar 2025 16:04:06 +0530

> dissatisfaction. However, I'm fine with whatever others decide.

Sawada-San, others, do you have an opinion on whether to backpatch this change?

-- 
With Regards,
Amit Kapila.

---

From: Dilip Kumar <[email-masked]>
Date: Wed, 12 Mar 2025 16:15:29 +0530

> The attached v7 version patch has the changes for the same.

Thanks, Vignesh, for adding the test. I believe you've tested the
effect of DROP PUBLICATION. However, I think we should also test the
behavior of ALTER SUBSCRIPTION...SET PUBLICATION before creating the
PUBLICATION, and then create the PUBLICATION at a later stage.

---

From: Masahiko Sawada <[email-masked]>
Date: Wed, 12 Mar 2025 14:51:04 -0700

> Sawada-San, others, do you have an opinion on whether to backpatch this change?

I'm also afraid of backpatching it so I guess it would be better to
push it to only HEAD. I think if users have encountered and we see
reported the issue we can consider backpatching again. If regression
tests on backbranches continue to fail intermittently, probably we can
consider adding waits as the patch Osumi-san proposed[1]?

Regards,

[1] https://www.postgresql.org/message-id/TYCPR01MB83737A68CD5D554EA82BD7B9EDD39%40TYCPR01MB8373.jpnprd01.prod.outlook.com

---

From: vignesh C <[email-masked]>
Date: Thu, 13 Mar 2025 07:38:16 +0530

> PUBLICATION, and then create the PUBLICATION at a later stage.

I felt having only one test case for this is enough, I have removed
the DROP PUBLICATION test and added the SET PUBLICATION test. The
attached v8 version patch has the changes for the same.

Regards,
Vignesh
From c2f806ce866e3b0dfcebcd639683627a83e4e35a Mon Sep 17 00:00:00 2001
From: Vignesh <[email-masked]>
Date: Mon, 3 Mar 2025 11:54:27 +0530
Subject: [PATCH v8] Fix logical replication breakage after ALTER SUBSCRIPTION
 ... SET PUBLICATION

Altering a subscription with `ALTER SUBSCRIPTION ... SET PUBLICATION`
could cause logical replication to break under certain conditions. When
the apply worker restarts after executing SET PUBLICATION, it continues
using the existing replication slot and origin. If the replication origin
was not updated before the restart, the WAL start location could point to
a position prior to the existence of the specified publication, leading to
persistent start of apply worker and reporting errors.

This patch skips loading the publication if the publication does not exist
and loads the publication and updates the relation entry when the publication
gets created.

Discussion: https://www.postgresql.org/message-id/flat/CALDaNm0-n8FGAorM%2BbTxkzn%2BAOUyx5%3DL_XmnvOP6T24%2B-NcBKg%40mail.gmail.com
Discussion: https://www.postgresql.org/message-id/[email-masked]
---
 src/backend/replication/pgoutput/pgoutput.c | 26 +++++++++++-
 src/test/subscription/t/024_add_drop_pub.pl | 44 ++++++++++++++++++++-
 2 files changed, 67 insertions(+), 3 deletions(-)

diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 9063af6e1df..991aa6f7282 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1762,6 +1762,8 @@ pgoutput_shutdown(LogicalDecodingContext *ctx)
 
 /*
  * Load publications from the list of publication names.
+ *
+ * Here, we just skip the publications that don't exist yet.
  */
 static List *
 LoadPublications(List *pubnames)
@@ -1772,9 +1774,29 @@ LoadPublications(List *pubnames)
 	foreach(lc, pubnames)
 	{
 		char	   *pubname = (char *) lfirst(lc);
-		Publication *pub = GetPublicationByName(pubname, false);
+		Publication *pub = GetPublicationByName(pubname, true);
 
-		result = lappend(result, pub);
+		if (pub)
+			result = lappend(result, pub);
+		else
+		{
+			/*
+			 * When executing 'ALTER SUBSCRIPTION ... SET PUBLICATION', the
+			 * apply worker continues using the existing replication slot and
+			 * origin after restarting. If the replication origin is not
+			 * updated before the restart, the WAL start location may point to
+			 * a position before the specified publication exists, causing
+			 * persistent apply worker restarts and errors.
+			 *
+			 * This ensures that the publication is skipped if it does not
+			 * exist and is loaded when the corresponding WAL record is
+			 * encountered.
+			 */
+			ereport(WARNING,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("skipped loading publication: %s", pubname),
+					errhint("If the publication already exists, ignore it as it will be loaded upon reaching the corresponding WAL record; otherwise, create it."));
+		}
 	}
 
 	return result;
diff --git a/src/test/subscription/t/024_add_drop_pub.pl b/src/test/subscription/t/024_add_drop_pub.pl
index 4428a3413db..4eb17f45ec3 100644
--- a/src/test/subscription/t/024_add_drop_pub.pl
+++ b/src/test/subscription/t/024_add_drop_pub.pl
@@ -1,7 +1,9 @@
 
 # Copyright (c) 2021-2025, PostgreSQL Global Development Group
 
-# This test checks behaviour of ALTER SUBSCRIPTION ... ADD/DROP PUBLICATION
+# This test checks behaviour of ALTER SUBSCRIPTION ... ADD/DROP PUBLICATION and
+# ensures that dropping a publication associated with a subscription does not
+# disrupt existing logical replication.
 use strict;
 use warnings FATAL => 'all';
 use PostgreSQL::Test::Cluster;
@@ -80,6 +82,46 @@ $result = $node_subscriber->safe_psql('postgres',
 	"SELECT count(*), min(a), max(a) FROM tab_1");
 is($result, qq(20|1|10), 'check initial data is copied to subscriber');
 
+# Ensure that setting a missing publication to the subscription does not
+# disrupt existing logical replication. Instead, it should log a warning
+# while allowing replication to continue. Additionally, verify that replication
+# resumes after the missing publication is created for the publication table.
+
+# Create table on publisher and subscriber
+$node_publisher->safe_psql('postgres', "CREATE TABLE tab_3 (a int)");
+$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_3 (a int)");
+
+# Set the subscription with a missing publication
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub SET PUBLICATION tap_pub_3");
+
+my $offset = -s $node_publisher->logfile;
+
+$node_publisher->safe_psql('postgres',"INSERT INTO tab_3 values(1)");
+
+# Verify that a warning is logged.
+$node_publisher->wait_for_log(
+	qr/WARNING: ( [A-Z0-9]+:)? skipped loading publication: tap_pub_3/, $offset);
+
+$node_publisher->safe_psql('postgres',
+	"CREATE PUBLICATION tap_pub_3 FOR TABLE tab_3");
+
+$node_subscriber->safe_psql('postgres',
+	"ALTER SUBSCRIPTION tap_sub REFRESH  PUBLICATION");
+
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub');
+
+$node_publisher->safe_psql('postgres', "INSERT INTO tab_3 values(2)");
+
+$node_publisher->wait_for_catchup('tap_sub');
+
+# Verify that the insert operation gets replicated to subscriber after
+# publication is created.
+$result = $node_subscriber->safe_psql('postgres',
+	"SELECT * FROM tab_3");
+is($result, qq(1
+2), 'check that the incremental data is replicated after the publication is created');
+
 # shutdown
 $node_subscriber->stop('fast');
 $node_publisher->stop('fast');
-- 
2.43.0

---

From: Dilip Kumar <[email-masked]>
Date: Thu, 13 Mar 2025 09:18:34 +0530

> attached v8 version patch has the changes for the same.

Thanks looks good to me.

While looking at the patch, I have a few comments/questions

+ if (pub)
+ errhint("If the publication already exists, ignore it as it will be
loaded upon reaching the corresponding WAL record; otherwise, create
it."));
+ }

This comment focuses on a specific use case regarding the problem with
'ALTER SUBSCRIPTION ... SET PUBLICATION,' but in reality, we are
addressing a more general case where the user is trying to SET
PUBLICATION or even CREATE SUBSCRIPTION, and some publications are
missing. Wouldn't it be better to rephrase the comment?

2. + errhint("If the publication already exists, ignore it as it will
be loaded upon reaching the corresponding WAL record; otherwise,
create it."));

Is this hint correct? This is a question rather than a comment: When
we reach a particular WAL where the publication was created, will the
publication automatically load, or does the user need to REFRESH the
publications?

---

From: vignesh C <[email-masked]>
Date: Thu, 13 Mar 2025 10:49:00 +0530

> missing. Wouldn't it be better to rephrase the comment?

How about a comment something like below:
/*
* In 'ALTER SUBSCRIPTION ... ADD/SET PUBLICATION' and
* 'CREATE SUBSCRIPTION', if the replication origin is not updated
* before the worker exits, the WAL start location might point to a
* position before the publication's WAL record. This can lead to
* persistent apply worker restarts and errors.
*
* Additionally, dropping a subscription's publication should not
* disrupt logical replication.
*
* This ensures that a missing publication is skipped and loaded
* when its corresponding WAL record is encountered.
*/
ereport(WARNING,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("skipped loading publication: %s", pubname),
errhint("If the publication is missing, create and refresh it.
Otherwise, wait for the slot to reach the WAL record, then refresh"));

> publication automatically load, or does the user need to REFRESH the

Users need to refresh the publication in case the relation is not
already added to pg_subscription_rel and apply incremental changes.
How about an error hint like:
"If the publication is missing, create and refresh it. Otherwise, wait
for the slot to reach the WAL record for the created publication, then
refresh"

Regards,
Vignesh

---

From: Dilip Kumar <[email-masked]>
Date: Thu, 13 Mar 2025 11:43:16 +0530

> * when its corresponding WAL record is encountered.

Looks fine, shall we add the missing publication point as well
something like below

/*
* In operations like 'ALTER SUBSCRIPTION ... ADD/SET PUBLICATION' and
* 'CREATE SUBSCRIPTION', if the specified publication does not exist or
* if the replication origin is not updated before the worker exits,
* the WAL start location may point to a position prior to the publication's
* WAL record. This can cause persistent restarts and errors
* in the apply worker.
*
*
*/

> for the slot to reach the WAL record for the created publication, then

+1

---

From: Amit Kapila <[email-masked]>
Date: Thu, 13 Mar 2025 15:20:01 +0530

> * WAL record. This can cause persistent restarts and errors

I think that is too much related to pub-sub model, and ideally,
pgoutput should not care about it. I have written a comment
considering somebody using pgoutput decoding module via APIs.

> > for the slot to reach the WAL record for the created publication, then

I have tried to split this information into errdetail and errhint in
the attached. See and let me know what you think of the same.

-- 
With Regards,
Amit Kapila.

---

From: Dilip Kumar <[email-masked]>
Date: Thu, 13 Mar 2025 19:26:28 +0530

> considering somebody using pgoutput decoding module via APIs.

I agree, here we just need to talk about skipping the missing
publication, not different scenarios where that can happen. This
comments look much better.

> the attached. See and let me know what you think of the same.

Yes, the errhint also makes sense to me.

---

From: Amit Kapila <[email-masked]>
Date: Fri, 14 Mar 2025 14:28:21 +0530

> > considering somebody using pgoutput decoding module via APIs.

Thanks, Dilip and Sawada-San, for the inputs, and Vignesh for the
patch. I have pushed the change.

-- 
With Regards,
Amit Kapila.

---

From: vignesh C <[email-masked]>
Date: Wed, 30 Apr 2025 22:01:11 +0530

On Wed, 30 Apr 2025 at 17:41, Amit Kapila <[email-masked]> wrote:
>
> On Wed, Apr 30, 2025 at 11:22 AM Tom Lane <[email-masked]> wrote:
> >
> > Xuneng Zhou pointed out on Discord that the test case added by
> > 7c99dc587 has caused repeated failures in CI --- though oddly,
> > it's not failed in the buildfarm so far as I can find.  The
> > failures look like
> >
> > timed out waiting for match: (?^:WARNING: ( [A-Z0-9]+:)? skipped loading publication: tap_pub_3) at /tmp/cirrus-ci-build/src/test/subscription/t/024_add_drop_pub.pl line 103.
> >
>
> I analyzed the relevant publisher-side CI Logs [1]:
> ...
> 2025-04-19 08:24:14.096 UTC [21961][client backend]
> [024_add_drop_pub.pl][7/4:0] LOG:  statement: INSERT INTO tab_3
> values(1)
> 2025-04-19 08:24:14.098 UTC [21961][client backend]
> [024_add_drop_pub.pl][:0] LOG:  disconnection: session time:
> 0:00:00.003 user=postgres database=postgres host=[local]
> 2025-04-19 08:24:14.108 UTC [21797][walsender] [tap_sub][30/0:0] LOG:
> released logical replication slot "tap_sub"
> 2025-04-19 08:24:14.108 UTC [21797][walsender] [tap_sub][:0] LOG:
> disconnection: session time: 0:00:00.329 user=postgres
> database=postgres host=[local]
> 2025-04-19 08:24:14.127 UTC [21979][not initialized] [[unknown]][:0]
> LOG:  connection received: host=[local]
> 2025-04-19 08:24:14.128 UTC [21979][walsender] [[unknown]][23/5:0]
> LOG:  connection authenticated: user="postgres" method=trust
> (/tmp/cirrus-ci-build/build/testrun/subscription/024_add_drop_pub/data/t_024_add_drop_pub_publisher_data/pgdata/pg_hba.conf:117)
> 2025-04-19 08:24:14.128 UTC [21979][walsender] [[unknown]][23/5:0]
> LOG:  replication connection authorized: user=postgres
> application_name=tap_sub
> 2025-04-19 08:24:14.129 UTC [21979][walsender] [tap_sub][23/6:0] LOG:
> statement: SELECT pg_catalog.set_config('search_path', '', false);
> 2025-04-19 08:24:14.130 UTC [21979][walsender] [tap_sub][23/0:0] LOG:
> received replication command: IDENTIFY_SYSTEM
> 2025-04-19 08:24:14.130 UTC [21979][walsender] [tap_sub][23/0:0]
> STATEMENT:  IDENTIFY_SYSTEM
> 2025-04-19 08:24:14.131 UTC [21979][walsender] [tap_sub][23/0:0] LOG:
> received replication command: START_REPLICATION SLOT "tap_sub" LOGICAL
> 0/0 (proto_version '4', streaming 'parallel', origin 'any',
> publication_names '"tap_pub_
> ...
>
> This shows that walsender restarts after the "INSERT INTO tab_3
> values(1)" is processed by the previous walsender ("released logical
> replication slot "tap_sub"" is after "INSERT INTO tab_3 values(1)").
> So, it is possible that the old apply worker has sent the confirmation
> of WAL received location after the Insert (due to keep_alive message
> handling). So, after the restart, the new walsender will start
> processing WAL after the INSERT and wait for the skipped message LOG
> timed out.
>
> Considering the above theory is correct, after "ALTER SUBSCRIPTION
> tap_sub SET PUBLICATION tap_pub_3", we should wait for the new
> walsender to restart. We are already doing the same for a similar case
> in 001_rep_changes.pl (See "# check that change of connection string
> and/or publication list causes restart of subscription workers. We
> check the state along with application_name to ensure that the
> walsender is (re)started.).
>
> Unfortunately, I will be away for the rest of the week. In the
> meantime, if you or someone else is able to reproduce and fix it, then
> good; otherwise, I'll take care of it after I return.

I agree with your analysis. I was able to reproduce the issue by
delaying the invalidation of the subscription until the walsender
finished decoding the INSERT operation following the ALTER
SUBSCRIPTION through a debugger and using the lsn from the pg_waldump
of the INSERT after the ALTER SUBSCRIPTION.  In this scenario, the
confirmed_flush_lsn ends up pointing to a location after the INSERT.
When the invalidation is eventually received and the apply
worker/walsender is restarted, the restarted walsender begins decoding
from that LSN—after the INSERT—which means the "skipped loading
publication" warning is never triggered, causing the test to fail.

Attached is a patch that ensures the walsender process is properly
restarted after ALTER SUBSCRIPTION, preventing this race condition.

Regards,
Vignesh
From 26c8efbf59a456f4ad8a87b504180449efe1cd69 Mon Sep 17 00:00:00 2001
From: Vignesh <[email-masked]>
Date: Wed, 30 Apr 2025 21:38:33 +0530
Subject: [PATCH] Fix race condition after ALTER SUBSCRIPTION SET PUBLICATION

Previously, after executing ALTER SUBSCRIPTION tap_sub SET PUBLICATION, we
did not wait for the new walsender process to restart. As a result, an INSERT
executed immediately after the ALTER could be decoded and the confirmed flush
lsn is advanced. This could cause replication to resume from a point after the
INSERT. In such cases, we miss the expected warning about the missing
publication.

To fix this, we now ensure that the walsender has restarted before continuing
after ALTER SUBSCRIPTION.
---
 src/test/subscription/t/024_add_drop_pub.pl | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/test/subscription/t/024_add_drop_pub.pl b/src/test/subscription/t/024_add_drop_pub.pl
index b594941c7cb..e995d8b3839 100644
--- a/src/test/subscription/t/024_add_drop_pub.pl
+++ b/src/test/subscription/t/024_add_drop_pub.pl
@@ -91,10 +91,21 @@ is($result, qq(20|1|10), 'check initial data is copied to subscriber');
 $node_publisher->safe_psql('postgres', "CREATE TABLE tab_3 (a int)");
 $node_subscriber->safe_psql('postgres', "CREATE TABLE tab_3 (a int)");
 
+my $oldpid = $node_publisher->safe_psql('postgres',
+	"SELECT pid FROM pg_stat_replication WHERE application_name = 'tap_sub' AND state = 'streaming';"
+);
+
 # Set the subscription with a missing publication
 $node_subscriber->safe_psql('postgres',
 	"ALTER SUBSCRIPTION tap_sub SET PUBLICATION tap_pub_3");
 
+# Wait for the walsender to restart after altering the subscription
+$node_publisher->poll_query_until('postgres',
+	"SELECT pid != $oldpid FROM pg_stat_replication WHERE application_name = 'tap_sub' AND state = 'streaming';"
+  )
+  or die
+  "Timed out while waiting for apply worker to restart after altering the subscription";
+
 my $offset = -s $node_publisher->logfile;
 
 $node_publisher->safe_psql('postgres',"INSERT INTO tab_3 values(1)");
-- 
2.43.0

---

From: Xuneng Zhou <[email-masked]>
Date: Fri, 2 May 2025 11:43:45 +0800

+1,  I was unable to reproduce this with lldb, not sure my way is
appropriate or not.

>
> Can you be a little more specific about how you reproduced this?
> I tried inserting sleep() calls in various likely-looking spots
> and could not get a failure that way.
>
>                         regards, tom lane

---

From: vignesh C <[email-masked]>
Date: Fri, 2 May 2025 09:23:30 +0530

On Fri, 2 May 2025 at 06:30, Tom Lane <[email-masked]> wrote:
>
> vignesh C <[email-masked]> writes:
> > of the INSERT after the ALTER SUBSCRIPTION.
>

Test Steps:
1) Set up logical replication:
Create a publication on the publisher
Create a subscription on the subscriber
2) Create the following table on the publisher:
CREATE TABLE tab_3 (a int);
3) Create the same table on the subscriber:
CREATE TABLE tab_3 (a int);
4) On the subscriber, alter the subscription to refer to a
non-existent publication:
ALTER SUBSCRIPTION sub1 SET PUBLICATION tap_pub_3;
5) Insert data on the publisher:
INSERT INTO tab_3 VALUES (1);

As expected, the publisher logs the following warning in normal case:
2025-05-02 08:56:45.350 IST [516197] WARNING:  skipped loading
publication: tap_pub_3
2025-05-02 08:56:45.350 IST [516197] DETAIL:   The publication does
not exist at this point in the WAL.
2025-05-02 08:56:45.350 IST [516197] HINT:     Create the publication
if it does not exist.

To simulate a delay in subscription invalidation, I modified the
maybe_reread_subscription() function as follows:
diff --git a/src/backend/replication/logical/worker.c
b/src/backend/replication/logical/worker.c
index 4151a4b2a96..0831784aca3 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -3970,6 +3970,10 @@ maybe_reread_subscription(void)
     MemoryContext oldctx;
     Subscription *newsub;
     bool            started_tx = false;
+    bool            test = true;
+
+    if (test)
+        return;

This change delays the subscription invalidation logic, preventing the
apply worker from detecting the subscription change immediately.

With the patch applied, repeat steps 1–5.
Using pg_waldump, identify the LSN of the insert:
rmgr: Heap        len (rec/tot):     59/    59, tx: 756, lsn:
0/01711848, prev 0/01711810, desc: INSERT+INIT off: 1
rmgr: Transaction len (rec/tot):     46/    46, tx: 756, lsn:
0/01711888, prev 0/01711848, desc: COMMIT 2025-05-02 09:06:09.400926
IST

Check the confirmed flush LSN from the walsender via gdb by attaching
it to the walsender process
(gdb) p *MyReplicationSlot
...
confirmed_flush = 24241928
(gdb) p /x 24241928
$4 = 0x171e708

Now attach to the apply worker, set a breakpoint at
maybe_reread_subscription, and continue execution. Once control
reaches the function, set test = false. Now it will identify that
subscription is invalidated and restart the apply worker.

As the walsender has already confirmed_flush position after the
insert, causing the newly started apply worker to miss the inserted
row entirely. This leads to the CI failure. This issue can arise when
the walsender advances more quickly than the apply worker is able to
detect and react to the subscription change.

I could not find a simpler way to reproduce this.

Regards,
Vignesh
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 4151a4b2a96..0831784aca3 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -3970,6 +3970,10 @@ maybe_reread_subscription(void)
 	MemoryContext oldctx;
 	Subscription *newsub;
 	bool		started_tx = false;
+	bool		test = true;
+
+	if (test)
+		return;
 
 	/* When cache state is valid there is nothing to do here. */
 	if (MySubscriptionValid)

---

From: Xuneng Zhou <[email-masked]>
Date: Fri, 2 May 2025 12:41:02 +0800

Hi,
Is this an expected behavior?

A race between subscriber LSN feedback and publisher subscription change
processing allows the walsender to restart decoding past relevant WAL
records, bypassing the updated subscription rules for those records.

On Wed, 30 Apr 2025 at 17:41, Amit Kapila <[email-masked]> wrote:
> > > timed out waiting for match: (?^:WARNING: ( [A-Z0-9]+:)? skipped
> loading publication: tap_pub_3) at
> /tmp/cirrus-ci-build/src/test/subscription/t/024_add_drop_pub.pl line 103.
> > >
> >

---

From: Tom Lane <[email-masked]>
Date: Fri, 02 May 2025 00:46:00 -0400

> Is this an expected behavior?

I'm wondering that too.  I don't see how the repro method Vignesh
describes could correspond to a simple timing issue.  It smells
like there's a bug here somewhere.

			regards, tom lane

---

From: vignesh C <[email-masked]>
Date: Fri, 2 May 2025 10:52:43 +0530

On Fri, 2 May 2025 at 10:11, Xuneng Zhou <[email-masked]> wrote:
> A race between subscriber LSN feedback and publisher subscription change processing allows the walsender to restart decoding past relevant WAL records, bypassing the updated subscription rules for those records.

We have three processes involved in this scenario:
A walsender process on the publisher, responsible for decoding and
sending WAL changes.
An apply worker process on the subscriber, which applies the changes.
A session executing the ALTER SUBSCRIPTION command.

Due to the asynchronous nature of these processes, the ALTER
SUBSCRIPTION command may not be immediately observed by the apply
worker. Meanwhile, the walsender may process and decode an INSERT
statement.
If the insert targets a table (e.g., tab_3) that does not belong to
the current publication (pub1), the walsender silently skips
replicating the record and advances its decoding position. This
position is sent in a keepalive message to the subscriber, and since
there are no pending transactions to flush, the apply worker reports
it as the latest received LSN.
Later, when the apply worker eventually detects the subscription
change, it restarts—but by then, the insert has already been skipped
and is no longer eligible for replay, as the table was not part of the
publication (pub1) at the time of decoding.
This race condition arises because the three processes run
independently and may progress at different speeds due to CPU
scheduling or system load.
Thoughts?

Regards,
Vignesh

---

From: Xuneng Zhou <[email-masked]>
Date: Fri, 2 May 2025 18:44:31 +0800

Yeh, tks for your clarification.  I have a basic understanding of it now. I
mean is this considered a bug or design defect in the codebase? If so,
should we prevent it from occuring in general, not just for this specific
test.

vignesh C <[email-masked]>

---

From: Tom Lane <[email-masked]>
Date: Fri, 02 May 2025 12:54:01 -0400

> it as the latest received LSN.

So this theory presumes that the apply worker receives and reacts to
the keepalive message, yet it has not observed a relevant
subscriber-side catalog update that surely committed before the
keepalive was generated.  It's fairly hard to see how that is okay,
because it's at least adjacent to something that must be considered a
bug: applying transmitted data without having observed DDL updates to
the target table.  Why is the processing of keepalives laxer than the
processing of data messages?

			regards, tom lane

---

From: vignesh C <[email-masked]>
Date: Sun, 4 May 2025 18:44:09 +0530

> I could not find a simpler way to reproduce this.

A simpler way to consistently reproduce the issue is to add a 1-second
sleep in the LogicalRepApplyLoop function, just before the call to
WaitLatchOrSocket. This reproduces the test failure consistently for
me. The failure reason is the same as in [1].

[1] - https://www.postgresql.org/message-id/CALDaNm2Q_pfwiCkaV920iXEbh4D%3D5MmD_tNQm_GRGX6-MsLxoQ%40mail.gmail.com

Regards,
Vignesh
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 4151a4b2a96..d0056f5655c 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -3702,6 +3702,7 @@ LogicalRepApplyLoop(XLogRecPtr last_received)
 						if (last_received < end_lsn)
 							last_received = end_lsn;
 
+						elog(LOG, "Send feedback from 1");
 						send_feedback(last_received, reply_requested, false);
 						UpdateWorkerStats(last_received, timestamp, true);
 					}
@@ -3714,6 +3715,7 @@ LogicalRepApplyLoop(XLogRecPtr last_received)
 			}
 		}
 
+		elog(LOG, "Send feedback from 2");
 		/* confirm all writes so far */
 		send_feedback(last_received, false, false);
 
@@ -3739,6 +3741,8 @@ LogicalRepApplyLoop(XLogRecPtr last_received)
 		if (endofstream)
 			break;
 
+		sleep(1);
+
 		/*
 		 * Wait for more data or latch.  If we have unflushed transactions,
 		 * wake up after WalWriterDelay to see if they've been flushed yet (in
@@ -3812,6 +3816,7 @@ LogicalRepApplyLoop(XLogRecPtr last_received)
 				}
 			}
 
+			elog(LOG, "Send feedback from 3");
 			send_feedback(last_received, requestReply, requestReply);
 
 			/*
@@ -3910,7 +3915,7 @@ send_feedback(XLogRecPtr recvpos, bool force, bool requestReply)
 	pq_sendint64(reply_message, now);	/* sendTime */
 	pq_sendbyte(reply_message, requestReply);	/* replyRequested */
 
-	elog(DEBUG2, "sending feedback (force %d) to recv %X/%X, write %X/%X, flush %X/%X",
+	elog(LOG, "sending feedback (force %d) to recv %X/%X, write %X/%X, flush %X/%X",
 		 force,
 		 LSN_FORMAT_ARGS(recvpos),
 		 LSN_FORMAT_ARGS(writepos),
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 9fa8beb6103..9896a8d74d5 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -4062,7 +4062,8 @@ pg_stat_get_wal_senders(PG_FUNCTION_ARGS)
 static void
 WalSndKeepalive(bool requestReply, XLogRecPtr writePtr)
 {
-	elog(DEBUG2, "sending replication keepalive");
+	elog(LOG, "sending replication keepalive - writePtr %X/%X",
+		 LSN_FORMAT_ARGS(writePtr));
 
 	/* construct the message... */
 	resetStringInfo(&output_message);

---

From: Amit Kapila <[email-masked]>
Date: Mon, 5 May 2025 11:48:35 +0530

> processing of data messages?

Valid question, as of now, we don't have a specific rule about
ordering the processing of keepalives or invalidation messages. The
effect of invalidation messages is realized by calling
maybe_reread_subscription at three different times after accepting
invalidation message, (a) after starting a transaction in
begin_replication_step, (b) in the commit message handling if there is
no data modification happened in that transaction, and (c) when we
don't get any transactions for a while

The (a) ensures we consume any target table change before applying a
new transaction. The other two places ensure that we keep consuming
invalidation messages from time to time.

Now, we can consume invalidation messages during keepalive message
handling and or at some other places, to ensure that we never process
any remote message before consuming an invalidation message. However,
it is not clear to if this is a must kind of thing. We can provide
strict guarantees for ordering of messages from any one of the
servers, but providing it across nodes doesn't sound to be a
must-criterion.

-- 
With Regards,
Amit Kapila.

---

From: Xuneng Zhou <[email-masked]>
Date: Tue, 6 May 2025 18:03:40 +0800

Hi,

A clear benefit of addressing this in code is to ensure that the user sees
the log message, which can be valuable for trouble-shooting—even under race
conditions.

                        ereport(WARNING,

                                        errmsg("skipped loading
publication: %s", pubname),

                                        errdetail("The publication does not
exist at this point in the WAL."),

                                        errhint("Create the publication if
it does not exist."));

The performance impact appears low, assuming the
AcceptInvalidationMessages and maybe_reread_subscription check are
introduced only in the code path that handles keepalive messages requiring
a reply.

---

From: Amit Kapila <[email-masked]>
Date: Tue, 6 May 2025 17:17:37 +0530

On Tue, May 6, 2025 at 3:33 PM Xuneng Zhou <[email-masked]> wrote:
>
> A clear benefit of addressing this in code is to ensure that the user sees the log message, which can be valuable for trouble-shooting—even under race conditions.
>

I don't think we can take that guarantee because if the Insert is
concurrent or slightly before the Alter Subscription command, then
there won't be a guarantee that users will see the skipped LOG
message.

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Sat, 10 May 2025 17:15:22 +0530

> there won't be a guarantee that users will see the skipped LOG

I am planning to proceed with the test-fix proposed by Vignesh [1]
early next week unless we want to discuss more on this issue.

[1] - https://www.postgresql.org/message-id/CALDaNm3TH3J8fwj%2BNcdjdN8DZCdrnmm1kzBsHBW2nkN%2Bh6up3A%40mail.gmail.com

-- 
With Regards,
Amit Kapila.

---

From: Xuneng Zhou <[email-masked]>
Date: Sun, 11 May 2025 23:19:22 +0800

Hi, I was able to reproduce the failure by adding a 1-second sleep in the
LogicalRepApplyLoop function
However, I noticed that the tests under src/test/subscription run
significantly slower— is this normal?

Also, it looks like the patch mentioned in this thread addresses the issue:
https://www.postgresql.org/message-id/CALDaNm2Q_pfwiCkaV920iXEbh4D%3D5MmD_tNQm_GRGX6-MsLxoQ%40mail.gmail.com

> [1] -
> https://www.postgresql.org/message-id/CALDaNm2Q_pfwiCkaV920iXEbh4D%3D5MmD_tNQm_GRGX6-MsLxoQ%40mail.gmail.com
>
> Regards,
> Vignesh
>

---

From: Amit Kapila <[email-masked]>
Date: Mon, 12 May 2025 11:11:28 +0530

On Sun, May 11, 2025 at 8:49 PM Xuneng Zhou <[email-masked]> wrote:
>
> Hi, I was able to reproduce the failure by adding a 1-second sleep in the LogicalRepApplyLoop function
> However, I noticed that the tests under src/test/subscription run significantly slower— is this normal?
>

Yes, because you made apply slower by adding a sleep.

> https://www.postgresql.org/message-id/CALDaNm2Q_pfwiCkaV920iXEbh4D%3D5MmD_tNQm_GRGX6-MsLxoQ%40mail.gmail.com

So you are okay with a test-only fix?

With Regards,
Amit Kapila.

---

From: Xuneng Zhou <[email-masked]>
Date: Mon, 12 May 2025 17:01:33 +0800

If the presumed theory regarding the cause of the issue is correct — as
outlined in this email
<https://www.postgresql.org/message-id/CALDaNm2Pmbc-7KM3nRgZcq1EBhbdvWJSTie-st57oGuKP4O44w%40mail.gmail.com>
— and no data replication occurs in this scenario
<https://www.postgresql.org/message-id/CAA4eK1Jz20hnPRDtPDo2BbSt0Xf8u2zY4Tc84R0OAQN8M%3D9iCQ%40mail.gmail.com>
, then the proposed fix seems ok to me.
But I don’t have the expertise to fully assess the trade-offs between
enforcing strict ordering across nodes and maintaining the current behavior.

---

From: Amit Kapila <[email-masked]>
Date: Tue, 13 May 2025 17:03:25 +0530

> early next week unless we want to discuss more on this issue.

Pushed.

-- 
With Regards,
Amit Kapila.