Avoid retaining conflict-related data when no tables are subscribed
Hi,
My colleague Nisha reported an issue to me off-list: dead tuples can't
be removed when retain_dead_tuples is enabled for a subscription with no tables.
This appears to stem from the inability to advance the non-removable transaction
ID when AllTablesyncsReady() returns false. Since this function returns false
when no tables are present, which leads to unnecessary data retention until a
table is added to the subscription.
Since dead tuples don't need to be retained when no tables are subscribed, here
is a patch to fix it, modifying AllTablesyncsReady() to allows no tables to be
treated as a ready state when explicitly requested.
Best Regards,
Hou zj
Attachments:
v1-0001-Avoid-retaining-conflict-related-data-when-no-tab.patchapplication/octet-stream; name=v1-0001-Avoid-retaining-conflict-related-data-when-no-tab.patchDownload
From a7bd2863fd2c15c3acc5221252673a7715025124 Mon Sep 17 00:00:00 2001
From: Zhijie Hou <houzj.fnst@fujitsu.com>
Date: Wed, 27 Aug 2025 18:11:38 +0800
Subject: [PATCH v1] Avoid retaining conflict-related data when no tables are
subscribed
This commit fixes an issue where conflict-related data was unnecessarily
retained when the subscription does not have a table.
---
.../replication/logical/applyparallelworker.c | 2 +-
src/backend/replication/logical/tablesync.c | 18 +++++++++++++-----
src/backend/replication/logical/worker.c | 14 ++++++++++++--
src/include/replication/worker_internal.h | 2 +-
4 files changed, 27 insertions(+), 9 deletions(-)
diff --git a/src/backend/replication/logical/applyparallelworker.c b/src/backend/replication/logical/applyparallelworker.c
index 31a92d1a24a..5b4fb6a08c4 100644
--- a/src/backend/replication/logical/applyparallelworker.c
+++ b/src/backend/replication/logical/applyparallelworker.c
@@ -309,7 +309,7 @@ pa_can_start(void)
* should_apply_changes_for_rel) as we won't know remote_final_lsn by that
* time. So, we don't start the new parallel apply worker in this case.
*/
- if (!AllTablesyncsReady())
+ if (!AllTablesyncsReady(false))
return false;
return true;
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index d3356bc84ee..d0dca0ebf5e 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -664,7 +664,7 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn)
if (MySubscription->twophasestate == LOGICALREP_TWOPHASE_STATE_PENDING)
{
CommandCounterIncrement(); /* make updates visible */
- if (AllTablesyncsReady())
+ if (AllTablesyncsReady(false))
{
ereport(LOG,
(errmsg("logical replication apply worker for subscription \"%s\" will restart so that two_phase can be enabled",
@@ -1759,15 +1759,19 @@ TablesyncWorkerMain(Datum main_arg)
}
/*
- * If the subscription has no tables then return false.
+ * Check if all tablesyncs are READY for the current subscription.
*
- * Otherwise, are all tablesyncs READY?
+ * If the subscription has no tables, return the value determined by
+ * 'ready_if_no_tables'.
+ *
+ * Otherwise, return whether all the tables for the subscription are in the
+ * READY state.
*
* Note: This function is not suitable to be called from outside of apply or
* tablesync workers because MySubscription needs to be already initialized.
*/
bool
-AllTablesyncsReady(void)
+AllTablesyncsReady(bool ready_if_no_tables)
{
bool started_tx = false;
bool has_subrels = false;
@@ -1781,11 +1785,15 @@ AllTablesyncsReady(void)
pgstat_report_stat(true);
}
+ /* If there are no tables, decide readiness based on the parameter */
+ if (!has_subrels)
+ return ready_if_no_tables;
+
/*
* Return false when there are no tables in subscription or not all tables
* are in ready state; true otherwise.
*/
- return has_subrels && (table_states_not_ready == NIL);
+ return table_states_not_ready == NIL;
}
/*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 22ad9051db3..136584d4569 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4547,8 +4547,18 @@ wait_for_local_flush(RetainDeadTuplesData *rdt_data)
* It is safe to add new tables with initial states to the subscription
* after this check because any changes applied to these tables should
* have a WAL position greater than the rdt_data->remote_lsn.
+ *
+ * Advancing the transaction ID is also necessary when no tables are
+ * subscribed, as it prevents unnecessary retention of dead tuples. Although
+ * it seem feasible to skip all phases and directly assign candidate_xid to
+ * oldest_nonremovable_xid in the RDT_GET_CANDIDATE_XID phase when no tables
+ * are currently subscribed, this approach is unsafe. This is because new
+ * tables may be added to the subscription after the initial table check,
+ * requiring tuples deleted before candidate_xid for conflict detection in
+ * upcoming transactions. Therefore, it remains necessary to wait for all
+ * concurrent transactions to be fully applied.
*/
- if (!AllTablesyncsReady())
+ if (!AllTablesyncsReady(true))
return;
/*
@@ -5345,7 +5355,7 @@ run_apply_worker()
* work.
*/
if (MySubscription->twophasestate == LOGICALREP_TWOPHASE_STATE_PENDING &&
- AllTablesyncsReady())
+ AllTablesyncsReady(false))
{
/* Start streaming with two_phase enabled */
options.proto.logical.twophase = true;
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 7c0204dd6f4..08a8dcaba20 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -268,7 +268,7 @@ extern int logicalrep_sync_worker_count(Oid subid);
extern void ReplicationOriginNameForLogicalRep(Oid suboid, Oid relid,
char *originname, Size szoriginname);
-extern bool AllTablesyncsReady(void);
+extern bool AllTablesyncsReady(bool ready_if_no_tables);
extern void UpdateTwoPhaseState(Oid suboid, char new_state);
extern void process_syncing_tables(XLogRecPtr current_lsn);
--
2.51.0.windows.1
On Thu, Aug 28, 2025 at 7:54 AM Zhijie Hou (Fujitsu)
<houzj.fnst@fujitsu.com> wrote:
Hi,
My colleague Nisha reported an issue to me off-list: dead tuples can't
be removed when retain_dead_tuples is enabled for a subscription with no tables.This appears to stem from the inability to advance the non-removable transaction
ID when AllTablesyncsReady() returns false. Since this function returns false
when no tables are present, which leads to unnecessary data retention until a
table is added to the subscription.Since dead tuples don't need to be retained when no tables are subscribed, here
is a patch to fix it, modifying AllTablesyncsReady() to allows no tables to be
treated as a ready state when explicitly requested.
The patch LGTM. Verified it, fixes the issue.
thanks
Shveta
On Thu, Aug 28, 2025 at 7:54 AM Zhijie Hou (Fujitsu)
<houzj.fnst@fujitsu.com> wrote:
Hi,
My colleague Nisha reported an issue to me off-list: dead tuples can't
be removed when retain_dead_tuples is enabled for a subscription with no tables.This appears to stem from the inability to advance the non-removable transaction
ID when AllTablesyncsReady() returns false. Since this function returns false
when no tables are present, which leads to unnecessary data retention until a
table is added to the subscription.Since dead tuples don't need to be retained when no tables are subscribed, here
is a patch to fix it, modifying AllTablesyncsReady() to allows no tables to be
treated as a ready state when explicitly requested.
+ /* If there are no tables, decide readiness based on the parameter */
+ if (!has_subrels)
+ return ready_if_no_tables;
+
/*
* Return false when there are no tables in subscription or not all tables
* are in ready state; true otherwise.
*/
- return has_subrels && (table_states_not_ready == NIL);
+ return table_states_not_ready == NIL;
The first part of the comment "Return false when there are no tables
in subscription" is outdated now with your fix. Otherwise it looks
fine.
--
Regards,
Dilip Kumar
Google
Just a typo:
+ * it seem feasible to skip all phases and directly assign
Should be "it seems".
Regards,
Steven
在 2025/8/28 10:23, Zhijie Hou (Fujitsu) 写道:
Show quoted text
Hi,
My colleague Nisha reported an issue to me off-list: dead tuples can't
be removed when retain_dead_tuples is enabled for a subscription with no tables.This appears to stem from the inability to advance the non-removable transaction
ID when AllTablesyncsReady() returns false. Since this function returns false
when no tables are present, which leads to unnecessary data retention until a
table is added to the subscription.Since dead tuples don't need to be retained when no tables are subscribed, here
is a patch to fix it, modifying AllTablesyncsReady() to allows no tables to be
treated as a ready state when explicitly requested.Best Regards,
Hou zj
On Thu, Aug 28, 2025 at 7:54 AM Zhijie Hou (Fujitsu)
<houzj.fnst@fujitsu.com> wrote:
Hi,
My colleague Nisha reported an issue to me off-list: dead tuples can't
be removed when retain_dead_tuples is enabled for a subscription with no tables.This appears to stem from the inability to advance the non-removable transaction
ID when AllTablesyncsReady() returns false. Since this function returns false
when no tables are present, which leads to unnecessary data retention until a
table is added to the subscription.Since dead tuples don't need to be retained when no tables are subscribed, here
is a patch to fix it, modifying AllTablesyncsReady() to allows no tables to be
treated as a ready state when explicitly requested.
Thank you Hou-san for the patch. I've verified the fix and tested the
changes. The patch LGTM.
--
Thanks,
Nisha
On Thu, Aug 28, 2025 at 7:54 AM Zhijie Hou (Fujitsu)
<houzj.fnst@fujitsu.com> wrote:
Hi,
My colleague Nisha reported an issue to me off-list: dead tuples can't
be removed when retain_dead_tuples is enabled for a subscription with no tables.This appears to stem from the inability to advance the non-removable transaction
ID when AllTablesyncsReady() returns false. Since this function returns false
when no tables are present, which leads to unnecessary data retention until a
table is added to the subscription.Since dead tuples don't need to be retained when no tables are subscribed, here
is a patch to fix it, modifying AllTablesyncsReady() to allows no tables to be
treated as a ready state when explicitly requested.
Few comments:
============
Aren't following two paragraphs in comments contradict each other:
* It is safe to add new tables with initial states to the subscription
* after this check because any changes applied to these tables should
* have a WAL position greater than the rdt_data->remote_lsn.
+ *
+ * Advancing the transaction ID is also necessary when no tables are
+ * subscribed, as it prevents unnecessary retention of dead tuples. Although
+ * it seem feasible to skip all phases and directly assign candidate_xid to
+ * oldest_nonremovable_xid in the RDT_GET_CANDIDATE_XID phase when no tables
+ * are currently subscribed, this approach is unsafe. This is because new
+ * tables may be added to the subscription after the initial table check,
+ * requiring tuples deleted before candidate_xid for conflict detection in
+ * upcoming transactions. Therefore, it remains necessary to wait for all
+ * concurrent transactions to be fully applied.
*/
In the first para, the comments say that it is okay to add tables
after this check and in the second para, it says that is not okay?
2.
+ * If the subscription has no tables, return the value determined by
+ * 'ready_if_no_tables'.
+ *
+ * Otherwise, return whether all the tables for the subscription are in the
+ * READY state.
*
* Note: This function is not suitable to be called from outside of apply or
* tablesync workers because MySubscription needs to be already initialized.
*/
bool
-AllTablesyncsReady(void)
+AllTablesyncsReady(bool ready_if_no_tables)
This change serves the purpose but I find it makes the API complex to
understand because now it needs to make decisions based on different
states depending on the boolean parameter passed. Can we introduce a
new API for the empty subscription case?
--
With Regards,
Amit Kapila.
On Friday, August 29, 2025 12:05 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Thu, Aug 28, 2025 at 7:54 AM Zhijie Hou (Fujitsu)
<houzj.fnst@fujitsu.com> wrote:Hi,
My colleague Nisha reported an issue to me off-list: dead tuples can't
be removed when retain_dead_tuples is enabled for a subscription with notables.
This appears to stem from the inability to advance the non-removable
transaction ID when AllTablesyncsReady() returns false. Since this
function returns false when no tables are present, which leads to
unnecessary data retention until a table is added to the subscription.Since dead tuples don't need to be retained when no tables are
subscribed, here is a patch to fix it, modifying AllTablesyncsReady()
to allows no tables to be treated as a ready state when explicitly requested.Few comments:
============
Aren't following two paragraphs in comments contradict each other:* It is safe to add new tables with initial states to the subscription * after this check because any changes applied to these tables should * have a WAL position greater than the rdt_data->remote_lsn. + * + * Advancing the transaction ID is also necessary when no tables are + * subscribed, as it prevents unnecessary retention of dead tuples. +Although + * it seem feasible to skip all phases and directly assign +candidate_xid to + * oldest_nonremovable_xid in the RDT_GET_CANDIDATE_XID phase when no +tables + * are currently subscribed, this approach is unsafe. This is because +new + * tables may be added to the subscription after the initial table +check, + * requiring tuples deleted before candidate_xid for conflict +detection in + * upcoming transactions. Therefore, it remains necessary to wait for +all + * concurrent transactions to be fully applied. */In the first para, the comments say that it is okay to add tables after this check
and in the second para, it says that is not okay?
I have removed the 1st para because it's inaccurate.
2. + * If the subscription has no tables, return the value determined by + * 'ready_if_no_tables'. + * + * Otherwise, return whether all the tables for the subscription are in + the + * READY state. * * Note: This function is not suitable to be called from outside of apply or * tablesync workers because MySubscription needs to be already initialized. */ bool -AllTablesyncsReady(void) +AllTablesyncsReady(bool ready_if_no_tables)This change serves the purpose but I find it makes the API complex to
understand because now it needs to make decisions based on different states
depending on the boolean parameter passed. Can we introduce a new API for
the empty subscription case?
Added a new function HasSubscriptionRelationsCached() as suggested.
Attached is the V2 patch which addresses the above comments and
fixes a typo.
Apart from the original reported issue, we noticed another point that can be
improved during the implementation of update_deleted patches: When a disabled
subscription is created with retain_dead_tuples set to true, the launcher is not
woken up immediately, which may lead to delays in creating the conflict
detection slot and cause user confusion. So, I prepared the 0002 patch to fix
this issue.
Best Regards,
Hou zj
Attachments:
v2-0001-Avoid-retaining-conflict-related-data-when-no-tab.patchapplication/octet-stream; name=v2-0001-Avoid-retaining-conflict-related-data-when-no-tab.patchDownload
From d553885c36a2b80c5eb9b0704ba18bd76ee079cb Mon Sep 17 00:00:00 2001
From: Zhijie Hou <houzj.fnst@fujitsu.com>
Date: Wed, 27 Aug 2025 18:11:38 +0800
Subject: [PATCH v2 1/2] Avoid retaining conflict-related data when no tables
are subscribed
This commit fixes an issue where conflict-related data was unnecessarily
retained when the subscription does not have a table.
---
src/backend/replication/logical/tablesync.c | 26 +++++++++++++++++++++
src/backend/replication/logical/worker.c | 14 +++++++----
src/include/replication/worker_internal.h | 1 +
3 files changed, 37 insertions(+), 4 deletions(-)
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index d3356bc84ee..e6da4028d39 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1788,6 +1788,32 @@ AllTablesyncsReady(void)
return has_subrels && (table_states_not_ready == NIL);
}
+/*
+ * Return whether the subscription currently has any relations.
+ *
+ * Note: Unlike HasSubscriptionRelations(), this function relies on cached
+ * information for subscription relations. Additionally, it should not be
+ * invoked outside of apply or tablesync workers, as MySubscription must be
+ * initialized first.
+ */
+bool
+HasSubscriptionRelationsCached(void)
+{
+ bool started_tx;
+ bool has_subrels;
+
+ /* We need up-to-date subscription tables info here */
+ has_subrels = FetchTableStates(&started_tx);
+
+ if (started_tx)
+ {
+ CommitTransactionCommand();
+ pgstat_report_stat(true);
+ }
+
+ return has_subrels;
+}
+
/*
* Update the two_phase state of the specified subscription in pg_subscription.
*/
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index f1ebd63e792..38f27e1cb50 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4595,11 +4595,17 @@ wait_for_local_flush(RetainDeadTuplesData *rdt_data)
* workers is complex and not worth the effort, so we simply return if not
* all tables are in the READY state.
*
- * It is safe to add new tables with initial states to the subscription
- * after this check because any changes applied to these tables should
- * have a WAL position greater than the rdt_data->remote_lsn.
+ * Advancing the transaction ID is also necessary when no tables are
+ * subscribed, as it prevents unnecessary retention of dead tuples. Although
+ * it seems feasible to skip all phases and directly assign candidate_xid to
+ * oldest_nonremovable_xid in the RDT_GET_CANDIDATE_XID phase when no tables
+ * are currently subscribed, this approach is unsafe. This is because new
+ * tables may be added to the subscription after the initial table check,
+ * requiring tuples deleted before candidate_xid for conflict detection in
+ * upcoming transactions. Therefore, it remains necessary to wait for all
+ * concurrent transactions to be fully applied.
*/
- if (!AllTablesyncsReady())
+ if (HasSubscriptionRelationsCached() && !AllTablesyncsReady())
{
TimestampTz now;
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 62ea1a00580..de003802612 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -272,6 +272,7 @@ extern void ReplicationOriginNameForLogicalRep(Oid suboid, Oid relid,
char *originname, Size szoriginname);
extern bool AllTablesyncsReady(void);
+extern bool HasSubscriptionRelationsCached(void);
extern void UpdateTwoPhaseState(Oid suboid, char new_state);
extern void process_syncing_tables(XLogRecPtr current_lsn);
--
2.51.0.windows.1
v2-0002-Wakeup-launcher-on-subscription-creation-to-creat.patchapplication/octet-stream; name=v2-0002-Wakeup-launcher-on-subscription-creation-to-creat.patchDownload
From 2d73fc12ff3c81ce2cba8bdcbc404ea8b96535e6 Mon Sep 17 00:00:00 2001
From: Zhijie Hou <houzj.fnst@fujitsu.com>
Date: Tue, 2 Sep 2025 13:01:36 +0800
Subject: [PATCH v2 2/2] Wakeup launcher on subscription creation to create the
conflict detection slot
When a disabled subscription is created with retain_dead_tuples set to true, the
launcher is not woken up immediately, which may lead to delays in creating the
conflict detection slot and cause user confusion. This commit ensures that the
subscription creation command triggers an immediate launcher wakeup.
---
src/backend/commands/subscriptioncmds.c | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 82cf65fae73..faec92b8078 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -854,7 +854,12 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
pgstat_create_subscription(subid);
- if (opts.enabled)
+ /*
+ * Notify the launcher to start the apply worker if the subscription is
+ * enabled or to create the conflict detection slot if retain_dead_tuples is
+ * enabled.
+ */
+ if (opts.enabled || opts.retaindeadtuples)
ApplyLauncherWakeupAtCommit();
ObjectAddressSet(myself, SubscriptionRelationId, subid);
--
2.51.0.windows.1
On Tue, Sep 2, 2025 at 10:51 AM Zhijie Hou (Fujitsu)
<houzj.fnst@fujitsu.com> wrote:
On Friday, August 29, 2025 12:05 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
bool -AllTablesyncsReady(void) +AllTablesyncsReady(bool ready_if_no_tables)This change serves the purpose but I find it makes the API complex to
understand because now it needs to make decisions based on different states
depending on the boolean parameter passed. Can we introduce a new API for
the empty subscription case?Added a new function HasSubscriptionRelationsCached() as suggested.
Thanks, the new API looks better. I have a question related to one of
the comments:
*
Although
+ * it seems feasible to skip all phases and directly assign candidate_xid to
+ * oldest_nonremovable_xid in the RDT_GET_CANDIDATE_XID phase when no tables
+ * are currently subscribed, this approach is unsafe. This is because new
+ * tables may be added to the subscription after the initial table check,
+ * requiring tuples deleted before candidate_xid for conflict detection in
+ * upcoming transactions.
--
How is it possible that new tables added will have any data (deleted
data in this case) from a transaction prior to the candidate_xid
transaction?
* Can we add a test to show that dead tuples are not retained even
with retain_dead_tuples=true when subscription has no relations?
--
With Regards,
Amit Kapila.
On Thursday, September 4, 2025 7:38 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Sep 2, 2025 at 10:51 AM Zhijie Hou (Fujitsu) <houzj.fnst@fujitsu.com>
wrote:On Friday, August 29, 2025 12:05 PM Amit Kapila
<amit.kapila16@gmail.com> wrote:
bool -AllTablesyncsReady(void) +AllTablesyncsReady(bool ready_if_no_tables)This change serves the purpose but I find it makes the API complex
to understand because now it needs to make decisions based on
different states depending on the boolean parameter passed. Can we
introduce a new API for the empty subscription case?Added a new function HasSubscriptionRelationsCached() as suggested.
Thanks, the new API looks better. I have a question related to one of the
comments:
Thanks for the comments.
* Although + * it seems feasible to skip all phases and directly assign +candidate_xid to + * oldest_nonremovable_xid in the RDT_GET_CANDIDATE_XID phase when no +tables + * are currently subscribed, this approach is unsafe. This is because +new + * tables may be added to the subscription after the initial table +check, + * requiring tuples deleted before candidate_xid for conflict +detection in + * upcoming transactions.--
How is it possible that new tables added will have any data (deleted data in
this case) from a transaction prior to the candidate_xid transaction?
I initially considered the scenario where a table is created on the subscriber,
and a user deletes a row before adding the table to the subscription. Upon
reconsideration, it appears that retention is not guaranteed prior to table
addition.
However, after analyzing it further, there are some other reasons for checking
the subscription table in the final phase. The comments have been updated
accordingly to reflect the reasons. Please refer to the comments in the new
patch version for details.
* Can we add a test to show that dead tuples are not retained even with
retain_dead_tuples=true when subscription has no relations?
Yes, I have added a test for this.
Here is the V3 patch. I merged two patches because they
are both related to conflict data retention.
Best Regards,
Hou zj
Attachments:
v3-0001-Post-commit-review-fixes-for-228c370.patchapplication/octet-stream; name=v3-0001-Post-commit-review-fixes-for-228c370.patchDownload
From bf7594e1aea108560aaa8f0858246cf126890bbb Mon Sep 17 00:00:00 2001
From: Zhijie Hou <houzj.fnst@fujitsu.com>
Date: Wed, 27 Aug 2025 18:11:38 +0800
Subject: [PATCH v3] Post-commit review fixes for 228c370
This commit fixes two issues:
1) When a disabled subscription is created with retain_dead_tuples set to true,
the launcher is not woken up immediately, which may lead to delays in creating
the conflict detection slot.
Creating the conflict detection slot is essential even when the subscription is
not enabled. This ensures that dead tuples are retained, which is necessary for
accurately identifying the type of conflict during replication.
2) Conflict-related data was unnecessarily retained when the subscription does
not have a table.
---
src/backend/commands/subscriptioncmds.c | 12 ++++++++-
src/backend/replication/logical/tablesync.c | 26 ++++++++++++++++++
src/backend/replication/logical/worker.c | 25 +++++++++++++++---
src/include/replication/worker_internal.h | 1 +
src/test/subscription/t/035_conflicts.pl | 29 +++++++++++++++++++++
5 files changed, 88 insertions(+), 5 deletions(-)
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 82cf65fae73..750d262fcca 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -854,7 +854,17 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
pgstat_create_subscription(subid);
- if (opts.enabled)
+ /*
+ * Notify the launcher to start the apply worker if the subscription is
+ * enabled, or to create the conflict detection slot if retain_dead_tuples
+ * is enabled.
+ *
+ * Creating the conflict detection slot is essential even when the
+ * subscription is not enabled. This ensures that dead tuples are
+ * retained, which is necessary for accurately identifying the type of
+ * conflict during replication.
+ */
+ if (opts.enabled || opts.retaindeadtuples)
ApplyLauncherWakeupAtCommit();
ObjectAddressSet(myself, SubscriptionRelationId, subid);
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index d3356bc84ee..e6da4028d39 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1788,6 +1788,32 @@ AllTablesyncsReady(void)
return has_subrels && (table_states_not_ready == NIL);
}
+/*
+ * Return whether the subscription currently has any relations.
+ *
+ * Note: Unlike HasSubscriptionRelations(), this function relies on cached
+ * information for subscription relations. Additionally, it should not be
+ * invoked outside of apply or tablesync workers, as MySubscription must be
+ * initialized first.
+ */
+bool
+HasSubscriptionRelationsCached(void)
+{
+ bool started_tx;
+ bool has_subrels;
+
+ /* We need up-to-date subscription tables info here */
+ has_subrels = FetchTableStates(&started_tx);
+
+ if (started_tx)
+ {
+ CommitTransactionCommand();
+ pgstat_report_stat(true);
+ }
+
+ return has_subrels;
+}
+
/*
* Update the two_phase state of the specified subscription in pg_subscription.
*/
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index f1ebd63e792..c0f6bef5c28 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -4595,11 +4595,28 @@ wait_for_local_flush(RetainDeadTuplesData *rdt_data)
* workers is complex and not worth the effort, so we simply return if not
* all tables are in the READY state.
*
- * It is safe to add new tables with initial states to the subscription
- * after this check because any changes applied to these tables should
- * have a WAL position greater than the rdt_data->remote_lsn.
+ * Advancing the transaction ID is necessary even when no tables are
+ * currently subscribed, to avoid retaining dead tuples unnecessarily.
+ * While it might seem safe to skip all phases and directly assign
+ * candidate_xid to oldest_nonremovable_xid during the
+ * RDT_GET_CANDIDATE_XID phase in such cases, this is unsafe. If users
+ * concurrently add tables to the subscription, the apply worker may not
+ * process invalidations in time. Consequently,
+ * HasSubscriptionRelationsCached() might miss the new tables, leading to
+ * premature advancement of oldest_nonremovable_xid.
+ *
+ * Performing the check during RDT_WAIT_FOR_LOCAL_FLUSH is safe, as
+ * invalidations are guaranteed to be processed before applying changes
+ * from newly added tables while waiting for the local flush to reach
+ * remote_lsn.
+ *
+ * Additionally, even if we check for subscription tables during
+ * RDT_GET_CANDIDATE_XID, they might be dropped before reaching
+ * RDT_WAIT_FOR_LOCAL_FLUSH. Therefore, it's still necessary to verify
+ * subscription tables at this stage to prevent unnecessary tuple
+ * retention.
*/
- if (!AllTablesyncsReady())
+ if (HasSubscriptionRelationsCached() && !AllTablesyncsReady())
{
TimestampTz now;
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 62ea1a00580..de003802612 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -272,6 +272,7 @@ extern void ReplicationOriginNameForLogicalRep(Oid suboid, Oid relid,
char *originname, Size szoriginname);
extern bool AllTablesyncsReady(void);
+extern bool HasSubscriptionRelationsCached(void);
extern void UpdateTwoPhaseState(Oid suboid, char new_state);
extern void process_syncing_tables(XLogRecPtr current_lsn);
diff --git a/src/test/subscription/t/035_conflicts.pl b/src/test/subscription/t/035_conflicts.pl
index 51b23a39fa9..e06429c288f 100644
--- a/src/test/subscription/t/035_conflicts.pl
+++ b/src/test/subscription/t/035_conflicts.pl
@@ -386,6 +386,35 @@ ok( $logfile =~
.*Remote row \(2, 4\); replica identity full \(2, 2\)/,
'update target row was deleted in tab');
+###############################################################################
+# Check that the xmin value of the conflict detection slot can be advanced when
+# the subscription has no tables.
+###############################################################################
+
+# Remove the table from the publication
+$node_B->safe_psql('postgres', "ALTER PUBLICATION tap_pub_B DROP TABLE tab");
+
+$node_A->safe_psql('postgres',
+ "ALTER SUBSCRIPTION $subname_AB REFRESH PUBLICATION");
+
+# Remember the next transaction ID to be assigned
+$next_xid = $node_A->safe_psql('postgres', "SELECT txid_current() + 1;");
+
+# Confirm that the xmin value is advanced to the latest nextXid. If no
+# transactions are running, the apply worker selects nextXid as the candidate
+# for the non-removable xid. See GetOldestActiveTransactionId().
+ok( $node_A->poll_query_until(
+ 'postgres',
+ "SELECT xmin = $next_xid from pg_replication_slots WHERE slot_name = 'pg_conflict_detection'"
+ ),
+ "the xmin value of slot 'pg_conflict_detection' is updated on Node A");
+
+# Re-add the table to the publication for further tests
+$node_B->safe_psql('postgres', "ALTER PUBLICATION tap_pub_B ADD TABLE tab");
+
+$node_A->safe_psql('postgres',
+ "ALTER SUBSCRIPTION $subname_AB REFRESH PUBLICATION WITH (copy_data = false)");
+
###############################################################################
# Check that dead tuple retention stops due to the wait time surpassing
# max_retention_duration.
--
2.31.1