On Fri, Nov 13, 2026 at 7:35 PM Amit Kapila <[email protected]> wrote:
>
> On Fri, Nov 13, 2026 at 7:29 PM Bertrand Drouvot
> <[email protected]> wrote:
> >
> > On Fri, Nov 13, 2026 at 04:45:00PM +0530, Amit Kapila wrote:
> > >
> > > + else if (slot->data.persistency == RS_TEMPORARY)
> > > + {
> > > + /*
> > > + * We reach this point when a previous cycle updated confirmed_flush
> > > + * but failed to build a consistent snapshot, and the remote slot has
> > > + * not advanced beyond the local slot since then.
> > > + */
> > > + skip_reason = SS_SKIP_NO_CONSISTENT_SNAPSHOT;
> > > + }
> > >
> > > Is it better to directly use the slot's skip_reason rather than
> > > assuming SS_SKIP_NO_CONSISTENT_SNAPSHOT?
> >
> > That could work but what about the case I mentioned upthread where the local
> > catalog_xmin gets ahead and the remote later catches up? I think that that 
> > would
> > keep slotsync_skip_reason as SS_SKIP_WAL_OR_ROWS_REMOVED, although the slot 
> > is
> > still inconsistent due to the missing snapshot.
> >
>
> Right the reason can remain as SS_SKIP_WAL_OR_ROWS_REMOVED. Even in
> the first cycle, the reason could be either SS_SKIP_WAL_NOT_FLUSHED or
> SS_SKIP_WAL_OR_ROWS_REMOVED and in that case also the caller
> update_and_persist_local_synced_slot() won't persist the slot. So, I
> don't see a problem with retaining the previous reason when we didn't
> perform anything in a particular sync cycle.

While I agree that retaining the previous reason doesn't make us
persist the slot, I think it's safe only because the previous reason
is never SS_SKIP_NONE for a temporary slot at that point. That's true
today, but it depends on several things and it would be hard to modify
these code correctly in the future.

I think the root cause is that commit 788ec96d591 replaced
found_consistent_snapshot with the check of slotsync_skip_reason, but
they're not equivalent: found_consistent_snapshot is false unless the
current cycle synced the slot and found a consistent snapshot, whereas
slotsync_skip_reason is SS_SKIP_NONE unless the current cycle detected
a reason to skip. So an alternative idea would be to bring back
found_consistent_snapshot part from the commit. That way, the decision
of persisting the slot doesn't depend on the skip_reasons at all.

As for the reported reason, a temporary slot is the one we have not
confirmed to reach a consistent point yet. So I think we can simply
report no_consistent_snapshot for a temporary slot if the current
cycle didn't find a consistent snapshot, without relying on what
happened in the previous cycles.

I've attached a PoC patch.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index aa4cc436ad7..1e760a798e4 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -217,18 +217,27 @@ update_slotsync_skip_stats(SlotSyncSkipReason skip_reason)
  *
  * If no update was needed (the data of the remote slot is the same as the
  * local slot) return false, otherwise true.
+ *
+ * *found_consistent_snapshot will be true iff the remote slot's LSN or xmin is
+ * synced in this call, and decoding from the corresponding LSN's can reach a
+ * consistent snapshot.
  */
 static bool
-update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid)
+update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid,
+						 bool *found_consistent_snapshot)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
 	bool		updated_xmin_or_lsn = false;
 	bool		updated_config = false;
+	bool		found_consistent = false;
 	SlotSyncSkipReason skip_reason = SS_SKIP_NONE;
 	XLogRecPtr	latestFlushPtr = GetStandbyFlushRecPtr(NULL);
 
 	Assert(slot->data.invalidated == RS_INVAL_NONE);
 
+	if (found_consistent_snapshot)
+		*found_consistent_snapshot = false;
+
 	/*
 	 * Make sure that concerned WAL is received and flushed before syncing
 	 * slot to target lsn received from the primary server.
@@ -335,16 +344,16 @@ update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid)
 			SpinLockRelease(&slot->mutex);
 
 			updated_xmin_or_lsn = true;
+			found_consistent = true;
 		}
 		else
 		{
-			bool		found_consistent_snapshot;
 			XLogRecPtr	old_confirmed_lsn = slot->data.confirmed_flush;
 			XLogRecPtr	old_restart_lsn = slot->data.restart_lsn;
 			TransactionId old_catalog_xmin = slot->data.catalog_xmin;
 
 			LogicalSlotAdvanceAndCheckSnapState(remote_slot->confirmed_lsn,
-												&found_consistent_snapshot);
+												&found_consistent);
 
 			/* Sanity check */
 			if (slot->data.confirmed_flush != remote_slot->confirmed_lsn)
@@ -359,7 +368,7 @@ update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid)
 			 * If we can't reach a consistent snapshot, the slot won't be
 			 * persisted. See update_and_persist_local_synced_slot().
 			 */
-			if (!found_consistent_snapshot)
+			if (!found_consistent)
 			{
 				Assert(MyReplicationSlot->data.persistency == RS_TEMPORARY);
 
@@ -383,6 +392,18 @@ update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid)
 		}
 	}
 
+	/*
+	 * A temporary slot is not sync-ready until we find that decoding from its
+	 * restart_lsn reaches a consistent point. That's the case not only when
+	 * we failed to find one above, but also when the remote slot has not
+	 * advanced beyond the local slot and so we didn't attempt to sync it.
+	 */
+	if (slot->data.persistency == RS_TEMPORARY && !found_consistent)
+		skip_reason = SS_SKIP_NO_CONSISTENT_SNAPSHOT;
+
+	if (found_consistent_snapshot)
+		*found_consistent_snapshot = found_consistent;
+
 	/* Update slot sync skip stats */
 	update_slotsync_skip_stats(skip_reason);
 
@@ -678,21 +699,25 @@ update_and_persist_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid,
 									 bool *slot_persistence_pending)
 {
 	ReplicationSlot *slot = MyReplicationSlot;
+	bool		found_consistent_snapshot;
 
 	/* Slotsync skip stats are handled in function update_local_synced_slot() */
-	(void) update_local_synced_slot(remote_slot, remote_dbid);
+	(void) update_local_synced_slot(remote_slot, remote_dbid,
+									&found_consistent_snapshot);
 
 	/*
-	 * Check if the slot cannot be synchronized. Refer to the comment atop the
-	 * file for details on this check.
+	 * Persist the slot only if this cycle has synced it to the remote slot
+	 * and found that decoding from its restart_lsn reaches a consistent
+	 * point. Refer to the comment atop the file for details on this check.
 	 */
-	if (slot->slotsync_skip_reason != SS_SKIP_NONE)
+	if (!found_consistent_snapshot)
 	{
 		/*
 		 * We reach this point when the remote slot didn't catch up to locally
 		 * reserved position, or it cannot reach the consistent point from the
 		 * restart_lsn, or the WAL prior to the remote confirmed flush LSN has
-		 * not been received and flushed.
+		 * not been received and flushed, or there was nothing to sync because
+		 * the remote slot has not advanced beyond the local slot.
 		 *
 		 * We do not drop the slot because the restart_lsn and confirmed_lsn
 		 * can be ahead of the current location when recreating the slot in
@@ -709,6 +734,8 @@ update_and_persist_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid,
 		return false;
 	}
 
+	Assert(slot->slotsync_skip_reason == SS_SKIP_NONE);
+
 	ReplicationSlotPersist();
 
 	ereport(LOG,
@@ -831,7 +858,8 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid,
 										   LSN_FORMAT_ARGS(slot->data.confirmed_flush),
 										   LSN_FORMAT_ARGS(remote_slot->confirmed_lsn)));
 
-			slot_updated = update_local_synced_slot(remote_slot, remote_dbid);
+			slot_updated = update_local_synced_slot(remote_slot, remote_dbid,
+													NULL);
 		}
 	}
 	/* Otherwise create the slot first. */
diff --git a/src/test/recovery/t/040_standby_failover_slots_sync.pl b/src/test/recovery/t/040_standby_failover_slots_sync.pl
index b0f4dfad6f2..8272dae95c7 100644
--- a/src/test/recovery/t/040_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/040_standby_failover_slots_sync.pl
@@ -1114,4 +1114,57 @@ $standby2->wait_for_log(
 
 $h->quit;
 
+##################################################
+# Test that a synced slot stays temporary when the standby cannot build a
+# consistent snapshot across multiple sync cycles.
+##################################################
+
+# Disable the subscription so lsub1_slot does not advance and serialize
+# snapshots on standby2 while still holding back catalog_xmin on standby2.
+$subscriber1->safe_psql('postgres',
+	"ALTER SUBSCRIPTION regress_mysub1 DISABLE");
+$primary->poll_query_until('postgres',
+	"SELECT NOT active FROM pg_replication_slots WHERE slot_name = 'lsub1_slot'"
+);
+
+$primary->safe_psql('postgres',
+	"SELECT pg_create_logical_replication_slot('inconsistent_slot', 'test_decoding', false, false, true);"
+);
+
+# Keep a transaction with an assigned XID open while logging xl_running_xacts
+# and advancing inconsistent_slot so its restart_lsn moves to that record and
+# concurrent bgwriter snapshots cannot make it consistent. Advance the slot
+# twice in case a concurrent xl_running_xacts record logged before the open
+# transaction was used as the candidate restart_lsn in the first call.
+my $xact_h = $primary->background_psql('postgres');
+$xact_h->query_safe('BEGIN; SELECT pg_current_xact_id();');
+
+$primary->safe_psql(
+	'postgres', qq(
+	SELECT pg_log_standby_snapshot();
+	BEGIN;
+	INSERT INTO tab_int VALUES (100);
+	COMMIT;
+	SELECT pg_replication_slot_advance('inconsistent_slot', pg_current_wal_lsn());
+	SELECT pg_replication_slot_advance('inconsistent_slot', pg_current_wal_lsn());
+));
+
+$xact_h->query_safe('COMMIT;');
+$xact_h->quit;
+
+$primary->wait_for_replay_catchup($standby2);
+
+$standby2->append_conf('postgresql.conf', 'sync_replication_slots = on');
+$standby2->reload;
+
+ok( $standby2->poll_query_until(
+		'postgres', qq(
+		SELECT slotsync_skip_count >= 2 AND temporary AND
+			   slotsync_skip_reason = 'no_consistent_snapshot'
+		FROM pg_stat_replication_slots
+		JOIN pg_replication_slots USING (slot_name)
+		WHERE slot_name = 'inconsistent_slot';
+	)),
+	'synced slot without a consistent snapshot stays temporary');
+
 done_testing();

Reply via email to