Hi all,
While reviewing slotsync.c, I hit the assertion failure:
TRAP: failed Assert("MyReplicationSlot->data.persistency ==
RS_TEMPORARY"), slotsync.c:364
IIUC this is a regression of commit 788ec96d591 (CC'ed Hou and Amit).
In update_local_synced_slot(), we try to advance the local slot only
when the remote slot is ahead of it, and if the standby cannot reach a
consistent snapshot we set the skip reason to
SS_SKIP_NO_CONSISTENT_SNAPSHOT so that
update_and_persist_local_synced_slot() doesn't persist the slot.
However, LogicalSlotAdvanceAndCheckSnapState() has already advanced
confirmed_flush_lsn to the remote's value in that case (restart_lsn
and catalog_xmin were already equal to the remote's when the slot was
created). So in the next cycle the remote slot is no longer ahead, we
skip the advance entirely, and update_slotsync_skip_stats() overwrites
the skip reason with SS_SKIP_NONE.
update_and_persist_local_synced_slot() then persists the slot and
reports it as sync-ready even though nothing has changed since the
previous cycle. Once the remote slot advances again and the standby
still cannot find a consistent snapshot, the assertion above fails.
On non-assert builds, IIUC the problem is worse: the persisted slot
can be used after promotion, and decoding from it skips the
transactions between its restart_lsn and the first consistent point,
causing data loss.
Before that commit, update_and_persist_local_synced_slot() also
checked found_consistent_snapshot returned by
update_local_synced_slot() for the current cycle, so a cycle that
didn't attempt to advance the slot never persisted it.
I've attached a TAP test to reproduce this issue, generated by AI.
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From 87732112c48550c848a6d6b18bae57f5e616ce2d Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Mon, 9 Nov 2026 11:52:02 -0800
Subject: [PATCH] Add test for slot sync persisting a slot without a consistent
snapshot.
---
src/test/recovery/meson.build | 1 +
.../t/061_slotsync_persist_inconsistent.pl | 96 +++++++++++++++++++
2 files changed, 97 insertions(+)
create mode 100644 src/test/recovery/t/061_slotsync_persist_inconsistent.pl
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 87dbdb6326e..dbd776cbd80 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -68,6 +68,7 @@ tests += {
't/057_snapshot_commit_race.pl',
't/059_remote_apply_status_interval.pl',
't/060_multixact_crashed_updater.pl',
+ 't/061_slotsync_persist_inconsistent.pl',
],
},
}
diff --git a/src/test/recovery/t/061_slotsync_persist_inconsistent.pl b/src/test/recovery/t/061_slotsync_persist_inconsistent.pl
new file mode 100644
index 00000000000..98f3df35634
--- /dev/null
+++ b/src/test/recovery/t/061_slotsync_persist_inconsistent.pl
@@ -0,0 +1,96 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Slot synchronization must keep a synced slot temporary until the standby
+# can build a consistent snapshot from its restart_lsn. Since commit
+# 788ec96d591, update_and_persist_local_synced_slot() decides from
+# slotsync_skip_reason, which a no-op sync cycle resets to SS_SKIP_NONE,
+# so a slot that failed the consistency check in one cycle is persisted in
+# the next one. Once the primary slot advances, the next sync of that
+# now-persistent slot hits Assert(persistency == RS_TEMPORARY) at
+# slotsync.c:364.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $primary = PostgreSQL::Test::Cluster->new('primary');
+$primary->init(allows_streaming => 'logical');
+$primary->append_conf('postgresql.conf', "autovacuum = off");
+$primary->start;
+$primary->safe_psql(
+ 'postgres', q{
+ CREATE TABLE t (a int);
+ SELECT pg_create_physical_replication_slot('phys');
+ SELECT pg_create_logical_replication_slot('fslot', 'test_decoding',
+ false, false, true);
+});
+
+$primary->backup('bkp');
+my $connstr = $primary->connstr;
+my $standby = PostgreSQL::Test::Cluster->new('standby');
+$standby->init_from_backup($primary, 'bkp', has_streaming => 1);
+$standby->append_conf(
+ 'postgresql.conf', qq{
+hot_standby_feedback = on
+primary_slot_name = 'phys'
+primary_conninfo = '$connstr dbname=postgres'
+});
+$standby->start;
+$primary->wait_for_replay_catchup($standby);
+
+# Pause replay so that the standby never sees the xid assigned below;
+# the synced slot's catalog_xmin then equals the primary slot's.
+$standby->safe_psql('postgres', 'SELECT pg_wal_replay_pause()');
+
+# An open transaction with an xid, and a running_xacts record listing it.
+my $xact = $primary->background_psql('postgres');
+my $xid = $xact->query_safe('BEGIN; SELECT pg_current_xact_id()');
+
+# Each statement in its own transaction; the second get_changes moves the
+# slot's restart_lsn to the running_xacts record above.
+$primary->safe_psql('postgres', 'SELECT pg_log_standby_snapshot()');
+$primary->safe_psql('postgres', 'INSERT INTO t VALUES (1)');
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_slot_get_changes('fslot', NULL, NULL)") for 1 .. 2;
+
+my ($catalog_xmin, $confirmed_flush) = split /\|/,
+ $primary->safe_psql('postgres',
+ "SELECT catalog_xmin, confirmed_flush_lsn FROM pg_replication_slots WHERE slot_name = 'fslot'"
+ );
+is($catalog_xmin, $xid, 'primary slot restart point lists the open xid');
+
+$standby->poll_query_until('postgres',
+ "SELECT pg_last_wal_receive_lsn() >= '$confirmed_flush'::pg_lsn");
+
+# Synchronize. The call blocks in SnapBuildWaitSnapshot() until the open
+# transaction ends, then fails to reach a consistent point before the
+# confirmed_flush_lsn and retries 200ms later.
+my $log_offset = -s $standby->logfile;
+my $sync = $standby->background_psql('postgres', on_error_stop => 0);
+$sync->query_until(
+ qr/start/, q{
+ \echo start
+ SELECT pg_sync_replication_slots();
+});
+$standby->safe_psql('postgres', 'SELECT pg_wal_replay_resume()');
+$standby->wait_for_log(qr/Waiting for transactions/, $log_offset);
+$xact->quit;
+
+$standby->wait_for_log(qr/could not synchronize replication slot "fslot"/,
+ $log_offset);
+$standby->wait_for_log(
+ qr/newly created replication slot "fslot" is sync-ready now/,
+ $log_offset);
+$sync->quit;
+
+is( $standby->safe_psql(
+ 'postgres',
+ "SELECT temporary FROM pg_replication_slots WHERE slot_name = 'fslot'"
+ ),
+ 't',
+ 'synced slot without a consistent snapshot stays temporary');
+
+done_testing();
--
2.56.0