public inbox for [email protected]
help / color / mirror / Atom feedRe: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
25+ messages / 7 participants
[nested] [flat]
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
@ 2026-02-26 04:58 Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
0 siblings, 1 reply; 25+ messages in thread
From: Ashutosh Sharma @ 2026-02-26 04:58 UTC (permalink / raw)
To: SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi,
On Wed, Feb 25, 2026 at 7:21 PM Ashutosh Sharma <[email protected]> wrote:
>
> Hi Satya,
>
> On Wed, Feb 25, 2026 at 3:38 AM SATYANARAYANA NARLAPURAM
> <[email protected]> wrote:
> >
> >
> > Hi hackers,
> >
> > synchronized_standby_slots requires that every physical slot listed in the GUC has caught up before a logical failover slot is allowed to proceed with decoding. This is an ALL-of-N slots semantic. The logical slot availability model does not align with quorum replication semantics set using synchronous_standby_names which can be configured for quorum commit (ANY M of N).
> >
> > In a typical 3 Node HA deployment with quorum sync rep:
> >
> > Primary, standby1 (corresponds to sb1_slot), standby2 (corresponds to sb2_slot)
> > synchronized_standby_slots = ' sb1_slot, sb2_slot'
> > synchronous_standby_names = 'Any 1 ('standby1','standby2')'
> >
> > If standby1 goes down, synchronous commits still succeed because standby2 satisfies the quorum. However, logical decoding blocks indefinitely in WaitForStandbyConfirmation(), waiting for sb1_slot (corresponds to standby1) to catch up — even though the transaction is already safely committed on a quorum of synchronous standbys. This blocks logical decoding consumers from progressing and is inconsistent with the availability guarantee the DBA intended by choosing quorum commit.
>
> +1. This can indeed be a blocker for failover enabled logical
> replication. It not only has the potential to disrupt logical
> replication, but can also impact the primary server. Over time, it may
> silently lead to significant WAL accumulation on the primary,
> eventually causing disk-full scenarios and degrading the performance
> of applications running on the primary instance. Therefore, I too
> strongly believe this needs to be addressed to prevent such
> potentially disruptive situations.
>
> >
> >
> > Proposal:
> >
> > Make synchronized_standby_slots quorum aware i.e. extend the GUC to accept an ANY M (slot1, slot2, ...) syntax similar to synchronous_standby_names, so StandbySlotsHaveCaughtup() can return true when M of N slots (where M <= N and M >= 1) have caught up. I still prefer two different GUCs for this as the list of slots to be synchronized can still be different (for example, DBA may want to ensure Geo standby to be sync before allowing the logical decoding client to read the changes). I kept synchronized_standby_slots parse logic similar to synchronous_standby_names to keep things simple. The default behavior is also not changed for synchronized_standby_slots.
> >
>
> Thank you for the proposal. I can spend some time reviewing the
> changes and help take this forward. I would also be happy to hear
> others' thoughts and feedback on the proposal.
>
Thinking about this further, using quorum settings for
synchronized_standby_slots can/will certainly result in at least one
sync standby lagging behind the logical replica, making it probably
impossible to continue with the existing logical replication setup
after a failover to the standby that lags behind. Here is what I am
mean:
Let's say we have 2 synchronous standbys with
"synchronized_standby_slots" configured as ANY 1 (sync_standby1,
sync_standby2). With this quorum setting, WAL only needs to be
confirmed by any one of the two standbys before it can be forwarded to
the logical replica. Now consider a scenario where sync_standby1 is
ahead of sync_standby2, new WAL gets confirmed by sync_standby1 and
subsequently delivered to the logical replica. If sync_standby1 then
goes down and we failover to sync_standby2, the new primary will be at
a lower LSN than the logical replica, since sync_standby2 never
received that WAL. At this point, the logical replication slot on the
new primary is essentially stale, and the logical replication setup
that existed before the failover cannot be resumed. Hence, I think
it's important to ensure that the WAL (including all the necessary
data needed for logical replication) gets delivered to all the
servers/slots specified in synchronized_standby_slots before it gets
delivered to the logical replica.
While I agree that not allowing quorum like settings for this has the
potential to accumulate WAL and impact logical replication, I think we
can explore other ways to mitigate that concern separately.
Let's see what experts have to say on this.
--
With Regards,
Ashutosh Sharma.
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
@ 2026-02-26 06:19 ` Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:02 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
0 siblings, 2 replies; 25+ messages in thread
From: Amit Kapila @ 2026-02-26 06:19 UTC (permalink / raw)
To: Ashutosh Sharma <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
On Thu, Feb 26, 2026 at 10:28 AM Ashutosh Sharma <[email protected]> wrote:
>
>
> > >
> > > Proposal:
> > >
> > > Make synchronized_standby_slots quorum aware i.e. extend the GUC to accept an ANY M (slot1, slot2, ...) syntax similar to synchronous_standby_names, so StandbySlotsHaveCaughtup() can return true when M of N slots (where M <= N and M >= 1) have caught up. I still prefer two different GUCs for this as the list of slots to be synchronized can still be different (for example, DBA may want to ensure Geo standby to be sync before allowing the logical decoding client to read the changes). I kept synchronized_standby_slots parse logic similar to synchronous_standby_names to keep things simple. The default behavior is also not changed for synchronized_standby_slots.
> > >
...
>
> Thinking about this further, using quorum settings for
> synchronized_standby_slots can/will certainly result in at least one
> sync standby lagging behind the logical replica, making it probably
> impossible to continue with the existing logical replication setup
> after a failover to the standby that lags behind. Here is what I am
> mean:
>
But won't that be true even for synchronous_standby_names? I think in
the case of quorum, it is the responsibility of the failover solution
to select the most recent synced standby among all the standby's
specified in synchronous_standby_names. Similarly here before failing
over logical subscriber to one of physical standby, the failover tool
needs to ensure it is switching over to the synced replica. We have
given steps in the docs [1] that could be used to identify the replica
where the subscriber can switchover. Will that address your concern?
BTW, I have also suggested this idea in thread [2]. I don't recall all
the ideas/points discussed in that thread but it would be good to
check that thread for any alternative ideas and points raised, so that
we don't miss anything.
[1] - https://www.postgresql.org/docs/current/logical-replication-failover.html
[2] - https://www.postgresql.org/message-id/CAA4eK1KLFdmj8CLrZNL0D4phqyQihb7NXOjmqvrU5DT8moQn9Q%40mail.gma...
--
With Regards,
Amit Kapila.
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
@ 2026-02-26 07:42 ` Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
1 sibling, 1 reply; 25+ messages in thread
From: Ashutosh Sharma @ 2026-02-26 07:42 UTC (permalink / raw)
To: Amit Kapila <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi Amit,
On Thu, Feb 26, 2026 at 11:50 AM Amit Kapila <[email protected]> wrote:
>
> On Thu, Feb 26, 2026 at 10:28 AM Ashutosh Sharma <[email protected]> wrote:
> >
> >
> > > >
> > > > Proposal:
> > > >
> > > > Make synchronized_standby_slots quorum aware i.e. extend the GUC to accept an ANY M (slot1, slot2, ...) syntax similar to synchronous_standby_names, so StandbySlotsHaveCaughtup() can return true when M of N slots (where M <= N and M >= 1) have caught up. I still prefer two different GUCs for this as the list of slots to be synchronized can still be different (for example, DBA may want to ensure Geo standby to be sync before allowing the logical decoding client to read the changes). I kept synchronized_standby_slots parse logic similar to synchronous_standby_names to keep things simple. The default behavior is also not changed for synchronized_standby_slots.
> > > >
> ...
> >
> > Thinking about this further, using quorum settings for
> > synchronized_standby_slots can/will certainly result in at least one
> > sync standby lagging behind the logical replica, making it probably
> > impossible to continue with the existing logical replication setup
> > after a failover to the standby that lags behind. Here is what I am
> > mean:
> >
>
> But won't that be true even for synchronous_standby_names? I think in
> the case of quorum, it is the responsibility of the failover solution
> to select the most recent synced standby among all the standby's
> specified in synchronous_standby_names. Similarly here before failing
> over logical subscriber to one of physical standby, the failover tool
> needs to ensure it is switching over to the synced replica. We have
> given steps in the docs [1] that could be used to identify the replica
> where the subscriber can switchover. Will that address your concern?
>
Here's my understanding of this:
I don't think we should be comparing "synchronous_standby_names" with
"synchronized_standby_slots", even though they appear similar in
purpose. All values listed in synchronous_standby_names represent
synchronous standbys exclusively, whereas synchronized_standby_slots
can hold values for both synchronous and asynchronous standbys. In
other words, every server referenced by synchronous_standby_names is
of the same type, but that may not be the case with
synchronized_standby_slots.
If a GUC can hold values of different types (sync vs. async), does it
really make sense to use a qualifier like ANY 1 (val1, val2) when val1
and val2 are different in nature? For example, suppose val1 is a
synchronous standby and val2 is an asynchronous standby, and we
configure ANY 1 (val1, val2). It's possible for val2 to get ahead of
val1 in terms of replication progress, which in turn could mean the
logical replica is also ahead of val1. So if we were to fail over to
val1 (since it's the only synchronous standby), we will not be able to
use the existing logical replication setup.
Please correct me if I have misunderstood anything here.
--
With Regards,
Ashutosh Sharma.
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
@ 2026-02-26 08:23 ` SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
0 siblings, 1 reply; 25+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-02-26 08:23 UTC (permalink / raw)
To: Ashutosh Sharma <[email protected]>; +Cc: Amit Kapila <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi Ashutosh,
On Wed, Feb 25, 2026 at 11:42 PM Ashutosh Sharma <[email protected]>
wrote:
>
> I don't think we should be comparing "synchronous_standby_names" with
> "synchronized_standby_slots", even though they appear similar in
> purpose. All values listed in synchronous_standby_names represent
> synchronous standbys exclusively, whereas synchronized_standby_slots
> can hold values for both synchronous and asynchronous standbys. In
> other words, every server referenced by synchronous_standby_names is
> of the same type, but that may not be the case with
> synchronized_standby_slots.
>
> If a GUC can hold values of different types (sync vs. async), does it
> really make sense to use a qualifier like ANY 1 (val1, val2) when val1
> and val2 are different in nature? For example, suppose val1 is a
> synchronous standby and val2 is an asynchronous standby, and we
> configure ANY 1 (val1, val2). It's possible for val2 to get ahead of
> val1 in terms of replication progress, which in turn could mean the
> logical replica is also ahead of val1. So if we were to fail over to
> val1 (since it's the only synchronous standby), we will not be able to
> use the existing logical replication setup.
>
If the failover orchestrator cannot ensure standby1 to not get the quorum
committed WAL (from archive or standby2) then the setting ANY 1 (val1,
val2) is invalid.
This setup also has issues because in your scenario, standby2 is ahead of
the new primary (standby1) and standby2 requires now to rewind to be in
sync with the new primary. Additionally, it allowed readers to read data
that was lost at the end of the failover. We ideally need a mechanism to
not send WAL to async replicas before the sync replicas commit (honoring
syncrhnous_standby_names GUC) feature (similar to
synchronized_standby_slots). It could be a different thread on its own.
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
@ 2026-02-26 08:45 ` shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 09:29 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Alexander Kukushkin <[email protected]>
0 siblings, 2 replies; 25+ messages in thread
From: shveta malik @ 2026-02-26 08:45 UTC (permalink / raw)
To: SATYANARAYANA NARLAPURAM <[email protected]>; +Cc: Ashutosh Sharma <[email protected]>; Amit Kapila <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>; shveta malik <[email protected]>
On Thu, Feb 26, 2026 at 1:54 PM SATYANARAYANA NARLAPURAM
<[email protected]> wrote:
>
> Hi Ashutosh,
>
> On Wed, Feb 25, 2026 at 11:42 PM Ashutosh Sharma <[email protected]> wrote:
>>
>>
>> I don't think we should be comparing "synchronous_standby_names" with
>> "synchronized_standby_slots", even though they appear similar in
>> purpose. All values listed in synchronous_standby_names represent
>> synchronous standbys exclusively, whereas synchronized_standby_slots
>> can hold values for both synchronous and asynchronous standbys. In
>> other words, every server referenced by synchronous_standby_names is
>> of the same type, but that may not be the case with
>> synchronized_standby_slots.
>>
>> If a GUC can hold values of different types (sync vs. async), does it
>> really make sense to use a qualifier like ANY 1 (val1, val2) when val1
>> and val2 are different in nature? For example, suppose val1 is a
>> synchronous standby and val2 is an asynchronous standby, and we
>> configure ANY 1 (val1, val2). It's possible for val2 to get ahead of
>> val1 in terms of replication progress, which in turn could mean the
>> logical replica is also ahead of val1. So if we were to fail over to
>> val1 (since it's the only synchronous standby), we will not be able to
>> use the existing logical replication setup.
>
>
> If the failover orchestrator cannot ensure standby1 to not get the quorum committed WAL (from archive or standby2) then the setting ANY 1 (val1, val2) is invalid.
> This setup also has issues because in your scenario, standby2 is ahead of the new primary (standby1) and standby2 requires now to rewind to be in sync with the new primary. Additionally, it allowed readers to read data that was lost at the end of the failover. We ideally need a mechanism to not send WAL to async replicas before the sync replicas commit (honoring syncrhnous_standby_names GUC) feature (similar to synchronized_standby_slots). It could be a different thread on its own.
+1 on the overall idea of the patch.
I understand the concern raised above that one of the standbys in the
quorum (synchronized_standby_slots) might lag behind the logical
replica, and a user could potentially failover to such a standby. But
I also agree with Amit that configuring failover correctly is
ultimately the responsibility of failover-solution. And instructions
in doc should be followed before deciding if a standby is
failover-ready or not.
As suggested in [1], IMO, it is a reasonably good idea for
'synchronized_standby_slots' to DEFAULT to the value of
'synchronous_standby_names'. That way, even if the user missed to
configure 'synchronized_standby_slots' explicitly, we would still have
reasonable protection in place. At the same time, if a user
intentionally chooses not to configure it, a NULL/NONE value should
remain a valid option.
[1]: https://www.postgresql.org/message-id/CAJpy0uCZ04ZQFHs-tV5LprkYtSSwtBtUJW4O%3D0S01yc%2BTRw7EQ%40mail...
Thanks,
Shveta
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
@ 2026-02-26 09:11 ` Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
1 sibling, 1 reply; 25+ messages in thread
From: Ashutosh Sharma @ 2026-02-26 09:11 UTC (permalink / raw)
To: shveta malik <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Amit Kapila <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi,
On Thu, Feb 26, 2026 at 2:15 PM shveta malik <[email protected]> wrote:
>
> On Thu, Feb 26, 2026 at 1:54 PM SATYANARAYANA NARLAPURAM
> <[email protected]> wrote:
> >
> > Hi Ashutosh,
> >
> > On Wed, Feb 25, 2026 at 11:42 PM Ashutosh Sharma <[email protected]> wrote:
> >>
> >>
> >> I don't think we should be comparing "synchronous_standby_names" with
> >> "synchronized_standby_slots", even though they appear similar in
> >> purpose. All values listed in synchronous_standby_names represent
> >> synchronous standbys exclusively, whereas synchronized_standby_slots
> >> can hold values for both synchronous and asynchronous standbys. In
> >> other words, every server referenced by synchronous_standby_names is
> >> of the same type, but that may not be the case with
> >> synchronized_standby_slots.
> >>
> >> If a GUC can hold values of different types (sync vs. async), does it
> >> really make sense to use a qualifier like ANY 1 (val1, val2) when val1
> >> and val2 are different in nature? For example, suppose val1 is a
> >> synchronous standby and val2 is an asynchronous standby, and we
> >> configure ANY 1 (val1, val2). It's possible for val2 to get ahead of
> >> val1 in terms of replication progress, which in turn could mean the
> >> logical replica is also ahead of val1. So if we were to fail over to
> >> val1 (since it's the only synchronous standby), we will not be able to
> >> use the existing logical replication setup.
> >
> >
> > If the failover orchestrator cannot ensure standby1 to not get the quorum committed WAL (from archive or standby2) then the setting ANY 1 (val1, val2) is invalid.
> > This setup also has issues because in your scenario, standby2 is ahead of the new primary (standby1) and standby2 requires now to rewind to be in sync with the new primary. Additionally, it allowed readers to read data that was lost at the end of the failover. We ideally need a mechanism to not send WAL to async replicas before the sync replicas commit (honoring syncrhnous_standby_names GUC) feature (similar to synchronized_standby_slots). It could be a different thread on its own.
>
>
> +1 on the overall idea of the patch.
> I understand the concern raised above that one of the standbys in the
> quorum (synchronized_standby_slots) might lag behind the logical
> replica, and a user could potentially failover to such a standby. But
> I also agree with Amit that configuring failover correctly is
> ultimately the responsibility of failover-solution. And instructions
> in doc should be followed before deciding if a standby is
> failover-ready or not.
>
> As suggested in [1], IMO, it is a reasonably good idea for
> 'synchronized_standby_slots' to DEFAULT to the value of
> 'synchronous_standby_names'. That way, even if the user missed to
> configure 'synchronized_standby_slots' explicitly, we would still have
> reasonable protection in place. At the same time, if a user
> intentionally chooses not to configure it, a NULL/NONE value should
> remain a valid option.
>
AFAIU, not all names listed in "synchronous_standby_names" are
necessarily synchronous standbys. Tools like pg_receivewal, for
example, can establish a replication connection to the primary and
appear in that list. Therefore, deriving "synchronized_standby_slots"
from "synchronous_standby_names", if not set by the user would cause
logical slots to be synchronized to whatever nodes those names
represent, including a host running pg_receivewal, which is certainly
not something the user would have intended to do. Therefore I feel
this might not just be the good choice.
--
With Regards,
Ashutosh Sharma.
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
@ 2026-02-26 10:46 ` SATYANARAYANA NARLAPURAM <[email protected]>
2026-05-21 09:12 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
0 siblings, 2 replies; 25+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-02-26 10:46 UTC (permalink / raw)
To: Ashutosh Sharma <[email protected]>; +Cc: shveta malik <[email protected]>; Amit Kapila <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi Ashutosh,
On Thu, Feb 26, 2026 at 1:11 AM Ashutosh Sharma <[email protected]>
wrote:
> Hi,
>
> On Thu, Feb 26, 2026 at 2:15 PM shveta malik <[email protected]>
> wrote:
> >
> > On Thu, Feb 26, 2026 at 1:54 PM SATYANARAYANA NARLAPURAM
> > <[email protected]> wrote:
> > >
> > > Hi Ashutosh,
> > >
> > > On Wed, Feb 25, 2026 at 11:42 PM Ashutosh Sharma <
> [email protected]> wrote:
> > >>
> > >>
> > >> I don't think we should be comparing "synchronous_standby_names" with
> > >> "synchronized_standby_slots", even though they appear similar in
> > >> purpose. All values listed in synchronous_standby_names represent
> > >> synchronous standbys exclusively, whereas synchronized_standby_slots
> > >> can hold values for both synchronous and asynchronous standbys. In
> > >> other words, every server referenced by synchronous_standby_names is
> > >> of the same type, but that may not be the case with
> > >> synchronized_standby_slots.
> > >>
> > >> If a GUC can hold values of different types (sync vs. async), does it
> > >> really make sense to use a qualifier like ANY 1 (val1, val2) when val1
> > >> and val2 are different in nature? For example, suppose val1 is a
> > >> synchronous standby and val2 is an asynchronous standby, and we
> > >> configure ANY 1 (val1, val2). It's possible for val2 to get ahead of
> > >> val1 in terms of replication progress, which in turn could mean the
> > >> logical replica is also ahead of val1. So if we were to fail over to
> > >> val1 (since it's the only synchronous standby), we will not be able to
> > >> use the existing logical replication setup.
> > >
> > >
> > > If the failover orchestrator cannot ensure standby1 to not get the
> quorum committed WAL (from archive or standby2) then the setting ANY 1
> (val1, val2) is invalid.
> > > This setup also has issues because in your scenario, standby2 is ahead
> of the new primary (standby1) and standby2 requires now to rewind to be in
> sync with the new primary. Additionally, it allowed readers to read data
> that was lost at the end of the failover. We ideally need a mechanism to
> not send WAL to async replicas before the sync replicas commit (honoring
> syncrhnous_standby_names GUC) feature (similar to
> synchronized_standby_slots). It could be a different thread on its own.
> >
> >
> > +1 on the overall idea of the patch.
> > I understand the concern raised above that one of the standbys in the
> > quorum (synchronized_standby_slots) might lag behind the logical
> > replica, and a user could potentially failover to such a standby. But
> > I also agree with Amit that configuring failover correctly is
> > ultimately the responsibility of failover-solution. And instructions
> > in doc should be followed before deciding if a standby is
> > failover-ready or not.
> >
> > As suggested in [1], IMO, it is a reasonably good idea for
> > 'synchronized_standby_slots' to DEFAULT to the value of
> > 'synchronous_standby_names'. That way, even if the user missed to
> > configure 'synchronized_standby_slots' explicitly, we would still have
> > reasonable protection in place. At the same time, if a user
> > intentionally chooses not to configure it, a NULL/NONE value should
> > remain a valid option.
> >
>
> AFAIU, not all names listed in "synchronous_standby_names" are
> necessarily synchronous standbys. Tools like pg_receivewal, for
> example, can establish a replication connection to the primary and
> appear in that list. Therefore, deriving "synchronized_standby_slots"
> from "synchronous_standby_names", if not set by the user would cause
> logical slots to be synchronized to whatever nodes those names
> represent, including a host running pg_receivewal, which is certainly
> not something the user would have intended to do. Therefore I feel
> this might not just be the good choice.
Agreed, not a good idea to have synchronized_standby_slots default to
synchronous_standby_names because application_names and slot names are
different as stated.
Thanks,
Satya
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
@ 2026-05-21 09:12 ` Ashutosh Sharma <[email protected]>
1 sibling, 0 replies; 25+ messages in thread
From: Ashutosh Sharma @ 2026-05-21 09:12 UTC (permalink / raw)
To: shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Hou, Zhijie/侯 志杰 <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>; shveta malik <[email protected]>
Hi Shveta,
On Fri, May 15, 2026 at 9:28 AM shveta malik <[email protected]> wrote:
>
> Ashutosh, while testing further, I noticed that
> 'synchronized_standby_slots' does not filter duplicate entries. As an
> example, if user ends up giving one entry twice in priority
> configuration, then we will end up waiting on one slot twice rather
> than waiting on 2 different slots.
>
Thank you for raising this concern. It is indeed an issue that needs
fixing. We will ensure it is addressed in the next patch version.
--
With Regards,
Ashutosh Sharma.
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
@ 2026-06-03 11:00 ` Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
1 sibling, 1 reply; 25+ messages in thread
From: Ashutosh Sharma @ 2026-06-03 11:00 UTC (permalink / raw)
To: shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Hou, Zhijie/侯 志杰 <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>; shveta malik <[email protected]>
Hi Shveta,
On Fri, May 15, 2026 at 9:28 AM shveta malik <[email protected]> wrote:
>
>
> Ashutosh, while testing further, I noticed that
> 'synchronized_standby_slots' does not filter duplicate entries. As an
> example, if user ends up giving one entry twice in priority
> configuration, then we will end up waiting on one slot twice rather
> than waiting on 2 different slots.
>
> Example:
> alter system set synchronized_standby_slots = 'FIRST 2 (standby_1,
> standby_1, standby_2, standby_3)';
> select pg_reload_conf();
> insert into tab1 values (10), (20), (30);
> select pg_logical_slot_get_binary_changes('sub1', NULL, NULL,
> 'proto_version', '4', 'publication_names', 'pub1');
>
> The last statement works even though standby_2 and standby_3 do not
> exist. It consumes standby_1 twice and thinks that the required number
> of slots has caught-up.
>
> OTOH, if we use the same configuration for
> 'synchronous_standby_names', it correctly waits for standby_2 and does
> not count on standby_1 twice.
>
> alter system set synchronous_standby_names = 'FIRST 2 (standby_1,
> standby_1, standby_2, standby_3)';
> insert into tab1 values (10), (20), (30); ----> This will wait on standby_2
>
> This is perhaps because 'synchronous_standby_names ' waits on active
> WAL senders rather than repeated strings in configuration. But our
> code changes wait on the names present in 'synchronized_standby_slots'
> without filtering out duplicates.
>
May I know what your expectation is here? Would you like the check
hook for synchronized_standby_slots to automatically resolve
duplicates into a unique set of values, or should it detect duplicate
entries and raise an error so that the user can correct the
configuration?
If we automatically resolve duplicates, the user would still see the
GUC configured exactly as they specified, even though it would not
function the same way internally. For example, if a user sets:
FIRST 2 (s1, s1, s1, s2)
it might internally be resolved to:
FIRST 2 (s1, s2)
However, when the user runs SHOW, it would still display the original
configuration. This could give the user an incorrect impression of how
the setting is actually being interpreted. Because of this, I feel we
should treat duplicate entries as an invalid configuration and raise
an error.
As far as synchronous_standby_names is concerned, I can see that
configurations such as:
FIRST 2 (s1, s1, s1, s1)
are currently accepted, which I don't think is correct either and
should have been rejected, possibly resulted in the server startup
failure.
--
With Regards,
Ashutosh Sharma,
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
@ 2026-06-04 03:43 ` shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
0 siblings, 1 reply; 25+ messages in thread
From: shveta malik @ 2026-06-04 03:43 UTC (permalink / raw)
To: Ashutosh Sharma <[email protected]>; +Cc: Amit Kapila <[email protected]>; Hou, Zhijie/侯 志杰 <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>; shveta malik <[email protected]>
On Wed, Jun 3, 2026 at 4:30 PM Ashutosh Sharma <[email protected]> wrote:
>
> Hi Shveta,
>
> On Fri, May 15, 2026 at 9:28 AM shveta malik <[email protected]> wrote:
> >
> >
> > Ashutosh, while testing further, I noticed that
> > 'synchronized_standby_slots' does not filter duplicate entries. As an
> > example, if user ends up giving one entry twice in priority
> > configuration, then we will end up waiting on one slot twice rather
> > than waiting on 2 different slots.
> >
> > Example:
> > alter system set synchronized_standby_slots = 'FIRST 2 (standby_1,
> > standby_1, standby_2, standby_3)';
> > select pg_reload_conf();
> > insert into tab1 values (10), (20), (30);
> > select pg_logical_slot_get_binary_changes('sub1', NULL, NULL,
> > 'proto_version', '4', 'publication_names', 'pub1');
> >
> > The last statement works even though standby_2 and standby_3 do not
> > exist. It consumes standby_1 twice and thinks that the required number
> > of slots has caught-up.
> >
> > OTOH, if we use the same configuration for
> > 'synchronous_standby_names', it correctly waits for standby_2 and does
> > not count on standby_1 twice.
> >
> > alter system set synchronous_standby_names = 'FIRST 2 (standby_1,
> > standby_1, standby_2, standby_3)';
> > insert into tab1 values (10), (20), (30); ----> This will wait on standby_2
> >
> > This is perhaps because 'synchronous_standby_names ' waits on active
> > WAL senders rather than repeated strings in configuration. But our
> > code changes wait on the names present in 'synchronized_standby_slots'
> > without filtering out duplicates.
> >
>
> May I know what your expectation is here? Would you like the check
> hook for synchronized_standby_slots to automatically resolve
> duplicates into a unique set of values, or should it detect duplicate
> entries and raise an error so that the user can correct the
> configuration?
>
> If we automatically resolve duplicates, the user would still see the
> GUC configured exactly as they specified, even though it would not
> function the same way internally. For example, if a user sets:
>
> FIRST 2 (s1, s1, s1, s2)
>
> it might internally be resolved to:
>
> FIRST 2 (s1, s2)
>
> However, when the user runs SHOW, it would still display the original
> configuration. This could give the user an incorrect impression of how
> the setting is actually being interpreted. Because of this, I feel we
> should treat duplicate entries as an invalid configuration and raise
> an error.
>
> As far as synchronous_standby_names is concerned, I can see that
> configurations such as:
>
> FIRST 2 (s1, s1, s1, s1)
>
> are currently accepted, which I don't think is correct either and
> should have been rejected, possibly resulted in the server startup
> failure.
>
My preference, and original intent, was to accept duplicate entries
and skip them internally. Doc can be updated to say 'duplicate entries
are skipped'. A server startup failure due to duplicate entries in a
GUC does not seem right to me. If the alter-system command fails due
to duplicate entries, that is still fine, but a startup failure seems
excessive. But let's see what others have to say on this.
thanks
Shveta
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
@ 2026-06-04 07:36 ` Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
0 siblings, 1 reply; 25+ messages in thread
From: Ashutosh Sharma @ 2026-06-04 07:36 UTC (permalink / raw)
To: shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Hou, Zhijie/侯 志杰 <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi,
On Thu, Jun 4, 2026 at 9:14 AM shveta malik <[email protected]> wrote:
>
> On Wed, Jun 3, 2026 at 4:30 PM Ashutosh Sharma <[email protected]> wrote:
> >
> > Hi Shveta,
> >
> > On Fri, May 15, 2026 at 9:28 AM shveta malik <[email protected]> wrote:
> > >
> > >
> > > Ashutosh, while testing further, I noticed that
> > > 'synchronized_standby_slots' does not filter duplicate entries. As an
> > > example, if user ends up giving one entry twice in priority
> > > configuration, then we will end up waiting on one slot twice rather
> > > than waiting on 2 different slots.
> > >
> > > Example:
> > > alter system set synchronized_standby_slots = 'FIRST 2 (standby_1,
> > > standby_1, standby_2, standby_3)';
> > > select pg_reload_conf();
> > > insert into tab1 values (10), (20), (30);
> > > select pg_logical_slot_get_binary_changes('sub1', NULL, NULL,
> > > 'proto_version', '4', 'publication_names', 'pub1');
> > >
> > > The last statement works even though standby_2 and standby_3 do not
> > > exist. It consumes standby_1 twice and thinks that the required number
> > > of slots has caught-up.
> > >
> > > OTOH, if we use the same configuration for
> > > 'synchronous_standby_names', it correctly waits for standby_2 and does
> > > not count on standby_1 twice.
> > >
> > > alter system set synchronous_standby_names = 'FIRST 2 (standby_1,
> > > standby_1, standby_2, standby_3)';
> > > insert into tab1 values (10), (20), (30); ----> This will wait on standby_2
> > >
> > > This is perhaps because 'synchronous_standby_names ' waits on active
> > > WAL senders rather than repeated strings in configuration. But our
> > > code changes wait on the names present in 'synchronized_standby_slots'
> > > without filtering out duplicates.
> > >
> >
> > May I know what your expectation is here? Would you like the check
> > hook for synchronized_standby_slots to automatically resolve
> > duplicates into a unique set of values, or should it detect duplicate
> > entries and raise an error so that the user can correct the
> > configuration?
> >
> > If we automatically resolve duplicates, the user would still see the
> > GUC configured exactly as they specified, even though it would not
> > function the same way internally. For example, if a user sets:
> >
> > FIRST 2 (s1, s1, s1, s2)
> >
> > it might internally be resolved to:
> >
> > FIRST 2 (s1, s2)
> >
> > However, when the user runs SHOW, it would still display the original
> > configuration. This could give the user an incorrect impression of how
> > the setting is actually being interpreted. Because of this, I feel we
> > should treat duplicate entries as an invalid configuration and raise
> > an error.
> >
> > As far as synchronous_standby_names is concerned, I can see that
> > configurations such as:
> >
> > FIRST 2 (s1, s1, s1, s1)
> >
> > are currently accepted, which I don't think is correct either and
> > should have been rejected, possibly resulted in the server startup
> > failure.
> >
>
> My preference, and original intent, was to accept duplicate entries
> and skip them internally. Doc can be updated to say 'duplicate entries
> are skipped'. A server startup failure due to duplicate entries in a
> GUC does not seem right to me. If the alter-system command fails due
> to duplicate entries, that is still fine, but a startup failure seems
> excessive. But let's see what others have to say on this.
>
Okay, the attached patch adds the capability to automatically remove
duplicate entries from the synchronized_standby_slots list. In N of M
mode, if N > M after removing duplicate entries, an error is raised.
This behavior has been documented, and test cases verifying the change
have been added.
A few other minor comments from [1] have also been addressed. Please
have a look at the attached patches with these changes.
[1] - https://www.postgresql.org/message-id/CAJpy0uCKGCkfCXCd%3DtsDH5e85x155LsdbZW46WpWfsZJUe82bw%40mail.g...
--
With Regards,
Ashutosh Sharma.
Attachments:
[application/octet-stream] 0001-Refactor-syncrep-parsing-to-represent-bare-standby-l.patch (3.1K, 2-0001-Refactor-syncrep-parsing-to-represent-bare-standby-l.patch)
download | inline diff:
From 3dde13617ddda4156001be75984df3df5318c342 Mon Sep 17 00:00:00 2001
From: Ashutosh Sharma <[email protected]>
Date: Wed, 13 May 2026 06:59:58 +0000
Subject: [PATCH 1/3] Refactor syncrep parsing to represent bare standby lists
explicitly
The syncrep parser currently reduces a simple list form to FIRST 1
(SYNC_REP_PRIORITY). That is acceptable for synchronous_standby_names,
but it loses information about whether FIRST was explicitly written.
Introduce SYNC_REP_DEFAULT to represent the bare list form parsed
from standby_list. This allows callers to distinguish:
- explicit priority syntax (FIRST N (...) or N (...))
- quorum syntax (ANY N (...))
- simple list syntax without FIRST/ANY
With this change:
- syncrep grammar emits SYNC_REP_DEFAULT for bare standby lists
- check_synchronous_standby_names() maps SYNC_REP_DEFAULT to
SYNC_REP_PRIORITY, preserving existing synchronous_standby_names
behavior
This is a preparatory patch for future synchronized_standby_slots
changes, where callers can directly interpret SYNC_REP_DEFAULT as
plain-list semantics, while keeping existing synchronous_standby_names
semantics intact.
Per suggestion from Zhijie Hou <[email protected]>
---
src/backend/replication/syncrep.c | 4 ++++
src/backend/replication/syncrep_gram.y | 2 +-
src/include/replication/syncrep.h | 1 +
3 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/backend/replication/syncrep.c b/src/backend/replication/syncrep.c
index e0e30579c59..ae8ecfa0711 100644
--- a/src/backend/replication/syncrep.c
+++ b/src/backend/replication/syncrep.c
@@ -1100,6 +1100,10 @@ check_synchronous_standby_names(char **newval, void **extra, GucSource source)
return false;
}
+ /* Default to FIRST 1 (name ...) priority method if not specified */
+ if (syncrep_parse_result->syncrep_method == SYNC_REP_DEFAULT)
+ syncrep_parse_result->syncrep_method = SYNC_REP_PRIORITY;
+
/* GUC extra value must be guc_malloc'd, not palloc'd */
pconf = (SyncRepConfigData *)
guc_malloc(LOG, syncrep_parse_result->config_size);
diff --git a/src/backend/replication/syncrep_gram.y b/src/backend/replication/syncrep_gram.y
index 1b9d7b2edc4..f1550e109ef 100644
--- a/src/backend/replication/syncrep_gram.y
+++ b/src/backend/replication/syncrep_gram.y
@@ -65,7 +65,7 @@ result:
;
standby_config:
- standby_list { $$ = create_syncrep_config("1", $1, SYNC_REP_PRIORITY); }
+ standby_list { $$ = create_syncrep_config("1", $1, SYNC_REP_DEFAULT); }
| NUM '(' standby_list ')' { $$ = create_syncrep_config($1, $3, SYNC_REP_PRIORITY); }
| ANY NUM '(' standby_list ')' { $$ = create_syncrep_config($2, $4, SYNC_REP_QUORUM); }
| FIRST NUM '(' standby_list ')' { $$ = create_syncrep_config($2, $4, SYNC_REP_PRIORITY); }
diff --git a/src/include/replication/syncrep.h b/src/include/replication/syncrep.h
index b42b5862a70..130c7f6f242 100644
--- a/src/include/replication/syncrep.h
+++ b/src/include/replication/syncrep.h
@@ -34,6 +34,7 @@
/* syncrep_method of SyncRepConfigData */
#define SYNC_REP_PRIORITY 0
#define SYNC_REP_QUORUM 1
+#define SYNC_REP_DEFAULT 2
/*
* SyncRepGetCandidateStandbys returns an array of these structs,
--
2.43.0
[application/octet-stream] 0003-Add-FIRST-N-and-N-.-priority-syntax-to-synchronized_.patch (22.8K, 3-0003-Add-FIRST-N-and-N-.-priority-syntax-to-synchronized_.patch)
download | inline diff:
From 0e9e7c08da0644c745fe313498d59823c5f04c11 Mon Sep 17 00:00:00 2001
From: Ashutosh Sharma <[email protected]>
Date: Thu, 4 Jun 2026 07:16:02 +0000
Subject: [PATCH 3/3] Add FIRST N and N (...) priority syntax to
synchronized_standby_slots
Extend synchronized_standby_slots to support explicit priority
forms aligned with synchronous_standby_names.
- FIRST N (slot1, slot2, ...)
- N (slot1, slot2, ...) as shorthand for FIRST N
Implementation details:
- Use the SYNC_REP_DEFAULT parser distinction from the earlier
refactor so plain-list syntax remains separate from priority
syntax.
- Extend StandbySlotsHaveCaughtup() priority handling.
- Select slots in list order.
- Skip missing, logical, invalidated, and inactive lagging slots.
- Wait for active lagging higher-priority slots.
- Clarify duplicate handling for priority syntax in the
synchronized_standby_slots documentation.
- Simplify caught-up comments and clarify standby confirmation wait
comments to match the final control flow.
Tests and docs:
- Add coverage for FIRST behavior and shorthand N (...) behavior.
- Add plain-list disambiguation with first-prefixed slot names.
- Add FIRST duplicate-entry recovery coverage to show duplicates do
not create extra priority positions.
- Update docs for FIRST and shorthand priority syntax semantics.
- Clarify that duplicate slot names are ignored in priority-based
forms and preserve first-occurrence order.
Author: Satya Narlapuram <[email protected]>
Author: Ashutosh Sharma <[email protected]>
Reviewed-by: Shveta Malik <[email protected]>
Reviewed-by: Ajin Cherian <[email protected]>
Reviewed-by: Hou, Zhijie <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Surya Poondla <[email protected]>
Reviewed-by: Japin Li <[email protected]>
---
doc/src/sgml/config.sgml | 37 ++-
src/backend/replication/slot.c | 42 +--
.../053_synchronized_standby_slots_quorum.pl | 268 ++++++++++++++++--
3 files changed, 306 insertions(+), 41 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index a03ae94a12f..7d8f4b75717 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5194,6 +5194,7 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
sender processes must wait on before delivering decoded changes. This
parameter uses the following syntax:
<synopsis>
+ [FIRST] <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="parameter">slot_name</replaceable> [, ...] )
ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="parameter">slot_name</replaceable> [, ...] )
<replaceable class="parameter">slot_name</replaceable> [, ...]
</synopsis>
@@ -5205,16 +5206,35 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
<replaceable class="parameter">num_sync</replaceable>
must be an integer value greater than zero and must not exceed the
number of listed slots.
- Other forms supported by
- <xref linkend="guc-synchronous-standby-names"/>, such as priority
- syntax, are not supported.
+ </para>
+ <para>
+ The keyword <literal>FIRST</literal>, coupled with
+ <replaceable class="parameter">num_sync</replaceable>, specifies
+ priority-based semantics. Logical decoding will wait for the first
+ <replaceable class="parameter">num_sync</replaceable> available
+ physical slots in priority order (the order they appear in the list).
+ Missing, logical, or invalidated slots are skipped. Inactive slots are
+ skipped only while they are lagging. However, if a slot exists and is
+ valid and active but has not yet caught up, the system will wait for it
+ rather than skipping to lower-priority slots. If, after skipping
+ unusable slots, fewer than
+ <replaceable class="parameter">num_sync</replaceable> usable slots
+ remain, logical decoding waits until enough slots become usable and
+ caught up, or until the configuration is changed. The keyword
+ <literal>FIRST</literal> is optional in this form, so
+ <literal>2 (slot1, slot2, slot3)</literal> and
+ <literal>FIRST 2 (slot1, slot2, slot3)</literal> are equivalent.
</para>
<para>
If the same physical replication slot name appears more than once,
duplicate entries are ignored and only the first occurrence is used.
The semantics of <varname>synchronized_standby_slots</varname> are
therefore based on the unique set of listed slot names, preserving the
- original order of first occurrence. In particular,
+ original order of first occurrence. This means that, in
+ priority-based forms, duplicates do not create additional priority
+ positions: for example,
+ <literal>FIRST 2 (slot1, slot1, slot2, slot3)</literal> is treated the
+ same as <literal>FIRST 2 (slot1, slot2, slot3)</literal>. In particular,
<replaceable class="parameter">num_sync</replaceable> must not exceed
the number of unique listed slots.
</para>
@@ -5248,9 +5268,12 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
(<literal>synchronous_standby_names = 'ANY ...'</literal>), so that
logical decoding availability matches the commit durability guarantee.
</para>
- <para>
- <literal>ANY</literal> is case-insensitive.
- </para>
+ <para>
+ <literal>FIRST</literal> and <literal>ANY</literal> are case-insensitive.
+ If these keywords are used as the name of a replication slot,
+ the <replaceable class="parameter">slot_name</replaceable> must
+ be double-quoted.
+ </para>
<para>
The use of <varname>synchronized_standby_slots</varname> guarantees
that logical replication
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index b89becaf6ba..fb45a1cd581 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -3054,6 +3054,8 @@ CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
*
* slot1, slot2 -- wait for ALL listed slots
* ANY N (slot1, slot2, ...) -- wait for any N-of-M (quorum)
+ * FIRST N (slot1, slot2, ...) -- wait for first N in priority order
+ * N (slot1, slot2, ...) -- shorthand for FIRST N
*
* Note: Simple list syntax is interpreted as "wait for ALL" for this GUC,
* unlike synchronous_standby_names where it means "FIRST 1".
@@ -3094,14 +3096,6 @@ check_synchronized_standby_slots(char **newval, void **extra, GucSource source)
return false;
}
- if (syncrep_parse_result->syncrep_method == SYNC_REP_PRIORITY)
- {
- GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
- GUC_check_errmsg("priority syntax is not supported for parameter \"%s\"",
- "synchronized_standby_slots");
- return false;
- }
-
if (syncrep_parse_result->num_sync <= 0)
{
GUC_check_errmsg("number of synchronized standby slots (%d) must be greater than zero",
@@ -3319,6 +3313,12 @@ ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo *slot_states,
* Simple list (e.g., "slot1, slot2"):
* ALL slots must have caught up. Returns false otherwise.
*
+ * FIRST N (e.g., "FIRST 2 (slot1, slot2, slot3)"):
+ * Wait for the first N eligible slots in priority order. Skips missing,
+ * invalid, logical, and inactive-lagging slots to find N eligible slots.
+ * If an active slot is lagging, waits for it (does not skip to lower
+ * priority slots).
+ *
* ANY N (e.g., "ANY 2 (slot1, slot2, slot3)"):
* Wait for any N eligible slots. Skips missing, invalid, logical, and
* lagging slots (inactive or active) to find N slots that have caught up.
@@ -3369,14 +3369,18 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
* on the first slot that is missing/invalid/logical, or the first slot
* that is lagging (inactive or active).
*
- * wait_for_all = false means we select N from M candidates (ANY N syntax).
- * In this mode, slots already caught up are counted even if inactive, and
- * lagging slots are skipped until enough slots have caught up.
- * Duplicate configured slot names do not appear here because the check hook
- * compacts them out of the parsed configuration.
+ * wait_for_all = false means we select N from M candidates (FIRST N or
+ * ANY N syntax). In this mode, slots already caught up are counted even if
+ * inactive. In FIRST N mode, we skip missing/invalid/logical slots and
+ * lagging inactive slots, but wait for an active lagging slot with higher
+ * priority. In ANY N mode, we skip lagging slots (inactive or active) to
+ * find any N that have caught up. Duplicate configured slot names do not
+ * appear here because the check hook compacts them out of the parsed
+ * configuration.
*/
required = synchronized_standby_slots_config->num_sync;
- wait_for_all = (required == synchronized_standby_slots_config->nslotnames);
+ wait_for_all =
+ (synchronized_standby_slots_config->syncrep_method == SYNC_REP_DEFAULT);
/*
* Allocate array to track slot states. Size it to the total number of
@@ -3457,6 +3461,8 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
* If it is active and lagging, report it as lagging.
*
* In ALL mode: must wait for it.
+ * In FIRST N (priority) mode: lagging active slots block, while
+ * inactive slots can be skipped.
* In ANY N (quorum) mode: skip and use another slot.
*/
slot_states[num_slot_states].slot_name = name;
@@ -3465,7 +3471,9 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
slot_states[num_slot_states].restart_lsn = restart_lsn;
num_slot_states++;
- if (wait_for_all)
+ if (wait_for_all ||
+ (!inactive &&
+ synchronized_standby_slots_config->syncrep_method == SYNC_REP_PRIORITY))
break;
goto next_slot;
}
@@ -3478,7 +3486,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
caught_up_slot_num++;
- /* Stop processing if the required number of slots have caught up. */
+ /* Stop once the required number of slots have caught up. */
if (caught_up_slot_num >= required)
break;
@@ -3542,7 +3550,7 @@ WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
ProcessConfigFile(PGC_SIGHUP);
}
- /* Exit if done waiting for every slot. */
+ /* Exit once the configured synchronized_standby_slots requirement is met. */
if (StandbySlotsHaveCaughtup(wait_for_lsn, WARNING))
break;
diff --git a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
index 760f10f38a0..67a5b1d9657 100644
--- a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
+++ b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
@@ -4,6 +4,7 @@
# Test synchronized_standby_slots with different syntax modes:
# - Plain list (ALL mode): slot1, slot2
# - ANY N (quorum mode): ANY N (slot1, slot2, ...)
+# - FIRST N (priority mode): FIRST N (slot1, slot2, ...)
#
# Setup: a 3-node cluster with one primary, two physical standbys, and a
# logical decoding client using a failover-enabled slot.
@@ -25,6 +26,12 @@
# - Skips missing/invalid/logical slots and lagging slots (inactive or active)
# to find N caught-up slots
#
+# C) FIRST N (sb1_slot, sb2_slot) (priority mode)
+# - Selects first N slots in priority order (list order)
+# - Skips missing/invalid/logical slots and inactive lagging slots,
+# but waits for active lagging slots
+# - FIRST 1 works with one slot down (unlike plain list)
+
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
@@ -211,16 +218,168 @@ is($decoded_bc, '1',
'plain list: works when all standbys are up');
##################################################
-# PART D: ANY 2 waits on an active lagging slot
+# PART D: Verify FIRST N priority semantics
##################################################
-# Stop standby1 so sb1_slot can be controlled by a raw replication connection
-# that keeps the slot active while lagging.
+# FIRST N should:
+# 1. Select first N slots in priority order (list order)
+# 2. Skip missing/invalid/logical slots and inactive lagging slots to find
+# N caught-up slots
+# 3. Wait for active lagging slots (not skip to lower priority)
+
+# Test FIRST 2 (sb1_slot, sb2_slot) with both up; should wait for both.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 2 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_2_both_up');"
+);
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+my $decoded_e2 = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%first_2_both_up%';});
+is($decoded_e2, '1',
+ 'FIRST 2: decoding works when all required slots are up');
+
+# Test FIRST 1 (sb1_slot, sb2_slot) with sb1_slot unavailable.
$standby1->stop;
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_1_skip_unavailable');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+# FIRST 1 should skip sb1_slot (unavailable) and use sb2_slot.
+my $decoded_e1 = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%first_1_skip_unavailable%';});
+is($decoded_e1, '1',
+ 'FIRST 1: skips unavailable first slot, uses second slot');
+
+# Test shorthand priority syntax: N (...) means FIRST N (...).
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'num_1_shorthand_priority');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+my $decoded_num1 = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%num_1_shorthand_priority%';});
+is($decoded_num1, '1',
+ '1 (...): shorthand priority syntax behaves like FIRST 1');
+
+##################################################
+# PART E: FIRST 1 and ANY 2 wait on an active lagging slot
+##################################################
+
+# Bring standby1 back so sb1_slot is active and caught up.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# To test the active-but-lagging slot path deterministically, we open a raw
+# replication connection to sb1_slot starting from a deliberately old LSN.
+# psql in replication mode never sends Standby Status Update messages, so
+# the walsender keeps sb1_slot's active_pid set but restart_lsn never
+# advances.
+
+# Stop standby1 so its walsender releases sb1_slot, allowing our replication
+# connection below to acquire it.
+$standby1->stop;
+
+# Capture a safely old LSN to stream from, before the test WAL record.
my $old_lsn = $primary->safe_psql('postgres',
"SELECT pg_current_wal_lsn();");
+# FIRST 1 must wait for the highest-priority slot when it is active but lagging.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+my $first_lag_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_1_lagging_blocks');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+# Open a raw replication connection to sb1_slot starting from $old_lsn.
+# This activates the slot (active_pid IS NOT NULL) while keeping restart_lsn
+# frozen below $first_lag_lsn for the lifetime of the connection.
+my $repl_first = $primary->background_psql(
+ 'postgres',
+ replication => 'database',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$repl_first->query_until(
+ qr/^$/,
+ "START_REPLICATION SLOT sb1_slot PHYSICAL $old_lsn;\n");
+
+# Wait until sb1_slot shows active_pid, confirming the walsender is live.
+$primary->poll_query_until('postgres', q{
+ SELECT active_pid IS NOT NULL
+ FROM pg_replication_slots
+ WHERE slot_name = 'sb1_slot'
+}) or die "replication connection did not activate sb1_slot";
+
+# sb1_slot is now active and its restart_lsn is behind $first_lag_lsn.
+# Start logical decoding in the background; it must block.
+my $bg_first = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_first->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'FIRST 1: decoding waits for active lagging higher-priority slot');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_first->quit;
+$repl_first->quit;
+
+# Ensure the previous replication connection has fully released sb1_slot
+# before reusing it in the next subtest.
+$primary->poll_query_until('postgres', q{
+ SELECT active_pid IS NULL
+ FROM pg_replication_slots
+ WHERE slot_name = 'sb1_slot'
+}) or die "replication connection did not release sb1_slot";
+
+# Consume the change so the slot is clean for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# ANY 2 must also wait when only one of two required slots has caught up.
+# Reuse the same technique: open a raw replication connection to sb1_slot
+# from $old_lsn so it is active but its restart_lsn stays behind the target.
+
+# Capture another old LSN baseline before the next test WAL record.
+$old_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_current_wal_lsn();");
+
$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
"'ANY 2 (sb1_slot, sb2_slot)'");
$primary->reload;
@@ -283,7 +442,54 @@ $primary->wait_for_replay_catchup($standby1);
##################################################
-# PART E: Duplicate entries are ignored for quorum counting
+# PART F: Plain list with first-prefixed slot name still means ALL mode
+##################################################
+
+# Create a slot name starting with "first_" for parser disambiguation checks.
+$primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('first_slot');");
+
+# If simple-list syntax starts with a slot name like "first_slot", it must
+# still be treated as ALL mode (not as explicit FIRST N syntax).
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'first_slot, sb2_slot'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_prefix_all_mode_blocks');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+$log_offset = -s $primary->logfile;
+
+$bg = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+# Plain list must require all listed slots; first_slot is intentionally inactive.
+$primary->wait_for_log(
+ qr/replication slot \"first_slot\" specified in parameter "synchronized_standby_slots" does not have active_pid/,
+ $log_offset);
+
+pass('plain list with first-prefixed slot name blocks in ALL mode');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg->quit;
+
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+##################################################
+# PART G: Duplicate entries are ignored for quorum counting
##################################################
# Stop standby2 so only sb1_slot can catch up.
@@ -324,6 +530,46 @@ $primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
$primary->reload;
$bg_dup->quit;
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# FIRST duplicates must also not create extra priority positions.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 2 (sb1_slot, sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_duplicate_entries_ignored');"
+);
+$primary->wait_for_replay_catchup($standby1);
+
+my $bg_first_dup = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_first_dup->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'FIRST duplicates are ignored when counting priority slots');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_first_dup->quit;
+
# Consume the change for the next test.
$primary->safe_psql('postgres',
q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
@@ -334,7 +580,7 @@ $primary->wait_for_replay_catchup($standby2);
##################################################
-# PART F: Verify GUC validation rejects bad values
+# PART H: Verify GUC validation rejects bad values
##################################################
my ($result, $stdout, $stderr);
@@ -351,18 +597,6 @@ like($stderr, qr/ERROR/,
like($stderr, qr/ERROR/,
'GUC rejects malformed ANY syntax');
-# Priority syntax is not supported by synchronized_standby_slots yet
-($result, $stdout, $stderr) = $primary->psql('postgres',
- "ALTER SYSTEM SET synchronized_standby_slots = 'FIRST 1 (sb1_slot, sb2_slot)';");
-like($stderr, qr/priority syntax is not supported/,
- 'GUC rejects FIRST syntax');
-
-# Legacy priority syntax is not supported by synchronized_standby_slots yet
-($result, $stdout, $stderr) = $primary->psql('postgres',
- "ALTER SYSTEM SET synchronized_standby_slots = '1 (sb1_slot, sb2_slot)';");
-like($stderr, qr/priority syntax is not supported/,
- 'GUC rejects legacy priority syntax');
-
# Invalid slot name
($result, $stdout, $stderr) = $primary->psql('postgres',
"ALTER SYSTEM SET synchronized_standby_slots = 'ANY 1 (INVALID_UPPER)';");
--
2.43.0
[application/octet-stream] 0002-Add-ANY-N-semantics-to-synchronized_standby_slots.patch (42.9K, 4-0002-Add-ANY-N-semantics-to-synchronized_standby_slots.patch)
download | inline diff:
From 673c54267064e17a35a1907644bf6859759827b0 Mon Sep 17 00:00:00 2001
From: Ashutosh Sharma <[email protected]>
Date: Thu, 4 Jun 2026 07:09:08 +0000
Subject: [PATCH 2/3] Add ANY N semantics to synchronized_standby_slots
Extend synchronized_standby_slots with quorum syntax for logical
failover slot synchronization:
- ANY N (slot1, slot2, ...)
Plain-list semantics are preserved as-is:
- slot1, slot2 continues to mean all listed slots are required
Implementation details:
- Reuse syncrep parser infrastructure in the GUC check hook and
map parsed output into synchronized_standby_slots semantics.
- Consume SYNC_REP_DEFAULT from the preparatory parser refactor to
distinguish plain-list syntax from explicit parser modes.
- In StandbySlotsHaveCaughtup(), enforce mode-specific behavior for:
- existing all-listed-slots semantics (plain list)
- quorum N-of-M behavior (ANY N)
- Validation rejects configurations where N exceeds the number of
listed slots.
- Ignore duplicate synchronized_standby_slots entries, preserving the
first occurrence and applying semantics to the resulting unique list.
- Clarify synchronized_standby_slots comments and lagging restart_lsn
reporting to match the implemented behavior.
Tests and docs:
- Add recovery coverage for plain-list behavior and ANY quorum
behavior, including lagging-slot and validation-error scenarios.
- Add duplicate-entry recovery coverage for synchronized_standby_slots.
- Document ANY syntax and clarify plain-list behavior for this GUC.
- Document that duplicate slot names are ignored and counted only once.
Author: Satya Narlapuram <[email protected]>
Author: Ashutosh Sharma <[email protected]>
Reviewed-by: Shveta Malik <[email protected]>
Reviewed-by: Ajin Cherian <[email protected]>
Reviewed-by: Hou, Zhijie <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Surya Poondla <[email protected]>
Reviewed-by: Japin Li <[email protected]>
---
doc/src/sgml/config.sgml | 81 ++-
src/backend/replication/slot.c | 496 ++++++++++++++----
.../053_synchronized_standby_slots_quorum.pl | 378 +++++++++++++
3 files changed, 830 insertions(+), 125 deletions(-)
create mode 100644 src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index cebae4b6d1b..a03ae94a12f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5190,17 +5190,78 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
</term>
<listitem>
<para>
- A comma-separated list of streaming replication standby server slot names
- that logical WAL sender processes will wait for. Logical WAL sender processes
- will send decoded changes to plugins only after the specified replication
- slots confirm receiving WAL. This guarantees that logical replication
+ Specifies the streaming replication standby slots that logical WAL
+ sender processes must wait on before delivering decoded changes. This
+ parameter uses the following syntax:
+<synopsis>
+ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="parameter">slot_name</replaceable> [, ...] )
+<replaceable class="parameter">slot_name</replaceable> [, ...]
+</synopsis>
+ where <replaceable class="parameter">num_sync</replaceable> is
+ the number of physical replication slots that must confirm WAL
+ receipt before logical decoding proceeds,
+ and <replaceable class="parameter">slot_name</replaceable>
+ is the name of a physical replication slot.
+ <replaceable class="parameter">num_sync</replaceable>
+ must be an integer value greater than zero and must not exceed the
+ number of listed slots.
+ Other forms supported by
+ <xref linkend="guc-synchronous-standby-names"/>, such as priority
+ syntax, are not supported.
+ </para>
+ <para>
+ If the same physical replication slot name appears more than once,
+ duplicate entries are ignored and only the first occurrence is used.
+ The semantics of <varname>synchronized_standby_slots</varname> are
+ therefore based on the unique set of listed slot names, preserving the
+ original order of first occurrence. In particular,
+ <replaceable class="parameter">num_sync</replaceable> must not exceed
+ the number of unique listed slots.
+ </para>
+ <para>
+ A plain comma-separated list without a keyword specifies that
+ <emphasis>all</emphasis> listed physical slots must confirm WAL
+ receipt. This differs from <xref linkend="guc-synchronous-standby-names"/>
+ where a simple list means <literal>FIRST 1</literal>. For
+ <varname>synchronized_standby_slots</varname>, requiring all slots
+ provides safer failover semantics by default.
+ </para>
+ <para>
+ The keyword <literal>ANY</literal>, coupled with
+ <replaceable class="parameter">num_sync</replaceable>, specifies
+ quorum-based semantics. Logical decoding proceeds once at least
+ <replaceable class="parameter">num_sync</replaceable> of the listed
+ slots have caught up. Missing, logical, and invalidated slots are
+ skipped when determining candidates. Lagging slots (inactive or
+ active) simply do not count toward the required number until they
+ catch up.
+ If fewer than <replaceable class="parameter">num_sync</replaceable>
+ slots have caught up at a given moment, logical decoding waits until
+ that threshold is reached.
+ i.e., there is no priority ordering.
+ For example, a setting of <literal>ANY 1 (sb1_slot, sb2_slot)</literal>
+ allows logical decoding to proceed as soon as either physical slot has
+ confirmed WAL receipt. If none of the slots are available or have
+ caught up, logical decoding waits until at least one slot meets the
+ required condition. This is useful in conjunction with
+ quorum-based synchronous replication
+ (<literal>synchronous_standby_names = 'ANY ...'</literal>), so that
+ logical decoding availability matches the commit durability guarantee.
+ </para>
+ <para>
+ <literal>ANY</literal> is case-insensitive.
+ </para>
+ <para>
+ The use of <varname>synchronized_standby_slots</varname> guarantees
+ that logical replication
failover slots do not consume changes until those changes are received
- and flushed to corresponding physical standbys. If a
+ and flushed to the required physical standbys. If a
logical replication connection is meant to switch to a physical standby
after the standby is promoted, the physical replication slot for the
standby should be listed here. Note that logical replication will not
- proceed if the slots specified in the
- <varname>synchronized_standby_slots</varname> do not exist or are invalidated.
+ proceed if the required number of physical slots specified in
+ <varname>synchronized_standby_slots</varname> do not exist or are
+ invalidated.
Additionally, the replication management functions
<link linkend="pg-replication-slot-advance">
<function>pg_replication_slot_advance</function></link>,
@@ -5208,9 +5269,9 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
<function>pg_logical_slot_get_changes</function></link>, and
<link linkend="pg-logical-slot-peek-changes">
<function>pg_logical_slot_peek_changes</function></link>,
- when used with logical failover slots, will block until all
- physical slots specified in <varname>synchronized_standby_slots</varname> have
- confirmed WAL receipt.
+ when used with logical failover slots, will block until the required
+ physical slots specified in <varname>synchronized_standby_slots</varname>
+ have confirmed WAL receipt.
</para>
<para>
The standbys corresponding to the physical replication slots in
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index d7fb9f5a67f..b89becaf6ba 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -50,6 +50,7 @@
#include "replication/logicallauncher.h"
#include "replication/slotsync.h"
#include "replication/slot.h"
+#include "replication/syncrep.h"
#include "replication/walsender_private.h"
#include "storage/fd.h"
#include "storage/ipc.h"
@@ -91,11 +92,19 @@ typedef struct ReplicationSlotOnDisk
* Note: this must be a flat representation that can be held in a single chunk
* of guc_malloc'd memory, so that it can be stored as the "extra" data for the
* synchronized_standby_slots GUC.
+ *
+ * The layout mirrors SyncRepConfigData so that the same quorum and priority
+ * semantics can be expressed. The syncrep_method field uses the
+ * SYNC_REP_DEFAULT, SYNC_REP_PRIORITY, and SYNC_REP_QUORUM constants from
+ * syncrep.h.
*/
typedef struct
{
- /* Number of slot names in the slot_names[] */
- int nslotnames;
+ int config_size; /* total size of this struct, in bytes */
+ int num_sync; /* number of slots that must confirm WAL
+ * receipt before logical decoding proceeds */
+ uint8 syncrep_method; /* SYNC_REP_* method */
+ int nslotnames; /* number of slot names that follow */
/*
* slot_names contains 'nslotnames' consecutive null-terminated C strings.
@@ -103,6 +112,28 @@ typedef struct
char slot_names[FLEXIBLE_ARRAY_MEMBER];
} SyncStandbySlotsConfigData;
+/*
+ * State of a replication slot specified in synchronized_standby_slots GUC.
+ */
+typedef enum
+{
+ SS_SLOT_NOT_FOUND, /* slot does not exist */
+ SS_SLOT_LOGICAL, /* slot is logical, not physical */
+ SS_SLOT_INVALIDATED, /* slot has been invalidated */
+ SS_SLOT_INACTIVE_LAGGING, /* slot is inactive and behind wait_for_lsn */
+ SS_SLOT_ACTIVE_LAGGING, /* slot is active and behind wait_for_lsn */
+} SyncStandbySlotsState;
+
+/*
+ * Information about a synchronized standby slot's state.
+ */
+typedef struct
+{
+ const char *slot_name; /* name of the slot */
+ SyncStandbySlotsState state; /* state of the slot */
+ XLogRecPtr restart_lsn; /* current restart_lsn (valid for lagging states) */
+} SyncStandbySlotsStateInfo;
+
/*
* Lookup table for slot invalidation causes.
*/
@@ -2963,94 +2994,190 @@ GetSlotInvalidationCauseName(ReplicationSlotInvalidationCause cause)
}
/*
- * A helper function to validate slots specified in GUC synchronized_standby_slots.
+ * Remove duplicate member names from a flat SyncRepConfigData in place.
*
- * The rawname will be parsed, and the result will be saved into *elemlist.
+ * The first occurrence of each name is kept and input order is preserved.
*/
-static bool
-validate_sync_standby_slots(char *rawname, List **elemlist)
+static void
+CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
{
- /* Verify syntax and parse string into a list of identifiers */
- if (!SplitIdentifierString(rawname, ',', elemlist))
- {
- GUC_check_errdetail("List syntax is invalid.");
- return false;
- }
+ char *src_name;
+ char *dst_name;
+ int unique_members = 0;
+ Size unique_size = offsetof(SyncRepConfigData, member_names);
+
+ src_name = config->member_names;
+ dst_name = config->member_names;
- /* Iterate the list to validate each slot name */
- foreach_ptr(char, name, *elemlist)
+ for (int i = 0; i < config->nmembers; i++)
{
- int err_code;
- char *err_msg = NULL;
- char *err_hint = NULL;
+ char *existing_name;
+ size_t name_size;
+ bool duplicate = false;
- if (!ReplicationSlotValidateNameInternal(name, false, &err_code,
- &err_msg, &err_hint))
+ name_size = strlen(src_name) + 1;
+ existing_name = config->member_names;
+
+ for (int j = 0; j < unique_members; j++)
{
- GUC_check_errcode(err_code);
- GUC_check_errdetail("%s", err_msg);
- if (err_hint != NULL)
- GUC_check_errhint("%s", err_hint);
- return false;
+ if (strcmp(existing_name, src_name) == 0)
+ {
+ duplicate = true;
+ break;
+ }
+
+ existing_name += strlen(existing_name) + 1;
}
+
+ if (!duplicate)
+ {
+ if (dst_name != src_name)
+ memmove(dst_name, src_name, name_size);
+
+ dst_name += name_size;
+ unique_members++;
+ unique_size += name_size;
+ }
+
+ src_name += name_size;
}
- return true;
+ config->nmembers = unique_members;
+ config->config_size = (int) unique_size;
}
/*
* GUC check_hook for synchronized_standby_slots
+ *
+ * This reuses the syncrep_yyparse/syncrep_scanner infrastructure that is
+ * also used for synchronous_standby_names, and accepts these forms:
+ *
+ * slot1, slot2 -- wait for ALL listed slots
+ * ANY N (slot1, slot2, ...) -- wait for any N-of-M (quorum)
+ *
+ * Note: Simple list syntax is interpreted as "wait for ALL" for this GUC,
+ * unlike synchronous_standby_names where it means "FIRST 1".
+ *
+ * After parsing, we validate every name as a legal replication slot name,
+ * omit duplicate entries while preserving first-occurrence order, and then
+ * apply the resulting unique list to the configured semantics.
*/
bool
check_synchronized_standby_slots(char **newval, void **extra, GucSource source)
{
- char *rawname;
- char *ptr;
- List *elemlist;
- int size;
- bool ok;
- SyncStandbySlotsConfigData *config;
-
- if ((*newval)[0] == '\0')
- return true;
+ if (*newval != NULL && (*newval)[0] != '\0')
+ {
+ yyscan_t scanner;
+ int parse_rc;
+ SyncStandbySlotsConfigData *config;
+ const char *mname;
+
+ /* Result of parsing is returned in one of these two variables */
+ SyncRepConfigData *syncrep_parse_result = NULL;
+ char *syncrep_parse_error_msg = NULL;
+
+ /* Parse the synchronized standby slots configuration */
+ syncrep_scanner_init(*newval, &scanner);
+ parse_rc = syncrep_yyparse(&syncrep_parse_result,
+ &syncrep_parse_error_msg,
+ scanner);
+ syncrep_scanner_finish(scanner);
+
+ if (parse_rc != 0 || syncrep_parse_result == NULL)
+ {
+ GUC_check_errcode(ERRCODE_SYNTAX_ERROR);
+ if (syncrep_parse_error_msg)
+ GUC_check_errdetail("%s", syncrep_parse_error_msg);
+ else
+ GUC_check_errdetail("\"%s\" parser failed.",
+ "synchronized_standby_slots");
+ return false;
+ }
- /* Need a modifiable copy of the GUC string */
- rawname = pstrdup(*newval);
+ if (syncrep_parse_result->syncrep_method == SYNC_REP_PRIORITY)
+ {
+ GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
+ GUC_check_errmsg("priority syntax is not supported for parameter \"%s\"",
+ "synchronized_standby_slots");
+ return false;
+ }
- /* Now verify if the specified slots exist and have correct type */
- ok = validate_sync_standby_slots(rawname, &elemlist);
+ if (syncrep_parse_result->num_sync <= 0)
+ {
+ GUC_check_errmsg("number of synchronized standby slots (%d) must be greater than zero",
+ syncrep_parse_result->num_sync);
+ return false;
+ }
- if (!ok || elemlist == NIL)
- {
- pfree(rawname);
- list_free(elemlist);
- return ok;
- }
+ /* validate every member name as a slot name */
+ mname = syncrep_parse_result->member_names;
- /* Compute the size required for the SyncStandbySlotsConfigData struct */
- size = offsetof(SyncStandbySlotsConfigData, slot_names);
- foreach_ptr(char, slot_name, elemlist)
- size += strlen(slot_name) + 1;
+ for (int i = 0; i < syncrep_parse_result->nmembers; i++)
+ {
+ int err_code;
+ char *err_msg = NULL;
+ char *err_hint = NULL;
- /* GUC extra value must be guc_malloc'd, not palloc'd */
- config = (SyncStandbySlotsConfigData *) guc_malloc(LOG, size);
- if (!config)
- return false;
+ if (!ReplicationSlotValidateNameInternal(mname, false, &err_code,
+ &err_msg, &err_hint))
+ {
+ GUC_check_errcode(err_code);
+ GUC_check_errdetail("%s", err_msg);
+ if (err_hint != NULL)
+ GUC_check_errhint("%s", err_hint);
+ return false;
+ }
- /* Transform the data into SyncStandbySlotsConfigData */
- config->nslotnames = list_length(elemlist);
+ mname += strlen(mname) + 1;
+ }
- ptr = config->slot_names;
- foreach_ptr(char, slot_name, elemlist)
- {
- strcpy(ptr, slot_name);
- ptr += strlen(slot_name) + 1;
- }
+ /* Omit duplicate slot names so one slot is considered only once. */
+ CompactSyncRepConfigMemberNames(syncrep_parse_result);
- *extra = config;
+ /*
+ * For synchronized_standby_slots, a comma-separated list means all
+ * listed slots are required. The syncrep parser preserves this shape
+ * as SYNC_REP_DEFAULT, so map num_sync to nmembers to enforce all-mode
+ * semantics after removing duplicate names.
+ */
+ if (syncrep_parse_result->syncrep_method == SYNC_REP_DEFAULT)
+ syncrep_parse_result->num_sync = syncrep_parse_result->nmembers;
+
+ /* Reject num_sync > nmembers after duplicates have been omitted. */
+ if (syncrep_parse_result->num_sync > syncrep_parse_result->nmembers)
+ {
+ GUC_check_errmsg("number of synchronized standby slots (%d) must not exceed the number of unique listed slots (%d)",
+ syncrep_parse_result->num_sync,
+ syncrep_parse_result->nmembers);
+ return false;
+ }
+
+ /*
+ * Build SyncStandbySlotsConfigData from the parsed SyncRepConfigData.
+ * Since the structures have identical layout, we can use the same
+ * config_size.
+ */
+ config = (SyncStandbySlotsConfigData *)
+ guc_malloc(LOG, syncrep_parse_result->config_size);
+ if (!config)
+ return false;
+
+ config->config_size = syncrep_parse_result->config_size;
+ config->num_sync = syncrep_parse_result->num_sync;
+ config->syncrep_method = syncrep_parse_result->syncrep_method;
+ config->nslotnames = syncrep_parse_result->nmembers;
+
+ /* Copy all slot names in one operation */
+ memcpy(config->slot_names,
+ syncrep_parse_result->member_names,
+ syncrep_parse_result->config_size -
+ offsetof(SyncRepConfigData, member_names));
+
+ *extra = config;
+ }
+ else
+ *extra = NULL;
- pfree(rawname);
- list_free(elemlist);
return true;
}
@@ -3099,18 +3226,117 @@ SlotExistsInSyncStandbySlots(const char *slot_name)
}
/*
- * Return true if the slots specified in synchronized_standby_slots have caught up to
- * the given WAL location, false otherwise.
+ * Report problem states for synchronized standby slots that prevented the
+ * catch-up requirement from being met.
+ */
+static void
+ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo *slot_states,
+ int num_slot_states, int elevel,
+ XLogRecPtr wait_for_lsn)
+{
+ for (int i = 0; i < num_slot_states; i++)
+ {
+ const char *slot_name = slot_states[i].slot_name;
+
+ switch (slot_states[i].state)
+ {
+ case SS_SLOT_NOT_FOUND:
+ ereport(elevel,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" does not exist",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
+ slot_name),
+ errhint("Create the replication slot \"%s\" or amend parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_LOGICAL:
+ ereport(elevel,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot specify logical replication slot \"%s\" in parameter \"%s\"",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting for correction on replication slot \"%s\".",
+ slot_name),
+ errhint("Remove the logical replication slot \"%s\" from parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_INVALIDATED:
+ ereport(elevel,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("physical replication slot \"%s\" specified in parameter \"%s\" has been invalidated",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
+ slot_name),
+ errhint("Drop and recreate the replication slot \"%s\", or amend parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_INACTIVE_LAGGING:
+ ereport(elevel,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
+ slot_name),
+ errhint("Start the standby associated with the replication slot \"%s\", or amend parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_ACTIVE_LAGGING:
+ if (!XLogRecPtrIsValid(slot_states[i].restart_lsn))
+ ereport(DEBUG1,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("The slot's restart_lsn is not yet set; required LSN is %X/%X.",
+ LSN_FORMAT_ARGS(wait_for_lsn)));
+ else
+ ereport(DEBUG1,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("The slot's restart_lsn %X/%X is behind the required %X/%X.",
+ LSN_FORMAT_ARGS(slot_states[i].restart_lsn),
+ LSN_FORMAT_ARGS(wait_for_lsn)));
+ break;
+
+ default:
+ /* Should not happen */
+ Assert(false);
+ break;
+ }
+ }
+}
+
+/*
+ * Return true if the required standby slots have caught up to the given WAL
+ * location, false otherwise.
+ *
+ * The behavior depends on the synchronized_standby_slots configuration:
*
- * The elevel parameter specifies the error level used for logging messages
- * related to slots that do not exist, are invalidated, or are inactive.
+ * Simple list (e.g., "slot1, slot2"):
+ * ALL slots must have caught up. Returns false otherwise.
+ *
+ * ANY N (e.g., "ANY 2 (slot1, slot2, slot3)"):
+ * Wait for any N eligible slots. Skips missing, invalid, logical, and
+ * lagging slots (inactive or active) to find N slots that have caught up.
+ *
+ * The elevel parameter specifies the error level used for reporting issues
+ * related to the slots specified in synchronized_standby_slots when the
+ * catch-up requirement is not met.
*/
bool
StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
{
const char *name;
int caught_up_slot_num = 0;
+ int required;
XLogRecPtr min_restart_lsn = InvalidXLogRecPtr;
+ bool wait_for_all;
+ SyncStandbySlotsStateInfo *slot_states;
+ int num_slot_states = 0;
/*
* Don't need to wait for the standbys to catch up if there is no value in
@@ -3134,12 +3360,44 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
ss_oldest_flush_lsn >= wait_for_lsn)
return true;
+ /*
+ * Determine how many slots are required and whether we're in "wait for
+ * ALL" mode versus "wait for N-of-M" mode.
+ *
+ * wait_for_all = true means we need ALL slots to be ready (simple
+ * list syntax like "slot1, slot2"). In this mode, we stop checking
+ * on the first slot that is missing/invalid/logical, or the first slot
+ * that is lagging (inactive or active).
+ *
+ * wait_for_all = false means we select N from M candidates (ANY N syntax).
+ * In this mode, slots already caught up are counted even if inactive, and
+ * lagging slots are skipped until enough slots have caught up.
+ * Duplicate configured slot names do not appear here because the check hook
+ * compacts them out of the parsed configuration.
+ */
+ required = synchronized_standby_slots_config->num_sync;
+ wait_for_all = (required == synchronized_standby_slots_config->nslotnames);
+
+ /*
+ * Allocate array to track slot states. Size it to the total number of
+ * configured slots since in the worst case all could have problem states.
+ */
+ slot_states = palloc_array(SyncStandbySlotsStateInfo,
+ synchronized_standby_slots_config->nslotnames);
+
/*
* To prevent concurrent slot dropping and creation while filtering the
* slots, take the ReplicationSlotControlLock outside of the loop.
*/
LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+ /*
+ * Iterate through configured slots, checking their state and tracking
+ * how many have caught up. Problem states are recorded for deferred
+ * reporting: missing/logical/invalidated slots, and lagging slots
+ * (inactive or active). Messages are only emitted if the catch-up
+ * requirement isn't met.
+ */
name = synchronized_standby_slots_config->slot_names;
for (int i = 0; i < synchronized_standby_slots_config->nslotnames; i++)
{
@@ -3150,35 +3408,28 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
slot = SearchNamedReplicationSlot(name, false);
- /*
- * If a slot name provided in synchronized_standby_slots does not
- * exist, report a message and exit the loop.
- */
if (!slot)
{
- ereport(elevel,
- errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("replication slot \"%s\" specified in parameter \"%s\" does not exist",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
- name),
- errhint("Create the replication slot \"%s\" or amend parameter \"%s\".",
- name, "synchronized_standby_slots"));
- break;
+ /* Record Slot State */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state = SS_SLOT_NOT_FOUND;
+ num_slot_states++;
+
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
- /* Same as above: if a slot is not physical, exit the loop. */
if (SlotIsLogical(slot))
{
- ereport(elevel,
- errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("cannot specify logical replication slot \"%s\" in parameter \"%s\"",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting for correction on replication slot \"%s\".",
- name),
- errhint("Remove the logical replication slot \"%s\" from parameter \"%s\".",
- name, "synchronized_standby_slots"));
- break;
+ /* Record Slot State */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state = SS_SLOT_LOGICAL;
+ num_slot_states++;
+
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
SpinLockAcquire(&slot->mutex);
@@ -3189,33 +3440,34 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
if (invalidated)
{
- /* Specified physical slot has been invalidated */
- ereport(elevel,
- errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("physical replication slot \"%s\" specified in parameter \"%s\" has been invalidated",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
- name),
- errhint("Drop and recreate the replication slot \"%s\", or amend parameter \"%s\".",
- name, "synchronized_standby_slots"));
- break;
+ /* Record Slot State */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state = SS_SLOT_INVALIDATED;
+ num_slot_states++;
+
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
if (!XLogRecPtrIsValid(restart_lsn) || restart_lsn < wait_for_lsn)
{
- /* Log a message if no active_pid for this physical slot */
- if (inactive)
- ereport(elevel,
- errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
- name),
- errhint("Start the standby associated with the replication slot \"%s\", or amend parameter \"%s\".",
- name, "synchronized_standby_slots"));
+ /*
+ * If a slot is inactive and lagging, report it as inactive.
+ * If it is active and lagging, report it as lagging.
+ *
+ * In ALL mode: must wait for it.
+ * In ANY N (quorum) mode: skip and use another slot.
+ */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state =
+ inactive ? SS_SLOT_INACTIVE_LAGGING : SS_SLOT_ACTIVE_LAGGING;
+ slot_states[num_slot_states].restart_lsn = restart_lsn;
+ num_slot_states++;
- /* Continue if the current slot hasn't caught up. */
- break;
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
Assert(restart_lsn >= wait_for_lsn);
@@ -3226,17 +3478,30 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
caught_up_slot_num++;
+ /* Stop processing if the required number of slots have caught up. */
+ if (caught_up_slot_num >= required)
+ break;
+
+next_slot:
name += strlen(name) + 1;
}
LWLockRelease(ReplicationSlotControlLock);
/*
- * Return false if not all the standbys have caught up to the specified
- * WAL location.
+ * If the required number of slots have not caught up, report any
+ * recorded problem states and return false.
+ *
+ * We only emit messages when the requirement is not met to avoid
+ * misleading messages in quorum/priority mode where other slots may
+ * have satisfied the condition despite some slots having issues.
*/
- if (caught_up_slot_num != synchronized_standby_slots_config->nslotnames)
+ if (caught_up_slot_num < required)
+ {
+ ReportUnavailableSyncStandbySlots(slot_states, num_slot_states, elevel, wait_for_lsn);
+ pfree(slot_states);
return false;
+ }
/* The ss_oldest_flush_lsn must not retreat. */
Assert(!XLogRecPtrIsValid(ss_oldest_flush_lsn) ||
@@ -3244,6 +3509,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
ss_oldest_flush_lsn = min_restart_lsn;
+ pfree(slot_states);
return true;
}
diff --git a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
new file mode 100644
index 00000000000..760f10f38a0
--- /dev/null
+++ b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
@@ -0,0 +1,378 @@
+
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test synchronized_standby_slots with different syntax modes:
+# - Plain list (ALL mode): slot1, slot2
+# - ANY N (quorum mode): ANY N (slot1, slot2, ...)
+#
+# Setup: a 3-node cluster with one primary, two physical standbys, and a
+# logical decoding client using a failover-enabled slot.
+#
+# | ----> standby1 (primary_slot_name = sb1_slot)
+# primary ------|
+# | ----> standby2 (primary_slot_name = sb2_slot)
+#
+# synchronous_standby_names = 'ANY 1 (standby1, standby2)'
+#
+# Test scenarios:
+#
+# A) Plain list 'sb1_slot, sb2_slot' (ALL mode)
+# - Works when all slots are available
+# - Blocks immediately if ANY slot is unavailable
+#
+# B) ANY N (sb1_slot, sb2_slot, ...) (quorum mode)
+# - Proceeds when at least N slots have caught up
+# - Skips missing/invalid/logical slots and lagging slots (inactive or active)
+# to find N caught-up slots
+#
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# ---------------------------------------------------------------------------
+# 1. Create a primary with logical replication level, autovacuum off
+# ---------------------------------------------------------------------------
+my $primary = PostgreSQL::Test::Cluster->new('primary');
+$primary->init(allows_streaming => 'logical');
+$primary->append_conf(
+ 'postgresql.conf', qq{
+autovacuum = off
+});
+$primary->start;
+
+# Physical replication slots for the two standbys
+$primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('sb1_slot');");
+$primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('sb2_slot');");
+
+# ---------------------------------------------------------------------------
+# 2. Create standby1 and standby2 from a fresh backup
+# ---------------------------------------------------------------------------
+my $backup_name = 'base_backup';
+$primary->backup($backup_name);
+
+my $connstr = $primary->connstr;
+
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+ $primary, $backup_name,
+ has_streaming => 1,
+ has_restoring => 1);
+$standby1->append_conf(
+ 'postgresql.conf', qq(
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr dbname=postgres'
+));
+
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+ $primary, $backup_name,
+ has_streaming => 1,
+ has_restoring => 1);
+$standby2->append_conf(
+ 'postgresql.conf', qq(
+hot_standby_feedback = on
+primary_slot_name = 'sb2_slot'
+primary_conninfo = '$connstr dbname=postgres'
+));
+
+$standby1->start;
+$standby2->start;
+
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+# ---------------------------------------------------------------------------
+# 3. Create a logical failover slot on the primary
+# ---------------------------------------------------------------------------
+$primary->safe_psql('postgres',
+ "SELECT pg_create_logical_replication_slot('logical_failover', 'test_decoding', false, false, true);"
+);
+
+# ---------------------------------------------------------------------------
+# 4. Configure quorum sync rep with ALL-mode synchronized_standby_slots
+# ---------------------------------------------------------------------------
+$primary->append_conf(
+ 'postgresql.conf', qq{
+synchronous_standby_names = 'ANY 1 (standby1, standby2)'
+synchronized_standby_slots = 'sb1_slot, sb2_slot'
+});
+$primary->reload;
+
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+# ---------------------------------------------------------------------------
+# 5. Confirm that quorum sync rep is active for both standbys
+# ---------------------------------------------------------------------------
+is( $primary->safe_psql(
+ 'postgres',
+ q{SELECT count(*) FROM pg_stat_replication WHERE sync_state = 'quorum';}
+ ),
+ '2',
+ 'both standbys are in quorum sync state');
+
+##################################################
+# PART A: Plain list (ALL mode) blocks when any slot is unavailable
+##################################################
+
+$standby1->stop;
+
+# Commit succeeds since standby2 satisfies the quorum.
+my $emit_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'all_mode_blocks');"
+);
+like($emit_lsn, qr/^[0-9A-F]+\/[0-9A-F]+$/,
+ 'synchronous commit succeeds with quorum (standby2 alive)');
+
+$primary->wait_for_replay_catchup($standby2);
+
+my $log_offset = -s $primary->logfile;
+
+my $bg = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+# Wait for the primary to log a warning about sb1_slot not being active.
+$primary->wait_for_log(
+ qr/replication slot \"sb1_slot\" specified in parameter "synchronized_standby_slots" does not have active_pid/,
+ $log_offset);
+
+pass('plain list (ALL mode): logical decoding blocked by unavailable sb1_slot');
+
+# Unblock by clearing synchronized_standby_slots.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg->quit;
+
+# Consume the change so the slot is clean for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+##################################################
+# PART B: ANY mode (quorum) — logical decoding proceeds with N-of-M slots
+##################################################
+
+# Switch synchronized_standby_slots to quorum mode: need only 1 of 2 slots.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'ANY 1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+# standby1 is still down; standby2 is up.
+
+# Emit another transactional message — commits via quorum.
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'quorum_mode_works');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+# In quorum mode, logical decoding should NOT block because sb2_slot has
+# caught up and 1-of-2 is sufficient.
+my $decoded = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%quorum_mode_works%';});
+is($decoded, '1',
+ 'ANY mode: logical decoding proceeds with only sb2_slot caught up');
+
+##################################################
+# PART C: Re-check plain list (ALL mode) works when both standbys are up
+##################################################
+
+# Bring standby1 back.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Switch to plain list (ALL mode) with both slots.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'sb1_slot, sb2_slot'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'both_caught_up');"
+);
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+my $decoded_bc = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%both_caught_up%';});
+is($decoded_bc, '1',
+ 'plain list: works when all standbys are up');
+
+##################################################
+# PART D: ANY 2 waits on an active lagging slot
+##################################################
+
+# Stop standby1 so sb1_slot can be controlled by a raw replication connection
+# that keeps the slot active while lagging.
+$standby1->stop;
+
+my $old_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_current_wal_lsn();");
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'ANY 2 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+my $any2_lag_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'any_2_lagging_blocks');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+my $repl_any2 = $primary->background_psql(
+ 'postgres',
+ replication => 'database',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$repl_any2->query_until(
+ qr/^$/,
+ "START_REPLICATION SLOT sb1_slot PHYSICAL $old_lsn;\n");
+
+$primary->poll_query_until('postgres', q{
+ SELECT active_pid IS NOT NULL
+ FROM pg_replication_slots
+ WHERE slot_name = 'sb1_slot'
+}) or die "replication connection did not activate sb1_slot";
+
+my $bg_any2 = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_any2->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'ANY 2: decoding waits when only one slot has caught up');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_any2->quit;
+$repl_any2->quit;
+
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# Bring standby1 back up for the remaining tests.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+
+##################################################
+# PART E: Duplicate entries are ignored for quorum counting
+##################################################
+
+# Stop standby2 so only sb1_slot can catch up.
+$standby2->stop;
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'ANY 2 (sb1_slot, sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'duplicate_entries_ignored');"
+);
+$primary->wait_for_replay_catchup($standby1);
+
+my $bg_dup = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_dup->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'duplicate entries are ignored when counting quorum slots');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_dup->quit;
+
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# Bring standby2 back up for validation tests.
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+
+##################################################
+# PART F: Verify GUC validation rejects bad values
+##################################################
+
+my ($result, $stdout, $stderr);
+
+# N exceeds number of listed slots
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'ANY 3 (sb1_slot, sb2_slot)';");
+like($stderr, qr/ERROR/,
+ 'GUC rejects ANY N when N > number of listed slots');
+
+# Missing closing parenthesis
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'ANY 1 (sb1_slot, sb2_slot';");
+like($stderr, qr/ERROR/,
+ 'GUC rejects malformed ANY syntax');
+
+# Priority syntax is not supported by synchronized_standby_slots yet
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'FIRST 1 (sb1_slot, sb2_slot)';");
+like($stderr, qr/priority syntax is not supported/,
+ 'GUC rejects FIRST syntax');
+
+# Legacy priority syntax is not supported by synchronized_standby_slots yet
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = '1 (sb1_slot, sb2_slot)';");
+like($stderr, qr/priority syntax is not supported/,
+ 'GUC rejects legacy priority syntax');
+
+# Invalid slot name
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'ANY 1 (INVALID_UPPER)';");
+like($stderr, qr/ERROR/,
+ 'GUC rejects invalid slot name in ANY syntax');
+
+# ---------------------------------------------------------------------------
+# Cleanup
+# ---------------------------------------------------------------------------
+$primary->safe_psql('postgres',
+ "SELECT pg_drop_replication_slot('logical_failover');");
+
+done_testing();
--
2.43.0
^ permalink raw reply [nested|flat] 25+ messages in thread
* RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
@ 2026-06-04 08:24 ` Zhijie Hou (Fujitsu) <[email protected]>
2026-06-04 09:26 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
0 siblings, 1 reply; 25+ messages in thread
From: Zhijie Hou (Fujitsu) @ 2026-06-04 08:24 UTC (permalink / raw)
To: Ashutosh Sharma <[email protected]>; shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
On Thursday, June 4, 2026 3:36 PM Ashutosh Sharma <[email protected]> wrote:
> On Thu, Jun 4, 2026 at 9:14 AM shveta malik <[email protected]>
> wrote:
> >
> > On Wed, Jun 3, 2026 at 4:30 PM Ashutosh Sharma
> <[email protected]> wrote:
> > > On Fri, May 15, 2026 at 9:28 AM shveta malik <[email protected]>
> wrote:
> > > >
> > > >
> > > > Ashutosh, while testing further, I noticed that
> > > > 'synchronized_standby_slots' does not filter duplicate entries. As an
> > > > example, if user ends up giving one entry twice in priority
> > > > configuration, then we will end up waiting on one slot twice rather
> > > > than waiting on 2 different slots.
> > > >
> > > > Example:
> > > > alter system set synchronized_standby_slots = 'FIRST 2 (standby_1,
> > > > standby_1, standby_2, standby_3)';
> > > > select pg_reload_conf();
> > > > insert into tab1 values (10), (20), (30);
> > > > select pg_logical_slot_get_binary_changes('sub1', NULL, NULL,
> > > > 'proto_version', '4', 'publication_names', 'pub1');
> > > >
> > > > The last statement works even though standby_2 and standby_3 do not
> > > > exist. It consumes standby_1 twice and thinks that the required number
> > > > of slots has caught-up.
> > > >
> > > > OTOH, if we use the same configuration for
> > > > 'synchronous_standby_names', it correctly waits for standby_2 and does
> > > > not count on standby_1 twice.
> > > >
> > > > alter system set synchronous_standby_names = 'FIRST 2 (standby_1,
> > > > standby_1, standby_2, standby_3)';
> > > > insert into tab1 values (10), (20), (30); ----> This will wait on standby_2
> > > >
> > > > This is perhaps because 'synchronous_standby_names ' waits on active
> > > > WAL senders rather than repeated strings in configuration. But our
> > > > code changes wait on the names present in
> 'synchronized_standby_slots'
> > > > without filtering out duplicates.
> > > >
> > >
> > > May I know what your expectation is here? Would you like the check
> > > hook for synchronized_standby_slots to automatically resolve
> > > duplicates into a unique set of values, or should it detect duplicate
> > > entries and raise an error so that the user can correct the
> > > configuration?
> > >
> > > If we automatically resolve duplicates, the user would still see the
> > > GUC configured exactly as they specified, even though it would not
> > > function the same way internally. For example, if a user sets:
> > >
> > > FIRST 2 (s1, s1, s1, s2)
> > >
> > > it might internally be resolved to:
> > >
> > > FIRST 2 (s1, s2)
> > >
> > > However, when the user runs SHOW, it would still display the original
> > > configuration. This could give the user an incorrect impression of how
> > > the setting is actually being interpreted. Because of this, I feel we
> > > should treat duplicate entries as an invalid configuration and raise
> > > an error.
> > >
> > > As far as synchronous_standby_names is concerned, I can see that
> > > configurations such as:
> > >
> > > FIRST 2 (s1, s1, s1, s1)
> > >
> > > are currently accepted, which I don't think is correct either and
> > > should have been rejected, possibly resulted in the server startup
> > > failure.
> > >
> >
> > My preference, and original intent, was to accept duplicate entries
> > and skip them internally. Doc can be updated to say 'duplicate entries
> > are skipped'. A server startup failure due to duplicate entries in a
> > GUC does not seem right to me. If the alter-system command fails due
> > to duplicate entries, that is still fine, but a startup failure seems
> > excessive. But let's see what others have to say on this.
> >
>
> Okay, the attached patch adds the capability to automatically remove
> duplicate entries from the synchronized_standby_slots list.
Thanks for updating the patch.
I agree with Shveta that reporting an ERROR is not ideal. I also think it (ERROR) would
be inconsistent with existing GUCs, as most of them, such as
synchronous_standby_names, search_path, and session_preload_libraries, do not
enforce uniqueness.
The most similar GUC, synchronous_standby_names, also clarifies this in the
documentation:
" There is no mechanism to enforce uniqueness of standby names. In case of
duplicates one of the matching standbys will be considered as higher priority,
though exactly which one is indeterminate."[1]
> In N of M
> mode, if N > M after removing duplicate entries, an error is raised.
I'm not entirely sure about this case. It seems similar to when the number of
specified slots is less than N (in ANY N or FIRST N), given that we want to skip
duplicate slots. In that situation, the natural behavior to me would be to
simply block replication rather than raise an error. And
synchronous_standby_names would also simply block the transaction in this case.
[1] https://www.postgresql.org/docs/devel/runtime-config-replication.html#GUC-SYNCHRONOUS-STANDBY-NAMES
Best Regards,
Hou zj
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
@ 2026-06-04 09:26 ` Ashutosh Sharma <[email protected]>
2026-06-05 03:04 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-05 10:02 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
0 siblings, 2 replies; 25+ messages in thread
From: Ashutosh Sharma @ 2026-06-04 09:26 UTC (permalink / raw)
To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: shveta malik <[email protected]>; Amit Kapila <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi,
Thanks for your feedback.
On Thu, Jun 4, 2026 at 1:54 PM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> On Thursday, June 4, 2026 3:36 PM Ashutosh Sharma <[email protected]> wrote:
> > On Thu, Jun 4, 2026 at 9:14 AM shveta malik <[email protected]>
> > wrote:
> > >
> > > On Wed, Jun 3, 2026 at 4:30 PM Ashutosh Sharma
> > <[email protected]> wrote:
> > > > On Fri, May 15, 2026 at 9:28 AM shveta malik <[email protected]>
> > wrote:
> > > > >
> > > > >
> > > > > Ashutosh, while testing further, I noticed that
> > > > > 'synchronized_standby_slots' does not filter duplicate entries. As an
> > > > > example, if user ends up giving one entry twice in priority
> > > > > configuration, then we will end up waiting on one slot twice rather
> > > > > than waiting on 2 different slots.
> > > > >
> > > > > Example:
> > > > > alter system set synchronized_standby_slots = 'FIRST 2 (standby_1,
> > > > > standby_1, standby_2, standby_3)';
> > > > > select pg_reload_conf();
> > > > > insert into tab1 values (10), (20), (30);
> > > > > select pg_logical_slot_get_binary_changes('sub1', NULL, NULL,
> > > > > 'proto_version', '4', 'publication_names', 'pub1');
> > > > >
> > > > > The last statement works even though standby_2 and standby_3 do not
> > > > > exist. It consumes standby_1 twice and thinks that the required number
> > > > > of slots has caught-up.
> > > > >
> > > > > OTOH, if we use the same configuration for
> > > > > 'synchronous_standby_names', it correctly waits for standby_2 and does
> > > > > not count on standby_1 twice.
> > > > >
> > > > > alter system set synchronous_standby_names = 'FIRST 2 (standby_1,
> > > > > standby_1, standby_2, standby_3)';
> > > > > insert into tab1 values (10), (20), (30); ----> This will wait on standby_2
> > > > >
> > > > > This is perhaps because 'synchronous_standby_names ' waits on active
> > > > > WAL senders rather than repeated strings in configuration. But our
> > > > > code changes wait on the names present in
> > 'synchronized_standby_slots'
> > > > > without filtering out duplicates.
> > > > >
> > > >
> > > > May I know what your expectation is here? Would you like the check
> > > > hook for synchronized_standby_slots to automatically resolve
> > > > duplicates into a unique set of values, or should it detect duplicate
> > > > entries and raise an error so that the user can correct the
> > > > configuration?
> > > >
> > > > If we automatically resolve duplicates, the user would still see the
> > > > GUC configured exactly as they specified, even though it would not
> > > > function the same way internally. For example, if a user sets:
> > > >
> > > > FIRST 2 (s1, s1, s1, s2)
> > > >
> > > > it might internally be resolved to:
> > > >
> > > > FIRST 2 (s1, s2)
> > > >
> > > > However, when the user runs SHOW, it would still display the original
> > > > configuration. This could give the user an incorrect impression of how
> > > > the setting is actually being interpreted. Because of this, I feel we
> > > > should treat duplicate entries as an invalid configuration and raise
> > > > an error.
> > > >
> > > > As far as synchronous_standby_names is concerned, I can see that
> > > > configurations such as:
> > > >
> > > > FIRST 2 (s1, s1, s1, s1)
> > > >
> > > > are currently accepted, which I don't think is correct either and
> > > > should have been rejected, possibly resulted in the server startup
> > > > failure.
> > > >
> > >
> > > My preference, and original intent, was to accept duplicate entries
> > > and skip them internally. Doc can be updated to say 'duplicate entries
> > > are skipped'. A server startup failure due to duplicate entries in a
> > > GUC does not seem right to me. If the alter-system command fails due
> > > to duplicate entries, that is still fine, but a startup failure seems
> > > excessive. But let's see what others have to say on this.
> > >
> >
> > Okay, the attached patch adds the capability to automatically remove
> > duplicate entries from the synchronized_standby_slots list.
>
> Thanks for updating the patch.
>
> I agree with Shveta that reporting an ERROR is not ideal. I also think it (ERROR) would
> be inconsistent with existing GUCs, as most of them, such as
> synchronous_standby_names, search_path, and session_preload_libraries, do not
> enforce uniqueness.
>
> The most similar GUC, synchronous_standby_names, also clarifies this in the
> documentation:
>
> " There is no mechanism to enforce uniqueness of standby names. In case of
> duplicates one of the matching standbys will be considered as higher priority,
> though exactly which one is indeterminate."[1]
>
> > In N of M
> > mode, if N > M after removing duplicate entries, an error is raised.
>
> I'm not entirely sure about this case. It seems similar to when the number of
> specified slots is less than N (in ANY N or FIRST N), given that we want to skip
> duplicate slots. In that situation, the natural behavior to me would be to
> simply block replication rather than raise an error. And
> synchronous_standby_names would also simply block the transaction in this case.
>
For duplicate entries themselves, I agree with the direction of not
raising an error. Silently normalizing duplicates is reasonable for
this GUC, especially if we document it clearly. A repeated slot name
does not add any new information, so treating it as “same slot listed
twice by mistake” is practical.
But for N > M after deduplication, I would still lean toward raising an error.
Why I’d separate those cases:
1) Duplicate entries looks like a harmless normalization problem. ANY
2 (a, a, b) can be normalized to ANY 2 (a, b) without changing the
user’s apparent intent much.
2) N > M after deduplication is not a transient runtime state. ANY 2
(a, a) becomes one unique slot. That configuration can never succeed
unless the config itself changes. Blocking forever turns a static
configuration mistake into an operational liveness problem.
3) N > M after deduplication is different from ordinary “not enough
standbys are currently available”. If we configure ANY 2 (a, b) and
only a is currently caught up, blocking makes sense because the
situation may resolve at runtime. If we configure ANY 2 (a, a) and
duplicates are ignored, there is no possible future runtime in which
it succeeds without editing the GUC. That is why I think erroring is
better.
On the synchronous_standby_names comparison, I do not think it is
fully analogous. The quoted documentation is about there being no
reliable way to enforce uniqueness of standby names in the live
system, because those names are matched against runtime standbys and
the result can be indeterminate. Here, synchronized_standby_slots
names concrete replication slots, which are stable object identifiers.
Duplicate config entries are detectable and normalizable
deterministically at GUC parse time. That gives us a cleaner option
than synchronous_standby_names has.
So my preferred behavior would be:
1) duplicate names: normalize, do not error
2) after normalization, if num_sync > unique_slots: error immediately
--
With Regards,
Ashutosh Sharma.
^ permalink raw reply [nested|flat] 25+ messages in thread
* RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-04 09:26 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
@ 2026-06-05 03:04 ` Zhijie Hou (Fujitsu) <[email protected]>
2026-06-05 03:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
1 sibling, 1 reply; 25+ messages in thread
From: Zhijie Hou (Fujitsu) @ 2026-06-05 03:04 UTC (permalink / raw)
To: Ashutosh Sharma <[email protected]>; +Cc: shveta malik <[email protected]>; Amit Kapila <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
On Thursday, June 4, 2026 5:27 PM Ashutosh Sharma <[email protected]> wrote:
> On Thu, Jun 4, 2026 at 1:54 PM Zhijie Hou (Fujitsu)
> <[email protected]> wrote:
> >
> > On Thursday, June 4, 2026 3:36 PM Ashutosh Sharma
> <[email protected]> wrote:
> > > On Thu, Jun 4, 2026 at 9:14 AM shveta malik <[email protected]>
> > > wrote:
> > > > My preference, and original intent, was to accept duplicate entries
> > > > and skip them internally. Doc can be updated to say 'duplicate entries
> > > > are skipped'. A server startup failure due to duplicate entries in a
> > > > GUC does not seem right to me. If the alter-system command fails due
> > > > to duplicate entries, that is still fine, but a startup failure seems
> > > > excessive. But let's see what others have to say on this.
> > > >
> > >
> > > Okay, the attached patch adds the capability to automatically remove
> > > duplicate entries from the synchronized_standby_slots list.
> >
> > Thanks for updating the patch.
> >
> > I agree with Shveta that reporting an ERROR is not ideal. I also think it (ERROR) would
> > be inconsistent with existing GUCs, as most of them, such as
> > synchronous_standby_names, search_path, and session_preload_libraries, do not
> > enforce uniqueness.
> >
> > The most similar GUC, synchronous_standby_names, also clarifies this in the
> > documentation:
> >
> > " There is no mechanism to enforce uniqueness of standby names. In case of
> > duplicates one of the matching standbys will be considered as higher priority,
> > though exactly which one is indeterminate."[1]
> >
> > > In N of M
> > > mode, if N > M after removing duplicate entries, an error is raised.
> >
> > I'm not entirely sure about this case. It seems similar to when the number of
> > specified slots is less than N (in ANY N or FIRST N), given that we want to
> skip
> > duplicate slots. In that situation, the natural behavior to me would be to
> > simply block replication rather than raise an error. And
> > synchronous_standby_names would also simply block the transaction in this
> case.
> >
>
> For duplicate entries themselves, I agree with the direction of not
> raising an error. Silently normalizing duplicates is reasonable for
> this GUC, especially if we document it clearly. A repeated slot name
> does not add any new information, so treating it as “same slot listed
> twice by mistake” is practical.
>
> But for N > M after deduplication, I would still lean toward raising an error.
>
> Why I’d separate those cases:
>
> 1) Duplicate entries looks like a harmless normalization problem. ANY
> 2 (a, a, b) can be normalized to ANY 2 (a, b) without changing the
> user’s apparent intent much.
>
> 2) N > M after deduplication is not a transient runtime state. ANY 2
> (a, a) becomes one unique slot. That configuration can never succeed
> unless the config itself changes. Blocking forever turns a static
> configuration mistake into an operational liveness problem.
>
> 3) N > M after deduplication is different from ordinary “not enough
> standbys are currently available”. If we configure ANY 2 (a, b) and
> only a is currently caught up, blocking makes sense because the
> situation may resolve at runtime. If we configure ANY 2 (a, a) and
> duplicates are ignored, there is no possible future runtime in which
> it succeeds without editing the GUC. That is why I think erroring is
> better.
>
> On the synchronous_standby_names comparison, I do not think it is
> fully analogous. The quoted documentation is about there being no
> reliable way to enforce uniqueness of standby names in the live
> system, because those names are matched against runtime standbys and
> the result can be indeterminate. Here, synchronized_standby_slots
> names concrete replication slots, which are stable object identifiers.
> Duplicate config entries are detectable and normalizable
> deterministically at GUC parse time. That gives us a cleaner option
> than synchronous_standby_names has.
Thanks for the explanation.
What I was wondering is: ignoring duplicates, what should be the behavior of
"ANY 2 (standby)" when N > M?
I studied a bit for the behavior of synchronous_standby_names to understand the
difference. synchronous_standby_names does support syntax like "ANY 2 (standby)"
where N > M. Because even in that case, a transaction can still commit if there
are two standbys with the same name ("standby" in this example). I'm not sure
how common that use case is, but it may explain why no error is reported.
Given that, I'm not opposed to reporting an error in synchronized_standby_slots
when N > M. The situation is different here since there cannot be two slots with
the same name, making this a completely invalid use case.
Best Regards,
Hou zj
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-04 09:26 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-05 03:04 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
@ 2026-06-05 03:36 ` shveta malik <[email protected]>
2026-06-08 09:51 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
0 siblings, 1 reply; 25+ messages in thread
From: shveta malik @ 2026-06-05 03:36 UTC (permalink / raw)
To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Ashutosh Sharma <[email protected]>; Amit Kapila <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>; shveta malik <[email protected]>
On Fri, Jun 5, 2026 at 8:34 AM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> On Thursday, June 4, 2026 5:27 PM Ashutosh Sharma <[email protected]> wrote:
> > On Thu, Jun 4, 2026 at 1:54 PM Zhijie Hou (Fujitsu)
> > <[email protected]> wrote:
> > >
> > > On Thursday, June 4, 2026 3:36 PM Ashutosh Sharma
> > <[email protected]> wrote:
> > > > On Thu, Jun 4, 2026 at 9:14 AM shveta malik <[email protected]>
> > > > wrote:
> > > > > My preference, and original intent, was to accept duplicate entries
> > > > > and skip them internally. Doc can be updated to say 'duplicate entries
> > > > > are skipped'. A server startup failure due to duplicate entries in a
> > > > > GUC does not seem right to me. If the alter-system command fails due
> > > > > to duplicate entries, that is still fine, but a startup failure seems
> > > > > excessive. But let's see what others have to say on this.
> > > > >
> > > >
> > > > Okay, the attached patch adds the capability to automatically remove
> > > > duplicate entries from the synchronized_standby_slots list.
> > >
> > > Thanks for updating the patch.
> > >
> > > I agree with Shveta that reporting an ERROR is not ideal. I also think it (ERROR) would
> > > be inconsistent with existing GUCs, as most of them, such as
> > > synchronous_standby_names, search_path, and session_preload_libraries, do not
> > > enforce uniqueness.
> > >
> > > The most similar GUC, synchronous_standby_names, also clarifies this in the
> > > documentation:
> > >
> > > " There is no mechanism to enforce uniqueness of standby names. In case of
> > > duplicates one of the matching standbys will be considered as higher priority,
> > > though exactly which one is indeterminate."[1]
> > >
> > > > In N of M
> > > > mode, if N > M after removing duplicate entries, an error is raised.
> > >
> > > I'm not entirely sure about this case. It seems similar to when the number of
> > > specified slots is less than N (in ANY N or FIRST N), given that we want to
> > skip
> > > duplicate slots. In that situation, the natural behavior to me would be to
> > > simply block replication rather than raise an error. And
> > > synchronous_standby_names would also simply block the transaction in this
> > case.
> > >
> >
> > For duplicate entries themselves, I agree with the direction of not
> > raising an error. Silently normalizing duplicates is reasonable for
> > this GUC, especially if we document it clearly. A repeated slot name
> > does not add any new information, so treating it as “same slot listed
> > twice by mistake” is practical.
> >
> > But for N > M after deduplication, I would still lean toward raising an error.
> >
> > Why I’d separate those cases:
> >
> > 1) Duplicate entries looks like a harmless normalization problem. ANY
> > 2 (a, a, b) can be normalized to ANY 2 (a, b) without changing the
> > user’s apparent intent much.
> >
> > 2) N > M after deduplication is not a transient runtime state. ANY 2
> > (a, a) becomes one unique slot. That configuration can never succeed
> > unless the config itself changes. Blocking forever turns a static
> > configuration mistake into an operational liveness problem.
> >
> > 3) N > M after deduplication is different from ordinary “not enough
> > standbys are currently available”. If we configure ANY 2 (a, b) and
> > only a is currently caught up, blocking makes sense because the
> > situation may resolve at runtime. If we configure ANY 2 (a, a) and
> > duplicates are ignored, there is no possible future runtime in which
> > it succeeds without editing the GUC. That is why I think erroring is
> > better.
> >
> > On the synchronous_standby_names comparison, I do not think it is
> > fully analogous. The quoted documentation is about there being no
> > reliable way to enforce uniqueness of standby names in the live
> > system, because those names are matched against runtime standbys and
> > the result can be indeterminate. Here, synchronized_standby_slots
> > names concrete replication slots, which are stable object identifiers.
> > Duplicate config entries are detectable and normalizable
> > deterministically at GUC parse time. That gives us a cleaner option
> > than synchronous_standby_names has.
>
> Thanks for the explanation.
>
> What I was wondering is: ignoring duplicates, what should be the behavior of
> "ANY 2 (standby)" when N > M?
>
> I studied a bit for the behavior of synchronous_standby_names to understand the
> difference. synchronous_standby_names does support syntax like "ANY 2 (standby)"
> where N > M. Because even in that case, a transaction can still commit if there
> are two standbys with the same name ("standby" in this example). I'm not sure
> how common that use case is, but it may explain why no error is reported.
>
> Given that, I'm not opposed to reporting an error in synchronized_standby_slots
> when N > M. The situation is different here since there cannot be two slots with
> the same name, making this a completely invalid use case.
>
I also think, we can report error when N>M. IIRC, we were also
reporting earlier (without removing duplicates). Upon removing
duplicates, we can follow the same behaviour instead of walsender
being stuck indefinitely.
thanks
Shveta
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-04 09:26 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-05 03:04 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-05 03:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
@ 2026-06-08 09:51 ` Amit Kapila <[email protected]>
2026-06-10 06:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
0 siblings, 1 reply; 25+ messages in thread
From: Amit Kapila @ 2026-06-08 09:51 UTC (permalink / raw)
To: shveta malik <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Ashutosh Sharma <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
On Fri, Jun 5, 2026 at 9:06 AM shveta malik <[email protected]> wrote:
>
> On Fri, Jun 5, 2026 at 8:34 AM Zhijie Hou (Fujitsu)
> <[email protected]> wrote:
> >
> > On Thursday, June 4, 2026 5:27 PM Ashutosh Sharma <[email protected]> wrote:
> > > On Thu, Jun 4, 2026 at 1:54 PM Zhijie Hou (Fujitsu)
> > > <[email protected]> wrote:
> > > >
> > > > On Thursday, June 4, 2026 3:36 PM Ashutosh Sharma
> > > <[email protected]> wrote:
> > > > > On Thu, Jun 4, 2026 at 9:14 AM shveta malik <[email protected]>
> > > > > wrote:
> > > > > > My preference, and original intent, was to accept duplicate entries
> > > > > > and skip them internally. Doc can be updated to say 'duplicate entries
> > > > > > are skipped'. A server startup failure due to duplicate entries in a
> > > > > > GUC does not seem right to me. If the alter-system command fails due
> > > > > > to duplicate entries, that is still fine, but a startup failure seems
> > > > > > excessive. But let's see what others have to say on this.
> > > > > >
> > > > >
> > > > > Okay, the attached patch adds the capability to automatically remove
> > > > > duplicate entries from the synchronized_standby_slots list.
> > > >
> > > > Thanks for updating the patch.
> > > >
> > > > I agree with Shveta that reporting an ERROR is not ideal. I also think it (ERROR) would
> > > > be inconsistent with existing GUCs, as most of them, such as
> > > > synchronous_standby_names, search_path, and session_preload_libraries, do not
> > > > enforce uniqueness.
> > > >
> > > > The most similar GUC, synchronous_standby_names, also clarifies this in the
> > > > documentation:
> > > >
> > > > " There is no mechanism to enforce uniqueness of standby names. In case of
> > > > duplicates one of the matching standbys will be considered as higher priority,
> > > > though exactly which one is indeterminate."[1]
> > > >
> > > > > In N of M
> > > > > mode, if N > M after removing duplicate entries, an error is raised.
> > > >
> > > > I'm not entirely sure about this case. It seems similar to when the number of
> > > > specified slots is less than N (in ANY N or FIRST N), given that we want to
> > > skip
> > > > duplicate slots. In that situation, the natural behavior to me would be to
> > > > simply block replication rather than raise an error. And
> > > > synchronous_standby_names would also simply block the transaction in this
> > > case.
> > > >
> > >
> > > For duplicate entries themselves, I agree with the direction of not
> > > raising an error. Silently normalizing duplicates is reasonable for
> > > this GUC, especially if we document it clearly. A repeated slot name
> > > does not add any new information, so treating it as “same slot listed
> > > twice by mistake” is practical.
> > >
> > > But for N > M after deduplication, I would still lean toward raising an error.
> > >
> > > Why I’d separate those cases:
> > >
> > > 1) Duplicate entries looks like a harmless normalization problem. ANY
> > > 2 (a, a, b) can be normalized to ANY 2 (a, b) without changing the
> > > user’s apparent intent much.
> > >
> > > 2) N > M after deduplication is not a transient runtime state. ANY 2
> > > (a, a) becomes one unique slot. That configuration can never succeed
> > > unless the config itself changes. Blocking forever turns a static
> > > configuration mistake into an operational liveness problem.
> > >
> > > 3) N > M after deduplication is different from ordinary “not enough
> > > standbys are currently available”. If we configure ANY 2 (a, b) and
> > > only a is currently caught up, blocking makes sense because the
> > > situation may resolve at runtime. If we configure ANY 2 (a, a) and
> > > duplicates are ignored, there is no possible future runtime in which
> > > it succeeds without editing the GUC. That is why I think erroring is
> > > better.
> > >
> > > On the synchronous_standby_names comparison, I do not think it is
> > > fully analogous. The quoted documentation is about there being no
> > > reliable way to enforce uniqueness of standby names in the live
> > > system, because those names are matched against runtime standbys and
> > > the result can be indeterminate. Here, synchronized_standby_slots
> > > names concrete replication slots, which are stable object identifiers.
> > > Duplicate config entries are detectable and normalizable
> > > deterministically at GUC parse time. That gives us a cleaner option
> > > than synchronous_standby_names has.
> >
> > Thanks for the explanation.
> >
> > What I was wondering is: ignoring duplicates, what should be the behavior of
> > "ANY 2 (standby)" when N > M?
> >
> > I studied a bit for the behavior of synchronous_standby_names to understand the
> > difference. synchronous_standby_names does support syntax like "ANY 2 (standby)"
> > where N > M. Because even in that case, a transaction can still commit if there
> > are two standbys with the same name ("standby" in this example). I'm not sure
> > how common that use case is, but it may explain why no error is reported.
> >
> > Given that, I'm not opposed to reporting an error in synchronized_standby_slots
> > when N > M. The situation is different here since there cannot be two slots with
> > the same name, making this a completely invalid use case.
> >
>
> I also think, we can report error when N>M.
>
+1 for reporting an ERROR for this case.
--
With Regards,
Amit Kapila.
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-04 09:26 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-05 03:04 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-05 03:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-08 09:51 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
@ 2026-06-10 06:45 ` shveta malik <[email protected]>
2026-06-12 10:10 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
0 siblings, 1 reply; 25+ messages in thread
From: shveta malik @ 2026-06-10 06:45 UTC (permalink / raw)
To: Amit Kapila <[email protected]>; Ashutosh Sharma <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>; shveta malik <[email protected]>
Please find a few comments on June8 version fo patches:
1)
patch001:
SYNC_REP_DEFAULT: do we need to give one-line comment for this
somewhere as unlike PRIORITY and QUORUM, it is not self-explanatory.
patch002:
2)
It is better to avoid mentioning it as 'synchronized standby slots'.
We can make it as 'synchronized_standby_slots' in below comments:
+ /* Parse the synchronized standby slots configuration */
+ * Report problem states for synchronized standby slots that prevented the
3)
For these error-messages too, we need to mention GUC name to give
better clarity.
+ GUC_check_errmsg("number of synchronized standby slots (%d) must not
exceed the number of unique listed slots (%d)",
+ syncrep_parse_result->num_sync,
+ syncrep_parse_result->nmembers);
How about:
GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
GUC_check_errmsg("invalid value for parameter \"%s\: synchronization
requirement (%d) exceeds the number of unique listed slots (%d)",
"synchronized_standby_slots",
syncrep_parse_result->num_sync,
syncrep_parse_result->nmembers);
Or
GUC_check_errmsg("invalid value for parameter \"%s\: required number
of synchronized standby slots (%d) exceeds the number of unique listed
slots (%d)",
"synchronized_standby_slots",
syncrep_parse_result->num_sync,
syncrep_parse_result->nmembers);
3)
+ GUC_check_errmsg("number of synchronized standby slots (%d) must be
greater than zero",
+ syncrep_parse_result->num_sync)
How about:
GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
GUC_check_errmsg("invalid value for parameter \"%s\: required number
of synchronized standby slots (%d) must be greater than zero",
"synchronized_standby_slots",
syncrep_parse_result->num_sync);
---
Or, better yet, we can split the messages and details for both, example:
GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
GUC_check_errmsg("invalid value for parameter \"%s\",
"synchronized_standby_slots");
GUC_check_errdetail("The required number of synchronized standby slots
(%d) exceeds the number of unique listed slots (%d)",
syncrep_parse_result->num_sync,
syncrep_parse_result->nmembers);)
4)
+ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo * slot_states
We can get rid of space before slot_states. I think pgindent might
have put it in my patch. Sorry for the trouble.
5)
003:
- wait_for_all = (required == synchronized_standby_slots_config->nslotnames);
+ wait_for_all =
+ (synchronized_standby_slots_config->syncrep_method == SYNC_REP_DEFAULT);
This change can be moved to 002, right?
thanks
Shveta
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-04 09:26 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-05 03:04 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-05 03:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-08 09:51 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-06-10 06:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
@ 2026-06-12 10:10 ` Ashutosh Sharma <[email protected]>
2026-06-12 13:03 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Shlok Kyal <[email protected]>
0 siblings, 1 reply; 25+ messages in thread
From: Ashutosh Sharma @ 2026-06-12 10:10 UTC (permalink / raw)
To: shveta malik <[email protected]>; +Cc: Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi Shveta,
Thanks again for your review comments and suggestions. Please see my
comments inline below:
On Wed, Jun 10, 2026 at 12:16 PM shveta malik <[email protected]> wrote:
>
> Please find a few comments on June8 version fo patches:
>
> 1)
> patch001:
>
> SYNC_REP_DEFAULT: do we need to give one-line comment for this
> somewhere as unlike PRIORITY and QUORUM, it is not self-explanatory.
>
Yes, it does makes sense to include a one-line comment. I've added it
in the attached patch.
>
> patch002:
> 2)
> It is better to avoid mentioning it as 'synchronized standby slots'.
> We can make it as 'synchronized_standby_slots' in below comments:
>
> + /* Parse the synchronized standby slots configuration */
>
> + * Report problem states for synchronized standby slots that prevented the
>
Good point. I've made the suggested change in the attached patch
> 3)
> For these error-messages too, we need to mention GUC name to give
> better clarity.
>
> + GUC_check_errmsg("number of synchronized standby slots (%d) must not
> exceed the number of unique listed slots (%d)",
> + syncrep_parse_result->num_sync,
> + syncrep_parse_result->nmembers);
>
>
> How about:
> GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
> GUC_check_errmsg("invalid value for parameter \"%s\: synchronization
> requirement (%d) exceeds the number of unique listed slots (%d)",
> "synchronized_standby_slots",
> syncrep_parse_result->num_sync,
> syncrep_parse_result->nmembers);
>
Updated as suggested in the attached patch.
> 3)
> + GUC_check_errmsg("number of synchronized standby slots (%d) must be
> greater than zero",
> + syncrep_parse_result->num_sync)
>
> How about:
> GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
> GUC_check_errmsg("invalid value for parameter \"%s\: required number
> of synchronized standby slots (%d) must be greater than zero",
> "synchronized_standby_slots",
> syncrep_parse_result->num_sync);
>
>
>
Updated as suggested in the attached patch.
> 4)
> +ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo * slot_states
>
> We can get rid of space before slot_states. I think pgindent might
> have put it in my patch. Sorry for the trouble.
>
> 5)
> 003:
> - wait_for_all = (required == synchronized_standby_slots_config->nslotnames);
> + wait_for_all =
> + (synchronized_standby_slots_config->syncrep_method == SYNC_REP_DEFAULT);
>
> This change can be moved to 002, right?
Done.
PFA patch containing all the above changes.
--
With Regards,
Ashutosh Sharma.
Attachments:
[application/octet-stream] 0003-Add-FIRST-N-and-N-.-priority-syntax-to.patch (23.1K, 2-0003-Add-FIRST-N-and-N-.-priority-syntax-to.patch)
download | inline diff:
From b1e710e8b83a1f6ebc0b9e93287237f8d67bce48 Mon Sep 17 00:00:00 2001
From: Ashutosh Sharma <[email protected]>
Date: Fri, 12 Jun 2026 09:51:34 +0000
Subject: [PATCH 3/3] Add FIRST N and N (...) priority syntax to
synchronized_standby_slots
Extend synchronized_standby_slots to support explicit priority
forms aligned with synchronous_standby_names.
- FIRST N (slot1, slot2, ...)
- N (slot1, slot2, ...) as shorthand for FIRST N
Implementation details:
- Use the SYNC_REP_DEFAULT parser distinction from the earlier
refactor so plain-list syntax remains separate from priority
syntax.
- Extend StandbySlotsHaveCaughtup() priority handling.
- Select slots in list order.
- Skip missing, logical, invalidated, and inactive lagging slots.
- Wait for active lagging higher-priority slots.
- Clarify duplicate handling for priority syntax in the
synchronized_standby_slots documentation.
- Simplify caught-up comments and clarify standby confirmation
wait comments to match the final control flow.
Tests and docs:
- Add coverage for FIRST behavior and shorthand N (...) behavior.
- Add plain-list disambiguation with first-prefixed slot names.
- Add FIRST duplicate-entry recovery coverage to show duplicates
do not create extra priority positions.
- Update docs for FIRST and shorthand priority syntax semantics.
- Clarify that duplicate slot names are ignored in priority-based
forms and preserve first-occurrence order.
Author: Satya Narlapuram <[email protected]>
Author: Ashutosh Sharma <[email protected]>
Reviewed-by: Shveta Malik <[email protected]>
Reviewed-by: Ajin Cherian <[email protected]>
Reviewed-by: Hou, Zhijie <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Surya Poondla <[email protected]>
Reviewed-by: Japin Li <[email protected]>
---
doc/src/sgml/config.sgml | 45 ++-
src/backend/replication/slot.c | 44 +--
.../053_synchronized_standby_slots_quorum.pl | 262 ++++++++++++++++--
3 files changed, 304 insertions(+), 47 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1f176bd48f4..473f2641b90 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5194,6 +5194,7 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
sender processes must wait on before delivering decoded changes. This
parameter uses the following syntax:
<synopsis>
+ [FIRST] <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="parameter">slot_name</replaceable> [, ...] )
ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="parameter">slot_name</replaceable> [, ...] )
<replaceable class="parameter">slot_name</replaceable> [, ...]
</synopsis>
@@ -5205,9 +5206,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
<replaceable class="parameter">num_sync</replaceable>
must be an integer value greater than zero and must not exceed the
number of listed slots.
- Other forms supported by
- <xref linkend="guc-synchronous-standby-names"/>, such as priority
- syntax, are not supported.
+ </para>
+ <para>
+ The keyword <literal>FIRST</literal>, coupled with
+ <replaceable class="parameter">num_sync</replaceable>, specifies
+ priority-based semantics. Logical decoding will wait for the first
+ <replaceable class="parameter">num_sync</replaceable> available
+ physical slots in priority order (the order they appear in the list).
+ Missing, logical, or invalidated slots are skipped. Inactive slots are
+ skipped only while they are lagging. However, if a slot exists and is
+ valid and active but has not yet caught up, the system will wait for it
+ rather than skipping to lower-priority slots. If, after skipping
+ unusable slots, fewer than
+ <replaceable class="parameter">num_sync</replaceable> usable slots
+ remain, logical decoding waits until enough slots become usable and
+ caught up, or until the configuration is changed. The keyword
+ <literal>FIRST</literal> is optional in this form, so
+ <literal>2 (slot1, slot2, slot3)</literal> and
+ <literal>FIRST 2 (slot1, slot2, slot3)</literal> are equivalent.
</para>
<para>
A plain comma-separated list without a keyword specifies that
@@ -5244,19 +5260,26 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
duplicate entries are ignored and only the first occurrence is used.
The semantics of <varname>synchronized_standby_slots</varname> are
therefore based on the unique set of listed slot names, preserving the
- original order of first occurrence. This means that
- <literal>ANY 2 (slot1, slot1, slot2, slot3)</literal> is treated the
- same as <literal>ANY 2 (slot1, slot2, slot3)</literal>, and a plain
- list such as <literal>(slot1, slot1, slot2)</literal> is treated the
- same as <literal>(slot1, slot2)</literal>. In particular,
+ original order of first occurrence. This means that, in
+ priority-based forms, duplicates do not create additional priority
+ positions: for example,
+ <literal>FIRST 2 (slot1, slot1, slot2, slot3)</literal> is treated the
+ same as <literal>FIRST 2 (slot1, slot2, slot3)</literal>.
+ Likewise, <literal>ANY 2 (slot1, slot1, slot2, slot3)</literal> is
+ treated the same as <literal>ANY 2 (slot1, slot2, slot3)</literal>,
+ and a plain list such as <literal>(slot1, slot1, slot2)</literal>
+ is treated the same as <literal>(slot1, slot2)</literal>. In particular,
<replaceable class="parameter">num_sync</replaceable> must not exceed
the number of unique listed slots. Such a configuration results in an
error to prevent indefinite waits in WAL sender processes due to a
misconfigured <varname>synchronized_standby_slots</varname> setting.
</para>
- <para>
- <literal>ANY</literal> is case-insensitive.
- </para>
+ <para>
+ <literal>FIRST</literal> and <literal>ANY</literal> are case-insensitive.
+ If these keywords are used as the name of a replication slot,
+ the <replaceable class="parameter">slot_name</replaceable> must
+ be double-quoted.
+ </para>
<para>
The use of <varname>synchronized_standby_slots</varname> guarantees
that logical replication
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index e06c428d853..dde241eefc9 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -3068,6 +3068,8 @@ CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
*
* slot1, slot2 -- wait for ALL listed slots
* ANY N (slot1, slot2, ...) -- wait for any N-of-M (quorum)
+ * FIRST N (slot1, slot2, ...) -- wait for first N in priority order
+ * N (slot1, slot2, ...) -- shorthand for FIRST N
*
* Note: Simple list syntax is interpreted as "wait for ALL" for this GUC,
* unlike synchronous_standby_names where it means "FIRST 1".
@@ -3108,14 +3110,6 @@ check_synchronized_standby_slots(char **newval, void **extra, GucSource source)
return false;
}
- if (syncrep_parse_result->syncrep_method == SYNC_REP_PRIORITY)
- {
- GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
- GUC_check_errmsg("priority syntax is not supported for parameter \"%s\"",
- "synchronized_standby_slots");
- return false;
- }
-
if (syncrep_parse_result->num_sync <= 0)
{
GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
@@ -3337,6 +3331,12 @@ ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo *slot_states,
* Simple list (e.g., "slot1, slot2"):
* ALL slots must have caught up. Returns false otherwise.
*
+ * FIRST N (e.g., "FIRST 2 (slot1, slot2, slot3)"):
+ * Wait for the first N eligible slots in priority order. Skips missing,
+ * invalid, logical, and inactive-lagging slots to find N eligible slots.
+ * If an active slot is lagging, waits for it (does not skip to lower
+ * priority slots).
+ *
* ANY N (e.g., "ANY 2 (slot1, slot2, slot3)"):
* Wait for any N eligible slots. Skips missing, invalid, logical, and
* lagging slots (inactive or active) to find N slots that have caught up.
@@ -3387,11 +3387,14 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
* first slot that is missing/invalid/logical, or the first slot that is
* lagging (inactive or active).
*
- * wait_for_all = false means we select N from M candidates (ANY N syntax).
- * In this mode, slots already caught up are counted even if inactive, and
- * lagging slots are skipped until enough slots have caught up.
- * Duplicate configured slot names do not appear here because the check hook
- * compacts them out of the parsed configuration.
+ * wait_for_all = false means we select N from M candidates (FIRST N or
+ * ANY N syntax). In this mode, slots already caught up are counted even if
+ * inactive. In FIRST N mode, we skip missing/invalid/logical slots and
+ * lagging inactive slots, but wait for an active lagging slot with higher
+ * priority. In ANY N mode, we skip lagging slots (inactive or active) to
+ * find any N that have caught up. Duplicate configured slot names do not
+ * appear here because the check hook compacts them out of the parsed
+ * configuration.
*/
required = synchronized_standby_slots_config->num_sync;
wait_for_all = (synchronized_standby_slots_config->syncrep_method == SYNC_REP_DEFAULT);
@@ -3474,8 +3477,9 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
* If a slot is inactive and lagging, report it as inactive. If it
* is active and lagging, report it as lagging.
*
- * In ALL mode: must wait for it. In ANY N (quorum) mode: skip and
- * use another slot.
+ * In ALL mode: must wait for it. In FIRST N (priority) mode:
+ * lagging active slots block, while inactive slots can be
+ * skipped. In ANY N (quorum) mode: skip and use another slot.
*/
slot_states[num_slot_states].slot_name = name;
slot_states[num_slot_states].state =
@@ -3483,7 +3487,9 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
slot_states[num_slot_states].restart_lsn = restart_lsn;
num_slot_states++;
- if (wait_for_all)
+ if (wait_for_all ||
+ (!inactive &&
+ synchronized_standby_slots_config->syncrep_method == SYNC_REP_PRIORITY))
break;
goto next_slot;
}
@@ -3496,7 +3502,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
caught_up_slot_num++;
- /* Stop processing if the required number of slots have caught up. */
+ /* Stop once the required number of slots have caught up. */
if (caught_up_slot_num >= required)
break;
@@ -3511,8 +3517,8 @@ next_slot:
* problem states and return false.
*
* We only emit messages when the requirement is not met to avoid
- * misleading messages in quorum mode where other slots may have satisfied
- * the condition despite some slots having issues.
+ * misleading messages in quorum/priority mode where other slots may have
+ * satisfied the condition despite some slots having issues.
*/
if (caught_up_slot_num < required)
{
diff --git a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
index d387e0c7e7e..a4153a44b37 100644
--- a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
+++ b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
@@ -4,6 +4,7 @@
# Test synchronized_standby_slots with different syntax modes:
# - Plain list (ALL mode): slot1, slot2
# - ANY N (quorum mode): ANY N (slot1, slot2, ...)
+# - FIRST N (priority mode): FIRST N (slot1, slot2, ...)
#
# Setup: a 3-node cluster with one primary, two physical standbys, and a
# logical decoding client using a failover-enabled slot.
@@ -200,16 +201,168 @@ is($decoded_bc, '1',
'plain list: works when all standbys are up');
##################################################
-# PART D: ANY 2 waits on an active lagging slot
+# PART D: Verify FIRST N priority semantics
##################################################
-# Stop standby1 so sb1_slot can be controlled by a raw replication connection
-# that keeps the slot active while lagging.
+# FIRST N should:
+# 1. Select first N slots in priority order (list order)
+# 2. Skip missing/invalid/logical slots and inactive lagging slots to find
+# N caught-up slots
+# 3. Wait for active lagging slots (not skip to lower priority)
+
+# Test FIRST 2 (sb1_slot, sb2_slot) with both up; should wait for both.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 2 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_2_both_up');"
+);
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+my $decoded_e2 = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%first_2_both_up%';});
+is($decoded_e2, '1',
+ 'FIRST 2: decoding works when all required slots are up');
+
+# Test FIRST 1 (sb1_slot, sb2_slot) with sb1_slot unavailable.
$standby1->stop;
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_1_skip_unavailable');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+# FIRST 1 should skip sb1_slot (unavailable) and use sb2_slot.
+my $decoded_e1 = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%first_1_skip_unavailable%';});
+is($decoded_e1, '1',
+ 'FIRST 1: skips unavailable first slot, uses second slot');
+
+# Test shorthand priority syntax: N (...) means FIRST N (...).
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'num_1_shorthand_priority');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+my $decoded_num1 = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%num_1_shorthand_priority%';});
+is($decoded_num1, '1',
+ '1 (...): shorthand priority syntax behaves like FIRST 1');
+
+##################################################
+# PART E: FIRST 1 and ANY 2 wait on an active lagging slot
+##################################################
+
+# Bring standby1 back so sb1_slot is active and caught up.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# To test the active-but-lagging slot path deterministically, we open a raw
+# replication connection to sb1_slot starting from a deliberately old LSN.
+# psql in replication mode never sends Standby Status Update messages, so
+# the walsender keeps sb1_slot's active_pid set but restart_lsn never
+# advances.
+
+# Stop standby1 so its walsender releases sb1_slot, allowing our replication
+# connection below to acquire it.
+$standby1->stop;
+
+# Capture a safely old LSN to stream from, before the test WAL record.
my $old_lsn = $primary->safe_psql('postgres',
"SELECT pg_current_wal_lsn();");
+# FIRST 1 must wait for the highest-priority slot when it is active but lagging.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+my $first_lag_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_1_lagging_blocks');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+# Open a raw replication connection to sb1_slot starting from $old_lsn.
+# This activates the slot (active_pid IS NOT NULL) while keeping restart_lsn
+# frozen below $first_lag_lsn for the lifetime of the connection.
+my $repl_first = $primary->background_psql(
+ 'postgres',
+ replication => 'database',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$repl_first->query_until(
+ qr/^$/,
+ "START_REPLICATION SLOT sb1_slot PHYSICAL $old_lsn;\n");
+
+# Wait until sb1_slot shows active_pid, confirming the walsender is live.
+$primary->poll_query_until('postgres', q{
+ SELECT active_pid IS NOT NULL
+ FROM pg_replication_slots
+ WHERE slot_name = 'sb1_slot'
+}) or die "replication connection did not activate sb1_slot";
+
+# sb1_slot is now active and its restart_lsn is behind $first_lag_lsn.
+# Start logical decoding in the background; it must block.
+my $bg_first = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_first->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'FIRST 1: decoding waits for active lagging higher-priority slot');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_first->quit;
+$repl_first->quit;
+
+# Ensure the previous replication connection has fully released sb1_slot
+# before reusing it in the next subtest.
+$primary->poll_query_until('postgres', q{
+ SELECT active_pid IS NULL
+ FROM pg_replication_slots
+ WHERE slot_name = 'sb1_slot'
+}) or die "replication connection did not release sb1_slot";
+
+# Consume the change so the slot is clean for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# ANY 2 must also wait when only one of two required slots has caught up.
+# Reuse the same technique: open a raw replication connection to sb1_slot
+# from $old_lsn so it is active but its restart_lsn stays behind the target.
+
+# Capture another old LSN baseline before the next test WAL record.
+$old_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_current_wal_lsn();");
+
$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
"'ANY 2 (sb1_slot, sb2_slot)'");
$primary->reload;
@@ -272,7 +425,54 @@ $primary->wait_for_replay_catchup($standby1);
##################################################
-# PART E: Duplicate entries are ignored for quorum counting
+# PART F: Plain list with first-prefixed slot name still means ALL mode
+##################################################
+
+# Create a slot name starting with "first_" for parser disambiguation checks.
+$primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('first_slot');");
+
+# If simple-list syntax starts with a slot name like "first_slot", it must
+# still be treated as ALL mode (not as explicit FIRST N syntax).
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'first_slot, sb2_slot'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_prefix_all_mode_blocks');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+$log_offset = -s $primary->logfile;
+
+$bg = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+# Plain list must require all listed slots; first_slot is intentionally inactive.
+$primary->wait_for_log(
+ qr/replication slot \"first_slot\" specified in parameter "synchronized_standby_slots" does not have active_pid/,
+ $log_offset);
+
+pass('plain list with first-prefixed slot name blocks in ALL mode');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg->quit;
+
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+##################################################
+# PART G: Duplicate entries are ignored for quorum counting
##################################################
# Stop standby2 so only sb1_slot can catch up.
@@ -313,6 +513,46 @@ $primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
$primary->reload;
$bg_dup->quit;
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# FIRST duplicates must also not create extra priority positions.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 2 (sb1_slot, sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_duplicate_entries_ignored');"
+);
+$primary->wait_for_replay_catchup($standby1);
+
+my $bg_first_dup = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_first_dup->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'FIRST duplicates are ignored when counting priority slots');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_first_dup->quit;
+
# Consume the change for the next test.
$primary->safe_psql('postgres',
q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
@@ -323,7 +563,7 @@ $primary->wait_for_replay_catchup($standby2);
##################################################
-# PART F: Verify GUC validation rejects bad values
+# PART H: Verify GUC validation rejects bad values
##################################################
my ($result, $stdout, $stderr);
@@ -340,18 +580,6 @@ like($stderr, qr/ERROR/,
like($stderr, qr/ERROR/,
'GUC rejects malformed ANY syntax');
-# Priority syntax is not supported by synchronized_standby_slots yet
-($result, $stdout, $stderr) = $primary->psql('postgres',
- "ALTER SYSTEM SET synchronized_standby_slots = 'FIRST 1 (sb1_slot, sb2_slot)';");
-like($stderr, qr/priority syntax is not supported/,
- 'GUC rejects FIRST syntax');
-
-# Legacy priority syntax is not supported by synchronized_standby_slots yet
-($result, $stdout, $stderr) = $primary->psql('postgres',
- "ALTER SYSTEM SET synchronized_standby_slots = '1 (sb1_slot, sb2_slot)';");
-like($stderr, qr/priority syntax is not supported/,
- 'GUC rejects legacy priority syntax');
-
# Invalid slot name
($result, $stdout, $stderr) = $primary->psql('postgres',
"ALTER SYSTEM SET synchronized_standby_slots = 'ANY 1 (INVALID_UPPER)';");
--
2.43.0
[application/octet-stream] 0002-Add-ANY-N-semantics-to-synchronized_standby_slots.patch (44.1K, 3-0002-Add-ANY-N-semantics-to-synchronized_standby_slots.patch)
download | inline diff:
From cf5feaf37bb7436f7200688808e8a2921541944e Mon Sep 17 00:00:00 2001
From: Ashutosh Sharma <[email protected]>
Date: Fri, 12 Jun 2026 09:48:04 +0000
Subject: [PATCH 2/3] Add ANY N semantics to synchronized_standby_slots
Extend synchronized_standby_slots with quorum syntax for logical
failover slot synchronization:
- ANY N (slot1, slot2, ...)
Plain-list semantics are preserved as-is:
- slot1, slot2 continues to mean all listed slots are required
Implementation details:
- Reuse syncrep parser infrastructure in the GUC check hook and
map parsed output into synchronized_standby_slots semantics.
- Consume SYNC_REP_DEFAULT from the preparatory parser refactor to
distinguish plain-list syntax from explicit parser modes.
- In StandbySlotsHaveCaughtup(), enforce mode-specific behavior for:
- existing all-listed-slots semantics (plain list)
- quorum N-of-M behavior (ANY N)
- Validation rejects configurations where N exceeds the number of
listed slots.
- Ignore duplicate synchronized_standby_slots entries, preserving the
first occurrence and applying semantics to the resulting unique list.
- Clarify synchronized_standby_slots comments and lagging restart_lsn
reporting to match the implemented behavior.
Tests and docs:
- Add recovery coverage for plain-list behavior and ANY quorum
behavior, including lagging-slot and validation-error scenarios.
- Add duplicate-entry recovery coverage for synchronized_standby_slots.
- Document ANY syntax and clarify plain-list behavior for this GUC.
- Document that duplicate slot names are ignored and counted only once.
Author: Satya Narlapuram <[email protected]>
Author: Ashutosh Sharma <[email protected]>
Reviewed-by: Shveta Malik <[email protected]>
Reviewed-by: Ajin Cherian <[email protected]>
Reviewed-by: Hou, Zhijie <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Surya Poondla <[email protected]>
Reviewed-by: Japin Li <[email protected]>
---
doc/src/sgml/config.sgml | 87 ++-
src/backend/replication/slot.c | 519 ++++++++++++++----
.../053_synchronized_standby_slots_quorum.pl | 367 +++++++++++++
3 files changed, 847 insertions(+), 126 deletions(-)
create mode 100644 src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fa566c9e553..1f176bd48f4 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5190,17 +5190,84 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
</term>
<listitem>
<para>
- A comma-separated list of streaming replication standby server slot names
- that logical WAL sender processes will wait for. Logical WAL sender processes
- will send decoded changes to plugins only after the specified replication
- slots confirm receiving WAL. This guarantees that logical replication
+ Specifies the streaming replication standby slots that logical WAL
+ sender processes must wait on before delivering decoded changes. This
+ parameter uses the following syntax:
+<synopsis>
+ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="parameter">slot_name</replaceable> [, ...] )
+<replaceable class="parameter">slot_name</replaceable> [, ...]
+</synopsis>
+ where <replaceable class="parameter">num_sync</replaceable> is
+ the number of physical replication slots that must confirm WAL
+ receipt before logical decoding proceeds,
+ and <replaceable class="parameter">slot_name</replaceable>
+ is the name of a physical replication slot.
+ <replaceable class="parameter">num_sync</replaceable>
+ must be an integer value greater than zero and must not exceed the
+ number of listed slots.
+ Other forms supported by
+ <xref linkend="guc-synchronous-standby-names"/>, such as priority
+ syntax, are not supported.
+ </para>
+ <para>
+ A plain comma-separated list without a keyword specifies that
+ <emphasis>all</emphasis> listed physical slots must confirm WAL
+ receipt. This differs from <xref linkend="guc-synchronous-standby-names"/>
+ where a simple list means <literal>FIRST 1</literal>. For
+ <varname>synchronized_standby_slots</varname>, requiring all slots
+ provides safer failover semantics by default.
+ </para>
+ <para>
+ The keyword <literal>ANY</literal>, coupled with
+ <replaceable class="parameter">num_sync</replaceable>, specifies
+ quorum-based semantics. Logical decoding proceeds once at least
+ <replaceable class="parameter">num_sync</replaceable> of the listed
+ slots have caught up. Missing, logical, and invalidated slots are
+ skipped when determining candidates. Lagging slots (inactive or
+ active) simply do not count toward the required number until they
+ catch up.
+ If fewer than <replaceable class="parameter">num_sync</replaceable>
+ slots have caught up at a given moment, logical decoding waits until
+ that threshold is reached.
+ i.e., there is no priority ordering.
+ For example, a setting of <literal>ANY 1 (sb1_slot, sb2_slot)</literal>
+ allows logical decoding to proceed as soon as either physical slot has
+ confirmed WAL receipt. If none of the slots are available or have
+ caught up, logical decoding waits until at least one slot meets the
+ required condition. This is useful in conjunction with
+ quorum-based synchronous replication
+ (<literal>synchronous_standby_names = 'ANY ...'</literal>), so that
+ logical decoding availability matches the commit durability guarantee.
+ </para>
+ <para>
+ If the same physical replication slot name appears more than once,
+ duplicate entries are ignored and only the first occurrence is used.
+ The semantics of <varname>synchronized_standby_slots</varname> are
+ therefore based on the unique set of listed slot names, preserving the
+ original order of first occurrence. This means that
+ <literal>ANY 2 (slot1, slot1, slot2, slot3)</literal> is treated the
+ same as <literal>ANY 2 (slot1, slot2, slot3)</literal>, and a plain
+ list such as <literal>(slot1, slot1, slot2)</literal> is treated the
+ same as <literal>(slot1, slot2)</literal>. In particular,
+ <replaceable class="parameter">num_sync</replaceable> must not exceed
+ the number of unique listed slots. Such a configuration results in an
+ error to prevent indefinite waits in WAL sender processes due to a
+ misconfigured <varname>synchronized_standby_slots</varname> setting.
+ </para>
+ <para>
+ <literal>ANY</literal> is case-insensitive.
+ </para>
+ <para>
+ The use of <varname>synchronized_standby_slots</varname> guarantees
+ that logical replication
failover slots do not consume changes until those changes are received
- and flushed to corresponding physical standbys. If a
+ and flushed to the required physical standbys. If a
logical replication connection is meant to switch to a physical standby
after the standby is promoted, the physical replication slot for the
standby should be listed here. Note that logical replication will not
- proceed if the slots specified in the
- <varname>synchronized_standby_slots</varname> do not exist or are invalidated.
+ proceed if the required number of physical slots specified in
+ <varname>synchronized_standby_slots</varname> do not exist or are
+ invalidated.
Additionally, the replication management functions
<link linkend="pg-replication-slot-advance">
<function>pg_replication_slot_advance</function></link>,
@@ -5208,9 +5275,9 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
<function>pg_logical_slot_get_changes</function></link>, and
<link linkend="pg-logical-slot-peek-changes">
<function>pg_logical_slot_peek_changes</function></link>,
- when used with logical failover slots, will block until all
- physical slots specified in <varname>synchronized_standby_slots</varname> have
- confirmed WAL receipt.
+ when used with logical failover slots, will block until the required
+ physical slots specified in <varname>synchronized_standby_slots</varname>
+ have confirmed WAL receipt.
</para>
<para>
The standbys corresponding to the physical replication slots in
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index d7fb9f5a67f..e06c428d853 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -50,6 +50,7 @@
#include "replication/logicallauncher.h"
#include "replication/slotsync.h"
#include "replication/slot.h"
+#include "replication/syncrep.h"
#include "replication/walsender_private.h"
#include "storage/fd.h"
#include "storage/ipc.h"
@@ -91,11 +92,19 @@ typedef struct ReplicationSlotOnDisk
* Note: this must be a flat representation that can be held in a single chunk
* of guc_malloc'd memory, so that it can be stored as the "extra" data for the
* synchronized_standby_slots GUC.
+ *
+ * The layout mirrors SyncRepConfigData so that the same quorum and priority
+ * semantics can be expressed. The syncrep_method field uses the
+ * SYNC_REP_DEFAULT, SYNC_REP_PRIORITY, and SYNC_REP_QUORUM constants from
+ * syncrep.h.
*/
typedef struct
{
- /* Number of slot names in the slot_names[] */
- int nslotnames;
+ int config_size; /* total size of this struct, in bytes */
+ int num_sync; /* number of slots that must confirm WAL
+ * receipt before logical decoding proceeds */
+ uint8 syncrep_method; /* SYNC_REP_* method */
+ int nslotnames; /* number of slot names that follow */
/*
* slot_names contains 'nslotnames' consecutive null-terminated C strings.
@@ -103,6 +112,29 @@ typedef struct
char slot_names[FLEXIBLE_ARRAY_MEMBER];
} SyncStandbySlotsConfigData;
+/*
+ * State of a replication slot specified in synchronized_standby_slots GUC.
+ */
+typedef enum
+{
+ SS_SLOT_NOT_FOUND, /* slot does not exist */
+ SS_SLOT_LOGICAL, /* slot is logical, not physical */
+ SS_SLOT_INVALIDATED, /* slot has been invalidated */
+ SS_SLOT_INACTIVE_LAGGING, /* slot is inactive and behind wait_for_lsn */
+ SS_SLOT_ACTIVE_LAGGING, /* slot is active and behind wait_for_lsn */
+} SyncStandbySlotsState;
+
+/*
+ * Information about a synchronized standby slot's state.
+ */
+typedef struct
+{
+ const char *slot_name; /* name of the slot */
+ SyncStandbySlotsState state; /* state of the slot */
+ XLogRecPtr restart_lsn; /* current restart_lsn (valid for lagging
+ * states) */
+} SyncStandbySlotsStateInfo;
+
/*
* Lookup table for slot invalidation causes.
*/
@@ -2963,94 +2995,207 @@ GetSlotInvalidationCauseName(ReplicationSlotInvalidationCause cause)
}
/*
- * A helper function to validate slots specified in GUC synchronized_standby_slots.
+ * Remove duplicate member names from a SyncRepConfigData object.
*
- * The rawname will be parsed, and the result will be saved into *elemlist.
+ * The member_names array of SyncRepConfigData is compacted in place so
+ * that only the first occurrence of each member name is retained. The
+ * original ordering of retained names is preserved, and nmembers and
+ * config_size are updated to describe only the compacted portion of
+ * the array.
*/
-static bool
-validate_sync_standby_slots(char *rawname, List **elemlist)
+static void
+CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
{
- /* Verify syntax and parse string into a list of identifiers */
- if (!SplitIdentifierString(rawname, ',', elemlist))
- {
- GUC_check_errdetail("List syntax is invalid.");
- return false;
- }
+ char *src_name;
+ char *dst_name;
+ int nunique_members = 0;
+ Size unique_size = offsetof(SyncRepConfigData, member_names);
- /* Iterate the list to validate each slot name */
- foreach_ptr(char, name, *elemlist)
+ src_name = config->member_names;
+ dst_name = config->member_names;
+
+ for (int i = 0; i < config->nmembers; i++)
{
- int err_code;
- char *err_msg = NULL;
- char *err_hint = NULL;
+ char *unique_name;
+ size_t name_size;
+ bool duplicate = false;
+
+ name_size = strlen(src_name) + 1;
- if (!ReplicationSlotValidateNameInternal(name, false, &err_code,
- &err_msg, &err_hint))
+ /*
+ * Check whether src_name matches any previously retained unique name.
+ * Only the first nunique_members entries in member_names need to be
+ * examined for this.
+ */
+ unique_name = config->member_names;
+ for (int j = 0; j < nunique_members; j++)
{
- GUC_check_errcode(err_code);
- GUC_check_errdetail("%s", err_msg);
- if (err_hint != NULL)
- GUC_check_errhint("%s", err_hint);
- return false;
+ if (strcmp(unique_name, src_name) == 0)
+ {
+ duplicate = true;
+ break;
+ }
+
+ unique_name += strlen(unique_name) + 1;
+ }
+
+ if (!duplicate)
+ {
+ /*
+ * This src_name is a new unique name. Copy it immediately after the
+ * unique names retained so far.
+ */
+ if (dst_name != src_name)
+ memmove(dst_name, src_name, name_size);
+
+ dst_name += name_size;
+ nunique_members++;
+ unique_size += name_size;
}
+
+ src_name += name_size;
}
- return true;
+ config->nmembers = nunique_members;
+ config->config_size = (int) unique_size;
}
/*
* GUC check_hook for synchronized_standby_slots
+ *
+ * This reuses the syncrep_yyparse/syncrep_scanner infrastructure that is
+ * also used for synchronous_standby_names, and accepts these forms:
+ *
+ * slot1, slot2 -- wait for ALL listed slots
+ * ANY N (slot1, slot2, ...) -- wait for any N-of-M (quorum)
+ *
+ * Note: Simple list syntax is interpreted as "wait for ALL" for this GUC,
+ * unlike synchronous_standby_names where it means "FIRST 1".
+ *
+ * After parsing, we validate every name as a legal replication slot name,
+ * omit duplicate entries while preserving first-occurrence order, and then
+ * apply the resulting unique list to the configured semantics.
*/
bool
check_synchronized_standby_slots(char **newval, void **extra, GucSource source)
{
- char *rawname;
- char *ptr;
- List *elemlist;
- int size;
- bool ok;
- SyncStandbySlotsConfigData *config;
-
- if ((*newval)[0] == '\0')
- return true;
+ if (*newval != NULL && (*newval)[0] != '\0')
+ {
+ yyscan_t scanner;
+ int parse_rc;
+ SyncStandbySlotsConfigData *config;
+ const char *mname;
+
+ /* Result of parsing is returned in one of these two variables */
+ SyncRepConfigData *syncrep_parse_result = NULL;
+ char *syncrep_parse_error_msg = NULL;
+
+ /* Parse the synchronized_standby_slots configuration */
+ syncrep_scanner_init(*newval, &scanner);
+ parse_rc = syncrep_yyparse(&syncrep_parse_result,
+ &syncrep_parse_error_msg,
+ scanner);
+ syncrep_scanner_finish(scanner);
+
+ if (parse_rc != 0 || syncrep_parse_result == NULL)
+ {
+ GUC_check_errcode(ERRCODE_SYNTAX_ERROR);
+ if (syncrep_parse_error_msg)
+ GUC_check_errdetail("%s", syncrep_parse_error_msg);
+ else
+ GUC_check_errdetail("\"%s\" parser failed.",
+ "synchronized_standby_slots");
+ return false;
+ }
- /* Need a modifiable copy of the GUC string */
- rawname = pstrdup(*newval);
+ if (syncrep_parse_result->syncrep_method == SYNC_REP_PRIORITY)
+ {
+ GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
+ GUC_check_errmsg("priority syntax is not supported for parameter \"%s\"",
+ "synchronized_standby_slots");
+ return false;
+ }
- /* Now verify if the specified slots exist and have correct type */
- ok = validate_sync_standby_slots(rawname, &elemlist);
+ if (syncrep_parse_result->num_sync <= 0)
+ {
+ GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
+ GUC_check_errmsg("invalid value for parameter \"%s\": required number of synchronized standby slots (%d) must be greater than zero",
+ "synchronized_standby_slots",
+ syncrep_parse_result->num_sync);
+ return false;
+ }
- if (!ok || elemlist == NIL)
- {
- pfree(rawname);
- list_free(elemlist);
- return ok;
- }
+ /* validate every member name as a slot name */
+ mname = syncrep_parse_result->member_names;
- /* Compute the size required for the SyncStandbySlotsConfigData struct */
- size = offsetof(SyncStandbySlotsConfigData, slot_names);
- foreach_ptr(char, slot_name, elemlist)
- size += strlen(slot_name) + 1;
+ for (int i = 0; i < syncrep_parse_result->nmembers; i++)
+ {
+ int err_code;
+ char *err_msg = NULL;
+ char *err_hint = NULL;
- /* GUC extra value must be guc_malloc'd, not palloc'd */
- config = (SyncStandbySlotsConfigData *) guc_malloc(LOG, size);
- if (!config)
- return false;
+ if (!ReplicationSlotValidateNameInternal(mname, false, &err_code,
+ &err_msg, &err_hint))
+ {
+ GUC_check_errcode(err_code);
+ GUC_check_errdetail("%s", err_msg);
+ if (err_hint != NULL)
+ GUC_check_errhint("%s", err_hint);
+ return false;
+ }
- /* Transform the data into SyncStandbySlotsConfigData */
- config->nslotnames = list_length(elemlist);
+ mname += strlen(mname) + 1;
+ }
- ptr = config->slot_names;
- foreach_ptr(char, slot_name, elemlist)
- {
- strcpy(ptr, slot_name);
- ptr += strlen(slot_name) + 1;
- }
+ /* Omit duplicate slot names to ensure each slot is considered only once. */
+ CompactSyncRepConfigMemberNames(syncrep_parse_result);
- *extra = config;
+ /*
+ * For synchronized_standby_slots, a comma-separated list means all
+ * listed slots are required. The syncrep parser preserves this shape
+ * as SYNC_REP_DEFAULT, so map num_sync to nmembers to enforce
+ * all-mode semantics after removing duplicate names.
+ */
+ if (syncrep_parse_result->syncrep_method == SYNC_REP_DEFAULT)
+ syncrep_parse_result->num_sync = syncrep_parse_result->nmembers;
+
+ /* Reject num_sync > nmembers after duplicates have been omitted. */
+ if (syncrep_parse_result->num_sync > syncrep_parse_result->nmembers)
+ {
+ GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
+ GUC_check_errmsg("invalid value for parameter \"%s\": synchronization requirement (%d) exceeds the number of unique listed slots (%d)",
+ "synchronized_standby_slots",
+ syncrep_parse_result->num_sync,
+ syncrep_parse_result->nmembers);
+ return false;
+ }
+
+ /*
+ * Build SyncStandbySlotsConfigData from the parsed SyncRepConfigData.
+ * Since the structures have identical layout, we can use the same
+ * config_size.
+ */
+ config = (SyncStandbySlotsConfigData *)
+ guc_malloc(LOG, syncrep_parse_result->config_size);
+ if (!config)
+ return false;
+
+ config->config_size = syncrep_parse_result->config_size;
+ config->num_sync = syncrep_parse_result->num_sync;
+ config->syncrep_method = syncrep_parse_result->syncrep_method;
+ config->nslotnames = syncrep_parse_result->nmembers;
+
+ /* Copy all slot names in one operation */
+ memcpy(config->slot_names,
+ syncrep_parse_result->member_names,
+ syncrep_parse_result->config_size -
+ offsetof(SyncRepConfigData, member_names));
+
+ *extra = config;
+ }
+ else
+ *extra = NULL;
- pfree(rawname);
- list_free(elemlist);
return true;
}
@@ -3099,18 +3244,117 @@ SlotExistsInSyncStandbySlots(const char *slot_name)
}
/*
- * Return true if the slots specified in synchronized_standby_slots have caught up to
- * the given WAL location, false otherwise.
+ * Report problem states for synchronized_standby_slots that prevented the
+ * catch-up requirement from being met.
+ */
+static void
+ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo *slot_states,
+ int num_slot_states, int elevel,
+ XLogRecPtr wait_for_lsn)
+{
+ for (int i = 0; i < num_slot_states; i++)
+ {
+ const char *slot_name = slot_states[i].slot_name;
+
+ switch (slot_states[i].state)
+ {
+ case SS_SLOT_NOT_FOUND:
+ ereport(elevel,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" does not exist",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
+ slot_name),
+ errhint("Create the replication slot \"%s\" or amend parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_LOGICAL:
+ ereport(elevel,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot specify logical replication slot \"%s\" in parameter \"%s\"",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting for correction on replication slot \"%s\".",
+ slot_name),
+ errhint("Remove the logical replication slot \"%s\" from parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_INVALIDATED:
+ ereport(elevel,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("physical replication slot \"%s\" specified in parameter \"%s\" has been invalidated",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
+ slot_name),
+ errhint("Drop and recreate the replication slot \"%s\", or amend parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_INACTIVE_LAGGING:
+ ereport(elevel,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
+ slot_name),
+ errhint("Start the standby associated with the replication slot \"%s\", or amend parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_ACTIVE_LAGGING:
+ if (!XLogRecPtrIsValid(slot_states[i].restart_lsn))
+ ereport(DEBUG1,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("The slot's restart_lsn is not yet set; required LSN is %X/%X.",
+ LSN_FORMAT_ARGS(wait_for_lsn)));
+ else
+ ereport(DEBUG1,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("The slot's restart_lsn %X/%X is behind the required %X/%X.",
+ LSN_FORMAT_ARGS(slot_states[i].restart_lsn),
+ LSN_FORMAT_ARGS(wait_for_lsn)));
+ break;
+
+ default:
+ /* Should not happen */
+ Assert(false);
+ break;
+ }
+ }
+}
+
+/*
+ * Return true if the required standby slots have caught up to the given WAL
+ * location, false otherwise.
+ *
+ * The behavior depends on the synchronized_standby_slots configuration:
+ *
+ * Simple list (e.g., "slot1, slot2"):
+ * ALL slots must have caught up. Returns false otherwise.
*
- * The elevel parameter specifies the error level used for logging messages
- * related to slots that do not exist, are invalidated, or are inactive.
+ * ANY N (e.g., "ANY 2 (slot1, slot2, slot3)"):
+ * Wait for any N eligible slots. Skips missing, invalid, logical, and
+ * lagging slots (inactive or active) to find N slots that have caught up.
+ *
+ * The elevel parameter specifies the error level used for reporting issues
+ * related to the slots specified in synchronized_standby_slots when the
+ * catch-up requirement is not met.
*/
bool
StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
{
const char *name;
int caught_up_slot_num = 0;
+ int required;
XLogRecPtr min_restart_lsn = InvalidXLogRecPtr;
+ bool wait_for_all;
+ SyncStandbySlotsStateInfo *slot_states;
+ int num_slot_states = 0;
/*
* Don't need to wait for the standbys to catch up if there is no value in
@@ -3134,12 +3378,44 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
ss_oldest_flush_lsn >= wait_for_lsn)
return true;
+ /*
+ * Determine how many slots are required and whether we're in "wait for
+ * ALL" mode versus "wait for N-of-M" mode.
+ *
+ * wait_for_all = true means we need ALL slots to be ready (simple list
+ * syntax like "slot1, slot2"). In this mode, we stop checking on the
+ * first slot that is missing/invalid/logical, or the first slot that is
+ * lagging (inactive or active).
+ *
+ * wait_for_all = false means we select N from M candidates (ANY N syntax).
+ * In this mode, slots already caught up are counted even if inactive, and
+ * lagging slots are skipped until enough slots have caught up.
+ * Duplicate configured slot names do not appear here because the check hook
+ * compacts them out of the parsed configuration.
+ */
+ required = synchronized_standby_slots_config->num_sync;
+ wait_for_all = (synchronized_standby_slots_config->syncrep_method == SYNC_REP_DEFAULT);
+
+ /*
+ * Allocate array to track slot states. Size it to the total number of
+ * configured slots since in the worst case all could have problem states.
+ */
+ slot_states = palloc_array(SyncStandbySlotsStateInfo,
+ synchronized_standby_slots_config->nslotnames);
+
/*
* To prevent concurrent slot dropping and creation while filtering the
* slots, take the ReplicationSlotControlLock outside of the loop.
*/
LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+ /*
+ * Iterate through configured slots, checking their state and tracking how
+ * many have caught up. Problem states are recorded for deferred
+ * reporting: missing/logical/invalidated slots, and lagging slots
+ * (inactive or active). Messages are only emitted if the catch-up
+ * requirement isn't met.
+ */
name = synchronized_standby_slots_config->slot_names;
for (int i = 0; i < synchronized_standby_slots_config->nslotnames; i++)
{
@@ -3150,35 +3426,28 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
slot = SearchNamedReplicationSlot(name, false);
- /*
- * If a slot name provided in synchronized_standby_slots does not
- * exist, report a message and exit the loop.
- */
if (!slot)
{
- ereport(elevel,
- errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("replication slot \"%s\" specified in parameter \"%s\" does not exist",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
- name),
- errhint("Create the replication slot \"%s\" or amend parameter \"%s\".",
- name, "synchronized_standby_slots"));
- break;
+ /* Record Slot State */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state = SS_SLOT_NOT_FOUND;
+ num_slot_states++;
+
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
- /* Same as above: if a slot is not physical, exit the loop. */
if (SlotIsLogical(slot))
{
- ereport(elevel,
- errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("cannot specify logical replication slot \"%s\" in parameter \"%s\"",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting for correction on replication slot \"%s\".",
- name),
- errhint("Remove the logical replication slot \"%s\" from parameter \"%s\".",
- name, "synchronized_standby_slots"));
- break;
+ /* Record Slot State */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state = SS_SLOT_LOGICAL;
+ num_slot_states++;
+
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
SpinLockAcquire(&slot->mutex);
@@ -3189,33 +3458,34 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
if (invalidated)
{
- /* Specified physical slot has been invalidated */
- ereport(elevel,
- errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("physical replication slot \"%s\" specified in parameter \"%s\" has been invalidated",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
- name),
- errhint("Drop and recreate the replication slot \"%s\", or amend parameter \"%s\".",
- name, "synchronized_standby_slots"));
- break;
+ /* Record Slot State */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state = SS_SLOT_INVALIDATED;
+ num_slot_states++;
+
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
if (!XLogRecPtrIsValid(restart_lsn) || restart_lsn < wait_for_lsn)
{
- /* Log a message if no active_pid for this physical slot */
- if (inactive)
- ereport(elevel,
- errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
- name),
- errhint("Start the standby associated with the replication slot \"%s\", or amend parameter \"%s\".",
- name, "synchronized_standby_slots"));
+ /*
+ * If a slot is inactive and lagging, report it as inactive. If it
+ * is active and lagging, report it as lagging.
+ *
+ * In ALL mode: must wait for it. In ANY N (quorum) mode: skip and
+ * use another slot.
+ */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state =
+ inactive ? SS_SLOT_INACTIVE_LAGGING : SS_SLOT_ACTIVE_LAGGING;
+ slot_states[num_slot_states].restart_lsn = restart_lsn;
+ num_slot_states++;
- /* Continue if the current slot hasn't caught up. */
- break;
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
Assert(restart_lsn >= wait_for_lsn);
@@ -3226,17 +3496,30 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
caught_up_slot_num++;
+ /* Stop processing if the required number of slots have caught up. */
+ if (caught_up_slot_num >= required)
+ break;
+
+next_slot:
name += strlen(name) + 1;
}
LWLockRelease(ReplicationSlotControlLock);
/*
- * Return false if not all the standbys have caught up to the specified
- * WAL location.
+ * If the required number of slots have not caught up, report any recorded
+ * problem states and return false.
+ *
+ * We only emit messages when the requirement is not met to avoid
+ * misleading messages in quorum mode where other slots may have satisfied
+ * the condition despite some slots having issues.
*/
- if (caught_up_slot_num != synchronized_standby_slots_config->nslotnames)
+ if (caught_up_slot_num < required)
+ {
+ ReportUnavailableSyncStandbySlots(slot_states, num_slot_states, elevel, wait_for_lsn);
+ pfree(slot_states);
return false;
+ }
/* The ss_oldest_flush_lsn must not retreat. */
Assert(!XLogRecPtrIsValid(ss_oldest_flush_lsn) ||
@@ -3244,6 +3527,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
ss_oldest_flush_lsn = min_restart_lsn;
+ pfree(slot_states);
return true;
}
@@ -3276,7 +3560,10 @@ WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
ProcessConfigFile(PGC_SIGHUP);
}
- /* Exit if done waiting for every slot. */
+ /*
+ * Exit once the configured synchronized_standby_slots requirement is
+ * met.
+ */
if (StandbySlotsHaveCaughtup(wait_for_lsn, WARNING))
break;
diff --git a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
new file mode 100644
index 00000000000..d387e0c7e7e
--- /dev/null
+++ b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
@@ -0,0 +1,367 @@
+
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test synchronized_standby_slots with different syntax modes:
+# - Plain list (ALL mode): slot1, slot2
+# - ANY N (quorum mode): ANY N (slot1, slot2, ...)
+#
+# Setup: a 3-node cluster with one primary, two physical standbys, and a
+# logical decoding client using a failover-enabled slot.
+#
+# | ----> standby1 (primary_slot_name = sb1_slot)
+# primary ------|
+# | ----> standby2 (primary_slot_name = sb2_slot)
+#
+# synchronous_standby_names = 'ANY 1 (standby1, standby2)'
+#
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# ---------------------------------------------------------------------------
+# 1. Create a primary with logical replication level, autovacuum off
+# ---------------------------------------------------------------------------
+my $primary = PostgreSQL::Test::Cluster->new('primary');
+$primary->init(allows_streaming => 'logical');
+$primary->append_conf(
+ 'postgresql.conf', qq{
+autovacuum = off
+});
+$primary->start;
+
+# Physical replication slots for the two standbys
+$primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('sb1_slot');");
+$primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('sb2_slot');");
+
+# ---------------------------------------------------------------------------
+# 2. Create standby1 and standby2 from a fresh backup
+# ---------------------------------------------------------------------------
+my $backup_name = 'base_backup';
+$primary->backup($backup_name);
+
+my $connstr = $primary->connstr;
+
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+ $primary, $backup_name,
+ has_streaming => 1,
+ has_restoring => 1);
+$standby1->append_conf(
+ 'postgresql.conf', qq(
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr dbname=postgres'
+));
+
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+ $primary, $backup_name,
+ has_streaming => 1,
+ has_restoring => 1);
+$standby2->append_conf(
+ 'postgresql.conf', qq(
+hot_standby_feedback = on
+primary_slot_name = 'sb2_slot'
+primary_conninfo = '$connstr dbname=postgres'
+));
+
+$standby1->start;
+$standby2->start;
+
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+# ---------------------------------------------------------------------------
+# 3. Create a logical failover slot on the primary
+# ---------------------------------------------------------------------------
+$primary->safe_psql('postgres',
+ "SELECT pg_create_logical_replication_slot('logical_failover', 'test_decoding', false, false, true);"
+);
+
+# ---------------------------------------------------------------------------
+# 4. Configure quorum sync rep with ALL-mode synchronized_standby_slots
+# ---------------------------------------------------------------------------
+$primary->append_conf(
+ 'postgresql.conf', qq{
+synchronous_standby_names = 'ANY 1 (standby1, standby2)'
+synchronized_standby_slots = 'sb1_slot, sb2_slot'
+});
+$primary->reload;
+
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+# ---------------------------------------------------------------------------
+# 5. Confirm that quorum sync rep is active for both standbys
+# ---------------------------------------------------------------------------
+is( $primary->safe_psql(
+ 'postgres',
+ q{SELECT count(*) FROM pg_stat_replication WHERE sync_state = 'quorum';}
+ ),
+ '2',
+ 'both standbys are in quorum sync state');
+
+##################################################
+# PART A: Plain list (ALL mode) blocks when any slot is unavailable
+##################################################
+
+$standby1->stop;
+
+# Commit succeeds since standby2 satisfies the quorum.
+my $emit_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'all_mode_blocks');"
+);
+like($emit_lsn, qr/^[0-9A-F]+\/[0-9A-F]+$/,
+ 'synchronous commit succeeds with quorum (standby2 alive)');
+
+$primary->wait_for_replay_catchup($standby2);
+
+my $log_offset = -s $primary->logfile;
+
+my $bg = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+# Wait for the primary to log a warning about sb1_slot not being active.
+$primary->wait_for_log(
+ qr/replication slot \"sb1_slot\" specified in parameter "synchronized_standby_slots" does not have active_pid/,
+ $log_offset);
+
+pass('plain list (ALL mode): logical decoding blocked by unavailable sb1_slot');
+
+# Unblock by clearing synchronized_standby_slots.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg->quit;
+
+# Consume the change so the slot is clean for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+##################################################
+# PART B: ANY mode (quorum) — logical decoding proceeds with N-of-M slots
+##################################################
+
+# Switch synchronized_standby_slots to quorum mode: need only 1 of 2 slots.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'ANY 1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+# standby1 is still down; standby2 is up.
+
+# Emit another transactional message — commits via quorum.
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'quorum_mode_works');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+# In quorum mode, logical decoding should NOT block because sb2_slot has
+# caught up and 1-of-2 is sufficient.
+my $decoded = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%quorum_mode_works%';});
+is($decoded, '1',
+ 'ANY mode: logical decoding proceeds with only sb2_slot caught up');
+
+##################################################
+# PART C: Re-check plain list (ALL mode) works when both standbys are up
+##################################################
+
+# Bring standby1 back.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Switch to plain list (ALL mode) with both slots.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'sb1_slot, sb2_slot'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'both_caught_up');"
+);
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+my $decoded_bc = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%both_caught_up%';});
+is($decoded_bc, '1',
+ 'plain list: works when all standbys are up');
+
+##################################################
+# PART D: ANY 2 waits on an active lagging slot
+##################################################
+
+# Stop standby1 so sb1_slot can be controlled by a raw replication connection
+# that keeps the slot active while lagging.
+$standby1->stop;
+
+my $old_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_current_wal_lsn();");
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'ANY 2 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+my $any2_lag_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'any_2_lagging_blocks');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+my $repl_any2 = $primary->background_psql(
+ 'postgres',
+ replication => 'database',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$repl_any2->query_until(
+ qr/^$/,
+ "START_REPLICATION SLOT sb1_slot PHYSICAL $old_lsn;\n");
+
+$primary->poll_query_until('postgres', q{
+ SELECT active_pid IS NOT NULL
+ FROM pg_replication_slots
+ WHERE slot_name = 'sb1_slot'
+}) or die "replication connection did not activate sb1_slot";
+
+my $bg_any2 = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_any2->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'ANY 2: decoding waits when only one slot has caught up');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_any2->quit;
+$repl_any2->quit;
+
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# Bring standby1 back up for the remaining tests.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+
+##################################################
+# PART E: Duplicate entries are ignored for quorum counting
+##################################################
+
+# Stop standby2 so only sb1_slot can catch up.
+$standby2->stop;
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'ANY 2 (sb1_slot, sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'duplicate_entries_ignored');"
+);
+$primary->wait_for_replay_catchup($standby1);
+
+my $bg_dup = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_dup->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'duplicate entries are ignored when counting quorum slots');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_dup->quit;
+
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# Bring standby2 back up for validation tests.
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+
+##################################################
+# PART F: Verify GUC validation rejects bad values
+##################################################
+
+my ($result, $stdout, $stderr);
+
+# N exceeds number of listed slots
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'ANY 3 (sb1_slot, sb2_slot)';");
+like($stderr, qr/ERROR/,
+ 'GUC rejects ANY N when N > number of listed slots');
+
+# Missing closing parenthesis
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'ANY 1 (sb1_slot, sb2_slot';");
+like($stderr, qr/ERROR/,
+ 'GUC rejects malformed ANY syntax');
+
+# Priority syntax is not supported by synchronized_standby_slots yet
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'FIRST 1 (sb1_slot, sb2_slot)';");
+like($stderr, qr/priority syntax is not supported/,
+ 'GUC rejects FIRST syntax');
+
+# Legacy priority syntax is not supported by synchronized_standby_slots yet
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = '1 (sb1_slot, sb2_slot)';");
+like($stderr, qr/priority syntax is not supported/,
+ 'GUC rejects legacy priority syntax');
+
+# Invalid slot name
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'ANY 1 (INVALID_UPPER)';");
+like($stderr, qr/ERROR/,
+ 'GUC rejects invalid slot name in ANY syntax');
+
+# ---------------------------------------------------------------------------
+# Cleanup
+# ---------------------------------------------------------------------------
+$primary->safe_psql('postgres',
+ "SELECT pg_drop_replication_slot('logical_failover');");
+
+done_testing();
--
2.43.0
[application/octet-stream] 0001-Refactor-syncrep-parsing-to-represent-bare-standby-l.patch (3.2K, 4-0001-Refactor-syncrep-parsing-to-represent-bare-standby-l.patch)
download | inline diff:
From 3a8e1fb75188f96826accfb9d6b59a557084b8be Mon Sep 17 00:00:00 2001
From: Ashutosh Sharma <[email protected]>
Date: Fri, 12 Jun 2026 09:44:51 +0000
Subject: [PATCH 1/3] Refactor syncrep parsing to represent bare standby lists
explicitly
The syncrep parser currently reduces a simple list form to FIRST 1
(SYNC_REP_PRIORITY). That is acceptable for synchronous_standby_names,
but it loses information about whether FIRST was explicitly written.
Introduce SYNC_REP_DEFAULT to represent the bare list form parsed
from standby_list. This allows callers to distinguish:
- explicit priority syntax (FIRST N (...) or N (...))
- quorum syntax (ANY N (...))
- simple list syntax without FIRST/ANY
With this change:
- syncrep grammar emits SYNC_REP_DEFAULT for bare standby lists
- check_synchronous_standby_names() maps SYNC_REP_DEFAULT to
SYNC_REP_PRIORITY, preserving existing synchronous_standby_names
behavior
This is a preparatory patch for future synchronized_standby_slots
changes, where callers can directly interpret SYNC_REP_DEFAULT as
plain-list semantics, while keeping existing synchronous_standby_names
semantics intact.
Per suggestion from Zhijie Hou <[email protected]>
---
src/backend/replication/syncrep.c | 4 ++++
src/backend/replication/syncrep_gram.y | 2 +-
src/include/replication/syncrep.h | 1 +
3 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/backend/replication/syncrep.c b/src/backend/replication/syncrep.c
index e0e30579c59..ae8ecfa0711 100644
--- a/src/backend/replication/syncrep.c
+++ b/src/backend/replication/syncrep.c
@@ -1100,6 +1100,10 @@ check_synchronous_standby_names(char **newval, void **extra, GucSource source)
return false;
}
+ /* Default to FIRST 1 (name ...) priority method if not specified */
+ if (syncrep_parse_result->syncrep_method == SYNC_REP_DEFAULT)
+ syncrep_parse_result->syncrep_method = SYNC_REP_PRIORITY;
+
/* GUC extra value must be guc_malloc'd, not palloc'd */
pconf = (SyncRepConfigData *)
guc_malloc(LOG, syncrep_parse_result->config_size);
diff --git a/src/backend/replication/syncrep_gram.y b/src/backend/replication/syncrep_gram.y
index 1b9d7b2edc4..f1550e109ef 100644
--- a/src/backend/replication/syncrep_gram.y
+++ b/src/backend/replication/syncrep_gram.y
@@ -65,7 +65,7 @@ result:
;
standby_config:
- standby_list { $$ = create_syncrep_config("1", $1, SYNC_REP_PRIORITY); }
+ standby_list { $$ = create_syncrep_config("1", $1, SYNC_REP_DEFAULT); }
| NUM '(' standby_list ')' { $$ = create_syncrep_config($1, $3, SYNC_REP_PRIORITY); }
| ANY NUM '(' standby_list ')' { $$ = create_syncrep_config($2, $4, SYNC_REP_QUORUM); }
| FIRST NUM '(' standby_list ')' { $$ = create_syncrep_config($2, $4, SYNC_REP_PRIORITY); }
diff --git a/src/include/replication/syncrep.h b/src/include/replication/syncrep.h
index b42b5862a70..90ea48b1d14 100644
--- a/src/include/replication/syncrep.h
+++ b/src/include/replication/syncrep.h
@@ -34,6 +34,7 @@
/* syncrep_method of SyncRepConfigData */
#define SYNC_REP_PRIORITY 0
#define SYNC_REP_QUORUM 1
+#define SYNC_REP_DEFAULT 2 /* bare comma-separated list syntax */
/*
* SyncRepGetCandidateStandbys returns an array of these structs,
--
2.43.0
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-04 09:26 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-05 03:04 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-05 03:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-08 09:51 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-06-10 06:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-12 10:10 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
@ 2026-06-12 13:03 ` Shlok Kyal <[email protected]>
0 siblings, 0 replies; 25+ messages in thread
From: Shlok Kyal @ 2026-06-12 13:03 UTC (permalink / raw)
To: Ashutosh Sharma <[email protected]>; +Cc: shveta malik <[email protected]>; Amit Kapila <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
On Fri, 12 Jun 2026 at 15:40, Ashutosh Sharma <[email protected]> wrote:
>
> Hi Shveta,
>
> Thanks again for your review comments and suggestions. Please see my
> comments inline below:
>
> On Wed, Jun 10, 2026 at 12:16 PM shveta malik <[email protected]> wrote:
> >
> > Please find a few comments on June8 version fo patches:
> >
> > 1)
> > patch001:
> >
> > SYNC_REP_DEFAULT: do we need to give one-line comment for this
> > somewhere as unlike PRIORITY and QUORUM, it is not self-explanatory.
> >
>
> Yes, it does makes sense to include a one-line comment. I've added it
> in the attached patch.
>
> >
> > patch002:
> > 2)
> > It is better to avoid mentioning it as 'synchronized standby slots'.
> > We can make it as 'synchronized_standby_slots' in below comments:
> >
> > + /* Parse the synchronized standby slots configuration */
> >
> > + * Report problem states for synchronized standby slots that prevented the
> >
>
> Good point. I've made the suggested change in the attached patch
>
> > 3)
> > For these error-messages too, we need to mention GUC name to give
> > better clarity.
> >
> > + GUC_check_errmsg("number of synchronized standby slots (%d) must not
> > exceed the number of unique listed slots (%d)",
> > + syncrep_parse_result->num_sync,
> > + syncrep_parse_result->nmembers);
> >
> >
> > How about:
> > GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
> > GUC_check_errmsg("invalid value for parameter \"%s\: synchronization
> > requirement (%d) exceeds the number of unique listed slots (%d)",
> > "synchronized_standby_slots",
> > syncrep_parse_result->num_sync,
> > syncrep_parse_result->nmembers);
> >
>
> Updated as suggested in the attached patch.
>
> > 3)
> > + GUC_check_errmsg("number of synchronized standby slots (%d) must be
> > greater than zero",
> > + syncrep_parse_result->num_sync)
> >
> > How about:
> > GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
> > GUC_check_errmsg("invalid value for parameter \"%s\: required number
> > of synchronized standby slots (%d) must be greater than zero",
> > "synchronized_standby_slots",
> > syncrep_parse_result->num_sync);
> >
> >
> >
>
> Updated as suggested in the attached patch.
>
> > 4)
> > +ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo * slot_states
> >
> > We can get rid of space before slot_states. I think pgindent might
> > have put it in my patch. Sorry for the trouble.
> >
> > 5)
> > 003:
> > - wait_for_all = (required == synchronized_standby_slots_config->nslotnames);
> > + wait_for_all =
> > + (synchronized_standby_slots_config->syncrep_method == SYNC_REP_DEFAULT);
> >
> > This change can be moved to 002, right?
>
> Done.
>
> PFA patch containing all the above changes.
>
Hi Ashutosh,
I have reviewed the patches. Here are some comments:
1. Should we update the doc for function "pg_logical_slot_get_changes". It says:
```
If the specified slot is a logical failover slot then the function will
not return until all physical slots specified in
<link linkend="guc-synchronized-standby-slots"><varname>synchronized_standby_slots</varname></link>
have confirmed WAL receipt.
```
This line "return until all physical slots specified in" seems wrong
after introduction of "FIRST/ANY"
2. Similarly, should we update the doc for function
"pg_replication_slot_advance"? It also says:
```
If the specified slot is a
logical failover slot then the function will not return until all
physical slots specified in
<link linkend="guc-synchronized-standby-slots"><varname>synchronized_standby_slots</varname></link>
have confirmed WAL receipt.
```
3. Comment in syncrep_gram.y states:
* syncrep_gram.y - Parser for synchronous_standby_names
Since synchronosed_standby_slots is reusing this, should we update this comment?
4. Similarly for syncrep_scanner.l, we have comment:
* syncrep_scanner.l
* a lexical scanner for synchronous_standby_names
Should we update it?
5. enum "SyncStandbySlotsState" and stuct "SyncStandbySlotsStateInfo" should be
mentioned in typedefs.list, so that pgindent works properly.
Thanks,
Shlok Kyal
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-04 09:26 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
@ 2026-06-05 10:02 ` shveta malik <[email protected]>
2026-06-08 06:08 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
1 sibling, 1 reply; 25+ messages in thread
From: shveta malik @ 2026-06-05 10:02 UTC (permalink / raw)
To: Ashutosh Sharma <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Amit Kapila <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>; shveta malik <[email protected]>
On Thu, Jun 4, 2026 at 2:57 PM Ashutosh Sharma <[email protected]> wrote:
>
>
> So my preferred behavior would be:
>
> 1) duplicate names: normalize, do not error
> 2) after normalization, if num_sync > unique_slots: error immediately
>
Thanks for tha pathces. I have attached a patch (txt file) with a few
trivial changes, take it if you find the changes acceptable.
thanks
Shveta
From bf2dd159b3de2f44cdb248b658f4c518ead98476 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 5 Jun 2026 15:28:45 +0530
Subject: [PATCH] top-up changes
---
doc/src/sgml/config.sgml | 32 +++---
src/backend/replication/slot.c | 106 ++++++++++--------
.../053_synchronized_standby_slots_quorum.pl | 16 ---
3 files changed, 80 insertions(+), 74 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 7d8f4b75717..44276fe2bc0 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5225,19 +5225,6 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
<literal>2 (slot1, slot2, slot3)</literal> and
<literal>FIRST 2 (slot1, slot2, slot3)</literal> are equivalent.
</para>
- <para>
- If the same physical replication slot name appears more than once,
- duplicate entries are ignored and only the first occurrence is used.
- The semantics of <varname>synchronized_standby_slots</varname> are
- therefore based on the unique set of listed slot names, preserving the
- original order of first occurrence. This means that, in
- priority-based forms, duplicates do not create additional priority
- positions: for example,
- <literal>FIRST 2 (slot1, slot1, slot2, slot3)</literal> is treated the
- same as <literal>FIRST 2 (slot1, slot2, slot3)</literal>. In particular,
- <replaceable class="parameter">num_sync</replaceable> must not exceed
- the number of unique listed slots.
- </para>
<para>
A plain comma-separated list without a keyword specifies that
<emphasis>all</emphasis> listed physical slots must confirm WAL
@@ -5268,6 +5255,25 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
(<literal>synchronous_standby_names = 'ANY ...'</literal>), so that
logical decoding availability matches the commit durability guarantee.
</para>
+ <para>
+ If the same physical replication slot name appears more than once,
+ duplicate entries are ignored and only the first occurrence is used.
+ The semantics of <varname>synchronized_standby_slots</varname> are
+ therefore based on the unique set of listed slot names, preserving the
+ original order of first occurrence. This means that, in
+ priority-based forms, duplicates do not create additional priority
+ positions: for example,
+ <literal>FIRST 2 (slot1, slot1, slot2, slot3)</literal> is treated the
+ same as <literal>FIRST 2 (slot1, slot2, slot3)</literal>.
+ Likewise, <literal>ANY 2 (slot1, slot1, slot2, slot3)</literal> is
+ treated the same as <literal>ANY 2 (slot1, slot2, slot3)</literal>,
+ and a plain list such as <literal>(slot1, slot1, slot2)</literal>
+ is treated the same as <literal>(slot1, slot2)</literal>. In particular,
+ <replaceable class="parameter">num_sync</replaceable> must not exceed
+ the number of unique listed slots. Such a configuration results in an
+ error to prevent indefinite waits in WAL sender processes due to a
+ misconfigured <varname>synchronized_standby_slots</varname> setting.
+ </para>
<para>
<literal>FIRST</literal> and <literal>ANY</literal> are case-insensitive.
If these keywords are used as the name of a replication slot,
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index fb45a1cd581..1e2e631d6a4 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -103,7 +103,7 @@ typedef struct
int config_size; /* total size of this struct, in bytes */
int num_sync; /* number of slots that must confirm WAL
* receipt before logical decoding proceeds */
- uint8 syncrep_method; /* SYNC_REP_* method */
+ uint8 syncrep_method; /* SYNC_REP_* method */
int nslotnames; /* number of slot names that follow */
/*
@@ -117,12 +117,12 @@ typedef struct
*/
typedef enum
{
- SS_SLOT_NOT_FOUND, /* slot does not exist */
- SS_SLOT_LOGICAL, /* slot is logical, not physical */
- SS_SLOT_INVALIDATED, /* slot has been invalidated */
+ SS_SLOT_NOT_FOUND, /* slot does not exist */
+ SS_SLOT_LOGICAL, /* slot is logical, not physical */
+ SS_SLOT_INVALIDATED, /* slot has been invalidated */
SS_SLOT_INACTIVE_LAGGING, /* slot is inactive and behind wait_for_lsn */
- SS_SLOT_ACTIVE_LAGGING, /* slot is active and behind wait_for_lsn */
-} SyncStandbySlotsState;
+ SS_SLOT_ACTIVE_LAGGING, /* slot is active and behind wait_for_lsn */
+} SyncStandbySlotsState;
/*
* Information about a synchronized standby slot's state.
@@ -131,8 +131,9 @@ typedef struct
{
const char *slot_name; /* name of the slot */
SyncStandbySlotsState state; /* state of the slot */
- XLogRecPtr restart_lsn; /* current restart_lsn (valid for lagging states) */
-} SyncStandbySlotsStateInfo;
+ XLogRecPtr restart_lsn; /* current restart_lsn (valid for lagging
+ * states) */
+} SyncStandbySlotsStateInfo;
/*
* Lookup table for slot invalidation causes.
@@ -2994,16 +2995,20 @@ GetSlotInvalidationCauseName(ReplicationSlotInvalidationCause cause)
}
/*
- * Remove duplicate member names from a flat SyncRepConfigData in place.
+ * Remove duplicate member names from a SyncRepConfigData object.
*
- * The first occurrence of each name is kept and input order is preserved.
+ * The member_names array of SyncRepConfigData is compacted in place so
+ * that only the first occurrence of each member name is retained. The
+ * original ordering of retained names is preserved, and nmembers and
+ * config_size are updated to describe only the compacted portion of
+ * the array.
*/
static void
CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
{
char *src_name;
char *dst_name;
- int unique_members = 0;
+ int nunique_members = 0;
Size unique_size = offsetof(SyncRepConfigData, member_names);
src_name = config->member_names;
@@ -3011,38 +3016,47 @@ CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
for (int i = 0; i < config->nmembers; i++)
{
- char *existing_name;
+ char *unique_name;
size_t name_size;
bool duplicate = false;
name_size = strlen(src_name) + 1;
- existing_name = config->member_names;
- for (int j = 0; j < unique_members; j++)
+ /*
+ * Check whether src_name matches any previously retained unique name.
+ * Only the first nunique_members entries in member_names need to be
+ * examined for this.
+ */
+ unique_name = config->member_names;
+ for (int j = 0; j < nunique_members; j++)
{
- if (strcmp(existing_name, src_name) == 0)
+ if (strcmp(unique_name, src_name) == 0)
{
duplicate = true;
break;
}
- existing_name += strlen(existing_name) + 1;
+ unique_name += strlen(unique_name) + 1;
}
if (!duplicate)
{
+ /*
+ * This src_name is a new unique name. Copy it immediately after the
+ * unique names retained so far.
+ */
if (dst_name != src_name)
memmove(dst_name, src_name, name_size);
dst_name += name_size;
- unique_members++;
+ nunique_members++;
unique_size += name_size;
}
src_name += name_size;
}
- config->nmembers = unique_members;
+ config->nmembers = nunique_members;
config->config_size = (int) unique_size;
}
@@ -3125,14 +3139,14 @@ check_synchronized_standby_slots(char **newval, void **extra, GucSource source)
mname += strlen(mname) + 1;
}
- /* Omit duplicate slot names so one slot is considered only once. */
+ /* Omit duplicate slot names to ensure each slot is considered only once. */
CompactSyncRepConfigMemberNames(syncrep_parse_result);
/*
* For synchronized_standby_slots, a comma-separated list means all
* listed slots are required. The syncrep parser preserves this shape
- * as SYNC_REP_DEFAULT, so map num_sync to nmembers to enforce all-mode
- * semantics after removing duplicate names.
+ * as SYNC_REP_DEFAULT, so map num_sync to nmembers to enforce
+ * all-mode semantics after removing duplicate names.
*/
if (syncrep_parse_result->syncrep_method == SYNC_REP_DEFAULT)
syncrep_parse_result->num_sync = syncrep_parse_result->nmembers;
@@ -3224,7 +3238,7 @@ SlotExistsInSyncStandbySlots(const char *slot_name)
* catch-up requirement from being met.
*/
static void
-ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo *slot_states,
+ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo * slot_states,
int num_slot_states, int elevel,
XLogRecPtr wait_for_lsn)
{
@@ -3285,15 +3299,15 @@ ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo *slot_states,
errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
slot_name, "synchronized_standby_slots"),
errdetail("The slot's restart_lsn is not yet set; required LSN is %X/%X.",
- LSN_FORMAT_ARGS(wait_for_lsn)));
+ LSN_FORMAT_ARGS(wait_for_lsn)));
else
ereport(DEBUG1,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
slot_name, "synchronized_standby_slots"),
errdetail("The slot's restart_lsn %X/%X is behind the required %X/%X.",
- LSN_FORMAT_ARGS(slot_states[i].restart_lsn),
- LSN_FORMAT_ARGS(wait_for_lsn)));
+ LSN_FORMAT_ARGS(slot_states[i].restart_lsn),
+ LSN_FORMAT_ARGS(wait_for_lsn)));
break;
default:
@@ -3364,14 +3378,14 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
* Determine how many slots are required and whether we're in "wait for
* ALL" mode versus "wait for N-of-M" mode.
*
- * wait_for_all = true means we need ALL slots to be ready (simple
- * list syntax like "slot1, slot2"). In this mode, we stop checking
- * on the first slot that is missing/invalid/logical, or the first slot
- * that is lagging (inactive or active).
+ * wait_for_all = true means we need ALL slots to be ready (simple list
+ * syntax like "slot1, slot2"). In this mode, we stop checking on the
+ * first slot that is missing/invalid/logical, or the first slot that is
+ * lagging (inactive or active).
*
* wait_for_all = false means we select N from M candidates (FIRST N or
- * ANY N syntax). In this mode, slots already caught up are counted even if
- * inactive. In FIRST N mode, we skip missing/invalid/logical slots and
+ * ANY N syntax). In this mode, slots already caught up are counted even
+ * if inactive. In FIRST N mode, we skip missing/invalid/logical slots and
* lagging inactive slots, but wait for an active lagging slot with higher
* priority. In ANY N mode, we skip lagging slots (inactive or active) to
* find any N that have caught up. Duplicate configured slot names do not
@@ -3387,7 +3401,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
* configured slots since in the worst case all could have problem states.
*/
slot_states = palloc_array(SyncStandbySlotsStateInfo,
- synchronized_standby_slots_config->nslotnames);
+ synchronized_standby_slots_config->nslotnames);
/*
* To prevent concurrent slot dropping and creation while filtering the
@@ -3396,8 +3410,8 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
/*
- * Iterate through configured slots, checking their state and tracking
- * how many have caught up. Problem states are recorded for deferred
+ * Iterate through configured slots, checking their state and tracking how
+ * many have caught up. Problem states are recorded for deferred
* reporting: missing/logical/invalidated slots, and lagging slots
* (inactive or active). Messages are only emitted if the catch-up
* requirement isn't met.
@@ -3457,13 +3471,12 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
if (!XLogRecPtrIsValid(restart_lsn) || restart_lsn < wait_for_lsn)
{
/*
- * If a slot is inactive and lagging, report it as inactive.
- * If it is active and lagging, report it as lagging.
+ * If a slot is inactive and lagging, report it as inactive. If it
+ * is active and lagging, report it as lagging.
*
- * In ALL mode: must wait for it.
- * In FIRST N (priority) mode: lagging active slots block, while
- * inactive slots can be skipped.
- * In ANY N (quorum) mode: skip and use another slot.
+ * In ALL mode: must wait for it. In FIRST N (priority) mode:
+ * lagging active slots block, while inactive slots can be
+ * skipped. In ANY N (quorum) mode: skip and use another slot.
*/
slot_states[num_slot_states].slot_name = name;
slot_states[num_slot_states].state =
@@ -3497,12 +3510,12 @@ next_slot:
LWLockRelease(ReplicationSlotControlLock);
/*
- * If the required number of slots have not caught up, report any
- * recorded problem states and return false.
+ * If the required number of slots have not caught up, report any recorded
+ * problem states and return false.
*
* We only emit messages when the requirement is not met to avoid
- * misleading messages in quorum/priority mode where other slots may
- * have satisfied the condition despite some slots having issues.
+ * misleading messages in quorum/priority mode where other slots may have
+ * satisfied the condition despite some slots having issues.
*/
if (caught_up_slot_num < required)
{
@@ -3550,7 +3563,10 @@ WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
ProcessConfigFile(PGC_SIGHUP);
}
- /* Exit once the configured synchronized_standby_slots requirement is met. */
+ /*
+ * Exit once the configured synchronized_standby_slots requirement is
+ * met.
+ */
if (StandbySlotsHaveCaughtup(wait_for_lsn, WARNING))
break;
diff --git a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
index 67a5b1d9657..be54b788807 100644
--- a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
+++ b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
@@ -15,22 +15,6 @@
#
# synchronous_standby_names = 'ANY 1 (standby1, standby2)'
#
-# Test scenarios:
-#
-# A) Plain list 'sb1_slot, sb2_slot' (ALL mode)
-# - Works when all slots are available
-# - Blocks immediately if ANY slot is unavailable
-#
-# B) ANY N (sb1_slot, sb2_slot, ...) (quorum mode)
-# - Proceeds when at least N slots have caught up
-# - Skips missing/invalid/logical slots and lagging slots (inactive or active)
-# to find N caught-up slots
-#
-# C) FIRST N (sb1_slot, sb2_slot) (priority mode)
-# - Selects first N slots in priority order (list order)
-# - Skips missing/invalid/logical slots and inactive lagging slots,
-# but waits for active lagging slots
-# - FIRST 1 works with one slot down (unlike plain list)
use strict;
use warnings FATAL => 'all';
--
2.34.1
Attachments:
[text/plain] 0001-top-up-changes.patch.txt (14.9K, 2-0001-top-up-changes.patch.txt)
download | inline diff:
From bf2dd159b3de2f44cdb248b658f4c518ead98476 Mon Sep 17 00:00:00 2001
From: Shveta Malik <[email protected]>
Date: Fri, 5 Jun 2026 15:28:45 +0530
Subject: [PATCH] top-up changes
---
doc/src/sgml/config.sgml | 32 +++---
src/backend/replication/slot.c | 106 ++++++++++--------
.../053_synchronized_standby_slots_quorum.pl | 16 ---
3 files changed, 80 insertions(+), 74 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 7d8f4b75717..44276fe2bc0 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5225,19 +5225,6 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
<literal>2 (slot1, slot2, slot3)</literal> and
<literal>FIRST 2 (slot1, slot2, slot3)</literal> are equivalent.
</para>
- <para>
- If the same physical replication slot name appears more than once,
- duplicate entries are ignored and only the first occurrence is used.
- The semantics of <varname>synchronized_standby_slots</varname> are
- therefore based on the unique set of listed slot names, preserving the
- original order of first occurrence. This means that, in
- priority-based forms, duplicates do not create additional priority
- positions: for example,
- <literal>FIRST 2 (slot1, slot1, slot2, slot3)</literal> is treated the
- same as <literal>FIRST 2 (slot1, slot2, slot3)</literal>. In particular,
- <replaceable class="parameter">num_sync</replaceable> must not exceed
- the number of unique listed slots.
- </para>
<para>
A plain comma-separated list without a keyword specifies that
<emphasis>all</emphasis> listed physical slots must confirm WAL
@@ -5268,6 +5255,25 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
(<literal>synchronous_standby_names = 'ANY ...'</literal>), so that
logical decoding availability matches the commit durability guarantee.
</para>
+ <para>
+ If the same physical replication slot name appears more than once,
+ duplicate entries are ignored and only the first occurrence is used.
+ The semantics of <varname>synchronized_standby_slots</varname> are
+ therefore based on the unique set of listed slot names, preserving the
+ original order of first occurrence. This means that, in
+ priority-based forms, duplicates do not create additional priority
+ positions: for example,
+ <literal>FIRST 2 (slot1, slot1, slot2, slot3)</literal> is treated the
+ same as <literal>FIRST 2 (slot1, slot2, slot3)</literal>.
+ Likewise, <literal>ANY 2 (slot1, slot1, slot2, slot3)</literal> is
+ treated the same as <literal>ANY 2 (slot1, slot2, slot3)</literal>,
+ and a plain list such as <literal>(slot1, slot1, slot2)</literal>
+ is treated the same as <literal>(slot1, slot2)</literal>. In particular,
+ <replaceable class="parameter">num_sync</replaceable> must not exceed
+ the number of unique listed slots. Such a configuration results in an
+ error to prevent indefinite waits in WAL sender processes due to a
+ misconfigured <varname>synchronized_standby_slots</varname> setting.
+ </para>
<para>
<literal>FIRST</literal> and <literal>ANY</literal> are case-insensitive.
If these keywords are used as the name of a replication slot,
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index fb45a1cd581..1e2e631d6a4 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -103,7 +103,7 @@ typedef struct
int config_size; /* total size of this struct, in bytes */
int num_sync; /* number of slots that must confirm WAL
* receipt before logical decoding proceeds */
- uint8 syncrep_method; /* SYNC_REP_* method */
+ uint8 syncrep_method; /* SYNC_REP_* method */
int nslotnames; /* number of slot names that follow */
/*
@@ -117,12 +117,12 @@ typedef struct
*/
typedef enum
{
- SS_SLOT_NOT_FOUND, /* slot does not exist */
- SS_SLOT_LOGICAL, /* slot is logical, not physical */
- SS_SLOT_INVALIDATED, /* slot has been invalidated */
+ SS_SLOT_NOT_FOUND, /* slot does not exist */
+ SS_SLOT_LOGICAL, /* slot is logical, not physical */
+ SS_SLOT_INVALIDATED, /* slot has been invalidated */
SS_SLOT_INACTIVE_LAGGING, /* slot is inactive and behind wait_for_lsn */
- SS_SLOT_ACTIVE_LAGGING, /* slot is active and behind wait_for_lsn */
-} SyncStandbySlotsState;
+ SS_SLOT_ACTIVE_LAGGING, /* slot is active and behind wait_for_lsn */
+} SyncStandbySlotsState;
/*
* Information about a synchronized standby slot's state.
@@ -131,8 +131,9 @@ typedef struct
{
const char *slot_name; /* name of the slot */
SyncStandbySlotsState state; /* state of the slot */
- XLogRecPtr restart_lsn; /* current restart_lsn (valid for lagging states) */
-} SyncStandbySlotsStateInfo;
+ XLogRecPtr restart_lsn; /* current restart_lsn (valid for lagging
+ * states) */
+} SyncStandbySlotsStateInfo;
/*
* Lookup table for slot invalidation causes.
@@ -2994,16 +2995,20 @@ GetSlotInvalidationCauseName(ReplicationSlotInvalidationCause cause)
}
/*
- * Remove duplicate member names from a flat SyncRepConfigData in place.
+ * Remove duplicate member names from a SyncRepConfigData object.
*
- * The first occurrence of each name is kept and input order is preserved.
+ * The member_names array of SyncRepConfigData is compacted in place so
+ * that only the first occurrence of each member name is retained. The
+ * original ordering of retained names is preserved, and nmembers and
+ * config_size are updated to describe only the compacted portion of
+ * the array.
*/
static void
CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
{
char *src_name;
char *dst_name;
- int unique_members = 0;
+ int nunique_members = 0;
Size unique_size = offsetof(SyncRepConfigData, member_names);
src_name = config->member_names;
@@ -3011,38 +3016,47 @@ CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
for (int i = 0; i < config->nmembers; i++)
{
- char *existing_name;
+ char *unique_name;
size_t name_size;
bool duplicate = false;
name_size = strlen(src_name) + 1;
- existing_name = config->member_names;
- for (int j = 0; j < unique_members; j++)
+ /*
+ * Check whether src_name matches any previously retained unique name.
+ * Only the first nunique_members entries in member_names need to be
+ * examined for this.
+ */
+ unique_name = config->member_names;
+ for (int j = 0; j < nunique_members; j++)
{
- if (strcmp(existing_name, src_name) == 0)
+ if (strcmp(unique_name, src_name) == 0)
{
duplicate = true;
break;
}
- existing_name += strlen(existing_name) + 1;
+ unique_name += strlen(unique_name) + 1;
}
if (!duplicate)
{
+ /*
+ * This src_name is a new unique name. Copy it immediately after the
+ * unique names retained so far.
+ */
if (dst_name != src_name)
memmove(dst_name, src_name, name_size);
dst_name += name_size;
- unique_members++;
+ nunique_members++;
unique_size += name_size;
}
src_name += name_size;
}
- config->nmembers = unique_members;
+ config->nmembers = nunique_members;
config->config_size = (int) unique_size;
}
@@ -3125,14 +3139,14 @@ check_synchronized_standby_slots(char **newval, void **extra, GucSource source)
mname += strlen(mname) + 1;
}
- /* Omit duplicate slot names so one slot is considered only once. */
+ /* Omit duplicate slot names to ensure each slot is considered only once. */
CompactSyncRepConfigMemberNames(syncrep_parse_result);
/*
* For synchronized_standby_slots, a comma-separated list means all
* listed slots are required. The syncrep parser preserves this shape
- * as SYNC_REP_DEFAULT, so map num_sync to nmembers to enforce all-mode
- * semantics after removing duplicate names.
+ * as SYNC_REP_DEFAULT, so map num_sync to nmembers to enforce
+ * all-mode semantics after removing duplicate names.
*/
if (syncrep_parse_result->syncrep_method == SYNC_REP_DEFAULT)
syncrep_parse_result->num_sync = syncrep_parse_result->nmembers;
@@ -3224,7 +3238,7 @@ SlotExistsInSyncStandbySlots(const char *slot_name)
* catch-up requirement from being met.
*/
static void
-ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo *slot_states,
+ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo * slot_states,
int num_slot_states, int elevel,
XLogRecPtr wait_for_lsn)
{
@@ -3285,15 +3299,15 @@ ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo *slot_states,
errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
slot_name, "synchronized_standby_slots"),
errdetail("The slot's restart_lsn is not yet set; required LSN is %X/%X.",
- LSN_FORMAT_ARGS(wait_for_lsn)));
+ LSN_FORMAT_ARGS(wait_for_lsn)));
else
ereport(DEBUG1,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
slot_name, "synchronized_standby_slots"),
errdetail("The slot's restart_lsn %X/%X is behind the required %X/%X.",
- LSN_FORMAT_ARGS(slot_states[i].restart_lsn),
- LSN_FORMAT_ARGS(wait_for_lsn)));
+ LSN_FORMAT_ARGS(slot_states[i].restart_lsn),
+ LSN_FORMAT_ARGS(wait_for_lsn)));
break;
default:
@@ -3364,14 +3378,14 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
* Determine how many slots are required and whether we're in "wait for
* ALL" mode versus "wait for N-of-M" mode.
*
- * wait_for_all = true means we need ALL slots to be ready (simple
- * list syntax like "slot1, slot2"). In this mode, we stop checking
- * on the first slot that is missing/invalid/logical, or the first slot
- * that is lagging (inactive or active).
+ * wait_for_all = true means we need ALL slots to be ready (simple list
+ * syntax like "slot1, slot2"). In this mode, we stop checking on the
+ * first slot that is missing/invalid/logical, or the first slot that is
+ * lagging (inactive or active).
*
* wait_for_all = false means we select N from M candidates (FIRST N or
- * ANY N syntax). In this mode, slots already caught up are counted even if
- * inactive. In FIRST N mode, we skip missing/invalid/logical slots and
+ * ANY N syntax). In this mode, slots already caught up are counted even
+ * if inactive. In FIRST N mode, we skip missing/invalid/logical slots and
* lagging inactive slots, but wait for an active lagging slot with higher
* priority. In ANY N mode, we skip lagging slots (inactive or active) to
* find any N that have caught up. Duplicate configured slot names do not
@@ -3387,7 +3401,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
* configured slots since in the worst case all could have problem states.
*/
slot_states = palloc_array(SyncStandbySlotsStateInfo,
- synchronized_standby_slots_config->nslotnames);
+ synchronized_standby_slots_config->nslotnames);
/*
* To prevent concurrent slot dropping and creation while filtering the
@@ -3396,8 +3410,8 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
/*
- * Iterate through configured slots, checking their state and tracking
- * how many have caught up. Problem states are recorded for deferred
+ * Iterate through configured slots, checking their state and tracking how
+ * many have caught up. Problem states are recorded for deferred
* reporting: missing/logical/invalidated slots, and lagging slots
* (inactive or active). Messages are only emitted if the catch-up
* requirement isn't met.
@@ -3457,13 +3471,12 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
if (!XLogRecPtrIsValid(restart_lsn) || restart_lsn < wait_for_lsn)
{
/*
- * If a slot is inactive and lagging, report it as inactive.
- * If it is active and lagging, report it as lagging.
+ * If a slot is inactive and lagging, report it as inactive. If it
+ * is active and lagging, report it as lagging.
*
- * In ALL mode: must wait for it.
- * In FIRST N (priority) mode: lagging active slots block, while
- * inactive slots can be skipped.
- * In ANY N (quorum) mode: skip and use another slot.
+ * In ALL mode: must wait for it. In FIRST N (priority) mode:
+ * lagging active slots block, while inactive slots can be
+ * skipped. In ANY N (quorum) mode: skip and use another slot.
*/
slot_states[num_slot_states].slot_name = name;
slot_states[num_slot_states].state =
@@ -3497,12 +3510,12 @@ next_slot:
LWLockRelease(ReplicationSlotControlLock);
/*
- * If the required number of slots have not caught up, report any
- * recorded problem states and return false.
+ * If the required number of slots have not caught up, report any recorded
+ * problem states and return false.
*
* We only emit messages when the requirement is not met to avoid
- * misleading messages in quorum/priority mode where other slots may
- * have satisfied the condition despite some slots having issues.
+ * misleading messages in quorum/priority mode where other slots may have
+ * satisfied the condition despite some slots having issues.
*/
if (caught_up_slot_num < required)
{
@@ -3550,7 +3563,10 @@ WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
ProcessConfigFile(PGC_SIGHUP);
}
- /* Exit once the configured synchronized_standby_slots requirement is met. */
+ /*
+ * Exit once the configured synchronized_standby_slots requirement is
+ * met.
+ */
if (StandbySlotsHaveCaughtup(wait_for_lsn, WARNING))
break;
diff --git a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
index 67a5b1d9657..be54b788807 100644
--- a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
+++ b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
@@ -15,22 +15,6 @@
#
# synchronous_standby_names = 'ANY 1 (standby1, standby2)'
#
-# Test scenarios:
-#
-# A) Plain list 'sb1_slot, sb2_slot' (ALL mode)
-# - Works when all slots are available
-# - Blocks immediately if ANY slot is unavailable
-#
-# B) ANY N (sb1_slot, sb2_slot, ...) (quorum mode)
-# - Proceeds when at least N slots have caught up
-# - Skips missing/invalid/logical slots and lagging slots (inactive or active)
-# to find N caught-up slots
-#
-# C) FIRST N (sb1_slot, sb2_slot) (priority mode)
-# - Selects first N slots in priority order (list order)
-# - Skips missing/invalid/logical slots and inactive lagging slots,
-# but waits for active lagging slots
-# - FIRST 1 works with one slot down (unlike plain list)
use strict;
use warnings FATAL => 'all';
--
2.34.1
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-04 09:26 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-05 10:02 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
@ 2026-06-08 06:08 ` Ashutosh Sharma <[email protected]>
2026-06-08 08:53 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
0 siblings, 1 reply; 25+ messages in thread
From: Ashutosh Sharma @ 2026-06-08 06:08 UTC (permalink / raw)
To: shveta malik <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Amit Kapila <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi,
On Fri, Jun 5, 2026 at 3:32 PM shveta malik <[email protected]> wrote:
>
> On Thu, Jun 4, 2026 at 2:57 PM Ashutosh Sharma <[email protected]> wrote:
> >
> >
> > So my preferred behavior would be:
> >
> > 1) duplicate names: normalize, do not error
> > 2) after normalization, if num_sync > unique_slots: error immediately
> >
>
> Thanks for tha pathces. I have attached a patch (txt file) with a few
> trivial changes, take it if you find the changes acceptable.
>
Thanks, I will review it and share my feedback.
--
With Regards,
Ashutosh Sharma.
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:11 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-06-03 11:00 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-04 07:36 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` RE: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Zhijie Hou (Fujitsu) <[email protected]>
2026-06-04 09:26 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-06-05 10:02 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-06-08 06:08 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
@ 2026-06-08 08:53 ` Ashutosh Sharma <[email protected]>
0 siblings, 0 replies; 25+ messages in thread
From: Ashutosh Sharma @ 2026-06-08 08:53 UTC (permalink / raw)
To: shveta malik <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Amit Kapila <[email protected]>; Ajin Cherian <[email protected]>; SATYANARAYANA NARLAPURAM <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi,
On Mon, Jun 8, 2026 at 11:38 AM Ashutosh Sharma <[email protected]> wrote:
>
> Hi,
>
> On Fri, Jun 5, 2026 at 3:32 PM shveta malik <[email protected]> wrote:
> >
> > Thanks for tha pathces. I have attached a patch (txt file) with a few
> > trivial changes, take it if you find the changes acceptable.
> >
>
> Thanks, I will review it and share my feedback.
>
PFA the patches that incorporate the changes from the top-up patch
shared by Shveta in [1]. These changes primarily consist of
documentation updates, comment improvements, and indentation fixes at
a few places.
[1] - https://www.postgresql.org/message-id/CAJpy0uAdBxGpc4wtj-LcTGMNkVCYu4eMbDr27snEO_SrN2cV4A%40mail.gma...
--
With Regards,
Ashutosh Sharma.
Attachments:
[application/octet-stream] 0003-Add-FIRST-N-and-N-.-priority-syntax-to-synchronized_.patch (23.3K, 2-0003-Add-FIRST-N-and-N-.-priority-syntax-to-synchronized_.patch)
download | inline diff:
From 93ea69cfd769d553f156d5ab6596c1e6c46454f6 Mon Sep 17 00:00:00 2001
From: Ashutosh Sharma <[email protected]>
Date: Thu, 4 Jun 2026 07:16:02 +0000
Subject: [PATCH 3/3] Add FIRST N and N (...) priority syntax to
synchronized_standby_slots
Extend synchronized_standby_slots to support explicit priority
forms aligned with synchronous_standby_names.
- FIRST N (slot1, slot2, ...)
- N (slot1, slot2, ...) as shorthand for FIRST N
Implementation details:
- Use the SYNC_REP_DEFAULT parser distinction from the earlier
refactor so plain-list syntax remains separate from priority
syntax.
- Extend StandbySlotsHaveCaughtup() priority handling.
- Select slots in list order.
- Skip missing, logical, invalidated, and inactive lagging slots.
- Wait for active lagging higher-priority slots.
- Clarify duplicate handling for priority syntax in the
synchronized_standby_slots documentation.
- Simplify caught-up comments and clarify standby confirmation wait
comments to match the final control flow.
Tests and docs:
- Add coverage for FIRST behavior and shorthand N (...) behavior.
- Add plain-list disambiguation with first-prefixed slot names.
- Add FIRST duplicate-entry recovery coverage to show duplicates do
not create extra priority positions.
- Update docs for FIRST and shorthand priority syntax semantics.
- Clarify that duplicate slot names are ignored in priority-based
forms and preserve first-occurrence order.
Author: Satya Narlapuram <[email protected]>
Author: Ashutosh Sharma <[email protected]>
Reviewed-by: Shveta Malik <[email protected]>
Reviewed-by: Ajin Cherian <[email protected]>
Reviewed-by: Hou, Zhijie <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Surya Poondla <[email protected]>
Reviewed-by: Japin Li <[email protected]>
---
doc/src/sgml/config.sgml | 45 ++-
src/backend/replication/slot.c | 47 ++--
.../053_synchronized_standby_slots_quorum.pl | 262 ++++++++++++++++--
3 files changed, 306 insertions(+), 48 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 1f176bd48f4..473f2641b90 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5194,6 +5194,7 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
sender processes must wait on before delivering decoded changes. This
parameter uses the following syntax:
<synopsis>
+ [FIRST] <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="parameter">slot_name</replaceable> [, ...] )
ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="parameter">slot_name</replaceable> [, ...] )
<replaceable class="parameter">slot_name</replaceable> [, ...]
</synopsis>
@@ -5205,9 +5206,24 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
<replaceable class="parameter">num_sync</replaceable>
must be an integer value greater than zero and must not exceed the
number of listed slots.
- Other forms supported by
- <xref linkend="guc-synchronous-standby-names"/>, such as priority
- syntax, are not supported.
+ </para>
+ <para>
+ The keyword <literal>FIRST</literal>, coupled with
+ <replaceable class="parameter">num_sync</replaceable>, specifies
+ priority-based semantics. Logical decoding will wait for the first
+ <replaceable class="parameter">num_sync</replaceable> available
+ physical slots in priority order (the order they appear in the list).
+ Missing, logical, or invalidated slots are skipped. Inactive slots are
+ skipped only while they are lagging. However, if a slot exists and is
+ valid and active but has not yet caught up, the system will wait for it
+ rather than skipping to lower-priority slots. If, after skipping
+ unusable slots, fewer than
+ <replaceable class="parameter">num_sync</replaceable> usable slots
+ remain, logical decoding waits until enough slots become usable and
+ caught up, or until the configuration is changed. The keyword
+ <literal>FIRST</literal> is optional in this form, so
+ <literal>2 (slot1, slot2, slot3)</literal> and
+ <literal>FIRST 2 (slot1, slot2, slot3)</literal> are equivalent.
</para>
<para>
A plain comma-separated list without a keyword specifies that
@@ -5244,19 +5260,26 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
duplicate entries are ignored and only the first occurrence is used.
The semantics of <varname>synchronized_standby_slots</varname> are
therefore based on the unique set of listed slot names, preserving the
- original order of first occurrence. This means that
- <literal>ANY 2 (slot1, slot1, slot2, slot3)</literal> is treated the
- same as <literal>ANY 2 (slot1, slot2, slot3)</literal>, and a plain
- list such as <literal>(slot1, slot1, slot2)</literal> is treated the
- same as <literal>(slot1, slot2)</literal>. In particular,
+ original order of first occurrence. This means that, in
+ priority-based forms, duplicates do not create additional priority
+ positions: for example,
+ <literal>FIRST 2 (slot1, slot1, slot2, slot3)</literal> is treated the
+ same as <literal>FIRST 2 (slot1, slot2, slot3)</literal>.
+ Likewise, <literal>ANY 2 (slot1, slot1, slot2, slot3)</literal> is
+ treated the same as <literal>ANY 2 (slot1, slot2, slot3)</literal>,
+ and a plain list such as <literal>(slot1, slot1, slot2)</literal>
+ is treated the same as <literal>(slot1, slot2)</literal>. In particular,
<replaceable class="parameter">num_sync</replaceable> must not exceed
the number of unique listed slots. Such a configuration results in an
error to prevent indefinite waits in WAL sender processes due to a
misconfigured <varname>synchronized_standby_slots</varname> setting.
</para>
- <para>
- <literal>ANY</literal> is case-insensitive.
- </para>
+ <para>
+ <literal>FIRST</literal> and <literal>ANY</literal> are case-insensitive.
+ If these keywords are used as the name of a replication slot,
+ the <replaceable class="parameter">slot_name</replaceable> must
+ be double-quoted.
+ </para>
<para>
The use of <varname>synchronized_standby_slots</varname> guarantees
that logical replication
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 93797fcdde4..2c9d30de280 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -3068,6 +3068,8 @@ CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
*
* slot1, slot2 -- wait for ALL listed slots
* ANY N (slot1, slot2, ...) -- wait for any N-of-M (quorum)
+ * FIRST N (slot1, slot2, ...) -- wait for first N in priority order
+ * N (slot1, slot2, ...) -- shorthand for FIRST N
*
* Note: Simple list syntax is interpreted as "wait for ALL" for this GUC,
* unlike synchronous_standby_names where it means "FIRST 1".
@@ -3108,14 +3110,6 @@ check_synchronized_standby_slots(char **newval, void **extra, GucSource source)
return false;
}
- if (syncrep_parse_result->syncrep_method == SYNC_REP_PRIORITY)
- {
- GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
- GUC_check_errmsg("priority syntax is not supported for parameter \"%s\"",
- "synchronized_standby_slots");
- return false;
- }
-
if (syncrep_parse_result->num_sync <= 0)
{
GUC_check_errmsg("number of synchronized standby slots (%d) must be greater than zero",
@@ -3333,6 +3327,12 @@ ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo * slot_states,
* Simple list (e.g., "slot1, slot2"):
* ALL slots must have caught up. Returns false otherwise.
*
+ * FIRST N (e.g., "FIRST 2 (slot1, slot2, slot3)"):
+ * Wait for the first N eligible slots in priority order. Skips missing,
+ * invalid, logical, and inactive-lagging slots to find N eligible slots.
+ * If an active slot is lagging, waits for it (does not skip to lower
+ * priority slots).
+ *
* ANY N (e.g., "ANY 2 (slot1, slot2, slot3)"):
* Wait for any N eligible slots. Skips missing, invalid, logical, and
* lagging slots (inactive or active) to find N slots that have caught up.
@@ -3383,14 +3383,18 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
* first slot that is missing/invalid/logical, or the first slot that is
* lagging (inactive or active).
*
- * wait_for_all = false means we select N from M candidates (ANY N syntax).
- * In this mode, slots already caught up are counted even if inactive, and
- * lagging slots are skipped until enough slots have caught up.
- * Duplicate configured slot names do not appear here because the check hook
- * compacts them out of the parsed configuration.
+ * wait_for_all = false means we select N from M candidates (FIRST N or
+ * ANY N syntax). In this mode, slots already caught up are counted even if
+ * inactive. In FIRST N mode, we skip missing/invalid/logical slots and
+ * lagging inactive slots, but wait for an active lagging slot with higher
+ * priority. In ANY N mode, we skip lagging slots (inactive or active) to
+ * find any N that have caught up. Duplicate configured slot names do not
+ * appear here because the check hook compacts them out of the parsed
+ * configuration.
*/
required = synchronized_standby_slots_config->num_sync;
- wait_for_all = (required == synchronized_standby_slots_config->nslotnames);
+ wait_for_all =
+ (synchronized_standby_slots_config->syncrep_method == SYNC_REP_DEFAULT);
/*
* Allocate array to track slot states. Size it to the total number of
@@ -3470,8 +3474,9 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
* If a slot is inactive and lagging, report it as inactive. If it
* is active and lagging, report it as lagging.
*
- * In ALL mode: must wait for it. In ANY N (quorum) mode: skip and
- * use another slot.
+ * In ALL mode: must wait for it. In FIRST N (priority) mode:
+ * lagging active slots block, while inactive slots can be
+ * skipped. In ANY N (quorum) mode: skip and use another slot.
*/
slot_states[num_slot_states].slot_name = name;
slot_states[num_slot_states].state =
@@ -3479,7 +3484,9 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
slot_states[num_slot_states].restart_lsn = restart_lsn;
num_slot_states++;
- if (wait_for_all)
+ if (wait_for_all ||
+ (!inactive &&
+ synchronized_standby_slots_config->syncrep_method == SYNC_REP_PRIORITY))
break;
goto next_slot;
}
@@ -3492,7 +3499,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
caught_up_slot_num++;
- /* Stop processing if the required number of slots have caught up. */
+ /* Stop once the required number of slots have caught up. */
if (caught_up_slot_num >= required)
break;
@@ -3507,8 +3514,8 @@ next_slot:
* problem states and return false.
*
* We only emit messages when the requirement is not met to avoid
- * misleading messages in quorum mode where other slots may have satisfied
- * the condition despite some slots having issues.
+ * misleading messages in quorum/priority mode where other slots may have
+ * satisfied the condition despite some slots having issues.
*/
if (caught_up_slot_num < required)
{
diff --git a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
index d387e0c7e7e..a4153a44b37 100644
--- a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
+++ b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
@@ -4,6 +4,7 @@
# Test synchronized_standby_slots with different syntax modes:
# - Plain list (ALL mode): slot1, slot2
# - ANY N (quorum mode): ANY N (slot1, slot2, ...)
+# - FIRST N (priority mode): FIRST N (slot1, slot2, ...)
#
# Setup: a 3-node cluster with one primary, two physical standbys, and a
# logical decoding client using a failover-enabled slot.
@@ -200,16 +201,168 @@ is($decoded_bc, '1',
'plain list: works when all standbys are up');
##################################################
-# PART D: ANY 2 waits on an active lagging slot
+# PART D: Verify FIRST N priority semantics
##################################################
-# Stop standby1 so sb1_slot can be controlled by a raw replication connection
-# that keeps the slot active while lagging.
+# FIRST N should:
+# 1. Select first N slots in priority order (list order)
+# 2. Skip missing/invalid/logical slots and inactive lagging slots to find
+# N caught-up slots
+# 3. Wait for active lagging slots (not skip to lower priority)
+
+# Test FIRST 2 (sb1_slot, sb2_slot) with both up; should wait for both.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 2 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_2_both_up');"
+);
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+my $decoded_e2 = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%first_2_both_up%';});
+is($decoded_e2, '1',
+ 'FIRST 2: decoding works when all required slots are up');
+
+# Test FIRST 1 (sb1_slot, sb2_slot) with sb1_slot unavailable.
$standby1->stop;
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_1_skip_unavailable');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+# FIRST 1 should skip sb1_slot (unavailable) and use sb2_slot.
+my $decoded_e1 = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%first_1_skip_unavailable%';});
+is($decoded_e1, '1',
+ 'FIRST 1: skips unavailable first slot, uses second slot');
+
+# Test shorthand priority syntax: N (...) means FIRST N (...).
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'num_1_shorthand_priority');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+my $decoded_num1 = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%num_1_shorthand_priority%';});
+is($decoded_num1, '1',
+ '1 (...): shorthand priority syntax behaves like FIRST 1');
+
+##################################################
+# PART E: FIRST 1 and ANY 2 wait on an active lagging slot
+##################################################
+
+# Bring standby1 back so sb1_slot is active and caught up.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# To test the active-but-lagging slot path deterministically, we open a raw
+# replication connection to sb1_slot starting from a deliberately old LSN.
+# psql in replication mode never sends Standby Status Update messages, so
+# the walsender keeps sb1_slot's active_pid set but restart_lsn never
+# advances.
+
+# Stop standby1 so its walsender releases sb1_slot, allowing our replication
+# connection below to acquire it.
+$standby1->stop;
+
+# Capture a safely old LSN to stream from, before the test WAL record.
my $old_lsn = $primary->safe_psql('postgres',
"SELECT pg_current_wal_lsn();");
+# FIRST 1 must wait for the highest-priority slot when it is active but lagging.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+my $first_lag_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_1_lagging_blocks');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+# Open a raw replication connection to sb1_slot starting from $old_lsn.
+# This activates the slot (active_pid IS NOT NULL) while keeping restart_lsn
+# frozen below $first_lag_lsn for the lifetime of the connection.
+my $repl_first = $primary->background_psql(
+ 'postgres',
+ replication => 'database',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$repl_first->query_until(
+ qr/^$/,
+ "START_REPLICATION SLOT sb1_slot PHYSICAL $old_lsn;\n");
+
+# Wait until sb1_slot shows active_pid, confirming the walsender is live.
+$primary->poll_query_until('postgres', q{
+ SELECT active_pid IS NOT NULL
+ FROM pg_replication_slots
+ WHERE slot_name = 'sb1_slot'
+}) or die "replication connection did not activate sb1_slot";
+
+# sb1_slot is now active and its restart_lsn is behind $first_lag_lsn.
+# Start logical decoding in the background; it must block.
+my $bg_first = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_first->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'FIRST 1: decoding waits for active lagging higher-priority slot');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_first->quit;
+$repl_first->quit;
+
+# Ensure the previous replication connection has fully released sb1_slot
+# before reusing it in the next subtest.
+$primary->poll_query_until('postgres', q{
+ SELECT active_pid IS NULL
+ FROM pg_replication_slots
+ WHERE slot_name = 'sb1_slot'
+}) or die "replication connection did not release sb1_slot";
+
+# Consume the change so the slot is clean for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# ANY 2 must also wait when only one of two required slots has caught up.
+# Reuse the same technique: open a raw replication connection to sb1_slot
+# from $old_lsn so it is active but its restart_lsn stays behind the target.
+
+# Capture another old LSN baseline before the next test WAL record.
+$old_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_current_wal_lsn();");
+
$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
"'ANY 2 (sb1_slot, sb2_slot)'");
$primary->reload;
@@ -272,7 +425,54 @@ $primary->wait_for_replay_catchup($standby1);
##################################################
-# PART E: Duplicate entries are ignored for quorum counting
+# PART F: Plain list with first-prefixed slot name still means ALL mode
+##################################################
+
+# Create a slot name starting with "first_" for parser disambiguation checks.
+$primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('first_slot');");
+
+# If simple-list syntax starts with a slot name like "first_slot", it must
+# still be treated as ALL mode (not as explicit FIRST N syntax).
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'first_slot, sb2_slot'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_prefix_all_mode_blocks');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+$log_offset = -s $primary->logfile;
+
+$bg = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+# Plain list must require all listed slots; first_slot is intentionally inactive.
+$primary->wait_for_log(
+ qr/replication slot \"first_slot\" specified in parameter "synchronized_standby_slots" does not have active_pid/,
+ $log_offset);
+
+pass('plain list with first-prefixed slot name blocks in ALL mode');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg->quit;
+
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+##################################################
+# PART G: Duplicate entries are ignored for quorum counting
##################################################
# Stop standby2 so only sb1_slot can catch up.
@@ -313,6 +513,46 @@ $primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
$primary->reload;
$bg_dup->quit;
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# FIRST duplicates must also not create extra priority positions.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'FIRST 2 (sb1_slot, sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'first_duplicate_entries_ignored');"
+);
+$primary->wait_for_replay_catchup($standby1);
+
+my $bg_first_dup = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_first_dup->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'FIRST duplicates are ignored when counting priority slots');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_first_dup->quit;
+
# Consume the change for the next test.
$primary->safe_psql('postgres',
q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
@@ -323,7 +563,7 @@ $primary->wait_for_replay_catchup($standby2);
##################################################
-# PART F: Verify GUC validation rejects bad values
+# PART H: Verify GUC validation rejects bad values
##################################################
my ($result, $stdout, $stderr);
@@ -340,18 +580,6 @@ like($stderr, qr/ERROR/,
like($stderr, qr/ERROR/,
'GUC rejects malformed ANY syntax');
-# Priority syntax is not supported by synchronized_standby_slots yet
-($result, $stdout, $stderr) = $primary->psql('postgres',
- "ALTER SYSTEM SET synchronized_standby_slots = 'FIRST 1 (sb1_slot, sb2_slot)';");
-like($stderr, qr/priority syntax is not supported/,
- 'GUC rejects FIRST syntax');
-
-# Legacy priority syntax is not supported by synchronized_standby_slots yet
-($result, $stdout, $stderr) = $primary->psql('postgres',
- "ALTER SYSTEM SET synchronized_standby_slots = '1 (sb1_slot, sb2_slot)';");
-like($stderr, qr/priority syntax is not supported/,
- 'GUC rejects legacy priority syntax');
-
# Invalid slot name
($result, $stdout, $stderr) = $primary->psql('postgres',
"ALTER SYSTEM SET synchronized_standby_slots = 'ANY 1 (INVALID_UPPER)';");
--
2.43.0
[application/octet-stream] 0001-Refactor-syncrep-parsing-to-represent-bare-standby-l.patch (3.1K, 3-0001-Refactor-syncrep-parsing-to-represent-bare-standby-l.patch)
download | inline diff:
From 789018468cf3a503436450922a75e6d4085f727f Mon Sep 17 00:00:00 2001
From: Ashutosh Sharma <[email protected]>
Date: Wed, 13 May 2026 06:59:58 +0000
Subject: [PATCH 1/3] Refactor syncrep parsing to represent bare standby lists
explicitly
The syncrep parser currently reduces a simple list form to FIRST 1
(SYNC_REP_PRIORITY). That is acceptable for synchronous_standby_names,
but it loses information about whether FIRST was explicitly written.
Introduce SYNC_REP_DEFAULT to represent the bare list form parsed
from standby_list. This allows callers to distinguish:
- explicit priority syntax (FIRST N (...) or N (...))
- quorum syntax (ANY N (...))
- simple list syntax without FIRST/ANY
With this change:
- syncrep grammar emits SYNC_REP_DEFAULT for bare standby lists
- check_synchronous_standby_names() maps SYNC_REP_DEFAULT to
SYNC_REP_PRIORITY, preserving existing synchronous_standby_names
behavior
This is a preparatory patch for future synchronized_standby_slots
changes, where callers can directly interpret SYNC_REP_DEFAULT as
plain-list semantics, while keeping existing synchronous_standby_names
semantics intact.
Per suggestion from Zhijie Hou <[email protected]>
---
src/backend/replication/syncrep.c | 4 ++++
src/backend/replication/syncrep_gram.y | 2 +-
src/include/replication/syncrep.h | 1 +
3 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/backend/replication/syncrep.c b/src/backend/replication/syncrep.c
index e0e30579c59..ae8ecfa0711 100644
--- a/src/backend/replication/syncrep.c
+++ b/src/backend/replication/syncrep.c
@@ -1100,6 +1100,10 @@ check_synchronous_standby_names(char **newval, void **extra, GucSource source)
return false;
}
+ /* Default to FIRST 1 (name ...) priority method if not specified */
+ if (syncrep_parse_result->syncrep_method == SYNC_REP_DEFAULT)
+ syncrep_parse_result->syncrep_method = SYNC_REP_PRIORITY;
+
/* GUC extra value must be guc_malloc'd, not palloc'd */
pconf = (SyncRepConfigData *)
guc_malloc(LOG, syncrep_parse_result->config_size);
diff --git a/src/backend/replication/syncrep_gram.y b/src/backend/replication/syncrep_gram.y
index 1b9d7b2edc4..f1550e109ef 100644
--- a/src/backend/replication/syncrep_gram.y
+++ b/src/backend/replication/syncrep_gram.y
@@ -65,7 +65,7 @@ result:
;
standby_config:
- standby_list { $$ = create_syncrep_config("1", $1, SYNC_REP_PRIORITY); }
+ standby_list { $$ = create_syncrep_config("1", $1, SYNC_REP_DEFAULT); }
| NUM '(' standby_list ')' { $$ = create_syncrep_config($1, $3, SYNC_REP_PRIORITY); }
| ANY NUM '(' standby_list ')' { $$ = create_syncrep_config($2, $4, SYNC_REP_QUORUM); }
| FIRST NUM '(' standby_list ')' { $$ = create_syncrep_config($2, $4, SYNC_REP_PRIORITY); }
diff --git a/src/include/replication/syncrep.h b/src/include/replication/syncrep.h
index b42b5862a70..130c7f6f242 100644
--- a/src/include/replication/syncrep.h
+++ b/src/include/replication/syncrep.h
@@ -34,6 +34,7 @@
/* syncrep_method of SyncRepConfigData */
#define SYNC_REP_PRIORITY 0
#define SYNC_REP_QUORUM 1
+#define SYNC_REP_DEFAULT 2
/*
* SyncRepGetCandidateStandbys returns an array of these structs,
--
2.43.0
[application/octet-stream] 0002-Add-ANY-N-semantics-to-synchronized_standby_slots.patch (43.9K, 4-0002-Add-ANY-N-semantics-to-synchronized_standby_slots.patch)
download | inline diff:
From e1a4cab301522a2347a3677e12cd7bc970c70335 Mon Sep 17 00:00:00 2001
From: Ashutosh Sharma <[email protected]>
Date: Mon, 8 Jun 2026 08:26:40 +0000
Subject: [PATCH 2/3] Add ANY N semantics to synchronized_standby_slots
Extend synchronized_standby_slots with quorum syntax for logical
failover slot synchronization:
- ANY N (slot1, slot2, ...)
Plain-list semantics are preserved as-is:
- slot1, slot2 continues to mean all listed slots are required
Implementation details:
- Reuse syncrep parser infrastructure in the GUC check hook and
map parsed output into synchronized_standby_slots semantics.
- Consume SYNC_REP_DEFAULT from the preparatory parser refactor to
distinguish plain-list syntax from explicit parser modes.
- In StandbySlotsHaveCaughtup(), enforce mode-specific behavior for:
- existing all-listed-slots semantics (plain list)
- quorum N-of-M behavior (ANY N)
- Validation rejects configurations where N exceeds the number of
listed slots.
- Ignore duplicate synchronized_standby_slots entries, preserving the
first occurrence and applying semantics to the resulting unique list.
- Clarify synchronized_standby_slots comments and lagging restart_lsn
reporting to match the implemented behavior.
Tests and docs:
- Add recovery coverage for plain-list behavior and ANY quorum
behavior, including lagging-slot and validation-error scenarios.
- Add duplicate-entry recovery coverage for synchronized_standby_slots.
- Document ANY syntax and clarify plain-list behavior for this GUC.
- Document that duplicate slot names are ignored and counted only once.
Author: Satya Narlapuram <[email protected]>
Author: Ashutosh Sharma <[email protected]>
Reviewed-by: Shveta Malik <[email protected]>
Reviewed-by: Ajin Cherian <[email protected]>
Reviewed-by: Hou, Zhijie <[email protected]>
Reviewed-by: Dilip Kumar <[email protected]>
Reviewed-by: Surya Poondla <[email protected]>
Reviewed-by: Japin Li <[email protected]>
---
doc/src/sgml/config.sgml | 87 ++-
src/backend/replication/slot.c | 515 ++++++++++++++----
.../053_synchronized_standby_slots_quorum.pl | 367 +++++++++++++
3 files changed, 843 insertions(+), 126 deletions(-)
create mode 100644 src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fa566c9e553..1f176bd48f4 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -5190,17 +5190,84 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
</term>
<listitem>
<para>
- A comma-separated list of streaming replication standby server slot names
- that logical WAL sender processes will wait for. Logical WAL sender processes
- will send decoded changes to plugins only after the specified replication
- slots confirm receiving WAL. This guarantees that logical replication
+ Specifies the streaming replication standby slots that logical WAL
+ sender processes must wait on before delivering decoded changes. This
+ parameter uses the following syntax:
+<synopsis>
+ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="parameter">slot_name</replaceable> [, ...] )
+<replaceable class="parameter">slot_name</replaceable> [, ...]
+</synopsis>
+ where <replaceable class="parameter">num_sync</replaceable> is
+ the number of physical replication slots that must confirm WAL
+ receipt before logical decoding proceeds,
+ and <replaceable class="parameter">slot_name</replaceable>
+ is the name of a physical replication slot.
+ <replaceable class="parameter">num_sync</replaceable>
+ must be an integer value greater than zero and must not exceed the
+ number of listed slots.
+ Other forms supported by
+ <xref linkend="guc-synchronous-standby-names"/>, such as priority
+ syntax, are not supported.
+ </para>
+ <para>
+ A plain comma-separated list without a keyword specifies that
+ <emphasis>all</emphasis> listed physical slots must confirm WAL
+ receipt. This differs from <xref linkend="guc-synchronous-standby-names"/>
+ where a simple list means <literal>FIRST 1</literal>. For
+ <varname>synchronized_standby_slots</varname>, requiring all slots
+ provides safer failover semantics by default.
+ </para>
+ <para>
+ The keyword <literal>ANY</literal>, coupled with
+ <replaceable class="parameter">num_sync</replaceable>, specifies
+ quorum-based semantics. Logical decoding proceeds once at least
+ <replaceable class="parameter">num_sync</replaceable> of the listed
+ slots have caught up. Missing, logical, and invalidated slots are
+ skipped when determining candidates. Lagging slots (inactive or
+ active) simply do not count toward the required number until they
+ catch up.
+ If fewer than <replaceable class="parameter">num_sync</replaceable>
+ slots have caught up at a given moment, logical decoding waits until
+ that threshold is reached.
+ i.e., there is no priority ordering.
+ For example, a setting of <literal>ANY 1 (sb1_slot, sb2_slot)</literal>
+ allows logical decoding to proceed as soon as either physical slot has
+ confirmed WAL receipt. If none of the slots are available or have
+ caught up, logical decoding waits until at least one slot meets the
+ required condition. This is useful in conjunction with
+ quorum-based synchronous replication
+ (<literal>synchronous_standby_names = 'ANY ...'</literal>), so that
+ logical decoding availability matches the commit durability guarantee.
+ </para>
+ <para>
+ If the same physical replication slot name appears more than once,
+ duplicate entries are ignored and only the first occurrence is used.
+ The semantics of <varname>synchronized_standby_slots</varname> are
+ therefore based on the unique set of listed slot names, preserving the
+ original order of first occurrence. This means that
+ <literal>ANY 2 (slot1, slot1, slot2, slot3)</literal> is treated the
+ same as <literal>ANY 2 (slot1, slot2, slot3)</literal>, and a plain
+ list such as <literal>(slot1, slot1, slot2)</literal> is treated the
+ same as <literal>(slot1, slot2)</literal>. In particular,
+ <replaceable class="parameter">num_sync</replaceable> must not exceed
+ the number of unique listed slots. Such a configuration results in an
+ error to prevent indefinite waits in WAL sender processes due to a
+ misconfigured <varname>synchronized_standby_slots</varname> setting.
+ </para>
+ <para>
+ <literal>ANY</literal> is case-insensitive.
+ </para>
+ <para>
+ The use of <varname>synchronized_standby_slots</varname> guarantees
+ that logical replication
failover slots do not consume changes until those changes are received
- and flushed to corresponding physical standbys. If a
+ and flushed to the required physical standbys. If a
logical replication connection is meant to switch to a physical standby
after the standby is promoted, the physical replication slot for the
standby should be listed here. Note that logical replication will not
- proceed if the slots specified in the
- <varname>synchronized_standby_slots</varname> do not exist or are invalidated.
+ proceed if the required number of physical slots specified in
+ <varname>synchronized_standby_slots</varname> do not exist or are
+ invalidated.
Additionally, the replication management functions
<link linkend="pg-replication-slot-advance">
<function>pg_replication_slot_advance</function></link>,
@@ -5208,9 +5275,9 @@ ANY <replaceable class="parameter">num_sync</replaceable> ( <replaceable class="
<function>pg_logical_slot_get_changes</function></link>, and
<link linkend="pg-logical-slot-peek-changes">
<function>pg_logical_slot_peek_changes</function></link>,
- when used with logical failover slots, will block until all
- physical slots specified in <varname>synchronized_standby_slots</varname> have
- confirmed WAL receipt.
+ when used with logical failover slots, will block until the required
+ physical slots specified in <varname>synchronized_standby_slots</varname>
+ have confirmed WAL receipt.
</para>
<para>
The standbys corresponding to the physical replication slots in
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index d7fb9f5a67f..93797fcdde4 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -50,6 +50,7 @@
#include "replication/logicallauncher.h"
#include "replication/slotsync.h"
#include "replication/slot.h"
+#include "replication/syncrep.h"
#include "replication/walsender_private.h"
#include "storage/fd.h"
#include "storage/ipc.h"
@@ -91,11 +92,19 @@ typedef struct ReplicationSlotOnDisk
* Note: this must be a flat representation that can be held in a single chunk
* of guc_malloc'd memory, so that it can be stored as the "extra" data for the
* synchronized_standby_slots GUC.
+ *
+ * The layout mirrors SyncRepConfigData so that the same quorum and priority
+ * semantics can be expressed. The syncrep_method field uses the
+ * SYNC_REP_DEFAULT, SYNC_REP_PRIORITY, and SYNC_REP_QUORUM constants from
+ * syncrep.h.
*/
typedef struct
{
- /* Number of slot names in the slot_names[] */
- int nslotnames;
+ int config_size; /* total size of this struct, in bytes */
+ int num_sync; /* number of slots that must confirm WAL
+ * receipt before logical decoding proceeds */
+ uint8 syncrep_method; /* SYNC_REP_* method */
+ int nslotnames; /* number of slot names that follow */
/*
* slot_names contains 'nslotnames' consecutive null-terminated C strings.
@@ -103,6 +112,29 @@ typedef struct
char slot_names[FLEXIBLE_ARRAY_MEMBER];
} SyncStandbySlotsConfigData;
+/*
+ * State of a replication slot specified in synchronized_standby_slots GUC.
+ */
+typedef enum
+{
+ SS_SLOT_NOT_FOUND, /* slot does not exist */
+ SS_SLOT_LOGICAL, /* slot is logical, not physical */
+ SS_SLOT_INVALIDATED, /* slot has been invalidated */
+ SS_SLOT_INACTIVE_LAGGING, /* slot is inactive and behind wait_for_lsn */
+ SS_SLOT_ACTIVE_LAGGING, /* slot is active and behind wait_for_lsn */
+} SyncStandbySlotsState;
+
+/*
+ * Information about a synchronized standby slot's state.
+ */
+typedef struct
+{
+ const char *slot_name; /* name of the slot */
+ SyncStandbySlotsState state; /* state of the slot */
+ XLogRecPtr restart_lsn; /* current restart_lsn (valid for lagging
+ * states) */
+} SyncStandbySlotsStateInfo;
+
/*
* Lookup table for slot invalidation causes.
*/
@@ -2963,94 +2995,203 @@ GetSlotInvalidationCauseName(ReplicationSlotInvalidationCause cause)
}
/*
- * A helper function to validate slots specified in GUC synchronized_standby_slots.
+ * Remove duplicate member names from a SyncRepConfigData object.
*
- * The rawname will be parsed, and the result will be saved into *elemlist.
+ * The member_names array of SyncRepConfigData is compacted in place so
+ * that only the first occurrence of each member name is retained. The
+ * original ordering of retained names is preserved, and nmembers and
+ * config_size are updated to describe only the compacted portion of
+ * the array.
*/
-static bool
-validate_sync_standby_slots(char *rawname, List **elemlist)
+static void
+CompactSyncRepConfigMemberNames(SyncRepConfigData *config)
{
- /* Verify syntax and parse string into a list of identifiers */
- if (!SplitIdentifierString(rawname, ',', elemlist))
- {
- GUC_check_errdetail("List syntax is invalid.");
- return false;
- }
+ char *src_name;
+ char *dst_name;
+ int nunique_members = 0;
+ Size unique_size = offsetof(SyncRepConfigData, member_names);
- /* Iterate the list to validate each slot name */
- foreach_ptr(char, name, *elemlist)
+ src_name = config->member_names;
+ dst_name = config->member_names;
+
+ for (int i = 0; i < config->nmembers; i++)
{
- int err_code;
- char *err_msg = NULL;
- char *err_hint = NULL;
+ char *unique_name;
+ size_t name_size;
+ bool duplicate = false;
+
+ name_size = strlen(src_name) + 1;
- if (!ReplicationSlotValidateNameInternal(name, false, &err_code,
- &err_msg, &err_hint))
+ /*
+ * Check whether src_name matches any previously retained unique name.
+ * Only the first nunique_members entries in member_names need to be
+ * examined for this.
+ */
+ unique_name = config->member_names;
+ for (int j = 0; j < nunique_members; j++)
{
- GUC_check_errcode(err_code);
- GUC_check_errdetail("%s", err_msg);
- if (err_hint != NULL)
- GUC_check_errhint("%s", err_hint);
- return false;
+ if (strcmp(unique_name, src_name) == 0)
+ {
+ duplicate = true;
+ break;
+ }
+
+ unique_name += strlen(unique_name) + 1;
+ }
+
+ if (!duplicate)
+ {
+ /*
+ * This src_name is a new unique name. Copy it immediately after the
+ * unique names retained so far.
+ */
+ if (dst_name != src_name)
+ memmove(dst_name, src_name, name_size);
+
+ dst_name += name_size;
+ nunique_members++;
+ unique_size += name_size;
}
+
+ src_name += name_size;
}
- return true;
+ config->nmembers = nunique_members;
+ config->config_size = (int) unique_size;
}
/*
* GUC check_hook for synchronized_standby_slots
+ *
+ * This reuses the syncrep_yyparse/syncrep_scanner infrastructure that is
+ * also used for synchronous_standby_names, and accepts these forms:
+ *
+ * slot1, slot2 -- wait for ALL listed slots
+ * ANY N (slot1, slot2, ...) -- wait for any N-of-M (quorum)
+ *
+ * Note: Simple list syntax is interpreted as "wait for ALL" for this GUC,
+ * unlike synchronous_standby_names where it means "FIRST 1".
+ *
+ * After parsing, we validate every name as a legal replication slot name,
+ * omit duplicate entries while preserving first-occurrence order, and then
+ * apply the resulting unique list to the configured semantics.
*/
bool
check_synchronized_standby_slots(char **newval, void **extra, GucSource source)
{
- char *rawname;
- char *ptr;
- List *elemlist;
- int size;
- bool ok;
- SyncStandbySlotsConfigData *config;
-
- if ((*newval)[0] == '\0')
- return true;
+ if (*newval != NULL && (*newval)[0] != '\0')
+ {
+ yyscan_t scanner;
+ int parse_rc;
+ SyncStandbySlotsConfigData *config;
+ const char *mname;
+
+ /* Result of parsing is returned in one of these two variables */
+ SyncRepConfigData *syncrep_parse_result = NULL;
+ char *syncrep_parse_error_msg = NULL;
+
+ /* Parse the synchronized standby slots configuration */
+ syncrep_scanner_init(*newval, &scanner);
+ parse_rc = syncrep_yyparse(&syncrep_parse_result,
+ &syncrep_parse_error_msg,
+ scanner);
+ syncrep_scanner_finish(scanner);
+
+ if (parse_rc != 0 || syncrep_parse_result == NULL)
+ {
+ GUC_check_errcode(ERRCODE_SYNTAX_ERROR);
+ if (syncrep_parse_error_msg)
+ GUC_check_errdetail("%s", syncrep_parse_error_msg);
+ else
+ GUC_check_errdetail("\"%s\" parser failed.",
+ "synchronized_standby_slots");
+ return false;
+ }
- /* Need a modifiable copy of the GUC string */
- rawname = pstrdup(*newval);
+ if (syncrep_parse_result->syncrep_method == SYNC_REP_PRIORITY)
+ {
+ GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
+ GUC_check_errmsg("priority syntax is not supported for parameter \"%s\"",
+ "synchronized_standby_slots");
+ return false;
+ }
- /* Now verify if the specified slots exist and have correct type */
- ok = validate_sync_standby_slots(rawname, &elemlist);
+ if (syncrep_parse_result->num_sync <= 0)
+ {
+ GUC_check_errmsg("number of synchronized standby slots (%d) must be greater than zero",
+ syncrep_parse_result->num_sync);
+ return false;
+ }
- if (!ok || elemlist == NIL)
- {
- pfree(rawname);
- list_free(elemlist);
- return ok;
- }
+ /* validate every member name as a slot name */
+ mname = syncrep_parse_result->member_names;
- /* Compute the size required for the SyncStandbySlotsConfigData struct */
- size = offsetof(SyncStandbySlotsConfigData, slot_names);
- foreach_ptr(char, slot_name, elemlist)
- size += strlen(slot_name) + 1;
+ for (int i = 0; i < syncrep_parse_result->nmembers; i++)
+ {
+ int err_code;
+ char *err_msg = NULL;
+ char *err_hint = NULL;
- /* GUC extra value must be guc_malloc'd, not palloc'd */
- config = (SyncStandbySlotsConfigData *) guc_malloc(LOG, size);
- if (!config)
- return false;
+ if (!ReplicationSlotValidateNameInternal(mname, false, &err_code,
+ &err_msg, &err_hint))
+ {
+ GUC_check_errcode(err_code);
+ GUC_check_errdetail("%s", err_msg);
+ if (err_hint != NULL)
+ GUC_check_errhint("%s", err_hint);
+ return false;
+ }
- /* Transform the data into SyncStandbySlotsConfigData */
- config->nslotnames = list_length(elemlist);
+ mname += strlen(mname) + 1;
+ }
- ptr = config->slot_names;
- foreach_ptr(char, slot_name, elemlist)
- {
- strcpy(ptr, slot_name);
- ptr += strlen(slot_name) + 1;
- }
+ /* Omit duplicate slot names to ensure each slot is considered only once. */
+ CompactSyncRepConfigMemberNames(syncrep_parse_result);
- *extra = config;
+ /*
+ * For synchronized_standby_slots, a comma-separated list means all
+ * listed slots are required. The syncrep parser preserves this shape
+ * as SYNC_REP_DEFAULT, so map num_sync to nmembers to enforce
+ * all-mode semantics after removing duplicate names.
+ */
+ if (syncrep_parse_result->syncrep_method == SYNC_REP_DEFAULT)
+ syncrep_parse_result->num_sync = syncrep_parse_result->nmembers;
+
+ /* Reject num_sync > nmembers after duplicates have been omitted. */
+ if (syncrep_parse_result->num_sync > syncrep_parse_result->nmembers)
+ {
+ GUC_check_errmsg("number of synchronized standby slots (%d) must not exceed the number of unique listed slots (%d)",
+ syncrep_parse_result->num_sync,
+ syncrep_parse_result->nmembers);
+ return false;
+ }
+
+ /*
+ * Build SyncStandbySlotsConfigData from the parsed SyncRepConfigData.
+ * Since the structures have identical layout, we can use the same
+ * config_size.
+ */
+ config = (SyncStandbySlotsConfigData *)
+ guc_malloc(LOG, syncrep_parse_result->config_size);
+ if (!config)
+ return false;
+
+ config->config_size = syncrep_parse_result->config_size;
+ config->num_sync = syncrep_parse_result->num_sync;
+ config->syncrep_method = syncrep_parse_result->syncrep_method;
+ config->nslotnames = syncrep_parse_result->nmembers;
+
+ /* Copy all slot names in one operation */
+ memcpy(config->slot_names,
+ syncrep_parse_result->member_names,
+ syncrep_parse_result->config_size -
+ offsetof(SyncRepConfigData, member_names));
+
+ *extra = config;
+ }
+ else
+ *extra = NULL;
- pfree(rawname);
- list_free(elemlist);
return true;
}
@@ -3099,18 +3240,117 @@ SlotExistsInSyncStandbySlots(const char *slot_name)
}
/*
- * Return true if the slots specified in synchronized_standby_slots have caught up to
- * the given WAL location, false otherwise.
+ * Report problem states for synchronized standby slots that prevented the
+ * catch-up requirement from being met.
+ */
+static void
+ReportUnavailableSyncStandbySlots(SyncStandbySlotsStateInfo * slot_states,
+ int num_slot_states, int elevel,
+ XLogRecPtr wait_for_lsn)
+{
+ for (int i = 0; i < num_slot_states; i++)
+ {
+ const char *slot_name = slot_states[i].slot_name;
+
+ switch (slot_states[i].state)
+ {
+ case SS_SLOT_NOT_FOUND:
+ ereport(elevel,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" does not exist",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
+ slot_name),
+ errhint("Create the replication slot \"%s\" or amend parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_LOGICAL:
+ ereport(elevel,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot specify logical replication slot \"%s\" in parameter \"%s\"",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting for correction on replication slot \"%s\".",
+ slot_name),
+ errhint("Remove the logical replication slot \"%s\" from parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_INVALIDATED:
+ ereport(elevel,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("physical replication slot \"%s\" specified in parameter \"%s\" has been invalidated",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
+ slot_name),
+ errhint("Drop and recreate the replication slot \"%s\", or amend parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_INACTIVE_LAGGING:
+ ereport(elevel,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
+ slot_name),
+ errhint("Start the standby associated with the replication slot \"%s\", or amend parameter \"%s\".",
+ slot_name, "synchronized_standby_slots"));
+ break;
+
+ case SS_SLOT_ACTIVE_LAGGING:
+ if (!XLogRecPtrIsValid(slot_states[i].restart_lsn))
+ ereport(DEBUG1,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("The slot's restart_lsn is not yet set; required LSN is %X/%X.",
+ LSN_FORMAT_ARGS(wait_for_lsn)));
+ else
+ ereport(DEBUG1,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("replication slot \"%s\" specified in parameter \"%s\" has not caught up",
+ slot_name, "synchronized_standby_slots"),
+ errdetail("The slot's restart_lsn %X/%X is behind the required %X/%X.",
+ LSN_FORMAT_ARGS(slot_states[i].restart_lsn),
+ LSN_FORMAT_ARGS(wait_for_lsn)));
+ break;
+
+ default:
+ /* Should not happen */
+ Assert(false);
+ break;
+ }
+ }
+}
+
+/*
+ * Return true if the required standby slots have caught up to the given WAL
+ * location, false otherwise.
+ *
+ * The behavior depends on the synchronized_standby_slots configuration:
+ *
+ * Simple list (e.g., "slot1, slot2"):
+ * ALL slots must have caught up. Returns false otherwise.
*
- * The elevel parameter specifies the error level used for logging messages
- * related to slots that do not exist, are invalidated, or are inactive.
+ * ANY N (e.g., "ANY 2 (slot1, slot2, slot3)"):
+ * Wait for any N eligible slots. Skips missing, invalid, logical, and
+ * lagging slots (inactive or active) to find N slots that have caught up.
+ *
+ * The elevel parameter specifies the error level used for reporting issues
+ * related to the slots specified in synchronized_standby_slots when the
+ * catch-up requirement is not met.
*/
bool
StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
{
const char *name;
int caught_up_slot_num = 0;
+ int required;
XLogRecPtr min_restart_lsn = InvalidXLogRecPtr;
+ bool wait_for_all;
+ SyncStandbySlotsStateInfo *slot_states;
+ int num_slot_states = 0;
/*
* Don't need to wait for the standbys to catch up if there is no value in
@@ -3134,12 +3374,44 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
ss_oldest_flush_lsn >= wait_for_lsn)
return true;
+ /*
+ * Determine how many slots are required and whether we're in "wait for
+ * ALL" mode versus "wait for N-of-M" mode.
+ *
+ * wait_for_all = true means we need ALL slots to be ready (simple list
+ * syntax like "slot1, slot2"). In this mode, we stop checking on the
+ * first slot that is missing/invalid/logical, or the first slot that is
+ * lagging (inactive or active).
+ *
+ * wait_for_all = false means we select N from M candidates (ANY N syntax).
+ * In this mode, slots already caught up are counted even if inactive, and
+ * lagging slots are skipped until enough slots have caught up.
+ * Duplicate configured slot names do not appear here because the check hook
+ * compacts them out of the parsed configuration.
+ */
+ required = synchronized_standby_slots_config->num_sync;
+ wait_for_all = (required == synchronized_standby_slots_config->nslotnames);
+
+ /*
+ * Allocate array to track slot states. Size it to the total number of
+ * configured slots since in the worst case all could have problem states.
+ */
+ slot_states = palloc_array(SyncStandbySlotsStateInfo,
+ synchronized_standby_slots_config->nslotnames);
+
/*
* To prevent concurrent slot dropping and creation while filtering the
* slots, take the ReplicationSlotControlLock outside of the loop.
*/
LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+ /*
+ * Iterate through configured slots, checking their state and tracking how
+ * many have caught up. Problem states are recorded for deferred
+ * reporting: missing/logical/invalidated slots, and lagging slots
+ * (inactive or active). Messages are only emitted if the catch-up
+ * requirement isn't met.
+ */
name = synchronized_standby_slots_config->slot_names;
for (int i = 0; i < synchronized_standby_slots_config->nslotnames; i++)
{
@@ -3150,35 +3422,28 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
slot = SearchNamedReplicationSlot(name, false);
- /*
- * If a slot name provided in synchronized_standby_slots does not
- * exist, report a message and exit the loop.
- */
if (!slot)
{
- ereport(elevel,
- errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("replication slot \"%s\" specified in parameter \"%s\" does not exist",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
- name),
- errhint("Create the replication slot \"%s\" or amend parameter \"%s\".",
- name, "synchronized_standby_slots"));
- break;
+ /* Record Slot State */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state = SS_SLOT_NOT_FOUND;
+ num_slot_states++;
+
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
- /* Same as above: if a slot is not physical, exit the loop. */
if (SlotIsLogical(slot))
{
- ereport(elevel,
- errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("cannot specify logical replication slot \"%s\" in parameter \"%s\"",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting for correction on replication slot \"%s\".",
- name),
- errhint("Remove the logical replication slot \"%s\" from parameter \"%s\".",
- name, "synchronized_standby_slots"));
- break;
+ /* Record Slot State */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state = SS_SLOT_LOGICAL;
+ num_slot_states++;
+
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
SpinLockAcquire(&slot->mutex);
@@ -3189,33 +3454,34 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
if (invalidated)
{
- /* Specified physical slot has been invalidated */
- ereport(elevel,
- errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("physical replication slot \"%s\" specified in parameter \"%s\" has been invalidated",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
- name),
- errhint("Drop and recreate the replication slot \"%s\", or amend parameter \"%s\".",
- name, "synchronized_standby_slots"));
- break;
+ /* Record Slot State */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state = SS_SLOT_INVALIDATED;
+ num_slot_states++;
+
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
if (!XLogRecPtrIsValid(restart_lsn) || restart_lsn < wait_for_lsn)
{
- /* Log a message if no active_pid for this physical slot */
- if (inactive)
- ereport(elevel,
- errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
- errmsg("replication slot \"%s\" specified in parameter \"%s\" does not have active_pid",
- name, "synchronized_standby_slots"),
- errdetail("Logical replication is waiting on the standby associated with replication slot \"%s\".",
- name),
- errhint("Start the standby associated with the replication slot \"%s\", or amend parameter \"%s\".",
- name, "synchronized_standby_slots"));
+ /*
+ * If a slot is inactive and lagging, report it as inactive. If it
+ * is active and lagging, report it as lagging.
+ *
+ * In ALL mode: must wait for it. In ANY N (quorum) mode: skip and
+ * use another slot.
+ */
+ slot_states[num_slot_states].slot_name = name;
+ slot_states[num_slot_states].state =
+ inactive ? SS_SLOT_INACTIVE_LAGGING : SS_SLOT_ACTIVE_LAGGING;
+ slot_states[num_slot_states].restart_lsn = restart_lsn;
+ num_slot_states++;
- /* Continue if the current slot hasn't caught up. */
- break;
+ if (wait_for_all)
+ break;
+ goto next_slot;
}
Assert(restart_lsn >= wait_for_lsn);
@@ -3226,17 +3492,30 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
caught_up_slot_num++;
+ /* Stop processing if the required number of slots have caught up. */
+ if (caught_up_slot_num >= required)
+ break;
+
+next_slot:
name += strlen(name) + 1;
}
LWLockRelease(ReplicationSlotControlLock);
/*
- * Return false if not all the standbys have caught up to the specified
- * WAL location.
+ * If the required number of slots have not caught up, report any recorded
+ * problem states and return false.
+ *
+ * We only emit messages when the requirement is not met to avoid
+ * misleading messages in quorum mode where other slots may have satisfied
+ * the condition despite some slots having issues.
*/
- if (caught_up_slot_num != synchronized_standby_slots_config->nslotnames)
+ if (caught_up_slot_num < required)
+ {
+ ReportUnavailableSyncStandbySlots(slot_states, num_slot_states, elevel, wait_for_lsn);
+ pfree(slot_states);
return false;
+ }
/* The ss_oldest_flush_lsn must not retreat. */
Assert(!XLogRecPtrIsValid(ss_oldest_flush_lsn) ||
@@ -3244,6 +3523,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel)
ss_oldest_flush_lsn = min_restart_lsn;
+ pfree(slot_states);
return true;
}
@@ -3276,7 +3556,10 @@ WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn)
ProcessConfigFile(PGC_SIGHUP);
}
- /* Exit if done waiting for every slot. */
+ /*
+ * Exit once the configured synchronized_standby_slots requirement is
+ * met.
+ */
if (StandbySlotsHaveCaughtup(wait_for_lsn, WARNING))
break;
diff --git a/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
new file mode 100644
index 00000000000..d387e0c7e7e
--- /dev/null
+++ b/src/test/recovery/t/053_synchronized_standby_slots_quorum.pl
@@ -0,0 +1,367 @@
+
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test synchronized_standby_slots with different syntax modes:
+# - Plain list (ALL mode): slot1, slot2
+# - ANY N (quorum mode): ANY N (slot1, slot2, ...)
+#
+# Setup: a 3-node cluster with one primary, two physical standbys, and a
+# logical decoding client using a failover-enabled slot.
+#
+# | ----> standby1 (primary_slot_name = sb1_slot)
+# primary ------|
+# | ----> standby2 (primary_slot_name = sb2_slot)
+#
+# synchronous_standby_names = 'ANY 1 (standby1, standby2)'
+#
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# ---------------------------------------------------------------------------
+# 1. Create a primary with logical replication level, autovacuum off
+# ---------------------------------------------------------------------------
+my $primary = PostgreSQL::Test::Cluster->new('primary');
+$primary->init(allows_streaming => 'logical');
+$primary->append_conf(
+ 'postgresql.conf', qq{
+autovacuum = off
+});
+$primary->start;
+
+# Physical replication slots for the two standbys
+$primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('sb1_slot');");
+$primary->safe_psql('postgres',
+ "SELECT pg_create_physical_replication_slot('sb2_slot');");
+
+# ---------------------------------------------------------------------------
+# 2. Create standby1 and standby2 from a fresh backup
+# ---------------------------------------------------------------------------
+my $backup_name = 'base_backup';
+$primary->backup($backup_name);
+
+my $connstr = $primary->connstr;
+
+my $standby1 = PostgreSQL::Test::Cluster->new('standby1');
+$standby1->init_from_backup(
+ $primary, $backup_name,
+ has_streaming => 1,
+ has_restoring => 1);
+$standby1->append_conf(
+ 'postgresql.conf', qq(
+hot_standby_feedback = on
+primary_slot_name = 'sb1_slot'
+primary_conninfo = '$connstr dbname=postgres'
+));
+
+my $standby2 = PostgreSQL::Test::Cluster->new('standby2');
+$standby2->init_from_backup(
+ $primary, $backup_name,
+ has_streaming => 1,
+ has_restoring => 1);
+$standby2->append_conf(
+ 'postgresql.conf', qq(
+hot_standby_feedback = on
+primary_slot_name = 'sb2_slot'
+primary_conninfo = '$connstr dbname=postgres'
+));
+
+$standby1->start;
+$standby2->start;
+
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+# ---------------------------------------------------------------------------
+# 3. Create a logical failover slot on the primary
+# ---------------------------------------------------------------------------
+$primary->safe_psql('postgres',
+ "SELECT pg_create_logical_replication_slot('logical_failover', 'test_decoding', false, false, true);"
+);
+
+# ---------------------------------------------------------------------------
+# 4. Configure quorum sync rep with ALL-mode synchronized_standby_slots
+# ---------------------------------------------------------------------------
+$primary->append_conf(
+ 'postgresql.conf', qq{
+synchronous_standby_names = 'ANY 1 (standby1, standby2)'
+synchronized_standby_slots = 'sb1_slot, sb2_slot'
+});
+$primary->reload;
+
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+# ---------------------------------------------------------------------------
+# 5. Confirm that quorum sync rep is active for both standbys
+# ---------------------------------------------------------------------------
+is( $primary->safe_psql(
+ 'postgres',
+ q{SELECT count(*) FROM pg_stat_replication WHERE sync_state = 'quorum';}
+ ),
+ '2',
+ 'both standbys are in quorum sync state');
+
+##################################################
+# PART A: Plain list (ALL mode) blocks when any slot is unavailable
+##################################################
+
+$standby1->stop;
+
+# Commit succeeds since standby2 satisfies the quorum.
+my $emit_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'all_mode_blocks');"
+);
+like($emit_lsn, qr/^[0-9A-F]+\/[0-9A-F]+$/,
+ 'synchronous commit succeeds with quorum (standby2 alive)');
+
+$primary->wait_for_replay_catchup($standby2);
+
+my $log_offset = -s $primary->logfile;
+
+my $bg = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+# Wait for the primary to log a warning about sb1_slot not being active.
+$primary->wait_for_log(
+ qr/replication slot \"sb1_slot\" specified in parameter "synchronized_standby_slots" does not have active_pid/,
+ $log_offset);
+
+pass('plain list (ALL mode): logical decoding blocked by unavailable sb1_slot');
+
+# Unblock by clearing synchronized_standby_slots.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg->quit;
+
+# Consume the change so the slot is clean for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+##################################################
+# PART B: ANY mode (quorum) — logical decoding proceeds with N-of-M slots
+##################################################
+
+# Switch synchronized_standby_slots to quorum mode: need only 1 of 2 slots.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'ANY 1 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+# standby1 is still down; standby2 is up.
+
+# Emit another transactional message — commits via quorum.
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'quorum_mode_works');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+# In quorum mode, logical decoding should NOT block because sb2_slot has
+# caught up and 1-of-2 is sufficient.
+my $decoded = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%quorum_mode_works%';});
+is($decoded, '1',
+ 'ANY mode: logical decoding proceeds with only sb2_slot caught up');
+
+##################################################
+# PART C: Re-check plain list (ALL mode) works when both standbys are up
+##################################################
+
+# Bring standby1 back.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+# Switch to plain list (ALL mode) with both slots.
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'sb1_slot, sb2_slot'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'both_caught_up');"
+);
+$primary->wait_for_replay_catchup($standby1);
+$primary->wait_for_replay_catchup($standby2);
+
+my $decoded_bc = $primary->safe_psql('postgres',
+ q{SELECT count(*) FROM pg_logical_slot_get_changes('logical_failover', NULL, NULL)
+ WHERE data LIKE '%both_caught_up%';});
+is($decoded_bc, '1',
+ 'plain list: works when all standbys are up');
+
+##################################################
+# PART D: ANY 2 waits on an active lagging slot
+##################################################
+
+# Stop standby1 so sb1_slot can be controlled by a raw replication connection
+# that keeps the slot active while lagging.
+$standby1->stop;
+
+my $old_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_current_wal_lsn();");
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'ANY 2 (sb1_slot, sb2_slot)'");
+$primary->reload;
+
+my $any2_lag_lsn = $primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'any_2_lagging_blocks');"
+);
+$primary->wait_for_replay_catchup($standby2);
+
+my $repl_any2 = $primary->background_psql(
+ 'postgres',
+ replication => 'database',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$repl_any2->query_until(
+ qr/^$/,
+ "START_REPLICATION SLOT sb1_slot PHYSICAL $old_lsn;\n");
+
+$primary->poll_query_until('postgres', q{
+ SELECT active_pid IS NOT NULL
+ FROM pg_replication_slots
+ WHERE slot_name = 'sb1_slot'
+}) or die "replication connection did not activate sb1_slot";
+
+my $bg_any2 = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_any2->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'ANY 2: decoding waits when only one slot has caught up');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_any2->quit;
+$repl_any2->quit;
+
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# Bring standby1 back up for the remaining tests.
+$standby1->start;
+$primary->wait_for_replay_catchup($standby1);
+
+
+##################################################
+# PART E: Duplicate entries are ignored for quorum counting
+##################################################
+
+# Stop standby2 so only sb1_slot can catch up.
+$standby2->stop;
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots',
+ "'ANY 2 (sb1_slot, sb1_slot, sb2_slot)'");
+$primary->reload;
+
+$primary->safe_psql('postgres',
+ "SELECT pg_logical_emit_message(true, 'qtest', 'duplicate_entries_ignored');"
+);
+$primary->wait_for_replay_catchup($standby1);
+
+my $bg_dup = $primary->background_psql(
+ 'postgres',
+ on_error_stop => 0,
+ timeout => $PostgreSQL::Test::Utils::timeout_default);
+
+$bg_dup->query_until(
+ qr/decode_start/, q(
+ \echo decode_start
+ SELECT pg_logical_slot_peek_changes('logical_failover', NULL, NULL);
+));
+
+ok( $primary->poll_query_until(
+ 'postgres', q{
+SELECT EXISTS (
+ SELECT 1
+ FROM pg_stat_activity
+ WHERE wait_event = 'WaitForStandbyConfirmation'
+ AND query LIKE '%pg_logical_slot_peek_changes(''logical_failover''%'
+);
+}),
+ 'duplicate entries are ignored when counting quorum slots');
+
+$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''");
+$primary->reload;
+$bg_dup->quit;
+
+# Consume the change for the next test.
+$primary->safe_psql('postgres',
+ q{SELECT pg_logical_slot_get_changes('logical_failover', NULL, NULL);});
+
+# Bring standby2 back up for validation tests.
+$standby2->start;
+$primary->wait_for_replay_catchup($standby2);
+
+
+##################################################
+# PART F: Verify GUC validation rejects bad values
+##################################################
+
+my ($result, $stdout, $stderr);
+
+# N exceeds number of listed slots
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'ANY 3 (sb1_slot, sb2_slot)';");
+like($stderr, qr/ERROR/,
+ 'GUC rejects ANY N when N > number of listed slots');
+
+# Missing closing parenthesis
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'ANY 1 (sb1_slot, sb2_slot';");
+like($stderr, qr/ERROR/,
+ 'GUC rejects malformed ANY syntax');
+
+# Priority syntax is not supported by synchronized_standby_slots yet
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'FIRST 1 (sb1_slot, sb2_slot)';");
+like($stderr, qr/priority syntax is not supported/,
+ 'GUC rejects FIRST syntax');
+
+# Legacy priority syntax is not supported by synchronized_standby_slots yet
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = '1 (sb1_slot, sb2_slot)';");
+like($stderr, qr/priority syntax is not supported/,
+ 'GUC rejects legacy priority syntax');
+
+# Invalid slot name
+($result, $stdout, $stderr) = $primary->psql('postgres',
+ "ALTER SYSTEM SET synchronized_standby_slots = 'ANY 1 (INVALID_UPPER)';");
+like($stderr, qr/ERROR/,
+ 'GUC rejects invalid slot name in ANY syntax');
+
+# ---------------------------------------------------------------------------
+# Cleanup
+# ---------------------------------------------------------------------------
+$primary->safe_psql('postgres',
+ "SELECT pg_drop_replication_slot('logical_failover');");
+
+done_testing();
--
2.43.0
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
@ 2026-02-26 09:29 ` Alexander Kukushkin <[email protected]>
2026-02-26 10:38 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
1 sibling, 1 reply; 25+ messages in thread
From: Alexander Kukushkin @ 2026-02-26 09:29 UTC (permalink / raw)
To: shveta malik <[email protected]>; +Cc: SATYANARAYANA NARLAPURAM <[email protected]>; Ashutosh Sharma <[email protected]>; Amit Kapila <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi,
On Thu, 26 Feb 2026 at 09:45, shveta malik <[email protected]> wrote:
>
> As suggested in [1], IMO, it is a reasonably good idea for
> 'synchronized_standby_slots' to DEFAULT to the value of
> 'synchronous_standby_names'. That way, even if the user missed to
> configure 'synchronized_standby_slots' explicitly, we would still have
> reasonable protection in place.
Hmm.
synchronous_standby_names contains application_names,
while synchronized_standby_slots contains names of physical replication
slots.
These are two different things, and in fact sync replication doesn't even
require to use replication slots.
What is worse, even when all standbys use physical replication slots there
is no guarantee that values in synchronous_standby_names will match
physical slot names.
Regards,
--
Alexander Kukushkin
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
2026-02-26 07:42 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication shveta malik <[email protected]>
2026-02-26 09:29 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Alexander Kukushkin <[email protected]>
@ 2026-02-26 10:38 ` SATYANARAYANA NARLAPURAM <[email protected]>
0 siblings, 0 replies; 25+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-02-26 10:38 UTC (permalink / raw)
To: Alexander Kukushkin <[email protected]>; +Cc: shveta malik <[email protected]>; Ashutosh Sharma <[email protected]>; Amit Kapila <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi Alexnader,
On Thu, Feb 26, 2026 at 1:29 AM Alexander Kukushkin <[email protected]>
wrote:
> Hi,
>
> On Thu, 26 Feb 2026 at 09:45, shveta malik <[email protected]> wrote:
>
>>
>> As suggested in [1], IMO, it is a reasonably good idea for
>> 'synchronized_standby_slots' to DEFAULT to the value of
>> 'synchronous_standby_names'. That way, even if the user missed to
>> configure 'synchronized_standby_slots' explicitly, we would still have
>> reasonable protection in place.
>
>
> Hmm.
> synchronous_standby_names contains application_names,
> while synchronized_standby_slots contains names of physical replication
> slots.
> These are two different things, and in fact sync replication doesn't even
> require to use replication slots.
> What is worse, even when all standbys use physical replication slots there
> is no guarantee that values in synchronous_standby_names will match
> physical slot names
>
That's right, thanks for reminding me. I am convinced that we can't use the
defaults of synchronous_standby_names for synchronized_standby_slots. What
do you think about the rest of the proposal?
Thanks,
Satya
^ permalink raw reply [nested|flat] 25+ messages in thread
* Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Amit Kapila <[email protected]>
@ 2026-02-26 08:02 ` SATYANARAYANA NARLAPURAM <[email protected]>
1 sibling, 0 replies; 25+ messages in thread
From: SATYANARAYANA NARLAPURAM @ 2026-02-26 08:02 UTC (permalink / raw)
To: Amit Kapila <[email protected]>; +Cc: Ashutosh Sharma <[email protected]>; pgsql-hackers; PostgreSQL Hackers <[email protected]>
Hi Amit,
On Wed, Feb 25, 2026 at 10:20 PM Amit Kapila <[email protected]>
wrote:
> ...
> >
> > Thinking about this further, using quorum settings for
> > synchronized_standby_slots can/will certainly result in at least one
> > sync standby lagging behind the logical replica, making it probably
> > impossible to continue with the existing logical replication setup
> > after a failover to the standby that lags behind. Here is what I am
> > mean:
> >
>
> But won't that be true even for synchronous_standby_names? I think in
> the case of quorum, it is the responsibility of the failover solution
> to select the most recent synced standby among all the standby's
> specified in synchronous_standby_names. Similarly here before failing
> over logical subscriber to one of physical standby, the failover tool
> needs to ensure it is switching over to the synced replica. We have
> given steps in the docs [1] that could be used to identify the replica
> where the subscriber can switchover. Will that address your concern?
>
+1, the job of failover orchestration is to ensure the new primary is
caught up at least until the quorum LSN. Otherwise, it can be a durability
issue where users see missing committed transactions.
> BTW, I have also suggested this idea in thread [2]. I don't recall all
> the ideas/points discussed in that thread but it would be good to
> check that thread for any alternative ideas and points raised, so that
> we don't miss anything.
>
Thanks for sharing the links, the approach is similar. DEFAULT to
SAME_AS_SYNCREP_STANDBYS is an interesting option.
I like the idea of avoiding duplicate lists unless the user wants to
maintain a separate list.
Thanks,
Satya
^ permalink raw reply [nested|flat] 25+ messages in thread
end of thread, other threads:[~2026-06-12 13:03 UTC | newest]
Thread overview: 25+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-02-26 04:58 Re: synchronized_standby_slots behavior inconsistent with quorum-based synchronous replication Ashutosh Sharma <[email protected]>
2026-02-26 06:19 ` Amit Kapila <[email protected]>
2026-02-26 07:42 ` Ashutosh Sharma <[email protected]>
2026-02-26 08:23 ` SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:45 ` shveta malik <[email protected]>
2026-02-26 09:11 ` Ashutosh Sharma <[email protected]>
2026-02-26 10:46 ` SATYANARAYANA NARLAPURAM <[email protected]>
2026-05-21 09:12 ` Ashutosh Sharma <[email protected]>
2026-06-03 11:00 ` Ashutosh Sharma <[email protected]>
2026-06-04 03:43 ` shveta malik <[email protected]>
2026-06-04 07:36 ` Ashutosh Sharma <[email protected]>
2026-06-04 08:24 ` Zhijie Hou (Fujitsu) <[email protected]>
2026-06-04 09:26 ` Ashutosh Sharma <[email protected]>
2026-06-05 03:04 ` Zhijie Hou (Fujitsu) <[email protected]>
2026-06-05 03:36 ` shveta malik <[email protected]>
2026-06-08 09:51 ` Amit Kapila <[email protected]>
2026-06-10 06:45 ` shveta malik <[email protected]>
2026-06-12 10:10 ` Ashutosh Sharma <[email protected]>
2026-06-12 13:03 ` Shlok Kyal <[email protected]>
2026-06-05 10:02 ` shveta malik <[email protected]>
2026-06-08 06:08 ` Ashutosh Sharma <[email protected]>
2026-06-08 08:53 ` Ashutosh Sharma <[email protected]>
2026-02-26 09:29 ` Alexander Kukushkin <[email protected]>
2026-02-26 10:38 ` SATYANARAYANA NARLAPURAM <[email protected]>
2026-02-26 08:02 ` SATYANARAYANA NARLAPURAM <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox