v52-3-0001-Don-t-lose-a-conflict-when-the-conflict-log-table.txt

text/plain

Filename: v52-3-0001-Don-t-lose-a-conflict-when-the-conflict-log-table.txt
Type: text/plain
Part: 0
Message: Re: Proposal: Conflict log history table for Logical Replication
From e5954eb1ef36a98b7dcc31697dd2427070892de3 Mon Sep 17 00:00:00 2001
From: Amit Kapila <akapila@postgresql.org>
Date: Mon, 22 Jun 2026 12:18:17 +0530
Subject: [PATCH v3] Don't lose a conflict when the conflict log table is
 dropped mid-logging

A concurrent ALTER SUBSCRIPTION changing conflict_log_destination can drop the
conflict log table while a conflict is being logged, making table_open() fail
and the conflict lost from both the table and the server log.  Open it with
try_table_open() instead and, on NULL, fall back to full server-log reporting.
---
 src/backend/replication/logical/conflict.c | 108 +++++++++++++++------
 src/include/replication/conflict.h         |   3 +-
 2 files changed, 82 insertions(+), 29 deletions(-)

diff --git a/src/backend/replication/logical/conflict.c b/src/backend/replication/logical/conflict.c
index c2c15f055e6..983cdf94cf0 100644
--- a/src/backend/replication/logical/conflict.c
+++ b/src/backend/replication/logical/conflict.c
@@ -319,7 +319,6 @@ ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel,
 					TupleTableSlot *remoteslot, List *conflicttuples)
 {
 	Relation		localrel = relinfo->ri_RelationDesc;
-	ConflictLogDest	dest;
 	Relation		conflictlogrel;
 	bool			log_dest_table;
 	bool 			log_dest_logfile;
@@ -327,13 +326,14 @@ ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel,
 	pgstat_report_subscription_conflict(MySubscription->oid, type);
 
 	/*
-	 * Get the conflict log destination. Also, (if there is one) return the
-	 * CLT relation already opened and ready for insertion.
+	 * Get the conflict log destinations.  Also, if a table is one of the
+	 * destinations, return the CLT relation already opened and ready for
+	 * insertion.  If the table was requested but has been dropped concurrently,
+	 * GetConflictLogDestAndTable() returns NULL and reports server-log as the
+	 * destination instead, so the conflict is still recorded.
 	 */
-	conflictlogrel = GetConflictLogDestAndTable(&dest);
-
-	log_dest_table = CONFLICTS_LOGGED_TO_TABLE(dest);
-	log_dest_logfile = CONFLICTS_LOGGED_TO_LOG(dest);
+	conflictlogrel = GetConflictLogDestAndTable(&log_dest_table,
+												&log_dest_logfile);
 
 	/*
 	 * Prepare the conflict log tuple first when the destination includes the
@@ -466,7 +466,6 @@ void
 ProcessPendingConflictLogTuple(void)
 {
 	Relation	conflictlogrel;
-	ConflictLogDest dest;
 
 	/* Nothing to do */
 	if (MyLogicalRepWorker->conflict_log_tuple == NULL)
@@ -474,23 +473,40 @@ ProcessPendingConflictLogTuple(void)
 
 	/*
 	 * Insert the deferred conflict log tuple in its own transaction.  A
-	 * failure here (e.g. the conflict log table was dropped, or an
-	 * out-of-disk-space error) is treated like any other apply error and
-	 * raises an ERROR; such failures are expected to be rare and persistent.
-	 * Callers must therefore have already reported (and cleared) any
-	 * in-progress apply error before calling this, so that this error does not
-	 * mask the original one.
+	 * failure here (e.g. an out-of-disk-space error) is treated like any other
+	 * apply error and raises an ERROR; such failures are expected to be rare
+	 * and persistent.  Callers must therefore have already reported (and
+	 * cleared) any in-progress apply error before calling this, so that this
+	 * error does not mask the original one.
 	 */
 	StartTransactionCommand();
 	PushActiveSnapshot(GetTransactionSnapshot());
 
-	/* Open conflict log table and insert the tuple */
-	conflictlogrel = GetConflictLogDestAndTable(&dest);
-	Assert(conflictlogrel);
+	/* Open the conflict log table and insert the tuple. */
+	conflictlogrel = GetConflictLogDestAndTable(NULL, NULL);
 
-	InsertConflictLogTuple(conflictlogrel);
-
-	table_close(conflictlogrel, RowExclusiveLock);
+	if (conflictlogrel != NULL)
+	{
+		InsertConflictLogTuple(conflictlogrel);
+		table_close(conflictlogrel, RowExclusiveLock);
+	}
+	else
+	{
+		/*
+		 * The conflict log table was dropped concurrently (e.g. by an ALTER
+		 * SUBSCRIPTION that changed conflict_log_destination) after the
+		 * conflict was already reported to the server log by
+		 * ReportApplyConflict().  Nothing more to do; just discard the prepared
+		 * tuple and its context string.
+		 */
+		heap_freetuple(MyLogicalRepWorker->conflict_log_tuple);
+		MyLogicalRepWorker->conflict_log_tuple = NULL;
+		if (MyLogicalRepWorker->conflict_log_errcontext)
+		{
+			pfree(MyLogicalRepWorker->conflict_log_errcontext);
+			MyLogicalRepWorker->conflict_log_errcontext = NULL;
+		}
+	}
 
 	PopActiveSnapshot();
 	CommitTransactionCommand();
@@ -531,29 +547,65 @@ InitConflictIndexes(ResultRelInfo *relInfo)
  * GetConflictLogDestAndTable
  *
  * Fetches conflict logging metadata from the cached MySubscription pointer.
- * Sets the destination enum in *log_dest and, if applicable, opens and
- * returns the relation handle for the conflict log table.
+ * Reports whether conflicts should be logged to the table and/or the server
+ * log via *log_dest_table and *log_dest_logfile (either may be NULL if the
+ * caller is not interested), and, if a table is one of the destinations, opens
+ * and returns the relation handle for the conflict log table.
+ *
+ * The table is opened with try_table_open().  If the conflict log table has
+ * been dropped concurrently (e.g. by an ALTER SUBSCRIPTION that changed
+ * conflict_log_destination), NULL is returned and the reported destinations
+ * are adjusted to fall back to the server log (*log_dest_table = false,
+ * *log_dest_logfile = true), so the caller still records the conflict instead
+ * of failing.
  */
 Relation
-GetConflictLogDestAndTable(ConflictLogDest *log_dest)
+GetConflictLogDestAndTable(bool *log_dest_table, bool *log_dest_logfile)
 {
+	ConflictLogDest dest;
 	Oid			conflictlogrelid;
+	Relation	conflictlogrel;
 
 	/*
-	 * Convert the text log destination to the internal enum.  MySubscription
-	 * already contains the data from pg_subscription.
+	 * Convert the text log destination to the internal enum and report the
+	 * individual destinations to the caller.  MySubscription already contains
+	 * the data from pg_subscription.
 	 */
-	*log_dest = GetConflictLogDest(MySubscription->conflictlogdest);
+	dest = GetConflictLogDest(MySubscription->conflictlogdest);
+
+	if (log_dest_table)
+		*log_dest_table = CONFLICTS_LOGGED_TO_TABLE(dest);
+	if (log_dest_logfile)
+		*log_dest_logfile = CONFLICTS_LOGGED_TO_LOG(dest);
 
 	/* Quick exit if a conflict log table was not requested. */
-	if (!CONFLICTS_LOGGED_TO_TABLE(*log_dest))
+	if (!CONFLICTS_LOGGED_TO_TABLE(dest))
 		return NULL;
 
 	conflictlogrelid = MySubscription->conflictlogrelid;
 
 	Assert(OidIsValid(conflictlogrelid));
 
-	return table_open(conflictlogrelid, RowExclusiveLock);
+	/*
+	 * Use try_table_open(): the table may have been dropped concurrently by an
+	 * ALTER SUBSCRIPTION that changed conflict_log_destination.
+	 */
+	conflictlogrel = try_table_open(conflictlogrelid, RowExclusiveLock);
+
+	/*
+	 * If the table is gone, fall back to logging the conflict to the server
+	 * log so it is not lost: report the table as no longer a destination and
+	 * force server-log reporting.
+	 */
+	if (conflictlogrel == NULL)
+	{
+		if (log_dest_table)
+			*log_dest_table = false;
+		if (log_dest_logfile)
+			*log_dest_logfile = true;
+	}
+
+	return conflictlogrel;
 }
 
 /*
diff --git a/src/include/replication/conflict.h b/src/include/replication/conflict.h
index b727cc285e9..760f375d088 100644
--- a/src/include/replication/conflict.h
+++ b/src/include/replication/conflict.h
@@ -117,6 +117,7 @@ extern void ReportApplyConflict(EState *estate, ResultRelInfo *relinfo,
 								List *conflicttuples);
 extern void ProcessPendingConflictLogTuple(void);
 extern void InitConflictIndexes(ResultRelInfo *relInfo);
-extern Relation GetConflictLogDestAndTable(ConflictLogDest *log_dest);
+extern Relation GetConflictLogDestAndTable(bool *log_dest_table,
+										   bool *log_dest_logfile);
 extern void InsertConflictLogTuple(Relation conflictlogrel);
 #endif
-- 
2.54.0