v1-0001-Separate-catalog_xmin-from-xmin-via-ephemeral-phy.patch
application/octet-stream
Filename: v1-0001-Separate-catalog_xmin-from-xmin-via-ephemeral-phy.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v1-0001
Subject: Separate catalog_xmin from xmin via ephemeral physical slot
| File | + | − |
|---|---|---|
| src/backend/commands/repack_worker.c | 1 | 1 |
| src/backend/replication/logical/launcher.c | 1 | 1 |
| src/backend/replication/logical/slotsync.c | 1 | 1 |
| src/backend/replication/slot.c | 17 | 2 |
| src/backend/replication/slotfuncs.c | 2 | 2 |
| src/backend/replication/walsender.c | 25 | 10 |
| src/include/replication/slot.h | 2 | 2 |
From b9b73e7f610ec236d04c3f1c769a47d4582fd928 Mon Sep 17 00:00:00 2001
From: Rui Zhao <zhaorui126@gmail.com>
Date: Wed, 10 Jun 2026 00:59:21 +0800
Subject: [PATCH v1] Separate catalog_xmin from xmin via ephemeral physical
slot
When a standby connects without a replication slot and sends hot standby
feedback, lazily create an ephemeral physical slot so xmin and
catalog_xmin can be tracked separately and atomically (under the slot
mutex), instead of conflating both into the walsender's PGPROC xmin.
Add an error_if_full flag to ReplicationSlotCreate so the walsender can
degrade gracefully to the legacy min(xmin, catalog_xmin) behavior when the
slot pool is exhausted, rather than tearing down the connection.
---
src/backend/commands/repack_worker.c | 2 +-
src/backend/replication/logical/launcher.c | 2 +-
src/backend/replication/logical/slotsync.c | 2 +-
src/backend/replication/slot.c | 19 ++++++++++--
src/backend/replication/slotfuncs.c | 4 +--
src/backend/replication/walsender.c | 35 +++++++++++++++-------
src/include/replication/slot.h | 4 +--
7 files changed, 49 insertions(+), 19 deletions(-)
diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c
index db9ff057cc..95e5be471d 100644
--- a/src/backend/commands/repack_worker.c
+++ b/src/backend/commands/repack_worker.c
@@ -220,7 +220,7 @@ repack_setup_logical_decoding(Oid relid)
*/
snprintf(slotname, NAMEDATALEN, "pg_repack_%d", MyProcPid);
ReplicationSlotCreate(slotname, true, RS_TEMPORARY, false, true,
- false, false);
+ false, false, true);
EnsureLogicalDecodingEnabled();
/*
diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c
index 313e31ff2e..756ad5f12e 100644
--- a/src/backend/replication/logical/launcher.c
+++ b/src/backend/replication/logical/launcher.c
@@ -1576,7 +1576,7 @@ CreateConflictDetectionSlot(void)
errmsg("creating replication conflict detection slot"));
ReplicationSlotCreate(CONFLICT_DETECTION_SLOT, false, RS_PERSISTENT, false,
- false, false, false);
+ false, false, false, true);
init_conflict_slot_xmin();
}
diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c
index 96107c9475..fd12bc9766 100644
--- a/src/backend/replication/logical/slotsync.c
+++ b/src/backend/replication/logical/slotsync.c
@@ -851,7 +851,7 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid,
remote_slot->two_phase,
false,
remote_slot->failover,
- true);
+ true, true);
/* For shorter lines. */
slot = MyReplicationSlot;
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index d7fb9f5a67..ab0d1b3f6e 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -373,11 +373,17 @@ IsSlotForConflictCheck(const char *name)
* failover: If enabled, allows the slot to be synced to standbys so
* that logical replication can be resumed after failover.
* synced: True if the slot is synchronized from the primary server.
+ * error_if_full: If true, raise an error when no slot is free; if false,
+ * return false instead so the caller can degrade gracefully.
+ *
+ * Returns true if a slot was created, false only when error_if_full is false
+ * and the slot pool is exhausted.
*/
-void
+bool
ReplicationSlotCreate(const char *name, bool db_specific,
ReplicationSlotPersistency persistency,
- bool two_phase, bool repack, bool failover, bool synced)
+ bool two_phase, bool repack, bool failover, bool synced,
+ bool error_if_full)
{
ReplicationSlot *slot = NULL;
int startpoint,
@@ -456,11 +462,18 @@ ReplicationSlotCreate(const char *name, bool db_specific,
/* If all slots are in use, we're out of luck. */
if (slot == NULL)
+ {
+ if (!error_if_full)
+ {
+ LWLockRelease(ReplicationSlotAllocationLock);
+ return false;
+ }
ereport(ERROR,
(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
errmsg("all replication slots are in use"),
errhint("Free one or increase \"%s\".",
repack ? "max_repack_replication_slots" : "max_replication_slots")));
+ }
/*
* Since this slot is not in use, nobody should be looking at any part of
@@ -537,6 +550,8 @@ ReplicationSlotCreate(const char *name, bool db_specific,
/* Let everybody know we've modified this slot */
ConditionVariableBroadcast(&slot->active_cv);
+
+ return true;
}
/*
diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c
index 16fbd38373..7ff4d04317 100644
--- a/src/backend/replication/slotfuncs.c
+++ b/src/backend/replication/slotfuncs.c
@@ -53,7 +53,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve,
/* acquire replication slot, this will check for conflicting names */
ReplicationSlotCreate(name, false,
temporary ? RS_TEMPORARY : RS_PERSISTENT, false,
- false, false, false);
+ false, false, false, true);
if (immediately_reserve)
{
@@ -146,7 +146,7 @@ create_logical_replication_slot(char *name, char *plugin,
*/
ReplicationSlotCreate(name, true,
temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase,
- false, failover, false);
+ false, failover, false, true);
/*
* Ensure the logical decoding is enabled before initializing the logical
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 04aa770d98..edc2dd2283 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1248,7 +1248,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
{
ReplicationSlotCreate(cmd->slotname, false,
cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT,
- false, false, false, false);
+ false, false, false, false, true);
if (reserve_wal)
{
@@ -1279,7 +1279,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
*/
ReplicationSlotCreate(cmd->slotname, true,
cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL,
- two_phase, false, failover, false);
+ two_phase, false, failover, false, true);
/*
* Do options check early so that we can bail before calling the
@@ -2786,18 +2786,33 @@ ProcessStandbyHSFeedbackMessage(void)
* obviously safe, and if we're moving it backwards, well, the data is at
* risk already since a VACUUM could already have determined the horizon.)
*
- * If we're using a replication slot we reserve the xmin via that,
- * otherwise via the walsender's PGPROC entry. We can only track the
- * catalog xmin separately when using a slot, so we store the least of the
- * two provided when not using a slot.
- *
- * XXX: It might make sense to generalize the ephemeral slot concept and
- * always use the slot mechanism to handle the feedback xmin.
+ * If we're using a replication slot we reserve the xmin via that. When the
+ * standby connected without one, lazily create an ephemeral physical slot
+ * here, so that we can still track xmin and catalog_xmin separately (and
+ * atomically, under the slot mutex). The ephemeral slot is dropped
+ * automatically when this walsender exits.
*/
- if (MyReplicationSlot != NULL) /* XXX: persistency configurable? */
+ if (MyReplicationSlot == NULL)
+ {
+ char slotname[NAMEDATALEN];
+
+ snprintf(slotname, sizeof(slotname), "pg_walsender_%d", MyProcPid);
+ ReplicationSlotCreate(slotname, false, RS_EPHEMERAL,
+ false, false, false, false, false);
+ }
+
+ if (MyReplicationSlot != NULL)
PhysicalReplicationSlotNewXmin(feedbackXmin, feedbackCatalogXmin);
else
{
+ /*
+ * The slot pool is exhausted, so we cannot track the two horizons
+ * separately. Degrade gracefully to the pre-existing behavior of
+ * holding back both via the walsender's PGPROC entry, using the
+ * older of the two values. This loses the catalog/data separation
+ * for this standby until a slot frees up, but never breaks the
+ * connection.
+ */
if (TransactionIdIsNormal(feedbackCatalogXmin)
&& TransactionIdPrecedes(feedbackCatalogXmin, feedbackXmin))
MyProc->xmin = feedbackCatalogXmin;
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 9b29444cbc..ba90d574af 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -329,10 +329,10 @@ extern PGDLLIMPORT char *synchronized_standby_slots;
extern PGDLLIMPORT int idle_replication_slot_timeout_secs;
/* management of individual slots */
-extern void ReplicationSlotCreate(const char *name, bool db_specific,
+extern bool ReplicationSlotCreate(const char *name, bool db_specific,
ReplicationSlotPersistency persistency,
bool two_phase, bool repack, bool failover,
- bool synced);
+ bool synced, bool error_if_full);
extern void ReplicationSlotPersist(void);
extern void ReplicationSlotDrop(const char *name, bool nowait);
extern void ReplicationSlotDropAcquired(bool try_disable);
--
2.43.7