From e97d8e7bc2556c6d6a779c3a3219bbdd4fb39fe6 Mon Sep 17 00:00:00 2001
From: Rafia Sabih <rafia.sabih@cybertec.at>
Date: Fri, 18 Sep 2026 13:01:37 +0200
Subject: [PATCH v1] Add pg_stat_subscription_stats.worker_launch_failure_count

Track, per subscription, how many times logicalrep_worker_launch()
failed because no free logical replication worker slot was
available. This makes worker-slot exhaustion observable without a
synchronous pre-check, and covers cases where no CREATE/ALTER
SUBSCRIPTION is involved at all -- e.g. max_logical_replication_workers
lowered before a restart, after which the launcher's own retry loop
keeps failing to start already-enabled subscriptions.

Being a cumulative counter, a nonzero value means the subscription
has failed at least once since the last stats reset, not that it is
currently affected; pair with pg_stat_subscription to tell the two
apart.
---
 src/backend/catalog/system_views.sql             |  1 +
 src/backend/replication/logical/launcher.c       |  1 +
 src/backend/utils/activity/pgstat_subscription.c | 14 ++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c              | 10 ++++++++--
 src/include/catalog/catversion.h                 |  2 +-
 src/include/catalog/pg_proc.dat                  |  6 +++---
 src/include/pgstat.h                             |  4 +++-
 src/test/regress/expected/rules.out              |  3 ++-
 8 files changed, 33 insertions(+), 8 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index ad340887f54..bb957087add 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1550,6 +1550,7 @@ CREATE VIEW pg_stat_subscription_stats AS
         ss.apply_error_count,
         ss.sync_seq_error_count,
         ss.sync_table_error_count,
+        ss.worker_launch_failure_count,
         ss.confl_insert_exists,
         ss.confl_update_origin_differs,
         ss.confl_update_exists,
diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c
index 5057a928adb..262d31054f9 100644
--- a/src/backend/replication/logical/launcher.c
+++ b/src/backend/replication/logical/launcher.c
@@ -464,6 +464,7 @@ retry:
 	if (worker == NULL)
 	{
 		LWLockRelease(LogicalRepWorkerLock);
+		pgstat_report_subscription_worker_launch_failure(subid);
 		ereport(WARNING,
 				(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
 				 errmsg("out of logical replication worker slots"),
diff --git a/src/backend/utils/activity/pgstat_subscription.c b/src/backend/utils/activity/pgstat_subscription.c
index 3eaf3e0390f..a6fd4f7edce 100644
--- a/src/backend/utils/activity/pgstat_subscription.c
+++ b/src/backend/utils/activity/pgstat_subscription.c
@@ -56,6 +56,20 @@ pgstat_report_subscription_error(Oid subid)
 	}
 }
 
+/*
+ * Report a worker error
+ */
+void
+pgstat_report_subscription_worker_launch_failure(Oid subid)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStat_BackendSubEntry *sub_entry;
+
+	entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_SUBSCRIPTION, InvalidOid, subid, NULL);
+	sub_entry = (PgStat_BackendSubEntry *) entry_ref->pending;
+	sub_entry->worker_launch_failure_count++;
+}
+
 /*
  * Report a subscription conflict.
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 64b6f60516c..985103e25f4 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2415,7 +2415,7 @@ pg_stat_get_replication_slot(PG_FUNCTION_ARGS)
 Datum
 pg_stat_get_subscription_stats(PG_FUNCTION_ARGS)
 {
-#define PG_STAT_GET_SUBSCRIPTION_STATS_COLS	13
+#define PG_STAT_GET_SUBSCRIPTION_STATS_COLS	14
 	Oid			subid = PG_GETARG_OID(0);
 	TupleDesc	tupdesc;
 	Datum		values[PG_STAT_GET_SUBSCRIPTION_STATS_COLS] = {0};
@@ -2455,6 +2455,8 @@ pg_stat_get_subscription_stats(PG_FUNCTION_ARGS)
 					   INT8OID, -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 13, "stats_reset",
 					   TIMESTAMPTZOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 14, "worker_launch_failure_count",
+					   INT8OID, -1, 0);
 	TupleDescFinalize(tupdesc);
 	BlessTupleDesc(tupdesc);
 
@@ -2486,8 +2488,12 @@ pg_stat_get_subscription_stats(PG_FUNCTION_ARGS)
 		nulls[i] = true;
 	else
 		values[i] = TimestampTzGetDatum(subentry->stat_reset_timestamp);
+	i++;
+
+	/* worker_launch_failure_count */
+	values[i++] = Int64GetDatum(subentry->worker_launch_failure_count);
 
-	Assert(i + 1 == PG_STAT_GET_SUBSCRIPTION_STATS_COLS);
+	Assert(i == PG_STAT_GET_SUBSCRIPTION_STATS_COLS);
 
 	/* Returns the record as Datum */
 	PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)));
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 6f3e526de96..6cb7dc2289b 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202609152
+#define CATALOG_VERSION_NO	202609181
 
 #endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f46427258e3..33512da3dfc 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5742,9 +5742,9 @@
 { oid => '6231', descr => 'statistics: information about subscription stats',
   proname => 'pg_stat_get_subscription_stats', provolatile => 's',
   proparallel => 'r', prorettype => 'record', proargtypes => 'oid',
-  proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,timestamptz}',
-  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{subid,subid,apply_error_count,sync_seq_error_count,sync_table_error_count,confl_insert_exists,confl_update_origin_differs,confl_update_exists,confl_update_deleted,confl_update_missing,confl_delete_origin_differs,confl_delete_missing,confl_multiple_unique_conflicts,stats_reset}',
+  proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,timestamptz,int8}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{subid,subid,apply_error_count,sync_seq_error_count,sync_table_error_count,confl_insert_exists,confl_update_origin_differs,confl_update_exists,confl_update_deleted,confl_update_missing,confl_delete_origin_differs,confl_delete_missing,confl_multiple_unique_conflicts,stats_reset,worker_launch_failure_count}',
   prosrc => 'pg_stat_get_subscription_stats' },
 { oid => '6118', descr => 'statistics: information about subscription',
   proname => 'pg_stat_get_subscription', prorows => '10', proisstrict => 'f',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 187d82c96fe..411829a8a9b 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -116,6 +116,7 @@ typedef struct PgStat_BackendSubEntry
 	PgStat_Counter sync_seq_error_count;
 	PgStat_Counter sync_table_error_count;
 	PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
+	PgStat_Counter worker_launch_failure_count;
 } PgStat_BackendSubEntry;
 
 /* ----------
@@ -502,6 +503,7 @@ typedef struct PgStat_StatSubEntry
 	PgStat_Counter sync_seq_error_count;
 	PgStat_Counter sync_table_error_count;
 	PgStat_Counter conflict_count[CONFLICT_NUM_TYPES];
+	PgStat_Counter worker_launch_failure_count;
 	TimestampTz stat_reset_timestamp;
 } PgStat_StatSubEntry;
 
@@ -925,7 +927,7 @@ extern void pgstat_report_subscription_conflict(Oid subid, ConflictType type);
 extern void pgstat_create_subscription(Oid subid);
 extern void pgstat_drop_subscription(Oid subid);
 extern PgStat_StatSubEntry *pgstat_fetch_stat_subscription(Oid subid);
-
+extern void pgstat_report_subscription_worker_launch_failure(Oid subid);
 
 /*
  * Functions in pgstat_xact.c
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 4a8cc759d7b..03daa8521da 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2316,6 +2316,7 @@ pg_stat_subscription_stats| SELECT ss.subid,
     ss.apply_error_count,
     ss.sync_seq_error_count,
     ss.sync_table_error_count,
+    ss.worker_launch_failure_count,
     ss.confl_insert_exists,
     ss.confl_update_origin_differs,
     ss.confl_update_exists,
@@ -2326,7 +2327,7 @@ pg_stat_subscription_stats| SELECT ss.subid,
     ss.confl_multiple_unique_conflicts,
     ss.stats_reset
    FROM pg_subscription s,
-    LATERAL pg_stat_get_subscription_stats(s.oid) ss(subid, apply_error_count, sync_seq_error_count, sync_table_error_count, confl_insert_exists, confl_update_origin_differs, confl_update_exists, confl_update_deleted, confl_update_missing, confl_delete_origin_differs, confl_delete_missing, confl_multiple_unique_conflicts, stats_reset);
+    LATERAL pg_stat_get_subscription_stats(s.oid) ss(subid, apply_error_count, sync_seq_error_count, sync_table_error_count, confl_insert_exists, confl_update_origin_differs, confl_update_exists, confl_update_deleted, confl_update_missing, confl_delete_origin_differs, confl_delete_missing, confl_multiple_unique_conflicts, stats_reset, worker_launch_failure_count);
 pg_stat_sys_indexes| SELECT relid,
     indexrelid,
     schemaname,
-- 
2.39.5 (Apple Git-154)

