Subject: Re: Slot's restart_lsn may point to removed WAL segment after hard restart unexpectedly

From: Alexander Korotkov <[email-masked]>
Date: Fri, 4 Apr 2025 06:22:19 +0300

Hi, Vitaly!

On Mon, Mar 3, 2025 at 5:12 PM Vitaly Davydov <[email-masked]> wrote:
> The slot data is flushed to the disk at the beginning of checkpoint. If
> an existing slot is advanced in the middle of checkpoint execution, its
> advanced restart LSN is taken to calculate the oldest LSN for WAL
> segments removal at the end of checkpoint. If the node is restarted just
> after the checkpoint, the slots data will be read from the disk at
> recovery with the oldest restart LSN which can refer to removed WAL
> segments.
>
> The patch introduces a new in-memory state for slots -
> flushed_restart_lsn which is used to calculate the oldest LSN for WAL
> segments removal. This state is updated every time with the current
> restart_lsn at the moment, when the slot is saving to disk.

Thank you for your work on this subject.   I think generally your
approach is correct.  When we're truncating the WAL log, we need to
reply on the position that would be used in the case of server crush.
That is the position flushed to the disk.

While your patch is generality looks good, I'd like make following notes:

1) As ReplicationSlotsComputeRequiredLSN() is called each time we need
to advance the position of WAL needed by replication slots, the usage
pattern probably could be changed.  Thus, we probably need to call
ReplicationSlotsComputeRequiredLSN() somewhere after change of
restart_lsn_flushed while restart_lsn is not changed.  And probably
can skip ReplicationSlotsComputeRequiredLSN() in some cases when only
restart_lsn is changed.
2) I think it's essential to include into the patch test caches which
fail without patch.  You could start from integrating [1] test into
your patch, and then add more similar tests for different situations.

Links.
1.  https://www.postgresql.org/message-id/e3ac0535-e7a2-4a96-9b36-9f765e9cfec5%40vondra.me

------
Regards,
Alexander Korotkov
Supabase

---

From: "Vitaly Davydov" <[email-masked]>
Date: Thu, 24 Apr 2025 17:32:39 +0300

Hi Alexander,

Thank you for the review. I apologize for a late reply. I missed your email.

> can skip ReplicationSlotsComputeRequiredLSN() in some cases when only

Yes, it is a good idea for investigation, thank you! I guess, It may work for
persistent slots but I'm not sure about other types of slots (ephemeral and
temporary). I have no clear understanding of consequences at the moment. I
propose to postpone it for future, because the proposed changes will me more
invasive.

> your patch, and then add more similar tests for different situations.

The problem with TAP tests - it is hard to reproduce without injection points.
The Tomas Vondra's tests create two new injection points. I have to add more
injection points for new tests as well. Injection points help to test the code
but make the code unreadable. I'm not sure, what is the policy of creating
injection points? Personally, I would not like to add new injection points
only to check this particular rare case. I'm trying to create a test without
injection points that should fail occasionally, but I haven't succeeded at
the moment.

I have a question - is there any interest to backport the solution into
existing major releases? I can prepare a patch where restart_lsn_flushed stored
outside of ReplicationSlot structure and doesn't affect the existing API.

With best regards,
Vitaly

---

From: Alexander Korotkov <[email-masked]>
Date: Mon, 28 Apr 2025 18:17:12 +0300

> propose to postpone it for future, because the proposed changes will me more

Yes, that's different for different types of slots.  So, removing
ReplicationSlotsComputeRequiredLSN() doesn't look safe.  But at least,
we need to analyze if we need to add extra calls.

> injection points that should fail occasionally, but I haven't succeeded at

I don't know if there is an explicit policy.  I think we just add them
as needed to reproduce important situations in TAP tests.  So, feel
free to them as many as you want to reproduce all the problematic
situations.  During review we can find if they seem too many, but
don't bother about this at present stage.

> existing major releases?

As long as this is the bug, it should be backpatched to all supported
affected releases.

> I can prepare a patch where restart_lsn_flushed stored

Yes, please!

------
Regards,
Alexander Korotkov
Supabase

---

From: Masahiko Sawada <[email-masked]>
Date: Mon, 28 Apr 2025 18:02:55 -0700

> As long as this is the bug, it should be backpatched to all supported

Yes, but I think we cannot back-patch the proposed fix to back
branches as it changes the ReplicationSlot struct defined in slot.h,
which breaks ABI compatibility.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com

---

From: Alexander Korotkov <[email-masked]>
Date: Tue, 29 Apr 2025 04:39:23 +0300

> which breaks ABI compatibility.

Yes, and I think Vitaly already proposed to address this issue.  This
aspect also needs to be carefully reviewed for sure.

------
Regards,
Alexander Korotkov
Supabase

---

From: Masahiko Sawada <[email-masked]>
Date: Tue, 29 Apr 2025 10:25:00 -0700

> > outside of ReplicationSlot structure and doesn't affect the existing API.

Oh, I totally missed this part. Sorry for making noise. I'll review
the patch once submitted.

Regarding the proposed patch, I think we can somewhat follow
last_saved_confirmed_flush field of ReplicationSlot. For example, we
can set restart_lsn_flushed when restoring the slot from the disk,
etc.

Regards,

---

From: "Vitaly Davydov" <[email-masked]>
Date: Fri, 02 May 2025 20:47:48 +0300

Dear All,

Thank you for the attention to the patch. I updated a patch with a better
solution for the master branch which can be easily backported to the other
branches as we agree on the final solution.

Two tests are introduced which are based on Tomas Vondra's test for logical slots
with injection points from the discussion (patches: [1], [2], [3]). Tests
are implemented as module tests in src/test/modules/test_replslot_required_lsn
directory. I slightly modified the original test for logical slots and created a
new simpler test for physical slots without any additional injection points.

I prepared a new solution (patch [4]) which is also based on Tomas Vondra's
proposal. With a fresh eye, I realized that it can fix the issue as well. It is
easy and less invasive to implement. The new solution differs from my original
solution: it is backward compatible (doesn't require any changes in ReplicationSlot
structure). My original solution can be backward compatible as well if to
allocate flushed_restart_lsn in a separate array in shmem, not in the
ReplicationSlot structure, but I believe the new solution is the better one. If
you still think that my previous solution is the better (I don't think so), I
will prepare a backward compatible patch with my previous solution.

I also proposed one more commit (patch [5]) which removes unnecessary calls of
ReplicationSlotsComputeRequiredLSN function which seems to be redundant. This
function updates the oldest required LSN for slots and it is called every time
when slots' restart_lsn is changed. Once, we use the oldest required LSN in
CreateCheckPoint/CreateRestartPoint to remove old WAL segments, I believe, there
is no need to calculate the oldest value immediately when the slot is advancing
and in other cases when restart_lsn is changed. It may affect on
GetWALAvailability function because the oldest required LSN will be not up to
date, but this function seems to be used in the system view
pg_get_replication_slots and doesn't affect the logic of old WAL segments
removal. I also have some doubts concerning advancing of logical replication
slots: the call of ReplicationSlotsComputeRequiredLSN was removed. Not sure, how
it can affect on ReplicationSlotsComputeRequiredXmin. This commit is not
necessary for the fix but I think it is worth to consider. It may be dropped or
applied only to the master branch.

This patch can be easily backported to the major release branches. I will
quickly prepare the patches for the major releases as we agree on the final
solution.

I apologize for such too late change in patch when it is already on commitfest.
I'm not well experienced yet in the internals of PostgreSQL at the moment,
sometimes the better solution needs some time to grow. In doing we learn :)

[1] 0001-Add-injection-points-to-test-replication-slot-advanc.v2.patch
[2] 0002-Add-TAP-test-to-check-logical-repl-slot-advance-duri.v2.patch
[3] 0003-Add-TAP-test-to-check-physical-repl-slot-advance-dur.v2.patch
[4] 0004-Keep-WAL-segments-by-slot-s-flushed-restart-LSN.v2.patch
[5] 0005-Remove-redundant-ReplicationSlotsComputeRequiredLSN-.v2.patch

With best regards,
Vitaly
From a8693c3003df7f9850af0be5284bb6f0e7a82fa6 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <[email-masked]>
Date: Wed, 30 Apr 2025 12:48:27 +0300
Subject: [PATCH 3/5] Add TAP test to check physical repl slot advance during
 checkpoint

The test verifies that the physical replication slot is still valid
after immediate restart on checkpoint completion in case when the slot
was advanced during checkpoint.

Discussion: https://www.postgresql.org/message-id/flat/1d12d2-67235980-35-19a406a0%4063439497
---
 .../test_replslot_required_lsn/meson.build    |   3 +-
 .../t/002_physical_slot.pl                    | 126 ++++++++++++++++++
 2 files changed, 128 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/test_replslot_required_lsn/t/002_physical_slot.pl

diff --git a/src/test/modules/test_replslot_required_lsn/meson.build b/src/test/modules/test_replslot_required_lsn/meson.build
index 999c16201fb..44d2546632b 100644
--- a/src/test/modules/test_replslot_required_lsn/meson.build
+++ b/src/test/modules/test_replslot_required_lsn/meson.build
@@ -9,7 +9,8 @@ tests += {
        'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
     },
     'tests': [
-      't/001_logical_slot.pl'
+      't/001_logical_slot.pl',
+      't/002_physical_slot.pl'
     ],
   },
 }
diff --git a/src/test/modules/test_replslot_required_lsn/t/002_physical_slot.pl b/src/test/modules/test_replslot_required_lsn/t/002_physical_slot.pl
new file mode 100644
index 00000000000..f89aec1da32
--- /dev/null
+++ b/src/test/modules/test_replslot_required_lsn/t/002_physical_slot.pl
@@ -0,0 +1,126 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+#
+# This test verifies the case when the physical slot is advanced during
+# checkpoint. The test checks that the physical slot's restart_lsn still refers
+# to an existed WAL segment after immediate restart.
+#
+# Discussion:
+# https://www.postgresql.org/message-id/flat/1d12d2-67235980-35-19a406a0%4063439497
+#
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+my ($node, $result);
+
+$node = PostgreSQL::Test::Cluster->new('mike');
+$node->init();
+$node->append_conf('postgresql.conf',
+	"shared_preload_libraries = 'injection_points'");
+$node->append_conf('postgresql.conf',
+	"wal_level = 'replica'");
+$node->start();
+$node->safe_psql('postgres', q(CREATE EXTENSION injection_points));
+
+# create a simple table to generate data into
+$node->safe_psql('postgres',
+	q{create table t (id serial primary key, b text)});
+
+# create a physical replication slot
+$node->safe_psql('postgres',
+	q{select pg_create_physical_replication_slot('slot_physical', true)});
+
+# advance slot to current position, just to have everything "valid"
+$node->safe_psql('postgres',
+	q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())});
+
+# run checkpoint, to flush current state to disk and set a baseline
+$node->safe_psql('postgres', q{checkpoint});
+
+# insert 2M rows, that's about 260MB (~20 segments) worth of WAL
+$node->safe_psql('postgres',
+	q{insert into t (b) select md5(i::text) from generate_series(1,100000) s(i)});
+
+# advance slot to current position, just to have everything "valid"
+$node->safe_psql('postgres',
+	q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())});
+
+# run another checkpoint, to set a new restore LSN
+$node->safe_psql('postgres', q{checkpoint});
+
+# another 2M rows, that's about 260MB (~20 segments) worth of WAL
+$node->safe_psql('postgres',
+	q{insert into t (b) select md5(i::text) from generate_series(1,1000000) s(i)});
+
+my $restart_lsn_init = $node->safe_psql('postgres',
+	q{select restart_lsn from pg_replication_slots where slot_name = 'slot_physical'});
+chomp($restart_lsn_init);
+note("restart lsn before checkpoint: $restart_lsn_init");
+
+# run another checkpoint, this time in the background, and make it wait
+# on the injection point), so that the checkpoint stops right before
+# removing old WAL segments
+note('starting checkpoint');
+
+my $checkpoint = $node->background_psql('postgres');
+$checkpoint->query_safe(
+	q{select injection_points_attach('checkpoint-before-old-wal-removal','wait')});
+$checkpoint->query_until(qr/starting_checkpoint/,
+q(\echo starting_checkpoint
+checkpoint;
+\q
+));
+
+# wait until the checkpoint stops right before removing WAL segments
+note('waiting for injection_point');
+$node->wait_for_event('checkpointer', 'checkpoint-before-old-wal-removal');
+note('injection_point is reached');
+
+# OK, we're in the right situation,  time to advance the physical slot,
+# which recalculates the required LSN, and then unblock the checkpoint,
+# which removes the WAL still needed by the logical slot
+$node->safe_psql('postgres',
+	q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())});
+
+# Continue checkpoint
+$node->safe_psql('postgres',
+	q{select injection_points_wakeup('checkpoint-before-old-wal-removal')});
+
+my $restart_lsn_old = $node->safe_psql('postgres',
+	q{select restart_lsn from pg_replication_slots where slot_name = 'slot_physical'});
+chomp($restart_lsn_old);
+note("restart lsn before stop: $restart_lsn_old");
+
+# abruptly stop the server (1 second should be enough for the checkpoint
+# to finish, would be better )
+$node->stop('immediate');
+
+$node->start;
+
+# Get the restart_lsn of the slot right after restarting
+my $restart_lsn = $node->safe_psql('postgres',
+	q{select restart_lsn from pg_replication_slots where slot_name = 'slot_physical'});
+chomp($restart_lsn);
+note("restart lsn: $restart_lsn");
+
+# Get wal segment name for slot's restart_lsn
+my $restart_lsn_segment = $node->safe_psql('postgres',
+	"SELECT pg_walfile_name('$restart_lsn'::pg_lsn)");
+chomp($restart_lsn_segment);
+
+# Check if the required wal segment exists
+note("required by slot segment name: $restart_lsn_segment");
+my $datadir = $node->data_dir;
+ok(-f "$datadir/pg_wal/$restart_lsn_segment",
+	"WAL segment $restart_lsn_segment for physical slot's restart_lsn $restart_lsn exists");
+
+done_testing();
-- 
2.34.1

From f17ba2642b7c7ef13163c3e411f3d9218a1faa11 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <[email-masked]>
Date: Wed, 30 Apr 2025 14:09:21 +0300
Subject: [PATCH 4/5] Keep WAL segments by slot's flushed restart LSN

The patch fixes the issue with unexpected removal of old WAL segments
after checkpoint followed by immediate restart. The issue occurs when a
slot is advanced after the start of checkpoint and before old WAL
segments removal at end of checkpoint.

The idea of the patch is to get the minimal restart_lsn at the beginning
of checkpoint (or restart point) creation and use this value when
calculating oldest LSN for WAL segments removal at the end of
checkpoint. This idea was proposed by Tomas Vondra in the discussion.

Discussion:
https://www.postgresql.org/message-id/flat/1d12d2-67235980-35-19a406a0%4063439497
---
 src/backend/access/transam/xlog.c | 37 ++++++++++++++++++++++++-------
 1 file changed, 29 insertions(+), 8 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 1f2256a3b86..79a21e2d088 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -677,7 +677,8 @@ static XLogRecPtr CreateOverwriteContrecordRecord(XLogRecPtr aborted_lsn,
 												  XLogRecPtr pagePtr,
 												  TimeLineID newTLI);
 static void CheckPointGuts(XLogRecPtr checkPointRedo, int flags);
-static void KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo);
+static void KeepLogSeg(XLogRecPtr recptr, XLogRecPtr slotsMinLSN,
+					   XLogSegNo *logSegNo);
 static XLogRecPtr XLogGetReplicationSlotMinimumLSN(void);
 
 static void AdvanceXLInsertBuffer(XLogRecPtr upto, TimeLineID tli,
@@ -7087,6 +7088,7 @@ CreateCheckPoint(int flags)
 	VirtualTransactionId *vxids;
 	int			nvxids;
 	int			oldXLogAllowed = 0;
+	XLogRecPtr	slotsMinReqLSN;
 
 	/*
 	 * An end-of-recovery checkpoint is really a shutdown checkpoint, just
@@ -7315,6 +7317,11 @@ CreateCheckPoint(int flags)
 	 */
 	END_CRIT_SECTION();
 
+	/*
+	 * Get the current minimum LSN to be used later in WAL segments cleanup.
+	 */
+	slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
+
 	/*
 	 * In some cases there are groups of actions that must all occur on one
 	 * side or the other of a checkpoint record. Before flushing the
@@ -7507,17 +7514,20 @@ CreateCheckPoint(int flags)
 	 * prevent the disk holding the xlog from growing full.
 	 */
 	XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
-	KeepLogSeg(recptr, &_logSegNo);
+	KeepLogSeg(recptr, slotsMinReqLSN, &_logSegNo);
 	if (InvalidateObsoleteReplicationSlots(RS_INVAL_WAL_REMOVED | RS_INVAL_IDLE_TIMEOUT,
 										   _logSegNo, InvalidOid,
 										   InvalidTransactionId))
 	{
+		slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
+		CheckPointReplicationSlots(shutdown);
+
 		/*
 		 * Some slots have been invalidated; recalculate the old-segment
 		 * horizon, starting again from RedoRecPtr.
 		 */
 		XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
-		KeepLogSeg(recptr, &_logSegNo);
+		KeepLogSeg(recptr, slotsMinReqLSN, &_logSegNo);
 	}
 	_logSegNo--;
 	RemoveOldXlogFiles(_logSegNo, RedoRecPtr, recptr,
@@ -7792,6 +7802,7 @@ CreateRestartPoint(int flags)
 	XLogRecPtr	endptr;
 	XLogSegNo	_logSegNo;
 	TimestampTz xtime;
+	XLogRecPtr	slotsMinReqLSN;
 
 	/* Concurrent checkpoint/restartpoint cannot happen */
 	Assert(!IsUnderPostmaster || MyBackendType == B_CHECKPOINTER);
@@ -7874,6 +7885,11 @@ CreateRestartPoint(int flags)
 	MemSet(&CheckpointStats, 0, sizeof(CheckpointStats));
 	CheckpointStats.ckpt_start_t = GetCurrentTimestamp();
 
+	/*
+	 * Get the current minimum LSN to be used later in WAL segments cleanup.
+	 */
+	slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
+
 	if (log_checkpoints)
 		LogCheckpointStart(flags, true);
 
@@ -7962,17 +7978,20 @@ CreateRestartPoint(int flags)
 	receivePtr = GetWalRcvFlushRecPtr(NULL, NULL);
 	replayPtr = GetXLogReplayRecPtr(&replayTLI);
 	endptr = (receivePtr < replayPtr) ? replayPtr : receivePtr;
-	KeepLogSeg(endptr, &_logSegNo);
+	KeepLogSeg(endptr, slotsMinReqLSN, &_logSegNo);
 	if (InvalidateObsoleteReplicationSlots(RS_INVAL_WAL_REMOVED | RS_INVAL_IDLE_TIMEOUT,
 										   _logSegNo, InvalidOid,
 										   InvalidTransactionId))
 	{
+		slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
+		CheckPointReplicationSlots(flags & CHECKPOINT_IS_SHUTDOWN);
+
 		/*
 		 * Some slots have been invalidated; recalculate the old-segment
 		 * horizon, starting again from RedoRecPtr.
 		 */
 		XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
-		KeepLogSeg(endptr, &_logSegNo);
+		KeepLogSeg(endptr, slotsMinReqLSN, &_logSegNo);
 	}
 	_logSegNo--;
 
@@ -8067,6 +8086,7 @@ GetWALAvailability(XLogRecPtr targetLSN)
 	XLogSegNo	oldestSegMaxWalSize;	/* oldest segid kept by max_wal_size */
 	XLogSegNo	oldestSlotSeg;	/* oldest segid kept by slot */
 	uint64		keepSegs;
+	XLogRecPtr	slotsMinReqLSN;
 
 	/*
 	 * slot does not reserve WAL. Either deactivated, or has never been active
@@ -8080,8 +8100,9 @@ GetWALAvailability(XLogRecPtr targetLSN)
 	 * oldestSlotSeg to the current segment.
 	 */
 	currpos = GetXLogWriteRecPtr();
+	slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
 	XLByteToSeg(currpos, oldestSlotSeg, wal_segment_size);
-	KeepLogSeg(currpos, &oldestSlotSeg);
+	KeepLogSeg(currpos, slotsMinReqLSN, &oldestSlotSeg);
 
 	/*
 	 * Find the oldest extant segment file. We get 1 until checkpoint removes
@@ -8142,7 +8163,7 @@ GetWALAvailability(XLogRecPtr targetLSN)
  * invalidation is optionally done here, instead.
  */
 static void
-KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
+KeepLogSeg(XLogRecPtr recptr, XLogRecPtr slotsMinReqLSN, XLogSegNo *logSegNo)
 {
 	XLogSegNo	currSegNo;
 	XLogSegNo	segno;
@@ -8155,7 +8176,7 @@ KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
 	 * Calculate how many segments are kept by slots first, adjusting for
 	 * max_slot_wal_keep_size.
 	 */
-	keep = XLogGetReplicationSlotMinimumLSN();
+	keep = slotsMinReqLSN;
 	if (keep != InvalidXLogRecPtr && keep < recptr)
 	{
 		XLByteToSeg(keep, segno, wal_segment_size);
-- 
2.34.1

From 68b16da5448ec64661319bca07939e07066fe2a6 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Thu, 21 Nov 2024 20:37:00 +0100
Subject: [PATCH 1/5] Add injection points to test replication slot advance

New injection points:

* checkpoint-before-old-wal-removal - triggered in the checkpointer
  process just before old WAL segments cleanup.

* logical-replication-slot-advance-segment - triggered in
  LogicalConfirmReceivedLocation when restart_lsn was changed enough to
  point to a next WAL segment.

Original patch by: Tomas Vondra <[email-masked]>
Modified by: Vitaly Davydov <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/1d12d2-67235980-35-19a406a0%4063439497
---
 src/backend/access/transam/xlog.c         |  4 ++++
 src/backend/replication/logical/logical.c | 18 ++++++++++++++++++
 2 files changed, 22 insertions(+)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 2d4c346473b..1f2256a3b86 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7498,6 +7498,10 @@ CreateCheckPoint(int flags)
 	if (PriorRedoPtr != InvalidXLogRecPtr)
 		UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);
 
+#ifdef USE_INJECTION_POINTS
+	INJECTION_POINT("checkpoint-before-old-wal-removal");
+#endif
+
 	/*
 	 * Delete old log files, those no longer needed for last checkpoint to
 	 * prevent the disk holding the xlog from growing full.
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index a8d2e024d34..2163dc5e275 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -30,6 +30,7 @@
 
 #include "access/xact.h"
 #include "access/xlogutils.h"
+#include "access/xlog_internal.h"
 #include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -41,6 +42,7 @@
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/builtins.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 
@@ -1825,9 +1827,13 @@ LogicalConfirmReceivedLocation(XLogRecPtr lsn)
 	{
 		bool		updated_xmin = false;
 		bool		updated_restart = false;
+		XLogRecPtr	restart_lsn pg_attribute_unused();
 
 		SpinLockAcquire(&MyReplicationSlot->mutex);
 
+		/* remember the old restart lsn */
+		restart_lsn = MyReplicationSlot->data.restart_lsn;
+
 		MyReplicationSlot->data.confirmed_flush = lsn;
 
 		/* if we're past the location required for bumping xmin, do so */
@@ -1869,6 +1875,18 @@ LogicalConfirmReceivedLocation(XLogRecPtr lsn)
 		/* first write new xmin to disk, so we know what's up after a crash */
 		if (updated_xmin || updated_restart)
 		{
+#ifdef USE_INJECTION_POINTS
+			XLogSegNo	seg1,
+						seg2;
+
+			XLByteToSeg(restart_lsn, seg1, wal_segment_size);
+			XLByteToSeg(MyReplicationSlot->data.restart_lsn, seg2, wal_segment_size);
+
+			/* trigger injection point, but only if segment changes */
+			if (seg1 != seg2)
+				INJECTION_POINT("logical-replication-slot-advance-segment");
+#endif
+
 			ReplicationSlotMarkDirty();
 			ReplicationSlotSave();
 			elog(DEBUG1, "updated xmin: %u restart: %u", updated_xmin, updated_restart);
-- 
2.34.1

From 5836ce690d6167d4f79181cc3af0531245969df6 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <[email-masked]>
Date: Thu, 1 May 2025 12:18:52 +0300
Subject: [PATCH 5/5] Remove redundant ReplicationSlotsComputeRequiredLSN calls

The function ReplicationSlotsComputeRequiredLSN is used to calculate the
oldest slots' required LSN. It is called every time when restart_lsn
value of any slot is changed (for example, when a slot is advancing).
The slot's oldest required LSN is used to remote old WAL segments in two
places - when checkpoint or restart point is created (CreateCheckPoint,
CreateRestartPoint functions). Old WAL segments seems to be truncated in
these two functions only.

The idea of the patch is to call ReplicationSlotsComputeRequiredLSN in
CreateCheckPoint or CreateRestartPoint functions only, before call of
RemoveOldXlogFiles function where old WAL segments are removed. There
is no obvious need to recalculate oldest required LSN every time when a
slot's restart_lsn is changed.

The value of the oldest required lsn can affect on slot invalidation.
The function InvalidateObsoleteReplicationSlots with non zero second
parameter (oldestSegno) is called in CreateCheckPoint,
CreateRestartPoint functions only where slot invalidation occurs with
reason RS_INVAL_WAL_REMOVED. Once we update the oldest slots' required
lsn in the beginning of these functions, the proposed patch should not
break the behaviour of slot invalidation function in this case.
---
 src/backend/access/transam/xlog.c          | 4 ++++
 src/backend/replication/logical/logical.c  | 1 -
 src/backend/replication/logical/slotsync.c | 4 ----
 src/backend/replication/slot.c             | 5 -----
 src/backend/replication/slotfuncs.c        | 2 --
 src/backend/replication/walsender.c        | 1 -
 6 files changed, 4 insertions(+), 13 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 79a21e2d088..5875b5f7b9c 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7320,6 +7320,7 @@ CreateCheckPoint(int flags)
 	/*
 	 * Get the current minimum LSN to be used later in WAL segments cleanup.
 	 */
+	ReplicationSlotsComputeRequiredLSN();
 	slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
 
 	/*
@@ -7519,6 +7520,7 @@ CreateCheckPoint(int flags)
 										   _logSegNo, InvalidOid,
 										   InvalidTransactionId))
 	{
+		ReplicationSlotsComputeRequiredLSN();
 		slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
 		CheckPointReplicationSlots(shutdown);
 
@@ -7888,6 +7890,7 @@ CreateRestartPoint(int flags)
 	/*
 	 * Get the current minimum LSN to be used later in WAL segments cleanup.
 	 */
+	ReplicationSlotsComputeRequiredLSN();
 	slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
 
 	if (log_checkpoints)
@@ -7983,6 +7986,7 @@ CreateRestartPoint(int flags)
 										   _logSegNo, InvalidOid,
 										   InvalidTransactionId))
 	{
+		ReplicationSlotsComputeRequiredLSN();
 		slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
 		CheckPointReplicationSlots(flags & CHECKPOINT_IS_SHUTDOWN);
 
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 2163dc5e275..e796023033c 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -1905,7 +1905,6 @@ LogicalConfirmReceivedLocation(XLogRecPtr lsn)
 			SpinLockRelease(&MyReplicationSlot->mutex);
 
 			ReplicationSlotsComputeRequiredXmin(false);
-			ReplicationSlotsComputeRequiredLSN();
 		}
 	}
 	else
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 656e66e0ae0..30662c09275 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -335,7 +335,6 @@ update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid,
 		SpinLockRelease(&slot->mutex);
 
 		ReplicationSlotsComputeRequiredXmin(false);
-		ReplicationSlotsComputeRequiredLSN();
 	}
 
 	return updated_config || updated_xmin_or_lsn;
@@ -502,9 +501,6 @@ reserve_wal_for_local_slot(XLogRecPtr restart_lsn)
 		slot->data.restart_lsn = restart_lsn;
 		SpinLockRelease(&slot->mutex);
 
-		/* Prevent WAL removal as fast as possible */
-		ReplicationSlotsComputeRequiredLSN();
-
 		XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size);
 
 		/*
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 600b87fa9cb..dd18fe10f7d 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -1008,7 +1008,6 @@ ReplicationSlotDropPtr(ReplicationSlot *slot)
 	 * limits.
 	 */
 	ReplicationSlotsComputeRequiredXmin(false);
-	ReplicationSlotsComputeRequiredLSN();
 
 	/*
 	 * If removing the directory fails, the worst thing that will happen is
@@ -1494,9 +1493,6 @@ ReplicationSlotReserveWal(void)
 		slot->data.restart_lsn = restart_lsn;
 		SpinLockRelease(&slot->mutex);
 
-		/* prevent WAL removal as fast as possible */
-		ReplicationSlotsComputeRequiredLSN();
-
 		/*
 		 * If all required WAL is still there, great, otherwise retry. The
 		 * slot should prevent further removal of WAL, unless there's a
@@ -2014,7 +2010,6 @@ restart:
 	if (invalidated)
 	{
 		ReplicationSlotsComputeRequiredXmin(false);
-		ReplicationSlotsComputeRequiredLSN();
 	}
 
 	return invalidated;
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 36cc2ed4e44..3300fb9b1c9 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -583,7 +583,6 @@ pg_replication_slot_advance(PG_FUNCTION_ARGS)
 	 * advancing potentially done.
 	 */
 	ReplicationSlotsComputeRequiredXmin(false);
-	ReplicationSlotsComputeRequiredLSN();
 
 	ReplicationSlotRelease();
 
@@ -819,7 +818,6 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot)
 
 		ReplicationSlotMarkDirty();
 		ReplicationSlotsComputeRequiredXmin(false);
-		ReplicationSlotsComputeRequiredLSN();
 		ReplicationSlotSave();
 
 #ifdef USE_ASSERT_CHECKING
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 9fa8beb6103..0767c2803d9 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -2384,7 +2384,6 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	if (changed)
 	{
 		ReplicationSlotMarkDirty();
-		ReplicationSlotsComputeRequiredLSN();
 		PhysicalWakeupLogicalWalSnd();
 	}
 
-- 
2.34.1

From 2a5ead4a45c9624eace2dbad63f18ca76c307db6 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Thu, 21 Nov 2024 23:07:22 +0100
Subject: [PATCH 2/5] Add TAP test to check logical repl slot advance during
 checkpoint

The test verifies that logical replication slot is still valid after
immediate restart on checkpoint completion in case when the slot was
advanced during checkpoint.

Original patch by: Tomas Vondra <[email-masked]>
Modified by: Vitaly Davydov <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/1d12d2-67235980-35-19a406a0%4063439497
---
 src/test/modules/Makefile                     |   4 +-
 src/test/modules/meson.build                  |   1 +
 .../test_replslot_required_lsn/Makefile       |  18 +++
 .../test_replslot_required_lsn/meson.build    |  15 +++
 .../t/001_logical_slot.pl                     | 124 ++++++++++++++++++
 5 files changed, 160 insertions(+), 2 deletions(-)
 create mode 100644 src/test/modules/test_replslot_required_lsn/Makefile
 create mode 100644 src/test/modules/test_replslot_required_lsn/meson.build
 create mode 100644 src/test/modules/test_replslot_required_lsn/t/001_logical_slot.pl

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index aa1d27bbed3..53d3dd8e0ed 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -46,9 +46,9 @@ SUBDIRS = \

 ifeq ($(enable_injection_points),yes)
-SUBDIRS += injection_points gin typcache
+SUBDIRS += injection_points gin typcache test_replslot_required_lsn
 else
-ALWAYS_SUBDIRS += injection_points gin typcache
+ALWAYS_SUBDIRS += injection_points gin typcache test_replslot_required_lsn
 endif
 
 ifeq ($(with_ssl),openssl)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 9de0057bd1d..ac0dbd1f10f 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -43,3 +43,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_replslot_required_lsn')
diff --git a/src/test/modules/test_replslot_required_lsn/Makefile b/src/test/modules/test_replslot_required_lsn/Makefile
new file mode 100644
index 00000000000..e5ff8af255b
--- /dev/null
+++ b/src/test/modules/test_replslot_required_lsn/Makefile
@@ -0,0 +1,18 @@
+# src/test/modules/test_replslot_required_lsn/Makefile
+
+EXTRA_INSTALL=src/test/modules/injection_points \
+	contrib/test_decoding
+
+export enable_injection_points
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_replslot_required_lsn
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_replslot_required_lsn/meson.build b/src/test/modules/test_replslot_required_lsn/meson.build
new file mode 100644
index 00000000000..999c16201fb
--- /dev/null
+++ b/src/test/modules/test_replslot_required_lsn/meson.build
@@ -0,0 +1,15 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+tests += {
+  'name': 'test_replslot_required_lsn',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
+    'tests': [
+      't/001_logical_slot.pl'
+    ],
+  },
+}
diff --git a/src/test/modules/test_replslot_required_lsn/t/001_logical_slot.pl b/src/test/modules/test_replslot_required_lsn/t/001_logical_slot.pl
new file mode 100644
index 00000000000..ff13c741ad0
--- /dev/null
+++ b/src/test/modules/test_replslot_required_lsn/t/001_logical_slot.pl
@@ -0,0 +1,124 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+#
+# This test verifies the case when the logical slot is advanced during
+# checkpoint. The test checks that the logical slot's restart_lsn still refers
+# to an existed WAL segment after immediate restart.
+#
+# Discussion:
+# https://www.postgresql.org/message-id/flat/1d12d2-67235980-35-19a406a0%4063439497
+#
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+my ($node, $result);
+
+$node = PostgreSQL::Test::Cluster->new('mike');
+$node->init;
+$node->append_conf('postgresql.conf',
+	"shared_preload_libraries = 'injection_points'");
+$node->append_conf('postgresql.conf',
+	"wal_level = 'logical'");
+$node->start;
+$node->safe_psql('postgres', q(CREATE EXTENSION injection_points));
+
+# create a simple table to generate data into
+$node->safe_psql('postgres',
+	q{create table t (id serial primary key, b text)});
+
+# create the two slots we'll need
+$node->safe_psql('postgres',
+	q{select pg_create_logical_replication_slot('slot_logical', 'test_decoding')});
+$node->safe_psql('postgres',
+	q{select pg_create_physical_replication_slot('slot_physical', true)});
+
+# advance both to current position, just to have everything "valid"
+$node->safe_psql('postgres',
+	q{select count(*) from pg_logical_slot_get_changes('slot_logical', null, null)});
+$node->safe_psql('postgres',
+	q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())});
+
+# run checkpoint, to flush current state to disk and set a baseline
+$node->safe_psql('postgres', q{checkpoint});
+
+# generate transactions to get RUNNING_XACTS
+my $xacts = $node->background_psql('postgres');
+$xacts->query_until(qr/run_xacts/,
+q(\echo run_xacts
+SELECT 1 \watch 0.1
+\q
+));
+
+# insert 2M rows, that's about 260MB (~20 segments) worth of WAL
+$node->safe_psql('postgres',
+	q{insert into t (b) select md5(i::text) from generate_series(1,1000000) s(i)});
+
+# run another checkpoint, to set a new restore LSN
+$node->safe_psql('postgres', q{checkpoint});
+
+# another 2M rows, that's about 260MB (~20 segments) worth of WAL
+$node->safe_psql('postgres',
+	q{insert into t (b) select md5(i::text) from generate_series(1,1000000) s(i)});
+
+# run another checkpoint, this time in the background, and make it wait
+# on the injection point), so that the checkpoint stops right before
+# removing old WAL segments
+print('starting checkpoint\n');
+
+my $checkpoint = $node->background_psql('postgres');
+$checkpoint->query_safe(q(select injection_points_attach('checkpoint-before-old-wal-removal','wait')));
+$checkpoint->query_until(qr/starting_checkpoint/,
+q(\echo starting_checkpoint
+checkpoint;
+\q
+));
+
+print('waiting for injection_point\n');
+# wait until the checkpoint stops right before removing WAL segments
+$node->wait_for_event('checkpointer', 'checkpoint-before-old-wal-removal');
+
+
+# try to advance the logical slot, but make it stop when it moves to the
+# next WAL segment (has to happen in the background too)
+my $logical = $node->background_psql('postgres');
+$logical->query_safe(q{select injection_points_attach('logical-replication-slot-advance-segment','wait');});
+$logical->query_until(qr/get_changes/,
+q(
+\echo get_changes
+select count(*) from pg_logical_slot_get_changes('slot_logical', null, null) \watch 1
+\q
+));
+
+# wait until the checkpoint stops right before removing WAL segments
+$node->wait_for_event('client backend', 'logical-replication-slot-advance-segment');
+
+# OK, we're in the right situation,  time to advance the physical slot,
+# which recalculates the required LSN, and then unblock the checkpoint,
+# which removes the WAL still needed by the logical slot
+$node->safe_psql('postgres',
+	q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())});
+
+$node->safe_psql('postgres',
+	q{select injection_points_wakeup('checkpoint-before-old-wal-removal')});
+
+# abruptly stop the server (1 second should be enough for the checkpoint
+# to finish, would be better )
+$node->stop('immediate');
+
+$node->start;
+
+eval {
+	$node->safe_psql('postgres', q{select count(*) from pg_logical_slot_get_changes('slot_logical', null, null);});
+};
+is($@, '', "Logical slot still valid");
+
+done_testing();
-- 
2.34.1

---

From: Alexander Korotkov <[email-masked]>
Date: Mon, 19 May 2025 00:25:36 +0300

Hi Vitaly!

> new simpler test for physical slots without any additional injection points.

The patchset doesn't seem to build after 371f2db8b0, which adjusted
the signature of the INJECTION_POINT() macro.  Could you, please,
update the patchset accordingly.

> will prepare a backward compatible patch with my previous solution.

I see in 0004 patch we're calling XLogGetReplicationSlotMinimumLSN()
before slots synchronization then use it for WAL truncation.
Generally looks good.  But what about the "if
(InvalidateObsoleteReplicationSlots(...))" branch?  It calls
XLogGetReplicationSlotMinimumLSN() again.  Why would the value
obtained from the latter call reflect slots as they are synchronized
to the disk?

------
Regards,
Alexander Korotkov
Supabase

---

From: "Vitaly Davydov" <[email-masked]>
Date: Tue, 20 May 2025 18:44:44 +0300

Hi Alexander,

Thank you very much for the review!

> update the patchset accordingly.

I've updated the patch (see attached). Thanks.

> obtained from the latter call reflect slots as they are synchronized

In patch 0004 I call XLogGetReplicationSlotMinimumLSN() again to keep the old
behaviour - this function was called in KeepLogSeg prior to my change. I also
call CheckPointReplicationSlots at the next line to save the invalidated and
other dirty slots on disk again to make sure, the new oldest LSN is in sync.

The problem I tried to solve in this if-branch is to fix test
src/test/recovery/t/019_replslot_limit.pl which was failed because the WAL was
not truncated enought for the test to pass ok. In general, this branch is not
necessary and we may fix the test by calling checkpoint twice (please, see the
alternative.rej patch for this case). If you think, we should incorporate this
new change, I'm ok to do it. But the WAL will be truncating more lazily.

Furthermore, I think we can save slots on disk right after invalidation, not in
CheckPointGuts to avoid saving invalidated slots twice.

With best regards,
Vitaly
From 41eed2a90d68f4d9ac1ee3d00c89879358d19fd1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Thu, 21 Nov 2024 23:07:22 +0100
Subject: [PATCH 2/5] Add TAP test to check logical repl slot advance during
 checkpoint

 

From 88078156f67aabeaa91d226ca4967c827dee977f Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <[email-masked]>
Date: Wed, 30 Apr 2025 14:09:21 +0300
Subject: [PATCH 4/5] Keep WAL segments by slot's flushed restart LSN

Discussion:

index 47ffc0a2307..9c0f9a0af28 100644

 	/*
 
+	/*

+	/*

 
 	/*
 
 	/*

From 2131771f97fc3497906568612f9fdda027238d42 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <[email-masked]>
Date: Wed, 30 Apr 2025 12:48:27 +0300
Subject: [PATCH 3/5] Add TAP test to check physical repl slot advance during
 checkpoint

From 7a7f0c8404bee31291f7cd0c07b307b04805aab1 Mon Sep 17 00:00:00 2001
From: Vitaly Davydov <[email-masked]>
Date: Thu, 1 May 2025 12:18:52 +0300
Subject: [PATCH 5/5] Remove redundant ReplicationSlotsComputeRequiredLSN calls

index 9c0f9a0af28..624be87a609 100644
 
 	/*

 	if (log_checkpoints)
 
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index de4d86afa22..bc88ccb0207 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -1917,7 +1917,6 @@ LogicalConfirmReceivedLocation(XLogRecPtr lsn)
 			SpinLockRelease(&MyReplicationSlot->mutex);

 

 		/*
 
 	/*

 	return invalidated;

 

-- 
2.34.1

From 372dca207ea1275aa21b95c7bc5b8e07f71b075e Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Thu, 21 Nov 2024 20:37:00 +0100
Subject: [PATCH 1/5] Add injection points to test replication slot advance

New injection points:

index 1914859b2ee..47ffc0a2307 100644
 
+#ifdef USE_INJECTION_POINTS
+	INJECTION_POINT("checkpoint-before-old-wal-removal", NULL);
+#endif
+
 	/*
 	 * Delete old log files, those no longer needed for last checkpoint to
 	 * prevent the disk holding the xlog from growing full.
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 1d56d0c4ef3..de4d86afa22 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -30,6 +30,7 @@
 
 #include "access/xact.h"

 
+		/* remember the old restart lsn */
+		restart_lsn = MyReplicationSlot->data.restart_lsn;
+
 		/*
 		 * Prevent moving the confirmed_flush backwards, as this could lead to
 		 * data duplication issues caused by replicating already replicated
@@ -1881,6 +1887,18 @@ LogicalConfirmReceivedLocation(XLogRecPtr lsn)
 		/* first write new xmin to disk, so we know what's up after a crash */
 		if (updated_xmin || updated_restart)
 		{
+#ifdef USE_INJECTION_POINTS
+			XLogSegNo	seg1,
+						seg2;
+
+			XLByteToSeg(restart_lsn, seg1, wal_segment_size);
+			XLByteToSeg(MyReplicationSlot->data.restart_lsn, seg2, wal_segment_size);
+
+			/* trigger injection point, but only if segment changes */
+			if (seg1 != seg2)
+				INJECTION_POINT("logical-replication-slot-advance-segment", NULL);
+#endif
+
 			ReplicationSlotMarkDirty();
 			ReplicationSlotSave();
 			elog(DEBUG1, "updated xmin: %u restart: %u", updated_xmin, updated_restart);
-- 
2.34.1

---

From: Alexander Korotkov <[email-masked]>
Date: Fri, 23 May 2025 00:10:27 +0300

Hi, Vitaly!

> CheckPointGuts to avoid saving invalidated slots twice.

Thank you for the clarification.  It's all good.  I just missed that
CheckPointReplicationSlots() syncs slots inside the "if" branch.

I've reordered the patchset.  Fix should come first, tests comes
second.  So, tests pass after each commit.  Also I've joined both
tests and injection points into single commit.  I don't see reason to
place tests into src/test/modules, because there is no module.  I've
moved them into src/test/recovery.

I also improved some comments and commit messages.  I think 0001
should go to all supported releases as it fixes material bug, while
0002 should be backpatched to 17, where injection points fist appears.
0003 should go to pg19 after branching.  I'm continuing reviewing
this.

------
Regards,
Alexander Korotkov
Supabase

---

From: Alexander Korotkov <[email-masked]>
Date: Sat, 24 May 2025 13:37:52 +0300

> 0003 should go to pg19 after branching.  I'm continuing reviewing

I spend more time on this.  The next revision is attached.  It
contains revised comments and other cosmetic changes.  I'm going to
backpatch 0001 to all supported branches, and 0002 to 17 where
injection points were introduced.

------
Regards,
Alexander Korotkov
Supabase

---

From: Amit Kapila <[email-masked]>
Date: Sat, 24 May 2025 16:44:49 +0530

On Sat, May 24, 2025 at 4:08 PM Alexander Korotkov <[email-masked]> wrote:
> backpatch 0001 to all supported branches,
>

Is my understanding correct that we need 0001 because
PhysicalConfirmReceivedLocation() doesn't save the slot to disk after
changing the slot's restart_lsn? If so, shouldn't the comments (One
could argue that the slot should be saved to disk now, but that'd be
energy wasted - the worst thing lost information could cause here is
to give wrong information in a statistics view) in
PhysicalConfirmReceivedLocation() be changed to mention the risk of
not saving the slot?

Also, after 0001, even the same solution will be true for logical
slots as well, where we are already careful to save the slot to disk
if its restart_lsn is changed, see LogicalConfirmReceivedLocation().
So, won't that effort be wasted? Even if we don't want to do anything
about it (which doesn't sound like a good idea), we should note that
in comments somewhere.

-- 
With Regards,
Amit Kapila.

---

From: Alexander Korotkov <[email-masked]>
Date: Sat, 24 May 2025 16:28:55 +0300

Hi, Amit!

Thank you for your attention to this patchset!

On Sat, May 24, 2025 at 2:15 PM Amit Kapila <[email-masked]> wrote:
> changing the slot's restart_lsn?

Yes.  Also, even if it would save slot to the disk, there is still
race condition that concurrent checkpoint could use updated value from
the shared memory to clean old WAL segments, and then crash happens
before we managed to write the slot to the disk.

> If so, shouldn't the comments (One

I have added the comments about both points in the attached revision
of the patchset.

------
Regards,
Alexander Korotkov
Supabase

---

From: Amit Kapila <[email-masked]>
Date: Mon, 26 May 2025 12:19:00 +0530

> before we managed to write the slot to the disk.

How can that happen, if we first write the updated value to disk and
then update the shared memory as we do in
LogicalConfirmReceivedLocation?

BTW, won't there be a similar problem with physical slot's xmin
computation as well? In PhysicalReplicationSlotNewXmin(), after
updating the slot's xmin computation, we mark it as dirty and update
shared memory values. Now, say after checkpointer writes these xmin
values to disk, walsender receives another feedback message and
updates the slot's xmin values. Now using these updated shared memory
values, vacuum removes the rows, however, a restart will show the
older xmin values in the slot, which mean vacuum would have removed
the required rows before restart.

As per my understanding, neither the xmin nor the LSN problem exists
for logical slots. I am pointing this out to indicate we may need to
think of a different solution for physical slots, if these are
problems only for physical slots.

-- 
With Regards,
Amit Kapila.

---

From: Alexander Korotkov <[email-masked]>
Date: Mon, 26 May 2025 11:57:03 +0300

> LogicalConfirmReceivedLocation?

I don't see this to be true for LogicalConfirmReceivedLocation() and
restart_lsn.  We clearly don't update restart_lsn in shared memory
after flush.  It we previously proposed to resolve a problem for
restart_lsn like this, but that approach breaks ABI and couldn't be
backpatched.

> the required rows before restart.

I don't yet see why this should be a problem.  feedbackXmin provides a
barrier for vacuum, but unlike restart_lsn it doesn't refers removed
tuples as the resource.  If vacuum would remove some tuples based on
the last feedback message that tuples are not needed by replica.  If
after restart we would have outdated feedbackXmin that would make our
vacuum even more conservative for a while, but I don't see how that
would lead to material problem.  On contrast you can see in
046_logical_slot.pl how lack of restart_lsn synchronization leads to
an error while attempting to decode the changes, because the current
code expects WAL at restart_lsn to exist.

> problems only for physical slots.

There is indeed a problem for logical slots.  You can apply 0002 patch
alone and test for logical slots will fail.

------
Regards,
Alexander Korotkov
Supabase

---

From: "Vitaly Davydov" <[email-masked]>
Date: Mon, 26 May 2025 13:22:51 +0300

Dear Alexander, Amit, All

> Amit wrote:

I guess, that the problem with logical slots still exist. Please, see the tap
test: src/test/recovery/t/046_logical_slot.pl from the v6 version of the patch.
A race condition may happen when logical slot's restart_lsn was changed but not
yet written to the disk. Imagine, there is another physical slot which is
advanced at this moment. It recomputes oldest min LSN and takes into account
changed but not saved to disk restart_lsn of the logical slot. We come to the
situation when the WAL segment for the logical slot's restart_lsn may be
truncated after immediate restart.

I'm not sure what may happen with two checkpoints which execute in parallel, but
I would say that the patch 0001 guarantees that every checkpoint run will trim
the WAL segments based on the already saved on disk restart LSNs of slots. The
rule to trim the WAL by saved slot's restart_lsn will not be violated.

> Amit wrote:

As I've already told, it indirectly affects the logical slots as well.

> Alexander wrote:

Alexander, thank you for polishing the patch. Just my opinion, I would prefer
to put tests before the fix due to reason that you can reproduce the problem
when simply checkout the commit with tests. Once, the tests are after the fix
you are not able to do this way. Anyway, I'm ok with your changes. Thank you!

I did some changes in the patch (v7 is attached):
* Removed modules/test_replslot_required_lsn directory. It is not needed anymore,
once you've moved test files to another directory.
* Renamed tests to 046_checkpoint_logical_slot.pl, 047_checkpoint_physical_slot.pl.
I believe, such names are more descriptive.

Please, consider these changes.

With best regards,
Vitaly
From b85ffd56eb1e28d5e61e6221ba97e7e3bea7a982 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sat, 24 May 2025 13:34:36 +0300
Subject: [PATCH 3/3] Remove redundant ReplicationSlotsComputeRequiredLSN calls

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 0a7f7a71d8b..bdfd0a59ab7 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7324,6 +7324,7 @@ CreateCheckPoint(int flags)
 	 * might be advanced concurrently, so we call this before
 	 * CheckPointReplicationSlots() synchronizes replication slots.
 	 */
+	ReplicationSlotsComputeRequiredLSN();
 	slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
 
 	/*
@@ -7528,6 +7529,7 @@ CreateCheckPoint(int flags)
 		 * cleanup.  Then, we must synchronize the replication slots again in
 		 * order to make this LSN safe to use.
 		 */
+		ReplicationSlotsComputeRequiredLSN();
 		slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
 		CheckPointReplicationSlots(shutdown);
 
@@ -7901,6 +7903,7 @@ CreateRestartPoint(int flags)
 	 * might be advanced concurrently, so we call this before
 	 * CheckPointReplicationSlots() synchronizes replication slots.
 	 */
+	ReplicationSlotsComputeRequiredLSN();
 	slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
 
 	if (log_checkpoints)
@@ -8001,6 +8004,7 @@ CreateRestartPoint(int flags)
 		 * cleanup.  Then, we must synchronize the replication slots again in
 		 * order to make this LSN safe to use.
 		 */
+		ReplicationSlotsComputeRequiredLSN();
 		slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
 		CheckPointReplicationSlots(flags & CHECKPOINT_IS_SHUTDOWN);
 
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 081e6593722..34e973393c2 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -1925,7 +1925,6 @@ LogicalConfirmReceivedLocation(XLogRecPtr lsn)
 			SpinLockRelease(&MyReplicationSlot->mutex);

 

 		/*
 
 	/*

 	return invalidated;

 
 
index d751d34295d..dff749f00a8 100644
 
-- 
2.34.1

From c978cc88848615670fce667c83cda3fe874d80c0 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sat, 24 May 2025 13:26:28 +0300
Subject: [PATCH 2/3] Add TAP tests to check replication slot advance during
 the checkpoint

The new tests verify that logical and physical replication slots are still
valid after an immediate restart on checkpoint completion when the slot was
advanced during the checkpoint.

This commit introduces two new injection points to make these tests possible:

* checkpoint-before-old-wal-removal - triggered in the checkpointer process
  just before old WAL segments cleanup;
* logical-replication-slot-advance-segment - triggered in
  LogicalConfirmReceivedLocation() when restart_lsn was changed enough to
  point to the next WAL segment.

Discussion: https://postgr.es/m/flat/1d12d2-67235980-35-19a406a0%4063439497
Author: Vitaly Davydov <[email-masked]>
Author: Tomas Vondra <[email-masked]>
Reviewed-by: Alexander Korotkov <[email-masked]>
Backpatch-through: 17
---
 src/backend/access/transam/xlog.c             |   4 +
 src/backend/replication/logical/logical.c     |  18 +++
 src/test/recovery/meson.build                 |   2 +
 .../recovery/t/046_checkpoint_logical_slot.pl | 139 ++++++++++++++++++
 .../t/047_checkpoint_physical_slot.pl         | 133 +++++++++++++++++
 5 files changed, 296 insertions(+)
 create mode 100644 src/test/recovery/t/046_checkpoint_logical_slot.pl
 create mode 100644 src/test/recovery/t/047_checkpoint_physical_slot.pl

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index a0e589e9c4b..0a7f7a71d8b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7509,6 +7509,10 @@ CreateCheckPoint(int flags)
 	if (PriorRedoPtr != InvalidXLogRecPtr)
 		UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);
 
index 6b3995133e2..081e6593722 100644
@@ -29,6 +29,7 @@
 #include "postgres.h"
 
 #include "access/xact.h"

 
+		/* remember the old restart lsn */
+		restart_lsn = MyReplicationSlot->data.restart_lsn;
+
 		/*
 		 * Prevent moving the confirmed_flush backwards, as this could lead to
 		 * data duplication issues caused by replicating already replicated
@@ -1889,6 +1895,18 @@ LogicalConfirmReceivedLocation(XLogRecPtr lsn)
 		 */
 		if (updated_xmin || updated_restart)
 		{
+#ifdef USE_INJECTION_POINTS
+			XLogSegNo	seg1,
+						seg2;
+
+			XLByteToSeg(restart_lsn, seg1, wal_segment_size);
+			XLByteToSeg(MyReplicationSlot->data.restart_lsn, seg2, wal_segment_size);
+
+			/* trigger injection point, but only if segment changes */
+			if (seg1 != seg2)
+				INJECTION_POINT("logical-replication-slot-advance-segment", NULL);
+#endif
+
 			ReplicationSlotMarkDirty();
 			ReplicationSlotSave();
 			elog(DEBUG1, "updated xmin: %u restart: %u", updated_xmin, updated_restart);
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index cb983766c67..92429d28402 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -54,6 +54,8 @@ tests += {
       't/043_no_contrecord_switch.pl',
       't/044_invalidate_inactive_slots.pl',
       't/045_archive_restartpoint.pl',
+      't/046_checkpoint_logical_slot.pl',
+      't/047_checkpoint_physical_slot.pl'
     ],
   },
 }
diff --git a/src/test/recovery/t/046_checkpoint_logical_slot.pl b/src/test/recovery/t/046_checkpoint_logical_slot.pl
new file mode 100644
index 00000000000..b4265c4a6a5
--- /dev/null
+++ b/src/test/recovery/t/046_checkpoint_logical_slot.pl
@@ -0,0 +1,139 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+#
+# This test verifies the case when the logical slot is advanced during
+# checkpoint. The test checks that the logical slot's restart_lsn still refers
+# to an existed WAL segment after immediate restart.
+#
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+my ($node, $result);
+
+$node = PostgreSQL::Test::Cluster->new('mike');
+$node->init;
+$node->append_conf('postgresql.conf',
+	"shared_preload_libraries = 'injection_points'");
+$node->append_conf('postgresql.conf', "wal_level = 'logical'");
+$node->start;
+$node->safe_psql('postgres', q(CREATE EXTENSION injection_points));
+
+# Create a simple table to generate data into.
+$node->safe_psql('postgres',
+	q{create table t (id serial primary key, b text)});
+
+# Create the two slots we'll need.
+$node->safe_psql('postgres',
+	q{select pg_create_logical_replication_slot('slot_logical', 'test_decoding')}
+);
+$node->safe_psql('postgres',
+	q{select pg_create_physical_replication_slot('slot_physical', true)});
+
+# Advance both slots to the current position just to have everything "valid".
+$node->safe_psql('postgres',
+	q{select count(*) from pg_logical_slot_get_changes('slot_logical', null, null)}
+);
+$node->safe_psql('postgres',
+	q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())}
+);
+
+# Run checkpoint to flush current state to disk and set a baseline.
+$node->safe_psql('postgres', q{checkpoint});
+
+# Generate some transactions to get RUNNING_XACTS.
+my $xacts = $node->background_psql('postgres');
+$xacts->query_until(
+	qr/run_xacts/,
+	q(\echo run_xacts
+SELECT 1 \watch 0.1
+\q
+));
+
+# Insert 2M rows; that's about 260MB (~20 segments) worth of WAL.
+$node->safe_psql('postgres',
+	q{insert into t (b) select md5(i::text) from generate_series(1,1000000) s(i)}
+);
+
+# Run another checkpoint to set a new restore LSN.
+$node->safe_psql('postgres', q{checkpoint});
+
+# Another 2M rows; that's about 260MB (~20 segments) worth of WAL.
+$node->safe_psql('postgres',
+	q{insert into t (b) select md5(i::text) from generate_series(1,1000000) s(i)}
+);
+
+# Run another checkpoint, this time in the background, and make it wait
+# on the injection point) so that the checkpoint stops right before
+# removing old WAL segments.
+note('starting checkpoint\n');
+
+my $checkpoint = $node->background_psql('postgres');
+$checkpoint->query_safe(
+	q(select injection_points_attach('checkpoint-before-old-wal-removal','wait'))
+);
+$checkpoint->query_until(
+	qr/starting_checkpoint/,
+	q(\echo starting_checkpoint
+checkpoint;
+\q
+));
+
+# Wait until the checkpoint stops right before removing WAL segments.
+note('waiting for injection_point\n');
+$node->wait_for_event('checkpointer', 'checkpoint-before-old-wal-removal');
+note('injection_point is reached');
+
+# Try to advance the logical slot, but make it stop when it moves to the next
+# WAL segment (this has to happen in the background, too).
+my $logical = $node->background_psql('postgres');
+$logical->query_safe(
+	q{select injection_points_attach('logical-replication-slot-advance-segment','wait');}
+);
+$logical->query_until(
+	qr/get_changes/,
+	q(
+\echo get_changes
+select count(*) from pg_logical_slot_get_changes('slot_logical', null, null) \watch 1
+\q
+));
+
+# Wait until the slot's restart_lsn points to the next WAL segment.
+note('waiting for injection_point\n');
+$node->wait_for_event('client backend',
+	'logical-replication-slot-advance-segment');
+note('injection_point is reached');
+
+# OK, we're in the right situation: time to advance the physical slot, which
+# recalculates the required LSN, and then unblock the checkpoint, which
+# removes the WAL still needed by the logical slot.
+$node->safe_psql('postgres',
+	q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())}
+);
+
+# Continue the checkpoint.
+$node->safe_psql('postgres',
+	q{select injection_points_wakeup('checkpoint-before-old-wal-removal')});
+
+# Abruptly stop the server (1 second should be enough for the checkpoint
+# to finish; it would be better).
+$node->stop('immediate');
+
+$node->start;
+
+eval {
+	$node->safe_psql('postgres',
+		q{select count(*) from pg_logical_slot_get_changes('slot_logical', null, null);}
+	);
+};
+is($@, '', "Logical slot still valid");
+
+done_testing();
diff --git a/src/test/recovery/t/047_checkpoint_physical_slot.pl b/src/test/recovery/t/047_checkpoint_physical_slot.pl
new file mode 100644
index 00000000000..454e56b9bd2
--- /dev/null
+++ b/src/test/recovery/t/047_checkpoint_physical_slot.pl
@@ -0,0 +1,133 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+#
+# This test verifies the case when the physical slot is advanced during
+# checkpoint. The test checks that the physical slot's restart_lsn still refers
+# to an existed WAL segment after immediate restart.
+#
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+
+use Test::More;
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+my ($node, $result);
+
+$node = PostgreSQL::Test::Cluster->new('mike');
+$node->init;
+$node->append_conf('postgresql.conf',
+	"shared_preload_libraries = 'injection_points'");
+$node->append_conf('postgresql.conf', "wal_level = 'replica'");
+$node->start;
+$node->safe_psql('postgres', q(CREATE EXTENSION injection_points));
+
+# Create a simple table to generate data into.
+$node->safe_psql('postgres',
+	q{create table t (id serial primary key, b text)});
+
+# Create a physical replication slot.
+$node->safe_psql('postgres',
+	q{select pg_create_physical_replication_slot('slot_physical', true)});
+
+# Advance slot to the current position, just to have everything "valid".
+$node->safe_psql('postgres',
+	q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())}
+);
+
+# Run checkpoint to flush current state to disk and set a baseline.
+$node->safe_psql('postgres', q{checkpoint});
+
+# Insert 2M rows; that's about 260MB (~20 segments) worth of WAL.
+$node->safe_psql('postgres',
+	q{insert into t (b) select md5(i::text) from generate_series(1,100000) s(i)}
+);
+
+# Advance slot to the current position, just to have everything "valid".
+$node->safe_psql('postgres',
+	q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())}
+);
+
+# Run another checkpoint to set a new restore LSN.
+$node->safe_psql('postgres', q{checkpoint});
+
+# Another 2M rows; that's about 260MB (~20 segments) worth of WAL.
+$node->safe_psql('postgres',
+	q{insert into t (b) select md5(i::text) from generate_series(1,1000000) s(i)}
+);
+
+my $restart_lsn_init = $node->safe_psql('postgres',
+	q{select restart_lsn from pg_replication_slots where slot_name = 'slot_physical'}
+);
+chomp($restart_lsn_init);
+note("restart lsn before checkpoint: $restart_lsn_init");
+
+# Run another checkpoint, this time in the background, and make it wait
+# on the injection point) so that the checkpoint stops right before
+# removing old WAL segments.
+note('starting checkpoint');
+
+my $checkpoint = $node->background_psql('postgres');
+$checkpoint->query_safe(
+	q{select injection_points_attach('checkpoint-before-old-wal-removal','wait')}
+);
+$checkpoint->query_until(
+	qr/starting_checkpoint/,
+	q(\echo starting_checkpoint
+checkpoint;
+\q
+));
+
+# Wait until the checkpoint stops right before removing WAL segments.
+note('waiting for injection_point');
+$node->wait_for_event('checkpointer', 'checkpoint-before-old-wal-removal');
+note('injection_point is reached');
+
+# OK, we're in the right situation: time to advance the physical slot, which
+# recalculates the required LSN and then unblock the checkpoint, which
+# removes the WAL still needed by the physical slot.
+$node->safe_psql('postgres',
+	q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())}
+);
+
+# Continue the checkpoint.
+$node->safe_psql('postgres',
+	q{select injection_points_wakeup('checkpoint-before-old-wal-removal')});
+
+my $restart_lsn_old = $node->safe_psql('postgres',
+	q{select restart_lsn from pg_replication_slots where slot_name = 'slot_physical'}
+);
+chomp($restart_lsn_old);
+note("restart lsn before stop: $restart_lsn_old");
+
+# Abruptly stop the server (1 second should be enough for the checkpoint
+# to finish; it would be better).
+$node->stop('immediate');
+
+$node->start;
+
+# Get the restart_lsn of the slot right after restarting.
+my $restart_lsn = $node->safe_psql('postgres',
+	q{select restart_lsn from pg_replication_slots where slot_name = 'slot_physical'}
+);
+chomp($restart_lsn);
+note("restart lsn: $restart_lsn");
+
+# Get the WAL segment name for the slot's restart_lsn.
+my $restart_lsn_segment = $node->safe_psql('postgres',
+	"SELECT pg_walfile_name('$restart_lsn'::pg_lsn)");
+chomp($restart_lsn_segment);
+
+# Check if the required wal segment exists.
+note("required by slot segment name: $restart_lsn_segment");
+my $datadir = $node->data_dir;
+ok( -f "$datadir/pg_wal/$restart_lsn_segment",
+	"WAL segment $restart_lsn_segment for physical slot's restart_lsn $restart_lsn exists"
+);
+
+done_testing();
-- 
2.34.1

From 1e0629efc65f190a58ec729db6f3ada4f8b83897 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email-masked]>
Date: Sat, 24 May 2025 16:26:27 +0300
Subject: [PATCH 1/3] Keep WAL segments by the flushed value of the slot's
 restart LSN

The patch fixes the issue with the unexpected removal of old WAL segments
after checkpoint, followed by an immediate restart. The issue occurs when
a slot is advanced after the start of the checkpoint and before old WAL
segments are removed at the end of the checkpoint.

The idea of the patch is to get the minimal restart_lsn at the beginning
of checkpoint (or restart point) creation and use this value when calculating
the oldest LSN for WAL segments removal at the end of checkpoint. This idea
was proposed by Tomas Vondra in the discussion.

Discussion: https://postgr.es/m/flat/1d12d2-67235980-35-19a406a0%4063439497
Author: Vitaly Davydov <[email-masked]>
Reviewed-by: Tomas Vondra <[email-masked]>
Reviewed-by: Alexander Korotkov <[email-masked]>
Backpatch-through: 13
---
 src/backend/access/transam/xlog.c         | 55 +++++++++++++++++++----
 src/backend/replication/logical/logical.c | 10 ++++-
 src/backend/replication/walsender.c       |  4 ++
 3 files changed, 60 insertions(+), 9 deletions(-)

index 1914859b2ee..a0e589e9c4b 100644

 	/*
 	 * An end-of-recovery checkpoint is really a shutdown checkpoint, just
@@ -7315,6 +7317,15 @@ CreateCheckPoint(int flags)
 	 */
 	END_CRIT_SECTION();
 
+	/*
+	 * Get the current minimum LSN to be used later in the WAL segment
+	 * cleanup.  We may clean up only WAL segments, which are not needed
+	 * according to synchronized LSNs of replication slots.  The slot's LSN
+	 * might be advanced concurrently, so we call this before
+	 * CheckPointReplicationSlots() synchronizes replication slots.
+	 */
+	slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
+
 	/*
 	 * In some cases there are groups of actions that must all occur on one
 	 * side or the other of a checkpoint record. Before flushing the
@@ -7503,17 +7514,25 @@ CreateCheckPoint(int flags)
 	 * prevent the disk holding the xlog from growing full.
 	 */
 	XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
-	KeepLogSeg(recptr, &_logSegNo);
+	KeepLogSeg(recptr, slotsMinReqLSN, &_logSegNo);
 	if (InvalidateObsoleteReplicationSlots(RS_INVAL_WAL_REMOVED | RS_INVAL_IDLE_TIMEOUT,
 										   _logSegNo, InvalidOid,
 										   InvalidTransactionId))
 	{
+		/*
+		 * Recalculate the current minimum LSN to be used in the WAL segment
+		 * cleanup.  Then, we must synchronize the replication slots again in
+		 * order to make this LSN safe to use.
+		 */
+		slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
+		CheckPointReplicationSlots(shutdown);
+
 		/*
 		 * Some slots have been invalidated; recalculate the old-segment
 		 * horizon, starting again from RedoRecPtr.
 		 */
 		XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
-		KeepLogSeg(recptr, &_logSegNo);
+		KeepLogSeg(recptr, slotsMinReqLSN, &_logSegNo);
 	}
 	_logSegNo--;
 	RemoveOldXlogFiles(_logSegNo, RedoRecPtr, recptr,
@@ -7788,6 +7807,7 @@ CreateRestartPoint(int flags)
 	XLogRecPtr	endptr;
 	XLogSegNo	_logSegNo;
 	TimestampTz xtime;
+	XLogRecPtr	slotsMinReqLSN;
 
@@ -7870,6 +7890,15 @@ CreateRestartPoint(int flags)
 
+	/*
+	 * Get the current minimum LSN to be used later in the WAL segment
+	 * cleanup.  We may clean up only WAL segments, which are not needed
+	 * according to synchronized LSNs of replication slots.  The slot's LSN
+	 * might be advanced concurrently, so we call this before
+	 * CheckPointReplicationSlots() synchronizes replication slots.
+	 */
+	slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
+
 	if (log_checkpoints)
 		LogCheckpointStart(flags, true);
 
@@ -7958,17 +7987,25 @@ CreateRestartPoint(int flags)
 	receivePtr = GetWalRcvFlushRecPtr(NULL, NULL);
 	replayPtr = GetXLogReplayRecPtr(&replayTLI);
 	endptr = (receivePtr < replayPtr) ? replayPtr : receivePtr;
-	KeepLogSeg(endptr, &_logSegNo);
+	KeepLogSeg(endptr, slotsMinReqLSN, &_logSegNo);
 	if (InvalidateObsoleteReplicationSlots(RS_INVAL_WAL_REMOVED | RS_INVAL_IDLE_TIMEOUT,
 										   _logSegNo, InvalidOid,
 										   InvalidTransactionId))
 	{
+		/*
+		 * Recalculate the current minimum LSN to be used in the WAL segment
+		 * cleanup.  Then, we must synchronize the replication slots again in
+		 * order to make this LSN safe to use.
+		 */
+		slotsMinReqLSN = XLogGetReplicationSlotMinimumLSN();
+		CheckPointReplicationSlots(flags & CHECKPOINT_IS_SHUTDOWN);
+
 		/*
 		 * Some slots have been invalidated; recalculate the old-segment
 		 * horizon, starting again from RedoRecPtr.
 		 */
 		XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
-		KeepLogSeg(endptr, &_logSegNo);
+		KeepLogSeg(endptr, slotsMinReqLSN, &_logSegNo);
 	}
 	_logSegNo--;
 
@@ -8063,6 +8100,7 @@ GetWALAvailability(XLogRecPtr targetLSN)
 	XLogSegNo	oldestSegMaxWalSize;	/* oldest segid kept by max_wal_size */
 	XLogSegNo	oldestSlotSeg;	/* oldest segid kept by slot */
 	uint64		keepSegs;
+	XLogRecPtr	slotsMinReqLSN;
 
 	/*
@@ -8076,8 +8114,9 @@ GetWALAvailability(XLogRecPtr targetLSN)
 
 	/*
 	 * Find the oldest extant segment file. We get 1 until checkpoint removes
@@ -8138,7 +8177,7 @@ GetWALAvailability(XLogRecPtr targetLSN)
  * invalidation is optionally done here, instead.
  */
 static void
-KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
+KeepLogSeg(XLogRecPtr recptr, XLogRecPtr slotsMinReqLSN, XLogSegNo *logSegNo)
 {
 	XLogSegNo	currSegNo;
 	XLogSegNo	segno;
@@ -8151,7 +8190,7 @@ KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo)
 	 * Calculate how many segments are kept by slots first, adjusting for
 	 * max_slot_wal_keep_size.
 	 */
-	keep = XLogGetReplicationSlotMinimumLSN();
+	keep = slotsMinReqLSN;
 	if (keep != InvalidXLogRecPtr && keep < recptr)
 	{
 		XLByteToSeg(keep, segno, wal_segment_size);
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 1d56d0c4ef3..6b3995133e2 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -1878,7 +1878,15 @@ LogicalConfirmReceivedLocation(XLogRecPtr lsn)

-		/* first write new xmin to disk, so we know what's up after a crash */
+		/*
+		 * First, write new xmin and restart_lsn to disk so we know what's up
+		 * after a crash.  Even when we do this, the checkpointer can see the
+		 * updated restart_lsn value in the shared memory; then, a crash can
+		 * happen before we manage to write that value to the disk.  Thus,
+		 * checkpointer still needs to make special efforts to keep WAL
+		 * segments required by the restart_lsn written to the disk.  See
+		 * CreateCheckPoint() and CreateRestartPoint() for details.
+		 */
 		if (updated_xmin || updated_restart)
 		{
 			ReplicationSlotMarkDirty();
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 9fa8beb6103..d751d34295d 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -2393,6 +2393,10 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn)
 	 * be energy wasted - the worst thing lost information could cause here is
 	 * to give wrong information in a statistics view - we'll just potentially
 	 * be more conservative in removing files.
+	 *
+	 * Checkpointer makes special efforts to keep the WAL segments required by
+	 * the restart_lsn written to the disk. See CreateCheckPoint() and
+	 * CreateRestartPoint() for details.
 	 */
 }
 
-- 
2.34.1

---

From: Amit Kapila <[email-masked]>
Date: Mon, 26 May 2025 17:13:46 +0530

> truncated after immediate restart.

Okay, so I was missing the point that the physical slots can consider
the updated value of the logical slot's restart_lsn. The point I was
advocating for logical slots sanctity was when no physical slots are
involved. When updating replicationSlotMinLSN value in shared memory,
the logical slot machinery took care that the value we use should be
flushed to disk. One can argue that we should improve physical slots
machinery so that it also takes care to write the slot to disk before
updating the replicationSlotMinLSN, which is used to remove WAL. I
understand that the downside is physical slots will be written to disk
with a greater frequency, which will not be good from the performance
point of view, but can we think of doing it for the period when a
checkpoint is in progress? OTOH, if we don't want to adjust physical
slot machinery, it seems saving the logical slots to disk immediately
when its restart_lsn is updated is a waste of effort after your patch,
no? If so, why are we okay with that?

I understand that your proposed patch fixes the reported problem but I
am slightly afraid that the proposed solution is not a good idea w.r.t
logical slots so I am trying to see if there are any other alternative
ideas to fix this issue.

-- 
With Regards,
Amit Kapila.

---

From: "Vitaly Davydov" <[email-masked]>
Date: Mon, 26 May 2025 18:34:07 +0300

Dear Amit,

> OTOH, if we don't want to adjust physical

I agree, that saving logical slots at advance is a possible waste of effort. But
I don't understand original ideas behind it. I haven't touched it to make
the minimal patch which should not break the existing functionality.

We trim WAL in checkpoint (or restart point) operation only. The slots'
restart_lsn is used to keep the wal from truncation. I believe, we need to
compute the slots' oldest lsn as the minimal value of restart_lsn values only
when executing checkpoint (or restart point). I guess, it doesn't depend on
slot's type (logical or physical). We have 0003 patch to fix it.

I haven't deeply investigated yet slot's xmin values but I guess the xmin values
are a different story than restart_lsn. It is used to avoid tuple deletions by
vacuum and it is updated by a different way. I can't say that
LogicalConfirmReceivedLocation is the right place to update saved on disk xmin
values. I would propose to update these values in SaveSlotToPath under some lock
to avoid concurrent reads of unsaved values or do in a checkpoint like as for
restart_lsn. We may investigate and improve it in an another patch.

With best regards,
Vitaly

---

From: Alexander Korotkov <[email-masked]>
Date: Mon, 26 May 2025 20:06:23 +0300

On Mon, May 26, 2025 at 2:43 PM Amit Kapila <[email-masked]> wrote:
> checkpoint is in progress?

That could cause replication slowdown while checkpointing is
in-progress.  This is certainly better than slowing down the
replication permanently, but still doesn't look good.

> no? If so, why are we okay with that?

I don't think so.  I think the reason why logical slots are synced to
disk immediately after update is that logical changes are not
idempotent (you can't safely apply the same change twice) unlike
physical block-level changes.  This is why logical slots need to be
synced to prevent double replication of same changes, which could
lead, for example, to double insertion.

> logical slots so I am trying to see if there are any other alternative

I don't understand exact concerns about this fix.  For sure, we can
try to implement a fix hacking LogicalConfirmReceivedLocation() and
PhysicalConfirmReceivedLocation().  But that would be way more
cumbersome, especially if we have to keep ABI compatibility.  Also, it
doesn't seem to me that either LogicalConfirmReceivedLocation() or
PhysicalConfirmReceivedLocation() currently try to address this issue:
LogicalConfirmReceivedLocation() implements immediate sync for
different reasons.

------
Regards,
Alexander Korotkov
Supabase

---

From: Amit Kapila <[email-masked]>
Date: Tue, 27 May 2025 09:38:45 +0530

> lead, for example, to double insertion.

Hmm, if this has to be true, then even in the else branch of
LogicalConfirmReceivedLocation [1], we should have saved the slot.
AFAIU, whether the logical changes are sent to the client is decided
based on two things: (a) the replication origins, which tracks
replication progress and are maintained by clients (which for built-in
replication are subscriber nodes), see [2]; and (b) confirmed_flush
LSN maintained in the slot by the server. Now, for each ack by the
client after applying/processing changes, we update the
confirmed_flush LSN of the slot but don't immediately flush it. This
shouldn't let us send the changes again because even if the system
crashes and restarts, the client will send the server the location to
start sending the changes from based on its origin tracking. There is
more to it, like there are cases when confirm_flush LSN in the slot
could be ahead the origin's LSN, and we handle all such cases, but I
don't think those are directly related here, so I am skipping those
details for now.

Note that LogicalConfirmReceivedLocation won't save the slot to disk
if it updates only confirmed_flush LSN, which is used to decide
whether to send the changes.

> LogicalConfirmReceivedLocation() implements immediate sync for

I may be missing something, but let's discuss some more before we conclude this.

[1]:
else
{

/*
* Prevent moving the confirmed_flush backwards. See comments above
* for the details.
*/
if (lsn > MyReplicationSlot->data.confirmed_flush)
MyReplicationSlot->data.confirmed_flush = lsn;

[2]: https://www.postgresql.org/docs/devel/replication-origins.html

-- 
With Regards,
Amit Kapila.

---

From: Alexander Korotkov <[email-masked]>
Date: Tue, 27 May 2025 12:12:41 +0300

> whether to send the changes.

You're right, I didn't study these aspects careful enough.

> I may be missing something, but let's discuss some more before we conclude this.

So, yes probably LogicalConfirmReceivedLocation() tries to care about
keeping all WAL segments after the synchronized value of restart_lsn.
But it just doesn't care about concurrent
ReplicationSlotsComputeRequiredLSN().  In order to fix that logic, we
need effective_restart_lsn field by analogy to effective_catalog_xmin
(similar approach was discussed in this thread before).  But that
would require ABI compatibility breakage.

So, I'd like to propose following: backpatch 0001 and 0002, but
implement effective_restart_lsn field for pg19.  What do you think?

------
Regards,
Alexander Korotkov
Supabase

---

From: Alexander Korotkov <[email-masked]>
Date: Tue, 27 May 2025 12:18:12 +0300

> implement effective_restart_lsn field for pg19.  What do you think?

Possibly we could implement effective_restart_lsn even for pg18.  As I
know, keeping ABI compatibility is not required for beta.

------
Regards,
Alexander Korotkov
Supabase

---

From: Amit Kapila <[email-masked]>
Date: Tue, 27 May 2025 16:55:53 +0530

> know, keeping ABI compatibility is not required for beta.

Yeah, we should be able to change ABI during beta, but I can't comment
on the idea of effective_restart_lsn without seeing the patch or a
detailed explanation of this idea.

Now, you see my point related to restart_lsn computation for logical
slots, it is better to also do some analysis of the problem related to
xmin I have highlighted in one of my previous emails [1]. I see your
response to it, but I feel someone needs to give it a try by writing a
test and see the behavior. I am saying because logical slots took
precaution of flushing to disk before updating shared values of xmin
for a reason, whereas similar precautions are not taken for physical
slots, so there could be a problem with that computation as well.

[1]: https://www.postgresql.org/message-id/CAA4eK1KMaPA5jir_SFu%2Bqr3qu55OOdFWVZpuUkqTSGZ9fyPpHA%40mail.gmail.com
-- 
With Regards,
Amit Kapila.

---

From: Alexander Korotkov <[email-masked]>
Date: Thu, 29 May 2025 14:59:39 +0300

> detailed explanation of this idea.

Could you, please, check the patch [1].  It implements this idea
except it names new field restart_lsn_flushed instead of
effective_restart_lsn.

> slots, so there could be a problem with that computation as well.

I see LogicalConfirmReceivedLocation() performs correctly while
updating effective_catalog_xmin only after syncing the slot to the
disk.  I don't see how effective_xmin gets updates with the logical
replication progress though.  Could you get me some clue on this,
please?

Links.
1. https://www.postgresql.org/message-id/1538a2-67c5c700-7-77ec5a80%40179382871

------
Regards,
Alexander Korotkov
Supabase

---

From: Amit Kapila <[email-masked]>
Date: Mon, 2 Jun 2025 17:23:43 +0530

> except it names new field restart_lsn_flushed instead of

This appears to be a better direction than the other patch, at least
for HEAD. I noticed a few points while looking at the patch.

1. restart_lsn_flushed: Can we name it as last_saved_restart_lsn based
on existing variable last_saved_confirmed_flush?
2. There are no comments as to why this is considered only for
persistent slots when CheckPointReplicationSlots doesn't have any such
check.
3. Please see if it makes sense to copy it in the copy_replication_slot.

Apart from these, I am not sure if there are still any pending
comments in the thread to be handled for this patch, so please see to
avoid missing anything.

> replication progress though.  Could you get me some clue on this,

As per my understanding, for logical slots, effective_xmin is only set
during the initial copy phase (or say if one has to export a
snapshot), after that, its value won't change. Please read the
comments in CreateInitDecodingContext() where we set its value. If you
still have questions about it, we can discuss further.

-- 
With Regards,
Amit Kapila.

---

From: Alexander Korotkov <[email-masked]>
Date: Tue, 3 Jun 2025 16:21:19 +0300

> on existing variable last_saved_confirmed_flush?

Good point, renamed.

> persistent slots when CheckPointReplicationSlots doesn't have any such

Relevant comments added.

> 3. Please see if it makes sense to copy it in the copy_replication_slot.

Thank you for pointing, but I don't think this is necessary.
copy_replication_slot() calls ReplicationSlotSave(), which updates
last_saved_restart_lsn.

Also, I've changed ReplicationSlotsComputeRequiredLSN() call to
CheckPointReplicationSlots() to update required LSN after
SaveSlotToPath() updated last_saved_restart_lsn.  This helps to pass
checks in 001_stream_rep.pl without additional hacks.

> still have questions about it, we can discuss further.

OK, thank you for the clarification.   I've read the comments in
CreateInitDecodingContext() as you suggested.  All of above makes me
think *_xmin fields are handled properly.

------
Regards,
Alexander Korotkov
Supabase

---

From: "Vitaly Davydov" <[email-masked]>
Date: Thu, 05 Jun 2025 18:21:20 +0300

Dear Alexander, Amit

> checks in 001_stream_rep.pl without additional hacks.

Thank you for the improvement and patch preparation. I confirm the test is
passed without additional hacks now.

I sill do not understand why this solution is favored. It is, in my opinion,
a non backward-compatible solution. In any case, I'm ok to go with this patch.
If needed, I may prepare a backward-compatible solution where
last_saved_restart_lsn values will be in an another place of the shmem, rather
than in ReplicationSlot struct.

I still would like to add my 5 cents to the discussion.

The purpose of the xmin value is to prevent tuples from vacuuming. Slots'
restart_lsn values are used to calculate the oldest lsn to keep WAL segments
from removal in checkpoint. These processes are pretty independent.

The logical slots are advanced in 2 steps. At the first step, the logical
decoding stuff periodically sets consistent candidate values for catalog_xmin and
restart_lsn. At the second step, when LogicalConfirmReceivedLocation is called,
the candidate values are assigned on catalog_xmin and restart_lsn values based
on the confirmed lsn value. The slot is saved with these consistent values.
It is important, that the candidate values are consistent, decoding guarantees
it. In case of a crash, we should guarantee that the loaded from the disk
catalog_xmin and restart_lsn values are consistent and valid for logical slots.
LogicalConfirmReceivedLocation function keeps this consistency by updating them
from consistent candidate values in a single operation.

We have to guarantee that we use saved to disk values to calculate xmin horizon
and slots' oldest lsn. For this purpose, effective_catalog_xmin is used. We
update effective_catalog_xmin in LogicalConfirmReceivedLocation just
after saving slot to disk. Another place where we update effective_catalog_xmin
is when walsender receives hot standby feedback message.

Once, we have two independent processes (vacuuming, checkpoint), we can calculate
xmin horizon and oldest WAL lsn values independently (at different times) from
the saved to disk values. Note, these values are updated in a non atomic way.

The xmin value is set when the node receives hot standby feedback and it is used
to keep tuples from vacuuming as well as catalog_xmin for decoding stuff. Not
sure, xmin is applicable for logical replication.

The confirmed flush lsn is used as a startpoint when a peer node doesn't provide
the start lsn and to check that the start lsn is not older than the latest
confirmed flush lsn. The saving of the slot on disk at each call of
LogicalConfirmReceivedLocation doesn't help to avoid conflicts completely, but
it helps to decrease the probability of conflicts. So, i'm still not sure, we
need to save logical slots on each advance to avoid conflicts, because it
doesn't help in general. The conflicts should be resolved by other means.

Once, we truncate old wal segments in checkpoint only. I believe, it is ok if we
calculate the oldest lsn only at the beginning of the checkpoint, as it was in
the alternative solution. I think, we can update xmin horizon in checkpoint only
but the horizon advancing will be more lazy in this case.

Taking into account these thoughts, I can't see any problems with the alternative
patch where oldest wal lsn is calculated only in checkpoint.

With best regards,
Vitaly

---

From: Amit Kapila <[email-masked]>
Date: Fri, 6 Jun 2025 10:22:52 +0530

> than in ReplicationSlot struct.

I think we can use this approach for HEAD and probably keep the
previous idea for backbranches. Keeping some value in shared_memory
per slot sounds risky to me in terms of introducing new bugs.

> to keep tuples from vacuuming as well as catalog_xmin for decoding stuff.
>

Yeah, but with physical slots, it is possible that the slot's xmin
value is pointing to some value, say 700 (after restart), but vacuum
would have removed tuples from transaction IDs greater than 700 as
explained in email [1].

 Not
> it helps to decrease the probability of conflicts.
>

We don't save slots at each call of LogicalConfirmReceivedLocation()
and when we save also, it is not to avoid conflicts but to avoid
removing required WAL segments and tuples.

 So, i'm still not sure, we
> patch where oldest wal lsn is calculated only in checkpoint.

The alternative will needlessly prevent removing WAL segments in some
cases when logical slots are in use.

[1] - https://www.postgresql.org/message-id/CAA4eK1KMaPA5jir_SFu%2Bqr3qu55OOdFWVZpuUkqTSGZ9fyPpHA%40mail.gmail.com

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Fri, 6 Jun 2025 10:49:13 +0530

> think *_xmin fields are handled properly.

Yes, they handled properly for logical slots, but there is no similar
safety mechanism for physical slots.

One minor comment:
+
+ /* Latest restart_lsn that has been flushed to disk. For persistent slots
+ * the flushed LSN should be taken into account when calculating the oldest

This doesn't follow our practice for multi-line comments.

-- 
With Regards,
Amit Kapila.

---

From: "Vitaly Davydov" <[email-masked]>
Date: Mon, 09 Jun 2025 19:09:58 +0300

Hi Amit,

> per slot sounds risky to me in terms of introducing new bugs.

Not sure, what kind of problems may occur. I propose to allocate in shmem an
array of last_saved_restart_lsn like below which is not a part of the public
api (see below). It will be allocated and deallocated in shmem the same way as
ReplicationSlotCtlData. I can prepare a patch, if needed.

typedef struct ReplicationSlotCtlDataExt {
	XLogRecPtr last_saved_restart_lsn[1];
} ReplicationSlotCtlDataExt;

> would have removed tuples from transaction IDs greater than 700 as

I think, we have no xmin problem for physical slots. The xmin values of
physical slots are used to process HSF messages. If I correctly understood what
you mean, you are telling about the problem which is solved by hot standby
feedback messages. This message is used to disable tuples vacuuming on the
primary to avoid delete conflicts on the replica in queries (some queries may
select some tuples which were vacuumed on the primary and deletions are
replicated to the standby). If the primary receives a HSF message after slot
saving, I believe, it is allowable if autovacuum cleans tuples with xmin later
than the last saved value. If the primary restarts, the older value will be
loaded but the replica already confirmed the newer value. Concerning replica,
it is the obligation of the replica to send such HSF xmin that will survive
replica's immediate restart.

>cases when logical slots are in use.

IMHO, I'm not sure, it will significantly impact the wal removal. We remove WAL
segments only in checkpoint. The alternate solution gets the oldest WAL segment
at the beginning of checkpoint, then saves dirty slots to disk, and removes old
WAL segments at the end of checkpoint using the oldest WAL segment obtained at
the beginning of checkpoint. The alternate solution may not be so effective
in terms of WAL segments removal, if a logical slot is advanced during
checkpoint, but I do not think it is a significant issue. From the other hand,
the alternate solution simplifies the logic of WAL removal, backward compatible
(avoids addition new in-memory states), decreases the number of locks in
ReplicationSlotsComputeRequiredLSN - no need to recalculate oldest slots'
restart lsn every time when a slot is advanced.

With best regards,
Vitaly

---

From: Alexander Korotkov <[email-masked]>
Date: Tue, 10 Jun 2025 23:14:16 +0300

> } ReplicationSlotCtlDataExt;

This could work, but I think this is not a solution for HEAD anyway.
In the HEAD, it would be better to keep everything inside the
ReplicationSlot struct.  In the same time, I don't like idea to have
different shared memory structs between branches if we can avoid that.

> replica's immediate restart.

+1

> restart lsn every time when a slot is advanced.

So, my proposal is to commit the attached patchset to the HEAD, and
commit [1] to the back branches.  Any objections?

Links.
1. https://www.postgresql.org/message-id/CAPpHfdutKQxpm-gJgiZRb2ouKC9%2BHZx3fG3F00zd%3DxdxDidm_g%40mail.gmail.com

------
Regards,
Alexander Korotkov
Supabase

---

From: Amit Kapila <[email-masked]>
Date: Thu, 12 Jun 2025 16:54:36 +0530

> > replica's immediate restart.

The point is about the general principle of slot's xmin values, which
is that the rows with xid greater than slot's xmin should be available
(or can't be removed by vacuum). But here, such a principle could be
violated after a restart. I don't have a test to show what harm it can
cause, but will try to think/investigate more on it.

> commit [1] to the back branches.  Any objections?

No objections. I think we can keep discussing if slot's xmin
computation has any issues or not, but you can proceed with the LSN
stuff.

-- 
With Regards,
Amit Kapila.

---

From: Alexander Lakhin <[email-masked]>
Date: Sun, 15 Jun 2025 12:00:01 +0300

Hello Alexander,

> commit [1] to the back branches.  Any objections?

As the buildfarm animal prion shows [1], the 046_checkpoint_logical_slot
test fails with "-DRELCACHE_FORCE_RELEASE -DCATCACHE_FORCE_RELEASE":
# poll_query_until timed out executing this query:
#
#         SELECT count(*) > 0 FROM pg_stat_activity
#         WHERE backend_type = 'client backend' AND wait_event = 'logical-replication-slot-advance-segment'
#
# expecting this output:
# t
# last actual query output:
# f
# with stderr:
[04:16:27] t/046_checkpoint_logical_slot.pl ......
Dubious, test returned 29 (wstat 7424, 0x1d00)
No subtests run
[04:20:58] t/047_checkpoint_physical_slot.pl ..... ok   271294 ms ( 0.00 usr  0.00 sys +  0.37 cusr  0.26 csys =  0.63 CPU)

I'm able to reproduce this locally as well. Though the test passes for me
with the increased timeout, that is it's not stuck:
PG_TEST_TIMEOUT_DEFAULT=360 PROVE_TESTS="t/046*" make -s check -C src/test/recovery/
# +++ tap check in src/test/recovery +++
t/046_checkpoint_logical_slot.pl .. ok
All tests successful.
Files=1, Tests=1, 533 wallclock secs ( 0.01 usr  0.00 sys +  4.70 cusr  9.61 csys = 14.32 CPU)
Result: PASS

Could you have a look?

[1] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=prion&dt=2025-06-14%2001%3A58%3A06

Best regards,
Alexander

---

From: Alexander Korotkov <[email-masked]>
Date: Sun, 15 Jun 2025 12:50:18 +0300

Hi, Alexander!

> [1] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=prion&dt=2025-06-14%2001%3A58%3A06

Hmm... It seems to take too long to advance the segment with these
options on.  Sure, I'll check this!

------
Regards,
Alexander Korotkov
Supabase

---

From: Alexander Korotkov <[email-masked]>
Date: Sun, 15 Jun 2025 14:02:44 +0300

> [1] https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=prion&dt=2025-06-14%2001%3A58%3A06

Could you, please, check this patch?  On my system it makes 046 and
047 execute in 140 secs with -O0 and -DRELCACHE_FORCE_RELEASE
-DCATCACHE_FORCE_RELEASE.

------
Regards,
Alexander Korotkov
Supabase

---

From: Alexander Lakhin <[email-masked]>
Date: Sun, 15 Jun 2025 18:00:01 +0300

> -DCATCACHE_FORCE_RELEASE.

Thank you for the patch!

It decreases the test's duration significantly:
# +++ tap check in src/test/recovery +++
t/046_checkpoint_logical_slot.pl .. ok
All tests successful.
Files=1, Tests=1, 29 wallclock secs ( 0.01 usr  0.00 sys +  0.23 cusr  0.56 csys =  0.80 CPU)

Without the patch:
t/046_checkpoint_logical_slot.pl .. ok
All tests successful.
Files=1, Tests=1, 519 wallclock secs ( 0.01 usr  0.00 sys +  3.05 cusr  7.64 csys = 10.70 CPU)
Result: PASS

Best regards,
Alexander

---

From: Tom Lane <[email-masked]>
Date: Sun, 15 Jun 2025 12:05:56 -0400

BTW, while you're cleaning up this commit, could you remove the
excess newlines in some of the "note" commands in 046 and 047, like

note('starting checkpoint\n');

This produces bizarre output, as shown in the buildfarm logs:

[04:04:38.953](603.550s) # starting checkpoint\\n

			regards, tom lane

---

From: Alexander Korotkov <[email-masked]>
Date: Mon, 16 Jun 2025 04:58:51 +0300

Hi, Tom!

> This produces bizarre output, as shown in the buildfarm logs:

Thank you for reporting this.  The revised patch is attached.  In
addition to reducing tests runtime, it removes excess newlines from
some note() calls.  The commit message is here.  I'm going to push
this if no objections.

------
Regards,
Alexander Korotkov
Supabase

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Mon, 16 Jun 2025 09:11:27 +0000

Dear Alexander,

Thanks for pushing the fix patch! BTW, I have few comments for your commits.
Can you check and include them if needed?

01.
```
$node->append_conf('postgresql.conf',
	"shared_preload_libraries = 'injection_points'");
```

No need to set shared_preload_libraries in 046/047. ISTM it must be set when we
enable the statistics.

02.
We should also check whether the injection_points can be installed or not.
You can check check_extension() and callers.

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: Alexander Korotkov <[email-masked]>
Date: Mon, 16 Jun 2025 14:16:42 +0300

Dear Kuroda-san,

> You can check check_extension() and callers.

Thank you!  All of these totally make sense.  The updated patch is attached.

------
Regards,
Alexander Korotkov
Supabase

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Mon, 16 Jun 2025 11:44:44 +0000

Dear Alexander,

> Thank you!  All of these totally make sense.  The updated patch is attached.

Thanks for the update. I found another point.

```
-# Another 2M rows; that's about 260MB (~20 segments) worth of WAL.
+# Another 50K rows; that's about 86MB (~5 segments) worth of WAL.
 $node->safe_psql('postgres',
-	q{insert into t (b) select md5(i::text) from generate_series(1,1000000) s(i)}
+	q{insert into t (b) select repeat(md5(i::text),50) from generate_series(1,50000) s(i)}
 );
```

I think a perl function advance_wal() can be used instead of doing actual INSERT
command because no one refers the replicated result. Same thing can be said in
046/047.

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: vignesh C <[email-masked]>
Date: Tue, 17 Jun 2025 08:24:41 +0530

Hi Alexander,

While tracking buildfarm for one of other commits, I noticed this failure:
TRAP: failed Assert("s->data.restart_lsn >=
s->last_saved_restart_lsn"), File:
"../pgsql/src/backend/replication/slot.c", Line: 1813, PID: 3945797
postgres: standby: checkpointer (ExceptionalCondition+0x83) [0x55fa69b79f5e]
postgres: standby: checkpointer
(InvalidateObsoleteReplicationSlots+0x53c) [0x55fa69982171]
postgres: standby: checkpointer (CreateCheckPoint+0x9ad) [0x55fa6971feb2]
postgres: standby: checkpointer (CheckpointerMain+0x4b1) [0x55fa6996431c]
postgres: standby: checkpointer (postmaster_child_launch+0x130) [0x55fa69964b41]
postgres: standby: checkpointer (+0x40a1a7) [0x55fa699671a7]
postgres: standby: checkpointer (PostmasterMain+0x1563) [0x55fa6996aed6]
postgres: standby: checkpointer (main+0x7f0) [0x55fa6989f798]
/lib/x86_64-linux-gnu/libc.so.6(+0x29ca8) [0x7f1876a54ca8]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x85) [0x7f1876a54d65]
postgres: standby: checkpointer (_start+0x21) [0x55fa696421a1]

Scorpion is failing for pg_basebackup's 020_pg_receivewal test at [1].
[1] - https://buildfarm.postgresql.org/cgi-bin/show_stage_log.pl?nm=scorpion&dt=2025-06-17%2000%3A40%3A46&stg=pg_basebackup-check

Regards,
Vignesh

---

From: Alexander Korotkov <[email-masked]>
Date: Tue, 17 Jun 2025 18:08:11 +0300

Hi, Vitaly!

On Tue, Jun 17, 2025 at 6:02 PM Vitaly Davydov <[email-masked]> wrote:
> Thank you for reporting the issue.
>
> This assert was introduced in the patch. Now, I think, it is a wrong one. Let me
> please explain one of the possible scenarios when it can be triggered. In case
> of physical replication, when walsender receives a standby reply message, it
> calls PhysicalConfirmReceivedLocation function which updates slots' restart_lsn
> from received flush_lsn value. This value may be older than the saved value. If
> it happens during checkpoint, after slot saving to disk, this assert will be
> triggered, because last_saved_restart_lsn value may be lesser than the new
> restart_lsn value, updated from walsender.
>
> I propose to remove this assert.

Yes, but is it OK for restart_lsn to move backward?  That might mean
that if checkpoint happen faster than
PhysicalConfirmReceivedLocation(), then crash could cause this WAL
location to be unavailable.  Is that true?

Also, what do you think about proposed changes in [1]?  I wonder if it
could somehow decrease the coverage.

Links.
1. https://www.postgresql.org/message-id/OSCPR01MB149665B3F0629D10731B18E5AF570A%40OSCPR01MB14966.jpnprd01.prod.outlook.com

------
Regards,
Alexander Korotkov
Supabase

---

From: Tom Lane <[email-masked]>
Date: Tue, 17 Jun 2025 16:18:30 -0400

> "../pgsql/src/backend/replication/slot.c", Line: 1813, PID: 3945797

My animal mamba is also showing this assertion failure, but in a
different test (recovery/t/040_standby_failover_slots_sync.pl).
It's failed in two out of its three runs since ca307d5ce went in,
so it's more reproducible than scorpion's report, though still not
perfectly so.

I suspect that mamba is prone to this simply because it's slow,
although perhaps there's a different reason.  Anyway, happy to
investigate manually if there's something you'd like me to
check for.

			regards, tom lane

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Wed, 18 Jun 2025 07:25:32 +0000

Dear Vitaly,

I've been working on the bug...

> from received flush_lsn value. This value may be older than the saved value. 

To confirm, can you tell me the theory why the walsender received old LSN?
It is sent by the walreceiver, so is there a case that LogstreamResult.Flush can go backward?
Not sure we can accept the situation.

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: vignesh C <[email-masked]>
Date: Wed, 18 Jun 2025 15:33:17 +0530

On Wed, 18 Jun 2025 at 14:35, Vitaly Davydov <[email-masked]> wrote:
> Dear Hayato,
> I can't say anything about the origin of the issue, but it can be easily reproduced
> on the master branch:
>
> 1. Add an assert in PhysicalConfirmReceivedLocation (apply the attached patch)
> 2. Compile & install with tap tests and assertions enabled
> 3. cd src/bin/pg_basebackup/
> 3. PROVE_TESTS=t/020_pg_receivewal.pl gmake check

Thanks for the steps, I was able to reproduce the issue with the
suggested steps.

> The test will fail because of the assertion. I plan to investigate the issue
> but I need some more time for it. Once, it happens on the original master
> branch, I think, this problem already exists. The proposed patch seems
> to be not guilty.

This issue occurs even prior to this commit, I was able to reproduce
it on a version just before it. I’ll also look into analyzing the root
cause further.

> It may be the same problem as discussed in:
> https://www.postgresql.org/message-id/CALDaNm2uQbhEVJzvnja6rw7Q9AYu9FpVmET%3DTbwLjV3DcPRhLw%40mail.gmail.com

This issue was related to confirmed_flush and was addressed in commit
d1ffcc7fa3c54de8b2a677a3e503fc808c7b419c. It is not related to
restart_lsn.

Regards,
Vignesh

---

From: Alexander Korotkov <[email-masked]>
Date: Wed, 18 Jun 2025 19:47:09 +0300

On Wed, Jun 18, 2025 at 6:50 PM Vitaly Davydov <[email-masked]> wrote:
> > I think, it is a good idea. Once we do not use the generated data, it is ok
> > just to generate WAL segments using the proposed function. I've tested this
> > function. The tests worked as expected with and without the fix. The attached
> > patch does the change.
>
> Sorry, forgot to attach the patch. It is created on the current master branch.
> It may conflict with your corrections. I hope, it could be useful.

Thank you.  I've integrated this into a patch to improve these tests.

Regarding assertion failure, I've found that assert in
PhysicalConfirmReceivedLocation() conflicts with restart_lsn
previously set by ReplicationSlotReserveWal().  As I can see,
ReplicationSlotReserveWal() just picks fresh XLogCtl->RedoRecPtr lsn.
So, it doesn't seems there is a guarantee that restart_lsn never goes
backward.  The commit in ReplicationSlotReserveWal() even states there
is a "chance that we have to retry".  Thus, I propose to remove the
assertion introduced by ca307d5cec90.

Any objection from backpatching 0001 though 17 and pushing 0002 to the head?

------
Regards,
Alexander Korotkov
Supabase

---

From: Amit Kapila <[email-masked]>
Date: Thu, 19 Jun 2025 15:59:25 +0530

On Wed, Jun 18, 2025 at 10:17 PM Alexander Korotkov
<[email-masked]> wrote:
> is a "chance that we have to retry".
>

I don't see how this theory can lead to a restart_lsn of a slot going
backwards. The retry mentioned there is just a retry to reserve the
slot's position again if the required WAL is already removed. Such a
retry can only get the position later than the previous restart_lsn.

>  Thus, I propose to remove the

If what I said above is correct, then the following part of the commit
message will be incorrect:
"As stated in the ReplicationSlotReserveWal() comment, this is not
always true. Additionally, this issue has been spotted by some
buildfarm
members."

-- 
With Regards,
Amit Kapila.

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Thu, 19 Jun 2025 11:05:49 +0000

Dear Amit, Alexander,

> retry can only get the position later than the previous restart_lsn.

We analyzed the assertion failure happened at pg_basebackup/020_pg_receivewal,
and confirmed that restart_lsn can go backward. This meant that Assert() added
by the ca307d5 can cause crash.

Background
===========
When pg_receivewal starts the replication and it uses the replication slot, it
sets as the beginning of the segment where restart_lsn exists, as the startpoint.
E.g., if the restart_lsn of the slot is 0/B000D0, pg_receivewal requests WALs
from 0/B00000.
More detail of this behavior, see f61e1dd2 and d9bae531.

What happened here
==================
Based on above theory, walsender sent from the beginning of segment (0/B00000).
When walreceiver receives, it tried to send reply. At that time the flushed
location of WAL would be 0/B00000. walsender sets the received lsn as restart_lsn
in PhysicalConfirmReceivedLocation(). Here the restart_lsn went backward (0/B000D0->0/B00000).

The assertion failure could happen if CHECKPOINT happened at that time.
Attribute last_saved_restart_lsn of the slot was 0/B000D0, but the data.restart_lsn
was 0/B00000. It could not satisfy the assertion added in InvalidatePossiblyObsoleteSlot().

Note
====
1.
In this case, starting from the beginning of the segment is not a problem, because
the checkpoint process only removes WAL files from segments that precede the
restart_lsn's wal segment. The current segment (0/B00000) will not be removed,
so there is no risk of data loss or inconsistency.

2.
A similar pattern applies to pg_basebackup. Both use logic that adjusts the
requested streaming position to the start of the segment, and it replies the
received LSN as flushed.

3.
I considered the theory above, but I could not reproduce 040_standby_failover_slots_sync
because it is a timing issue. Have someone else reproduced?

We are still investigating failure caused at 040_standby_failover_slots_sync.

[1]: https://buildfarm.postgresql.org/cgi-bin/show_stage_log.pl?nm=scorpion&dt=2025-06-17%2000%3A40%3A46&stg=pg_basebackup-check

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: Alexander Korotkov <[email-masked]>
Date: Fri, 20 Jun 2025 03:18:20 +0300

> retry can only get the position later than the previous restart_lsn.

Yes, if retry is needed, then the new position must be later for sure.
What I mean is that ReplicationSlotReserveWal() can reserve something
later than what standby is going to read (and correspondingly report
with PhysicalConfirmReceivedLocation()).

> always true. Additionally, this issue has been spotted by some

I agree, this comment needs improvement in terms of clarity.

Meanwhile I've pushed the patch for TAP tests, which I think didn't
get any objections.

------
Regards,
Alexander Korotkov
Supabase

---

From: Alexander Korotkov <[email-masked]>
Date: Fri, 20 Jun 2025 03:24:16 +0300

Dear Kuroda-san,

> by the ca307d5 can cause crash.

Thank you for your detailed explanation!

> We are still investigating failure caused at 040_standby_failover_slots_sync.

I didn't manage to reproduce this.  But as I see from the logs [1] on
mamba that START_REPLICATION command was issued just before assert
trap.  Could it be something similar to what I described in [2].
Namely:
1. ReplicationSlotReserveWal() sets restart_lsn for the slot.
2. Concurrent checkpoint flushes that restart_lsn to the disk.
3. PhysicalConfirmReceivedLocation() sets restart_lsn of the slot to
the beginning of the segment.

[1] https://buildfarm.postgresql.org/cgi-bin/show_stage_log.pl?nm=mamba&dt=2025-06-17%2005%3A10%3A36&stg=recovery-check
[2] https://www.postgresql.org/message-id/CAPpHfdv3UEUBjsLhB_CwJT0xX9LmN6U%2B__myYopq4KcgvCSbTg%40mail.gmail.com

------
Regards,
Alexander Korotkov
Supabase

---

From: Amit Kapila <[email-masked]>
Date: Fri, 20 Jun 2025 08:25:34 +0530

> Meanwhile I've pushed the patch for TAP tests, which I think didn't

Sounds reasonable. As per analysis till now, it seems removal of new
assert is correct and we just need to figure out the reason in all
failure cases as to why the physical slot's restart_lsn goes backward,
and then add a comment somewhere to ensure that we don't repeat a
similar mistake in the future.

-- 
With Regards,
Amit Kapila.

---

From: vignesh C <[email-masked]>
Date: Fri, 20 Jun 2025 18:11:57 +0530

> the beginning of the segment.

Here is my analysis for the 040_standby_failover_slots_sync test
failure where in physical replication slot can point to backward lsn:
In certain rare cases the restart LSN can go backwards. This scenario
can be reproduced by performing checkpoint continuously and slowing
the WAL applying. I have a patch with changes to handle this.

Here's a summary of the sequence of events:
1) Standby confirms a new LSN (0/40369C8) when primary sends some WAL contents:
After standby writes the received WAL contents in XLogWalRcvWrite, the
standby sends this lsn 0/40369C8 as the confirmed flush location. The
primary acknowledges this and updates the replication slot's
restart_lsn accordingly:
2025-06-20 14:33:21.777 IST [134998] standby1 LOG:
PhysicalConfirmReceivedLocation replication slot "sb1_slot" set
restart_lsn to 0/40369C8
2025-06-20 14:33:21.777 IST [134998] standby1 STATEMENT:
START_REPLICATION SLOT "sb1_slot" 0/3000000 TIMELINE 1
Checkpoint persists the new restart_lsn:

2) Shortly after receiving the new LSN, a checkpoint occurs which
saves this restart_lsn:
2025-06-20 14:33:21.780 IST [134913] LOG:  checkpoint complete: wrote
0 buffers (0.0%), wrote 0 SLRU buffers; 0 WAL file(s) added, 0
removed, 0 recycled; write=0.001 s, sync=0.001 s, total=0.002 s; sync
files=0, longest=0.000 s, average=0.000 s; distance=0 kB, estimate=0
kB; lsn=0/4036A20, redo lsn=0/40369C8

3)Streaming replication is restarted because of primary_conninfo
change and reload
The WAL receiver process is restarted:
25-06-20 14:33:21.779 IST [134997] FATAL: terminating walreceiver
process due to administrator command

4) Standby sends an older flush pointer after restart:
Upon restart, the WAL receiver sends a flush location (0/401D448)
derived from XLogRecoveryCtl->lastReplayedEndRecPtr, which is older
than the previously confirmed restart_lsn. It is important to note
that we are sending the lastReplayedEndRecPtr which is the last
successfully replayed lsn in this case:
5-06-20 14:33:21.796 IST [135135] LOG:  WalReceiverMain
LogstreamResult.Flush initialized to 0/401D448
2025-06-20 14:33:21.796 IST [135135] LOG:  sending write 0/401D448
flush 0/401D448 apply 0/401D448

This is done from here:
....
/* Initialize LogstreamResult and buffers for processing messages */
LogstreamResult.Write = LogstreamResult.Flush = GetXLogReplayRecPtr(NULL);

initStringInfo(&reply_message);

/* Initialize nap wakeup times. */
now = GetCurrentTimestamp();
for (int i = 0; i < NUM_WALRCV_WAKEUPS; ++i)
WalRcvComputeNextWakeup(i, now);

/* Send initial reply/feedback messages. */
XLogWalRcvSendReply(true, false);
...

In case of step 1, we are sending the lsn of the WAL that is written,
since we have slowed down the WAL applying the replay location is
lesser and the replay location is being sent here.

5) I have added logs to detect this inconsistency:
This leads to a scenario where the standby tries to confirm a
restart_lsn older than the one currently held by the primary:
2025-06-20 14:33:21.797 IST [135136] standby1 LOG:  crash scenario -
slot sb1_slot, cannot confirm a restart LSN (0/401D448) that is older
than the current one (0/40369C8)

If a checkpoint happens concurrently during this condition, it may
trigger an assertion failure on the primary due to the restart_lsn
being less than the last_saved_restart_lsn.
Currently this does not break physical replication, but I'm not sure
if the gap increases to many WAL files and if the WAL files get
deleted, how it will behave.
Attached the patch changes with which you can reproduce. Grep for
"crash scenario" in the logs. For me it occurs with every run. The
reproduced logs are attached.

This proves that the restart_lsn can go backward in cases where the
standby is slowly applying. But this has nothing to do with this
thread, I felt you can commit the assert removal patch. I will
continue the analysis further and see if there is any impact or not
and we can later add comments accordingly.

Regards,
Vignesh

---

From: Alexander Korotkov <[email-masked]>
Date: Sun, 22 Jun 2025 03:20:23 +0300

Hi, Vignesh!

> and we can later add comments accordingly.

Thank you and other thread participants for the analysis!
I'd like to also ask for help with [1].  It seems that new test
triggers another bug, which I suppose was there before.

Links.
1. https://www.postgresql.org/message-id/CAAKRu_ZCOzQpEumLFgG_%2Biw3FTa%2BhJ4SRpxzaQBYxxM_ZAzWcA%40mail.gmail.com

------
Regards,
Alexander Korotkov
Supabase

---

From: Alexander Korotkov <[email-masked]>
Date: Mon, 23 Jun 2025 02:05:59 +0300

On Fri, Jun 20, 2025 at 2:24 PM vignesh C <[email-masked]> wrote:
> failure where the physical replication slot can point to backward lsn:
> After the standby writes the received WAL contents in XLogWalRcvWrite,
> the standby sends this lsn 0/40369C8 as the confirmed flush location.
> The primary acknowledges this and updates the replication slot's
> restart_lsn accordingly:
> 3) Streaming replication is restarted because of primary_conninfo guc
> change and reload
> lesser.
> Attached the restart_lsn_backup_repro_v1 patch changes with which you
> can reproduce the 040_standby_failover_slots_sync failure. grep for
> "crash scenario - slot sb1_slot" in the logs. For me it occurs with
> every run. The reproduced logs
> 040_standby_failover_slots_sync_publisher and
> 040_standby_failover_slots_sync_standby1.log are attached.
>
> continue the analysis further and see if there is any impact or not on
> physical replication, and we can later add comments accordingly.

I think this thread now have well-documented cases when restart_lsn
goes backward.  I suppose the first thing we should do is to remove
the assert to make buildfarm stop failing on this.  I've prepared a
patch with revised commit message.  Then we can focus on documenting
that correctly in the code comments.

------
Regards,
Alexander Korotkov
Supabase

---

From: vignesh C <[email-masked]>
Date: Mon, 23 Jun 2025 22:33:50 +0530

> that correctly in the code comments.

+1 to proceed this way, one minor suggestion we can include
pg_basebackup also in the commit message as I noticed this can happen
with pg_basebackup too.

Regards,
Vignesh

---

From: Alexander Korotkov <[email-masked]>
Date: Mon, 23 Jun 2025 21:50:50 +0300

> pg_basebackup also in the commit message as I noticed this can happen

Thank you!  Pushed including your suggestion to mention pg_basebackup.

------
Regards,
Alexander Korotkov
Supabase

---

From: "Zhijie Hou (Fujitsu)" <[email-masked]>
Date: Wed, 25 Jun 2025 05:27:27 +0000

Hi,

After commit ca307d5, I noticed another crash when testing
some other logical replication features.

The server with max_replication_slots set to 0 would crash when executing CHECKPOINT.

TRAP: failed Assert("ReplicationSlotCtl != NULL"), File: "slot.c", Line: 1162, PID: 577315
postgres: checkpointer (ExceptionalCondition+0x9e)[0xc046cb]
postgres: checkpointer (ReplicationSlotsComputeRequiredLSN+0x30)[0x99697f]
postgres: checkpointer (CheckPointReplicationSlots+0x191)[0x997dc1]
postgres: checkpointer [0x597b1b]
postgres: checkpointer (CreateCheckPoint+0x6d1)[0x59729e]
postgres: checkpointer (CheckpointerMain+0x559)[0x93ee79]
postgres: checkpointer (postmaster_child_launch+0x15f)[0x940311]
postgres: checkpointer [0x9468b0]
postgres: checkpointer (PostmasterMain+0x1258)[0x9434f8]
postgres: checkpointer (main+0x2fe)[0x7f5f9c]
/lib64/libc.so.6(__libc_start_main+0xe5)[0x7f7585f81d85]
postgres: checkpointer (_start+0x2e)[0x4958ee]

I think it is trying to access the replication slots when the shared memory
for them was not allocated.

Best Regards,
Hou zj

---

From: Dilip Kumar <[email-masked]>
Date: Wed, 25 Jun 2025 11:47:46 +0530

> for them was not allocated.

I do not understand why CheckPointReplicationSlots() calls
ReplicationSlotsComputeRequiredLSN() unconditionally, shouldn't this
be called under the check[1], If not then instead of asserting
Assert("ReplicationSlotCtl != NULL"), this should just return if
ReplicationSlotCtl is NULL, isn't it, because ReplicationSlotCtl is
not allocated if max_replication_slots is 0.

[1]

@@ -2131,7 +2131,8 @@ CheckPointReplicationSlots(bool is_shutdown)
         * Recompute the required LSN as SaveSlotToPath() updated
         * last_saved_restart_lsn for slots.
         */
-       ReplicationSlotsComputeRequiredLSN();
+       if (max_replication_slots > 0)
+               ReplicationSlotsComputeRequiredLSN();
 }

-- 
Regards,
Dilip Kumar
Google

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Wed, 25 Jun 2025 07:48:43 +0000

Dear Dilip,

Another idea is to call ReplicationSlotsComputeRequiredLSN() when at least one
of the restart_lsn is updated, like attached. I feel this could reduce the computation
bit more.

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: Dilip Kumar <[email-masked]>
Date: Wed, 25 Jun 2025 13:55:34 +0530

> of the restart_lsn is updated, like attached. I feel this could reduce the computation

Right, that makes sense, if there is nothing updated on disk then we
can avoid computing this.

-- 
Regards,
Dilip Kumar
Google

---

From: Alexander Korotkov <[email-masked]>
Date: Thu, 26 Jun 2025 00:50:29 +0300

> can avoid computing this.

Good idea.  But I think we should associate the "updated" flag
directly to the fact that one slot (no matter logical or physical)
changed its last_saved_restart_lsn.  See the attached patch.  I'm
going to push it if no objections.

------
Regards,
Alexander Korotkov
Supabase

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Thu, 26 Jun 2025 03:46:44 +0000

Dear Alexander,
> going to push it if no objections.

+		/*
+		 * Track if we're going to update slot's last_saved_restart_lsn.
+		 * We need this to know if we need to recompute the required LSN.
+		 */
+		if (s->last_saved_restart_lsn != s->data.restart_lsn)
+			last_saved_restart_lsn_updated = true;

I feel no need to set to true if last_saved_restart_lsn_updated is already true.
Other than that it's OK for me.

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: Alexander Korotkov <[email-masked]>
Date: Thu, 26 Jun 2025 17:48:45 +0300

Dear Kuroda-san,

> Other than that it's OK for me.

Thank you for your feedback.

Regarding last_saved_restart_lsn_updated, I think the opposite.  I
think we should check if last_saved_restart_lsn_updated is set already
only if it could promise us some economy of resources.  In our case
the main check only compares two fields of slot.  And that fields are
to be accessed anyway.  So, we are not going to save any RAM accesses.
Therefore, checking for last_saved_restart_lsn_updated seems like
unnecessary code complication (and I don't see we're doing that in
other places).  So, I'm going to push this patch "as is".

------
Regards,
Alexander Korotkov
Supabase

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Fri, 27 Jun 2025 02:22:27 +0000

Dear Alexander,

> other places).  So, I'm going to push this patch "as is".

To clarify: I have no objections. Thanks for giving the knowledge.

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: Dilip Kumar <[email-masked]>
Date: Fri, 27 Jun 2025 09:47:19 +0530

> going to push it if no objections.

Looks good to me.

-- 
Regards,
Dilip Kumar
Google

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Sun, 29 Jun 2025 06:22:30 +0000

Dear hackers,

Thanks everyone who are working on the bug. IIUC the remained task is

> similar mistake in the future.

I've wrote a draft for that. How do you think?

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: vignesh C <[email-masked]>
Date: Wed, 2 Jul 2025 22:50:22 +0530

> I've wrote a draft for that. How do you think?

I analyzed a scenario involving physical replication where the
restart_lsn appears to go backward by fewer files:
In my case, current restart_lsn = 0/A721E68, which corresponds to a
WAL record in the file 00000001000000000000000A. However, the recovery
process is progressing slowly and is currently at 0/3058078.
Now when the primary_conninfo is changed the walsender/walreceiver
process will be restarted. As a result, the standby starts requesting
WAL streaming from 0/3058078 the last applied wal lsn, which is in the
file 000000010000000000000003.

Since this WAL segment (000000010000000000000003) has already been
removed due to the restart_lsn being far ahead, the primary throws an
error saying WAL segment is removed. Even though the apply pointer is
at 0/3058078 (the record currently being applied), the standby already
has WAL segments up to 0/A721E68 written and available locally. So,
despite the walsender process failing to start due to missing
segments, the recovery process on the standby continues applying WAL
up to 0/A721E68 using the locally available files (including
00000001000000000000000A). Once all local WAL has been applied, the
standby will attempt to start streaming again—this time using the
latest WAL record (in my case, 0/A74000). Since this record still
exists on the primary, the walsender process starts successfully, and
physical replication resumes normally.

This confirms that even in physical replication, a backward-moving
restart_lsn does not cause issues as the standby retains the necessary
WAL locally and can catch up without requiring removed segments from
the primary.

I was able to reproduce this scenario using the
physical_replication_restart_lsn_backward patch attached, here I have
changed to increase the checkpoint to happen frequently and slowed the
recovery process to apply the wal records with delays. For me I was
able to reproduce this on every run. The logs for the same are
attached in the logs.zip file and test used to reproduce this is
available at test_restart_lsn_backward.zip file:

Important log information from the attached logs:
Restart_lsn has moved to 0/A6E8000 in the primary node:
2025-07-02 22:34:44.563 IST walsender[330205] standby1 LOG:
PhysicalConfirmReceivedLocation replication slot "sb1_slot" set
restart_lsn to 0/A6E8000
2025-07-02 22:34:44.563 IST walsender[330205] standby1 STATEMENT:
START_REPLICATION SLOT "sb1_slot" 0/3000000 TIMELINE 1

When the walsender is restarted, it will request a older WAL which has
been removed and throw an error:
2025-07-02 22:34:44.577 IST walsender[330428] standby1 STATEMENT:
START_REPLICATION SLOT "sb1_slot" 0/3000000 TIMELINE 1
2025-07-02 22:34:44.577 IST walsender[330428] standby1 ERROR:
requested WAL segment 000000010000000000000003 has already been
removed

Standby node has written WAL upto 0/A6E8000:
2025-07-02 22:34:44.563 IST walreceiver[330204] LOG:  XLogWalRcvFlush
LogstreamResult.Flush initialized to 0/A6E8000
2025-07-02 22:34:44.563 IST walreceiver[330204] LOG:  sending write
0/A6E8000 flush 0/A6E8000 apply 0/

Walreceiver process terminated:
2025-07-02 22:34:44.564 IST walreceiver[330204] FATAL:  terminating
walreceiver process due to administrator command

Walreceiver requests wal from 000000010000000000000003 WAL file which
is removed:
2025-07-02 22:34:44.577 IST walreceiver[330427] LOG:  started
streaming WAL from primary at 0/3000000 on timeline 1
2025-07-02 22:34:44.577 IST walreceiver[330427] LOG:  WalReceiverMain
LogstreamResult.Flush initialized to 0/3047958
2025-07-02 22:34:44.577 IST walreceiver[330427] LOG:  sending write
0/3047958 flush 0/3047958 apply 0/3047958
2025-07-02 22:34:44.577 IST walreceiver[330427] FATAL:  could not
receive data from WAL stream: ERROR:  requested WAL segment
000000010000000000000003 has already been removed

Even though the WAL file is removed there is no issue, recovery
process applies the WAL from locally available WAL file:
2025-07-02 22:34:44.578 IST startup[330195] LOG:  Applied wal record
end rec 0/3047990, read rec 0/3047958
2025-07-02 22:34:44.580 IST checkpointer[330193] LOG:  recovery
restart point at 0/3047938

Once all the available WAL's are applied the walsender is started
again and replication proceeds smoothly:
2025-07-02 22:35:15.002 IST walsender[330922] standby1 LOG:  received
replication command: START_REPLICATION SLOT "sb1_slot" 0/A000000
TIMELINE 1
2025-07-02 22:35:15.002 IST walsender[330922] standby1 STATEMENT:
START_REPLICATION SLOT "sb1_slot" 0/A000000 TIMELINE 1

I'm ok with adding the comments.

Regards,
Vignesh

---

From: Alexander Korotkov <[email-masked]>
Date: Fri, 18 Jul 2025 13:43:04 +0300

> restart_lsn appears to go backward by fewer files:

This is indeed an interesting case.  But does restart_lsn go so much
backwards in this case?  I've checked the logs.  It looks like standby
requested a position several segments back, but restart_lsn keeps
increasing.

> I'm ok with adding the comments.

Thank you for your feedback!

------
Regards,
Alexander Korotkov
Supabase

---

From: Alexander Korotkov <[email-masked]>
Date: Fri, 18 Jul 2025 13:44:47 +0300

> I've wrote a draft for that. How do you think?

Looks good to me.  I'm going to push this if no objections.

------
Regards,
Alexander Korotkov
Supabase

---

From: Amit Kapila <[email-masked]>
Date: Fri, 18 Jul 2025 16:18:22 +0530

> Looks good to me.  I'm going to push this if no objections.

As discussed earlier, it is a good idea to add comments in this area.
But as this is for pre-existing cases, won't it be better to start a
new thread explaining the cases and a patch? We may get feedback from
others as well.

-- 
With Regards,
Amit Kapila.

---

From: Alexander Korotkov <[email-masked]>
Date: Fri, 18 Jul 2025 14:41:31 +0300

> new thread explaining the cases and a patch? We may get feedback from

OK, done.

https://www.postgresql.org/message-id/CAPpHfdvuyMrUg0Vs5jPfwLOo1M9B-GP5j_My9URnBX0B%3DnrHKw%40mail.gmail.com

------
Regards,
Alexander Korotkov
Supabase