v61-0002-Resume-retaining-the-information-for-conflict-de.patch

application/octet-stream

Filename: v61-0002-Resume-retaining-the-information-for-conflict-de.patch
Type: application/octet-stream
Part: 0
Message: RE: Conflict detection for update_deleted in logical replication

Patch

Format: format-patch
Series: patch v61-0002
Subject: Resume retaining the information for conflict detection
File+
doc/src/sgml/ref/create_subscription.sgml 5 2
src/backend/replication/logical/launcher.c 66 17
src/backend/replication/logical/worker.c 134 17
src/include/replication/worker_internal.h 7 0
From ce63de11056cd515e5f435ac408bf5a355ed0366 Mon Sep 17 00:00:00 2001
From: Zhijie Hou <houzj.fnst@fujitsu.com>
Date: Wed, 13 Aug 2025 11:33:24 +0800
Subject: [PATCH v61 2/2] Resume retaining the information for conflict
 detection

The patch allows the launcher to re-initialized invalidated slot, if at
least one apply worker has confirmed that the retention duration is now within
the max_conflict_retention_duration.
---
 doc/src/sgml/ref/create_subscription.sgml  |   7 +-
 src/backend/replication/logical/launcher.c |  83 ++++++++---
 src/backend/replication/logical/worker.c   | 151 ++++++++++++++++++---
 src/include/replication/worker_internal.h  |   7 +
 4 files changed, 212 insertions(+), 36 deletions(-)

diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index 4190c190ae8..73cc82d52a0 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -539,9 +539,12 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
           <literal>retain_dead_tuples</literal> is enabled, confirm that the
           retention duration exceeded the
           <literal>max_conflict_retention_duration</literal> set within the
-          corresponding subscription. To re-enable retention, you can disable
-          <literal>retain_dead_tuples</literal> for all subscriptions and
+          corresponding subscription. To re-enable retention manually, you can
+          disable <literal>retain_dead_tuples</literal> for all subscriptions and
           re-enable it after confirming this replication slot has been dropped.
+          Alternatively, the retention will be automatically resumed once at
+          least one apply worker confirms that the retention duration is within
+          the specified limit.
          </para>
          <para>
           Note that overall retention will not stop if other subscriptions
diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c
index f1dfb51ccf8..b85acd95e6b 100644
--- a/src/backend/replication/logical/launcher.c
+++ b/src/backend/replication/logical/launcher.c
@@ -102,9 +102,12 @@ static int	logicalrep_pa_worker_count(Oid subid);
 static void logicalrep_launcher_attach_dshmem(void);
 static void ApplyLauncherSetWorkerStartTime(Oid subid, TimestampTz start_time);
 static TimestampTz ApplyLauncherGetWorkerStartTime(Oid subid);
-static void compute_min_nonremovable_xid(LogicalRepWorker *worker, TransactionId *xmin);
+static void compute_min_nonremovable_xid(LogicalRepWorker *worker,
+										 bool can_advance_xmin,
+										 TransactionId *xmin);
 static bool acquire_conflict_slot_if_exists(void);
 static void update_conflict_slot_xmin(TransactionId new_xmin);
+static void init_conflict_slot_xmin(void);
 
 
 /*
@@ -464,6 +467,8 @@ retry:
 	worker->stream_fileset = NULL;
 	worker->leader_pid = is_parallel_apply_worker ? MyProcPid : InvalidPid;
 	worker->parallel_apply = is_parallel_apply_worker;
+	worker->stop_conflict_info_retention = (retain_dead_tuples &&
+											!TransactionIdIsValid(MyReplicationSlot->data.xmin));
 	worker->oldest_nonremovable_xid = retain_dead_tuples
 		? MyReplicationSlot->data.xmin
 		: InvalidTransactionId;
@@ -1258,8 +1263,8 @@ ApplyLauncherMain(Datum main_arg)
 				 * required for conflict detection among all running apply
 				 * workers that enables retain_dead_tuples.
 				 */
-				if (sub->retaindeadtuples && can_advance_xmin)
-					compute_min_nonremovable_xid(w, &xmin);
+				if (sub->retaindeadtuples)
+					compute_min_nonremovable_xid(w, can_advance_xmin, &xmin);
 
 				/* worker is running already */
 				continue;
@@ -1369,9 +1374,11 @@ ApplyLauncherMain(Datum main_arg)
  * in *xmin.
  */
 static void
-compute_min_nonremovable_xid(LogicalRepWorker *worker, TransactionId *xmin)
+compute_min_nonremovable_xid(LogicalRepWorker *worker, bool can_advance_xmin,
+							 TransactionId *xmin)
 {
 	TransactionId nonremovable_xid;
+	bool		stop_retention;
 
 	Assert(worker != NULL);
 
@@ -1383,13 +1390,43 @@ compute_min_nonremovable_xid(LogicalRepWorker *worker, TransactionId *xmin)
 
 	SpinLockAcquire(&worker->mutex);
 	nonremovable_xid = worker->oldest_nonremovable_xid;
+	stop_retention = worker->stop_conflict_info_retention;
 	SpinLockRelease(&worker->mutex);
 
 	/*
 	 * Skip collecting oldest_nonremovable_xid for workers that have stopped
 	 * conflict retention.
 	 */
+	if (stop_retention)
+		return;
+
+	/*
+	 * Initialize slot.xmin as a apply worker resumes retention of information
+	 * critical for conflict detection.
+	 */
+	if (!TransactionIdIsValid(MyReplicationSlot->data.xmin))
+		init_conflict_slot_xmin();
+
+	/*
+	 * Assign slot.xmin to the apply worker's oldest_nonremovable_xid if the
+	 * latter is invalid. This ensures the apply worker continues to maintain
+	 * the oldest_nonremovable_xid (see get_candidate_xid).
+	 */
 	if (!TransactionIdIsValid(nonremovable_xid))
+	{
+		nonremovable_xid = MyReplicationSlot->data.xmin;
+
+		SpinLockAcquire(&worker->mutex);
+		worker->oldest_nonremovable_xid = nonremovable_xid;
+		SpinLockRelease(&worker->mutex);
+
+		/* Notify the apply worker to start the next cycle of management */
+		LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
+		logicalrep_worker_wakeup_ptr(worker);
+		LWLockRelease(LogicalRepWorkerLock);
+	}
+
+	if (!can_advance_xmin)
 		return;
 
 	if (!TransactionIdIsValid(*xmin) ||
@@ -1451,23 +1488,15 @@ update_conflict_slot_xmin(TransactionId new_xmin)
 }
 
 /*
- * Create and acquire the replication slot used to retain information for
- * conflict detection, if not yet.
+ * Initialize the xmin for the conflict detection slot.
  */
-void
-CreateConflictDetectionSlot(void)
+static void
+init_conflict_slot_xmin(void)
 {
 	TransactionId xmin_horizon;
 
-	/* Exit early, if the replication slot is already created and acquired */
-	if (MyReplicationSlot)
-		return;
-
-	ereport(LOG,
-			errmsg("creating replication conflict detection slot"));
-
-	ReplicationSlotCreate(CONFLICT_DETECTION_SLOT, false, RS_PERSISTENT, false,
-						  false, false);
+	Assert(MyReplicationSlot &&
+		   !TransactionIdIsValid(MyReplicationSlot->data.xmin));
 
 	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
 
@@ -1487,6 +1516,26 @@ CreateConflictDetectionSlot(void)
 	ReplicationSlotSave();
 }
 
+/*
+ * Create and acquire the replication slot used to retain information for
+ * conflict detection, if not yet.
+ */
+void
+CreateConflictDetectionSlot(void)
+{
+	/* Exit early, if the replication slot is already created and acquired */
+	if (MyReplicationSlot)
+		return;
+
+	ereport(LOG,
+			errmsg("creating replication conflict detection slot"));
+
+	ReplicationSlotCreate(CONFLICT_DETECTION_SLOT, false, RS_PERSISTENT, false,
+						  false, false);
+
+	init_conflict_slot_xmin();
+}
+
 /*
  * Is current process the logical replication launcher?
  */
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index afa3f96ea3c..8f1bf276384 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -418,6 +418,10 @@ typedef struct RetainDeadTuplesData
 	long		table_sync_wait_time;	/* time spent waiting for table sync
 										 * to finish */
 
+	bool		wait_for_initial_xid;	/* wait for the launcher to initialize
+										 * the apply worker's
+										 * oldest_nonremovable_xid */
+
 	/*
 	 * The following fields are used to determine the timing for the next
 	 * round of transaction ID advancement.
@@ -560,6 +564,7 @@ static void wait_for_publisher_status(RetainDeadTuplesData *rdt_data,
 static void wait_for_local_flush(RetainDeadTuplesData *rdt_data);
 static void reset_retention_data_fields(RetainDeadTuplesData *rdt_data);
 static bool should_stop_conflict_info_retention(RetainDeadTuplesData *rdt_data);
+static void resume_conflict_info_retention(RetainDeadTuplesData *rdt_data);
 static void adjust_xid_advance_interval(RetainDeadTuplesData *rdt_data,
 										bool new_xid_found);
 
@@ -4376,10 +4381,6 @@ can_advance_nonremovable_xid(RetainDeadTuplesData *rdt_data)
 	if (!MySubscription->retaindeadtuples)
 		return false;
 
-	/* No need to advance if we have already stopped retaining */
-	if (!TransactionIdIsValid(MyLogicalRepWorker->oldest_nonremovable_xid))
-		return false;
-
 	return true;
 }
 
@@ -4417,6 +4418,41 @@ get_candidate_xid(RetainDeadTuplesData *rdt_data)
 	TransactionId oldest_running_xid;
 	TimestampTz now;
 
+	/*
+	 * If retention has been stopped but max_conflict_retention_duration is now
+	 * set to 0, resume retention immediately.
+	 */
+	if (MyLogicalRepWorker->stop_conflict_info_retention &&
+		!MySubscription->maxconflretention)
+		resume_conflict_info_retention(rdt_data);
+
+	/*
+	 * No need to advance if the apply worker has resumed retention but the
+	 * launcher has not yet initialized slot.xmin and assigned it to
+	 * oldest_nonremovable_xid.
+	 *
+	 * It might seem feasible to directly check the conflict detection
+	 * slot.xmin instead of relying on the launcher to assign the worker's
+	 * oldest_nonremovable_xid; however, that could lead to a race condition
+	 * where slot.xmin is set to InvalidTransactionId immediately after the
+	 * check. In such cases, oldest_nonremovable_xid would no longer be
+	 * protected by a replication slot and could become unreliable if a
+	 * wraparound occurs.
+	 */
+	if (rdt_data->wait_for_initial_xid)
+	{
+		TransactionId nonremovable_xid;
+
+		SpinLockAcquire(&MyLogicalRepWorker->mutex);
+		nonremovable_xid = MyLogicalRepWorker->oldest_nonremovable_xid;
+		SpinLockRelease(&MyLogicalRepWorker->mutex);
+
+		if (!TransactionIdIsValid(nonremovable_xid))
+			return;
+
+		rdt_data->wait_for_initial_xid = false;
+	}
+
 	/*
 	 * Use last_recv_time when applying changes in the loop to avoid
 	 * unnecessary system time retrieval. If last_recv_time is not available,
@@ -4523,6 +4559,23 @@ wait_for_publisher_status(RetainDeadTuplesData *rdt_data,
 	if (!status_received)
 		return;
 
+	/*
+	 * If retention has been stopped but max_conflict_retention_duration is now
+	 * set to 0, resume retention immediately without proceeding to next phases.
+	 *
+	 * Do not check this condition before receiving the publisher message, as
+	 * doing so may result in the message being processed after the data has
+	 * been reset, potentially causing it to be mistakenly identified as a new
+	 * message. This could lead to the premature advancement of
+	 * oldest_nonremovable_xid.
+	 */
+	if (MyLogicalRepWorker->stop_conflict_info_retention &&
+		!MySubscription->maxconflretention)
+	{
+		resume_conflict_info_retention(rdt_data);
+		return;
+	}
+
 	/*
 	 * Stop retaining conflict information if required (See
 	 * should_stop_conflict_info_retention() for details).
@@ -4575,6 +4628,18 @@ wait_for_local_flush(RetainDeadTuplesData *rdt_data)
 	Assert(!XLogRecPtrIsInvalid(rdt_data->remote_lsn) &&
 		   TransactionIdIsValid(rdt_data->candidate_xid));
 
+	/*
+	 * If retention has been stopped but max_conflict_retention_duration is now
+	 * set to 0, resume retention immediately without waiting for the local
+	 * flush.
+	 */
+	if (MyLogicalRepWorker->stop_conflict_info_retention &&
+		!MySubscription->maxconflretention)
+	{
+		resume_conflict_info_retention(rdt_data);
+		return;
+	}
+
 	/*
 	 * We expect the publisher and subscriber clocks to be in sync using time
 	 * sync service like NTP. Otherwise, we will advance this worker's
@@ -4661,6 +4726,17 @@ wait_for_local_flush(RetainDeadTuplesData *rdt_data)
 	if (last_flushpos < rdt_data->remote_lsn)
 		return;
 
+	/*
+	 * If conflict info retention was previously stopped due to a timeout, and
+	 * the time required to advance the non-removable transaction ID has now
+	 * decreased to within acceptable limits, resume the rentention.
+	 */
+	if (MyLogicalRepWorker->stop_conflict_info_retention)
+	{
+		resume_conflict_info_retention(rdt_data);
+		return;
+	}
+
 	/*
 	 * Reaching here means the remote WAL position has been received, and all
 	 * transactions up to that position on the publisher have been applied and
@@ -4710,9 +4786,8 @@ reset_retention_data_fields(RetainDeadTuplesData *rdt_data)
  * to InvalidTransactionId, notify the launcher to set the slot.xmin to
  * InvalidTransactionId as well, and return true. Return false otherwise.
  *
- * Currently, the retention will not resume automatically unless user manually
- * disables retain_dead_tuples and re-enables it after confirming that the
- * replication slot has been dropped.
+ * The retention will resume automatically if the worker has confirmed that the
+ * retention duration is now within the max_conflict_retention_duration.
  */
 static bool
 should_stop_conflict_info_retention(RetainDeadTuplesData *rdt_data)
@@ -4743,24 +4818,65 @@ should_stop_conflict_info_retention(RetainDeadTuplesData *rdt_data)
 									rdt_data->table_sync_wait_time))
 		return false;
 
+	/*
+	 * Log a message and reset relevant data when the worker is about to stop
+	 * retaining conflict information.
+	 */
+	if (!MyLogicalRepWorker->stop_conflict_info_retention)
+	{
+		ereport(LOG,
+				errmsg("logical replication worker for subscription \"%s\" will stop retaining conflict information",
+					   MySubscription->name),
+				errdetail("The time spent advancing the non-removable transaction ID has exceeded the maximum limit of %u ms.",
+						  MySubscription->maxconflretention),
+				errhint("You might need to increase \"%s\".",
+						"max_conflict_retention_duration"));
+
+		SpinLockAcquire(&MyLogicalRepWorker->mutex);
+		MyLogicalRepWorker->oldest_nonremovable_xid = InvalidTransactionId;
+		MyLogicalRepWorker->stop_conflict_info_retention = true;
+		SpinLockRelease(&MyLogicalRepWorker->mutex);
+
+		/* Notify launcher to update the conflict slot */
+		ApplyLauncherWakeup();
+	}
+
+	reset_retention_data_fields(rdt_data);
+
+	return true;
+}
+
+/*
+ * Resume the retention if conflict info retention was previously stopped due to
+ * a timeout, and the time required to advance the non-removable transaction ID
+ * has now decreased to within acceptable limits.
+ */
+static void
+resume_conflict_info_retention(RetainDeadTuplesData *rdt_data)
+{
+	Assert(MyLogicalRepWorker->stop_conflict_info_retention);
+
 	ereport(LOG,
-			errmsg("logical replication worker for subscription \"%s\" will stop retaining conflict information",
+			errmsg("logical replication worker for subscription \"%s\" resumes retaining the information for detecting conflicts",
 				   MySubscription->name),
-			errdetail("The time spent advancing the non-removable transaction ID has exceeded the maximum limit of %u ms.",
-					  MySubscription->maxconflretention),
-			errhint("You might need to increase \"%s\".",
-					"max_conflict_retention_duration"));
+			errdetail("The time spent applying changes up to LSN %X/%X is now within the maximum limit of %u ms.",
+					  LSN_FORMAT_ARGS(rdt_data->remote_lsn),
+					  MySubscription->maxconflretention));
+
+	/*
+	 * The next step is to wait for the launcher to initialize the
+	 * oldest_nonremovable_xid.
+	 */
+	rdt_data->wait_for_initial_xid = true;
 
 	SpinLockAcquire(&MyLogicalRepWorker->mutex);
-	MyLogicalRepWorker->oldest_nonremovable_xid = InvalidTransactionId;
+	MyLogicalRepWorker->stop_conflict_info_retention = false;
 	SpinLockRelease(&MyLogicalRepWorker->mutex);
 
-	/* Notify launcher to update the conflict slot */
-	ApplyLauncherWakeup();
-
 	reset_retention_data_fields(rdt_data);
 
-	return true;
+	/* Notify launcher to update the xmin of the conflict slot */
+	ApplyLauncherWakeup();
 }
 
 /*
@@ -5611,6 +5727,7 @@ InitializeLogRepWorker(void)
 	 */
 	if (am_leader_apply_worker() &&
 		MySubscription->retaindeadtuples &&
+		!MyLogicalRepWorker->stop_conflict_info_retention &&
 		!TransactionIdIsValid(MyLogicalRepWorker->oldest_nonremovable_xid))
 	{
 		ereport(LOG,
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 9c0c2b8050c..32a1cbb6528 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -105,6 +105,13 @@ typedef struct LogicalRepWorker
 	 */
 	TransactionId oldest_nonremovable_xid;
 
+	/*
+	 * Indicates whether the apply worker has stopped retaining information
+	 * useful for conflict detection. This is used only when
+	 * retain_dead_tuples is enabled.
+	 */
+	bool		stop_conflict_info_retention;
+
 	/* Stats. */
 	XLogRecPtr	last_lsn;
 	TimestampTz last_send_time;
-- 
2.50.1.windows.1