Re: Introduce XID age based replication slot invalidation

From: Nisha Moond <nisha(dot)moond412(at)gmail(dot)com>
To: surya poondla <suryapoondla4(at)gmail(dot)com>
Cc: Bharath Rupireddy <bharath(dot)rupireddyforpostgres(at)gmail(dot)com>, pgsql-hackers <pgsql-hackers(at)postgresql(dot)org>, Bertrand Drouvot <bertranddrouvot(dot)pg(at)gmail(dot)com>, "Zhijie Hou (Fujitsu)" <houzj(dot)fnst(at)fujitsu(dot)com>, Amit Kapila <amit(dot)kapila16(at)gmail(dot)com>, "sawada(dot)mshk(at)gmail(dot)com" <sawada(dot)mshk(at)gmail(dot)com>, Srinath Reddy Sadipiralla <srinath2133(at)gmail(dot)com>, SATYANARAYANA NARLAPURAM <satyanarlapuram(at)gmail(dot)com>, "Hayato Kuroda (Fujitsu)" <kuroda(dot)hayato(at)fujitsu(dot)com>, "johnhyvr(at)gmail(dot)com" <johnhyvr(at)gmail(dot)com>
Subject: Re: Introduce XID age based replication slot invalidation
Date: 2026-09-04 08:41:40
Message-ID: CABdArM7M5=aa8DCxeGNR8-BuR8u8V7sFEdBmBgRbaZvvfcdN-g@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On Wed, Sep 2, 2026 at 10:32 AM surya poondla <suryapoondla4(at)gmail(dot)com> wrote:
>
> Hi Bharath,
>
>> Please find the attached v14 patches. 0003 now adds support for
>>
>> invalidating XID-aged synced replication slots on standbys.
>
>
> v14 applies cleanly and builds clean on the master. I think 0003 has a problem when sync_replication_slots is on: the invalidation doesn't stick,
> and the slot ends up worse off than before.
>
> With max_slot_xid_age = 100 on the standby only, and a failover logical slot on the primary with no consumer so its catalog_xmin stays at 695,
> consuming 200 XIDs and checkpointing both nodes gives:
>
> LOG: invalidating obsolete replication slot "failover_slot"
> DETAIL: The slot's catalog xmin age of 200 exceeds the configured
> "max_slot_xid_age" of 100 by 100 transactions.
> LOG: dropped replication slot "failover_slot" of database with OID 5
> LOG: could not synchronize replication slot "failover_slot" [repeats]
>
> The slot was temporary=false synced=true catalog_xmin=695 beforehand, and ends up temporary=true synced=true invalidation_reason=NULL
> catalog_xmin=895.
>
> The restartpoint invalidates it, then drop_local_obsolete_slots() drops it, since local_sync_slot_required() returns false for a slot "invalidated
> while the corresponding remote slot is still valid". synchronize_one_slot() re-creates it as RS_TEMPORARY with a fresh catalog_xmin from GetOldestSafeDecodingTransactionId() (895), and
> update_local_synced_slot() then won't sync it, because the remote's catalog_xmin, 695, precedes it. That recovery path works for the existing
> invalidation causes because the remote is normally being consumed and soon advances past the standby, but max_slot_xid_age targets slots whose horizon
> is not advancing.
>
> The primary's horizon does get released, the catalog_xmin on the physical slot the standby streams from advanced from 695 to 895. The
> cost is the synced slot: it doesn't become sync-ready again, and ReplicationSlotCleanup() drops the worker's temporary slots when it exits,
> so it is gone at promotion. invalidation_reason = 'xid_aged' doesn't persist either, so there's nothing left to alert on.
>
> ReplicationSlotRelease() clears active_proc only for RS_PERSISTENT slots, so the recreated slot stays active (active_pid was
> the slotsync worker itself), its frozen catalog_xmin ages out again, and the next restartpoint takes the active branch:
>
> LOG: terminating process 45192 to release replication slot "failover_slot"
> LOG: slot sync worker started
>
> After which the slot is recreated at catalog_xmin=1095. So roughly every max_slot_xid_age transactions, the checkpointer SIGTERMs the slot sync worker. None of this shows up in Testcase 6, which sets
> sync_replication_slots = off and drives the sync by hand, the only configuration in which nothing resurrects the slot.
>
> Would it make sense to keep the invalidation sticky for this cause, i.e. have local_sync_slot_required() not drop a synced slot invalidated with
> RS_INVAL_XID_AGE? The operator would see it, the horizon stays released, and neither the slot nor the worker gets churned. The alternative is Amit's
> earlier suggestion of relying on the primary invalidating its own slot, but that doesn't cover your cases 1 and 2.
>

Thanks for highlighting the case. A few concerns with making the
invalidation sticky:
- Users are not allowed to drop synced slots on a standby, so we
would end up keeping these slots there indefinitely. Recovering from
this would then require taking action on the primary, either by
dropping the slot there or by toggling failover off and on.
- IIUC, the main reason for allowing these slots to be invalidated in
the first place is to handle two cases as discussed at [1]:
1) the sync worker being stuck or failing, and
2) sync being disabled with rare or forgotten manual syncs.
If the invalidation is sticky, the recovery path for these slots is
also blocked. Once the worker recovers or is rerun,
synchronize_one_slot() will just keep skipping the slot at the "Skip
the sync of an invalidated slot" check (SS_SKIP_INVALID), since
nothing else clears a locally invalidated synced slot, so it can never
be recreated with fresh state.

Also, I think the same stuck state can already be reached when
"max_slot_wal_keep_size" is set only on the standby and the remote
slot's restart_lsn is not advancing. (I haven't reproduced this
though.) The comments above drop_local_obsolete_slots() already
anticipate standby-side invalidation from max_slot_wal_keep_size, but
assume the slot gets recreated successfully in the next cycle, which
doesn't hold when the remote isn't advancing.

So IMO max_slot_xid_age is as applicable to invalidating synced slots
as max_slot_wal_keep_size already is, and the sync worker's handling
of the consequences is the same in both cases.

[1] https://www.postgresql.org/message-id/CALj2ACXO6%3D35QSO2CSSCi2qSODCon8VLBZuB3YyXw3bkd1Pk-w%40mail.gmail.com

--
Thanks,
Nisha

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message David Rowley 2026-09-04 08:55:07 Re: SUM(int2)/SUM(int4) do not detect overflow of the int8 accumulator
Previous Message Nazir Bilal Yavuz 2026-09-04 08:40:38 Re: Stabilize recovery conflict stats checks in 031_recovery_conflict.pl