Hi hackers,

While reviewing the issue reported at [1] and the proposed solutions
at [2], I noticed a related problem: false negative conflict detection
when a 'ReplOriginId' gets reused.

In logical replication, conflict detection relies on the tuple’s
replication origin ('roident'). The problem is that if a subscription
is dropped and a new subscription later reuses the same origin ID, the
apply worker may incorrectly treat incoming changes as “its own”
changes and skip conflict detection.

A simple example:
  1. Create subscription sub1 with 'roident = 1'
  2. Replicate some rows into table 't1'
  3. Drop 'sub1'
  4. Create another subscription 'sub2'
  5. `sub2` reuses 'roident = 1'
  6. New updates arrive for rows previously written by 'sub1'
  At this point, conflict detection sees:
      tuple_origin == current_origin

and incorrectly assumes the row was written by the current
subscription instance, so no 'update_origin_differ' conflict is
raised.

This may look harmless in this simple setup, but it becomes
problematic if the new subscription is connected to a different
publisher, because real conflicts can then be silently missed.

I explored two possible approaches to solve this:

Approach 1. Zero out old origin IDs in commit_ts data when dropping a
subscription
----------------------
 - When a subscription is dropped and its replication origin becomes
free, scan all 'commit_ts' SLRU entries and replace that old origin ID
with 'InvalidRepOriginId (0)'.
 - So rows previously written by the old subscription would no longer
appear to belong to any active replication origin.
 - A new subscription reusing the same 'roident' will always conflict
with origin '0'.

Pros:
 - Fixes the stale-origin problem completely and may also help solve
the tablesync-origin issue discussed in [1]
 - No additional checks needed during conflict detection

Cons:
 - Requires scanning the entire 'commit_ts' SLRU during DROP
SUBSCRIPTION, so it can become very expensive on large systems
 - Not crash-safe currently(patch):
    - if the server crashes midway, some entries may still contain the
old origin ID
    - after restart, reused origins can again lead to missed conflicts
 - Making this fully crash-safe would likely require WAL logging or
recovery-time reprocessing.

Approach 2. Store replication origin creation time
----------------------
 - Add a creation timestamp for each replication origin
 - During conflict check:
    if tuple_origin != current_origin
        -> existing behavior
    if tuple_origin == current_origin
        -> compare tuple commit timestamp with origin creation time
        if tuple_commit_ts <= origin_creation_time
            -> treat as an origin reuse case and raise conflict

Pros:
-------
 - No additional processing during DROP SUBSCRIPTION
 - Lightweight runtime check (just one timestamp comparison)
 - Naturally crash-safe since origin creation is WAL-logged already

Cons:
 - Requires a catalog schema change
 - The <= comparison can produce false-positive conflicts for rows
committed at the exact same microsecond as origin creation
 -  May require additional handling for upgraded origins

IMO, the second approach currently looks more practical because it
avoids the heavy SLRU scan and crash-recovery complexity.

Attached:
 - Patch for approach 1
 - Patch for approach 2
 - A TAP test reproducing the issue

Note: The patches are manually tested for the reported issue, but not
yet tested for performance or additional edge cases.

Feedback and suggestions are welcome.

[1] 
https://www.postgresql.org/message-id/CALDaNm3Y6Y4Mub6QC8fZKnNy5jZspELQYCoQF_FL2Zwzweu%3Dog%40mail.gmail.com
[2] 
https://www.postgresql.org/message-id/CAA4eK1LxGXR7jOAKh0B8N362S-Q3b6GhBxxcV_HxUaicEPq5Cg%40mail.gmail.com

--
Thanks,
Nisha
From 5abf86e31601e19036dd0ba6da129b8a2563ce19 Mon Sep 17 00:00:00 2001
From: Nisha Moond <[email protected]>
Date: Wed, 13 May 2026 22:24:56 +0530
Subject: [PATCH v1 1/2] Approach 2 Detect origin-id reuse in origin differs
 conflict checksp

Add a 'rocreated' timestamptz column to pg_replication_origin, set at
repliorigin creation.

In the apply worker's UPDATE/DELETE conflict checks, when the local
row's origin roident matches the current subscription's roident (which
previously meant 'no conflict'), additionally verify that the local
row's commit_ts > ro_created.  If commit_ts <= ro_created, the roident
was reused from a prior DROP/CREATE SUBSCRIPTION and the row was
written by a different subscription; report the conflict.
---
 doc/src/sgml/catalogs.sgml                  |  9 ++++
 src/backend/replication/logical/origin.c    | 58 ++++++++++++++++++++-
 src/backend/replication/logical/worker.c    | 50 ++++++++++++++++--
 src/include/catalog/pg_replication_origin.h |  1 +
 src/include/replication/origin.h            |  1 +
 5 files changed, 113 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 4b474c13917..f4819594bdc 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -7360,6 +7360,15 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        origin.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rocreated</structfield> <type>timestamptz</type>
+      </para>
+      <para>
+       Time at which this replication origin was created.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/src/backend/replication/logical/origin.c b/src/backend/replication/logical/origin.c
index c9dfb094c2b..c90382af6a1 100644
--- a/src/backend/replication/logical/origin.c
+++ b/src/backend/replication/logical/origin.c
@@ -144,6 +144,14 @@ typedef struct ReplicationState
 	 * Lock protecting remote_lsn and local_lsn.
 	 */
 	LWLock		lock;
+
+	/*
+	 * Wall-clock time when this origin was created (from
+	 * pg_replication_origin.rocreated).  Used to detect roident reuse: if a
+	 * local row's commit_ts <= ro_created, the row was written by a prior
+	 * subscription that held this ID.
+	 */
+	TimestampTz ro_created;
 } ReplicationState;
 
 /*
@@ -359,6 +367,8 @@ replorigin_create(const char *roname)
 
 			values[Anum_pg_replication_origin_roident - 1] = ObjectIdGetDatum(roident);
 			values[Anum_pg_replication_origin_roname - 1] = roname_d;
+			values[Anum_pg_replication_origin_rocreated - 1] =
+				TimestampTzGetDatum(GetCurrentTimestamp());
 
 			tuple = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 			CatalogTupleInsert(rel, tuple);
@@ -839,6 +849,7 @@ StartupReplicationOrigin(void)
 
 		/* copy data to shared memory */
 		replication_states[last_state].roident = disk_state.roident;
+		replication_states[last_state].ro_created = DT_NOBEGIN;
 		replication_states[last_state].remote_lsn = disk_state.remote_lsn;
 		last_state++;
 
@@ -1134,6 +1145,24 @@ ReplicationOriginExitCleanup(int code, Datum arg)
 	replorigin_session_reset_internal();
 }
 
+/*
+ * Return the creation timestamp of the current session's replication origin,
+ * or DT_NOBEGIN if no session origin is active or the value has not been
+ * loaded yet.
+ *
+ * Used by the apply worker to detect roident reuse during conflict checking:
+ * if a local row's commit_ts <= the returned value, the row was written by a
+ * prior subscription that happened to hold the same origin ID.
+ */
+TimestampTz
+replorigin_get_creation_time(void)
+{
+	if (session_replication_state == NULL)
+		return DT_NOBEGIN;
+
+	return session_replication_state->ro_created;
+}
+
 /*
  * Setup a replication origin in the shared memory struct if it doesn't
  * already exist and cache access to the specific ReplicationSlot so the
@@ -1263,9 +1292,9 @@ replorigin_session_setup(ReplOriginId node, int acquired_by)
 		Assert(!XLogRecPtrIsValid(session_replication_state->remote_lsn));
 		Assert(!XLogRecPtrIsValid(session_replication_state->local_lsn));
 		session_replication_state->roident = node;
+		session_replication_state->ro_created = DT_NOBEGIN;
 	}
 
-
 	Assert(session_replication_state->roident != InvalidReplOriginId);
 
 	if (acquired_by == 0)
@@ -1289,6 +1318,33 @@ replorigin_session_setup(ReplOriginId node, int acquired_by)
 
 	/* probably this one is pointless */
 	ConditionVariableBroadcast(&session_replication_state->origin_cv);
+
+	/*
+	 * Load the origin's creation time from the catalog if not already cached.
+	 * The guard fires because ro_created is explicitly set to DT_NOBEGIN in
+	 * the free-slot branch above and in StartupReplicationOrigin().  It also
+	 * fires for origins that pre-date this feature: those have NULL in the
+	 * catalog, so isnull will be true and ro_created stays DT_NOBEGIN,
+	 * meaning the roident-reuse check is safely skipped for them.
+	 */
+	if (session_replication_state->ro_created == DT_NOBEGIN)
+	{
+		HeapTuple	tuple;
+
+		tuple = SearchSysCache1(REPLORIGIDENT, ObjectIdGetDatum(node));
+		if (HeapTupleIsValid(tuple))
+		{
+			bool		isnull;
+			Datum		d;
+
+			d = SysCacheGetAttr(REPLORIGIDENT, tuple,
+								Anum_pg_replication_origin_rocreated,
+								&isnull);
+			if (!isnull)
+				session_replication_state->ro_created = DatumGetTimestampTz(d);
+			ReleaseSysCache(tuple);
+		}
+	}
 }
 
 /*
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index dd6fc38a41e..e0b6c021dfa 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -2905,6 +2905,41 @@ apply_handle_update(StringInfo s)
 	end_replication_step();
 }
 
+/*
+ * Check whether the tuple's origin roident was reused by a new subscription.
+ * Returns true if a conflict should be reported despite matching roidents.
+ *
+ * When origins match (localorigin == current origin), the same numeric roident
+ * may have been recycled after a DROP/CREATE SUBSCRIPTION cycle.  If the local
+ * row's commit_ts is at or before the current origin's creation time, the row
+ * was written by an earlier subscription, not the current one.
+ *
+ * Uses <= rather than < so that a row committed at the exact same microsecond
+ * as the origin was created is treated as belonging to the prior subscription;
+ * a false positive (spurious conflict) is safer than a false negative here.
+ */
+static inline bool
+IsRoidentReused(ReplOriginId localorigin, TimestampTz localts)
+{
+	TimestampTz origin_created;
+
+	if (localorigin == InvalidReplOriginId)
+		return false;
+
+	origin_created = replorigin_get_creation_time();
+
+	/*
+	 * DT_NOBEGIN means no session origin is active, ro_created has not been
+	 * loaded yet, or the origin pre-dates this feature (NULL in catalog) --
+	 * in all cases skip the reuse check.  localts == 0 when
+	 * track_commit_timestamp is off; no timestamp to compare.
+	 */
+	if (origin_created == DT_NOBEGIN || localts == 0)
+		return false;
+
+	return (localts <= origin_created);
+}
+
 /*
  * Workhorse for apply_handle_update()
  * relinfo is for the relation we're actually updating in
@@ -2947,7 +2982,8 @@ apply_handle_update_internal(ApplyExecutionData *edata,
 		 */
 		if (GetTupleTransactionInfo(localslot, &conflicttuple.xmin,
 									&conflicttuple.origin, &conflicttuple.ts) &&
-			conflicttuple.origin != replorigin_xact_state.origin)
+			(conflicttuple.origin != replorigin_xact_state.origin ||
+			 IsRoidentReused(conflicttuple.origin, conflicttuple.ts)))
 		{
 			TupleTableSlot *newslot;
 
@@ -2989,7 +3025,8 @@ apply_handle_update_internal(ApplyExecutionData *edata,
 									   &conflicttuple.xmin,
 									   &conflicttuple.origin,
 									   &conflicttuple.ts) &&
-			conflicttuple.origin != replorigin_xact_state.origin)
+			(conflicttuple.origin != replorigin_xact_state.origin ||
+			 IsRoidentReused(conflicttuple.origin, conflicttuple.ts)))
 			type = CT_UPDATE_DELETED;
 		else
 			type = CT_UPDATE_MISSING;
@@ -3142,7 +3179,8 @@ apply_handle_delete_internal(ApplyExecutionData *edata,
 		 */
 		if (GetTupleTransactionInfo(localslot, &conflicttuple.xmin,
 									&conflicttuple.origin, &conflicttuple.ts) &&
-			conflicttuple.origin != replorigin_xact_state.origin)
+			(conflicttuple.origin != replorigin_xact_state.origin ||
+			 IsRoidentReused(conflicttuple.origin, conflicttuple.ts)))
 		{
 			conflicttuple.slot = localslot;
 			ReportApplyConflict(estate, relinfo, LOG, CT_DELETE_ORIGIN_DIFFERS,
@@ -3484,7 +3522,8 @@ apply_handle_tuple_routing(ApplyExecutionData *edata,
 												   &conflicttuple.xmin,
 												   &conflicttuple.origin,
 												   &conflicttuple.ts) &&
-						conflicttuple.origin != replorigin_xact_state.origin)
+						(conflicttuple.origin != replorigin_xact_state.origin ||
+						 IsRoidentReused(conflicttuple.origin, conflicttuple.ts)))
 						type = CT_UPDATE_DELETED;
 					else
 						type = CT_UPDATE_MISSING;
@@ -3510,7 +3549,8 @@ apply_handle_tuple_routing(ApplyExecutionData *edata,
 				if (GetTupleTransactionInfo(localslot, &conflicttuple.xmin,
 											&conflicttuple.origin,
 											&conflicttuple.ts) &&
-					conflicttuple.origin != replorigin_xact_state.origin)
+					(conflicttuple.origin != replorigin_xact_state.origin ||
+					 IsRoidentReused(conflicttuple.origin, conflicttuple.ts)))
 				{
 					TupleTableSlot *newslot;
 
diff --git a/src/include/catalog/pg_replication_origin.h b/src/include/catalog/pg_replication_origin.h
index 565d71ad0b3..d6079ca4844 100644
--- a/src/include/catalog/pg_replication_origin.h
+++ b/src/include/catalog/pg_replication_origin.h
@@ -51,6 +51,7 @@ CATALOG(pg_replication_origin,6000,ReplicationOriginRelationId) BKI_SHARED_RELAT
 	text		roname BKI_FORCE_NOT_NULL;
 
 #ifdef CATALOG_VARLEN			/* further variable-length fields */
+	timestamptz rocreated BKI_FORCE_NOT_NULL;
 #endif
 } FormData_pg_replication_origin;
 
diff --git a/src/include/replication/origin.h b/src/include/replication/origin.h
index a69faf6eaaf..22bed238140 100644
--- a/src/include/replication/origin.h
+++ b/src/include/replication/origin.h
@@ -71,6 +71,7 @@ extern void replorigin_session_advance(XLogRecPtr remote_commit,
 extern void replorigin_session_setup(ReplOriginId node, int acquired_by);
 extern void replorigin_session_reset(void);
 extern XLogRecPtr replorigin_session_get_progress(bool flush);
+extern TimestampTz replorigin_get_creation_time(void);
 
 /* Per-transaction replication origin state manipulation */
 extern void replorigin_xact_clear(bool clear_origin);
-- 
2.50.1 (Apple Git-155)

From ed125b8869186db8951842ea4f97467056684762 Mon Sep 17 00:00:00 2001
From: Nisha Moond <[email protected]>
Date: Fri, 8 May 2026 11:57:55 +0530
Subject: [PATCH v1 2/2] Add TAP test to detect conflict when origin-id is
 reused

---
 .../t/039_origin_reuse_conflict.pl            | 68 +++++++++++++++++++
 1 file changed, 68 insertions(+)
 create mode 100644 src/test/subscription/t/039_origin_reuse_conflict.pl

diff --git a/src/test/subscription/t/039_origin_reuse_conflict.pl b/src/test/subscription/t/039_origin_reuse_conflict.pl
new file mode 100644
index 00000000000..efc58790f46
--- /dev/null
+++ b/src/test/subscription/t/039_origin_reuse_conflict.pl
@@ -0,0 +1,68 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test that roident reuse is detected as an update_origin_differs conflict.
+#
+# sub1 and sub2 both use copy_data=false (no tablesync, one origin each).
+# Dropping sub1 and creating sub2 reuses the same roident.  A row written by
+# sub1's apply worker has the same origin number as sub2, so only
+# IsRoidentReused() can distinguish them.
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node_pub = PostgreSQL::Test::Cluster->new('publisher');
+$node_pub->init(allows_streaming => 'logical');
+$node_pub->start;
+
+my $node_sub = PostgreSQL::Test::Cluster->new('subscriber');
+$node_sub->init;
+$node_sub->append_conf('postgresql.conf', "track_commit_timestamp = on");
+$node_sub->start;
+
+$node_pub->safe_psql('postgres', "CREATE TABLE t (a int PRIMARY KEY, b text)");
+$node_sub->safe_psql('postgres', "CREATE TABLE t (a int PRIMARY KEY, b text)");
+
+my $pubconn = $node_pub->connstr . ' dbname=postgres';
+$node_pub->safe_psql('postgres', "CREATE PUBLICATION pub FOR TABLE t");
+
+$node_sub->safe_psql('postgres',
+	"CREATE SUBSCRIPTION sub1
+	 CONNECTION '$pubconn'
+	 PUBLICATION pub
+	 WITH (copy_data = false)");
+
+# INSERT flows through sub1's apply worker, stamping the row with roident 1.
+$node_pub->safe_psql('postgres', "INSERT INTO t VALUES (1, 'a')");
+$node_pub->wait_for_catchup('sub1');
+
+# Drop sub1 (frees roident 1), sleep so sub2's ro_created > row's commit_ts.
+$node_sub->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+$node_sub->safe_psql('postgres', "SELECT pg_sleep(0.05)");
+
+$node_sub->safe_psql('postgres',
+	"CREATE SUBSCRIPTION sub2
+	 CONNECTION '$pubconn'
+	 PUBLICATION pub
+	 WITH (copy_data = false)");
+
+my $log_start = -s $node_sub->logfile;
+
+# The row has origin=roident1 from sub1 with commit_ts=T1.  sub2 also has
+# roident1 but ro_created=T2 > T1, so IsRoidentReused() fires.
+$node_pub->safe_psql('postgres', "UPDATE t SET b = 'b' WHERE a = 1");
+$node_pub->wait_for_catchup('sub2');
+
+my $logfile = slurp_file($node_sub->logfile, $log_start);
+like(
+	$logfile,
+	qr/conflict detected on relation "public\.t": conflict=update_origin_differs/,
+	'update_origin_differs conflict reported for reused roident');
+
+my $val = $node_sub->safe_psql('postgres', "SELECT b FROM t WHERE a = 1");
+is($val, 'b', 'row updated to latest value after conflict');
+
+$node_sub->safe_psql('postgres', "DROP SUBSCRIPTION sub2");
+
+done_testing();
-- 
2.50.1 (Apple Git-155)

From 9ad2b5f8a4bbcd133c013229c93039176516bb8b Mon Sep 17 00:00:00 2001
From: Nisha Moond <[email protected]>
Date: Fri, 8 May 2026 14:55:12 +0530
Subject: [PATCH v1] Approach 1 Zero commit_ts origin on replication origin
 drop

When a replication origin is dropped, scan the commit_ts data and set
nodeid = InvalidReplOriginId on every entry that records the freed
roident. This ensures that after DROP/CREATE SUBSCRIPTION reuse the
roident, the apply worker sees nodeid=0 != current_origin for rows
written by the old subscription and reports an origin_differs
conflict rather than silently skipping it.
---
 src/backend/access/transam/commit_ts.c   | 62 ++++++++++++++++++++++++
 src/backend/replication/logical/origin.c |  8 +++
 src/include/access/commit_ts.h           |  1 +
 src/test/subscription/t/029_on_error.pl  |  2 +-
 4 files changed, 72 insertions(+), 1 deletion(-)

diff --git a/src/backend/access/transam/commit_ts.c b/src/backend/access/transam/commit_ts.c
index 9e6fd5d4657..6851f21e810 100644
--- a/src/backend/access/transam/commit_ts.c
+++ b/src/backend/access/transam/commit_ts.c
@@ -271,6 +271,68 @@ TransactionIdSetCommitTs(TransactionId xid, TimestampTz ts,
 		   &entry, SizeOfCommitTimestampEntry);
 }
 
+/*
+ * Zero the nodeid field on every commit_ts SLRU entry that records the given
+ * origin.  Called when a replication origin is dropped so that rows stamped
+ * with the freed roident appear to have no origin; a future apply worker
+ * that reuses the same roident will then see nodeid=0 != current_origin and
+ * report a conflict via the ordinary origin-differs check.
+ *
+ * Does nothing when track_commit_timestamp is off.
+ */
+void
+InvalidateCommitTsOrigin(ReplOriginId origin)
+{
+	TransactionId oldest;
+	TransactionId newest;
+	int64		firstpage;
+	int64		lastpage;
+
+	if (!track_commit_timestamp)
+		return;
+
+	oldest = TransamVariables->oldestCommitTsXid;
+	newest = TransamVariables->newestCommitTsXid;
+
+	if (!TransactionIdIsValid(oldest))
+		return;
+
+	firstpage = TransactionIdToCTsPage(oldest);
+	lastpage = TransactionIdToCTsPage(newest);
+
+	for (int64 pageno = firstpage; pageno <= lastpage; pageno++)
+	{
+		LWLock	   *lock = SimpleLruGetBankLock(CommitTsCtl, pageno);
+		int			slotno;
+		bool		modified = false;
+		TransactionId xid = (TransactionId)
+		(pageno * COMMIT_TS_XACTS_PER_PAGE);
+
+		LWLockAcquire(lock, LW_EXCLUSIVE);
+		slotno = SimpleLruReadPage(CommitTsCtl, pageno, true, &xid);
+
+		for (int i = 0; i < COMMIT_TS_XACTS_PER_PAGE; i++)
+		{
+			CommitTimestampEntry entry;
+			char	   *entryptr = CommitTsCtl->shared->page_buffer[slotno]
+				+ SizeOfCommitTimestampEntry * i;
+
+			memcpy(&entry, entryptr, SizeOfCommitTimestampEntry);
+			if (entry.nodeid == origin)
+			{
+				entry.nodeid = InvalidReplOriginId;
+				memcpy(entryptr, &entry, SizeOfCommitTimestampEntry);
+				modified = true;
+			}
+		}
+
+		if (modified)
+			CommitTsCtl->shared->page_dirty[slotno] = true;
+
+		LWLockRelease(lock);
+	}
+}
+
 /*
  * Interrogate the commit timestamp of a transaction.
  *
diff --git a/src/backend/replication/logical/origin.c b/src/backend/replication/logical/origin.c
index c9dfb094c2b..30d38cd30d3 100644
--- a/src/backend/replication/logical/origin.c
+++ b/src/backend/replication/logical/origin.c
@@ -71,6 +71,7 @@
 #include <sys/stat.h>
 
 #include "access/genam.h"
+#include "access/commit_ts.h"
 #include "access/htup_details.h"
 #include "access/table.h"
 #include "access/xact.h"
@@ -490,6 +491,13 @@ replorigin_drop_by_name(const char *name, bool missing_ok, bool nowait)
 
 	replorigin_state_clear(roident, nowait);
 
+	/*
+	 * Zero any commit_ts entries stamped with this origin so that a future
+	 * subscription reusing the same roident sees nodeid=0 != current_origin
+	 * and reports a conflict via the ordinary origin-differs check.
+	 */
+	InvalidateCommitTsOrigin(roident);
+
 	/*
 	 * Now, we can delete the catalog entry.
 	 */
diff --git a/src/include/access/commit_ts.h b/src/include/access/commit_ts.h
index 825ccda90ed..400de80fac0 100644
--- a/src/include/access/commit_ts.h
+++ b/src/include/access/commit_ts.h
@@ -37,6 +37,7 @@ extern void TruncateCommitTs(TransactionId oldestXact);
 extern void SetCommitTsLimit(TransactionId oldestXact,
 							 TransactionId newestXact);
 extern void AdvanceOldestCommitTsXid(TransactionId oldestXact);
+extern void InvalidateCommitTsOrigin(ReplOriginId origin);
 
 extern int	committssyncfiletag(const FileTag *ftag, char *path);
 
diff --git a/src/test/subscription/t/029_on_error.pl b/src/test/subscription/t/029_on_error.pl
index 7d68759b6cd..c7c5c16d9f3 100644
--- a/src/test/subscription/t/029_on_error.pl
+++ b/src/test/subscription/t/029_on_error.pl
@@ -30,7 +30,7 @@ sub test_skip_lsn
 	# ERROR with its CONTEXT when retrieving this information.
 	my $contents = slurp_file($node_subscriber->logfile, $offset);
 	$contents =~
-	  qr/conflict detected on relation "public.tbl".*\n.*DETAIL:.* Could not apply remote change.*\n.*Key already exists in unique index "tbl_pkey", modified by .*origin.* in transaction \d+ at .*: key .*, local row .*\n.*CONTEXT:.* for replication target relation "public.tbl" in transaction \d+, finished at ([[:xdigit:]]+\/[[:xdigit:]]+)/m
+	  qr/conflict detected on relation "public.tbl".*\n.*DETAIL:.* Could not apply remote change.*\n.*Key already exists in unique index "tbl_pkey", modified (?:locally|by .*) in transaction \d+ at .*\n.*CONTEXT:.* for replication target relation "public.tbl" in transaction \d+, finished at ([[:xdigit:]]+\/[[:xdigit:]]+)/m
 	  or die "could not get error-LSN";
 	my $lsn = $1;
 
-- 
2.50.1 (Apple Git-155)

Reply via email to