From 774625317ff57a1871c4ca66c9605bb69a76d060 Mon Sep 17 00:00:00 2001
From: Hou Zhijie <houzj.fnst@cn.fujitsu.com>
Date: Thu, 26 Sep 2024 12:11:34 +0800
Subject: [PATCH v18 3/7] Maintain the replication slot in logical launcher to
 retain dead tuples

This patch enables the logical replication launcher to create and maintain a
replication slot named pg_conflict_detection.

The launcher periodically collects the oldest_nonremovable_xid from all apply
workers. It then computes the minimum transaction ID and advances the xmin
value of the replication slot if it precedes the computed value.

The interval for updating the slot (nap time) is dynamically adjusted based on
the activity of the apply workers. The launcher waits for a certain period
before performing the next update, with the duration varying depending on
whether the xmin value of the replication slot was updated during the last
cycle.
---
 src/backend/replication/logical/launcher.c | 210 +++++++++++++++++++++
 src/backend/replication/slot.c             |  13 ++
 src/backend/replication/slotfuncs.c        |   4 +
 src/backend/replication/walsender.c        |   2 +
 src/include/replication/slot.h             |   8 +
 5 files changed, 237 insertions(+)

diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c
index f8cf00c984..1a400d2228 100644
--- a/src/backend/replication/logical/launcher.c
+++ b/src/backend/replication/logical/launcher.c
@@ -46,6 +46,12 @@
 /* max sleep time between cycles (3min) */
 #define DEFAULT_NAPTIME_PER_CYCLE 180000L
 
+/*
+ * Min sleep time (200ms) between cycles to update the xmin value of the
+ * replication slot.
+ */
+#define MIN_NAPTIME_PER_SLOT_UPDATE 200
+
 /* GUC variables */
 int			max_logical_replication_workers = 4;
 int			max_sync_workers_per_subscription = 2;
@@ -100,6 +106,9 @@ 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 bool advance_conflict_slot_xmin(FullTransactionId new_xmin);
+static void compute_slot_update_naptime(bool slot_updated, long *sleep_time);
+static void drop_conflict_slot_if_exists(void);
 
 
 /*
@@ -1119,6 +1128,9 @@ ApplyLauncherWakeup(void)
 void
 ApplyLauncherMain(Datum main_arg)
 {
+	bool		slot_maybe_exist = true;
+	long		slot_update_wait_time = MIN_NAPTIME_PER_SLOT_UPDATE;
+
 	ereport(DEBUG1,
 			(errmsg_internal("logical replication launcher started")));
 
@@ -1147,6 +1159,8 @@ ApplyLauncherMain(Datum main_arg)
 		MemoryContext subctx;
 		MemoryContext oldctx;
 		long		wait_time = DEFAULT_NAPTIME_PER_CYCLE;
+		bool		can_advance_xmin = true;
+		FullTransactionId xmin = InvalidFullTransactionId;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -1167,14 +1181,43 @@ ApplyLauncherMain(Datum main_arg)
 			long		elapsed;
 
 			if (!sub->enabled)
+			{
+				can_advance_xmin = false;
 				continue;
+			}
 
 			LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 			w = logicalrep_worker_find(sub->oid, InvalidOid, false);
 			LWLockRelease(LogicalRepWorkerLock);
 
 			if (w != NULL)
+			{
+				/*
+				 * Collect non-removable transaction IDs from all apply
+				 * workers to determine the xmin for advancing the replication
+				 * slot used in conflict detection.
+				 */
+				if (can_advance_xmin)
+				{
+					FullTransactionId nonremovable_xid;
+
+					SpinLockAcquire(&w->relmutex);
+					nonremovable_xid = w->oldest_nonremovable_xid;
+					SpinLockRelease(&w->relmutex);
+
+					/*
+					 * Stop advancing xmin if an invalid non-removable
+					 * transaction ID is found, otherwise update xmin.
+					 */
+					if (!FullTransactionIdIsValid(nonremovable_xid))
+						can_advance_xmin = false;
+					else if (!FullTransactionIdIsValid(xmin) ||
+							 FullTransactionIdPrecedes(nonremovable_xid, xmin))
+						xmin = nonremovable_xid;
+				}
+
 				continue;		/* worker is running already */
+			}
 
 			/*
 			 * If the worker is eligible to start now, launch it.  Otherwise,
@@ -1207,6 +1250,35 @@ ApplyLauncherMain(Datum main_arg)
 			}
 		}
 
+		/*
+		 * Maintain the xmin value of the replication slot for conflict
+		 * detection if needed, and update the sleep time before the next
+		 * attempt.
+		 */
+		if (sublist)
+		{
+			bool		updated;
+
+			if (!can_advance_xmin)
+				xmin = InvalidFullTransactionId;
+
+			updated = advance_conflict_slot_xmin(xmin);
+
+			compute_slot_update_naptime(updated, &slot_update_wait_time);
+			wait_time = Min(wait_time, slot_update_wait_time);
+
+			slot_maybe_exist = true;
+		}
+
+		/*
+		 * Drop the slot if we're no longer retaining dead tuples.
+		 */
+		else if (slot_maybe_exist)
+		{
+			drop_conflict_slot_if_exists();
+			slot_maybe_exist = false;
+		}
+
 		/* Switch back to original memory context. */
 		MemoryContextSwitchTo(oldctx);
 		/* Clean the temporary memory. */
@@ -1234,6 +1306,144 @@ ApplyLauncherMain(Datum main_arg)
 	/* Not reachable */
 }
 
+/*
+ * Attempt to advance the xmin value of the replication slot used to retain
+ * dead tuples for conflict detection.
+ */
+static bool
+advance_conflict_slot_xmin(FullTransactionId new_xmin)
+{
+	TransactionId next_xid;
+	FullTransactionId next_full_xid;
+	FullTransactionId full_xmin;
+	uint32		xmin_epoch;
+
+	/*
+	 * Acquire the slot if it hasn't been acquired yet. If the slot is not yet
+	 * created, create it.
+	 */
+	if (!MyReplicationSlot)
+	{
+		if (SearchNamedReplicationSlot(CONFLICT_DETECTION_SLOT, true))
+		{
+			ReplicationSlotAcquire(CONFLICT_DETECTION_SLOT, true);
+		}
+		else
+		{
+			TransactionId xmin_horizon;
+
+			ReplicationSlotCreate(CONFLICT_DETECTION_SLOT, false,
+								  RS_EPHEMERAL, false, false, false);
+
+			LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
+
+			xmin_horizon = GetOldestSafeDecodingTransactionId(false);
+
+			SpinLockAcquire(&MyReplicationSlot->mutex);
+			MyReplicationSlot->effective_xmin = xmin_horizon;
+			MyReplicationSlot->data.xmin = xmin_horizon;
+			SpinLockRelease(&MyReplicationSlot->mutex);
+
+			ReplicationSlotsComputeRequiredXmin(true);
+
+			LWLockRelease(ProcArrayLock);
+
+			ReplicationSlotPersist();
+		}
+	}
+
+	if (!FullTransactionIdIsValid(new_xmin))
+		return false;
+
+	Assert(TransactionIdIsValid(MyReplicationSlot->data.xmin));
+
+	/*
+	 * Compute the epoch of the xmin value for the replication slot based on
+	 * the next full transaction ID and its epoch.
+	 */
+	next_full_xid = ReadNextFullTransactionId();
+	next_xid = XidFromFullTransactionId(next_full_xid);
+	xmin_epoch = EpochFromFullTransactionId(next_full_xid);
+
+	/*
+	 * Adjust the epoch if the next transaction ID is less than the current
+	 * xmin of the replication slot. This handles the case where transaction
+	 * ID wraparound has occurred.
+	 */
+	if (next_xid < MyReplicationSlot->data.xmin)
+		xmin_epoch--;
+
+	full_xmin = FullTransactionIdFromEpochAndXid(xmin_epoch,
+												 MyReplicationSlot->data.xmin);
+
+	if (FullTransactionIdPrecedesOrEquals(new_xmin, full_xmin))
+		return false;
+
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->data.xmin = XidFromFullTransactionId(new_xmin);
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	/* first write new xmin to disk, so we know what's up after a crash */
+
+	ReplicationSlotMarkDirty();
+	ReplicationSlotSave();
+	elog(DEBUG1, "updated xmin: %u", MyReplicationSlot->data.xmin);
+
+	/*
+	 * Now the new xmin is safely on disk, we can let the global value
+	 * advance. We do not take ProcArrayLock or similar since we only advance
+	 * xmin here and there's not much harm done by a concurrent computation
+	 * missing that.
+	 */
+	SpinLockAcquire(&MyReplicationSlot->mutex);
+	MyReplicationSlot->effective_xmin = MyReplicationSlot->data.xmin;
+	SpinLockRelease(&MyReplicationSlot->mutex);
+
+	ReplicationSlotsComputeRequiredXmin(false);
+
+	return true;
+}
+
+/*
+ * Update the sleep time before the next slot update.
+ *
+ * If there is no slot activity, the wait time between sync cycles will double
+ * (up to a maximum of 3 minutes). If there is some slot activity, the wait
+ * time between sync cycles is reset to the minimum (200ms).
+ */
+static void
+compute_slot_update_naptime(bool slot_updated, long *sleep_time)
+{
+	if (!slot_updated)
+	{
+		/*
+		 * The slot was not updated, so double the sleep time, but not beyond
+		 * the maximum allowable value.
+		 */
+		*sleep_time = Min(*sleep_time * 2, DEFAULT_NAPTIME_PER_CYCLE);
+	}
+	else
+	{
+		/*
+		 * The slot was updated since the last sleep, so reset the sleep time.
+		 */
+		*sleep_time = MIN_NAPTIME_PER_SLOT_UPDATE;
+	}
+}
+
+/*
+ * Drop the replication slot used to retain dead tuples for conflict detection,
+ * if it exists.
+ */
+static void
+drop_conflict_slot_if_exists(void)
+{
+	if (MyReplicationSlot)
+		ReplicationSlotDropAcquired();
+	else if (SearchNamedReplicationSlot(CONFLICT_DETECTION_SLOT, true))
+		ReplicationSlotDrop(CONFLICT_DETECTION_SLOT, true);
+}
+
 /*
  * Is current process the logical replication launcher?
  */
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 4a206f9527..937afa5b6a 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -288,6 +288,19 @@ ReplicationSlotValidateName(const char *name, int elevel)
 	return true;
 }
 
+/*
+ * Report an error if the replication slot name is "pg_conflict_detection".
+ */
+void
+ErrorOnReservedSlotName(const char *name)
+{
+	if (strcmp(name, CONFLICT_DETECTION_SLOT) == 0)
+		ereport(ERROR,
+				errcode(ERRCODE_RESERVED_NAME),
+				errmsg("replication slot name \"%s\" is reserved",
+					   name));
+}
+
 /*
  * Create a new replication slot and mark it as used by this backend.
  *
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 488a161b3e..d547d98124 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -38,6 +38,8 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
 {
 	Assert(!MyReplicationSlot);
 
+	ErrorOnReservedSlotName(name);
+
 	/* acquire replication slot, this will check for conflicting names */
 	ReplicationSlotCreate(name, false,
 						  temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
@@ -124,6 +126,8 @@ create_logical_replication_slot(char *name, char *plugin,
 
 	Assert(!MyReplicationSlot);
 
+	ErrorOnReservedSlotName(name);
+
 	/*
 	 * Acquire a logical decoding slot, this will check for conflicting names.
 	 * Initially create persistent slot as ephemeral - that allows us to
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 68ca0c5712..8ae2783440 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1190,6 +1190,8 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 	parseCreateReplSlotOptions(cmd, &reserve_wal, &snapshot_action, &two_phase,
 							   &failover);
 
+	ErrorOnReservedSlotName(cmd->slotname);
+
 	if (cmd->kind == REPLICATION_KIND_PHYSICAL)
 	{
 		ReplicationSlotCreate(cmd->slotname, false,
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index d2cf786fd5..82c4bf6160 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -20,6 +20,13 @@
 /* directory to store replication slot data in */
 #define PG_REPLSLOT_DIR     "pg_replslot"
 
+/*
+ * The reserved name for a replication slot used to retain dead tuples for
+ * conflict detection in logical replication. See
+ * maybe_advance_nonremovable_xid() for detail.
+ */
+#define CONFLICT_DETECTION_SLOT "pg_conflict_detection"
+
 /*
  * Behaviour of replication slots, upon release or crash.
  *
@@ -262,6 +269,7 @@ extern void ReplicationSlotMarkDirty(void);
 /* misc stuff */
 extern void ReplicationSlotInitialize(void);
 extern bool ReplicationSlotValidateName(const char *name, int elevel);
+extern void ErrorOnReservedSlotName(const char *name);
 extern void ReplicationSlotReserveWal(void);
 extern void ReplicationSlotsComputeRequiredXmin(bool already_locked);
 extern void ReplicationSlotsComputeRequiredLSN(void);
-- 
2.30.0.windows.2

