public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v2 2/3] add avx2 support in simd.h
39+ messages / 11 participants
[nested] [flat]

* [PATCH v2 2/3] add avx2 support in simd.h
@ 2024-03-15 17:26  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 39+ messages in thread

From: Nathan Bossart @ 2024-03-15 17:26 UTC (permalink / raw)

---
 src/include/port/simd.h | 58 ++++++++++++++++++++++++++++++++---------
 1 file changed, 45 insertions(+), 13 deletions(-)

diff --git a/src/include/port/simd.h b/src/include/port/simd.h
index 597496f2fb..767127b85c 100644
--- a/src/include/port/simd.h
+++ b/src/include/port/simd.h
@@ -18,7 +18,15 @@
 #ifndef SIMD_H
 #define SIMD_H
 
-#if (defined(__x86_64__) || defined(_M_AMD64))
+#if defined(__AVX2__)
+
+#include <immintrin.h>
+#define USE_AVX2
+typedef __m256i Vector8;
+typedef __m256i Vector32;
+
+#elif (defined(__x86_64__) || defined(_M_AMD64))
+
 /*
  * SSE2 instructions are part of the spec for the 64-bit x86 ISA. We assume
  * that compilers targeting this architecture understand SSE2 intrinsics.
@@ -107,7 +115,9 @@ static inline Vector32 vector32_eq(const Vector32 v1, const Vector32 v2);
 static inline void
 vector8_load(Vector8 *v, const uint8 *s)
 {
-#if defined(USE_SSE2)
+#if defined(USE_AVX2)
+	*v = _mm256_loadu_si256((const __m256i *) s);
+#elif defined(USE_SSE2)
 	*v = _mm_loadu_si128((const __m128i *) s);
 #elif defined(USE_NEON)
 	*v = vld1q_u8(s);
@@ -120,7 +130,9 @@ vector8_load(Vector8 *v, const uint8 *s)
 static inline void
 vector32_load(Vector32 *v, const uint32 *s)
 {
-#ifdef USE_SSE2
+#if defined(USE_AVX2)
+	*v = _mm256_loadu_si256((const __m256i *) s);
+#elif defined(USE_SSE2)
 	*v = _mm_loadu_si128((const __m128i *) s);
 #elif defined(USE_NEON)
 	*v = vld1q_u32(s);
@@ -134,7 +146,9 @@ vector32_load(Vector32 *v, const uint32 *s)
 static inline Vector8
 vector8_broadcast(const uint8 c)
 {
-#if defined(USE_SSE2)
+#if defined(USE_AVX2)
+	return _mm256_set1_epi8(c);
+#elif defined(USE_SSE2)
 	return _mm_set1_epi8(c);
 #elif defined(USE_NEON)
 	return vdupq_n_u8(c);
@@ -147,7 +161,9 @@ vector8_broadcast(const uint8 c)
 static inline Vector32
 vector32_broadcast(const uint32 c)
 {
-#ifdef USE_SSE2
+#if defined(USE_AVX2)
+	return _mm256_set1_epi32(c);
+#elif defined(USE_SSE2)
 	return _mm_set1_epi32(c);
 #elif defined(USE_NEON)
 	return vdupq_n_u32(c);
@@ -270,7 +286,9 @@ vector8_has_le(const Vector8 v, const uint8 c)
 static inline bool
 vector8_is_highbit_set(const Vector8 v)
 {
-#ifdef USE_SSE2
+#if defined(USE_AVX2)
+	return _mm256_movemask_epi8(v) != 0;
+#elif defined(USE_SSE2)
 	return _mm_movemask_epi8(v) != 0;
 #elif defined(USE_NEON)
 	return vmaxvq_u8(v) > 0x7F;
@@ -308,7 +326,9 @@ vector32_is_highbit_set(const Vector32 v)
 static inline uint32
 vector8_highbit_mask(const Vector8 v)
 {
-#ifdef USE_SSE2
+#if defined(USE_AVX2)
+	return (uint32) _mm256_movemask_epi8(v);
+#elif defined(USE_SSE2)
 	return (uint32) _mm_movemask_epi8(v);
 #elif defined(USE_NEON)
 	/*
@@ -337,7 +357,9 @@ vector8_highbit_mask(const Vector8 v)
 static inline Vector8
 vector8_or(const Vector8 v1, const Vector8 v2)
 {
-#ifdef USE_SSE2
+#if defined(USE_AVX2)
+	return _mm256_or_si256(v1, v2);
+#elif defined(USE_SSE2)
 	return _mm_or_si128(v1, v2);
 #elif defined(USE_NEON)
 	return vorrq_u8(v1, v2);
@@ -350,7 +372,9 @@ vector8_or(const Vector8 v1, const Vector8 v2)
 static inline Vector32
 vector32_or(const Vector32 v1, const Vector32 v2)
 {
-#ifdef USE_SSE2
+#if defined(USE_AVX2)
+	return _mm256_or_si256(v1, v2);
+#elif defined(USE_SSE2)
 	return _mm_or_si128(v1, v2);
 #elif defined(USE_NEON)
 	return vorrq_u32(v1, v2);
@@ -368,7 +392,9 @@ vector32_or(const Vector32 v1, const Vector32 v2)
 static inline Vector8
 vector8_ssub(const Vector8 v1, const Vector8 v2)
 {
-#ifdef USE_SSE2
+#if defined(USE_AVX2)
+	return _mm256_subs_epu8(v1, v2);
+#elif defined(USE_SSE2)
 	return _mm_subs_epu8(v1, v2);
 #elif defined(USE_NEON)
 	return vqsubq_u8(v1, v2);
@@ -384,7 +410,9 @@ vector8_ssub(const Vector8 v1, const Vector8 v2)
 static inline Vector8
 vector8_eq(const Vector8 v1, const Vector8 v2)
 {
-#ifdef USE_SSE2
+#if defined(USE_AVX2)
+	return _mm256_cmpeq_epi8(v1, v2);
+#elif defined(USE_SSE2)
 	return _mm_cmpeq_epi8(v1, v2);
 #elif defined(USE_NEON)
 	return vceqq_u8(v1, v2);
@@ -396,7 +424,9 @@ vector8_eq(const Vector8 v1, const Vector8 v2)
 static inline Vector32
 vector32_eq(const Vector32 v1, const Vector32 v2)
 {
-#ifdef USE_SSE2
+#if defined(USE_AVX2)
+	return _mm256_cmpeq_epi32(v1, v2);
+#elif defined(USE_SSE2)
 	return _mm_cmpeq_epi32(v1, v2);
 #elif defined(USE_NEON)
 	return vceqq_u32(v1, v2);
@@ -411,7 +441,9 @@ vector32_eq(const Vector32 v1, const Vector32 v2)
 static inline Vector8
 vector8_min(const Vector8 v1, const Vector8 v2)
 {
-#ifdef USE_SSE2
+#if defined(USE_AVX2)
+	return _mm256_min_epu8(v1, v2);
+#elif defined(USE_SSE2)
 	return _mm_min_epu8(v1, v2);
 #elif defined(USE_NEON)
 	return vminq_u8(v1, v2);
-- 
2.25.1


--6c2NcOVqGQ03X4Wi
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v2-0003-test_lfind32-benchmark.patch"



^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-07-29 20:16  Heikki Linnakangas <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Heikki Linnakangas @ 2024-07-29 20:16 UTC (permalink / raw)
  To: pgsql-hackers

On 06/07/2024 22:01, Heikki Linnakangas wrote:
> Reading through postmaster code, I spotted some refactoring 
> opportunities to make it slightly more readable.
> 
> Currently, when a child process exits, the postmaster first scans 
> through BackgroundWorkerList to see if it was a bgworker process. If not 
> found, it scans through the BackendList to see if it was a backend 
> process (which it really should be then). That feels a bit silly, 
> because every running background worker process also has an entry in 
> BackendList. There's a lot of duplication between 
> CleanupBackgroundWorker and CleanupBackend.
> 
> Before commit 8a02b3d732, we used to created Backend entries only for 
> background worker processes that connected to a database, not for other 
> background worker processes. I think that's why we have the code 
> structure we have. But now that we have a Backend entry for all bgworker 
> processes, it's more natural to have single function to deal with both 
> regular backends and bgworkers.
> 
> So I came up with the attached patches. This doesn't make any meaningful 
> user-visible changes, except for some incidental changes in log messages 
> (see commit message for details).

New patch version attached. Fixed conflicts with recent commits, no 
other changes.

-- 
Heikki Linnakangas
Neon (https://neon.tech)


Attachments:

  [text/x-patch] v2-0001-Fix-outdated-comment-all-running-bgworkers-are-in.patch (1.6K, ../../[email protected]/2-v2-0001-Fix-outdated-comment-all-running-bgworkers-are-in.patch)
  download | inline diff:
From 84ca49efab16cc2699f8446684a5ebe63dad1c38 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 29 Jul 2024 23:13:16 +0300
Subject: [PATCH v2 1/4] Fix outdated comment; all running bgworkers are in
 BackendList

Before commit 8a02b3d732, only bgworkers that connected to a database
had an entry in the Backendlist. Commit 8a02b3d732 changed that, but
forgot to update this comment.

Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/include/postmaster/bgworker_internals.h | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/include/postmaster/bgworker_internals.h b/src/include/postmaster/bgworker_internals.h
index 9106a0ef3f..61ba54117a 100644
--- a/src/include/postmaster/bgworker_internals.h
+++ b/src/include/postmaster/bgworker_internals.h
@@ -26,14 +26,14 @@
 /*
  * List of background workers, private to postmaster.
  *
- * A worker that requests a database connection during registration will have
- * rw_backend set, and will be present in BackendList.  Note: do not rely on
- * rw_backend being non-NULL for shmem-connected workers!
+ * All workers that are currently running will have rw_backend set, and will
+ * be present in BackendList.
  */
 typedef struct RegisteredBgWorker
 {
 	BackgroundWorker rw_worker; /* its registry entry */
-	struct bkend *rw_backend;	/* its BackendList entry, or NULL */
+	struct bkend *rw_backend;	/* its BackendList entry, or NULL if not
+								 * running */
 	pid_t		rw_pid;			/* 0 if not running */
 	int			rw_child_slot;
 	TimestampTz rw_crashed_at;	/* if not 0, time it last crashed */
-- 
2.39.2



  [text/x-patch] v2-0002-Minor-refactoring-of-assign_backendlist_entry.patch (3.2K, ../../[email protected]/3-v2-0002-Minor-refactoring-of-assign_backendlist_entry.patch)
  download | inline diff:
From e541a6d2c8481cd9f9c75fb2328e8e6031cddac6 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 29 Jul 2024 23:13:56 +0300
Subject: [PATCH v2 2/4] Minor refactoring of assign_backendlist_entry()

Make assign_backendlist_entry() responsible just for allocating the
Backend struct. Linking it to the RegisteredBgWorker is the caller's
responsibility now. Seems more clear that way.

Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/backend/postmaster/postmaster.c | 25 ++++++++++++-------------
 1 file changed, 12 insertions(+), 13 deletions(-)

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 02442a4b85..a3e9e8fdc0 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -416,7 +416,7 @@ static void TerminateChildren(int signal);
 #define SignalChildren(sig)			   SignalSomeChildren(sig, BACKEND_TYPE_ALL)
 
 static int	CountChildren(int target);
-static bool assign_backendlist_entry(RegisteredBgWorker *rw);
+static Backend *assign_backendlist_entry(void);
 static void maybe_start_bgworkers(void);
 static bool CreateOptsFile(int argc, char *argv[], char *fullprogname);
 static pid_t StartChildProcess(BackendType type);
@@ -4028,6 +4028,7 @@ MaxLivePostmasterChildren(void)
 static bool
 do_start_bgworker(RegisteredBgWorker *rw)
 {
+	Backend    *bn;
 	pid_t		worker_pid;
 
 	Assert(rw->rw_pid == 0);
@@ -4042,11 +4043,14 @@ do_start_bgworker(RegisteredBgWorker *rw)
 	 * tried again right away, most likely we'd find ourselves hitting the
 	 * same resource-exhaustion condition.
 	 */
-	if (!assign_backendlist_entry(rw))
+	bn = assign_backendlist_entry();
+	if (bn == NULL)
 	{
 		rw->rw_crashed_at = GetCurrentTimestamp();
 		return false;
 	}
+	rw->rw_backend = bn;
+	rw->rw_child_slot = bn->child_slot;
 
 	ereport(DEBUG1,
 			(errmsg_internal("starting background worker process \"%s\"",
@@ -4119,12 +4123,10 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
  * Allocate the Backend struct for a connected background worker, but don't
  * add it to the list of backends just yet.
  *
- * On failure, return false without changing any worker state.
- *
- * Some info from the Backend is copied into the passed rw.
+ * On failure, return NULL.
  */
-static bool
-assign_backendlist_entry(RegisteredBgWorker *rw)
+static Backend *
+assign_backendlist_entry(void)
 {
 	Backend    *bn;
 
@@ -4138,7 +4140,7 @@ assign_backendlist_entry(RegisteredBgWorker *rw)
 		ereport(LOG,
 				(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
 				 errmsg("no slot available for new background worker process")));
-		return false;
+		return NULL;
 	}
 
 	bn = palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
@@ -4147,7 +4149,7 @@ assign_backendlist_entry(RegisteredBgWorker *rw)
 		ereport(LOG,
 				(errcode(ERRCODE_OUT_OF_MEMORY),
 				 errmsg("out of memory")));
-		return false;
+		return NULL;
 	}
 
 	bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
@@ -4155,10 +4157,7 @@ assign_backendlist_entry(RegisteredBgWorker *rw)
 	bn->dead_end = false;
 	bn->bgworker_notify = false;
 
-	rw->rw_backend = bn;
-	rw->rw_child_slot = bn->child_slot;
-
-	return true;
+	return bn;
 }
 
 /*
-- 
2.39.2



  [text/x-patch] v2-0003-Make-BackgroundWorkerList-doubly-linked.patch (13.2K, ../../[email protected]/4-v2-0003-Make-BackgroundWorkerList-doubly-linked.patch)
  download | inline diff:
From c8b9c9a30121342ca15265d1d0c6ea533282f106 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 29 Jul 2024 23:14:00 +0300
Subject: [PATCH v2 3/4] Make BackgroundWorkerList doubly-linked

This allows ForgetBackgroundWorker() and ReportBackgroundWorkerExit()
to take a RegisteredBgWorker pointer as argument, rather than a list
iterator. That feels a little more natural. But more importantly, this
paves the way for more refactoring in the next commit.

Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/backend/postmaster/bgworker.c           | 62 ++++++++++-----------
 src/backend/postmaster/postmaster.c         | 40 ++++++-------
 src/include/postmaster/bgworker_internals.h | 10 ++--
 3 files changed, 54 insertions(+), 58 deletions(-)

diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 77707bb384..981d8177b0 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -37,7 +37,7 @@
 /*
  * The postmaster's list of registered background workers, in private memory.
  */
-slist_head	BackgroundWorkerList = SLIST_STATIC_INIT(BackgroundWorkerList);
+dlist_head	BackgroundWorkerList = DLIST_STATIC_INIT(BackgroundWorkerList);
 
 /*
  * BackgroundWorkerSlots exist in shared memory and can be accessed (via
@@ -168,7 +168,7 @@ BackgroundWorkerShmemInit(void)
 										   &found);
 	if (!IsUnderPostmaster)
 	{
-		slist_iter	siter;
+		dlist_iter	iter;
 		int			slotno = 0;
 
 		BackgroundWorkerData->total_slots = max_worker_processes;
@@ -181,12 +181,12 @@ BackgroundWorkerShmemInit(void)
 		 * correspondence between the postmaster's private list and the array
 		 * in shared memory.
 		 */
-		slist_foreach(siter, &BackgroundWorkerList)
+		dlist_foreach(iter, &BackgroundWorkerList)
 		{
 			BackgroundWorkerSlot *slot = &BackgroundWorkerData->slot[slotno];
 			RegisteredBgWorker *rw;
 
-			rw = slist_container(RegisteredBgWorker, rw_lnode, siter.cur);
+			rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 			Assert(slotno < max_worker_processes);
 			slot->in_use = true;
 			slot->terminate = false;
@@ -220,13 +220,13 @@ BackgroundWorkerShmemInit(void)
 static RegisteredBgWorker *
 FindRegisteredWorkerBySlotNumber(int slotno)
 {
-	slist_iter	siter;
+	dlist_iter	iter;
 
-	slist_foreach(siter, &BackgroundWorkerList)
+	dlist_foreach(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, siter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 		if (rw->rw_shmem_slot == slotno)
 			return rw;
 	}
@@ -413,29 +413,25 @@ BackgroundWorkerStateChange(bool allow_new_workers)
 				(errmsg_internal("registering background worker \"%s\"",
 								 rw->rw_worker.bgw_name)));
 
-		slist_push_head(&BackgroundWorkerList, &rw->rw_lnode);
+		dlist_push_head(&BackgroundWorkerList, &rw->rw_lnode);
 	}
 }
 
 /*
  * Forget about a background worker that's no longer needed.
  *
- * The worker must be identified by passing an slist_mutable_iter that
- * points to it.  This convention allows deletion of workers during
- * searches of the worker list, and saves having to search the list again.
+ * NOTE: The entry is unlinked from BackgroundWorkerList.  If the caller is
+ * iterating through it, better use a mutable iterator!
  *
  * Caller is responsible for notifying bgw_notify_pid, if appropriate.
  *
  * This function must be invoked only in the postmaster.
  */
 void
-ForgetBackgroundWorker(slist_mutable_iter *cur)
+ForgetBackgroundWorker(RegisteredBgWorker *rw)
 {
-	RegisteredBgWorker *rw;
 	BackgroundWorkerSlot *slot;
 
-	rw = slist_container(RegisteredBgWorker, rw_lnode, cur->cur);
-
 	Assert(rw->rw_shmem_slot < max_worker_processes);
 	slot = &BackgroundWorkerData->slot[rw->rw_shmem_slot];
 	Assert(slot->in_use);
@@ -454,7 +450,7 @@ ForgetBackgroundWorker(slist_mutable_iter *cur)
 			(errmsg_internal("unregistering background worker \"%s\"",
 							 rw->rw_worker.bgw_name)));
 
-	slist_delete_current(cur);
+	dlist_delete(&rw->rw_lnode);
 	pfree(rw);
 }
 
@@ -480,17 +476,17 @@ ReportBackgroundWorkerPID(RegisteredBgWorker *rw)
  * Report that the PID of a background worker is now zero because a
  * previously-running background worker has exited.
  *
+ * NOTE: The entry may be unlinked from BackgroundWorkerList.  If the caller
+ * is iterating through it, better use a mutable iterator!
+ *
  * This function should only be called from the postmaster.
  */
 void
-ReportBackgroundWorkerExit(slist_mutable_iter *cur)
+ReportBackgroundWorkerExit(RegisteredBgWorker *rw)
 {
-	RegisteredBgWorker *rw;
 	BackgroundWorkerSlot *slot;
 	int			notify_pid;
 
-	rw = slist_container(RegisteredBgWorker, rw_lnode, cur->cur);
-
 	Assert(rw->rw_shmem_slot < max_worker_processes);
 	slot = &BackgroundWorkerData->slot[rw->rw_shmem_slot];
 	slot->pid = rw->rw_pid;
@@ -505,7 +501,7 @@ ReportBackgroundWorkerExit(slist_mutable_iter *cur)
 	 */
 	if (rw->rw_terminate ||
 		rw->rw_worker.bgw_restart_time == BGW_NEVER_RESTART)
-		ForgetBackgroundWorker(cur);
+		ForgetBackgroundWorker(rw);
 
 	if (notify_pid != 0)
 		kill(notify_pid, SIGUSR1);
@@ -519,13 +515,13 @@ ReportBackgroundWorkerExit(slist_mutable_iter *cur)
 void
 BackgroundWorkerStopNotifications(pid_t pid)
 {
-	slist_iter	siter;
+	dlist_iter	iter;
 
-	slist_foreach(siter, &BackgroundWorkerList)
+	dlist_foreach(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, siter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 		if (rw->rw_worker.bgw_notify_pid == pid)
 			rw->rw_worker.bgw_notify_pid = 0;
 	}
@@ -546,14 +542,14 @@ BackgroundWorkerStopNotifications(pid_t pid)
 void
 ForgetUnstartedBackgroundWorkers(void)
 {
-	slist_mutable_iter iter;
+	dlist_mutable_iter iter;
 
-	slist_foreach_modify(iter, &BackgroundWorkerList)
+	dlist_foreach_modify(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 		BackgroundWorkerSlot *slot;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, iter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 		Assert(rw->rw_shmem_slot < max_worker_processes);
 		slot = &BackgroundWorkerData->slot[rw->rw_shmem_slot];
 
@@ -564,7 +560,7 @@ ForgetUnstartedBackgroundWorkers(void)
 			/* ... then zap it, and notify the waiter */
 			int			notify_pid = rw->rw_worker.bgw_notify_pid;
 
-			ForgetBackgroundWorker(&iter);
+			ForgetBackgroundWorker(rw);
 			if (notify_pid != 0)
 				kill(notify_pid, SIGUSR1);
 		}
@@ -584,13 +580,13 @@ ForgetUnstartedBackgroundWorkers(void)
 void
 ResetBackgroundWorkerCrashTimes(void)
 {
-	slist_mutable_iter iter;
+	dlist_mutable_iter iter;
 
-	slist_foreach_modify(iter, &BackgroundWorkerList)
+	dlist_foreach_modify(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, iter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 
 		if (rw->rw_worker.bgw_restart_time == BGW_NEVER_RESTART)
 		{
@@ -601,7 +597,7 @@ ResetBackgroundWorkerCrashTimes(void)
 			 * parallel_terminate_count will get incremented after we've
 			 * already zeroed parallel_register_count, which would be bad.)
 			 */
-			ForgetBackgroundWorker(&iter);
+			ForgetBackgroundWorker(rw);
 		}
 		else
 		{
@@ -1036,7 +1032,7 @@ RegisterBackgroundWorker(BackgroundWorker *worker)
 	rw->rw_crashed_at = 0;
 	rw->rw_terminate = false;
 
-	slist_push_head(&BackgroundWorkerList, &rw->rw_lnode);
+	dlist_push_head(&BackgroundWorkerList, &rw->rw_lnode);
 }
 
 /*
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index a3e9e8fdc0..fc00e39c44 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -1531,7 +1531,7 @@ DetermineSleepTime(void)
 
 	if (HaveCrashedWorker)
 	{
-		slist_mutable_iter siter;
+		dlist_mutable_iter iter;
 
 		/*
 		 * When there are crashed bgworkers, we sleep just long enough that
@@ -1539,12 +1539,12 @@ DetermineSleepTime(void)
 		 * determine the minimum of all wakeup times according to most recent
 		 * crash time and requested restart interval.
 		 */
-		slist_foreach_modify(siter, &BackgroundWorkerList)
+		dlist_foreach_modify(iter, &BackgroundWorkerList)
 		{
 			RegisteredBgWorker *rw;
 			TimestampTz this_wakeup;
 
-			rw = slist_container(RegisteredBgWorker, rw_lnode, siter.cur);
+			rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 
 			if (rw->rw_crashed_at == 0)
 				continue;
@@ -1552,7 +1552,7 @@ DetermineSleepTime(void)
 			if (rw->rw_worker.bgw_restart_time == BGW_NEVER_RESTART
 				|| rw->rw_terminate)
 			{
-				ForgetBackgroundWorker(&siter);
+				ForgetBackgroundWorker(rw);
 				continue;
 			}
 
@@ -2625,13 +2625,13 @@ CleanupBackgroundWorker(int pid,
 						int exitstatus) /* child's exit status */
 {
 	char		namebuf[MAXPGPATH];
-	slist_mutable_iter iter;
+	dlist_mutable_iter iter;
 
-	slist_foreach_modify(iter, &BackgroundWorkerList)
+	dlist_foreach_modify(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, iter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 
 		if (rw->rw_pid != pid)
 			continue;
@@ -2694,7 +2694,7 @@ CleanupBackgroundWorker(int pid,
 		rw->rw_backend = NULL;
 		rw->rw_pid = 0;
 		rw->rw_child_slot = 0;
-		ReportBackgroundWorkerExit(&iter);	/* report child death */
+		ReportBackgroundWorkerExit(rw); /* report child death */
 
 		LogChildExit(EXIT_STATUS_0(exitstatus) ? DEBUG1 : LOG,
 					 namebuf, pid, exitstatus);
@@ -2796,8 +2796,8 @@ CleanupBackend(int pid,
 static void
 HandleChildCrash(int pid, int exitstatus, const char *procname)
 {
-	dlist_mutable_iter iter;
-	slist_iter	siter;
+	dlist_iter	iter;
+	dlist_mutable_iter miter;
 	Backend    *bp;
 	bool		take_action;
 
@@ -2819,11 +2819,11 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	}
 
 	/* Process background workers. */
-	slist_foreach(siter, &BackgroundWorkerList)
+	dlist_foreach(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, siter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 		if (rw->rw_pid == 0)
 			continue;			/* not running */
 		if (rw->rw_pid == pid)
@@ -2853,9 +2853,9 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	}
 
 	/* Process regular backends */
-	dlist_foreach_modify(iter, &BackendList)
+	dlist_foreach_modify(miter, &BackendList)
 	{
-		bp = dlist_container(Backend, elem, iter.cur);
+		bp = dlist_container(Backend, elem, miter.cur);
 
 		if (bp->pid == pid)
 		{
@@ -2866,7 +2866,7 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 			{
 				(void) ReleasePostmasterChildSlot(bp->child_slot);
 			}
-			dlist_delete(iter.cur);
+			dlist_delete(miter.cur);
 			pfree(bp);
 			/* Keep looping so we can signal remaining backends */
 		}
@@ -4177,7 +4177,7 @@ maybe_start_bgworkers(void)
 #define MAX_BGWORKERS_TO_LAUNCH 100
 	int			num_launched = 0;
 	TimestampTz now = 0;
-	slist_mutable_iter iter;
+	dlist_mutable_iter iter;
 
 	/*
 	 * During crash recovery, we have no need to be called until the state
@@ -4194,11 +4194,11 @@ maybe_start_bgworkers(void)
 	StartWorkerNeeded = false;
 	HaveCrashedWorker = false;
 
-	slist_foreach_modify(iter, &BackgroundWorkerList)
+	dlist_foreach_modify(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, iter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 
 		/* ignore if already running */
 		if (rw->rw_pid != 0)
@@ -4207,7 +4207,7 @@ maybe_start_bgworkers(void)
 		/* if marked for death, clean up and remove from list */
 		if (rw->rw_terminate)
 		{
-			ForgetBackgroundWorker(&iter);
+			ForgetBackgroundWorker(rw);
 			continue;
 		}
 
@@ -4226,7 +4226,7 @@ maybe_start_bgworkers(void)
 
 				notify_pid = rw->rw_worker.bgw_notify_pid;
 
-				ForgetBackgroundWorker(&iter);
+				ForgetBackgroundWorker(rw);
 
 				/* Report worker is gone now. */
 				if (notify_pid != 0)
diff --git a/src/include/postmaster/bgworker_internals.h b/src/include/postmaster/bgworker_internals.h
index 61ba54117a..e55e38af65 100644
--- a/src/include/postmaster/bgworker_internals.h
+++ b/src/include/postmaster/bgworker_internals.h
@@ -39,17 +39,17 @@ typedef struct RegisteredBgWorker
 	TimestampTz rw_crashed_at;	/* if not 0, time it last crashed */
 	int			rw_shmem_slot;
 	bool		rw_terminate;
-	slist_node	rw_lnode;		/* list link */
+	dlist_node	rw_lnode;		/* list link */
 } RegisteredBgWorker;
 
-extern PGDLLIMPORT slist_head BackgroundWorkerList;
+extern PGDLLIMPORT dlist_head BackgroundWorkerList;
 
 extern Size BackgroundWorkerShmemSize(void);
 extern void BackgroundWorkerShmemInit(void);
 extern void BackgroundWorkerStateChange(bool allow_new_workers);
-extern void ForgetBackgroundWorker(slist_mutable_iter *cur);
-extern void ReportBackgroundWorkerPID(RegisteredBgWorker *);
-extern void ReportBackgroundWorkerExit(slist_mutable_iter *cur);
+extern void ForgetBackgroundWorker(RegisteredBgWorker *rw);
+extern void ReportBackgroundWorkerPID(RegisteredBgWorker *rw);
+extern void ReportBackgroundWorkerExit(RegisteredBgWorker *rw);
 extern void BackgroundWorkerStopNotifications(pid_t pid);
 extern void ForgetUnstartedBackgroundWorkers(void);
 extern void ResetBackgroundWorkerCrashTimes(void);
-- 
2.39.2



  [text/x-patch] v2-0004-Refactor-code-to-handle-death-of-a-backend-or-bgw.patch (19.8K, ../../[email protected]/5-v2-0004-Refactor-code-to-handle-death-of-a-backend-or-bgw.patch)
  download | inline diff:
From 8ad8a09e65ce52efa34de638a7a4a0161945574b Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 29 Jul 2024 23:14:04 +0300
Subject: [PATCH v2 4/4] Refactor code to handle death of a backend or bgworker
 in postmaster

Currently, when a child process exits, the postmaster first scans
through BackgroundWorkerList, to see if it the child process was a
background worker. If not found, then it scans through BackendList to
see if it was a regular backend. That leads to some duplication
between the bgworker and regular backend cleanup code, as both have an
entry in the BackendList that needs to be cleaned up in the same way.
Refactor that so that we scan just the BackendList to find the child
process, and if it was a background worker, do the additional
bgworker-specific cleanup in addition to the normal Backend cleanup.

Change HandleChildCrash so that it doesn't try to handle the cleanup
of the process that already exited, only the signaling of all the
other processes. When called for any of the aux processes, the caller
cleared the *PID global variable, so the code in HandleChildCrash() to
do that was unused.

On Windows, if a child process exits with ERROR_WAIT_NO_CHILDREN, it's
now logged with that exit code, instead of 0. Also, if a bgworker
exits with ERROR_WAIT_NO_CHILDREN, it's now treated as crashed and is
restarted. Previously it was treated as a normal exit.

If a child process is not found in the BackendList, the log message
now calls it "untracked child process" rather than "server process".
Arguably that should be a PANIC, because we do track all the child
processes in the list, so failing to find a child process is highly
unexpected. But if we want to change that, let's discuss and do that
as a separate commit.

Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/backend/postmaster/bgworker.c           |   4 -
 src/backend/postmaster/postmaster.c         | 434 ++++++++------------
 src/include/postmaster/bgworker_internals.h |   7 +-
 3 files changed, 165 insertions(+), 280 deletions(-)

diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 981d8177b0..b83967cda3 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -401,9 +401,7 @@ BackgroundWorkerStateChange(bool allow_new_workers)
 		}
 
 		/* Initialize postmaster bookkeeping. */
-		rw->rw_backend = NULL;
 		rw->rw_pid = 0;
-		rw->rw_child_slot = 0;
 		rw->rw_crashed_at = 0;
 		rw->rw_shmem_slot = slotno;
 		rw->rw_terminate = false;
@@ -1026,9 +1024,7 @@ RegisterBackgroundWorker(BackgroundWorker *worker)
 	}
 
 	rw->rw_worker = *worker;
-	rw->rw_backend = NULL;
 	rw->rw_pid = 0;
-	rw->rw_child_slot = 0;
 	rw->rw_crashed_at = 0;
 	rw->rw_terminate = false;
 
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index fc00e39c44..2c23a402b0 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -171,6 +171,7 @@ typedef struct bkend
 	int			child_slot;		/* PMChildSlot for this backend, if any */
 	int			bkend_type;		/* child process flavor, see above */
 	bool		dead_end;		/* is it going to send an error and quit? */
+	RegisteredBgWorker *rw;		/* bgworker info, if this is a bgworker */
 	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
 	dlist_node	elem;			/* list link in BackendList */
 } Backend;
@@ -396,8 +397,7 @@ static void process_pm_child_exit(void);
 static void process_pm_reload_request(void);
 static void process_pm_shutdown_request(void);
 static void dummy_handler(SIGNAL_ARGS);
-static void CleanupBackend(int pid, int exitstatus);
-static bool CleanupBackgroundWorker(int pid, int exitstatus);
+static void CleanupBackend(Backend *bp, int exitstatus);
 static void HandleChildCrash(int pid, int exitstatus, const char *procname);
 static void LogChildExit(int lev, const char *procname,
 						 int pid, int exitstatus);
@@ -2291,6 +2291,9 @@ process_pm_child_exit(void)
 
 	while ((pid = waitpid(-1, &exitstatus, WNOHANG)) > 0)
 	{
+		bool		found;
+		dlist_mutable_iter iter;
+
 		/*
 		 * Check if this child was a startup process.
 		 */
@@ -2590,18 +2593,34 @@ process_pm_child_exit(void)
 			continue;
 		}
 
-		/* Was it one of our background workers? */
-		if (CleanupBackgroundWorker(pid, exitstatus))
+		/*
+		 * Was it a backend or background worker?
+		 */
+		found = false;
+		dlist_foreach_modify(iter, &BackendList)
 		{
-			/* have it be restarted */
-			HaveCrashedWorker = true;
-			continue;
+			Backend    *bp = dlist_container(Backend, elem, iter.cur);
+
+			if (bp->pid == pid)
+			{
+				dlist_delete(iter.cur);
+				CleanupBackend(bp, exitstatus);
+				found = true;
+				break;
+			}
 		}
 
 		/*
-		 * Else do standard backend child cleanup.
+		 * We don't know anything about this child process.  That's highly
+		 * unexpected, as we do track all the child processes that we fork.
 		 */
-		CleanupBackend(pid, exitstatus);
+		if (!found)
+		{
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus, _("untracked child process"));
+			else
+				LogChildExit(LOG, _("untracked child process"), pid, exitstatus);
+		}
 	}							/* loop over pending child-death reports */
 
 	/*
@@ -2612,113 +2631,31 @@ process_pm_child_exit(void)
 }
 
 /*
- * Scan the bgworkers list and see if the given PID (which has just stopped
- * or crashed) is in it.  Handle its shutdown if so, and return true.  If not a
- * bgworker, return false.
+ * CleanupBackend -- cleanup after terminated backend or background worker.
  *
- * This is heavily based on CleanupBackend.  One important difference is that
- * we don't know yet that the dying process is a bgworker, so we must be silent
- * until we're sure it is.
+ * Remove all local state associated with backend.
  */
-static bool
-CleanupBackgroundWorker(int pid,
-						int exitstatus) /* child's exit status */
+static void
+CleanupBackend(Backend *bp,
+			   int exitstatus)	/* child's exit status. */
 {
 	char		namebuf[MAXPGPATH];
-	dlist_mutable_iter iter;
+	char	   *procname;
+	bool		crashed = false;
 
-	dlist_foreach_modify(iter, &BackgroundWorkerList)
+	/* Construct a process name for log message */
+	if (bp->dead_end)
+	{
+		procname = _("dead end backend");
+	}
+	else if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
 	{
-		RegisteredBgWorker *rw;
-
-		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
-
-		if (rw->rw_pid != pid)
-			continue;
-
-#ifdef WIN32
-		/* see CleanupBackend */
-		if (exitstatus == ERROR_WAIT_NO_CHILDREN)
-			exitstatus = 0;
-#endif
-
 		snprintf(namebuf, MAXPGPATH, _("background worker \"%s\""),
-				 rw->rw_worker.bgw_type);
-
-
-		if (!EXIT_STATUS_0(exitstatus))
-		{
-			/* Record timestamp, so we know when to restart the worker. */
-			rw->rw_crashed_at = GetCurrentTimestamp();
-		}
-		else
-		{
-			/* Zero exit status means terminate */
-			rw->rw_crashed_at = 0;
-			rw->rw_terminate = true;
-		}
-
-		/*
-		 * Additionally, just like a backend, any exit status other than 0 or
-		 * 1 is considered a crash and causes a system-wide restart.
-		 */
-		if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
-		{
-			HandleChildCrash(pid, exitstatus, namebuf);
-			return true;
-		}
-
-		/*
-		 * We must release the postmaster child slot. If the worker failed to
-		 * do so, it did not clean up after itself, requiring a crash-restart
-		 * cycle.
-		 */
-		if (!ReleasePostmasterChildSlot(rw->rw_child_slot))
-		{
-			HandleChildCrash(pid, exitstatus, namebuf);
-			return true;
-		}
-
-		/* Get it out of the BackendList and clear out remaining data */
-		dlist_delete(&rw->rw_backend->elem);
-
-		/*
-		 * It's possible that this background worker started some OTHER
-		 * background worker and asked to be notified when that worker started
-		 * or stopped.  If so, cancel any notifications destined for the
-		 * now-dead backend.
-		 */
-		if (rw->rw_backend->bgworker_notify)
-			BackgroundWorkerStopNotifications(rw->rw_pid);
-		pfree(rw->rw_backend);
-		rw->rw_backend = NULL;
-		rw->rw_pid = 0;
-		rw->rw_child_slot = 0;
-		ReportBackgroundWorkerExit(rw); /* report child death */
-
-		LogChildExit(EXIT_STATUS_0(exitstatus) ? DEBUG1 : LOG,
-					 namebuf, pid, exitstatus);
-
-		return true;
+				 bp->rw->rw_worker.bgw_type);
+		procname = namebuf;
 	}
-
-	return false;
-}
-
-/*
- * CleanupBackend -- cleanup after terminated backend.
- *
- * Remove all local state associated with backend.
- *
- * If you change this, see also CleanupBackgroundWorker.
- */
-static void
-CleanupBackend(int pid,
-			   int exitstatus)	/* child's exit status. */
-{
-	dlist_mutable_iter iter;
-
-	LogChildExit(DEBUG2, _("server process"), pid, exitstatus);
+	else
+		procname = _("server process");
 
 	/*
 	 * If a backend dies in an ugly way then we must signal all other backends
@@ -2726,6 +2663,8 @@ CleanupBackend(int pid,
 	 * assume everything is all right and proceed to remove the backend from
 	 * the active backend list.
 	 */
+	if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+		crashed = true;
 
 #ifdef WIN32
 
@@ -2738,52 +2677,79 @@ CleanupBackend(int pid,
 	 */
 	if (exitstatus == ERROR_WAIT_NO_CHILDREN)
 	{
-		LogChildExit(LOG, _("server process"), pid, exitstatus);
-		exitstatus = 0;
+		LogChildExit(LOG, procname, bp->pid, exitstatus);
+		crashed = false;
 	}
 #endif
 
-	if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+	/*
+	 * If the process attached to shared memory, check that it detached
+	 * cleanly.
+	 */
+	if (!bp->dead_end)
+	{
+		if (!ReleasePostmasterChildSlot(bp->child_slot))
+		{
+			/*
+			 * Uh-oh, the child failed to clean itself up.  Treat as a crash
+			 * after all.
+			 */
+			crashed = true;
+		}
+#ifdef EXEC_BACKEND
+		ShmemBackendArrayRemove(bp);
+#endif
+	}
+
+	if (crashed)
 	{
-		HandleChildCrash(pid, exitstatus, _("server process"));
+		HandleChildCrash(bp->pid, exitstatus, namebuf);
+		pfree(bp);
 		return;
 	}
 
-	dlist_foreach_modify(iter, &BackendList)
+	/*
+	 * This backend may have been slated to receive SIGUSR1 when some
+	 * background worker started or stopped.  Cancel those notifications, as
+	 * we don't want to signal PIDs that are not PostgreSQL backends.  This
+	 * gets skipped in the (probably very common) case where the backend has
+	 * never requested any such notifications.
+	 */
+	if (bp->bgworker_notify)
+		BackgroundWorkerStopNotifications(bp->pid);
+
+	/*
+	 * If it was a background worker, also update its RegisteredWorker entry.
+	 */
+	if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
 	{
-		Backend    *bp = dlist_container(Backend, elem, iter.cur);
+		RegisteredBgWorker *rw = bp->rw;
 
-		if (bp->pid == pid)
+		if (!EXIT_STATUS_0(exitstatus))
 		{
-			if (!bp->dead_end)
-			{
-				if (!ReleasePostmasterChildSlot(bp->child_slot))
-				{
-					/*
-					 * Uh-oh, the child failed to clean itself up.  Treat as a
-					 * crash after all.
-					 */
-					HandleChildCrash(pid, exitstatus, _("server process"));
-					return;
-				}
-			}
-			if (bp->bgworker_notify)
-			{
-				/*
-				 * This backend may have been slated to receive SIGUSR1 when
-				 * some background worker started or stopped.  Cancel those
-				 * notifications, as we don't want to signal PIDs that are not
-				 * PostgreSQL backends.  This gets skipped in the (probably
-				 * very common) case where the backend has never requested any
-				 * such notifications.
-				 */
-				BackgroundWorkerStopNotifications(bp->pid);
-			}
-			dlist_delete(iter.cur);
-			pfree(bp);
-			break;
+			/* Record timestamp, so we know when to restart the worker. */
+			rw->rw_crashed_at = GetCurrentTimestamp();
+		}
+		else
+		{
+			/* Zero exit status means terminate */
+			rw->rw_crashed_at = 0;
+			rw->rw_terminate = true;
 		}
+
+		rw->rw_pid = 0;
+		ReportBackgroundWorkerExit(rw); /* report child death */
+
+		LogChildExit(EXIT_STATUS_0(exitstatus) ? DEBUG1 : LOG,
+					 procname, bp->pid, exitstatus);
+
+		/* have it be restarted */
+		HaveCrashedWorker = true;
 	}
+	else
+		LogChildExit(DEBUG2, procname, bp->pid, exitstatus);
+
+	pfree(bp);
 }
 
 /*
@@ -2792,13 +2758,14 @@ CleanupBackend(int pid,
  *
  * The objectives here are to clean up our local state about the child
  * process, and to signal all other remaining children to quickdie.
+ *
+ * If it's a backend, the caller has already removed it from the
+ * BackendList. If it's an aux process, the corresponding *PID global variable
+ * has been reset already.
  */
 static void
 HandleChildCrash(int pid, int exitstatus, const char *procname)
 {
-	dlist_iter	iter;
-	dlist_mutable_iter miter;
-	Backend    *bp;
 	bool		take_action;
 
 	/*
@@ -2818,139 +2785,64 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 		SetQuitSignalReason(PMQUIT_FOR_CRASH);
 	}
 
-	/* Process background workers. */
-	dlist_foreach(iter, &BackgroundWorkerList)
+	if (take_action)
 	{
-		RegisteredBgWorker *rw;
+		dlist_iter	iter;
 
-		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
-		if (rw->rw_pid == 0)
-			continue;			/* not running */
-		if (rw->rw_pid == pid)
-		{
-			/*
-			 * Found entry for freshly-dead worker, so remove it.
-			 */
-			(void) ReleasePostmasterChildSlot(rw->rw_child_slot);
-			dlist_delete(&rw->rw_backend->elem);
-			pfree(rw->rw_backend);
-			rw->rw_backend = NULL;
-			rw->rw_pid = 0;
-			rw->rw_child_slot = 0;
-			/* don't reset crashed_at */
-			/* don't report child stop, either */
-			/* Keep looping so we can signal remaining workers */
-		}
-		else
+		dlist_foreach(iter, &BackendList)
 		{
-			/*
-			 * This worker is still alive.  Unless we did so already, tell it
-			 * to commit hara-kiri.
-			 */
-			if (take_action)
-				sigquit_child(rw->rw_pid);
-		}
-	}
-
-	/* Process regular backends */
-	dlist_foreach_modify(miter, &BackendList)
-	{
-		bp = dlist_container(Backend, elem, miter.cur);
+			Backend    *bp = dlist_container(Backend, elem, iter.cur);
 
-		if (bp->pid == pid)
-		{
-			/*
-			 * Found entry for freshly-dead backend, so remove it.
-			 */
-			if (!bp->dead_end)
-			{
-				(void) ReleasePostmasterChildSlot(bp->child_slot);
-			}
-			dlist_delete(miter.cur);
-			pfree(bp);
-			/* Keep looping so we can signal remaining backends */
-		}
-		else
-		{
 			/*
 			 * This backend is still alive.  Unless we did so already, tell it
 			 * to commit hara-kiri.
 			 *
 			 * We could exclude dead_end children here, but at least when
 			 * sending SIGABRT it seems better to include them.
-			 *
-			 * Background workers were already processed above; ignore them
-			 * here.
 			 */
-			if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
-				continue;
+			sigquit_child(bp->pid);
+		}
 
-			if (take_action)
-				sigquit_child(bp->pid);
+		if (StartupPID != 0)
+		{
+			sigquit_child(StartupPID);
+			StartupStatus = STARTUP_SIGNALED;
 		}
-	}
 
-	/* Take care of the startup process too */
-	if (pid == StartupPID)
-	{
-		StartupPID = 0;
-		/* Caller adjusts StartupStatus, so don't touch it here */
-	}
-	else if (StartupPID != 0 && take_action)
-	{
-		sigquit_child(StartupPID);
-		StartupStatus = STARTUP_SIGNALED;
-	}
+		/* Take care of the bgwriter too */
+		if (BgWriterPID != 0)
+			sigquit_child(BgWriterPID);
+
+		/* Take care of the checkpointer too */
+		if (CheckpointerPID != 0)
+			sigquit_child(CheckpointerPID);
+
+		/* Take care of the walwriter too */
+		if (WalWriterPID != 0)
+			sigquit_child(WalWriterPID);
 
-	/* Take care of the bgwriter too */
-	if (pid == BgWriterPID)
-		BgWriterPID = 0;
-	else if (BgWriterPID != 0 && take_action)
-		sigquit_child(BgWriterPID);
-
-	/* Take care of the checkpointer too */
-	if (pid == CheckpointerPID)
-		CheckpointerPID = 0;
-	else if (CheckpointerPID != 0 && take_action)
-		sigquit_child(CheckpointerPID);
-
-	/* Take care of the walwriter too */
-	if (pid == WalWriterPID)
-		WalWriterPID = 0;
-	else if (WalWriterPID != 0 && take_action)
-		sigquit_child(WalWriterPID);
-
-	/* Take care of the walreceiver too */
-	if (pid == WalReceiverPID)
-		WalReceiverPID = 0;
-	else if (WalReceiverPID != 0 && take_action)
-		sigquit_child(WalReceiverPID);
-
-	/* Take care of the walsummarizer too */
-	if (pid == WalSummarizerPID)
-		WalSummarizerPID = 0;
-	else if (WalSummarizerPID != 0 && take_action)
-		sigquit_child(WalSummarizerPID);
-
-	/* Take care of the autovacuum launcher too */
-	if (pid == AutoVacPID)
-		AutoVacPID = 0;
-	else if (AutoVacPID != 0 && take_action)
-		sigquit_child(AutoVacPID);
-
-	/* Take care of the archiver too */
-	if (pid == PgArchPID)
-		PgArchPID = 0;
-	else if (PgArchPID != 0 && take_action)
-		sigquit_child(PgArchPID);
-
-	/* Take care of the slot sync worker too */
-	if (pid == SlotSyncWorkerPID)
-		SlotSyncWorkerPID = 0;
-	else if (SlotSyncWorkerPID != 0 && take_action)
-		sigquit_child(SlotSyncWorkerPID);
-
-	/* We do NOT restart the syslogger */
+		/* Take care of the walreceiver too */
+		if (WalReceiverPID != 0)
+			sigquit_child(WalReceiverPID);
+
+		/* Take care of the walsummarizer too */
+		if (WalSummarizerPID != 0)
+			sigquit_child(WalSummarizerPID);
+
+		/* Take care of the autovacuum launcher too */
+		if (AutoVacPID != 0)
+			sigquit_child(AutoVacPID);
+
+		/* Take care of the archiver too */
+		if (PgArchPID != 0)
+			sigquit_child(PgArchPID);
+
+		/* Take care of the slot sync worker too */
+		if (SlotSyncWorkerPID != 0)
+			sigquit_child(SlotSyncWorkerPID);
+
+		/* We do NOT restart the syslogger */
+	}
 
 	if (Shutdown != ImmediateShutdown)
 		FatalError = true;
@@ -3480,6 +3372,7 @@ BackendStartup(ClientSocket *client_sock)
 	/* Pass down canAcceptConnections state */
 	startup_data.canAcceptConnections = canAcceptConnections(BACKEND_TYPE_NORMAL);
 	bn->dead_end = (startup_data.canAcceptConnections != CAC_OK);
+	bn->rw = NULL;
 
 	/*
 	 * Unless it's a dead_end child, assign it a child slot number
@@ -3865,6 +3758,7 @@ StartAutovacuumWorker(void)
 			bn->dead_end = false;
 			bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
 			bn->bgworker_notify = false;
+			bn->rw = NULL;
 
 			bn->pid = StartChildProcess(B_AUTOVAC_WORKER);
 			if (bn->pid > 0)
@@ -4049,8 +3943,7 @@ do_start_bgworker(RegisteredBgWorker *rw)
 		rw->rw_crashed_at = GetCurrentTimestamp();
 		return false;
 	}
-	rw->rw_backend = bn;
-	rw->rw_child_slot = bn->child_slot;
+	bn->rw = rw;
 
 	ereport(DEBUG1,
 			(errmsg_internal("starting background worker process \"%s\"",
@@ -4063,10 +3956,9 @@ do_start_bgworker(RegisteredBgWorker *rw)
 		ereport(LOG,
 				(errmsg("could not fork background worker process: %m")));
 		/* undo what assign_backendlist_entry did */
-		ReleasePostmasterChildSlot(rw->rw_child_slot);
-		rw->rw_child_slot = 0;
-		pfree(rw->rw_backend);
-		rw->rw_backend = NULL;
+		ReleasePostmasterChildSlot(bn->child_slot);
+		pfree(bn);
+
 		/* mark entry as crashed, so we'll try again later */
 		rw->rw_crashed_at = GetCurrentTimestamp();
 		return false;
@@ -4074,10 +3966,10 @@ do_start_bgworker(RegisteredBgWorker *rw)
 
 	/* in postmaster, fork successful ... */
 	rw->rw_pid = worker_pid;
-	rw->rw_backend->pid = rw->rw_pid;
+	bn->pid = rw->rw_pid;
 	ReportBackgroundWorkerPID(rw);
 	/* add new worker to lists of backends */
-	dlist_push_head(&BackendList, &rw->rw_backend->elem);
+	dlist_push_head(&BackendList, &bn->elem);
 	return true;
 }
 
diff --git a/src/include/postmaster/bgworker_internals.h b/src/include/postmaster/bgworker_internals.h
index e55e38af65..309a91124b 100644
--- a/src/include/postmaster/bgworker_internals.h
+++ b/src/include/postmaster/bgworker_internals.h
@@ -26,16 +26,13 @@
 /*
  * List of background workers, private to postmaster.
  *
- * All workers that are currently running will have rw_backend set, and will
- * be present in BackendList.
+ * All workers that are currently running will also have an entry in
+ * BackendList.
  */
 typedef struct RegisteredBgWorker
 {
 	BackgroundWorker rw_worker; /* its registry entry */
-	struct bkend *rw_backend;	/* its BackendList entry, or NULL if not
-								 * running */
 	pid_t		rw_pid;			/* 0 if not running */
-	int			rw_child_slot;
 	TimestampTz rw_crashed_at;	/* if not 0, time it last crashed */
 	int			rw_shmem_slot;
 	bool		rw_terminate;
-- 
2.39.2



^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-08-01 23:57  Heikki Linnakangas <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Heikki Linnakangas @ 2024-08-01 23:57 UTC (permalink / raw)
  To: pgsql-hackers

I committed the first two trivial patches, and have continued to work on 
postmaster.c, and how it manages all the child processes.

This is a lot of patches. They're built on top of each other, because 
that's the order I developed them in, but they probably could be applied 
in different order too. Please help me by reviewing these, before the 
stack grows even larger :-). Even partial reviews would be very helpful. 
I suggest to start reading them in order, and when you get tired, just 
send any comments you have up to that point.


* v3-0001-Make-BackgroundWorkerList-doubly-linked.patch

This is the same refactoring patch I started this thread with.

* v3-0003-Fix-comment-on-processes-being-kept-over-a-restar.patch
* v3-0004-Consolidate-postmaster-code-to-launch-background-.patch

Little refactoring of how postmaster launches the background processes.

* v3-0005-Add-test-for-connection-limits.patch
* v3-0006-Add-test-for-dead-end-backends.patch

A few new TAP tests for dead-end backends and enforcing connection 
limits. We didn't have much coverage for these before.

* v3-0007-Use-an-shmem_exit-callback-to-remove-backend-from.patch
* v3-0008-Introduce-a-separate-BackendType-for-dead-end-chi.patch

Some preliminary refactoring towards patch 
v3-0010-Assign-a-child-slot-to-every-postmaster-child-pro.patch

* v3-0009-Kill-dead-end-children-when-there-s-nothing-else-.patch

I noticed that we never send SIGTERM or SIGQUIT to dead-end backends, 
which seems silly. If the server is shutting down, dead-end backends 
might prevent the shutdown from completing. Dead-end backends will 
expire after authentication_timoeut (default 60s), so it won't last for 
too long, but still seems like we should kill dead-end backends if 
they're the only children preventing shutdown from completing.

* 3-0010-Assign-a-child-slot-to-every-postmaster-child-pro.patch

This is what I consider the main patch in this series. Currently, only 
regular backens, bgworkers and autovacuum workers have a PMChildFlags 
slot, which is used to detect when a postmaster child exits in an 
unclean way (in addition to the exit code). This patch assigns a child 
slot for all processes, except for dead-end backends. That includes all 
the aux processes.

While we're at it, I created separate pools of child slots for different 
kinds of backends, which fixes the issue that opening a lot of client 
connections can exhaust all the slots, so that background workers or 
autovacuum workers cannot start either [1].

[1] 
https://www.postgresql.org/message-id/55d2f50c-0b81-4b33-b202-cd2a406d69a3%40iki.fi

* v3-0011-Pass-MyPMChildSlot-as-an-explicit-argument-to-chi.patch

One more little refactoring, to pass MyPMChildSlot to the child process 
differently.


Where is all this leading? I'm not sure exactly, but having a postmaster 
child slot for every postmaster child seems highly useful. We could move 
the ProcSignal machinery to use those slot numbers for the indexes to 
the ProcSignal array, instead of ProcSignal, for example. That would 
allow all processes to participate in the signalling, even before they 
have a PGPROC entry. (Or with Thomas's interrupts refactoring, the 
interrupts array). With the multithreading work, PMChild struct could 
store a thread id, or whatever is needed for threads to communicate with 
each other. In any case, seems like it will come handy.

-- 
Heikki Linnakangas
Neon (https://neon.tech)


Attachments:

  [text/x-patch] v3-0001-Make-BackgroundWorkerList-doubly-linked.patch (13.2K, ../../[email protected]/2-v3-0001-Make-BackgroundWorkerList-doubly-linked.patch)
  download | inline diff:
From c5881b9f4b89b762cd8ef925936eec3602b565b5 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 29 Jul 2024 23:14:00 +0300
Subject: [PATCH v3 01/12] Make BackgroundWorkerList doubly-linked

This allows ForgetBackgroundWorker() and ReportBackgroundWorkerExit()
to take a RegisteredBgWorker pointer as argument, rather than a list
iterator. That feels a little more natural. But more importantly, this
paves the way for more refactoring in the next commit.

Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/backend/postmaster/bgworker.c           | 62 ++++++++++-----------
 src/backend/postmaster/postmaster.c         | 40 ++++++-------
 src/include/postmaster/bgworker_internals.h | 10 ++--
 3 files changed, 54 insertions(+), 58 deletions(-)

diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 77707bb384..981d8177b0 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -37,7 +37,7 @@
 /*
  * The postmaster's list of registered background workers, in private memory.
  */
-slist_head	BackgroundWorkerList = SLIST_STATIC_INIT(BackgroundWorkerList);
+dlist_head	BackgroundWorkerList = DLIST_STATIC_INIT(BackgroundWorkerList);
 
 /*
  * BackgroundWorkerSlots exist in shared memory and can be accessed (via
@@ -168,7 +168,7 @@ BackgroundWorkerShmemInit(void)
 										   &found);
 	if (!IsUnderPostmaster)
 	{
-		slist_iter	siter;
+		dlist_iter	iter;
 		int			slotno = 0;
 
 		BackgroundWorkerData->total_slots = max_worker_processes;
@@ -181,12 +181,12 @@ BackgroundWorkerShmemInit(void)
 		 * correspondence between the postmaster's private list and the array
 		 * in shared memory.
 		 */
-		slist_foreach(siter, &BackgroundWorkerList)
+		dlist_foreach(iter, &BackgroundWorkerList)
 		{
 			BackgroundWorkerSlot *slot = &BackgroundWorkerData->slot[slotno];
 			RegisteredBgWorker *rw;
 
-			rw = slist_container(RegisteredBgWorker, rw_lnode, siter.cur);
+			rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 			Assert(slotno < max_worker_processes);
 			slot->in_use = true;
 			slot->terminate = false;
@@ -220,13 +220,13 @@ BackgroundWorkerShmemInit(void)
 static RegisteredBgWorker *
 FindRegisteredWorkerBySlotNumber(int slotno)
 {
-	slist_iter	siter;
+	dlist_iter	iter;
 
-	slist_foreach(siter, &BackgroundWorkerList)
+	dlist_foreach(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, siter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 		if (rw->rw_shmem_slot == slotno)
 			return rw;
 	}
@@ -413,29 +413,25 @@ BackgroundWorkerStateChange(bool allow_new_workers)
 				(errmsg_internal("registering background worker \"%s\"",
 								 rw->rw_worker.bgw_name)));
 
-		slist_push_head(&BackgroundWorkerList, &rw->rw_lnode);
+		dlist_push_head(&BackgroundWorkerList, &rw->rw_lnode);
 	}
 }
 
 /*
  * Forget about a background worker that's no longer needed.
  *
- * The worker must be identified by passing an slist_mutable_iter that
- * points to it.  This convention allows deletion of workers during
- * searches of the worker list, and saves having to search the list again.
+ * NOTE: The entry is unlinked from BackgroundWorkerList.  If the caller is
+ * iterating through it, better use a mutable iterator!
  *
  * Caller is responsible for notifying bgw_notify_pid, if appropriate.
  *
  * This function must be invoked only in the postmaster.
  */
 void
-ForgetBackgroundWorker(slist_mutable_iter *cur)
+ForgetBackgroundWorker(RegisteredBgWorker *rw)
 {
-	RegisteredBgWorker *rw;
 	BackgroundWorkerSlot *slot;
 
-	rw = slist_container(RegisteredBgWorker, rw_lnode, cur->cur);
-
 	Assert(rw->rw_shmem_slot < max_worker_processes);
 	slot = &BackgroundWorkerData->slot[rw->rw_shmem_slot];
 	Assert(slot->in_use);
@@ -454,7 +450,7 @@ ForgetBackgroundWorker(slist_mutable_iter *cur)
 			(errmsg_internal("unregistering background worker \"%s\"",
 							 rw->rw_worker.bgw_name)));
 
-	slist_delete_current(cur);
+	dlist_delete(&rw->rw_lnode);
 	pfree(rw);
 }
 
@@ -480,17 +476,17 @@ ReportBackgroundWorkerPID(RegisteredBgWorker *rw)
  * Report that the PID of a background worker is now zero because a
  * previously-running background worker has exited.
  *
+ * NOTE: The entry may be unlinked from BackgroundWorkerList.  If the caller
+ * is iterating through it, better use a mutable iterator!
+ *
  * This function should only be called from the postmaster.
  */
 void
-ReportBackgroundWorkerExit(slist_mutable_iter *cur)
+ReportBackgroundWorkerExit(RegisteredBgWorker *rw)
 {
-	RegisteredBgWorker *rw;
 	BackgroundWorkerSlot *slot;
 	int			notify_pid;
 
-	rw = slist_container(RegisteredBgWorker, rw_lnode, cur->cur);
-
 	Assert(rw->rw_shmem_slot < max_worker_processes);
 	slot = &BackgroundWorkerData->slot[rw->rw_shmem_slot];
 	slot->pid = rw->rw_pid;
@@ -505,7 +501,7 @@ ReportBackgroundWorkerExit(slist_mutable_iter *cur)
 	 */
 	if (rw->rw_terminate ||
 		rw->rw_worker.bgw_restart_time == BGW_NEVER_RESTART)
-		ForgetBackgroundWorker(cur);
+		ForgetBackgroundWorker(rw);
 
 	if (notify_pid != 0)
 		kill(notify_pid, SIGUSR1);
@@ -519,13 +515,13 @@ ReportBackgroundWorkerExit(slist_mutable_iter *cur)
 void
 BackgroundWorkerStopNotifications(pid_t pid)
 {
-	slist_iter	siter;
+	dlist_iter	iter;
 
-	slist_foreach(siter, &BackgroundWorkerList)
+	dlist_foreach(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, siter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 		if (rw->rw_worker.bgw_notify_pid == pid)
 			rw->rw_worker.bgw_notify_pid = 0;
 	}
@@ -546,14 +542,14 @@ BackgroundWorkerStopNotifications(pid_t pid)
 void
 ForgetUnstartedBackgroundWorkers(void)
 {
-	slist_mutable_iter iter;
+	dlist_mutable_iter iter;
 
-	slist_foreach_modify(iter, &BackgroundWorkerList)
+	dlist_foreach_modify(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 		BackgroundWorkerSlot *slot;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, iter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 		Assert(rw->rw_shmem_slot < max_worker_processes);
 		slot = &BackgroundWorkerData->slot[rw->rw_shmem_slot];
 
@@ -564,7 +560,7 @@ ForgetUnstartedBackgroundWorkers(void)
 			/* ... then zap it, and notify the waiter */
 			int			notify_pid = rw->rw_worker.bgw_notify_pid;
 
-			ForgetBackgroundWorker(&iter);
+			ForgetBackgroundWorker(rw);
 			if (notify_pid != 0)
 				kill(notify_pid, SIGUSR1);
 		}
@@ -584,13 +580,13 @@ ForgetUnstartedBackgroundWorkers(void)
 void
 ResetBackgroundWorkerCrashTimes(void)
 {
-	slist_mutable_iter iter;
+	dlist_mutable_iter iter;
 
-	slist_foreach_modify(iter, &BackgroundWorkerList)
+	dlist_foreach_modify(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, iter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 
 		if (rw->rw_worker.bgw_restart_time == BGW_NEVER_RESTART)
 		{
@@ -601,7 +597,7 @@ ResetBackgroundWorkerCrashTimes(void)
 			 * parallel_terminate_count will get incremented after we've
 			 * already zeroed parallel_register_count, which would be bad.)
 			 */
-			ForgetBackgroundWorker(&iter);
+			ForgetBackgroundWorker(rw);
 		}
 		else
 		{
@@ -1036,7 +1032,7 @@ RegisterBackgroundWorker(BackgroundWorker *worker)
 	rw->rw_crashed_at = 0;
 	rw->rw_terminate = false;
 
-	slist_push_head(&BackgroundWorkerList, &rw->rw_lnode);
+	dlist_push_head(&BackgroundWorkerList, &rw->rw_lnode);
 }
 
 /*
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index a3e9e8fdc0..fc00e39c44 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -1531,7 +1531,7 @@ DetermineSleepTime(void)
 
 	if (HaveCrashedWorker)
 	{
-		slist_mutable_iter siter;
+		dlist_mutable_iter iter;
 
 		/*
 		 * When there are crashed bgworkers, we sleep just long enough that
@@ -1539,12 +1539,12 @@ DetermineSleepTime(void)
 		 * determine the minimum of all wakeup times according to most recent
 		 * crash time and requested restart interval.
 		 */
-		slist_foreach_modify(siter, &BackgroundWorkerList)
+		dlist_foreach_modify(iter, &BackgroundWorkerList)
 		{
 			RegisteredBgWorker *rw;
 			TimestampTz this_wakeup;
 
-			rw = slist_container(RegisteredBgWorker, rw_lnode, siter.cur);
+			rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 
 			if (rw->rw_crashed_at == 0)
 				continue;
@@ -1552,7 +1552,7 @@ DetermineSleepTime(void)
 			if (rw->rw_worker.bgw_restart_time == BGW_NEVER_RESTART
 				|| rw->rw_terminate)
 			{
-				ForgetBackgroundWorker(&siter);
+				ForgetBackgroundWorker(rw);
 				continue;
 			}
 
@@ -2625,13 +2625,13 @@ CleanupBackgroundWorker(int pid,
 						int exitstatus) /* child's exit status */
 {
 	char		namebuf[MAXPGPATH];
-	slist_mutable_iter iter;
+	dlist_mutable_iter iter;
 
-	slist_foreach_modify(iter, &BackgroundWorkerList)
+	dlist_foreach_modify(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, iter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 
 		if (rw->rw_pid != pid)
 			continue;
@@ -2694,7 +2694,7 @@ CleanupBackgroundWorker(int pid,
 		rw->rw_backend = NULL;
 		rw->rw_pid = 0;
 		rw->rw_child_slot = 0;
-		ReportBackgroundWorkerExit(&iter);	/* report child death */
+		ReportBackgroundWorkerExit(rw); /* report child death */
 
 		LogChildExit(EXIT_STATUS_0(exitstatus) ? DEBUG1 : LOG,
 					 namebuf, pid, exitstatus);
@@ -2796,8 +2796,8 @@ CleanupBackend(int pid,
 static void
 HandleChildCrash(int pid, int exitstatus, const char *procname)
 {
-	dlist_mutable_iter iter;
-	slist_iter	siter;
+	dlist_iter	iter;
+	dlist_mutable_iter miter;
 	Backend    *bp;
 	bool		take_action;
 
@@ -2819,11 +2819,11 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	}
 
 	/* Process background workers. */
-	slist_foreach(siter, &BackgroundWorkerList)
+	dlist_foreach(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, siter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 		if (rw->rw_pid == 0)
 			continue;			/* not running */
 		if (rw->rw_pid == pid)
@@ -2853,9 +2853,9 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	}
 
 	/* Process regular backends */
-	dlist_foreach_modify(iter, &BackendList)
+	dlist_foreach_modify(miter, &BackendList)
 	{
-		bp = dlist_container(Backend, elem, iter.cur);
+		bp = dlist_container(Backend, elem, miter.cur);
 
 		if (bp->pid == pid)
 		{
@@ -2866,7 +2866,7 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 			{
 				(void) ReleasePostmasterChildSlot(bp->child_slot);
 			}
-			dlist_delete(iter.cur);
+			dlist_delete(miter.cur);
 			pfree(bp);
 			/* Keep looping so we can signal remaining backends */
 		}
@@ -4177,7 +4177,7 @@ maybe_start_bgworkers(void)
 #define MAX_BGWORKERS_TO_LAUNCH 100
 	int			num_launched = 0;
 	TimestampTz now = 0;
-	slist_mutable_iter iter;
+	dlist_mutable_iter iter;
 
 	/*
 	 * During crash recovery, we have no need to be called until the state
@@ -4194,11 +4194,11 @@ maybe_start_bgworkers(void)
 	StartWorkerNeeded = false;
 	HaveCrashedWorker = false;
 
-	slist_foreach_modify(iter, &BackgroundWorkerList)
+	dlist_foreach_modify(iter, &BackgroundWorkerList)
 	{
 		RegisteredBgWorker *rw;
 
-		rw = slist_container(RegisteredBgWorker, rw_lnode, iter.cur);
+		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
 
 		/* ignore if already running */
 		if (rw->rw_pid != 0)
@@ -4207,7 +4207,7 @@ maybe_start_bgworkers(void)
 		/* if marked for death, clean up and remove from list */
 		if (rw->rw_terminate)
 		{
-			ForgetBackgroundWorker(&iter);
+			ForgetBackgroundWorker(rw);
 			continue;
 		}
 
@@ -4226,7 +4226,7 @@ maybe_start_bgworkers(void)
 
 				notify_pid = rw->rw_worker.bgw_notify_pid;
 
-				ForgetBackgroundWorker(&iter);
+				ForgetBackgroundWorker(rw);
 
 				/* Report worker is gone now. */
 				if (notify_pid != 0)
diff --git a/src/include/postmaster/bgworker_internals.h b/src/include/postmaster/bgworker_internals.h
index 61ba54117a..e55e38af65 100644
--- a/src/include/postmaster/bgworker_internals.h
+++ b/src/include/postmaster/bgworker_internals.h
@@ -39,17 +39,17 @@ typedef struct RegisteredBgWorker
 	TimestampTz rw_crashed_at;	/* if not 0, time it last crashed */
 	int			rw_shmem_slot;
 	bool		rw_terminate;
-	slist_node	rw_lnode;		/* list link */
+	dlist_node	rw_lnode;		/* list link */
 } RegisteredBgWorker;
 
-extern PGDLLIMPORT slist_head BackgroundWorkerList;
+extern PGDLLIMPORT dlist_head BackgroundWorkerList;
 
 extern Size BackgroundWorkerShmemSize(void);
 extern void BackgroundWorkerShmemInit(void);
 extern void BackgroundWorkerStateChange(bool allow_new_workers);
-extern void ForgetBackgroundWorker(slist_mutable_iter *cur);
-extern void ReportBackgroundWorkerPID(RegisteredBgWorker *);
-extern void ReportBackgroundWorkerExit(slist_mutable_iter *cur);
+extern void ForgetBackgroundWorker(RegisteredBgWorker *rw);
+extern void ReportBackgroundWorkerPID(RegisteredBgWorker *rw);
+extern void ReportBackgroundWorkerExit(RegisteredBgWorker *rw);
 extern void BackgroundWorkerStopNotifications(pid_t pid);
 extern void ForgetUnstartedBackgroundWorkers(void);
 extern void ResetBackgroundWorkerCrashTimes(void);
-- 
2.39.2



  [text/x-patch] v3-0002-Refactor-code-to-handle-death-of-a-backend-or-bgw.patch (19.8K, ../../[email protected]/3-v3-0002-Refactor-code-to-handle-death-of-a-backend-or-bgw.patch)
  download | inline diff:
From 6f59e21f98c9c95ef1d39f13718916f4ed571dbf Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Tue, 30 Jul 2024 14:28:19 +0300
Subject: [PATCH v3 02/12] Refactor code to handle death of a backend or
 bgworker in postmaster

Currently, when a child process exits, the postmaster first scans
through BackgroundWorkerList, to see if it the child process was a
background worker. If not found, then it scans through BackendList to
see if it was a regular backend. That leads to some duplication
between the bgworker and regular backend cleanup code, as both have an
entry in the BackendList that needs to be cleaned up in the same way.
Refactor that so that we scan just the BackendList to find the child
process, and if it was a background worker, do the additional
bgworker-specific cleanup in addition to the normal Backend cleanup.

Change HandleChildCrash so that it doesn't try to handle the cleanup
of the process that already exited, only the signaling of all the
other processes. When called for any of the aux processes, the caller
cleared the *PID global variable, so the code in HandleChildCrash() to
do that was unused.

On Windows, if a child process exits with ERROR_WAIT_NO_CHILDREN, it's
now logged with that exit code, instead of 0. Also, if a bgworker
exits with ERROR_WAIT_NO_CHILDREN, it's now treated as crashed and is
restarted. Previously it was treated as a normal exit.

If a child process is not found in the BackendList, the log message
now calls it "untracked child process" rather than "server process".
Arguably that should be a PANIC, because we do track all the child
processes in the list, so failing to find a child process is highly
unexpected. But if we want to change that, let's discuss and do that
as a separate commit.

Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/backend/postmaster/bgworker.c           |   4 -
 src/backend/postmaster/postmaster.c         | 431 ++++++++------------
 src/include/postmaster/bgworker_internals.h |   7 +-
 3 files changed, 162 insertions(+), 280 deletions(-)

diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 981d8177b0..b83967cda3 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -401,9 +401,7 @@ BackgroundWorkerStateChange(bool allow_new_workers)
 		}
 
 		/* Initialize postmaster bookkeeping. */
-		rw->rw_backend = NULL;
 		rw->rw_pid = 0;
-		rw->rw_child_slot = 0;
 		rw->rw_crashed_at = 0;
 		rw->rw_shmem_slot = slotno;
 		rw->rw_terminate = false;
@@ -1026,9 +1024,7 @@ RegisterBackgroundWorker(BackgroundWorker *worker)
 	}
 
 	rw->rw_worker = *worker;
-	rw->rw_backend = NULL;
 	rw->rw_pid = 0;
-	rw->rw_child_slot = 0;
 	rw->rw_crashed_at = 0;
 	rw->rw_terminate = false;
 
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index fc00e39c44..6e753f3865 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -171,6 +171,7 @@ typedef struct bkend
 	int			child_slot;		/* PMChildSlot for this backend, if any */
 	int			bkend_type;		/* child process flavor, see above */
 	bool		dead_end;		/* is it going to send an error and quit? */
+	RegisteredBgWorker *rw;		/* bgworker info, if this is a bgworker */
 	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
 	dlist_node	elem;			/* list link in BackendList */
 } Backend;
@@ -396,8 +397,7 @@ static void process_pm_child_exit(void);
 static void process_pm_reload_request(void);
 static void process_pm_shutdown_request(void);
 static void dummy_handler(SIGNAL_ARGS);
-static void CleanupBackend(int pid, int exitstatus);
-static bool CleanupBackgroundWorker(int pid, int exitstatus);
+static void CleanupBackend(Backend *bp, int exitstatus);
 static void HandleChildCrash(int pid, int exitstatus, const char *procname);
 static void LogChildExit(int lev, const char *procname,
 						 int pid, int exitstatus);
@@ -2291,6 +2291,9 @@ process_pm_child_exit(void)
 
 	while ((pid = waitpid(-1, &exitstatus, WNOHANG)) > 0)
 	{
+		bool		found;
+		dlist_mutable_iter iter;
+
 		/*
 		 * Check if this child was a startup process.
 		 */
@@ -2590,18 +2593,34 @@ process_pm_child_exit(void)
 			continue;
 		}
 
-		/* Was it one of our background workers? */
-		if (CleanupBackgroundWorker(pid, exitstatus))
+		/*
+		 * Was it a backend or background worker?
+		 */
+		found = false;
+		dlist_foreach_modify(iter, &BackendList)
 		{
-			/* have it be restarted */
-			HaveCrashedWorker = true;
-			continue;
+			Backend    *bp = dlist_container(Backend, elem, iter.cur);
+
+			if (bp->pid == pid)
+			{
+				dlist_delete(iter.cur);
+				CleanupBackend(bp, exitstatus);
+				found = true;
+				break;
+			}
 		}
 
 		/*
-		 * Else do standard backend child cleanup.
+		 * We don't know anything about this child process.  That's highly
+		 * unexpected, as we do track all the child processes that we fork.
 		 */
-		CleanupBackend(pid, exitstatus);
+		if (!found)
+		{
+			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+				HandleChildCrash(pid, exitstatus, _("untracked child process"));
+			else
+				LogChildExit(LOG, _("untracked child process"), pid, exitstatus);
+		}
 	}							/* loop over pending child-death reports */
 
 	/*
@@ -2612,113 +2631,31 @@ process_pm_child_exit(void)
 }
 
 /*
- * Scan the bgworkers list and see if the given PID (which has just stopped
- * or crashed) is in it.  Handle its shutdown if so, and return true.  If not a
- * bgworker, return false.
+ * CleanupBackend -- cleanup after terminated backend or background worker.
  *
- * This is heavily based on CleanupBackend.  One important difference is that
- * we don't know yet that the dying process is a bgworker, so we must be silent
- * until we're sure it is.
+ * Remove all local state associated with backend.
  */
-static bool
-CleanupBackgroundWorker(int pid,
-						int exitstatus) /* child's exit status */
+static void
+CleanupBackend(Backend *bp,
+			   int exitstatus)	/* child's exit status. */
 {
 	char		namebuf[MAXPGPATH];
-	dlist_mutable_iter iter;
+	char	   *procname;
+	bool		crashed = false;
 
-	dlist_foreach_modify(iter, &BackgroundWorkerList)
+	/* Construct a process name for log message */
+	if (bp->dead_end)
+	{
+		procname = _("dead end backend");
+	}
+	else if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
 	{
-		RegisteredBgWorker *rw;
-
-		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
-
-		if (rw->rw_pid != pid)
-			continue;
-
-#ifdef WIN32
-		/* see CleanupBackend */
-		if (exitstatus == ERROR_WAIT_NO_CHILDREN)
-			exitstatus = 0;
-#endif
-
 		snprintf(namebuf, MAXPGPATH, _("background worker \"%s\""),
-				 rw->rw_worker.bgw_type);
-
-
-		if (!EXIT_STATUS_0(exitstatus))
-		{
-			/* Record timestamp, so we know when to restart the worker. */
-			rw->rw_crashed_at = GetCurrentTimestamp();
-		}
-		else
-		{
-			/* Zero exit status means terminate */
-			rw->rw_crashed_at = 0;
-			rw->rw_terminate = true;
-		}
-
-		/*
-		 * Additionally, just like a backend, any exit status other than 0 or
-		 * 1 is considered a crash and causes a system-wide restart.
-		 */
-		if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
-		{
-			HandleChildCrash(pid, exitstatus, namebuf);
-			return true;
-		}
-
-		/*
-		 * We must release the postmaster child slot. If the worker failed to
-		 * do so, it did not clean up after itself, requiring a crash-restart
-		 * cycle.
-		 */
-		if (!ReleasePostmasterChildSlot(rw->rw_child_slot))
-		{
-			HandleChildCrash(pid, exitstatus, namebuf);
-			return true;
-		}
-
-		/* Get it out of the BackendList and clear out remaining data */
-		dlist_delete(&rw->rw_backend->elem);
-
-		/*
-		 * It's possible that this background worker started some OTHER
-		 * background worker and asked to be notified when that worker started
-		 * or stopped.  If so, cancel any notifications destined for the
-		 * now-dead backend.
-		 */
-		if (rw->rw_backend->bgworker_notify)
-			BackgroundWorkerStopNotifications(rw->rw_pid);
-		pfree(rw->rw_backend);
-		rw->rw_backend = NULL;
-		rw->rw_pid = 0;
-		rw->rw_child_slot = 0;
-		ReportBackgroundWorkerExit(rw); /* report child death */
-
-		LogChildExit(EXIT_STATUS_0(exitstatus) ? DEBUG1 : LOG,
-					 namebuf, pid, exitstatus);
-
-		return true;
+				 bp->rw->rw_worker.bgw_type);
+		procname = namebuf;
 	}
-
-	return false;
-}
-
-/*
- * CleanupBackend -- cleanup after terminated backend.
- *
- * Remove all local state associated with backend.
- *
- * If you change this, see also CleanupBackgroundWorker.
- */
-static void
-CleanupBackend(int pid,
-			   int exitstatus)	/* child's exit status. */
-{
-	dlist_mutable_iter iter;
-
-	LogChildExit(DEBUG2, _("server process"), pid, exitstatus);
+	else
+		procname = _("server process");
 
 	/*
 	 * If a backend dies in an ugly way then we must signal all other backends
@@ -2726,6 +2663,8 @@ CleanupBackend(int pid,
 	 * assume everything is all right and proceed to remove the backend from
 	 * the active backend list.
 	 */
+	if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+		crashed = true;
 
 #ifdef WIN32
 
@@ -2738,52 +2677,76 @@ CleanupBackend(int pid,
 	 */
 	if (exitstatus == ERROR_WAIT_NO_CHILDREN)
 	{
-		LogChildExit(LOG, _("server process"), pid, exitstatus);
-		exitstatus = 0;
+		LogChildExit(LOG, procname, bp->pid, exitstatus);
+		crashed = false;
 	}
 #endif
 
-	if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
+	/*
+	 * If the process attached to shared memory, check that it detached
+	 * cleanly.
+	 */
+	if (!bp->dead_end)
 	{
-		HandleChildCrash(pid, exitstatus, _("server process"));
+		if (!ReleasePostmasterChildSlot(bp->child_slot))
+		{
+			/*
+			 * Uh-oh, the child failed to clean itself up.  Treat as a crash
+			 * after all.
+			 */
+			crashed = true;
+		}
+	}
+
+	if (crashed)
+	{
+		HandleChildCrash(bp->pid, exitstatus, namebuf);
+		pfree(bp);
 		return;
 	}
 
-	dlist_foreach_modify(iter, &BackendList)
+	/*
+	 * This backend may have been slated to receive SIGUSR1 when some
+	 * background worker started or stopped.  Cancel those notifications, as
+	 * we don't want to signal PIDs that are not PostgreSQL backends.  This
+	 * gets skipped in the (probably very common) case where the backend has
+	 * never requested any such notifications.
+	 */
+	if (bp->bgworker_notify)
+		BackgroundWorkerStopNotifications(bp->pid);
+
+	/*
+	 * If it was a background worker, also update its RegisteredWorker entry.
+	 */
+	if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
 	{
-		Backend    *bp = dlist_container(Backend, elem, iter.cur);
+		RegisteredBgWorker *rw = bp->rw;
 
-		if (bp->pid == pid)
+		if (!EXIT_STATUS_0(exitstatus))
 		{
-			if (!bp->dead_end)
-			{
-				if (!ReleasePostmasterChildSlot(bp->child_slot))
-				{
-					/*
-					 * Uh-oh, the child failed to clean itself up.  Treat as a
-					 * crash after all.
-					 */
-					HandleChildCrash(pid, exitstatus, _("server process"));
-					return;
-				}
-			}
-			if (bp->bgworker_notify)
-			{
-				/*
-				 * This backend may have been slated to receive SIGUSR1 when
-				 * some background worker started or stopped.  Cancel those
-				 * notifications, as we don't want to signal PIDs that are not
-				 * PostgreSQL backends.  This gets skipped in the (probably
-				 * very common) case where the backend has never requested any
-				 * such notifications.
-				 */
-				BackgroundWorkerStopNotifications(bp->pid);
-			}
-			dlist_delete(iter.cur);
-			pfree(bp);
-			break;
+			/* Record timestamp, so we know when to restart the worker. */
+			rw->rw_crashed_at = GetCurrentTimestamp();
+		}
+		else
+		{
+			/* Zero exit status means terminate */
+			rw->rw_crashed_at = 0;
+			rw->rw_terminate = true;
 		}
+
+		rw->rw_pid = 0;
+		ReportBackgroundWorkerExit(rw); /* report child death */
+
+		LogChildExit(EXIT_STATUS_0(exitstatus) ? DEBUG1 : LOG,
+					 procname, bp->pid, exitstatus);
+
+		/* have it be restarted */
+		HaveCrashedWorker = true;
 	}
+	else
+		LogChildExit(DEBUG2, procname, bp->pid, exitstatus);
+
+	pfree(bp);
 }
 
 /*
@@ -2792,13 +2755,14 @@ CleanupBackend(int pid,
  *
  * The objectives here are to clean up our local state about the child
  * process, and to signal all other remaining children to quickdie.
+ *
+ * If it's a backend, the caller has already removed it from the
+ * BackendList. If it's an aux process, the corresponding *PID global variable
+ * has been reset already.
  */
 static void
 HandleChildCrash(int pid, int exitstatus, const char *procname)
 {
-	dlist_iter	iter;
-	dlist_mutable_iter miter;
-	Backend    *bp;
 	bool		take_action;
 
 	/*
@@ -2818,139 +2782,64 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 		SetQuitSignalReason(PMQUIT_FOR_CRASH);
 	}
 
-	/* Process background workers. */
-	dlist_foreach(iter, &BackgroundWorkerList)
+	if (take_action)
 	{
-		RegisteredBgWorker *rw;
+		dlist_iter	iter;
 
-		rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur);
-		if (rw->rw_pid == 0)
-			continue;			/* not running */
-		if (rw->rw_pid == pid)
+		dlist_foreach(iter, &BackendList)
 		{
-			/*
-			 * Found entry for freshly-dead worker, so remove it.
-			 */
-			(void) ReleasePostmasterChildSlot(rw->rw_child_slot);
-			dlist_delete(&rw->rw_backend->elem);
-			pfree(rw->rw_backend);
-			rw->rw_backend = NULL;
-			rw->rw_pid = 0;
-			rw->rw_child_slot = 0;
-			/* don't reset crashed_at */
-			/* don't report child stop, either */
-			/* Keep looping so we can signal remaining workers */
-		}
-		else
-		{
-			/*
-			 * This worker is still alive.  Unless we did so already, tell it
-			 * to commit hara-kiri.
-			 */
-			if (take_action)
-				sigquit_child(rw->rw_pid);
-		}
-	}
-
-	/* Process regular backends */
-	dlist_foreach_modify(miter, &BackendList)
-	{
-		bp = dlist_container(Backend, elem, miter.cur);
+			Backend    *bp = dlist_container(Backend, elem, iter.cur);
 
-		if (bp->pid == pid)
-		{
-			/*
-			 * Found entry for freshly-dead backend, so remove it.
-			 */
-			if (!bp->dead_end)
-			{
-				(void) ReleasePostmasterChildSlot(bp->child_slot);
-			}
-			dlist_delete(miter.cur);
-			pfree(bp);
-			/* Keep looping so we can signal remaining backends */
-		}
-		else
-		{
 			/*
 			 * This backend is still alive.  Unless we did so already, tell it
 			 * to commit hara-kiri.
 			 *
 			 * We could exclude dead_end children here, but at least when
 			 * sending SIGABRT it seems better to include them.
-			 *
-			 * Background workers were already processed above; ignore them
-			 * here.
 			 */
-			if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
-				continue;
+			sigquit_child(bp->pid);
+		}
 
-			if (take_action)
-				sigquit_child(bp->pid);
+		if (StartupPID != 0)
+		{
+			sigquit_child(StartupPID);
+			StartupStatus = STARTUP_SIGNALED;
 		}
-	}
 
-	/* Take care of the startup process too */
-	if (pid == StartupPID)
-	{
-		StartupPID = 0;
-		/* Caller adjusts StartupStatus, so don't touch it here */
-	}
-	else if (StartupPID != 0 && take_action)
-	{
-		sigquit_child(StartupPID);
-		StartupStatus = STARTUP_SIGNALED;
-	}
+		/* Take care of the bgwriter too */
+		if (BgWriterPID != 0)
+			sigquit_child(BgWriterPID);
+
+		/* Take care of the checkpointer too */
+		if (CheckpointerPID != 0)
+			sigquit_child(CheckpointerPID);
+
+		/* Take care of the walwriter too */
+		if (WalWriterPID != 0)
+			sigquit_child(WalWriterPID);
+
+		/* Take care of the walreceiver too */
+		if (WalReceiverPID != 0)
+			sigquit_child(WalReceiverPID);
+
+		/* Take care of the walsummarizer too */
+		if (WalSummarizerPID != 0)
+			sigquit_child(WalSummarizerPID);
+
+		/* Take care of the autovacuum launcher too */
+		if (AutoVacPID != 0)
+			sigquit_child(AutoVacPID);
 
-	/* Take care of the bgwriter too */
-	if (pid == BgWriterPID)
-		BgWriterPID = 0;
-	else if (BgWriterPID != 0 && take_action)
-		sigquit_child(BgWriterPID);
-
-	/* Take care of the checkpointer too */
-	if (pid == CheckpointerPID)
-		CheckpointerPID = 0;
-	else if (CheckpointerPID != 0 && take_action)
-		sigquit_child(CheckpointerPID);
-
-	/* Take care of the walwriter too */
-	if (pid == WalWriterPID)
-		WalWriterPID = 0;
-	else if (WalWriterPID != 0 && take_action)
-		sigquit_child(WalWriterPID);
-
-	/* Take care of the walreceiver too */
-	if (pid == WalReceiverPID)
-		WalReceiverPID = 0;
-	else if (WalReceiverPID != 0 && take_action)
-		sigquit_child(WalReceiverPID);
-
-	/* Take care of the walsummarizer too */
-	if (pid == WalSummarizerPID)
-		WalSummarizerPID = 0;
-	else if (WalSummarizerPID != 0 && take_action)
-		sigquit_child(WalSummarizerPID);
-
-	/* Take care of the autovacuum launcher too */
-	if (pid == AutoVacPID)
-		AutoVacPID = 0;
-	else if (AutoVacPID != 0 && take_action)
-		sigquit_child(AutoVacPID);
-
-	/* Take care of the archiver too */
-	if (pid == PgArchPID)
-		PgArchPID = 0;
-	else if (PgArchPID != 0 && take_action)
-		sigquit_child(PgArchPID);
-
-	/* Take care of the slot sync worker too */
-	if (pid == SlotSyncWorkerPID)
-		SlotSyncWorkerPID = 0;
-	else if (SlotSyncWorkerPID != 0 && take_action)
-		sigquit_child(SlotSyncWorkerPID);
-
-	/* We do NOT restart the syslogger */
+		/* Take care of the archiver too */
+		if (PgArchPID != 0)
+			sigquit_child(PgArchPID);
+
+		/* Take care of the slot sync worker too */
+		if (SlotSyncWorkerPID != 0)
+			sigquit_child(SlotSyncWorkerPID);
+
+		/* We do NOT restart the syslogger */
+	}
 
 	if (Shutdown != ImmediateShutdown)
 		FatalError = true;
@@ -3480,6 +3369,7 @@ BackendStartup(ClientSocket *client_sock)
 	/* Pass down canAcceptConnections state */
 	startup_data.canAcceptConnections = canAcceptConnections(BACKEND_TYPE_NORMAL);
 	bn->dead_end = (startup_data.canAcceptConnections != CAC_OK);
+	bn->rw = NULL;
 
 	/*
 	 * Unless it's a dead_end child, assign it a child slot number
@@ -3865,6 +3755,7 @@ StartAutovacuumWorker(void)
 			bn->dead_end = false;
 			bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
 			bn->bgworker_notify = false;
+			bn->rw = NULL;
 
 			bn->pid = StartChildProcess(B_AUTOVAC_WORKER);
 			if (bn->pid > 0)
@@ -4049,8 +3940,7 @@ do_start_bgworker(RegisteredBgWorker *rw)
 		rw->rw_crashed_at = GetCurrentTimestamp();
 		return false;
 	}
-	rw->rw_backend = bn;
-	rw->rw_child_slot = bn->child_slot;
+	bn->rw = rw;
 
 	ereport(DEBUG1,
 			(errmsg_internal("starting background worker process \"%s\"",
@@ -4063,10 +3953,9 @@ do_start_bgworker(RegisteredBgWorker *rw)
 		ereport(LOG,
 				(errmsg("could not fork background worker process: %m")));
 		/* undo what assign_backendlist_entry did */
-		ReleasePostmasterChildSlot(rw->rw_child_slot);
-		rw->rw_child_slot = 0;
-		pfree(rw->rw_backend);
-		rw->rw_backend = NULL;
+		ReleasePostmasterChildSlot(bn->child_slot);
+		pfree(bn);
+
 		/* mark entry as crashed, so we'll try again later */
 		rw->rw_crashed_at = GetCurrentTimestamp();
 		return false;
@@ -4074,10 +3963,10 @@ do_start_bgworker(RegisteredBgWorker *rw)
 
 	/* in postmaster, fork successful ... */
 	rw->rw_pid = worker_pid;
-	rw->rw_backend->pid = rw->rw_pid;
+	bn->pid = rw->rw_pid;
 	ReportBackgroundWorkerPID(rw);
 	/* add new worker to lists of backends */
-	dlist_push_head(&BackendList, &rw->rw_backend->elem);
+	dlist_push_head(&BackendList, &bn->elem);
 	return true;
 }
 
diff --git a/src/include/postmaster/bgworker_internals.h b/src/include/postmaster/bgworker_internals.h
index e55e38af65..309a91124b 100644
--- a/src/include/postmaster/bgworker_internals.h
+++ b/src/include/postmaster/bgworker_internals.h
@@ -26,16 +26,13 @@
 /*
  * List of background workers, private to postmaster.
  *
- * All workers that are currently running will have rw_backend set, and will
- * be present in BackendList.
+ * All workers that are currently running will also have an entry in
+ * BackendList.
  */
 typedef struct RegisteredBgWorker
 {
 	BackgroundWorker rw_worker; /* its registry entry */
-	struct bkend *rw_backend;	/* its BackendList entry, or NULL if not
-								 * running */
 	pid_t		rw_pid;			/* 0 if not running */
-	int			rw_child_slot;
 	TimestampTz rw_crashed_at;	/* if not 0, time it last crashed */
 	int			rw_shmem_slot;
 	bool		rw_terminate;
-- 
2.39.2



  [text/x-patch] v3-0003-Fix-comment-on-processes-being-kept-over-a-restar.patch (2.1K, ../../[email protected]/4-v3-0003-Fix-comment-on-processes-being-kept-over-a-restar.patch)
  download | inline diff:
From 3db4626d6ec21518682d38bd259a80c76f759be5 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 1 Aug 2024 21:53:44 +0300
Subject: [PATCH v3 03/12] Fix comment on processes being kept over a restart

All child processes except the syslogger are killed on a restart. The
archiver might be already running though, if it was started during
recovery.

The split in the comments between "other special children" and the
first group of "background tasks" seemed really arbitrary, so I just
merged them all into one group.
---
 src/backend/postmaster/postmaster.c | 13 +++----------
 1 file changed, 3 insertions(+), 10 deletions(-)

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 6e753f3865..b0152a0068 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2386,9 +2386,9 @@ process_pm_child_exit(void)
 			connsAllowed = true;
 
 			/*
-			 * Crank up the background tasks, if we didn't do that already
-			 * when we entered consistent recovery state.  It doesn't matter
-			 * if this fails, we'll just try again later.
+			 * Crank up any background tasks that we didn't start earlier
+			 * already.  It doesn't matter if any of these fail, we'll just
+			 * try again later.
 			 */
 			if (CheckpointerPID == 0)
 				CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
@@ -2397,18 +2397,11 @@ process_pm_child_exit(void)
 			if (WalWriterPID == 0)
 				WalWriterPID = StartChildProcess(B_WAL_WRITER);
 			MaybeStartWalSummarizer();
-
-			/*
-			 * Likewise, start other special children as needed.  In a restart
-			 * situation, some of them may be alive already.
-			 */
 			if (!IsBinaryUpgrade && AutoVacuumingActive() && AutoVacPID == 0)
 				AutoVacPID = StartChildProcess(B_AUTOVAC_LAUNCHER);
 			if (PgArchStartupAllowed() && PgArchPID == 0)
 				PgArchPID = StartChildProcess(B_ARCHIVER);
 			MaybeStartSlotSyncWorker();
-
-			/* workers may be scheduled to start now */
 			maybe_start_bgworkers();
 
 			/* at this point we are really open for business */
-- 
2.39.2



  [text/x-patch] v3-0004-Consolidate-postmaster-code-to-launch-background-.patch (13.4K, ../../[email protected]/5-v3-0004-Consolidate-postmaster-code-to-launch-background-.patch)
  download | inline diff:
From 41a6a279f16855b4e2e6978f172eb0c1c299aa50 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 1 Aug 2024 21:13:09 +0300
Subject: [PATCH v3 04/12] Consolidate postmaster code to launch background
 processes

Much of the code in process_pm_child_exit() to launch replacement
processes when one exits or when progressing to next postmaster state
was unnecessary, because the ServerLoop will launch any missing
background processes anyway. Remove the redundant code and let
ServerLoop handle it.

In ServerLoop, move the code to launch all the processes to a new
subroutine, to group it all together.
---
 src/backend/postmaster/postmaster.c | 278 ++++++++++++----------------
 1 file changed, 121 insertions(+), 157 deletions(-)

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index b0152a0068..d9a2783fb6 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -417,26 +417,12 @@ static void TerminateChildren(int signal);
 
 static int	CountChildren(int target);
 static Backend *assign_backendlist_entry(void);
+static void LaunchMissingBackgroundProcesses(void);
 static void maybe_start_bgworkers(void);
 static bool CreateOptsFile(int argc, char *argv[], char *fullprogname);
 static pid_t StartChildProcess(BackendType type);
 static void StartAutovacuumWorker(void);
-static void MaybeStartWalReceiver(void);
-static void MaybeStartWalSummarizer(void);
 static void InitPostmasterDeathWatchHandle(void);
-static void MaybeStartSlotSyncWorker(void);
-
-/*
- * Archiver is allowed to start up at the current postmaster state?
- *
- * If WAL archiving is enabled always, we are allowed to start archiver
- * even during recovery.
- */
-#define PgArchStartupAllowed()	\
-	(((XLogArchivingActive() && pmState == PM_RUN) ||			\
-	  (XLogArchivingAlways() &&									  \
-	   (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) && \
-	 PgArchCanRestart())
 
 #ifdef WIN32
 #define WNOHANG 0				/* ignored, so any integer value will do */
@@ -1670,53 +1656,11 @@ ServerLoop(void)
 			}
 		}
 
-		/* If we have lost the log collector, try to start a new one */
-		if (SysLoggerPID == 0 && Logging_collector)
-			SysLoggerPID = SysLogger_Start();
-
-		/*
-		 * If no background writer process is running, and we are not in a
-		 * state that prevents it, start one.  It doesn't matter if this
-		 * fails, we'll just try again later.  Likewise for the checkpointer.
-		 */
-		if (pmState == PM_RUN || pmState == PM_RECOVERY ||
-			pmState == PM_HOT_STANDBY || pmState == PM_STARTUP)
-		{
-			if (CheckpointerPID == 0)
-				CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
-			if (BgWriterPID == 0)
-				BgWriterPID = StartChildProcess(B_BG_WRITER);
-		}
-
 		/*
-		 * Likewise, if we have lost the walwriter process, try to start a new
-		 * one.  But this is needed only in normal operation (else we cannot
-		 * be writing any new WAL).
+		 * If we have need to launch any background processes after changing
+		 * state or because some exited, do so now.
 		 */
-		if (WalWriterPID == 0 && pmState == PM_RUN)
-			WalWriterPID = StartChildProcess(B_WAL_WRITER);
-
-		/*
-		 * If we have lost the autovacuum launcher, try to start a new one. We
-		 * don't want autovacuum to run in binary upgrade mode because
-		 * autovacuum might update relfrozenxid for empty tables before the
-		 * physical files are put in place.
-		 */
-		if (!IsBinaryUpgrade && AutoVacPID == 0 &&
-			(AutoVacuumingActive() || start_autovac_launcher) &&
-			pmState == PM_RUN)
-		{
-			AutoVacPID = StartChildProcess(B_AUTOVAC_LAUNCHER);
-			if (AutoVacPID != 0)
-				start_autovac_launcher = false; /* signal processed */
-		}
-
-		/* If we have lost the archiver, try to start a new one. */
-		if (PgArchPID == 0 && PgArchStartupAllowed())
-			PgArchPID = StartChildProcess(B_ARCHIVER);
-
-		/* If we need to start a slot sync worker, try to do that now */
-		MaybeStartSlotSyncWorker();
+		LaunchMissingBackgroundProcesses();
 
 		/* If we need to signal the autovacuum launcher, do so now */
 		if (avlauncher_needs_signal)
@@ -1726,17 +1670,6 @@ ServerLoop(void)
 				kill(AutoVacPID, SIGUSR2);
 		}
 
-		/* If we need to start a WAL receiver, try to do that now */
-		if (WalReceiverRequested)
-			MaybeStartWalReceiver();
-
-		/* If we need to start a WAL summarizer, try to do that now */
-		MaybeStartWalSummarizer();
-
-		/* Get other worker processes running, if needed */
-		if (StartWorkerNeeded || HaveCrashedWorker)
-			maybe_start_bgworkers();
-
 #ifdef HAVE_PTHREAD_IS_THREADED_NP
 
 		/*
@@ -2386,23 +2319,12 @@ process_pm_child_exit(void)
 			connsAllowed = true;
 
 			/*
-			 * Crank up any background tasks that we didn't start earlier
-			 * already.  It doesn't matter if any of these fail, we'll just
-			 * try again later.
+			 * At the next iteration of the postmaster's main loop, we will
+			 * crank up the background tasks like the checkpointer, autovacuum
+			 * launcher, and background workers that were not started earlier
+			 * already.
 			 */
-			if (CheckpointerPID == 0)
-				CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
-			if (BgWriterPID == 0)
-				BgWriterPID = StartChildProcess(B_BG_WRITER);
-			if (WalWriterPID == 0)
-				WalWriterPID = StartChildProcess(B_WAL_WRITER);
-			MaybeStartWalSummarizer();
-			if (!IsBinaryUpgrade && AutoVacuumingActive() && AutoVacPID == 0)
-				AutoVacPID = StartChildProcess(B_AUTOVAC_LAUNCHER);
-			if (PgArchStartupAllowed() && PgArchPID == 0)
-				PgArchPID = StartChildProcess(B_ARCHIVER);
-			MaybeStartSlotSyncWorker();
-			maybe_start_bgworkers();
+			StartWorkerNeeded = true;
 
 			/* at this point we are really open for business */
 			ereport(LOG,
@@ -2541,11 +2463,8 @@ process_pm_child_exit(void)
 		/*
 		 * Was it the archiver?  If exit status is zero (normal) or one (FATAL
 		 * exit), we assume everything is all right just like normal backends
-		 * and just try to restart a new one so that we immediately retry
-		 * archiving remaining files. (If fail, we'll try again in future
-		 * cycles of the postmaster's main loop.) Unless we were waiting for
-		 * it to shut down; don't restart it in that case, and
-		 * PostmasterStateMachine() will advance to the next shutdown step.
+		 * and just try to start a new one on the next cycle of the
+		 * postmaster's main loop, to retry archiving remaining files.
 		 */
 		if (pid == PgArchPID)
 		{
@@ -2553,8 +2472,6 @@ process_pm_child_exit(void)
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("archiver process"));
-			if (PgArchStartupAllowed())
-				PgArchPID = StartChildProcess(B_ARCHIVER);
 			continue;
 		}
 
@@ -3199,6 +3116,116 @@ PostmasterStateMachine(void)
 	}
 }
 
+/*
+ * Check the current pmState and the status of any background processes.  If
+ * there are any background processes missing that should be running in the
+ * current state, but are not, launch them.
+ */
+static void
+LaunchMissingBackgroundProcesses(void)
+{
+	/* If we have lost the log collector, try to start a new one */
+	if (SysLoggerPID == 0 && Logging_collector)
+		SysLoggerPID = SysLogger_Start();
+
+	/*
+	 * If no background writer process is running, and we are not in a state
+	 * that prevents it, start one.  It doesn't matter if this fails, we'll
+	 * just try again later.  Likewise for the checkpointer.
+	 */
+	if (pmState == PM_RUN || pmState == PM_RECOVERY ||
+		pmState == PM_HOT_STANDBY || pmState == PM_STARTUP)
+	{
+		if (CheckpointerPID == 0)
+			CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
+		if (BgWriterPID == 0)
+			BgWriterPID = StartChildProcess(B_BG_WRITER);
+	}
+
+	/*
+	 * Likewise, if we have lost the walwriter process, try to start a new
+	 * one.  But this is needed only in normal operation (else we cannot be
+	 * writing any new WAL).
+	 */
+	if (WalWriterPID == 0 && pmState == PM_RUN)
+		WalWriterPID = StartChildProcess(B_WAL_WRITER);
+
+	/*
+	 * If we have lost the autovacuum launcher, try to start a new one.  We
+	 * don't want autovacuum to run in binary upgrade mode because autovacuum
+	 * might update relfrozenxid for empty tables before the physical files
+	 * are put in place.
+	 */
+	if (!IsBinaryUpgrade && AutoVacPID == 0 &&
+		(AutoVacuumingActive() || start_autovac_launcher) &&
+		pmState == PM_RUN)
+	{
+		AutoVacPID = StartChildProcess(B_AUTOVAC_LAUNCHER);
+		if (AutoVacPID != 0)
+			start_autovac_launcher = false; /* signal processed */
+	}
+
+	/*
+	 * If we have lost the archiver, try to start a new one.
+	 *
+	 * If WAL archiving is enabled always, we are allowed to start archiver
+	 * even during recovery.
+	 */
+	if (PgArchPID == 0 &&
+		((XLogArchivingActive() && pmState == PM_RUN) ||
+		 (XLogArchivingAlways() && (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) &&
+		PgArchCanRestart())
+		PgArchPID = StartChildProcess(B_ARCHIVER);
+
+	/*
+	 * If we need to start a slot sync worker, try to do that now
+	 *
+	 * We allow to start the slot sync worker when we are on a hot standby,
+	 * fast or immediate shutdown is not in progress, slot sync parameters
+	 * are configured correctly, and it is the first time of worker's launch,
+	 * or enough time has passed since the worker was launched last.
+	 */
+	if (SlotSyncWorkerPID == 0 && pmState == PM_HOT_STANDBY &&
+		Shutdown <= SmartShutdown && sync_replication_slots &&
+		ValidateSlotSyncParams(LOG) && SlotSyncWorkerCanRestart())
+		SlotSyncWorkerPID = StartChildProcess(B_SLOTSYNC_WORKER);
+
+	/*
+	 * If we need to start a WAL receiver, try to do that now
+	 *
+	 * Note: if WalReceiverPID is already nonzero, it might seem that we
+	 * should clear WalReceiverRequested.  However, there's a race condition
+	 * if the walreceiver terminates and the startup process immediately
+	 * requests a new one: it's quite possible to get the signal for the
+	 * request before reaping the dead walreceiver process.  Better to risk
+	 * launching an extra walreceiver than to miss launching one we need.
+	 * (The walreceiver code has logic to recognize that it should go away if
+	 * not needed.)
+	 */
+	if (WalReceiverRequested)
+	{
+		if (WalReceiverPID == 0 &&
+			(pmState == PM_STARTUP || pmState == PM_RECOVERY ||
+			 pmState == PM_HOT_STANDBY) &&
+			Shutdown <= SmartShutdown)
+		{
+			WalReceiverPID = StartChildProcess(B_WAL_RECEIVER);
+			if (WalReceiverPID != 0)
+				WalReceiverRequested = false;
+			/* else leave the flag set, so we'll try again later */
+		}
+	}
+
+	/* If we need to start a WAL summarizer, try to do that now */
+	if (summarize_wal && WalSummarizerPID == 0 &&
+		(pmState == PM_RUN || pmState == PM_HOT_STANDBY) &&
+		Shutdown <= SmartShutdown)
+		WalSummarizerPID = StartChildProcess(B_WAL_SUMMARIZER);
+
+	/* Get other worker processes running, if needed */
+	if (StartWorkerNeeded || HaveCrashedWorker)
+		maybe_start_bgworkers();
+}
 
 /*
  * Send a signal to a postmaster child process
@@ -3550,9 +3577,6 @@ process_pm_pmsignal(void)
 		StartWorkerNeeded = true;
 	}
 
-	if (StartWorkerNeeded || HaveCrashedWorker)
-		maybe_start_bgworkers();
-
 	/* Tell syslogger to rotate logfile if requested */
 	if (SysLoggerPID != 0)
 	{
@@ -3592,9 +3616,7 @@ process_pm_pmsignal(void)
 	if (CheckPostmasterSignal(PMSIGNAL_START_WALRECEIVER))
 	{
 		/* Startup Process wants us to start the walreceiver process. */
-		/* Start immediately if possible, else remember request for later. */
 		WalReceiverRequested = true;
-		MaybeStartWalReceiver();
 	}
 
 	/*
@@ -3788,64 +3810,6 @@ StartAutovacuumWorker(void)
 	}
 }
 
-/*
- * MaybeStartWalReceiver
- *		Start the WAL receiver process, if not running and our state allows.
- *
- * Note: if WalReceiverPID is already nonzero, it might seem that we should
- * clear WalReceiverRequested.  However, there's a race condition if the
- * walreceiver terminates and the startup process immediately requests a new
- * one: it's quite possible to get the signal for the request before reaping
- * the dead walreceiver process.  Better to risk launching an extra
- * walreceiver than to miss launching one we need.  (The walreceiver code
- * has logic to recognize that it should go away if not needed.)
- */
-static void
-MaybeStartWalReceiver(void)
-{
-	if (WalReceiverPID == 0 &&
-		(pmState == PM_STARTUP || pmState == PM_RECOVERY ||
-		 pmState == PM_HOT_STANDBY) &&
-		Shutdown <= SmartShutdown)
-	{
-		WalReceiverPID = StartChildProcess(B_WAL_RECEIVER);
-		if (WalReceiverPID != 0)
-			WalReceiverRequested = false;
-		/* else leave the flag set, so we'll try again later */
-	}
-}
-
-/*
- * MaybeStartWalSummarizer
- *		Start the WAL summarizer process, if not running and our state allows.
- */
-static void
-MaybeStartWalSummarizer(void)
-{
-	if (summarize_wal && WalSummarizerPID == 0 &&
-		(pmState == PM_RUN || pmState == PM_HOT_STANDBY) &&
-		Shutdown <= SmartShutdown)
-		WalSummarizerPID = StartChildProcess(B_WAL_SUMMARIZER);
-}
-
-
-/*
- * MaybeStartSlotSyncWorker
- * 		Start the slot sync worker, if not running and our state allows.
- *
- * We allow to start the slot sync worker when we are on a hot standby,
- * fast or immediate shutdown is not in progress, slot sync parameters
- * are configured correctly, and it is the first time of worker's launch,
- * or enough time has passed since the worker was launched last.
- */
-static void
-MaybeStartSlotSyncWorker(void)
-{
-	if (SlotSyncWorkerPID == 0 && pmState == PM_HOT_STANDBY &&
-		Shutdown <= SmartShutdown && sync_replication_slots &&
-		ValidateSlotSyncParams(LOG) && SlotSyncWorkerCanRestart())
-		SlotSyncWorkerPID = StartChildProcess(B_SLOTSYNC_WORKER);
-}
 
 /*
  * Create the opts file
-- 
2.39.2



  [text/x-patch] v3-0005-Add-test-for-connection-limits.patch (6.4K, ../../[email protected]/6-v3-0005-Add-test-for-connection-limits.patch)
  download | inline diff:
From 5d9c11df6ad75ed1cde8de223f9875b729245f37 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 1 Aug 2024 18:24:08 +0300
Subject: [PATCH v3 05/12] Add test for connection limits

---
 src/test/Makefile                             |  2 +-
 src/test/meson.build                          |  1 +
 src/test/postmaster/Makefile                  | 23 ++++++
 src/test/postmaster/README                    | 27 +++++++
 src/test/postmaster/meson.build               | 12 +++
 .../postmaster/t/001_connection_limits.pl     | 79 +++++++++++++++++++
 6 files changed, 143 insertions(+), 1 deletion(-)
 create mode 100644 src/test/postmaster/Makefile
 create mode 100644 src/test/postmaster/README
 create mode 100644 src/test/postmaster/meson.build
 create mode 100644 src/test/postmaster/t/001_connection_limits.pl

diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..abdd6e5a98 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,7 +12,7 @@ subdir = src/test
 top_builddir = ../..
 include $(top_builddir)/src/Makefile.global
 
-SUBDIRS = perl regress isolation modules authentication recovery subscription
+SUBDIRS = perl postmaster regress isolation modules authentication recovery subscription
 
 ifeq ($(with_icu),yes)
 SUBDIRS += icu
diff --git a/src/test/meson.build b/src/test/meson.build
index c3d0dfedf1..67376e4b7f 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -4,6 +4,7 @@ subdir('regress')
 subdir('isolation')
 
 subdir('authentication')
+subdir('postmaster')
 subdir('recovery')
 subdir('subscription')
 subdir('modules')
diff --git a/src/test/postmaster/Makefile b/src/test/postmaster/Makefile
new file mode 100644
index 0000000000..dfcce9c9ee
--- /dev/null
+++ b/src/test/postmaster/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/postmaster
+#
+# Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/postmaster/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/postmaster
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean:
+	rm -rf tmp_check
diff --git a/src/test/postmaster/README b/src/test/postmaster/README
new file mode 100644
index 0000000000..7e47bf5cff
--- /dev/null
+++ b/src/test/postmaster/README
@@ -0,0 +1,27 @@
+src/test/postmaster/README
+
+Regression tests for postmaster
+===============================
+
+This directory contains a test suite for postmaster's handling of
+connections, connection limits, and startup/shutdown sequence.
+
+
+Running the tests
+=================
+
+NOTE: You must have given the --enable-tap-tests argument to configure.
+
+Run
+    make check
+or
+    make installcheck
+You can use "make installcheck" if you previously did "make install".
+In that case, the code in the installation tree is tested.  With
+"make check", a temporary installation tree is built from the current
+sources and then tested.
+
+Either way, this test initializes, starts, and stops a test Postgres
+cluster.
+
+See src/test/perl/README for more info about running these tests.
diff --git a/src/test/postmaster/meson.build b/src/test/postmaster/meson.build
new file mode 100644
index 0000000000..c2de2e0eb5
--- /dev/null
+++ b/src/test/postmaster/meson.build
@@ -0,0 +1,12 @@
+# Copyright (c) 2022-2024, PostgreSQL Global Development Group
+
+tests += {
+  'name': 'postmaster',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_connection_limits.pl',
+    ],
+  },
+}
diff --git a/src/test/postmaster/t/001_connection_limits.pl b/src/test/postmaster/t/001_connection_limits.pl
new file mode 100644
index 0000000000..f50aae4949
--- /dev/null
+++ b/src/test/postmaster/t/001_connection_limits.pl
@@ -0,0 +1,79 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# Test connection limits, i.e. max_connections, reserved_connections
+# and superuser_reserved_connections.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize the server with specific low connection limits
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->append_conf('postgresql.conf', "max_connections = 6");
+$node->append_conf('postgresql.conf', "reserved_connections = 2");
+$node->append_conf('postgresql.conf', "superuser_reserved_connections = 1");
+$node->append_conf('postgresql.conf', "log_connections = on");
+$node->start;
+
+$node->safe_psql(
+	'postgres', qq{
+CREATE USER regress_regular LOGIN;
+CREATE USER regress_reserved LOGIN;
+GRANT pg_use_reserved_connections TO regress_reserved;
+CREATE USER regress_superuser LOGIN SUPERUSER;
+});
+
+# With the limits we set in postgresql.conf, we can establish:
+# - 3 connections for any user with no special privileges
+# - 2 more connections for users belonging to "pg_use_reserved_connections"
+# - 1 more connection for superuser
+
+sub background_psql_as_user
+{
+	my $user = shift;
+
+	return $node->background_psql(
+		'postgres',
+		on_error_die => 1,
+		extra_params => [ '-U', $user ]);
+}
+
+my @sessions = ();
+
+push(@sessions, background_psql_as_user('regress_regular'));
+push(@sessions, background_psql_as_user('regress_regular'));
+push(@sessions, background_psql_as_user('regress_regular'));
+$node->connect_fails(
+	"dbname=postgres user=regress_regular",
+	"reserved_connections limit",
+	expected_stderr =>
+	  qr/FATAL:  remaining connection slots are reserved for roles with privileges of the "pg_use_reserved_connections" role/
+);
+
+push(@sessions, background_psql_as_user('regress_reserved'));
+push(@sessions, background_psql_as_user('regress_reserved'));
+$node->connect_fails(
+	"dbname=postgres user=regress_regular",
+	"reserved_connections limit",
+	expected_stderr =>
+	  qr/FATAL:  remaining connection slots are reserved for roles with the SUPERUSER attribute/
+);
+
+push(@sessions, background_psql_as_user('regress_superuser'));
+$node->connect_fails(
+	"dbname=postgres user=regress_superuser",
+	"superuser_reserved_connections limit",
+	expected_stderr => qr/FATAL:  sorry, too many clients already/);
+
+# TODO: test that query cancellation is still possible
+
+foreach my $session (@sessions)
+{
+	$session->quit;
+}
+
+done_testing();
-- 
2.39.2



  [text/x-patch] v3-0006-Add-test-for-dead-end-backends.patch (3.2K, ../../[email protected]/7-v3-0006-Add-test-for-dead-end-backends.patch)
  download | inline diff:
From 67dca31131f526be4c04feb941b453a320f29830 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 1 Aug 2024 18:36:10 +0300
Subject: [PATCH v3 06/12] Add test for dead-end backends

The code path for launching a dead-end backend because we're out of
slots was not covered by any tests, so add one. (Some tests did hit
the case of launching a dead-end backend because the server is still
starting up, though, so the gap in our test coverage wasn't as big as
it sounds.)
---
 src/test/perl/PostgreSQL/Test/Cluster.pm      | 39 +++++++++++++++++++
 .../postmaster/t/001_connection_limits.pl     | 17 +++++++-
 2 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 32ee98aebc..6d09f9c5f8 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -104,6 +104,7 @@ use File::Path qw(rmtree mkpath);
 use File::Spec;
 use File::stat qw(stat);
 use File::Temp ();
+use IO::Socket::INET;
 use IPC::Run;
 use PostgreSQL::Version;
 use PostgreSQL::Test::RecursiveCopy;
@@ -284,6 +285,44 @@ sub connstr
 	return "port=$pgport host=$pghost dbname='$dbname'";
 }
 
+=pod
+
+=item $node->raw_connect()
+
+Open a raw TCP or Unix domain socket connection to the server. This
+used by low-level protocol and connection limit tests.
+
+=cut
+
+sub raw_connect
+{
+	my ($self) = @_;
+	my $pgport = $self->port;
+	my $pghost = $self->host;
+
+	my $socket;
+	if ($PostgreSQL::Test::Utils::use_unix_sockets)
+	{
+		require IO::Socket::UNIX;
+		my $path = "$pghost/.s.PGSQL.$pgport";
+
+		$socket = IO::Socket::UNIX->new(
+			Type => SOCK_STREAM(),
+			Peer => $path,
+		) or die "Cannot create socket - $IO::Socket::errstr\n";
+	}
+	else
+	{
+		$socket = IO::Socket::INET->new(
+			PeerHost => $pghost,
+			PeerPort => $pgport,
+			Proto => 'tcp'
+		) or die "Cannot create socket - $IO::Socket::errstr\n";
+	}
+	return $socket;
+}
+
+
 =pod
 
 =item $node->group_access()
diff --git a/src/test/postmaster/t/001_connection_limits.pl b/src/test/postmaster/t/001_connection_limits.pl
index f50aae4949..3547b28bdd 100644
--- a/src/test/postmaster/t/001_connection_limits.pl
+++ b/src/test/postmaster/t/001_connection_limits.pl
@@ -43,6 +43,7 @@ sub background_psql_as_user
 }
 
 my @sessions = ();
+my @raw_connections = ();
 
 push(@sessions, background_psql_as_user('regress_regular'));
 push(@sessions, background_psql_as_user('regress_regular'));
@@ -69,11 +70,25 @@ $node->connect_fails(
 	"superuser_reserved_connections limit",
 	expected_stderr => qr/FATAL:  sorry, too many clients already/);
 
-# TODO: test that query cancellation is still possible
+# We can still open TCP (or Unix domain socket) connections, but
+# beyond a certain number (roughly 2x max_connections), they will be
+# "dead-end backends".
+for (my $i = 0; $i <= 20; $i++)
+{
+	push(@raw_connections, $node->raw_connect());
+}
+
+# TODO: test that query cancellation is still possible. A dead-end
+# backend can process a query cancellation packet.
 
+# Clean up
 foreach my $session (@sessions)
 {
 	$session->quit;
 }
+foreach my $socket (@raw_connections)
+{
+	$socket->close();
+}
 
 done_testing();
-- 
2.39.2



  [text/x-patch] v3-0007-Use-an-shmem_exit-callback-to-remove-backend-from.patch (4.9K, ../../[email protected]/8-v3-0007-Use-an-shmem_exit-callback-to-remove-backend-from.patch)
  download | inline diff:
From e71056398cd19c2800e174e9a4356eb2dd5456f1 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 1 Aug 2024 15:58:41 +0300
Subject: [PATCH v3 07/12] Use an shmem_exit callback to remove backend from
 PMChildFlags on exit

This seems nicer than having to duplicate the logic between
InitProcess() and ProcKill() for which child processes have a
PMChildFlags slot.

Move the MarkPostmasterChildActive() call earlier in InitProcess(),
out of the section protected by the spinlock.
---
 src/backend/storage/ipc/pmsignal.c | 10 ++++++--
 src/backend/storage/lmgr/proc.c    | 38 ++++++++++--------------------
 src/include/storage/pmsignal.h     |  1 -
 3 files changed, 21 insertions(+), 28 deletions(-)

diff --git a/src/backend/storage/ipc/pmsignal.c b/src/backend/storage/ipc/pmsignal.c
index 27844b46a2..cb99e77476 100644
--- a/src/backend/storage/ipc/pmsignal.c
+++ b/src/backend/storage/ipc/pmsignal.c
@@ -24,6 +24,7 @@
 #include "miscadmin.h"
 #include "postmaster/postmaster.h"
 #include "replication/walsender.h"
+#include "storage/ipc.h"
 #include "storage/pmsignal.h"
 #include "storage/shmem.h"
 #include "utils/memutils.h"
@@ -121,6 +122,8 @@ postmaster_death_handler(SIGNAL_ARGS)
 
 #endif							/* USE_POSTMASTER_DEATH_SIGNAL */
 
+static void MarkPostmasterChildInactive(int code, Datum arg);
+
 /*
  * PMSignalShmemSize
  *		Compute space needed for pmsignal.c's shared memory
@@ -328,6 +331,9 @@ MarkPostmasterChildActive(void)
 	slot--;
 	Assert(PMSignalState->PMChildFlags[slot] == PM_CHILD_ASSIGNED);
 	PMSignalState->PMChildFlags[slot] = PM_CHILD_ACTIVE;
+
+	/* Arrange to clean up at exit. */
+	on_shmem_exit(MarkPostmasterChildInactive, 0);
 }
 
 /*
@@ -352,8 +358,8 @@ MarkPostmasterChildWalSender(void)
  * MarkPostmasterChildInactive - mark a postmaster child as done using
  * shared memory.  This is called in the child process.
  */
-void
-MarkPostmasterChildInactive(void)
+static void
+MarkPostmasterChildInactive(int code, Datum arg)
 {
 	int			slot = MyPMChildSlot;
 
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 1b23efb26f..37b1c67600 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -307,6 +307,19 @@ InitProcess(void)
 	if (MyProc != NULL)
 		elog(ERROR, "you already exist");
 
+	/*
+	 * Before we start accessing the shared memory in a serious way, mark
+	 * ourselves as an active postmaster child; this is so that the postmaster
+	 * can detect it if we exit without cleaning up.  (XXX autovac launcher
+	 * currently doesn't participate in this; it probably should.)
+	 *
+	 * Slot sync worker also does not participate in it, see comments atop
+	 * 'struct bkend' in postmaster.c.
+	 */
+	if (IsUnderPostmaster && !AmAutoVacuumLauncherProcess() &&
+		!AmLogicalSlotSyncWorkerProcess())
+		MarkPostmasterChildActive();
+
 	/* Decide which list should supply our PGPROC. */
 	if (AmAutoVacuumLauncherProcess() || AmAutoVacuumWorkerProcess())
 		procgloballist = &ProcGlobal->autovacFreeProcs;
@@ -359,19 +372,6 @@ InitProcess(void)
 	 */
 	Assert(MyProc->procgloballist == procgloballist);
 
-	/*
-	 * Now that we have a PGPROC, mark ourselves as an active postmaster
-	 * child; this is so that the postmaster can detect it if we exit without
-	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
-	 * this; it probably should.)
-	 *
-	 * Slot sync worker also does not participate in it, see comments atop
-	 * 'struct bkend' in postmaster.c.
-	 */
-	if (IsUnderPostmaster && !AmAutoVacuumLauncherProcess() &&
-		!AmLogicalSlotSyncWorkerProcess())
-		MarkPostmasterChildActive();
-
 	/*
 	 * Initialize all fields of MyProc, except for those previously
 	 * initialized by InitProcGlobal.
@@ -941,18 +941,6 @@ ProcKill(int code, Datum arg)
 
 	SpinLockRelease(ProcStructLock);
 
-	/*
-	 * This process is no longer present in shared memory in any meaningful
-	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
-	 * autovac launcher should be included here someday)
-	 *
-	 * Slot sync worker is also not a postmaster child, so skip this shared
-	 * memory related processing here.
-	 */
-	if (IsUnderPostmaster && !AmAutoVacuumLauncherProcess() &&
-		!AmLogicalSlotSyncWorkerProcess())
-		MarkPostmasterChildInactive();
-
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
 	if (AutovacuumLauncherPid != 0)
 		kill(AutovacuumLauncherPid, SIGUSR2);
diff --git a/src/include/storage/pmsignal.h b/src/include/storage/pmsignal.h
index 0c9a7e32a8..3b9336b83c 100644
--- a/src/include/storage/pmsignal.h
+++ b/src/include/storage/pmsignal.h
@@ -74,7 +74,6 @@ extern int	AssignPostmasterChildSlot(void);
 extern bool ReleasePostmasterChildSlot(int slot);
 extern bool IsPostmasterChildWalSender(int slot);
 extern void MarkPostmasterChildActive(void);
-extern void MarkPostmasterChildInactive(void);
 extern void MarkPostmasterChildWalSender(void);
 extern bool PostmasterIsAliveInternal(void);
 extern void PostmasterDeathSignalInit(void);
-- 
2.39.2



  [text/x-patch] v3-0008-Introduce-a-separate-BackendType-for-dead-end-chi.patch (14.7K, ../../[email protected]/9-v3-0008-Introduce-a-separate-BackendType-for-dead-end-chi.patch)
  download | inline diff:
From ae9d3e8646a250bc93fc50bded359c703baa9977 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 1 Aug 2024 17:24:12 +0300
Subject: [PATCH v3 08/12] Introduce a separate BackendType for dead-end
 children

And replace postmaster.c's own "backend type" codes with BackendType

XXX: While working on this, many times I accidentally did something
like "foo |= B_SOMETHING" instead of "foo |= 1 << B_SOMETHING", when
constructing arguments to SignalSomeChildren or CountChildren, and
things broke in very subtle ways taking a long time to debug. The old
constants that were already bitmasks avoided that. Maybe we need some
macro magic or something to make this less error-prone.
---
 src/backend/postmaster/postmaster.c    | 106 ++++++++++++-------------
 src/backend/utils/activity/pgstat_io.c |   3 +
 src/backend/utils/init/miscinit.c      |   3 +
 src/include/miscadmin.h                |   1 +
 4 files changed, 56 insertions(+), 57 deletions(-)

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d9a2783fb6..2e917c5ea9 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -129,15 +129,11 @@
 
 
 /*
- * Possible types of a backend. Beyond being the possible bkend_type values in
- * struct bkend, these are OR-able request flag bits for SignalSomeChildren()
- * and CountChildren().
+ * CountChildren and SignalSomeChildren use a uint32 bitmask argument to
+ * represent BackendTypes to count or signal.
  */
-#define BACKEND_TYPE_NORMAL		0x0001	/* normal backend */
-#define BACKEND_TYPE_AUTOVAC	0x0002	/* autovacuum worker process */
-#define BACKEND_TYPE_WALSND		0x0004	/* walsender process */
-#define BACKEND_TYPE_BGWORKER	0x0008	/* bgworker process */
-#define BACKEND_TYPE_ALL		0x000F	/* OR of all the above */
+#define BACKEND_TYPE_ALL 0xffffffff
+StaticAssertDecl(BACKEND_NUM_TYPES < 32, "too many backend types for uint32");
 
 /*
  * List of active backends (or child processes anyway; we don't actually
@@ -148,7 +144,7 @@
  * As shown in the above set of backend types, this list includes not only
  * "normal" client sessions, but also autovacuum workers, walsenders, and
  * background workers.  (Note that at the time of launch, walsenders are
- * labeled BACKEND_TYPE_NORMAL; we relabel them to BACKEND_TYPE_WALSND
+ * labeled B_BACKEND; we relabel them to B_WAL_SENDER
  * upon noticing they've changed their PMChildFlags entry.  Hence that check
  * must be done before any operation that needs to distinguish walsenders
  * from normal backends.)
@@ -157,7 +153,8 @@
  * the purpose of sending a friendly rejection message to a would-be client.
  * We must track them because they are attached to shared memory, but we know
  * they will never become live backends.  dead_end children are not assigned a
- * PMChildSlot.  dead_end children have bkend_type NORMAL.
+ * PMChildSlot.  dead_end children have bkend_type B_DEAD_END_BACKEND.
+ * FIXME: a dead-end backend can send query cancel?
  *
  * "Special" children such as the startup, bgwriter, autovacuum launcher, and
  * slot sync worker tasks are not in this list.  They are tracked via StartupPID
@@ -169,8 +166,7 @@ typedef struct bkend
 {
 	pid_t		pid;			/* process id of backend */
 	int			child_slot;		/* PMChildSlot for this backend, if any */
-	int			bkend_type;		/* child process flavor, see above */
-	bool		dead_end;		/* is it going to send an error and quit? */
+	BackendType bkend_type;		/* child process flavor, see above */
 	RegisteredBgWorker *rw;		/* bgworker info, if this is a bgworker */
 	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
 	dlist_node	elem;			/* list link in BackendList */
@@ -410,12 +406,13 @@ static void report_fork_failure_to_client(ClientSocket *client_sock, int errnum)
 static CAC_state canAcceptConnections(int backend_type);
 static void signal_child(pid_t pid, int signal);
 static void sigquit_child(pid_t pid);
-static bool SignalSomeChildren(int signal, int target);
+static bool SignalSomeChildren(int signal, uint32 targetMask);
 static void TerminateChildren(int signal);
 
-#define SignalChildren(sig)			   SignalSomeChildren(sig, BACKEND_TYPE_ALL)
+#define SignalChildren(sig)		\
+	SignalSomeChildren(sig, BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND))
 
-static int	CountChildren(int target);
+static int	CountChildren(uint32 targetMask);
 static Backend *assign_backendlist_entry(void);
 static void LaunchMissingBackgroundProcesses(void);
 static void maybe_start_bgworkers(void);
@@ -1765,7 +1762,7 @@ canAcceptConnections(int backend_type)
 	 * bgworker_should_start_now() decided whether the DB state allows them.
 	 */
 	if (pmState != PM_RUN && pmState != PM_HOT_STANDBY &&
-		backend_type != BACKEND_TYPE_BGWORKER)
+		backend_type != B_BG_WORKER)
 	{
 		if (Shutdown > NoShutdown)
 			return CAC_SHUTDOWN;	/* shutdown is pending */
@@ -1782,7 +1779,7 @@ canAcceptConnections(int backend_type)
 	 * "Smart shutdown" restrictions are applied only to normal connections,
 	 * not to autovac workers or bgworkers.
 	 */
-	if (!connsAllowed && backend_type == BACKEND_TYPE_NORMAL)
+	if (!connsAllowed && backend_type == B_BACKEND)
 		return CAC_SHUTDOWN;	/* shutdown is pending */
 
 	/*
@@ -1797,7 +1794,7 @@ canAcceptConnections(int backend_type)
 	 * The limit here must match the sizes of the per-child-process arrays;
 	 * see comments for MaxLivePostmasterChildren().
 	 */
-	if (CountChildren(BACKEND_TYPE_ALL) >= MaxLivePostmasterChildren())
+	if (CountChildren(BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND)) >= MaxLivePostmasterChildren())
 		result = CAC_TOOMANY;
 
 	return result;
@@ -2554,11 +2551,11 @@ CleanupBackend(Backend *bp,
 	bool		crashed = false;
 
 	/* Construct a process name for log message */
-	if (bp->dead_end)
+	if (bp->bkend_type == B_DEAD_END_BACKEND)
 	{
 		procname = _("dead end backend");
 	}
-	else if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
+	else if (bp->bkend_type == B_BG_WORKER)
 	{
 		snprintf(namebuf, MAXPGPATH, _("background worker \"%s\""),
 				 bp->rw->rw_worker.bgw_type);
@@ -2596,7 +2593,7 @@ CleanupBackend(Backend *bp,
 	 * If the process attached to shared memory, check that it detached
 	 * cleanly.
 	 */
-	if (!bp->dead_end)
+	if (bp->bkend_type != B_DEAD_END_BACKEND)
 	{
 		if (!ReleasePostmasterChildSlot(bp->child_slot))
 		{
@@ -2628,7 +2625,7 @@ CleanupBackend(Backend *bp,
 	/*
 	 * If it was a background worker, also update its RegisteredWorker entry.
 	 */
-	if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
+	if (bp->bkend_type == B_BG_WORKER)
 	{
 		RegisteredBgWorker *rw = bp->rw;
 
@@ -2851,7 +2848,7 @@ PostmasterStateMachine(void)
 			 * This state ends when we have no normal client backends running.
 			 * Then we're ready to stop other children.
 			 */
-			if (CountChildren(BACKEND_TYPE_NORMAL) == 0)
+			if (CountChildren(1 << B_BACKEND) == 0)
 				pmState = PM_STOP_BACKENDS;
 		}
 	}
@@ -2872,7 +2869,7 @@ PostmasterStateMachine(void)
 
 		/* Signal all backend children except walsenders */
 		SignalSomeChildren(SIGTERM,
-						   BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND);
+						   BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND));
 		/* and the autovac launcher too */
 		if (AutoVacPID != 0)
 			signal_child(AutoVacPID, SIGTERM);
@@ -2914,7 +2911,7 @@ PostmasterStateMachine(void)
 		 * here. Walsenders and archiver are also disregarded, they will be
 		 * terminated later after writing the checkpoint record.
 		 */
-		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
+		if (CountChildren(BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND)) == 0 &&
 			StartupPID == 0 &&
 			WalReceiverPID == 0 &&
 			WalSummarizerPID == 0 &&
@@ -2988,7 +2985,7 @@ PostmasterStateMachine(void)
 		 * left by now anyway; what we're really waiting for is walsenders and
 		 * archiver.
 		 */
-		if (PgArchPID == 0 && CountChildren(BACKEND_TYPE_ALL) == 0)
+		if (PgArchPID == 0 && CountChildren(BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND)) == 0)
 		{
 			pmState = PM_WAIT_DEAD_END;
 		}
@@ -3285,10 +3282,10 @@ sigquit_child(pid_t pid)
 
 /*
  * Send a signal to the targeted children (but NOT special children;
- * dead_end children are never signaled, either).
+ * dead_end children are never signaled, either XXX).
  */
 static bool
-SignalSomeChildren(int signal, int target)
+SignalSomeChildren(int signal, uint32 targetMask)
 {
 	dlist_iter	iter;
 	bool		signaled = false;
@@ -3297,24 +3294,21 @@ SignalSomeChildren(int signal, int target)
 	{
 		Backend    *bp = dlist_container(Backend, elem, iter.cur);
 
-		if (bp->dead_end)
-			continue;
-
 		/*
 		 * Since target == BACKEND_TYPE_ALL is the most common case, we test
 		 * it first and avoid touching shared memory for every child.
 		 */
-		if (target != BACKEND_TYPE_ALL)
+		if (targetMask != BACKEND_TYPE_ALL)
 		{
 			/*
 			 * Assign bkend_type for any recently announced WAL Sender
 			 * processes.
 			 */
-			if (bp->bkend_type == BACKEND_TYPE_NORMAL &&
+			if (bp->bkend_type == B_BACKEND &&
 				IsPostmasterChildWalSender(bp->child_slot))
-				bp->bkend_type = BACKEND_TYPE_WALSND;
+				bp->bkend_type = B_WAL_SENDER;
 
-			if (!(target & bp->bkend_type))
+			if ((targetMask & (1 << bp->bkend_type)) == 0)
 				continue;
 		}
 
@@ -3387,17 +3381,22 @@ BackendStartup(ClientSocket *client_sock)
 	}
 
 	/* Pass down canAcceptConnections state */
-	startup_data.canAcceptConnections = canAcceptConnections(BACKEND_TYPE_NORMAL);
-	bn->dead_end = (startup_data.canAcceptConnections != CAC_OK);
+	startup_data.canAcceptConnections = canAcceptConnections(B_BACKEND);
 	bn->rw = NULL;
 
 	/*
 	 * Unless it's a dead_end child, assign it a child slot number
 	 */
-	if (!bn->dead_end)
+	if (startup_data.canAcceptConnections == CAC_OK)
+	{
+		bn->bkend_type = B_BACKEND;	/* Can change later to WALSND */
 		bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
+	}
 	else
+	{
+		bn->bkend_type = B_DEAD_END_BACKEND;
 		bn->child_slot = 0;
+	}
 
 	/* Hasn't asked to be notified about any bgworkers yet */
 	bn->bgworker_notify = false;
@@ -3410,7 +3409,7 @@ BackendStartup(ClientSocket *client_sock)
 		/* in parent, fork failed */
 		int			save_errno = errno;
 
-		if (!bn->dead_end)
+		if (bn->child_slot != 0)
 			(void) ReleasePostmasterChildSlot(bn->child_slot);
 		pfree(bn);
 		errno = save_errno;
@@ -3430,7 +3429,6 @@ BackendStartup(ClientSocket *client_sock)
 	 * of backends.
 	 */
 	bn->pid = pid;
-	bn->bkend_type = BACKEND_TYPE_NORMAL;	/* Can change later to WALSND */
 	dlist_push_head(&BackendList, &bn->elem);
 
 	return STATUS_OK;
@@ -3664,11 +3662,10 @@ dummy_handler(SIGNAL_ARGS)
 }
 
 /*
- * Count up number of child processes of specified types (dead_end children
- * are always excluded).
+ * Count up number of child processes of specified types.
  */
 static int
-CountChildren(int target)
+CountChildren(uint32 targetMask)
 {
 	dlist_iter	iter;
 	int			cnt = 0;
@@ -3677,24 +3674,21 @@ CountChildren(int target)
 	{
 		Backend    *bp = dlist_container(Backend, elem, iter.cur);
 
-		if (bp->dead_end)
-			continue;
-
 		/*
 		 * Since target == BACKEND_TYPE_ALL is the most common case, we test
 		 * it first and avoid touching shared memory for every child.
 		 */
-		if (target != BACKEND_TYPE_ALL)
+		if (targetMask != BACKEND_TYPE_ALL)
 		{
 			/*
 			 * Assign bkend_type for any recently announced WAL Sender
 			 * processes.
 			 */
-			if (bp->bkend_type == BACKEND_TYPE_NORMAL &&
+			if (bp->bkend_type == B_BACKEND &&
 				IsPostmasterChildWalSender(bp->child_slot))
-				bp->bkend_type = BACKEND_TYPE_WALSND;
+				bp->bkend_type = B_WAL_SENDER;
 
-			if (!(target & bp->bkend_type))
+			if ((targetMask & (1 << bp->bkend_type)) == 0)
 				continue;
 		}
 
@@ -3761,13 +3755,13 @@ StartAutovacuumWorker(void)
 	 * we have to check to avoid race-condition problems during DB state
 	 * changes.
 	 */
-	if (canAcceptConnections(BACKEND_TYPE_AUTOVAC) == CAC_OK)
+	if (canAcceptConnections(B_AUTOVAC_WORKER) == CAC_OK)
 	{
 		bn = (Backend *) palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
 		if (bn)
 		{
-			/* Autovac workers are not dead_end and need a child slot */
-			bn->dead_end = false;
+			/* Autovac workers need a child slot */
+			bn->bkend_type = B_AUTOVAC_WORKER;
 			bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
 			bn->bgworker_notify = false;
 			bn->rw = NULL;
@@ -3775,7 +3769,6 @@ StartAutovacuumWorker(void)
 			bn->pid = StartChildProcess(B_AUTOVAC_WORKER);
 			if (bn->pid > 0)
 			{
-				bn->bkend_type = BACKEND_TYPE_AUTOVAC;
 				dlist_push_head(&BackendList, &bn->elem);
 				/* all OK */
 				return;
@@ -3981,7 +3974,7 @@ assign_backendlist_entry(void)
 	 * only possible failure is CAC_TOOMANY, so we just log an error message
 	 * based on that rather than checking the error code precisely.
 	 */
-	if (canAcceptConnections(BACKEND_TYPE_BGWORKER) != CAC_OK)
+	if (canAcceptConnections(B_BG_WORKER) != CAC_OK)
 	{
 		ereport(LOG,
 				(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
@@ -3999,8 +3992,7 @@ assign_backendlist_entry(void)
 	}
 
 	bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
-	bn->bkend_type = BACKEND_TYPE_BGWORKER;
-	bn->dead_end = false;
+	bn->bkend_type = B_BG_WORKER;
 	bn->bgworker_notify = false;
 
 	return bn;
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 8af55989ee..9bad1040d6 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -312,6 +312,8 @@ pgstat_io_snapshot_cb(void)
 *
 * The following BackendTypes do not participate in the cumulative stats
 * subsystem or do not perform IO on which we currently track:
+* - Dead-end backend because it is not connected to shared memory and
+*   doesn't do any IO
 * - Syslogger because it is not connected to shared memory
 * - Archiver because most relevant archiving IO is delegated to a
 *   specialized command or module
@@ -334,6 +336,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 	switch (bktype)
 	{
 		case B_INVALID:
+		case B_DEAD_END_BACKEND:
 		case B_ARCHIVER:
 		case B_LOGGER:
 		case B_WAL_RECEIVER:
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 537d92c0cf..ae8b1a4331 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -281,6 +281,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_BACKEND:
 			backendDesc = "client backend";
 			break;
+		case B_DEAD_END_BACKEND:
+			backendDesc = "dead-end client backend";
+			break;
 		case B_BG_WORKER:
 			backendDesc = "background worker";
 			break;
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index ac16233b71..b21c4d43b9 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -337,6 +337,7 @@ typedef enum BackendType
 
 	/* Backends and other backend-like processes */
 	B_BACKEND,
+	B_DEAD_END_BACKEND,
 	B_AUTOVAC_LAUNCHER,
 	B_AUTOVAC_WORKER,
 	B_BG_WORKER,
-- 
2.39.2



  [text/x-patch] v3-0009-Kill-dead-end-children-when-there-s-nothing-else-.patch (7.5K, ../../[email protected]/10-v3-0009-Kill-dead-end-children-when-there-s-nothing-else-.patch)
  download | inline diff:
From a425f6f89afcf2bc9cf95b291bb17084bf79d21e Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 1 Aug 2024 18:40:20 +0300
Subject: [PATCH v3 09/12] Kill dead-end children when there's nothing else
 left

Previously, the postmaster would never try to kill dead-end child
processes, even if there was no other processes left. A dead-end
backend will eventually exit, when authentication_timeout expires, but
if a dead-end backend is the only thing that's preventing the server
from shutting down, it seems better to kill it immediately. It's
particularly important, if there was a bug in the early startup code
that prevented a dead-end child from timing out and exiting normally.

Includes a test for that case where a dead-end backend previously kept
the server from shutting down.
---
 src/backend/postmaster/postmaster.c     | 35 +++++++-------
 src/test/postmaster/meson.build         |  1 +
 src/test/postmaster/t/002_start_stop.pl | 64 +++++++++++++++++++++++++
 3 files changed, 81 insertions(+), 19 deletions(-)
 create mode 100644 src/test/postmaster/t/002_start_stop.pl

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2e917c5ea9..bfadb995cb 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -409,9 +409,6 @@ static void sigquit_child(pid_t pid);
 static bool SignalSomeChildren(int signal, uint32 targetMask);
 static void TerminateChildren(int signal);
 
-#define SignalChildren(sig)		\
-	SignalSomeChildren(sig, BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND))
-
 static int	CountChildren(uint32 targetMask);
 static Backend *assign_backendlist_entry(void);
 static void LaunchMissingBackgroundProcesses(void);
@@ -1963,7 +1960,7 @@ process_pm_reload_request(void)
 		ereport(LOG,
 				(errmsg("received SIGHUP, reloading configuration files")));
 		ProcessConfigFile(PGC_SIGHUP);
-		SignalChildren(SIGHUP);
+		SignalSomeChildren(SIGHUP, BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND));
 		if (StartupPID != 0)
 			signal_child(StartupPID, SIGHUP);
 		if (BgWriterPID != 0)
@@ -2382,7 +2379,7 @@ process_pm_child_exit(void)
 				 * Waken walsenders for the last time. No regular backends
 				 * should be around anymore.
 				 */
-				SignalChildren(SIGUSR2);
+				SignalSomeChildren(SIGUSR2, BACKEND_TYPE_ALL & (1 << B_DEAD_END_BACKEND));
 
 				pmState = PM_SHUTDOWN_2;
 			}
@@ -2867,7 +2864,7 @@ PostmasterStateMachine(void)
 		 */
 		ForgetUnstartedBackgroundWorkers();
 
-		/* Signal all backend children except walsenders */
+		/* Signal all backend children except walsenders and dead-end backends */
 		SignalSomeChildren(SIGTERM,
 						   BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND));
 		/* and the autovac launcher too */
@@ -2925,10 +2922,11 @@ PostmasterStateMachine(void)
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
 				/*
-				 * Start waiting for dead_end children to die.  This state
-				 * change causes ServerLoop to stop creating new ones.
+				 * Stop any dead_end children and stop creating new ones.
 				 */
 				pmState = PM_WAIT_DEAD_END;
+				ConfigurePostmasterWaitSet(false);
+				SignalSomeChildren(SIGQUIT, 1 << B_DEAD_END_BACKEND);
 
 				/*
 				 * We already SIGQUIT'd the archiver and stats processes, if
@@ -2967,9 +2965,10 @@ PostmasterStateMachine(void)
 					 */
 					FatalError = true;
 					pmState = PM_WAIT_DEAD_END;
+					ConfigurePostmasterWaitSet(false);
 
-					/* Kill the walsenders and archiver too */
-					SignalChildren(SIGQUIT);
+					/* Kill the walsenders and archiver, too */
+					SignalSomeChildren(SIGQUIT, BACKEND_TYPE_ALL);
 					if (PgArchPID != 0)
 						signal_child(PgArchPID, SIGQUIT);
 				}
@@ -2987,15 +2986,14 @@ PostmasterStateMachine(void)
 		 */
 		if (PgArchPID == 0 && CountChildren(BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND)) == 0)
 		{
+			ConfigurePostmasterWaitSet(false);
+			SignalSomeChildren(SIGTERM, 1 << B_DEAD_END_BACKEND);
 			pmState = PM_WAIT_DEAD_END;
 		}
 	}
 
 	if (pmState == PM_WAIT_DEAD_END)
 	{
-		/* Don't allow any new socket connection events. */
-		ConfigurePostmasterWaitSet(false);
-
 		/*
 		 * PM_WAIT_DEAD_END state ends when the BackendList is entirely empty
 		 * (ie, no dead_end children remain), and the archiver is gone too.
@@ -3281,8 +3279,7 @@ sigquit_child(pid_t pid)
 }
 
 /*
- * Send a signal to the targeted children (but NOT special children;
- * dead_end children are never signaled, either XXX).
+ * Send a signal to the targeted children (but NOT special children).
  */
 static bool
 SignalSomeChildren(int signal, uint32 targetMask)
@@ -3313,8 +3310,8 @@ SignalSomeChildren(int signal, uint32 targetMask)
 		}
 
 		ereport(DEBUG4,
-				(errmsg_internal("sending signal %d to process %d",
-								 signal, (int) bp->pid)));
+				(errmsg_internal("sending signal %d to %s process %d",
+								 signal, GetBackendTypeDesc(bp->bkend_type), (int) bp->pid)));
 		signal_child(bp->pid, signal);
 		signaled = true;
 	}
@@ -3323,12 +3320,12 @@ SignalSomeChildren(int signal, uint32 targetMask)
 
 /*
  * Send a termination signal to children.  This considers all of our children
- * processes, except syslogger and dead_end backends.
+ * processes, except syslogger.
  */
 static void
 TerminateChildren(int signal)
 {
-	SignalChildren(signal);
+	SignalSomeChildren(signal, BACKEND_TYPE_ALL);
 	if (StartupPID != 0)
 	{
 		signal_child(StartupPID, signal);
diff --git a/src/test/postmaster/meson.build b/src/test/postmaster/meson.build
index c2de2e0eb5..2d89adf520 100644
--- a/src/test/postmaster/meson.build
+++ b/src/test/postmaster/meson.build
@@ -7,6 +7,7 @@ tests += {
   'tap': {
     'tests': [
       't/001_connection_limits.pl',
+      't/002_start_stop.pl',
     ],
   },
 }
diff --git a/src/test/postmaster/t/002_start_stop.pl b/src/test/postmaster/t/002_start_stop.pl
new file mode 100644
index 0000000000..6f114659fa
--- /dev/null
+++ b/src/test/postmaster/t/002_start_stop.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# XXX
+# XXX
+
+use IO::Socket::INET;
+use IO::Socket::UNIX;
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(time);
+
+# Initialize the server with low connection limits, to test dead-end backends
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->append_conf('postgresql.conf', "max_connections = 5");
+$node->append_conf('postgresql.conf', "log_connections = on");
+$node->append_conf('postgresql.conf', "log_min_messages = debug2");
+
+# XX
+$node->append_conf('postgresql.conf', "authentication_timeout = '120 s'");
+
+$node->start;
+
+my @sessions = ();
+my @raw_connections = ();
+
+#for (my $i=0; $i <= 5; $i++) {
+#	push(@sessions, $node->background_psql('postgres', on_error_die => 1));
+#}
+#$node->connect_fails("dbname=postgres", "max_connections reached",
+#					 expected_stderr => qr/FATAL:  sorry, too many clients already/);
+
+# We can still open TCP (or Unix domain socket) connections, but beyond a
+# certain number (roughly 2x max_connections), they will be "dead-end backends"
+for (my $i = 0; $i <= 20; $i++)
+{
+	push(@raw_connections, $node->raw_connect());
+}
+
+# Test that the dead-end backends don't prevent the server from stopping.
+my $before = time();
+$node->stop();
+my $elapsed = time() - $before;
+ok($elapsed < 60);
+
+$node->start();
+
+$node->connect_ok("dbname=postgres", "works after restart");
+
+# Clean up
+foreach my $session (@sessions)
+{
+	$session->quit;
+}
+foreach my $socket (@raw_connections)
+{
+	$socket->close();
+}
+
+done_testing();
-- 
2.39.2



  [text/x-patch] v3-0010-Assign-a-child-slot-to-every-postmaster-child-pro.patch (64.8K, ../../[email protected]/11-v3-0010-Assign-a-child-slot-to-every-postmaster-child-pro.patch)
  download | inline diff:
From 7cf06ed8729c6be02a9fbc77f595491683dd0651 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 1 Aug 2024 17:54:23 +0300
Subject: [PATCH v3 10/12] Assign a child slot to every postmaster child
 process

Previously, only backends, autovacuum workers, and background workers
had an entry in the PMChildFlags array. With this commit, all
postmaster child processes, including all the aux processes, have an
entry.

We now maintain separate free-lists for different kinds of
backends. That ensures that there are always slots available for
autovacuum and background workers. Previously, pre-authorization
backends could prevent autovacuum or background workers from starting
up, by using up all the slots.

The code to manage the slots in the postmaster process is in a new
pmchild.c source file. Because postmaster.c is just so large.

Assigning pmsignal slot numbers is now pmchild.c's responsibility.
This replaces the PMChildInUse array in pmsignal.c.
---
 src/backend/postmaster/Makefile         |   1 +
 src/backend/postmaster/launch_backend.c |   1 +
 src/backend/postmaster/meson.build      |   1 +
 src/backend/postmaster/pmchild.c        | 287 ++++++++++
 src/backend/postmaster/postmaster.c     | 711 ++++++++++--------------
 src/backend/storage/ipc/pmsignal.c      |  83 +--
 src/backend/storage/lmgr/proc.c         |  12 +-
 src/include/postmaster/postmaster.h     |  40 ++
 src/include/storage/pmsignal.h          |   2 +-
 src/tools/pgindent/typedefs.list        |   2 +-
 10 files changed, 653 insertions(+), 487 deletions(-)
 create mode 100644 src/backend/postmaster/pmchild.c

diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index db08543d19..c977d91785 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -23,6 +23,7 @@ OBJS = \
 	launch_backend.o \
 	pgarch.o \
 	postmaster.o \
+	pmchild.o \
 	startup.o \
 	syslogger.o \
 	walsummarizer.o \
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index 0ae23fdf55..b0b91dc97f 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -182,6 +182,7 @@ static child_process_kind child_process_kinds[] = {
 	[B_INVALID] = {"invalid", NULL, false},
 
 	[B_BACKEND] = {"backend", BackendMain, true},
+	[B_DEAD_END_BACKEND] = {"dead-end backend", BackendMain, true},
 	[B_AUTOVAC_LAUNCHER] = {"autovacuum launcher", AutoVacLauncherMain, true},
 	[B_AUTOVAC_WORKER] = {"autovacuum worker", AutoVacWorkerMain, true},
 	[B_BG_WORKER] = {"bgworker", BackgroundWorkerMain, true},
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index 0ea4bbe084..388848bb52 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'launch_backend.c',
   'pgarch.c',
   'postmaster.c',
+  'pmchild.c',
   'startup.c',
   'syslogger.c',
   'walsummarizer.c',
diff --git a/src/backend/postmaster/pmchild.c b/src/backend/postmaster/pmchild.c
new file mode 100644
index 0000000000..e86982d6d1
--- /dev/null
+++ b/src/backend/postmaster/pmchild.c
@@ -0,0 +1,287 @@
+/*-------------------------------------------------------------------------
+ *
+ * pmchild.c
+ *	  Functions for keeping track of postmaster child processes.
+ *
+ * Keep track of all child processes, so that when a process exits, we know
+ * kind of a process it was and can clean up accordingly.  Every child process
+ * is allocated a PMChild struct, from a fixed pool of structs.  The size of
+ * the pool is determined by various settings that configure how many worker
+ * processes and backend connections are allowed, i.e. autovacuum_max_workers,
+ * max_worker_processes, max_wal_senders, and max_connections.
+ *
+ * The structures and functions in this file are private to the postmaster
+ * process.  But note that there is an array in shared memory, managed by
+ * pmsignal.c, that mirrors this.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/postmaster/pmchild.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "miscadmin.h"
+#include "postmaster/autovacuum.h"
+#include "postmaster/postmaster.h"
+#include "replication/walsender.h"
+#include "storage/pmsignal.h"
+#include "storage/proc.h"
+
+/*
+ * Freelists for different kinds of child processes.  We maintain separate
+ * pools for them, so that launching a lot of backends cannot exchaust all the
+ * slots, and prevent autovacuum or an aux process from launching.
+ */
+static dlist_head freeBackendList;
+static dlist_head freeAutoVacWorkerList;
+static dlist_head freeBgWorkerList;
+static dlist_head freeAuxList;
+
+/*
+ * List of active child processes.  This includes dead-end children.
+ */
+dlist_head	ActiveChildList;
+
+/*
+ * MaxLivePostmasterChildren
+ *
+ * This reports the number postmaster child processes that can be active.  It
+ * includes all children except for dead_end children.  This allows the array
+ * in shared memory (PMChildFlags) to have a fixed maximum size.
+ */
+int
+MaxLivePostmasterChildren(void)
+{
+	int			n = 0;
+
+	/* We know exactly how mamy worker and aux processes can be active */
+	n += autovacuum_max_workers;
+	n += max_worker_processes;
+	n += NUM_AUXILIARY_PROCS;
+
+	/*
+	 * We allow more connections here than we can have backends because some
+	 * might still be authenticating; they might fail auth, or some existing
+	 * backend might exit before the auth cycle is completed.  The exact
+	 * MaxBackends limit is enforced when a new backend tries to join the
+	 * shared-inval backend array.
+	 */
+	n += 2 * (MaxConnections + max_wal_senders);
+
+	return n;
+}
+
+static void
+init_slot(PMChild *pmchild, int slotno, dlist_head *freelist)
+{
+	pmchild->pid = 0;
+	pmchild->child_slot = slotno + 1;
+	pmchild->bkend_type = B_INVALID;
+	pmchild->rw = NULL;
+	pmchild->bgworker_notify = false;
+	dlist_push_tail(freelist, &pmchild->elem);
+}
+
+/*
+ * Initialize at postmaster startup
+ */
+void
+InitPostmasterChildSlots(void)
+{
+	int			num_pmchild_slots;
+	int			slotno;
+	PMChild    *slots;
+
+	dlist_init(&freeBackendList);
+	dlist_init(&freeAutoVacWorkerList);
+	dlist_init(&freeBgWorkerList);
+	dlist_init(&freeAuxList);
+	dlist_init(&ActiveChildList);
+
+	num_pmchild_slots = MaxLivePostmasterChildren();
+
+	slots = palloc(num_pmchild_slots * sizeof(PMChild));
+
+	slotno = 0;
+	for (int i = 0; i < 2 * (MaxConnections + max_wal_senders); i++)
+	{
+		init_slot(&slots[slotno], slotno, &freeBackendList);
+		slotno++;
+	}
+	for (int i = 0; i < autovacuum_max_workers; i++)
+	{
+		init_slot(&slots[slotno], slotno, &freeAutoVacWorkerList);
+		slotno++;
+	}
+	for (int i = 0; i < max_worker_processes; i++)
+	{
+		init_slot(&slots[slotno], slotno, &freeBgWorkerList);
+		slotno++;
+	}
+	for (int i = 0; i < NUM_AUXILIARY_PROCS; i++)
+	{
+		init_slot(&slots[slotno], slotno, &freeAuxList);
+		slotno++;
+	}
+	Assert(slotno == num_pmchild_slots);
+}
+
+/* Return the appropriate free-list for the given backend type */
+static dlist_head *
+GetFreeList(BackendType btype)
+{
+	switch (btype)
+	{
+		case B_BACKEND:
+		case B_BG_WORKER:
+		case B_WAL_SENDER:
+		case B_SLOTSYNC_WORKER:
+			return &freeBackendList;
+		case B_AUTOVAC_WORKER:
+			return &freeAutoVacWorkerList;
+
+			/*
+			 * Auxiliary processes.  There can be only one of each of these
+			 * running at a time.
+			 */
+		case B_AUTOVAC_LAUNCHER:
+		case B_ARCHIVER:
+		case B_BG_WRITER:
+		case B_CHECKPOINTER:
+		case B_STARTUP:
+		case B_WAL_RECEIVER:
+		case B_WAL_SUMMARIZER:
+		case B_WAL_WRITER:
+			return &freeAuxList;
+
+			/*
+			 * Logger is not connected to shared memory, and does not have a
+			 * PGPROC entry, but we still allocate a child slot for it.
+			 */
+		case B_LOGGER:
+			return &freeAuxList;
+
+		case B_STANDALONE_BACKEND:
+		case B_INVALID:
+		case B_DEAD_END_BACKEND:
+			break;
+	}
+	elog(ERROR, "unexpected BackendType: %d", (int) btype);
+	return NULL;
+}
+
+/*
+ * Allocate a PMChild entry for a backend of given type.
+ *
+ * The entry is taken from the right pool.
+ *
+ * pmchild->child_slot is unique among all active child processes
+ */
+PMChild *
+AssignPostmasterChildSlot(BackendType btype)
+{
+	dlist_head *freelist;
+	PMChild    *pmchild;
+
+	freelist = GetFreeList(btype);
+
+	if (dlist_is_empty(freelist))
+		return NULL;
+
+	pmchild = dlist_container(PMChild, elem, dlist_pop_head_node(freelist));
+	pmchild->pid = 0;
+	pmchild->bkend_type = btype;
+	pmchild->rw = NULL;
+	pmchild->bgworker_notify = true;
+
+	/*
+	 * pmchild->child_slot for each entry was initialized when the array of
+	 * slots was allocated.
+	 */
+
+	dlist_push_head(&ActiveChildList, &pmchild->elem);
+
+	ReservePostmasterChildSlot(pmchild->child_slot);
+
+	/* FIXME: find a more elegant way to pass this */
+	MyPMChildSlot = pmchild->child_slot;
+
+	elog(DEBUG2, "assigned pm child slot %d for %s", pmchild->child_slot, PostmasterChildName(btype));
+
+	return pmchild;
+}
+
+/*
+ * Release a PMChild slot, after the child process has exited.
+ *
+ * Returns true if the child detached cleanly from shared memory, false
+ * otherwise (see ReleasePostmasterChildSlot).
+ */
+bool
+FreePostmasterChildSlot(PMChild *pmchild)
+{
+	elog(LOG, "releasing pm child slot %d", pmchild->child_slot);
+
+	dlist_delete(&pmchild->elem);
+	if (pmchild->bkend_type == B_DEAD_END_BACKEND)
+	{
+		pfree(pmchild);
+		return true;
+	}
+	else
+	{
+		dlist_head *freelist;
+
+		freelist = GetFreeList(pmchild->bkend_type);
+		dlist_push_head(freelist, &pmchild->elem);
+		return ReleasePostmasterChildSlot(pmchild->child_slot);
+	}
+}
+
+PMChild *
+FindPostmasterChildByPid(int pid)
+{
+	dlist_iter	iter;
+
+	dlist_foreach(iter, &ActiveChildList)
+	{
+		PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
+
+		if (bp->pid == pid)
+			return bp;
+	}
+	return NULL;
+}
+
+/*
+ * Allocate a PMChild struct for a dead-end backend.  Dead-end children are
+ * not assigned a child_slot number.  The struct is palloc'd; returns NULL if
+ * out of memory.
+ */
+PMChild *
+AllocDeadEndChild(void)
+{
+	PMChild    *pmchild;
+
+	elog(LOG, "allocating dead-end child");
+
+	pmchild = (PMChild *) palloc_extended(sizeof(PMChild), MCXT_ALLOC_NO_OOM);
+	if (pmchild)
+	{
+		pmchild->pid = 0;
+		pmchild->child_slot = 0;
+		pmchild->bkend_type = B_DEAD_END_BACKEND;
+		pmchild->rw = NULL;
+		pmchild->bgworker_notify = false;
+
+		dlist_push_head(&ActiveChildList, &pmchild->elem);
+	}
+
+	return pmchild;
+}
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index bfadb995cb..a928a04c7a 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -135,49 +135,8 @@
 #define BACKEND_TYPE_ALL 0xffffffff
 StaticAssertDecl(BACKEND_NUM_TYPES < 32, "too many backend types for uint32");
 
-/*
- * List of active backends (or child processes anyway; we don't actually
- * know whether a given child has become a backend or is still in the
- * authorization phase).  This is used mainly to keep track of how many
- * children we have and send them appropriate signals when necessary.
- *
- * As shown in the above set of backend types, this list includes not only
- * "normal" client sessions, but also autovacuum workers, walsenders, and
- * background workers.  (Note that at the time of launch, walsenders are
- * labeled B_BACKEND; we relabel them to B_WAL_SENDER
- * upon noticing they've changed their PMChildFlags entry.  Hence that check
- * must be done before any operation that needs to distinguish walsenders
- * from normal backends.)
- *
- * Also, "dead_end" children are in it: these are children launched just for
- * the purpose of sending a friendly rejection message to a would-be client.
- * We must track them because they are attached to shared memory, but we know
- * they will never become live backends.  dead_end children are not assigned a
- * PMChildSlot.  dead_end children have bkend_type B_DEAD_END_BACKEND.
- * FIXME: a dead-end backend can send query cancel?
- *
- * "Special" children such as the startup, bgwriter, autovacuum launcher, and
- * slot sync worker tasks are not in this list.  They are tracked via StartupPID
- * and other pid_t variables below.  (Thus, there can't be more than one of any
- * given "special" child process type.  We use BackendList entries for any
- * child process there can be more than one of.)
- */
-typedef struct bkend
-{
-	pid_t		pid;			/* process id of backend */
-	int			child_slot;		/* PMChildSlot for this backend, if any */
-	BackendType bkend_type;		/* child process flavor, see above */
-	RegisteredBgWorker *rw;		/* bgworker info, if this is a bgworker */
-	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
-	dlist_node	elem;			/* list link in BackendList */
-} Backend;
-
-static dlist_head BackendList = DLIST_STATIC_INIT(BackendList);
-
 BackgroundWorker *MyBgworkerEntry = NULL;
 
-
-
 /* The socket number we are listening for connections on */
 int			PostPortNumber = DEF_PGPORT;
 
@@ -229,17 +188,17 @@ bool		remove_temp_files_after_crash = true;
 bool		send_abort_for_crash = false;
 bool		send_abort_for_kill = false;
 
-/* PIDs of special child processes; 0 when not running */
-static pid_t StartupPID = 0,
-			BgWriterPID = 0,
-			CheckpointerPID = 0,
-			WalWriterPID = 0,
-			WalReceiverPID = 0,
-			WalSummarizerPID = 0,
-			AutoVacPID = 0,
-			PgArchPID = 0,
-			SysLoggerPID = 0,
-			SlotSyncWorkerPID = 0;
+/* special child processes; NULL when not running */
+static PMChild *StartupPMChild = NULL,
+		   *BgWriterPMChild = NULL,
+		   *CheckpointerPMChild = NULL,
+		   *WalWriterPMChild = NULL,
+		   *WalReceiverPMChild = NULL,
+		   *WalSummarizerPMChild = NULL,
+		   *AutoVacLauncherPMChild = NULL,
+		   *PgArchPMChild = NULL,
+		   *SysLoggerPMChild = NULL,
+		   *SlotSyncWorkerPMChild = NULL;
 
 /* Startup process's status */
 typedef enum
@@ -287,7 +246,7 @@ static bool FatalError = false; /* T if recovering from backend crash */
  * PM_HOT_STANDBY state.  (connsAllowed can also restrict launching.)
  * In other states we handle connection requests by launching "dead_end"
  * child processes, which will simply send the client an error message and
- * quit.  (We track these in the BackendList so that we can know when they
+ * quit.  (We track these in the ActiveChildList so that we can know when they
  * are all gone; this is important because they're still connected to shared
  * memory, and would interfere with an attempt to destroy the shmem segment,
  * possibly leading to SHMALL failure when we try to make a new one.)
@@ -393,7 +352,7 @@ static void process_pm_child_exit(void);
 static void process_pm_reload_request(void);
 static void process_pm_shutdown_request(void);
 static void dummy_handler(SIGNAL_ARGS);
-static void CleanupBackend(Backend *bp, int exitstatus);
+static void CleanupBackend(PMChild *bp, int exitstatus);
 static void HandleChildCrash(int pid, int exitstatus, const char *procname);
 static void LogChildExit(int lev, const char *procname,
 						 int pid, int exitstatus);
@@ -403,18 +362,18 @@ static void ExitPostmaster(int status) pg_attribute_noreturn();
 static int	ServerLoop(void);
 static int	BackendStartup(ClientSocket *client_sock);
 static void report_fork_failure_to_client(ClientSocket *client_sock, int errnum);
-static CAC_state canAcceptConnections(int backend_type);
-static void signal_child(pid_t pid, int signal);
-static void sigquit_child(pid_t pid);
+static CAC_state canAcceptConnections(BackendType backend_type);
+static void signal_child(PMChild *pmchild, int signal);
+static void sigquit_child(PMChild *pmchild);
 static bool SignalSomeChildren(int signal, uint32 targetMask);
 static void TerminateChildren(int signal);
 
 static int	CountChildren(uint32 targetMask);
-static Backend *assign_backendlist_entry(void);
+static PMChild *assign_backendlist_entry(void);
 static void LaunchMissingBackgroundProcesses(void);
 static void maybe_start_bgworkers(void);
 static bool CreateOptsFile(int argc, char *argv[], char *fullprogname);
-static pid_t StartChildProcess(BackendType type);
+static PMChild *StartChildProcess(BackendType type);
 static void StartAutovacuumWorker(void);
 static void InitPostmasterDeathWatchHandle(void);
 
@@ -893,9 +852,11 @@ PostmasterMain(int argc, char *argv[])
 
 	/*
 	 * Now that loadable modules have had their chance to alter any GUCs,
-	 * calculate MaxBackends.
+	 * calculate MaxBackends, and initialize the machinery to track child
+	 * processes.
 	 */
 	InitializeMaxBackends();
+	InitPostmasterChildSlots();
 
 	/*
 	 * Give preloaded libraries a chance to request additional shared memory.
@@ -1019,7 +980,15 @@ PostmasterMain(int argc, char *argv[])
 	/*
 	 * If enabled, start up syslogger collection subprocess
 	 */
-	SysLoggerPID = SysLogger_Start();
+	SysLoggerPMChild = AssignPostmasterChildSlot(B_LOGGER);
+	if (!SysLoggerPMChild)
+		elog(ERROR, "no postmaster child slot available for syslogger");
+	SysLoggerPMChild->pid = SysLogger_Start();
+	if (SysLoggerPMChild->pid == 0)
+	{
+		FreePostmasterChildSlot(SysLoggerPMChild);
+		SysLoggerPMChild = NULL;
+	}
 
 	/*
 	 * Reset whereToSendOutput from DestDebug (its starting state) to
@@ -1321,16 +1290,16 @@ PostmasterMain(int argc, char *argv[])
 	AddToDataDirLockFile(LOCK_FILE_LINE_PM_STATUS, PM_STATUS_STARTING);
 
 	/* Start bgwriter and checkpointer so they can help with recovery */
-	if (CheckpointerPID == 0)
-		CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
-	if (BgWriterPID == 0)
-		BgWriterPID = StartChildProcess(B_BG_WRITER);
+	if (CheckpointerPMChild == NULL)
+		CheckpointerPMChild = StartChildProcess(B_CHECKPOINTER);
+	if (BgWriterPMChild == NULL)
+		BgWriterPMChild = StartChildProcess(B_BG_WRITER);
 
 	/*
 	 * We're ready to rock and roll...
 	 */
-	StartupPID = StartChildProcess(B_STARTUP);
-	Assert(StartupPID != 0);
+	StartupPMChild = StartChildProcess(B_STARTUP);
+	Assert(StartupPMChild != NULL);
 	StartupStatus = STARTUP_RUNNING;
 	pmState = PM_STARTUP;
 
@@ -1660,8 +1629,8 @@ ServerLoop(void)
 		if (avlauncher_needs_signal)
 		{
 			avlauncher_needs_signal = false;
-			if (AutoVacPID != 0)
-				kill(AutoVacPID, SIGUSR2);
+			if (AutoVacLauncherPMChild != NULL)
+				kill(AutoVacLauncherPMChild->pid, SIGUSR2);
 		}
 
 #ifdef HAVE_PTHREAD_IS_THREADED_NP
@@ -1748,7 +1717,7 @@ ServerLoop(void)
  * know whether a NORMAL connection might turn into a walsender.)
  */
 static CAC_state
-canAcceptConnections(int backend_type)
+canAcceptConnections(BackendType backend_type)
 {
 	CAC_state	result = CAC_OK;
 
@@ -1779,21 +1748,6 @@ canAcceptConnections(int backend_type)
 	if (!connsAllowed && backend_type == B_BACKEND)
 		return CAC_SHUTDOWN;	/* shutdown is pending */
 
-	/*
-	 * Don't start too many children.
-	 *
-	 * We allow more connections here than we can have backends because some
-	 * might still be authenticating; they might fail auth, or some existing
-	 * backend might exit before the auth cycle is completed.  The exact
-	 * MaxBackends limit is enforced when a new backend tries to join the
-	 * shared-inval backend array.
-	 *
-	 * The limit here must match the sizes of the per-child-process arrays;
-	 * see comments for MaxLivePostmasterChildren().
-	 */
-	if (CountChildren(BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND)) >= MaxLivePostmasterChildren())
-		result = CAC_TOOMANY;
-
 	return result;
 }
 
@@ -1961,26 +1915,6 @@ process_pm_reload_request(void)
 				(errmsg("received SIGHUP, reloading configuration files")));
 		ProcessConfigFile(PGC_SIGHUP);
 		SignalSomeChildren(SIGHUP, BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND));
-		if (StartupPID != 0)
-			signal_child(StartupPID, SIGHUP);
-		if (BgWriterPID != 0)
-			signal_child(BgWriterPID, SIGHUP);
-		if (CheckpointerPID != 0)
-			signal_child(CheckpointerPID, SIGHUP);
-		if (WalWriterPID != 0)
-			signal_child(WalWriterPID, SIGHUP);
-		if (WalReceiverPID != 0)
-			signal_child(WalReceiverPID, SIGHUP);
-		if (WalSummarizerPID != 0)
-			signal_child(WalSummarizerPID, SIGHUP);
-		if (AutoVacPID != 0)
-			signal_child(AutoVacPID, SIGHUP);
-		if (PgArchPID != 0)
-			signal_child(PgArchPID, SIGHUP);
-		if (SysLoggerPID != 0)
-			signal_child(SysLoggerPID, SIGHUP);
-		if (SlotSyncWorkerPID != 0)
-			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -2218,15 +2152,15 @@ process_pm_child_exit(void)
 
 	while ((pid = waitpid(-1, &exitstatus, WNOHANG)) > 0)
 	{
-		bool		found;
-		dlist_mutable_iter iter;
+		PMChild    *pmchild;
 
 		/*
 		 * Check if this child was a startup process.
 		 */
-		if (pid == StartupPID)
+		if (StartupPMChild && pid == StartupPMChild->pid)
 		{
-			StartupPID = 0;
+			FreePostmasterChildSlot(StartupPMChild);
+			StartupPMChild = NULL;
 
 			/*
 			 * Startup process exited in response to a shutdown request (or it
@@ -2338,9 +2272,10 @@ process_pm_child_exit(void)
 		 * one at the next iteration of the postmaster's main loop, if
 		 * necessary.  Any other exit condition is treated as a crash.
 		 */
-		if (pid == BgWriterPID)
+		if (BgWriterPMChild && pid == BgWriterPMChild->pid)
 		{
-			BgWriterPID = 0;
+			FreePostmasterChildSlot(BgWriterPMChild);
+			BgWriterPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("background writer process"));
@@ -2350,9 +2285,10 @@ process_pm_child_exit(void)
 		/*
 		 * Was it the checkpointer?
 		 */
-		if (pid == CheckpointerPID)
+		if (CheckpointerPMChild && pid == CheckpointerPMChild->pid)
 		{
-			CheckpointerPID = 0;
+			FreePostmasterChildSlot(CheckpointerPMChild);
+			CheckpointerPMChild = NULL;
 			if (EXIT_STATUS_0(exitstatus) && pmState == PM_SHUTDOWN)
 			{
 				/*
@@ -2372,14 +2308,14 @@ process_pm_child_exit(void)
 				Assert(Shutdown > NoShutdown);
 
 				/* Waken archiver for the last time */
-				if (PgArchPID != 0)
-					signal_child(PgArchPID, SIGUSR2);
+				if (PgArchPMChild != NULL)
+					signal_child(PgArchPMChild, SIGUSR2);
 
 				/*
 				 * Waken walsenders for the last time. No regular backends
 				 * should be around anymore.
 				 */
-				SignalSomeChildren(SIGUSR2, BACKEND_TYPE_ALL & (1 << B_DEAD_END_BACKEND));
+				SignalSomeChildren(SIGUSR2, (1 << B_WAL_SENDER));
 
 				pmState = PM_SHUTDOWN_2;
 			}
@@ -2401,9 +2337,10 @@ process_pm_child_exit(void)
 		 * new one at the next iteration of the postmaster's main loop, if
 		 * necessary.  Any other exit condition is treated as a crash.
 		 */
-		if (pid == WalWriterPID)
+		if (WalWriterPMChild && pid == WalWriterPMChild->pid)
 		{
-			WalWriterPID = 0;
+			FreePostmasterChildSlot(WalWriterPMChild);
+			WalWriterPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("WAL writer process"));
@@ -2416,9 +2353,10 @@ process_pm_child_exit(void)
 		 * backends.  (If we need a new wal receiver, we'll start one at the
 		 * next iteration of the postmaster's main loop.)
 		 */
-		if (pid == WalReceiverPID)
+		if (WalReceiverPMChild && pid == WalReceiverPMChild->pid)
 		{
-			WalReceiverPID = 0;
+			FreePostmasterChildSlot(WalReceiverPMChild);
+			WalReceiverPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("WAL receiver process"));
@@ -2430,9 +2368,10 @@ process_pm_child_exit(void)
 		 * a new one at the next iteration of the postmaster's main loop, if
 		 * necessary.  Any other exit condition is treated as a crash.
 		 */
-		if (pid == WalSummarizerPID)
+		if (WalSummarizerPMChild && pid == WalSummarizerPMChild->pid)
 		{
-			WalSummarizerPID = 0;
+			FreePostmasterChildSlot(WalSummarizerPMChild);
+			WalSummarizerPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("WAL summarizer process"));
@@ -2445,9 +2384,10 @@ process_pm_child_exit(void)
 		 * loop, if necessary.  Any other exit condition is treated as a
 		 * crash.
 		 */
-		if (pid == AutoVacPID)
+		if (AutoVacLauncherPMChild && pid == AutoVacLauncherPMChild->pid)
 		{
-			AutoVacPID = 0;
+			FreePostmasterChildSlot(AutoVacLauncherPMChild);
+			AutoVacLauncherPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("autovacuum launcher process"));
@@ -2460,9 +2400,10 @@ process_pm_child_exit(void)
 		 * and just try to start a new one on the next cycle of the
 		 * postmaster's main loop, to retry archiving remaining files.
 		 */
-		if (pid == PgArchPID)
+		if (PgArchPMChild && pid == PgArchPMChild->pid)
 		{
-			PgArchPID = 0;
+			FreePostmasterChildSlot(PgArchPMChild);
+			PgArchPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("archiver process"));
@@ -2470,11 +2411,15 @@ process_pm_child_exit(void)
 		}
 
 		/* Was it the system logger?  If so, try to start a new one */
-		if (pid == SysLoggerPID)
+		if (SysLoggerPMChild && pid == SysLoggerPMChild->pid)
 		{
-			SysLoggerPID = 0;
 			/* for safety's sake, launch new logger *first* */
-			SysLoggerPID = SysLogger_Start();
+			SysLoggerPMChild->pid = SysLogger_Start();
+			if (SysLoggerPMChild->pid == 0)
+			{
+				FreePostmasterChildSlot(SysLoggerPMChild);
+				SysLoggerPMChild = NULL;
+			}
 			if (!EXIT_STATUS_0(exitstatus))
 				LogChildExit(LOG, _("system logger process"),
 							 pid, exitstatus);
@@ -2488,9 +2433,10 @@ process_pm_child_exit(void)
 		 * start a new one at the next iteration of the postmaster's main
 		 * loop, if necessary. Any other exit condition is treated as a crash.
 		 */
-		if (pid == SlotSyncWorkerPID)
+		if (SlotSyncWorkerPMChild && pid == SlotSyncWorkerPMChild->pid)
 		{
-			SlotSyncWorkerPID = 0;
+			FreePostmasterChildSlot(SlotSyncWorkerPMChild);
+			SlotSyncWorkerPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("slot sync worker process"));
@@ -2500,25 +2446,17 @@ process_pm_child_exit(void)
 		/*
 		 * Was it a backend or background worker?
 		 */
-		found = false;
-		dlist_foreach_modify(iter, &BackendList)
+		pmchild = FindPostmasterChildByPid(pid);
+		if (pmchild)
 		{
-			Backend    *bp = dlist_container(Backend, elem, iter.cur);
-
-			if (bp->pid == pid)
-			{
-				dlist_delete(iter.cur);
-				CleanupBackend(bp, exitstatus);
-				found = true;
-				break;
-			}
+			CleanupBackend(pmchild, exitstatus);
 		}
 
 		/*
 		 * We don't know anything about this child process.  That's highly
 		 * unexpected, as we do track all the child processes that we fork.
 		 */
-		if (!found)
+		else
 		{
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus, _("untracked child process"));
@@ -2540,14 +2478,19 @@ process_pm_child_exit(void)
  * Remove all local state associated with backend.
  */
 static void
-CleanupBackend(Backend *bp,
+CleanupBackend(PMChild *bp,
 			   int exitstatus)	/* child's exit status. */
 {
 	char		namebuf[MAXPGPATH];
 	char	   *procname;
 	bool		crashed = false;
+	pid_t		bp_pid;
+	bool		bp_bgworker_notify;
+	BackendType bp_bkend_type;
+	RegisteredBgWorker *rw;
 
 	/* Construct a process name for log message */
+	/* FIXME: use GetBackendTypeDesc here? How does the localization of that work? */
 	if (bp->bkend_type == B_DEAD_END_BACKEND)
 	{
 		procname = _("dead end backend");
@@ -2587,25 +2530,28 @@ CleanupBackend(Backend *bp,
 #endif
 
 	/*
-	 * If the process attached to shared memory, check that it detached
-	 * cleanly.
+	 * Release the PMChild entry.
+	 *
+	 * If the process attached to shared memory, this also checks that it
+	 * detached cleanly.
 	 */
-	if (bp->bkend_type != B_DEAD_END_BACKEND)
+	bp_pid = bp->pid;
+	bp_bgworker_notify = bp->bgworker_notify;
+	bp_bkend_type = bp->bkend_type;
+	rw = bp->rw;
+	if (!FreePostmasterChildSlot(bp))
 	{
-		if (!ReleasePostmasterChildSlot(bp->child_slot))
-		{
-			/*
-			 * Uh-oh, the child failed to clean itself up.  Treat as a crash
-			 * after all.
-			 */
-			crashed = true;
-		}
+		/*
+		 * Uh-oh, the child failed to clean itself up.  Treat as a crash after
+		 * all.
+		 */
+		crashed = true;
 	}
+	bp = NULL;
 
 	if (crashed)
 	{
-		HandleChildCrash(bp->pid, exitstatus, namebuf);
-		pfree(bp);
+		HandleChildCrash(bp_pid, exitstatus, namebuf);
 		return;
 	}
 
@@ -2616,16 +2562,14 @@ CleanupBackend(Backend *bp,
 	 * gets skipped in the (probably very common) case where the backend has
 	 * never requested any such notifications.
 	 */
-	if (bp->bgworker_notify)
-		BackgroundWorkerStopNotifications(bp->pid);
+	if (bp_bgworker_notify)
+		BackgroundWorkerStopNotifications(bp_pid);
 
 	/*
 	 * If it was a background worker, also update its RegisteredWorker entry.
 	 */
-	if (bp->bkend_type == B_BG_WORKER)
+	if (bp_bkend_type == B_BG_WORKER)
 	{
-		RegisteredBgWorker *rw = bp->rw;
-
 		if (!EXIT_STATUS_0(exitstatus))
 		{
 			/* Record timestamp, so we know when to restart the worker. */
@@ -2642,15 +2586,13 @@ CleanupBackend(Backend *bp,
 		ReportBackgroundWorkerExit(rw); /* report child death */
 
 		LogChildExit(EXIT_STATUS_0(exitstatus) ? DEBUG1 : LOG,
-					 procname, bp->pid, exitstatus);
+					 procname, bp_pid, exitstatus);
 
 		/* have it be restarted */
 		HaveCrashedWorker = true;
 	}
 	else
-		LogChildExit(DEBUG2, procname, bp->pid, exitstatus);
-
-	pfree(bp);
+		LogChildExit(DEBUG2, procname, bp_pid, exitstatus);
 }
 
 /*
@@ -2690,9 +2632,16 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	{
 		dlist_iter	iter;
 
-		dlist_foreach(iter, &BackendList)
+		dlist_foreach(iter, &ActiveChildList)
 		{
-			Backend    *bp = dlist_container(Backend, elem, iter.cur);
+			PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
+
+			/* We do NOT restart the syslogger */
+			if (bp == SysLoggerPMChild)
+				continue;
+
+			if (bp == StartupPMChild)
+				StartupStatus = STARTUP_SIGNALED;
 
 			/*
 			 * This backend is still alive.  Unless we did so already, tell it
@@ -2701,48 +2650,8 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 			 * We could exclude dead_end children here, but at least when
 			 * sending SIGABRT it seems better to include them.
 			 */
-			sigquit_child(bp->pid);
+			sigquit_child(bp);
 		}
-
-		if (StartupPID != 0)
-		{
-			sigquit_child(StartupPID);
-			StartupStatus = STARTUP_SIGNALED;
-		}
-
-		/* Take care of the bgwriter too */
-		if (BgWriterPID != 0)
-			sigquit_child(BgWriterPID);
-
-		/* Take care of the checkpointer too */
-		if (CheckpointerPID != 0)
-			sigquit_child(CheckpointerPID);
-
-		/* Take care of the walwriter too */
-		if (WalWriterPID != 0)
-			sigquit_child(WalWriterPID);
-
-		/* Take care of the walreceiver too */
-		if (WalReceiverPID != 0)
-			sigquit_child(WalReceiverPID);
-
-		/* Take care of the walsummarizer too */
-		if (WalSummarizerPID != 0)
-			sigquit_child(WalSummarizerPID);
-
-		/* Take care of the autovacuum launcher too */
-		if (AutoVacPID != 0)
-			sigquit_child(AutoVacPID);
-
-		/* Take care of the archiver too */
-		if (PgArchPID != 0)
-			sigquit_child(PgArchPID);
-
-		/* Take care of the slot sync worker too */
-		if (SlotSyncWorkerPID != 0)
-			sigquit_child(SlotSyncWorkerPID);
-
-		/* We do NOT restart the syslogger */
 	}
 
 	if (Shutdown != ImmediateShutdown)
@@ -2845,7 +2754,7 @@ PostmasterStateMachine(void)
 			 * This state ends when we have no normal client backends running.
 			 * Then we're ready to stop other children.
 			 */
-			if (CountChildren(1 << B_BACKEND) == 0)
+			if (CountChildren(B_BACKEND) == 0)
 				pmState = PM_STOP_BACKENDS;
 		}
 	}
@@ -2857,6 +2766,8 @@ PostmasterStateMachine(void)
 	 */
 	if (pmState == PM_STOP_BACKENDS)
 	{
+		uint32		targetMask;
+
 		/*
 		 * Forget any pending requests for background workers, since we're no
 		 * longer willing to launch any new workers.  (If additional requests
@@ -2864,29 +2775,27 @@ PostmasterStateMachine(void)
 		 */
 		ForgetUnstartedBackgroundWorkers();
 
-		/* Signal all backend children except walsenders and dead-end backends */
-		SignalSomeChildren(SIGTERM,
-						   BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND));
+		/* Signal all backend children except walsenders */
+		/* dead-end children are not signalled yet */
+		targetMask = (1 << B_BACKEND);
+		targetMask |= (1 << B_BG_WORKER);
+
 		/* and the autovac launcher too */
-		if (AutoVacPID != 0)
-			signal_child(AutoVacPID, SIGTERM);
+		targetMask |= (1 << B_AUTOVAC_LAUNCHER);
 		/* and the bgwriter too */
-		if (BgWriterPID != 0)
-			signal_child(BgWriterPID, SIGTERM);
+		targetMask |= (1 << B_BG_WRITER);
 		/* and the walwriter too */
-		if (WalWriterPID != 0)
-			signal_child(WalWriterPID, SIGTERM);
+		targetMask |= (1 << B_WAL_WRITER);
 		/* If we're in recovery, also stop startup and walreceiver procs */
-		if (StartupPID != 0)
-			signal_child(StartupPID, SIGTERM);
-		if (WalReceiverPID != 0)
-			signal_child(WalReceiverPID, SIGTERM);
-		if (WalSummarizerPID != 0)
-			signal_child(WalSummarizerPID, SIGTERM);
-		if (SlotSyncWorkerPID != 0)
-			signal_child(SlotSyncWorkerPID, SIGTERM);
+		targetMask |= (1 << B_STARTUP);
+		targetMask |= (1 << B_WAL_RECEIVER);
+
+		targetMask |= (1 << B_WAL_SUMMARIZER);
+		targetMask |= (1 << B_SLOTSYNC_WORKER);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
+		SignalSomeChildren(SIGTERM, targetMask);
+
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
 		pmState = PM_WAIT_BACKENDS;
 	}
@@ -2908,16 +2817,14 @@ PostmasterStateMachine(void)
 		 * here. Walsenders and archiver are also disregarded, they will be
 		 * terminated later after writing the checkpoint record.
 		 */
-		if (CountChildren(BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND)) == 0 &&
-			StartupPID == 0 &&
-			WalReceiverPID == 0 &&
-			WalSummarizerPID == 0 &&
-			BgWriterPID == 0 &&
-			(CheckpointerPID == 0 ||
-			 (!FatalError && Shutdown < ImmediateShutdown)) &&
-			WalWriterPID == 0 &&
-			AutoVacPID == 0 &&
-			SlotSyncWorkerPID == 0)
+		uint32		remaining;
+
+		remaining = (1 << B_WAL_SENDER) | (1 << B_ARCHIVER) | (1 << B_LOGGER);
+		remaining |= (1 << B_DEAD_END_BACKEND);
+		if (!FatalError && Shutdown < ImmediateShutdown)
+			remaining |= (1 << B_CHECKPOINTER);
+
+		if (CountChildren(BACKEND_TYPE_ALL & ~remaining) == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -2943,12 +2850,12 @@ PostmasterStateMachine(void)
 				 */
 				Assert(Shutdown > NoShutdown);
 				/* Start the checkpointer if not running */
-				if (CheckpointerPID == 0)
-					CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
+				if (CheckpointerPMChild == NULL)
+					CheckpointerPMChild = StartChildProcess(B_CHECKPOINTER);
 				/* And tell it to shut down */
-				if (CheckpointerPID != 0)
+				if (CheckpointerPMChild != NULL)
 				{
-					signal_child(CheckpointerPID, SIGUSR2);
+					signal_child(CheckpointerPMChild, SIGUSR2);
 					pmState = PM_SHUTDOWN;
 				}
 				else
@@ -2969,8 +2876,8 @@ PostmasterStateMachine(void)
 
 					/* Kill the walsenders and archiver, too */
 					SignalSomeChildren(SIGQUIT, BACKEND_TYPE_ALL);
-					if (PgArchPID != 0)
-						signal_child(PgArchPID, SIGQUIT);
+					if (PgArchPMChild != NULL)
+						signal_child(PgArchPMChild, SIGQUIT);
 				}
 			}
 		}
@@ -2984,7 +2891,10 @@ PostmasterStateMachine(void)
 		 * left by now anyway; what we're really waiting for is walsenders and
 		 * archiver.
 		 */
-		if (PgArchPID == 0 && CountChildren(BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND)) == 0)
+		uint32		remaining;
+
+		remaining = (1 << B_LOGGER) | (1 << B_DEAD_END_BACKEND);
+		if (CountChildren(BACKEND_TYPE_ALL & ~remaining) == 0)
 		{
 			ConfigurePostmasterWaitSet(false);
 			SignalSomeChildren(SIGTERM, 1 << B_DEAD_END_BACKEND);
@@ -3006,17 +2916,19 @@ PostmasterStateMachine(void)
 		 * normal state transition leading up to PM_WAIT_DEAD_END, or during
 		 * FatalError processing.
 		 */
-		if (dlist_is_empty(&BackendList) && PgArchPID == 0)
+		if (dlist_is_empty(&ActiveChildList) ||
+			(dlist_head_node(&ActiveChildList) == &SysLoggerPMChild->elem &&
+			 dlist_tail_node(&ActiveChildList) == &SysLoggerPMChild->elem))
 		{
 			/* These other guys should be dead already */
-			Assert(StartupPID == 0);
-			Assert(WalReceiverPID == 0);
-			Assert(WalSummarizerPID == 0);
-			Assert(BgWriterPID == 0);
-			Assert(CheckpointerPID == 0);
-			Assert(WalWriterPID == 0);
-			Assert(AutoVacPID == 0);
-			Assert(SlotSyncWorkerPID == 0);
+			Assert(StartupPMChild == NULL);
+			Assert(WalReceiverPMChild == NULL);
+			Assert(WalSummarizerPMChild == NULL);
+			Assert(BgWriterPMChild == NULL);
+			Assert(CheckpointerPMChild == NULL);
+			Assert(WalWriterPMChild == NULL);
+			Assert(AutoVacLauncherPMChild == NULL);
+			Assert(SlotSyncWorkerPMChild == NULL);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -3099,8 +3011,8 @@ PostmasterStateMachine(void)
 		/* re-create shared memory and semaphores */
 		CreateSharedMemoryAndSemaphores();
 
-		StartupPID = StartChildProcess(B_STARTUP);
-		Assert(StartupPID != 0);
+		StartupPMChild = StartChildProcess(B_STARTUP);
+		Assert(StartupPMChild != NULL);
 		StartupStatus = STARTUP_RUNNING;
 		pmState = PM_STARTUP;
 		/* crash recovery started, reset SIGKILL flag */
@@ -3120,8 +3032,21 @@ static void
 LaunchMissingBackgroundProcesses(void)
 {
 	/* If we have lost the log collector, try to start a new one */
-	if (SysLoggerPID == 0 && Logging_collector)
-		SysLoggerPID = SysLogger_Start();
+	if (SysLoggerPMChild == NULL && Logging_collector)
+	{
+		SysLoggerPMChild = AssignPostmasterChildSlot(B_LOGGER);
+		if (!SysLoggerPMChild)
+			elog(LOG, "no postmaster child slot available for syslogger");
+		else
+		{
+			SysLoggerPMChild->pid = SysLogger_Start();
+			if (SysLoggerPMChild->pid == 0)
+			{
+				FreePostmasterChildSlot(SysLoggerPMChild);
+				SysLoggerPMChild = NULL;
+			}
+		}
+	}
 
 	/*
 	 * If no background writer process is running, and we are not in a state
@@ -3131,10 +3056,10 @@ LaunchMissingBackgroundProcesses(void)
 	if (pmState == PM_RUN || pmState == PM_RECOVERY ||
 		pmState == PM_HOT_STANDBY || pmState == PM_STARTUP)
 	{
-		if (CheckpointerPID == 0)
-			CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
-		if (BgWriterPID == 0)
-			BgWriterPID = StartChildProcess(B_BG_WRITER);
+		if (CheckpointerPMChild == NULL)
+			CheckpointerPMChild = StartChildProcess(B_CHECKPOINTER);
+		if (BgWriterPMChild == NULL)
+			BgWriterPMChild = StartChildProcess(B_BG_WRITER);
 	}
 
 	/*
@@ -3142,8 +3067,8 @@ LaunchMissingBackgroundProcesses(void)
 	 * one.  But this is needed only in normal operation (else we cannot be
 	 * writing any new WAL).
 	 */
-	if (WalWriterPID == 0 && pmState == PM_RUN)
-		WalWriterPID = StartChildProcess(B_WAL_WRITER);
+	if (WalWriterPMChild == NULL && pmState == PM_RUN)
+		WalWriterPMChild = StartChildProcess(B_WAL_WRITER);
 
 	/*
 	 * If we have lost the autovacuum launcher, try to start a new one.  We
@@ -3151,12 +3076,12 @@ LaunchMissingBackgroundProcesses(void)
 	 * might update relfrozenxid for empty tables before the physical files
 	 * are put in place.
 	 */
-	if (!IsBinaryUpgrade && AutoVacPID == 0 &&
+	if (!IsBinaryUpgrade && AutoVacLauncherPMChild == NULL &&
 		(AutoVacuumingActive() || start_autovac_launcher) &&
 		pmState == PM_RUN)
 	{
-		AutoVacPID = StartChildProcess(B_AUTOVAC_LAUNCHER);
-		if (AutoVacPID != 0)
+		AutoVacLauncherPMChild = StartChildProcess(B_AUTOVAC_LAUNCHER);
+		if (AutoVacLauncherPMChild != NULL)
 			start_autovac_launcher = false; /* signal processed */
 	}
 
@@ -3166,24 +3091,24 @@ LaunchMissingBackgroundProcesses(void)
 	 * If WAL archiving is enabled always, we are allowed to start archiver
 	 * even during recovery.
 	 */
-	if (PgArchPID == 0 &&
+	if (PgArchPMChild == NULL &&
 		((XLogArchivingActive() && pmState == PM_RUN) ||
 		 (XLogArchivingAlways() && (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) &&
 		PgArchCanRestart())
-		PgArchPID = StartChildProcess(B_ARCHIVER);
+		PgArchPMChild = StartChildProcess(B_ARCHIVER);
 
 	/*
 	 * If we need to start a slot sync worker, try to do that now
 	 *
 	 * We allow to start the slot sync worker when we are on a hot standby,
-	 * fast or immediate shutdown is not in progress, slot sync parameters
-	 * are configured correctly, and it is the first time of worker's launch,
-	 * or enough time has passed since the worker was launched last.
+	 * fast or immediate shutdown is not in progress, slot sync parameters are
+	 * configured correctly, and it is the first time of worker's launch, or
+	 * enough time has passed since the worker was launched last.
 	 */
-	if (SlotSyncWorkerPID == 0 && pmState == PM_HOT_STANDBY &&
+	if (SlotSyncWorkerPMChild == NULL && pmState == PM_HOT_STANDBY &&
 		Shutdown <= SmartShutdown && sync_replication_slots &&
 		ValidateSlotSyncParams(LOG) && SlotSyncWorkerCanRestart())
-		SlotSyncWorkerPID = StartChildProcess(B_SLOTSYNC_WORKER);
+		SlotSyncWorkerPMChild = StartChildProcess(B_SLOTSYNC_WORKER);
 
 	/*
 	 * If we need to start a WAL receiver, try to do that now
@@ -3199,23 +3124,23 @@ LaunchMissingBackgroundProcesses(void)
 	 */
 	if (WalReceiverRequested)
 	{
-		if (WalReceiverPID == 0 &&
+		if (WalReceiverPMChild == NULL &&
 			(pmState == PM_STARTUP || pmState == PM_RECOVERY ||
 			 pmState == PM_HOT_STANDBY) &&
 			Shutdown <= SmartShutdown)
 		{
-			WalReceiverPID = StartChildProcess(B_WAL_RECEIVER);
-			if (WalReceiverPID != 0)
+			WalReceiverPMChild = StartChildProcess(B_WAL_RECEIVER);
+			if (WalReceiverPMChild != 0)
 				WalReceiverRequested = false;
 			/* else leave the flag set, so we'll try again later */
 		}
 	}
 
 	/* If we need to start a WAL summarizer, try to do that now */
-	if (summarize_wal && WalSummarizerPID == 0 &&
+	if (summarize_wal && WalSummarizerPMChild == NULL &&
 		(pmState == PM_RUN || pmState == PM_HOT_STANDBY) &&
 		Shutdown <= SmartShutdown)
-		WalSummarizerPID = StartChildProcess(B_WAL_SUMMARIZER);
+		WalSummarizerPMChild = StartChildProcess(B_WAL_SUMMARIZER);
 
 	/* Get other worker processes running, if needed */
 	if (StartWorkerNeeded || HaveCrashedWorker)
@@ -3239,8 +3164,14 @@ LaunchMissingBackgroundProcesses(void)
  * child twice will not cause any problems.
  */
 static void
-signal_child(pid_t pid, int signal)
+signal_child(PMChild *pmchild, int signal)
 {
+	pid_t		pid;
+
+	if (pmchild == NULL || pmchild->pid == 0)
+		return;
+	pid = pmchild->pid;
+
 	if (kill(pid, signal) < 0)
 		elog(DEBUG3, "kill(%ld,%d) failed: %m", (long) pid, signal);
 #ifdef HAVE_SETSID
@@ -3269,13 +3200,13 @@ signal_child(pid_t pid, int signal)
  * to use SIGABRT to collect per-child core dumps.
  */
 static void
-sigquit_child(pid_t pid)
+sigquit_child(PMChild *pmchild)
 {
 	ereport(DEBUG2,
 			(errmsg_internal("sending %s to process %d",
 							 (send_abort_for_crash ? "SIGABRT" : "SIGQUIT"),
-							 (int) pid)));
-	signal_child(pid, (send_abort_for_crash ? SIGABRT : SIGQUIT));
+							 (int) pmchild->pid)));
+	signal_child(pmchild, (send_abort_for_crash ? SIGABRT : SIGQUIT));
 }
 
 /*
@@ -3287,13 +3218,13 @@ SignalSomeChildren(int signal, uint32 targetMask)
 	dlist_iter	iter;
 	bool		signaled = false;
 
-	dlist_foreach(iter, &BackendList)
+	dlist_foreach(iter, &ActiveChildList)
 	{
-		Backend    *bp = dlist_container(Backend, elem, iter.cur);
+		PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
 
 		/*
-		 * Since target == BACKEND_TYPE_ALL is the most common case, we test
-		 * it first and avoid touching shared memory for every child.
+		 * Since targetMask == BACKEND_TYPE_ALL is the most common case, we
+		 * test it first and avoid touching shared memory for every child.
 		 */
 		if (targetMask != BACKEND_TYPE_ALL)
 		{
@@ -3312,7 +3243,7 @@ SignalSomeChildren(int signal, uint32 targetMask)
 		ereport(DEBUG4,
 				(errmsg_internal("sending signal %d to %s process %d",
 								 signal, GetBackendTypeDesc(bp->bkend_type), (int) bp->pid)));
-		signal_child(bp->pid, signal);
+		signal_child(bp, signal);
 		signaled = true;
 	}
 	return signaled;
@@ -3325,29 +3256,12 @@ SignalSomeChildren(int signal, uint32 targetMask)
 static void
 TerminateChildren(int signal)
 {
-	SignalSomeChildren(signal, BACKEND_TYPE_ALL);
-	if (StartupPID != 0)
+	SignalSomeChildren(signal, BACKEND_TYPE_ALL & ~(1 << B_LOGGER));
+	if (StartupPMChild != NULL)
 	{
-		signal_child(StartupPID, signal);
 		if (signal == SIGQUIT || signal == SIGKILL || signal == SIGABRT)
 			StartupStatus = STARTUP_SIGNALED;
 	}
-	if (BgWriterPID != 0)
-		signal_child(BgWriterPID, signal);
-	if (CheckpointerPID != 0)
-		signal_child(CheckpointerPID, signal);
-	if (WalWriterPID != 0)
-		signal_child(WalWriterPID, signal);
-	if (WalReceiverPID != 0)
-		signal_child(WalReceiverPID, signal);
-	if (WalSummarizerPID != 0)
-		signal_child(WalSummarizerPID, signal);
-	if (AutoVacPID != 0)
-		signal_child(AutoVacPID, signal);
-	if (PgArchPID != 0)
-		signal_child(PgArchPID, signal);
-	if (SlotSyncWorkerPID != 0)
-		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -3360,45 +3274,45 @@ TerminateChildren(int signal)
 static int
 BackendStartup(ClientSocket *client_sock)
 {
-	Backend    *bn;				/* for backend cleanup */
+	PMChild    *bn = NULL;
 	pid_t		pid;
 	BackendStartupData startup_data;
+	CAC_state	cac;
 
-	/*
-	 * Create backend data structure.  Better before the fork() so we can
-	 * handle failure cleanly.
-	 */
-	bn = (Backend *) palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
+	cac = canAcceptConnections(B_BACKEND);
+	if (cac == CAC_OK)
+	{
+		bn = AssignPostmasterChildSlot(B_BACKEND);
+		if (!bn)
+		{
+			/*
+			 * Too many regular child processes; launch a dead-end child
+			 * process instead.
+			 */
+			cac = CAC_TOOMANY;
+		}
+	}
 	if (!bn)
 	{
-		ereport(LOG,
-				(errcode(ERRCODE_OUT_OF_MEMORY),
-				 errmsg("out of memory")));
-		return STATUS_ERROR;
+		bn = AllocDeadEndChild();
+		if (!bn)
+		{
+			ereport(LOG,
+					(errcode(ERRCODE_OUT_OF_MEMORY),
+					 errmsg("out of memory")));
+			return STATUS_ERROR;
+		}
 	}
 
 	/* Pass down canAcceptConnections state */
-	startup_data.canAcceptConnections = canAcceptConnections(B_BACKEND);
+	startup_data.canAcceptConnections = cac;
 	bn->rw = NULL;
 
-	/*
-	 * Unless it's a dead_end child, assign it a child slot number
-	 */
-	if (startup_data.canAcceptConnections == CAC_OK)
-	{
-		bn->bkend_type = B_BACKEND;	/* Can change later to WALSND */
-		bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
-	}
-	else
-	{
-		bn->bkend_type = B_DEAD_END_BACKEND;
-		bn->child_slot = 0;
-	}
-
 	/* Hasn't asked to be notified about any bgworkers yet */
 	bn->bgworker_notify = false;
 
-	pid = postmaster_child_launch(B_BACKEND,
+	MyPMChildSlot = bn->child_slot;
+	pid = postmaster_child_launch(bn->bkend_type,
 								  (char *) &startup_data, sizeof(startup_data),
 								  client_sock);
 	if (pid < 0)
@@ -3406,9 +3320,7 @@ BackendStartup(ClientSocket *client_sock)
 		/* in parent, fork failed */
 		int			save_errno = errno;
 
-		if (bn->child_slot != 0)
-			(void) ReleasePostmasterChildSlot(bn->child_slot);
-		pfree(bn);
+		(void) FreePostmasterChildSlot(bn);
 		errno = save_errno;
 		ereport(LOG,
 				(errmsg("could not fork new process for connection: %m")));
@@ -3426,7 +3338,6 @@ BackendStartup(ClientSocket *client_sock)
 	 * of backends.
 	 */
 	bn->pid = pid;
-	dlist_push_head(&BackendList, &bn->elem);
 
 	return STATUS_OK;
 }
@@ -3525,9 +3436,9 @@ process_pm_pmsignal(void)
 		 * Start the archiver if we're responsible for (re-)archiving received
 		 * files.
 		 */
-		Assert(PgArchPID == 0);
+		Assert(PgArchPMChild == NULL);
 		if (XLogArchivingAlways())
-			PgArchPID = StartChildProcess(B_ARCHIVER);
+			PgArchPMChild = StartChildProcess(B_ARCHIVER);
 
 		/*
 		 * If we aren't planning to enter hot standby mode later, treat
@@ -3573,16 +3484,16 @@ process_pm_pmsignal(void)
 	}
 
 	/* Tell syslogger to rotate logfile if requested */
-	if (SysLoggerPID != 0)
+	if (SysLoggerPMChild != NULL)
 	{
 		if (CheckLogrotateSignal())
 		{
-			signal_child(SysLoggerPID, SIGUSR1);
+			signal_child(SysLoggerPMChild, SIGUSR1);
 			RemoveLogrotateSignalFiles();
 		}
 		else if (CheckPostmasterSignal(PMSIGNAL_ROTATE_LOGFILE))
 		{
-			signal_child(SysLoggerPID, SIGUSR1);
+			signal_child(SysLoggerPMChild, SIGUSR1);
 		}
 	}
 
@@ -3629,7 +3540,7 @@ process_pm_pmsignal(void)
 		PostmasterStateMachine();
 	}
 
-	if (StartupPID != 0 &&
+	if (StartupPMChild != NULL &&
 		(pmState == PM_STARTUP || pmState == PM_RECOVERY ||
 		 pmState == PM_HOT_STANDBY) &&
 		CheckPromoteSignal())
@@ -3640,7 +3551,7 @@ process_pm_pmsignal(void)
 		 * Leave the promote signal file in place and let the Startup process
 		 * do the unlink.
 		 */
-		signal_child(StartupPID, SIGUSR2);
+		signal_child(StartupPMChild, SIGUSR2);
 	}
 }
 
@@ -3667,13 +3578,13 @@ CountChildren(uint32 targetMask)
 	dlist_iter	iter;
 	int			cnt = 0;
 
-	dlist_foreach(iter, &BackendList)
+	dlist_foreach(iter, &ActiveChildList)
 	{
-		Backend    *bp = dlist_container(Backend, elem, iter.cur);
+		PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
 
 		/*
-		 * Since target == BACKEND_TYPE_ALL is the most common case, we test
-		 * it first and avoid touching shared memory for every child.
+		 * Since targetMask == BACKEND_TYPE_ALL is the most common case, we
+		 * test it first and avoid touching shared memory for every child.
 		 */
 		if (targetMask != BACKEND_TYPE_ALL)
 		{
@@ -3704,15 +3615,33 @@ CountChildren(uint32 targetMask)
  * Return value of StartChildProcess is subprocess' PID, or 0 if failed
  * to start subprocess.
  */
-static pid_t
+static PMChild *
 StartChildProcess(BackendType type)
 {
+	PMChild    *pmchild;
 	pid_t		pid;
 
+	pmchild = AssignPostmasterChildSlot(type);
+	if (!pmchild)
+	{
+		if (type == B_AUTOVAC_WORKER)
+			ereport(LOG,
+					(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+					 errmsg("no slot available for new autovacuum worker process")));
+		else
+		{
+			/* shouldn't happen because we allocate enough slots */
+			elog(LOG, "no postmaster child slot available for aux process");
+		}
+		return NULL;
+	}
+
+	MyPMChildSlot = pmchild->child_slot;
 	pid = postmaster_child_launch(type, NULL, 0, NULL);
 	if (pid < 0)
 	{
 		/* in parent, fork failed */
+		FreePostmasterChildSlot(pmchild);
 		ereport(LOG,
 				(errmsg("could not fork \"%s\" process: %m", PostmasterChildName(type))));
 
@@ -3722,13 +3651,14 @@ StartChildProcess(BackendType type)
 		 */
 		if (type == B_STARTUP)
 			ExitPostmaster(1);
-		return 0;
+		return NULL;
 	}
 
 	/*
 	 * in parent, successful fork
 	 */
-	return pid;
+	pmchild->pid = pid;
+	return pmchild;
 }
 
 /*
@@ -3743,7 +3673,7 @@ StartChildProcess(BackendType type)
 static void
 StartAutovacuumWorker(void)
 {
-	Backend    *bn;
+	PMChild    *bn;
 
 	/*
 	 * If not in condition to run a process, don't try, but handle it like a
@@ -3754,34 +3684,20 @@ StartAutovacuumWorker(void)
 	 */
 	if (canAcceptConnections(B_AUTOVAC_WORKER) == CAC_OK)
 	{
-		bn = (Backend *) palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
+		bn = StartChildProcess(B_AUTOVAC_WORKER);
 		if (bn)
 		{
-			/* Autovac workers need a child slot */
-			bn->bkend_type = B_AUTOVAC_WORKER;
-			bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
 			bn->bgworker_notify = false;
 			bn->rw = NULL;
-
-			bn->pid = StartChildProcess(B_AUTOVAC_WORKER);
-			if (bn->pid > 0)
-			{
-				dlist_push_head(&BackendList, &bn->elem);
-				/* all OK */
-				return;
-			}
-
+			return;
+		}
+		else
+		{
 			/*
 			 * fork failed, fall through to report -- actual error message was
 			 * logged by StartChildProcess
 			 */
-			(void) ReleasePostmasterChildSlot(bn->child_slot);
-			pfree(bn);
 		}
-		else
-			ereport(LOG,
-					(errcode(ERRCODE_OUT_OF_MEMORY),
-					 errmsg("out of memory")));
 	}
 
 	/*
@@ -3793,7 +3709,7 @@ StartAutovacuumWorker(void)
 	 * quick succession between the autovac launcher and postmaster in case
 	 * things get ugly.
 	 */
-	if (AutoVacPID != 0)
+	if (AutoVacLauncherPMChild != NULL)
 	{
 		AutoVacWorkerFailed();
 		avlauncher_needs_signal = true;
@@ -3837,23 +3753,6 @@ CreateOptsFile(int argc, char *argv[], char *fullprogname)
 }
 
 
-/*
- * MaxLivePostmasterChildren
- *
- * This reports the number of entries needed in the per-child-process array
- * (PMChildFlags).  It includes regular backends, autovac workers, walsenders
- * and background workers, but not special children nor dead_end children.
- * This allows the array to have a fixed maximum size, to wit the same
- * too-many-children limit enforced by canAcceptConnections().  The exact value
- * isn't too critical as long as it's more than MaxBackends.
- */
-int
-MaxLivePostmasterChildren(void)
-{
-	return 2 * (MaxConnections + autovacuum_max_workers + 1 +
-				max_wal_senders + max_worker_processes);
-}
-
 /*
  * Start a new bgworker.
  * Starting time conditions must have been checked already.
@@ -3866,7 +3765,7 @@ MaxLivePostmasterChildren(void)
 static bool
 do_start_bgworker(RegisteredBgWorker *rw)
 {
-	Backend    *bn;
+	PMChild    *bn;
 	pid_t		worker_pid;
 
 	Assert(rw->rw_pid == 0);
@@ -3893,6 +3792,7 @@ do_start_bgworker(RegisteredBgWorker *rw)
 			(errmsg_internal("starting background worker process \"%s\"",
 							 rw->rw_worker.bgw_name)));
 
+	MyPMChildSlot = bn->child_slot;
 	worker_pid = postmaster_child_launch(B_BG_WORKER, (char *) &rw->rw_worker, sizeof(BackgroundWorker), NULL);
 	if (worker_pid == -1)
 	{
@@ -3900,8 +3800,7 @@ do_start_bgworker(RegisteredBgWorker *rw)
 		ereport(LOG,
 				(errmsg("could not fork background worker process: %m")));
 		/* undo what assign_backendlist_entry did */
-		ReleasePostmasterChildSlot(bn->child_slot);
-		pfree(bn);
+		FreePostmasterChildSlot(bn);
 
 		/* mark entry as crashed, so we'll try again later */
 		rw->rw_crashed_at = GetCurrentTimestamp();
@@ -3912,8 +3811,6 @@ do_start_bgworker(RegisteredBgWorker *rw)
 	rw->rw_pid = worker_pid;
 	bn->pid = rw->rw_pid;
 	ReportBackgroundWorkerPID(rw);
-	/* add new worker to lists of backends */
-	dlist_push_head(&BackendList, &bn->elem);
 	return true;
 }
 
@@ -3961,17 +3858,13 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
  *
  * On failure, return NULL.
  */
-static Backend *
+static PMChild *
 assign_backendlist_entry(void)
 {
-	Backend    *bn;
+	PMChild    *bn;
 
-	/*
-	 * Check that database state allows another connection.  Currently the
-	 * only possible failure is CAC_TOOMANY, so we just log an error message
-	 * based on that rather than checking the error code precisely.
-	 */
-	if (canAcceptConnections(B_BG_WORKER) != CAC_OK)
+	bn = AssignPostmasterChildSlot(B_BG_WORKER);
+	if (bn == NULL)
 	{
 		ereport(LOG,
 				(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
@@ -3979,16 +3872,6 @@ assign_backendlist_entry(void)
 		return NULL;
 	}
 
-	bn = palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
-	if (bn == NULL)
-	{
-		ereport(LOG,
-				(errcode(ERRCODE_OUT_OF_MEMORY),
-				 errmsg("out of memory")));
-		return NULL;
-	}
-
-	bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
 	bn->bkend_type = B_BG_WORKER;
 	bn->bgworker_notify = false;
 
@@ -4129,11 +4012,11 @@ bool
 PostmasterMarkPIDForWorkerNotify(int pid)
 {
 	dlist_iter	iter;
-	Backend    *bp;
+	PMChild    *bp;
 
-	dlist_foreach(iter, &BackendList)
+	dlist_foreach(iter, &ActiveChildList)
 	{
-		bp = dlist_container(Backend, elem, iter.cur);
+		bp = dlist_container(PMChild, elem, iter.cur);
 		if (bp->pid == pid)
 		{
 			bp->bgworker_notify = true;
diff --git a/src/backend/storage/ipc/pmsignal.c b/src/backend/storage/ipc/pmsignal.c
index cb99e77476..86970bf69b 100644
--- a/src/backend/storage/ipc/pmsignal.c
+++ b/src/backend/storage/ipc/pmsignal.c
@@ -47,11 +47,11 @@
  * exited without performing proper shutdown.  The per-child-process flags
  * have three possible states: UNUSED, ASSIGNED, ACTIVE.  An UNUSED slot is
  * available for assignment.  An ASSIGNED slot is associated with a postmaster
- * child process, but either the process has not touched shared memory yet,
- * or it has successfully cleaned up after itself.  A ACTIVE slot means the
- * process is actively using shared memory.  The slots are assigned to
- * child processes at random, and postmaster.c is responsible for tracking
- * which one goes with which PID.
+ * child process, but either the process has not touched shared memory yet, or
+ * it has successfully cleaned up after itself.  An ACTIVE slot means the
+ * process is actively using shared memory.  The slots are assigned to child
+ * processes by postmaster, and postmaster.c is responsible for tracking which
+ * one goes with which PID.
  *
  * Actually there is a fourth state, WALSENDER.  This is just like ACTIVE,
  * but carries the extra information that the child is a WAL sender.
@@ -83,15 +83,6 @@ struct PMSignalData
 /* PMSignalState pointer is valid in both postmaster and child processes */
 NON_EXEC_STATIC volatile PMSignalData *PMSignalState = NULL;
 
-/*
- * These static variables are valid only in the postmaster.  We keep a
- * duplicative private array so that we can trust its state even if some
- * failing child has clobbered the PMSignalData struct in shared memory.
- */
-static int	num_child_inuse;	/* # of entries in PMChildInUse[] */
-static int	next_child_inuse;	/* next slot to try to assign */
-static bool *PMChildInUse;		/* true if i'th flag slot is assigned */
-
 /*
  * Signal handler to be notified if postmaster dies.
  */
@@ -155,25 +146,7 @@ PMSignalShmemInit(void)
 	{
 		/* initialize all flags to zeroes */
 		MemSet(unvolatize(PMSignalData *, PMSignalState), 0, PMSignalShmemSize());
-		num_child_inuse = MaxLivePostmasterChildren();
-		PMSignalState->num_child_flags = num_child_inuse;
-
-		/*
-		 * Also allocate postmaster's private PMChildInUse[] array.  We
-		 * might've already done that in a previous shared-memory creation
-		 * cycle, in which case free the old array to avoid a leak.  (Do it
-		 * like this to support the possibility that MaxLivePostmasterChildren
-		 * changed.)  In a standalone backend, we do not need this.
-		 */
-		if (PostmasterContext != NULL)
-		{
-			if (PMChildInUse)
-				pfree(PMChildInUse);
-			PMChildInUse = (bool *)
-				MemoryContextAllocZero(PostmasterContext,
-									   num_child_inuse * sizeof(bool));
-		}
-		next_child_inuse = 0;
+		PMSignalState->num_child_flags = MaxLivePostmasterChildren();
 	}
 }
 
@@ -239,41 +212,22 @@ GetQuitSignalReason(void)
 
 
 /*
- * AssignPostmasterChildSlot - select an unused slot for a new postmaster
- * child process, and set its state to ASSIGNED.  Returns a slot number
- * (one to N).
+ * ReservePostmasterChildSlot - mark the given slot as ASSIGNED for a new
+ * postmaster child process.
  *
  * Only the postmaster is allowed to execute this routine, so we need no
  * special locking.
  */
-int
-AssignPostmasterChildSlot(void)
+void
+ReservePostmasterChildSlot(int slot)
 {
-	int			slot = next_child_inuse;
-	int			n;
+	Assert(slot > 0 && slot <= PMSignalState->num_child_flags);
+	slot--;
 
-	/*
-	 * Scan for a free slot.  Notice that we trust nothing about the contents
-	 * of PMSignalState, but use only postmaster-local data for this decision.
-	 * We track the last slot assigned so as not to waste time repeatedly
-	 * rescanning low-numbered slots.
-	 */
-	for (n = num_child_inuse; n > 0; n--)
-	{
-		if (--slot < 0)
-			slot = num_child_inuse - 1;
-		if (!PMChildInUse[slot])
-		{
-			PMChildInUse[slot] = true;
-			PMSignalState->PMChildFlags[slot] = PM_CHILD_ASSIGNED;
-			next_child_inuse = slot;
-			return slot + 1;
-		}
-	}
+	if (PMSignalState->PMChildFlags[slot] != PM_CHILD_UNUSED)
+		elog(FATAL, "postmaster child slot is already in use");
 
-	/* Out of slots ... should never happen, else postmaster.c messed up */
-	elog(FATAL, "no free slots in PMChildFlags array");
-	return 0;					/* keep compiler quiet */
+	PMSignalState->PMChildFlags[slot] = PM_CHILD_ASSIGNED;
 }
 
 /*
@@ -288,17 +242,18 @@ ReleasePostmasterChildSlot(int slot)
 {
 	bool		result;
 
-	Assert(slot > 0 && slot <= num_child_inuse);
+	Assert(slot > 0 && slot <= PMSignalState->num_child_flags);
 	slot--;
 
 	/*
 	 * Note: the slot state might already be unused, because the logic in
 	 * postmaster.c is such that this might get called twice when a child
 	 * crashes.  So we don't try to Assert anything about the state.
+	 *
+	 * FIXME: does that still happen?
 	 */
 	result = (PMSignalState->PMChildFlags[slot] == PM_CHILD_ASSIGNED);
 	PMSignalState->PMChildFlags[slot] = PM_CHILD_UNUSED;
-	PMChildInUse[slot] = false;
 	return result;
 }
 
@@ -309,7 +264,7 @@ ReleasePostmasterChildSlot(int slot)
 bool
 IsPostmasterChildWalSender(int slot)
 {
-	Assert(slot > 0 && slot <= num_child_inuse);
+	Assert(slot > 0 && slot <= PMSignalState->num_child_flags);
 	slot--;
 
 	if (PMSignalState->PMChildFlags[slot] == PM_CHILD_WALSENDER)
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 37b1c67600..24892d31a5 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -310,14 +310,9 @@ InitProcess(void)
 	/*
 	 * Before we start accessing the shared memory in a serious way, mark
 	 * ourselves as an active postmaster child; this is so that the postmaster
-	 * can detect it if we exit without cleaning up.  (XXX autovac launcher
-	 * currently doesn't participate in this; it probably should.)
-	 *
-	 * Slot sync worker also does not participate in it, see comments atop
-	 * 'struct bkend' in postmaster.c.
+	 * can detect it if we exit without cleaning up.
 	 */
-	if (IsUnderPostmaster && !AmAutoVacuumLauncherProcess() &&
-		!AmLogicalSlotSyncWorkerProcess())
+	if (IsUnderPostmaster)
 		MarkPostmasterChildActive();
 
 	/* Decide which list should supply our PGPROC. */
@@ -535,6 +530,9 @@ InitAuxiliaryProcess(void)
 	if (MyProc != NULL)
 		elog(ERROR, "you already exist");
 
+	if (IsUnderPostmaster)
+		MarkPostmasterChildActive();
+
 	/*
 	 * We use the ProcStructLock to protect assignment and releasing of
 	 * AuxiliaryProcs entries.
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 63c12917cf..deca2e8370 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -13,8 +13,39 @@
 #ifndef _POSTMASTER_H
 #define _POSTMASTER_H
 
+#include "lib/ilist.h"
 #include "miscadmin.h"
 
+/*
+ * A struct representing an active postmaster child process.  This is used
+ * mainly to keep track of how many children we have and send them appropriate
+ * signals when necessary.  All postmaster child processes are assigned a
+ * PMChild entry. That includes "normal" client sessions, but also autovacuum
+ * workers, walsenders, background workers, and aux processes.  (Note that at
+ * the time of launch, walsenders are labeled B_BACKEND; we relabel them to
+ * B_WAL_SENDER upon noticing they've changed their PMChildFlags entry.  Hence
+ * that check must be done before any operation that needs to distinguish
+ * walsenders from normal backends.)
+ *
+ * "dead_end" children are also allocated a PMChild entry: these are children
+ * launched just for the purpose of sending a friendly rejection message to a
+ * would-be client.  We must track them because they are attached to shared
+ * memory, but we know they will never become live backends.
+ *
+ * 'child_slot' is an identifier that is unique across all running child
+ * processes.  It is used as an index into the PMChildFlags array. dead_end
+ * children are not assigned a child_slot.
+ */
+typedef struct
+{
+	pid_t		pid;			/* process id of backend */
+	int			child_slot;		/* PMChildSlot for this backend, if any */
+	BackendType bkend_type;		/* child process flavor, see above */
+	struct RegisteredBgWorker *rw;	/* bgworker info, if this is a bgworker */
+	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
+	dlist_node	elem;			/* list link in BackendList */
+} PMChild;
+
 /* GUC options */
 extern PGDLLIMPORT bool EnableSSL;
 extern PGDLLIMPORT int SuperuserReservedConnections;
@@ -80,6 +111,15 @@ const char *PostmasterChildName(BackendType child_type);
 extern void SubPostmasterMain(int argc, char *argv[]) pg_attribute_noreturn();
 #endif
 
+/* prototypes for functions in pmchild.c */
+extern dlist_head ActiveChildList;
+
+extern void InitPostmasterChildSlots(void);
+extern PMChild *AssignPostmasterChildSlot(BackendType btype);
+extern bool FreePostmasterChildSlot(PMChild *pmchild);
+extern PMChild *FindPostmasterChildByPid(int pid);
+extern PMChild *AllocDeadEndChild(void);
+
 /*
  * Note: MAX_BACKENDS is limited to 2^18-1 because that's the width reserved
  * for buffer references in buf_internals.h.  This limitation could be lifted
diff --git a/src/include/storage/pmsignal.h b/src/include/storage/pmsignal.h
index 3b9336b83c..2ab198fc31 100644
--- a/src/include/storage/pmsignal.h
+++ b/src/include/storage/pmsignal.h
@@ -70,7 +70,7 @@ extern void SendPostmasterSignal(PMSignalReason reason);
 extern bool CheckPostmasterSignal(PMSignalReason reason);
 extern void SetQuitSignalReason(QuitSignalReason reason);
 extern QuitSignalReason GetQuitSignalReason(void);
-extern int	AssignPostmasterChildSlot(void);
+extern void ReservePostmasterChildSlot(int slot);
 extern bool ReleasePostmasterChildSlot(int slot);
 extern bool IsPostmasterChildWalSender(int slot);
 extern void MarkPostmasterChildActive(void);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8de9978ad8..b43d6eb558 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -230,7 +230,6 @@ BTWriteState
 BUF_MEM
 BYTE
 BY_HANDLE_FILE_INFORMATION
-Backend
 BackendParameters
 BackendStartupData
 BackendState
@@ -1927,6 +1926,7 @@ PLyTransformToOb
 PLyTupleToOb
 PLyUnicode_FromStringAndSize_t
 PLy_elog_impl_t
+PMChild
 PMINIDUMP_CALLBACK_INFORMATION
 PMINIDUMP_EXCEPTION_INFORMATION
 PMINIDUMP_USER_STREAM_INFORMATION
-- 
2.39.2



  [text/x-patch] v3-0011-Pass-MyPMChildSlot-as-an-explicit-argument-to-chi.patch (10.7K, ../../[email protected]/12-v3-0011-Pass-MyPMChildSlot-as-an-explicit-argument-to-chi.patch)
  download | inline diff:
From 35ffc568518383110fac9aee6316219d5708ac0d Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 1 Aug 2024 22:58:17 +0300
Subject: [PATCH v3 11/12] Pass MyPMChildSlot as an explicit argument to child
 process

All the other global variables passed from postmaster to child are
have the same value in all the processes, while MyPMChildSlot is more
like a parameter to each child process.
---
 src/backend/postmaster/launch_backend.c | 32 ++++++++++++++++---------
 src/backend/postmaster/pmchild.c        |  3 ---
 src/backend/postmaster/postmaster.c     | 16 ++++++-------
 src/backend/postmaster/syslogger.c      |  8 ++++---
 src/include/postmaster/postmaster.h     |  1 +
 src/include/postmaster/syslogger.h      |  2 +-
 6 files changed, 35 insertions(+), 27 deletions(-)

diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index b0b91dc97f..4e93bd1d94 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -96,7 +96,6 @@ typedef int InheritableSocket;
 typedef struct
 {
 	char		DataDir[MAXPGPATH];
-	int			MyPMChildSlot;
 #ifndef WIN32
 	unsigned long UsedShmemSegID;
 #else
@@ -137,6 +136,8 @@ typedef struct
 	char		my_exec_path[MAXPGPATH];
 	char		pkglib_path[MAXPGPATH];
 
+	int			MyPMChildSlot;
+
 	/*
 	 * These are only used by backend processes, but are here because passing
 	 * a socket needs some special handling on Windows. 'client_sock' is an
@@ -158,13 +159,16 @@ typedef struct
 static void read_backend_variables(char *id, char **startup_data, size_t *startup_data_len);
 static void restore_backend_variables(BackendParameters *param);
 
-static bool save_backend_variables(BackendParameters *param, ClientSocket *client_sock,
+static bool save_backend_variables(BackendParameters *param, int child_slot,
+								   ClientSocket *client_sock,
 #ifdef WIN32
 								   HANDLE childProcess, pid_t childPid,
 #endif
 								   char *startup_data, size_t startup_data_len);
 
-static pid_t internal_forkexec(const char *child_kind, char *startup_data, size_t startup_data_len, ClientSocket *client_sock);
+static pid_t internal_forkexec(const char *child_kind, int child_slot,
+							   char *startup_data, size_t startup_data_len,
+							   ClientSocket *client_sock);
 
 #endif							/* EXEC_BACKEND */
 
@@ -226,7 +230,7 @@ PostmasterChildName(BackendType child_type)
  * the child process.
  */
 pid_t
-postmaster_child_launch(BackendType child_type,
+postmaster_child_launch(BackendType child_type, int child_slot,
 						char *startup_data, size_t startup_data_len,
 						ClientSocket *client_sock)
 {
@@ -235,7 +239,7 @@ postmaster_child_launch(BackendType child_type,
 	Assert(IsPostmasterEnvironment && !IsUnderPostmaster);
 
 #ifdef EXEC_BACKEND
-	pid = internal_forkexec(child_process_kinds[child_type].name,
+	pid = internal_forkexec(child_process_kinds[child_type].name, child_slot,
 							startup_data, startup_data_len, client_sock);
 	/* the child process will arrive in SubPostmasterMain */
 #else							/* !EXEC_BACKEND */
@@ -263,6 +267,7 @@ postmaster_child_launch(BackendType child_type,
 		 */
 		MemoryContextSwitchTo(TopMemoryContext);
 
+		MyPMChildSlot = child_slot;
 		if (client_sock)
 		{
 			MyClientSocket = palloc(sizeof(ClientSocket));
@@ -289,7 +294,8 @@ postmaster_child_launch(BackendType child_type,
  * - fork():s, and then exec():s the child process
  */
 static pid_t
-internal_forkexec(const char *child_kind, char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
+internal_forkexec(const char *child_kind, int child_slot,
+				  char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
 {
 	static unsigned long tmpBackendFileNum = 0;
 	pid_t		pid;
@@ -309,7 +315,7 @@ internal_forkexec(const char *child_kind, char *startup_data, size_t startup_dat
 	 */
 	paramsz = SizeOfBackendParameters(startup_data_len);
 	param = palloc0(paramsz);
-	if (!save_backend_variables(param, client_sock, startup_data, startup_data_len))
+	if (!save_backend_variables(param, child_slot, client_sock, startup_data, startup_data_len))
 	{
 		pfree(param);
 		return -1;				/* log made by save_backend_variables */
@@ -398,7 +404,8 @@ internal_forkexec(const char *child_kind, char *startup_data, size_t startup_dat
  *	 file is complete.
  */
 static pid_t
-internal_forkexec(const char *child_kind, char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
+internal_forkexec(const char *child_kind, int child_slot,
+				  char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
 {
 	int			retry_count = 0;
 	STARTUPINFO si;
@@ -479,7 +486,9 @@ retry:
 		return -1;
 	}
 
-	if (!save_backend_variables(param, client_sock, pi.hProcess, pi.dwProcessId, startup_data, startup_data_len))
+	if (!save_backend_variables(param, child_slot, client_sock,
+								pi.hProcess, pi.dwProcessId,
+								startup_data, startup_data_len))
 	{
 		/*
 		 * log made by save_backend_variables, but we have to clean up the
@@ -691,7 +700,8 @@ static void read_inheritable_socket(SOCKET *dest, InheritableSocket *src);
 
 /* Save critical backend variables into the BackendParameters struct */
 static bool
-save_backend_variables(BackendParameters *param, ClientSocket *client_sock,
+save_backend_variables(BackendParameters *param,
+					   int child_slot, ClientSocket *client_sock,
 #ifdef WIN32
 					   HANDLE childProcess, pid_t childPid,
 #endif
@@ -708,7 +718,7 @@ save_backend_variables(BackendParameters *param, ClientSocket *client_sock,
 
 	strlcpy(param->DataDir, DataDir, MAXPGPATH);
 
-	param->MyPMChildSlot = MyPMChildSlot;
+	param->MyPMChildSlot = child_slot;
 
 #ifdef WIN32
 	param->ShmemProtectiveRegion = ShmemProtectiveRegion;
diff --git a/src/backend/postmaster/pmchild.c b/src/backend/postmaster/pmchild.c
index e86982d6d1..25cccc5514 100644
--- a/src/backend/postmaster/pmchild.c
+++ b/src/backend/postmaster/pmchild.c
@@ -209,9 +209,6 @@ AssignPostmasterChildSlot(BackendType btype)
 
 	ReservePostmasterChildSlot(pmchild->child_slot);
 
-	/* FIXME: find a more elegant way to pass this */
-	MyPMChildSlot = pmchild->child_slot;
-
 	elog(DEBUG2, "assigned pm child slot %d for %s", pmchild->child_slot, PostmasterChildName(btype));
 
 	return pmchild;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index a928a04c7a..67035241b3 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -983,7 +983,7 @@ PostmasterMain(int argc, char *argv[])
 	SysLoggerPMChild = AssignPostmasterChildSlot(B_LOGGER);
 	if (!SysLoggerPMChild)
 		elog(ERROR, "no postmaster child slot available for syslogger");
-	SysLoggerPMChild->pid = SysLogger_Start();
+	SysLoggerPMChild->pid = SysLogger_Start(SysLoggerPMChild->child_slot);
 	if (SysLoggerPMChild->pid == 0)
 	{
 		FreePostmasterChildSlot(SysLoggerPMChild);
@@ -2414,7 +2414,7 @@ process_pm_child_exit(void)
 		if (SysLoggerPMChild && pid == SysLoggerPMChild->pid)
 		{
 			/* for safety's sake, launch new logger *first* */
-			SysLoggerPMChild->pid = SysLogger_Start();
+			SysLoggerPMChild->pid = SysLogger_Start(SysLoggerPMChild->child_slot);
 			if (SysLoggerPMChild->pid == 0)
 			{
 				FreePostmasterChildSlot(SysLoggerPMChild);
@@ -3039,7 +3039,7 @@ LaunchMissingBackgroundProcesses(void)
 			elog(LOG, "no postmaster child slot available for syslogger");
 		else
 		{
-			SysLoggerPMChild->pid = SysLogger_Start();
+			SysLoggerPMChild->pid = SysLogger_Start(SysLoggerPMChild->child_slot);
 			if (SysLoggerPMChild->pid == 0)
 			{
 				FreePostmasterChildSlot(SysLoggerPMChild);
@@ -3311,8 +3311,7 @@ BackendStartup(ClientSocket *client_sock)
 	/* Hasn't asked to be notified about any bgworkers yet */
 	bn->bgworker_notify = false;
 
-	MyPMChildSlot = bn->child_slot;
-	pid = postmaster_child_launch(bn->bkend_type,
+	pid = postmaster_child_launch(bn->bkend_type, bn->child_slot,
 								  (char *) &startup_data, sizeof(startup_data),
 								  client_sock);
 	if (pid < 0)
@@ -3636,8 +3635,7 @@ StartChildProcess(BackendType type)
 		return NULL;
 	}
 
-	MyPMChildSlot = pmchild->child_slot;
-	pid = postmaster_child_launch(type, NULL, 0, NULL);
+	pid = postmaster_child_launch(type, pmchild->child_slot, NULL, 0, NULL);
 	if (pid < 0)
 	{
 		/* in parent, fork failed */
@@ -3792,8 +3790,8 @@ do_start_bgworker(RegisteredBgWorker *rw)
 			(errmsg_internal("starting background worker process \"%s\"",
 							 rw->rw_worker.bgw_name)));
 
-	MyPMChildSlot = bn->child_slot;
-	worker_pid = postmaster_child_launch(B_BG_WORKER, (char *) &rw->rw_worker, sizeof(BackgroundWorker), NULL);
+	worker_pid = postmaster_child_launch(B_BG_WORKER, bn->child_slot,
+										 (char *) &rw->rw_worker, sizeof(BackgroundWorker), NULL);
 	if (worker_pid == -1)
 	{
 		/* in postmaster, fork failed ... */
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index 7951599fa8..d68853d429 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -590,7 +590,7 @@ SysLoggerMain(char *startup_data, size_t startup_data_len)
  * Postmaster subroutine to start a syslogger subprocess.
  */
 int
-SysLogger_Start(void)
+SysLogger_Start(int child_slot)
 {
 	pid_t		sysloggerPid;
 	char	   *filename;
@@ -699,9 +699,11 @@ SysLogger_Start(void)
 	startup_data.syslogFile = syslogger_fdget(syslogFile);
 	startup_data.csvlogFile = syslogger_fdget(csvlogFile);
 	startup_data.jsonlogFile = syslogger_fdget(jsonlogFile);
-	sysloggerPid = postmaster_child_launch(B_LOGGER, (char *) &startup_data, sizeof(startup_data), NULL);
+	sysloggerPid = postmaster_child_launch(B_LOGGER, child_slot,
+										   (char *) &startup_data, sizeof(startup_data), NULL);
 #else
-	sysloggerPid = postmaster_child_launch(B_LOGGER, NULL, 0, NULL);
+	sysloggerPid = postmaster_child_launch(B_LOGGER, child_slot,
+										   NULL, 0, NULL);
 #endif							/* EXEC_BACKEND */
 
 	if (sysloggerPid == -1)
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index deca2e8370..81a3520021 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -103,6 +103,7 @@ extern PGDLLIMPORT struct ClientSocket *MyClientSocket;
 
 /* prototypes for functions in launch_backend.c */
 extern pid_t postmaster_child_launch(BackendType child_type,
+									 int child_slot,
 									 char *startup_data,
 									 size_t startup_data_len,
 									 struct ClientSocket *client_sock);
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index b5fc239ba9..d72b978b0a 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -86,7 +86,7 @@ extern PGDLLIMPORT HANDLE syslogPipe[2];
 #endif
 
 
-extern int	SysLogger_Start(void);
+extern int	SysLogger_Start(int child_slot);
 
 extern void write_syslogger_file(const char *buffer, int count, int destination);
 
-- 
2.39.2



^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-08-08 10:47  Thomas Munro <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Thomas Munro @ 2024-08-08 10:47 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: pgsql-hackers

On Fri, Aug 2, 2024 at 11:57 AM Heikki Linnakangas <[email protected]> wrote:
> * v3-0001-Make-BackgroundWorkerList-doubly-linked.patch

LGTM.

> [v3-0002-Refactor-code-to-handle-death-of-a-backend-or-bgw.patch]

    Currently, when a child process exits, the postmaster first scans
    through BackgroundWorkerList, to see if it the child process was a
    background worker. If not found, then it scans through BackendList to
    see if it was a regular backend. That leads to some duplication
    between the bgworker and regular backend cleanup code, as both have an
    entry in the BackendList that needs to be cleaned up in the same way.
    Refactor that so that we scan just the BackendList to find the child
    process, and if it was a background worker, do the additional
    bgworker-specific cleanup in addition to the normal Backend cleanup.

Makes sense.

    On Windows, if a child process exits with ERROR_WAIT_NO_CHILDREN, it's
    now logged with that exit code, instead of 0. Also, if a bgworker
    exits with ERROR_WAIT_NO_CHILDREN, it's now treated as crashed and is
    restarted. Previously it was treated as a normal exit.

Interesting.  So when that error was first specially handled in this thread:

https://www.postgresql.org/message-id/flat/AANLkTimCTkNKKrHCd3Ot6kAsrSS7SeDpOTcaLsEP7i%2BM%40mail.gm...

... it went from being considered a crash, to being considered like
exit(0).  It's true that shared memory can't be corrupted by a process
that never enters main(), but it's better not to hide the true reason
for the failure (if it is still possible -- I don't find many
references to that phenomenon in recent times).  Clobbering exitstatus
with 0 doesn't seem right at all, now that we have background workers
whose restart behaviour is affected by that.

    If a child process is not found in the BackendList, the log message
    now calls it "untracked child process" rather than "server process".
    Arguably that should be a PANIC, because we do track all the child
    processes in the list, so failing to find a child process is highly
    unexpected. But if we want to change that, let's discuss and do that
    as a separate commit.

Yeah, it would be highly unexpected if waitpid() told you about some
random other process (or we screwed up the bookkeeping and didn't
recognise it).  So at least having a different message seems good.

> * v3-0003-Fix-comment-on-processes-being-kept-over-a-restar.patch

+1

> * v3-0004-Consolidate-postmaster-code-to-launch-background-.patch

    Much of the code in process_pm_child_exit() to launch replacement
    processes when one exits or when progressing to next postmaster state
    was unnecessary, because the ServerLoop will launch any missing
    background processes anyway. Remove the redundant code and let
    ServerLoop handle it.

+1, makes sense.

    In ServerLoop, move the code to launch all the processes to a new
    subroutine, to group it all together.

+1, makes sense.

More soon...






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-08-09 21:13  Heikki Linnakangas <[email protected]>
  parent: Thomas Munro <[email protected]>
  0 siblings, 2 replies; 39+ messages in thread

From: Heikki Linnakangas @ 2024-08-09 21:13 UTC (permalink / raw)
  To: Thomas Munro <[email protected]>; +Cc: pgsql-hackers

On 08/08/2024 13:47, Thomas Munro wrote:
>      On Windows, if a child process exits with ERROR_WAIT_NO_CHILDREN, it's
>      now logged with that exit code, instead of 0. Also, if a bgworker
>      exits with ERROR_WAIT_NO_CHILDREN, it's now treated as crashed and is
>      restarted. Previously it was treated as a normal exit.
> 
> Interesting.  So when that error was first specially handled in this thread:
> 
> https://www.postgresql.org/message-id/flat/AANLkTimCTkNKKrHCd3Ot6kAsrSS7SeDpOTcaLsEP7i%2BM%40mail.gm...
> 
> ... it went from being considered a crash, to being considered like
> exit(0).  It's true that shared memory can't be corrupted by a process
> that never enters main(), but it's better not to hide the true reason
> for the failure (if it is still possible -- I don't find many
> references to that phenomenon in recent times).  Clobbering exitstatus
> with 0 doesn't seem right at all, now that we have background workers
> whose restart behaviour is affected by that.

I adjusted this ERROR_WAIT_NO_CHILDREN a little more, to avoid logging 
the death of the child twice in some cases.

>> * v3-0003-Fix-comment-on-processes-being-kept-over-a-restar.patch
> 
> +1

Committed the patches up to and including this one, with tiny comment 
changes.

>> * v3-0004-Consolidate-postmaster-code-to-launch-background-.patch
> 
>      Much of the code in process_pm_child_exit() to launch replacement
>      processes when one exits or when progressing to next postmaster state
>      was unnecessary, because the ServerLoop will launch any missing
>      background processes anyway. Remove the redundant code and let
>      ServerLoop handle it.

I'm going to work a little more on the comments on this one before 
committing; I had just moved all the "If we have lost the XXX, try to 
start a new one" comments as is, but they look pretty repetitive now.

Thanks for the review!

-- 
Heikki Linnakangas
Neon (https://neon.tech)







^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-08-12 09:55  Heikki Linnakangas <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  1 sibling, 2 replies; 39+ messages in thread

From: Heikki Linnakangas @ 2024-08-12 09:55 UTC (permalink / raw)
  To: Thomas Munro <[email protected]>; +Cc: pgsql-hackers

On 10/08/2024 00:13, Heikki Linnakangas wrote:
> On 08/08/2024 13:47, Thomas Munro wrote:
>>> * v3-0004-Consolidate-postmaster-code-to-launch-background-.patch
>>
>>      Much of the code in process_pm_child_exit() to launch replacement
>>      processes when one exits or when progressing to next postmaster 
>> state
>>      was unnecessary, because the ServerLoop will launch any missing
>>      background processes anyway. Remove the redundant code and let
>>      ServerLoop handle it.
> 
> I'm going to work a little more on the comments on this one before 
> committing; I had just moved all the "If we have lost the XXX, try to 
> start a new one" comments as is, but they look pretty repetitive now.

Pushed this now, after adjusting the comments a bit. Thanks again for 
the review!

Here are the remaining patches, rebased.

> commit a1c43d65907d20a999b203e465db1277ec842a0a
> Author: Heikki Linnakangas <[email protected]>
> Date:   Thu Aug 1 17:24:12 2024 +0300
> 
>     Introduce a separate BackendType for dead-end children
>     
>     And replace postmaster.c's own "backend type" codes with BackendType
>     
>     XXX: While working on this, many times I accidentally did something
>     like "foo |= B_SOMETHING" instead of "foo |= 1 << B_SOMETHING", when
>     constructing arguments to SignalSomeChildren or CountChildren, and
>     things broke in very subtle ways taking a long time to debug. The old
>     constants that were already bitmasks avoided that. Maybe we need some
>     macro magic or something to make this less error-prone.

While rebasing this today, I spotted another instance of that mistake 
mentioned in the XXX comment above. I called "CountChildren(B_BACKEND)" 
instead of "CountChildren(1 << B_BACKEND)". Some ideas on how to make 
that less error-prone:

1. Add a separate typedef for the bitmasks, and macros/functions to work 
with it. Something like:

typedef struct {
	uint32		mask;
} BackendTypeMask;

static const BackendTypeMask BTMASK_ALL = { 0xffffffff };
static const BackendTypeMask BTMASK_NONE = { 0 };

static inline BackendTypeMask
BTMASK_ADD(BackendTypeMask mask, BackendType t)
{
	mask.mask |= 1 << t;
	return mask;
}

static inline BackendTypeMask
BTMASK_DEL(BackendTypeMask mask, BackendType t)
{
	mask.mask &= ~(1 << t);
	return mask;
}

Now the compiler will complain if you try to pass a BackendType for the 
mask. We could do this just for BackendType, or we could put this in 
src/include/lib/ with a more generic name, like "bitmask_u32".

2. Another idea is to redefine the BackendType values to be separate 
bits, like the current BACKEND_TYPE_* values in postmaster.c:

typedef enum BackendType
{
	B_INVALID = 0,

	/* Backends and other backend-like processes */
	B_BACKEND = 1 << 1,
	B_DEAD_END_BACKEND = 1 << 2,
	B_AUTOVAC_LAUNCHER = 1 << 3,
	B_AUTOVAC_WORKER = 1 << 4,

	...
} BackendType;

Then you can use | and & on BackendTypes directly. It makes it less 
clear which function arguments are a BackendType and which are a 
bitmask, however.


Thoughts, other ideas?

-- 
Heikki Linnakangas
Neon (https://neon.tech)


Attachments:

  [text/x-patch] v4-0001-Add-test-for-connection-limits.patch (6.4K, ../../[email protected]/2-v4-0001-Add-test-for-connection-limits.patch)
  download | inline diff:
From 57216e6203deb99bed7a9cc5ab1b07bbdcf808cc Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 12 Aug 2024 10:55:05 +0300
Subject: [PATCH v4 1/8] Add test for connection limits

---
 src/test/Makefile                             |  2 +-
 src/test/meson.build                          |  1 +
 src/test/postmaster/Makefile                  | 23 ++++++
 src/test/postmaster/README                    | 27 +++++++
 src/test/postmaster/meson.build               | 12 +++
 .../postmaster/t/001_connection_limits.pl     | 79 +++++++++++++++++++
 6 files changed, 143 insertions(+), 1 deletion(-)
 create mode 100644 src/test/postmaster/Makefile
 create mode 100644 src/test/postmaster/README
 create mode 100644 src/test/postmaster/meson.build
 create mode 100644 src/test/postmaster/t/001_connection_limits.pl

diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..abdd6e5a98 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,7 +12,7 @@ subdir = src/test
 top_builddir = ../..
 include $(top_builddir)/src/Makefile.global
 
-SUBDIRS = perl regress isolation modules authentication recovery subscription
+SUBDIRS = perl postmaster regress isolation modules authentication recovery subscription
 
 ifeq ($(with_icu),yes)
 SUBDIRS += icu
diff --git a/src/test/meson.build b/src/test/meson.build
index c3d0dfedf1..67376e4b7f 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -4,6 +4,7 @@ subdir('regress')
 subdir('isolation')
 
 subdir('authentication')
+subdir('postmaster')
 subdir('recovery')
 subdir('subscription')
 subdir('modules')
diff --git a/src/test/postmaster/Makefile b/src/test/postmaster/Makefile
new file mode 100644
index 0000000000..dfcce9c9ee
--- /dev/null
+++ b/src/test/postmaster/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/postmaster
+#
+# Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/postmaster/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/postmaster
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean:
+	rm -rf tmp_check
diff --git a/src/test/postmaster/README b/src/test/postmaster/README
new file mode 100644
index 0000000000..7e47bf5cff
--- /dev/null
+++ b/src/test/postmaster/README
@@ -0,0 +1,27 @@
+src/test/postmaster/README
+
+Regression tests for postmaster
+===============================
+
+This directory contains a test suite for postmaster's handling of
+connections, connection limits, and startup/shutdown sequence.
+
+
+Running the tests
+=================
+
+NOTE: You must have given the --enable-tap-tests argument to configure.
+
+Run
+    make check
+or
+    make installcheck
+You can use "make installcheck" if you previously did "make install".
+In that case, the code in the installation tree is tested.  With
+"make check", a temporary installation tree is built from the current
+sources and then tested.
+
+Either way, this test initializes, starts, and stops a test Postgres
+cluster.
+
+See src/test/perl/README for more info about running these tests.
diff --git a/src/test/postmaster/meson.build b/src/test/postmaster/meson.build
new file mode 100644
index 0000000000..c2de2e0eb5
--- /dev/null
+++ b/src/test/postmaster/meson.build
@@ -0,0 +1,12 @@
+# Copyright (c) 2022-2024, PostgreSQL Global Development Group
+
+tests += {
+  'name': 'postmaster',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_connection_limits.pl',
+    ],
+  },
+}
diff --git a/src/test/postmaster/t/001_connection_limits.pl b/src/test/postmaster/t/001_connection_limits.pl
new file mode 100644
index 0000000000..f50aae4949
--- /dev/null
+++ b/src/test/postmaster/t/001_connection_limits.pl
@@ -0,0 +1,79 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# Test connection limits, i.e. max_connections, reserved_connections
+# and superuser_reserved_connections.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize the server with specific low connection limits
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->append_conf('postgresql.conf', "max_connections = 6");
+$node->append_conf('postgresql.conf', "reserved_connections = 2");
+$node->append_conf('postgresql.conf', "superuser_reserved_connections = 1");
+$node->append_conf('postgresql.conf', "log_connections = on");
+$node->start;
+
+$node->safe_psql(
+	'postgres', qq{
+CREATE USER regress_regular LOGIN;
+CREATE USER regress_reserved LOGIN;
+GRANT pg_use_reserved_connections TO regress_reserved;
+CREATE USER regress_superuser LOGIN SUPERUSER;
+});
+
+# With the limits we set in postgresql.conf, we can establish:
+# - 3 connections for any user with no special privileges
+# - 2 more connections for users belonging to "pg_use_reserved_connections"
+# - 1 more connection for superuser
+
+sub background_psql_as_user
+{
+	my $user = shift;
+
+	return $node->background_psql(
+		'postgres',
+		on_error_die => 1,
+		extra_params => [ '-U', $user ]);
+}
+
+my @sessions = ();
+
+push(@sessions, background_psql_as_user('regress_regular'));
+push(@sessions, background_psql_as_user('regress_regular'));
+push(@sessions, background_psql_as_user('regress_regular'));
+$node->connect_fails(
+	"dbname=postgres user=regress_regular",
+	"reserved_connections limit",
+	expected_stderr =>
+	  qr/FATAL:  remaining connection slots are reserved for roles with privileges of the "pg_use_reserved_connections" role/
+);
+
+push(@sessions, background_psql_as_user('regress_reserved'));
+push(@sessions, background_psql_as_user('regress_reserved'));
+$node->connect_fails(
+	"dbname=postgres user=regress_regular",
+	"reserved_connections limit",
+	expected_stderr =>
+	  qr/FATAL:  remaining connection slots are reserved for roles with the SUPERUSER attribute/
+);
+
+push(@sessions, background_psql_as_user('regress_superuser'));
+$node->connect_fails(
+	"dbname=postgres user=regress_superuser",
+	"superuser_reserved_connections limit",
+	expected_stderr => qr/FATAL:  sorry, too many clients already/);
+
+# TODO: test that query cancellation is still possible
+
+foreach my $session (@sessions)
+{
+	$session->quit;
+}
+
+done_testing();
-- 
2.39.2



  [text/x-patch] v4-0002-Add-test-for-dead-end-backends.patch (3.2K, ../../[email protected]/3-v4-0002-Add-test-for-dead-end-backends.patch)
  download | inline diff:
From 93b9e9b6e072f63af9009e0d66ab6d0d62ea8c15 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 12 Aug 2024 10:55:11 +0300
Subject: [PATCH v4 2/8] Add test for dead-end backends

The code path for launching a dead-end backend because we're out of
slots was not covered by any tests, so add one. (Some tests did hit
the case of launching a dead-end backend because the server is still
starting up, though, so the gap in our test coverage wasn't as big as
it sounds.)
---
 src/test/perl/PostgreSQL/Test/Cluster.pm      | 39 +++++++++++++++++++
 .../postmaster/t/001_connection_limits.pl     | 17 +++++++-
 2 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 32ee98aebc..6d09f9c5f8 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -104,6 +104,7 @@ use File::Path qw(rmtree mkpath);
 use File::Spec;
 use File::stat qw(stat);
 use File::Temp ();
+use IO::Socket::INET;
 use IPC::Run;
 use PostgreSQL::Version;
 use PostgreSQL::Test::RecursiveCopy;
@@ -284,6 +285,44 @@ sub connstr
 	return "port=$pgport host=$pghost dbname='$dbname'";
 }
 
+=pod
+
+=item $node->raw_connect()
+
+Open a raw TCP or Unix domain socket connection to the server. This
+used by low-level protocol and connection limit tests.
+
+=cut
+
+sub raw_connect
+{
+	my ($self) = @_;
+	my $pgport = $self->port;
+	my $pghost = $self->host;
+
+	my $socket;
+	if ($PostgreSQL::Test::Utils::use_unix_sockets)
+	{
+		require IO::Socket::UNIX;
+		my $path = "$pghost/.s.PGSQL.$pgport";
+
+		$socket = IO::Socket::UNIX->new(
+			Type => SOCK_STREAM(),
+			Peer => $path,
+		) or die "Cannot create socket - $IO::Socket::errstr\n";
+	}
+	else
+	{
+		$socket = IO::Socket::INET->new(
+			PeerHost => $pghost,
+			PeerPort => $pgport,
+			Proto => 'tcp'
+		) or die "Cannot create socket - $IO::Socket::errstr\n";
+	}
+	return $socket;
+}
+
+
 =pod
 
 =item $node->group_access()
diff --git a/src/test/postmaster/t/001_connection_limits.pl b/src/test/postmaster/t/001_connection_limits.pl
index f50aae4949..3547b28bdd 100644
--- a/src/test/postmaster/t/001_connection_limits.pl
+++ b/src/test/postmaster/t/001_connection_limits.pl
@@ -43,6 +43,7 @@ sub background_psql_as_user
 }
 
 my @sessions = ();
+my @raw_connections = ();
 
 push(@sessions, background_psql_as_user('regress_regular'));
 push(@sessions, background_psql_as_user('regress_regular'));
@@ -69,11 +70,25 @@ $node->connect_fails(
 	"superuser_reserved_connections limit",
 	expected_stderr => qr/FATAL:  sorry, too many clients already/);
 
-# TODO: test that query cancellation is still possible
+# We can still open TCP (or Unix domain socket) connections, but
+# beyond a certain number (roughly 2x max_connections), they will be
+# "dead-end backends".
+for (my $i = 0; $i <= 20; $i++)
+{
+	push(@raw_connections, $node->raw_connect());
+}
+
+# TODO: test that query cancellation is still possible. A dead-end
+# backend can process a query cancellation packet.
 
+# Clean up
 foreach my $session (@sessions)
 {
 	$session->quit;
 }
+foreach my $socket (@raw_connections)
+{
+	$socket->close();
+}
 
 done_testing();
-- 
2.39.2



  [text/x-patch] v4-0003-Use-an-shmem_exit-callback-to-remove-backend-from.patch (4.9K, ../../[email protected]/4-v4-0003-Use-an-shmem_exit-callback-to-remove-backend-from.patch)
  download | inline diff:
From 88287a2db95e584018f1c7fa9e992feb7d179ce8 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 12 Aug 2024 10:58:35 +0300
Subject: [PATCH v4 3/8] Use an shmem_exit callback to remove backend from
 PMChildFlags on exit

This seems nicer than having to duplicate the logic between
InitProcess() and ProcKill() for which child processes have a
PMChildFlags slot.

Move the MarkPostmasterChildActive() call earlier in InitProcess(),
out of the section protected by the spinlock.
---
 src/backend/storage/ipc/pmsignal.c | 10 ++++++--
 src/backend/storage/lmgr/proc.c    | 38 ++++++++++--------------------
 src/include/storage/pmsignal.h     |  1 -
 3 files changed, 21 insertions(+), 28 deletions(-)

diff --git a/src/backend/storage/ipc/pmsignal.c b/src/backend/storage/ipc/pmsignal.c
index 27844b46a2..cb99e77476 100644
--- a/src/backend/storage/ipc/pmsignal.c
+++ b/src/backend/storage/ipc/pmsignal.c
@@ -24,6 +24,7 @@
 #include "miscadmin.h"
 #include "postmaster/postmaster.h"
 #include "replication/walsender.h"
+#include "storage/ipc.h"
 #include "storage/pmsignal.h"
 #include "storage/shmem.h"
 #include "utils/memutils.h"
@@ -121,6 +122,8 @@ postmaster_death_handler(SIGNAL_ARGS)
 
 #endif							/* USE_POSTMASTER_DEATH_SIGNAL */
 
+static void MarkPostmasterChildInactive(int code, Datum arg);
+
 /*
  * PMSignalShmemSize
  *		Compute space needed for pmsignal.c's shared memory
@@ -328,6 +331,9 @@ MarkPostmasterChildActive(void)
 	slot--;
 	Assert(PMSignalState->PMChildFlags[slot] == PM_CHILD_ASSIGNED);
 	PMSignalState->PMChildFlags[slot] = PM_CHILD_ACTIVE;
+
+	/* Arrange to clean up at exit. */
+	on_shmem_exit(MarkPostmasterChildInactive, 0);
 }
 
 /*
@@ -352,8 +358,8 @@ MarkPostmasterChildWalSender(void)
  * MarkPostmasterChildInactive - mark a postmaster child as done using
  * shared memory.  This is called in the child process.
  */
-void
-MarkPostmasterChildInactive(void)
+static void
+MarkPostmasterChildInactive(int code, Datum arg)
 {
 	int			slot = MyPMChildSlot;
 
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index ac66da8638..9536469e89 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -308,6 +308,19 @@ InitProcess(void)
 	if (MyProc != NULL)
 		elog(ERROR, "you already exist");
 
+	/*
+	 * Before we start accessing the shared memory in a serious way, mark
+	 * ourselves as an active postmaster child; this is so that the postmaster
+	 * can detect it if we exit without cleaning up.  (XXX autovac launcher
+	 * currently doesn't participate in this; it probably should.)
+	 *
+	 * Slot sync worker also does not participate in it, see comments atop
+	 * 'struct bkend' in postmaster.c.
+	 */
+	if (IsUnderPostmaster && !AmAutoVacuumLauncherProcess() &&
+		!AmLogicalSlotSyncWorkerProcess())
+		MarkPostmasterChildActive();
+
 	/* Decide which list should supply our PGPROC. */
 	if (AmAutoVacuumLauncherProcess() || AmAutoVacuumWorkerProcess())
 		procgloballist = &ProcGlobal->autovacFreeProcs;
@@ -360,19 +373,6 @@ InitProcess(void)
 	 */
 	Assert(MyProc->procgloballist == procgloballist);
 
-	/*
-	 * Now that we have a PGPROC, mark ourselves as an active postmaster
-	 * child; this is so that the postmaster can detect it if we exit without
-	 * cleaning up.  (XXX autovac launcher currently doesn't participate in
-	 * this; it probably should.)
-	 *
-	 * Slot sync worker also does not participate in it, see comments atop
-	 * 'struct bkend' in postmaster.c.
-	 */
-	if (IsUnderPostmaster && !AmAutoVacuumLauncherProcess() &&
-		!AmLogicalSlotSyncWorkerProcess())
-		MarkPostmasterChildActive();
-
 	/*
 	 * Initialize all fields of MyProc, except for those previously
 	 * initialized by InitProcGlobal.
@@ -947,18 +947,6 @@ ProcKill(int code, Datum arg)
 
 	SpinLockRelease(ProcStructLock);
 
-	/*
-	 * This process is no longer present in shared memory in any meaningful
-	 * way, so tell the postmaster we've cleaned up acceptably well. (XXX
-	 * autovac launcher should be included here someday)
-	 *
-	 * Slot sync worker is also not a postmaster child, so skip this shared
-	 * memory related processing here.
-	 */
-	if (IsUnderPostmaster && !AmAutoVacuumLauncherProcess() &&
-		!AmLogicalSlotSyncWorkerProcess())
-		MarkPostmasterChildInactive();
-
 	/* wake autovac launcher if needed -- see comments in FreeWorkerInfo */
 	if (AutovacuumLauncherPid != 0)
 		kill(AutovacuumLauncherPid, SIGUSR2);
diff --git a/src/include/storage/pmsignal.h b/src/include/storage/pmsignal.h
index 0c9a7e32a8..3b9336b83c 100644
--- a/src/include/storage/pmsignal.h
+++ b/src/include/storage/pmsignal.h
@@ -74,7 +74,6 @@ extern int	AssignPostmasterChildSlot(void);
 extern bool ReleasePostmasterChildSlot(int slot);
 extern bool IsPostmasterChildWalSender(int slot);
 extern void MarkPostmasterChildActive(void);
-extern void MarkPostmasterChildInactive(void);
 extern void MarkPostmasterChildWalSender(void);
 extern bool PostmasterIsAliveInternal(void);
 extern void PostmasterDeathSignalInit(void);
-- 
2.39.2



  [text/x-patch] v4-0004-Introduce-a-separate-BackendType-for-dead-end-chi.patch (14.7K, ../../[email protected]/5-v4-0004-Introduce-a-separate-BackendType-for-dead-end-chi.patch)
  download | inline diff:
From dc53f89edbeec99f8633def8aa5f47cd98e7a150 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 12 Aug 2024 10:59:04 +0300
Subject: [PATCH v4 4/8] Introduce a separate BackendType for dead-end children

And replace postmaster.c's own "backend type" codes with BackendType

XXX: While working on this, many times I accidentally did something
like "foo |= B_SOMETHING" instead of "foo |= 1 << B_SOMETHING", when
constructing arguments to SignalSomeChildren or CountChildren, and
things broke in very subtle ways taking a long time to debug. The old
constants that were already bitmasks avoided that. Maybe we need some
macro magic or something to make this less error-prone.
---
 src/backend/postmaster/postmaster.c    | 106 ++++++++++++-------------
 src/backend/utils/activity/pgstat_io.c |   3 +
 src/backend/utils/init/miscinit.c      |   3 +
 src/include/miscadmin.h                |   1 +
 4 files changed, 56 insertions(+), 57 deletions(-)

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2c8e7fa7d6..9bbbbfe55f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -129,15 +129,11 @@
 
 
 /*
- * Possible types of a backend. Beyond being the possible bkend_type values in
- * struct bkend, these are OR-able request flag bits for SignalSomeChildren()
- * and CountChildren().
+ * CountChildren and SignalSomeChildren use a uint32 bitmask argument to
+ * represent BackendTypes to count or signal.
  */
-#define BACKEND_TYPE_NORMAL		0x0001	/* normal backend */
-#define BACKEND_TYPE_AUTOVAC	0x0002	/* autovacuum worker process */
-#define BACKEND_TYPE_WALSND		0x0004	/* walsender process */
-#define BACKEND_TYPE_BGWORKER	0x0008	/* bgworker process */
-#define BACKEND_TYPE_ALL		0x000F	/* OR of all the above */
+#define BACKEND_TYPE_ALL 0xffffffff
+StaticAssertDecl(BACKEND_NUM_TYPES < 32, "too many backend types for uint32");
 
 /*
  * List of active backends (or child processes anyway; we don't actually
@@ -148,7 +144,7 @@
  * As shown in the above set of backend types, this list includes not only
  * "normal" client sessions, but also autovacuum workers, walsenders, and
  * background workers.  (Note that at the time of launch, walsenders are
- * labeled BACKEND_TYPE_NORMAL; we relabel them to BACKEND_TYPE_WALSND
+ * labeled B_BACKEND; we relabel them to B_WAL_SENDER
  * upon noticing they've changed their PMChildFlags entry.  Hence that check
  * must be done before any operation that needs to distinguish walsenders
  * from normal backends.)
@@ -157,7 +153,8 @@
  * the purpose of sending a friendly rejection message to a would-be client.
  * We must track them because they are attached to shared memory, but we know
  * they will never become live backends.  dead_end children are not assigned a
- * PMChildSlot.  dead_end children have bkend_type NORMAL.
+ * PMChildSlot.  dead_end children have bkend_type B_DEAD_END_BACKEND.
+ * FIXME: a dead-end backend can send query cancel?
  *
  * "Special" children such as the startup, bgwriter, autovacuum launcher, and
  * slot sync worker tasks are not in this list.  They are tracked via StartupPID
@@ -169,8 +166,7 @@ typedef struct bkend
 {
 	pid_t		pid;			/* process id of backend */
 	int			child_slot;		/* PMChildSlot for this backend, if any */
-	int			bkend_type;		/* child process flavor, see above */
-	bool		dead_end;		/* is it going to send an error and quit? */
+	BackendType bkend_type;		/* child process flavor, see above */
 	RegisteredBgWorker *rw;		/* bgworker info, if this is a bgworker */
 	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
 	dlist_node	elem;			/* list link in BackendList */
@@ -410,12 +406,13 @@ static void report_fork_failure_to_client(ClientSocket *client_sock, int errnum)
 static CAC_state canAcceptConnections(int backend_type);
 static void signal_child(pid_t pid, int signal);
 static void sigquit_child(pid_t pid);
-static bool SignalSomeChildren(int signal, int target);
+static bool SignalSomeChildren(int signal, uint32 targetMask);
 static void TerminateChildren(int signal);
 
-#define SignalChildren(sig)			   SignalSomeChildren(sig, BACKEND_TYPE_ALL)
+#define SignalChildren(sig)		\
+	SignalSomeChildren(sig, BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND))
 
-static int	CountChildren(int target);
+static int	CountChildren(uint32 targetMask);
 static Backend *assign_backendlist_entry(void);
 static void LaunchMissingBackgroundProcesses(void);
 static void maybe_start_bgworkers(void);
@@ -1765,7 +1762,7 @@ canAcceptConnections(int backend_type)
 	 * bgworker_should_start_now() decided whether the DB state allows them.
 	 */
 	if (pmState != PM_RUN && pmState != PM_HOT_STANDBY &&
-		backend_type != BACKEND_TYPE_BGWORKER)
+		backend_type != B_BG_WORKER)
 	{
 		if (Shutdown > NoShutdown)
 			return CAC_SHUTDOWN;	/* shutdown is pending */
@@ -1782,7 +1779,7 @@ canAcceptConnections(int backend_type)
 	 * "Smart shutdown" restrictions are applied only to normal connections,
 	 * not to autovac workers or bgworkers.
 	 */
-	if (!connsAllowed && backend_type == BACKEND_TYPE_NORMAL)
+	if (!connsAllowed && backend_type == B_BACKEND)
 		return CAC_SHUTDOWN;	/* shutdown is pending */
 
 	/*
@@ -1797,7 +1794,7 @@ canAcceptConnections(int backend_type)
 	 * The limit here must match the sizes of the per-child-process arrays;
 	 * see comments for MaxLivePostmasterChildren().
 	 */
-	if (CountChildren(BACKEND_TYPE_ALL) >= MaxLivePostmasterChildren())
+	if (CountChildren(BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND)) >= MaxLivePostmasterChildren())
 		result = CAC_TOOMANY;
 
 	return result;
@@ -2555,11 +2552,11 @@ CleanupBackend(Backend *bp,
 	bool		logged = false;
 
 	/* Construct a process name for log message */
-	if (bp->dead_end)
+	if (bp->bkend_type == B_DEAD_END_BACKEND)
 	{
 		procname = _("dead end backend");
 	}
-	else if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
+	else if (bp->bkend_type == B_BG_WORKER)
 	{
 		snprintf(namebuf, MAXPGPATH, _("background worker \"%s\""),
 				 bp->rw->rw_worker.bgw_type);
@@ -2598,7 +2595,7 @@ CleanupBackend(Backend *bp,
 	 * If the process attached to shared memory, check that it detached
 	 * cleanly.
 	 */
-	if (!bp->dead_end)
+	if (bp->bkend_type != B_DEAD_END_BACKEND)
 	{
 		if (!ReleasePostmasterChildSlot(bp->child_slot))
 		{
@@ -2630,7 +2627,7 @@ CleanupBackend(Backend *bp,
 	/*
 	 * If it was a background worker, also update its RegisteredWorker entry.
 	 */
-	if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
+	if (bp->bkend_type == B_BG_WORKER)
 	{
 		RegisteredBgWorker *rw = bp->rw;
 
@@ -2858,7 +2855,7 @@ PostmasterStateMachine(void)
 			 * This state ends when we have no normal client backends running.
 			 * Then we're ready to stop other children.
 			 */
-			if (CountChildren(BACKEND_TYPE_NORMAL) == 0)
+			if (CountChildren(1 << B_BACKEND) == 0)
 				pmState = PM_STOP_BACKENDS;
 		}
 	}
@@ -2879,7 +2876,7 @@ PostmasterStateMachine(void)
 
 		/* Signal all backend children except walsenders */
 		SignalSomeChildren(SIGTERM,
-						   BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND);
+						   BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND));
 		/* and the autovac launcher too */
 		if (AutoVacPID != 0)
 			signal_child(AutoVacPID, SIGTERM);
@@ -2921,7 +2918,7 @@ PostmasterStateMachine(void)
 		 * here. Walsenders and archiver are also disregarded, they will be
 		 * terminated later after writing the checkpoint record.
 		 */
-		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
+		if (CountChildren(BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND)) == 0 &&
 			StartupPID == 0 &&
 			WalReceiverPID == 0 &&
 			WalSummarizerPID == 0 &&
@@ -2995,7 +2992,7 @@ PostmasterStateMachine(void)
 		 * left by now anyway; what we're really waiting for is walsenders and
 		 * archiver.
 		 */
-		if (PgArchPID == 0 && CountChildren(BACKEND_TYPE_ALL) == 0)
+		if (PgArchPID == 0 && CountChildren(BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND)) == 0)
 		{
 			pmState = PM_WAIT_DEAD_END;
 		}
@@ -3294,10 +3291,10 @@ sigquit_child(pid_t pid)
 
 /*
  * Send a signal to the targeted children (but NOT special children;
- * dead_end children are never signaled, either).
+ * dead_end children are never signaled, either XXX).
  */
 static bool
-SignalSomeChildren(int signal, int target)
+SignalSomeChildren(int signal, uint32 targetMask)
 {
 	dlist_iter	iter;
 	bool		signaled = false;
@@ -3306,24 +3303,21 @@ SignalSomeChildren(int signal, int target)
 	{
 		Backend    *bp = dlist_container(Backend, elem, iter.cur);
 
-		if (bp->dead_end)
-			continue;
-
 		/*
 		 * Since target == BACKEND_TYPE_ALL is the most common case, we test
 		 * it first and avoid touching shared memory for every child.
 		 */
-		if (target != BACKEND_TYPE_ALL)
+		if (targetMask != BACKEND_TYPE_ALL)
 		{
 			/*
 			 * Assign bkend_type for any recently announced WAL Sender
 			 * processes.
 			 */
-			if (bp->bkend_type == BACKEND_TYPE_NORMAL &&
+			if (bp->bkend_type == B_BACKEND &&
 				IsPostmasterChildWalSender(bp->child_slot))
-				bp->bkend_type = BACKEND_TYPE_WALSND;
+				bp->bkend_type = B_WAL_SENDER;
 
-			if (!(target & bp->bkend_type))
+			if ((targetMask & (1 << bp->bkend_type)) == 0)
 				continue;
 		}
 
@@ -3396,17 +3390,22 @@ BackendStartup(ClientSocket *client_sock)
 	}
 
 	/* Pass down canAcceptConnections state */
-	startup_data.canAcceptConnections = canAcceptConnections(BACKEND_TYPE_NORMAL);
-	bn->dead_end = (startup_data.canAcceptConnections != CAC_OK);
+	startup_data.canAcceptConnections = canAcceptConnections(B_BACKEND);
 	bn->rw = NULL;
 
 	/*
 	 * Unless it's a dead_end child, assign it a child slot number
 	 */
-	if (!bn->dead_end)
+	if (startup_data.canAcceptConnections == CAC_OK)
+	{
+		bn->bkend_type = B_BACKEND; /* Can change later to WALSND */
 		bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
+	}
 	else
+	{
+		bn->bkend_type = B_DEAD_END_BACKEND;
 		bn->child_slot = 0;
+	}
 
 	/* Hasn't asked to be notified about any bgworkers yet */
 	bn->bgworker_notify = false;
@@ -3419,7 +3418,7 @@ BackendStartup(ClientSocket *client_sock)
 		/* in parent, fork failed */
 		int			save_errno = errno;
 
-		if (!bn->dead_end)
+		if (bn->child_slot != 0)
 			(void) ReleasePostmasterChildSlot(bn->child_slot);
 		pfree(bn);
 		errno = save_errno;
@@ -3439,7 +3438,6 @@ BackendStartup(ClientSocket *client_sock)
 	 * of backends.
 	 */
 	bn->pid = pid;
-	bn->bkend_type = BACKEND_TYPE_NORMAL;	/* Can change later to WALSND */
 	dlist_push_head(&BackendList, &bn->elem);
 
 	return STATUS_OK;
@@ -3673,11 +3671,10 @@ dummy_handler(SIGNAL_ARGS)
 }
 
 /*
- * Count up number of child processes of specified types (dead_end children
- * are always excluded).
+ * Count up number of child processes of specified types.
  */
 static int
-CountChildren(int target)
+CountChildren(uint32 targetMask)
 {
 	dlist_iter	iter;
 	int			cnt = 0;
@@ -3686,24 +3683,21 @@ CountChildren(int target)
 	{
 		Backend    *bp = dlist_container(Backend, elem, iter.cur);
 
-		if (bp->dead_end)
-			continue;
-
 		/*
 		 * Since target == BACKEND_TYPE_ALL is the most common case, we test
 		 * it first and avoid touching shared memory for every child.
 		 */
-		if (target != BACKEND_TYPE_ALL)
+		if (targetMask != BACKEND_TYPE_ALL)
 		{
 			/*
 			 * Assign bkend_type for any recently announced WAL Sender
 			 * processes.
 			 */
-			if (bp->bkend_type == BACKEND_TYPE_NORMAL &&
+			if (bp->bkend_type == B_BACKEND &&
 				IsPostmasterChildWalSender(bp->child_slot))
-				bp->bkend_type = BACKEND_TYPE_WALSND;
+				bp->bkend_type = B_WAL_SENDER;
 
-			if (!(target & bp->bkend_type))
+			if ((targetMask & (1 << bp->bkend_type)) == 0)
 				continue;
 		}
 
@@ -3770,13 +3764,13 @@ StartAutovacuumWorker(void)
 	 * we have to check to avoid race-condition problems during DB state
 	 * changes.
 	 */
-	if (canAcceptConnections(BACKEND_TYPE_AUTOVAC) == CAC_OK)
+	if (canAcceptConnections(B_AUTOVAC_WORKER) == CAC_OK)
 	{
 		bn = (Backend *) palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
 		if (bn)
 		{
-			/* Autovac workers are not dead_end and need a child slot */
-			bn->dead_end = false;
+			/* Autovac workers need a child slot */
+			bn->bkend_type = B_AUTOVAC_WORKER;
 			bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
 			bn->bgworker_notify = false;
 			bn->rw = NULL;
@@ -3784,7 +3778,6 @@ StartAutovacuumWorker(void)
 			bn->pid = StartChildProcess(B_AUTOVAC_WORKER);
 			if (bn->pid > 0)
 			{
-				bn->bkend_type = BACKEND_TYPE_AUTOVAC;
 				dlist_push_head(&BackendList, &bn->elem);
 				/* all OK */
 				return;
@@ -3990,7 +3983,7 @@ assign_backendlist_entry(void)
 	 * only possible failure is CAC_TOOMANY, so we just log an error message
 	 * based on that rather than checking the error code precisely.
 	 */
-	if (canAcceptConnections(BACKEND_TYPE_BGWORKER) != CAC_OK)
+	if (canAcceptConnections(B_BG_WORKER) != CAC_OK)
 	{
 		ereport(LOG,
 				(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
@@ -4008,8 +4001,7 @@ assign_backendlist_entry(void)
 	}
 
 	bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
-	bn->bkend_type = BACKEND_TYPE_BGWORKER;
-	bn->dead_end = false;
+	bn->bkend_type = B_BG_WORKER;
 	bn->bgworker_notify = false;
 
 	return bn;
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 8af55989ee..9bad1040d6 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -312,6 +312,8 @@ pgstat_io_snapshot_cb(void)
 *
 * The following BackendTypes do not participate in the cumulative stats
 * subsystem or do not perform IO on which we currently track:
+* - Dead-end backend because it is not connected to shared memory and
+*   doesn't do any IO
 * - Syslogger because it is not connected to shared memory
 * - Archiver because most relevant archiving IO is delegated to a
 *   specialized command or module
@@ -334,6 +336,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 	switch (bktype)
 	{
 		case B_INVALID:
+		case B_DEAD_END_BACKEND:
 		case B_ARCHIVER:
 		case B_LOGGER:
 		case B_WAL_RECEIVER:
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 537d92c0cf..ae8b1a4331 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -281,6 +281,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_BACKEND:
 			backendDesc = "client backend";
 			break;
+		case B_DEAD_END_BACKEND:
+			backendDesc = "dead-end client backend";
+			break;
 		case B_BG_WORKER:
 			backendDesc = "background worker";
 			break;
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index ac16233b71..b21c4d43b9 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -337,6 +337,7 @@ typedef enum BackendType
 
 	/* Backends and other backend-like processes */
 	B_BACKEND,
+	B_DEAD_END_BACKEND,
 	B_AUTOVAC_LAUNCHER,
 	B_AUTOVAC_WORKER,
 	B_BG_WORKER,
-- 
2.39.2



  [text/x-patch] v4-0005-Kill-dead-end-children-when-there-s-nothing-else-.patch (7.5K, ../../[email protected]/6-v4-0005-Kill-dead-end-children-when-there-s-nothing-else-.patch)
  download | inline diff:
From 9c832ce33667abc5aef128a17fa9c27daaad872a Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 12 Aug 2024 10:59:27 +0300
Subject: [PATCH v4 5/8] Kill dead-end children when there's nothing else left

Previously, the postmaster would never try to kill dead-end child
processes, even if there were no other processes left. A dead-end
backend will eventually exit, when authentication_timeout expires, but
if a dead-end backend is the only thing that's preventing the server
from shutting down, it seems better to kill it immediately. It's
particularly important, if there was a bug in the early startup code
that prevented a dead-end child from timing out and exiting normally.

Includes a test for that case where a dead-end backend previously kept
the server from shutting down.
---
 src/backend/postmaster/postmaster.c     | 35 +++++++-------
 src/test/postmaster/meson.build         |  1 +
 src/test/postmaster/t/002_start_stop.pl | 64 +++++++++++++++++++++++++
 3 files changed, 81 insertions(+), 19 deletions(-)
 create mode 100644 src/test/postmaster/t/002_start_stop.pl

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 9bbbbfe55f..99c588ee0b 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -409,9 +409,6 @@ static void sigquit_child(pid_t pid);
 static bool SignalSomeChildren(int signal, uint32 targetMask);
 static void TerminateChildren(int signal);
 
-#define SignalChildren(sig)		\
-	SignalSomeChildren(sig, BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND))
-
 static int	CountChildren(uint32 targetMask);
 static Backend *assign_backendlist_entry(void);
 static void LaunchMissingBackgroundProcesses(void);
@@ -1963,7 +1960,7 @@ process_pm_reload_request(void)
 		ereport(LOG,
 				(errmsg("received SIGHUP, reloading configuration files")));
 		ProcessConfigFile(PGC_SIGHUP);
-		SignalChildren(SIGHUP);
+		SignalSomeChildren(SIGHUP, BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND));
 		if (StartupPID != 0)
 			signal_child(StartupPID, SIGHUP);
 		if (BgWriterPID != 0)
@@ -2381,7 +2378,7 @@ process_pm_child_exit(void)
 				 * Waken walsenders for the last time. No regular backends
 				 * should be around anymore.
 				 */
-				SignalChildren(SIGUSR2);
+				SignalSomeChildren(SIGUSR2, BACKEND_TYPE_ALL & (1 << B_DEAD_END_BACKEND));
 
 				pmState = PM_SHUTDOWN_2;
 			}
@@ -2874,7 +2871,7 @@ PostmasterStateMachine(void)
 		 */
 		ForgetUnstartedBackgroundWorkers();
 
-		/* Signal all backend children except walsenders */
+		/* Signal all backend children except walsenders and dead-end backends */
 		SignalSomeChildren(SIGTERM,
 						   BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND));
 		/* and the autovac launcher too */
@@ -2932,10 +2929,11 @@ PostmasterStateMachine(void)
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
 				/*
-				 * Start waiting for dead_end children to die.  This state
-				 * change causes ServerLoop to stop creating new ones.
+				 * Stop any dead_end children and stop creating new ones.
 				 */
 				pmState = PM_WAIT_DEAD_END;
+				ConfigurePostmasterWaitSet(false);
+				SignalSomeChildren(SIGQUIT, 1 << B_DEAD_END_BACKEND);
 
 				/*
 				 * We already SIGQUIT'd the archiver and stats processes, if
@@ -2974,9 +2972,10 @@ PostmasterStateMachine(void)
 					 */
 					FatalError = true;
 					pmState = PM_WAIT_DEAD_END;
+					ConfigurePostmasterWaitSet(false);
 
-					/* Kill the walsenders and archiver too */
-					SignalChildren(SIGQUIT);
+					/* Kill the walsenders and archiver, too */
+					SignalSomeChildren(SIGQUIT, BACKEND_TYPE_ALL);
 					if (PgArchPID != 0)
 						signal_child(PgArchPID, SIGQUIT);
 				}
@@ -2994,15 +2993,14 @@ PostmasterStateMachine(void)
 		 */
 		if (PgArchPID == 0 && CountChildren(BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND)) == 0)
 		{
+			ConfigurePostmasterWaitSet(false);
+			SignalSomeChildren(SIGTERM, 1 << B_DEAD_END_BACKEND);
 			pmState = PM_WAIT_DEAD_END;
 		}
 	}
 
 	if (pmState == PM_WAIT_DEAD_END)
 	{
-		/* Don't allow any new socket connection events. */
-		ConfigurePostmasterWaitSet(false);
-
 		/*
 		 * PM_WAIT_DEAD_END state ends when the BackendList is entirely empty
 		 * (ie, no dead_end children remain), and the archiver is gone too.
@@ -3290,8 +3288,7 @@ sigquit_child(pid_t pid)
 }
 
 /*
- * Send a signal to the targeted children (but NOT special children;
- * dead_end children are never signaled, either XXX).
+ * Send a signal to the targeted children (but NOT special children).
  */
 static bool
 SignalSomeChildren(int signal, uint32 targetMask)
@@ -3322,8 +3319,8 @@ SignalSomeChildren(int signal, uint32 targetMask)
 		}
 
 		ereport(DEBUG4,
-				(errmsg_internal("sending signal %d to process %d",
-								 signal, (int) bp->pid)));
+				(errmsg_internal("sending signal %d to %s process %d",
+								 signal, GetBackendTypeDesc(bp->bkend_type), (int) bp->pid)));
 		signal_child(bp->pid, signal);
 		signaled = true;
 	}
@@ -3332,12 +3329,12 @@ SignalSomeChildren(int signal, uint32 targetMask)
 
 /*
  * Send a termination signal to children.  This considers all of our children
- * processes, except syslogger and dead_end backends.
+ * processes, except syslogger.
  */
 static void
 TerminateChildren(int signal)
 {
-	SignalChildren(signal);
+	SignalSomeChildren(signal, BACKEND_TYPE_ALL);
 	if (StartupPID != 0)
 	{
 		signal_child(StartupPID, signal);
diff --git a/src/test/postmaster/meson.build b/src/test/postmaster/meson.build
index c2de2e0eb5..2d89adf520 100644
--- a/src/test/postmaster/meson.build
+++ b/src/test/postmaster/meson.build
@@ -7,6 +7,7 @@ tests += {
   'tap': {
     'tests': [
       't/001_connection_limits.pl',
+      't/002_start_stop.pl',
     ],
   },
 }
diff --git a/src/test/postmaster/t/002_start_stop.pl b/src/test/postmaster/t/002_start_stop.pl
new file mode 100644
index 0000000000..6f114659fa
--- /dev/null
+++ b/src/test/postmaster/t/002_start_stop.pl
@@ -0,0 +1,64 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# XXX
+# XXX
+
+use IO::Socket::INET;
+use IO::Socket::UNIX;
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(time);
+
+# Initialize the server with low connection limits, to test dead-end backends
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->append_conf('postgresql.conf', "max_connections = 5");
+$node->append_conf('postgresql.conf', "log_connections = on");
+$node->append_conf('postgresql.conf', "log_min_messages = debug2");
+
+# XX
+$node->append_conf('postgresql.conf', "authentication_timeout = '120 s'");
+
+$node->start;
+
+my @sessions = ();
+my @raw_connections = ();
+
+#for (my $i=0; $i <= 5; $i++) {
+#	push(@sessions, $node->background_psql('postgres', on_error_die => 1));
+#}
+#$node->connect_fails("dbname=postgres", "max_connections reached",
+#					 expected_stderr => qr/FATAL:  sorry, too many clients already/);
+
+# We can still open TCP (or Unix domain socket) connections, but beyond a
+# certain number (roughly 2x max_connections), they will be "dead-end backends"
+for (my $i = 0; $i <= 20; $i++)
+{
+	push(@raw_connections, $node->raw_connect());
+}
+
+# Test that the dead-end backends don't prevent the server from stopping.
+my $before = time();
+$node->stop();
+my $elapsed = time() - $before;
+ok($elapsed < 60);
+
+$node->start();
+
+$node->connect_ok("dbname=postgres", "works after restart");
+
+# Clean up
+foreach my $session (@sessions)
+{
+	$session->quit;
+}
+foreach my $socket (@raw_connections)
+{
+	$socket->close();
+}
+
+done_testing();
-- 
2.39.2



  [text/x-patch] v4-0006-Assign-a-child-slot-to-every-postmaster-child-pro.patch (64.1K, ../../[email protected]/7-v4-0006-Assign-a-child-slot-to-every-postmaster-child-pro.patch)
  download | inline diff:
From a4a0e77f90e5e2e69cd7280b65d0e198cf6067e7 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Mon, 12 Aug 2024 12:43:22 +0300
Subject: [PATCH v4 6/8] Assign a child slot to every postmaster child process

Previously, only backends, autovacuum workers, and background workers
had an entry in the PMChildFlags array. With this commit, all
postmaster child processes, including all the aux processes, have an
entry.

We now maintain separate free-lists for different kinds of
backends. That ensures that there are always slots available for
autovacuum and background workers. Previously, pre-authorization
backends could prevent autovacuum or background workers from starting
up, by using up all the slots.

The code to manage the slots in the postmaster process is in a new
pmchild.c source file. Because postmaster.c is just so large.

Assigning pmsignal slot numbers is now pmchild.c's responsibility.
This replaces the PMChildInUse array in pmsignal.c.
---
 src/backend/postmaster/Makefile         |   1 +
 src/backend/postmaster/launch_backend.c |   1 +
 src/backend/postmaster/meson.build      |   1 +
 src/backend/postmaster/pmchild.c        | 287 ++++++++++
 src/backend/postmaster/postmaster.c     | 708 ++++++++++--------------
 src/backend/storage/ipc/pmsignal.c      |  83 +--
 src/backend/storage/lmgr/proc.c         |  12 +-
 src/include/postmaster/postmaster.h     |  40 ++
 src/include/storage/pmsignal.h          |   2 +-
 src/tools/pgindent/typedefs.list        |   2 +-
 10 files changed, 654 insertions(+), 483 deletions(-)
 create mode 100644 src/backend/postmaster/pmchild.c

diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index db08543d19..c977d91785 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -23,6 +23,7 @@ OBJS = \
 	launch_backend.o \
 	pgarch.o \
 	postmaster.o \
+	pmchild.o \
 	startup.o \
 	syslogger.o \
 	walsummarizer.o \
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index 0ae23fdf55..b0b91dc97f 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -182,6 +182,7 @@ static child_process_kind child_process_kinds[] = {
 	[B_INVALID] = {"invalid", NULL, false},
 
 	[B_BACKEND] = {"backend", BackendMain, true},
+	[B_DEAD_END_BACKEND] = {"dead-end backend", BackendMain, true},
 	[B_AUTOVAC_LAUNCHER] = {"autovacuum launcher", AutoVacLauncherMain, true},
 	[B_AUTOVAC_WORKER] = {"autovacuum worker", AutoVacWorkerMain, true},
 	[B_BG_WORKER] = {"bgworker", BackgroundWorkerMain, true},
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index 0ea4bbe084..388848bb52 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -11,6 +11,7 @@ backend_sources += files(
   'launch_backend.c',
   'pgarch.c',
   'postmaster.c',
+  'pmchild.c',
   'startup.c',
   'syslogger.c',
   'walsummarizer.c',
diff --git a/src/backend/postmaster/pmchild.c b/src/backend/postmaster/pmchild.c
new file mode 100644
index 0000000000..735c66f8e7
--- /dev/null
+++ b/src/backend/postmaster/pmchild.c
@@ -0,0 +1,287 @@
+/*-------------------------------------------------------------------------
+ *
+ * pmchild.c
+ *	  Functions for keeping track of postmaster child processes.
+ *
+ * Keep track of all child processes, so that when a process exits, we know
+ * kind of a process it was and can clean up accordingly.  Every child process
+ * is allocated a PMChild struct, from a fixed pool of structs.  The size of
+ * the pool is determined by various settings that configure how many worker
+ * processes and backend connections are allowed, i.e. autovacuum_max_workers,
+ * max_worker_processes, max_wal_senders, and max_connections.
+ *
+ * The structures and functions in this file are private to the postmaster
+ * process.  But note that there is an array in shared memory, managed by
+ * pmsignal.c, that mirrors this.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/postmaster/pmchild.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "miscadmin.h"
+#include "postmaster/autovacuum.h"
+#include "postmaster/postmaster.h"
+#include "replication/walsender.h"
+#include "storage/pmsignal.h"
+#include "storage/proc.h"
+
+/*
+ * Freelists for different kinds of child processes.  We maintain separate
+ * pools for them, so that launching a lot of backends cannot exchaust all the
+ * slots, and prevent autovacuum or an aux process from launching.
+ */
+static dlist_head freeBackendList;
+static dlist_head freeAutoVacWorkerList;
+static dlist_head freeBgWorkerList;
+static dlist_head freeAuxList;
+
+/*
+ * List of active child processes.  This includes dead-end children.
+ */
+dlist_head	ActiveChildList;
+
+/*
+ * MaxLivePostmasterChildren
+ *
+ * This reports the number postmaster child processes that can be active.  It
+ * includes all children except for dead_end children.  This allows the array
+ * in shared memory (PMChildFlags) to have a fixed maximum size.
+ */
+int
+MaxLivePostmasterChildren(void)
+{
+	int			n = 0;
+
+	/* We know exactly how mamy worker and aux processes can be active */
+	n += autovacuum_max_workers;
+	n += max_worker_processes;
+	n += NUM_AUXILIARY_PROCS;
+
+	/*
+	 * We allow more connections here than we can have backends because some
+	 * might still be authenticating; they might fail auth, or some existing
+	 * backend might exit before the auth cycle is completed.  The exact
+	 * MaxBackends limit is enforced when a new backend tries to join the
+	 * shared-inval backend array.
+	 */
+	n += 2 * (MaxConnections + max_wal_senders);
+
+	return n;
+}
+
+static void
+init_slot(PMChild *pmchild, int slotno, dlist_head *freelist)
+{
+	pmchild->pid = 0;
+	pmchild->child_slot = slotno + 1;
+	pmchild->bkend_type = B_INVALID;
+	pmchild->rw = NULL;
+	pmchild->bgworker_notify = false;
+	dlist_push_tail(freelist, &pmchild->elem);
+}
+
+/*
+ * Initialize at postmaster startup
+ */
+void
+InitPostmasterChildSlots(void)
+{
+	int			num_pmchild_slots;
+	int			slotno;
+	PMChild    *slots;
+
+	dlist_init(&freeBackendList);
+	dlist_init(&freeAutoVacWorkerList);
+	dlist_init(&freeBgWorkerList);
+	dlist_init(&freeAuxList);
+	dlist_init(&ActiveChildList);
+
+	num_pmchild_slots = MaxLivePostmasterChildren();
+
+	slots = palloc(num_pmchild_slots * sizeof(PMChild));
+
+	slotno = 0;
+	for (int i = 0; i < 2 * (MaxConnections + max_wal_senders); i++)
+	{
+		init_slot(&slots[slotno], slotno, &freeBackendList);
+		slotno++;
+	}
+	for (int i = 0; i < autovacuum_max_workers; i++)
+	{
+		init_slot(&slots[slotno], slotno, &freeAutoVacWorkerList);
+		slotno++;
+	}
+	for (int i = 0; i < max_worker_processes; i++)
+	{
+		init_slot(&slots[slotno], slotno, &freeBgWorkerList);
+		slotno++;
+	}
+	for (int i = 0; i < NUM_AUXILIARY_PROCS; i++)
+	{
+		init_slot(&slots[slotno], slotno, &freeAuxList);
+		slotno++;
+	}
+	Assert(slotno == num_pmchild_slots);
+}
+
+/* Return the appropriate free-list for the given backend type */
+static dlist_head *
+GetFreeList(BackendType btype)
+{
+	switch (btype)
+	{
+		case B_BACKEND:
+		case B_BG_WORKER:
+		case B_WAL_SENDER:
+		case B_SLOTSYNC_WORKER:
+			return &freeBackendList;
+		case B_AUTOVAC_WORKER:
+			return &freeAutoVacWorkerList;
+
+			/*
+			 * Auxiliary processes.  There can be only one of each of these
+			 * running at a time.
+			 */
+		case B_AUTOVAC_LAUNCHER:
+		case B_ARCHIVER:
+		case B_BG_WRITER:
+		case B_CHECKPOINTER:
+		case B_STARTUP:
+		case B_WAL_RECEIVER:
+		case B_WAL_SUMMARIZER:
+		case B_WAL_WRITER:
+			return &freeAuxList;
+
+			/*
+			 * Logger is not connected to shared memory, and does not have a
+			 * PGPROC entry, but we still allocate a child slot for it.
+			 */
+		case B_LOGGER:
+			return &freeAuxList;
+
+		case B_STANDALONE_BACKEND:
+		case B_INVALID:
+		case B_DEAD_END_BACKEND:
+			break;
+	}
+	elog(ERROR, "unexpected BackendType: %d", (int) btype);
+	return NULL;
+}
+
+/*
+ * Allocate a PMChild entry for a backend of given type.
+ *
+ * The entry is taken from the right pool.
+ *
+ * pmchild->child_slot is unique among all active child processes
+ */
+PMChild *
+AssignPostmasterChildSlot(BackendType btype)
+{
+	dlist_head *freelist;
+	PMChild    *pmchild;
+
+	freelist = GetFreeList(btype);
+
+	if (dlist_is_empty(freelist))
+		return NULL;
+
+	pmchild = dlist_container(PMChild, elem, dlist_pop_head_node(freelist));
+	pmchild->pid = 0;
+	pmchild->bkend_type = btype;
+	pmchild->rw = NULL;
+	pmchild->bgworker_notify = true;
+
+	/*
+	 * pmchild->child_slot for each entry was initialized when the array of
+	 * slots was allocated.
+	 */
+
+	dlist_push_head(&ActiveChildList, &pmchild->elem);
+
+	ReservePostmasterChildSlot(pmchild->child_slot);
+
+	/* FIXME: find a more elegant way to pass this */
+	MyPMChildSlot = pmchild->child_slot;
+
+	elog(DEBUG2, "assigned pm child slot %d for %s", pmchild->child_slot, PostmasterChildName(btype));
+
+	return pmchild;
+}
+
+/*
+ * Release a PMChild slot, after the child process has exited.
+ *
+ * Returns true if the child detached cleanly from shared memory, false
+ * otherwise (see ReleasePostmasterChildSlot).
+ */
+bool
+FreePostmasterChildSlot(PMChild *pmchild)
+{
+	elog(DEBUG2, "releasing pm child slot %d", pmchild->child_slot);
+
+	dlist_delete(&pmchild->elem);
+	if (pmchild->bkend_type == B_DEAD_END_BACKEND)
+	{
+		pfree(pmchild);
+		return true;
+	}
+	else
+	{
+		dlist_head *freelist;
+
+		freelist = GetFreeList(pmchild->bkend_type);
+		dlist_push_head(freelist, &pmchild->elem);
+		return ReleasePostmasterChildSlot(pmchild->child_slot);
+	}
+}
+
+PMChild *
+FindPostmasterChildByPid(int pid)
+{
+	dlist_iter	iter;
+
+	dlist_foreach(iter, &ActiveChildList)
+	{
+		PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
+
+		if (bp->pid == pid)
+			return bp;
+	}
+	return NULL;
+}
+
+/*
+ * Allocate a PMChild struct for a dead-end backend.  Dead-end children are
+ * not assigned a child_slot number.  The struct is palloc'd; returns NULL if
+ * out of memory.
+ */
+PMChild *
+AllocDeadEndChild(void)
+{
+	PMChild    *pmchild;
+
+	elog(DEBUG2, "allocating dead-end child");
+
+	pmchild = (PMChild *) palloc_extended(sizeof(PMChild), MCXT_ALLOC_NO_OOM);
+	if (pmchild)
+	{
+		pmchild->pid = 0;
+		pmchild->child_slot = 0;
+		pmchild->bkend_type = B_DEAD_END_BACKEND;
+		pmchild->rw = NULL;
+		pmchild->bgworker_notify = false;
+
+		dlist_push_head(&ActiveChildList, &pmchild->elem);
+	}
+
+	return pmchild;
+}
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 99c588ee0b..a029e28786 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -135,49 +135,8 @@
 #define BACKEND_TYPE_ALL 0xffffffff
 StaticAssertDecl(BACKEND_NUM_TYPES < 32, "too many backend types for uint32");
 
-/*
- * List of active backends (or child processes anyway; we don't actually
- * know whether a given child has become a backend or is still in the
- * authorization phase).  This is used mainly to keep track of how many
- * children we have and send them appropriate signals when necessary.
- *
- * As shown in the above set of backend types, this list includes not only
- * "normal" client sessions, but also autovacuum workers, walsenders, and
- * background workers.  (Note that at the time of launch, walsenders are
- * labeled B_BACKEND; we relabel them to B_WAL_SENDER
- * upon noticing they've changed their PMChildFlags entry.  Hence that check
- * must be done before any operation that needs to distinguish walsenders
- * from normal backends.)
- *
- * Also, "dead_end" children are in it: these are children launched just for
- * the purpose of sending a friendly rejection message to a would-be client.
- * We must track them because they are attached to shared memory, but we know
- * they will never become live backends.  dead_end children are not assigned a
- * PMChildSlot.  dead_end children have bkend_type B_DEAD_END_BACKEND.
- * FIXME: a dead-end backend can send query cancel?
- *
- * "Special" children such as the startup, bgwriter, autovacuum launcher, and
- * slot sync worker tasks are not in this list.  They are tracked via StartupPID
- * and other pid_t variables below.  (Thus, there can't be more than one of any
- * given "special" child process type.  We use BackendList entries for any
- * child process there can be more than one of.)
- */
-typedef struct bkend
-{
-	pid_t		pid;			/* process id of backend */
-	int			child_slot;		/* PMChildSlot for this backend, if any */
-	BackendType bkend_type;		/* child process flavor, see above */
-	RegisteredBgWorker *rw;		/* bgworker info, if this is a bgworker */
-	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
-	dlist_node	elem;			/* list link in BackendList */
-} Backend;
-
-static dlist_head BackendList = DLIST_STATIC_INIT(BackendList);
-
 BackgroundWorker *MyBgworkerEntry = NULL;
 
-
-
 /* The socket number we are listening for connections on */
 int			PostPortNumber = DEF_PGPORT;
 
@@ -229,17 +188,17 @@ bool		remove_temp_files_after_crash = true;
 bool		send_abort_for_crash = false;
 bool		send_abort_for_kill = false;
 
-/* PIDs of special child processes; 0 when not running */
-static pid_t StartupPID = 0,
-			BgWriterPID = 0,
-			CheckpointerPID = 0,
-			WalWriterPID = 0,
-			WalReceiverPID = 0,
-			WalSummarizerPID = 0,
-			AutoVacPID = 0,
-			PgArchPID = 0,
-			SysLoggerPID = 0,
-			SlotSyncWorkerPID = 0;
+/* special child processes; NULL when not running */
+static PMChild *StartupPMChild = NULL,
+		   *BgWriterPMChild = NULL,
+		   *CheckpointerPMChild = NULL,
+		   *WalWriterPMChild = NULL,
+		   *WalReceiverPMChild = NULL,
+		   *WalSummarizerPMChild = NULL,
+		   *AutoVacLauncherPMChild = NULL,
+		   *PgArchPMChild = NULL,
+		   *SysLoggerPMChild = NULL,
+		   *SlotSyncWorkerPMChild = NULL;
 
 /* Startup process's status */
 typedef enum
@@ -287,7 +246,7 @@ static bool FatalError = false; /* T if recovering from backend crash */
  * PM_HOT_STANDBY state.  (connsAllowed can also restrict launching.)
  * In other states we handle connection requests by launching "dead_end"
  * child processes, which will simply send the client an error message and
- * quit.  (We track these in the BackendList so that we can know when they
+ * quit.  (We track these in the ActiveChildList so that we can know when they
  * are all gone; this is important because they're still connected to shared
  * memory, and would interfere with an attempt to destroy the shmem segment,
  * possibly leading to SHMALL failure when we try to make a new one.)
@@ -393,7 +352,7 @@ static void process_pm_child_exit(void);
 static void process_pm_reload_request(void);
 static void process_pm_shutdown_request(void);
 static void dummy_handler(SIGNAL_ARGS);
-static void CleanupBackend(Backend *bp, int exitstatus);
+static void CleanupBackend(PMChild *bp, int exitstatus);
 static void HandleChildCrash(int pid, int exitstatus, const char *procname);
 static void LogChildExit(int lev, const char *procname,
 						 int pid, int exitstatus);
@@ -403,18 +362,18 @@ static void ExitPostmaster(int status) pg_attribute_noreturn();
 static int	ServerLoop(void);
 static int	BackendStartup(ClientSocket *client_sock);
 static void report_fork_failure_to_client(ClientSocket *client_sock, int errnum);
-static CAC_state canAcceptConnections(int backend_type);
-static void signal_child(pid_t pid, int signal);
-static void sigquit_child(pid_t pid);
+static CAC_state canAcceptConnections(BackendType backend_type);
+static void signal_child(PMChild *pmchild, int signal);
+static void sigquit_child(PMChild *pmchild);
 static bool SignalSomeChildren(int signal, uint32 targetMask);
 static void TerminateChildren(int signal);
 
 static int	CountChildren(uint32 targetMask);
-static Backend *assign_backendlist_entry(void);
+static PMChild *assign_backendlist_entry(void);
 static void LaunchMissingBackgroundProcesses(void);
 static void maybe_start_bgworkers(void);
 static bool CreateOptsFile(int argc, char *argv[], char *fullprogname);
-static pid_t StartChildProcess(BackendType type);
+static PMChild *StartChildProcess(BackendType type);
 static void StartAutovacuumWorker(void);
 static void InitPostmasterDeathWatchHandle(void);
 
@@ -893,9 +852,11 @@ PostmasterMain(int argc, char *argv[])
 
 	/*
 	 * Now that loadable modules have had their chance to alter any GUCs,
-	 * calculate MaxBackends.
+	 * calculate MaxBackends, and initialize the machinery to track child
+	 * processes.
 	 */
 	InitializeMaxBackends();
+	InitPostmasterChildSlots();
 
 	/*
 	 * Give preloaded libraries a chance to request additional shared memory.
@@ -1019,7 +980,15 @@ PostmasterMain(int argc, char *argv[])
 	/*
 	 * If enabled, start up syslogger collection subprocess
 	 */
-	SysLoggerPID = SysLogger_Start();
+	SysLoggerPMChild = AssignPostmasterChildSlot(B_LOGGER);
+	if (!SysLoggerPMChild)
+		elog(ERROR, "no postmaster child slot available for syslogger");
+	SysLoggerPMChild->pid = SysLogger_Start();
+	if (SysLoggerPMChild->pid == 0)
+	{
+		FreePostmasterChildSlot(SysLoggerPMChild);
+		SysLoggerPMChild = NULL;
+	}
 
 	/*
 	 * Reset whereToSendOutput from DestDebug (its starting state) to
@@ -1321,16 +1290,16 @@ PostmasterMain(int argc, char *argv[])
 	AddToDataDirLockFile(LOCK_FILE_LINE_PM_STATUS, PM_STATUS_STARTING);
 
 	/* Start bgwriter and checkpointer so they can help with recovery */
-	if (CheckpointerPID == 0)
-		CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
-	if (BgWriterPID == 0)
-		BgWriterPID = StartChildProcess(B_BG_WRITER);
+	if (CheckpointerPMChild == NULL)
+		CheckpointerPMChild = StartChildProcess(B_CHECKPOINTER);
+	if (BgWriterPMChild == NULL)
+		BgWriterPMChild = StartChildProcess(B_BG_WRITER);
 
 	/*
 	 * We're ready to rock and roll...
 	 */
-	StartupPID = StartChildProcess(B_STARTUP);
-	Assert(StartupPID != 0);
+	StartupPMChild = StartChildProcess(B_STARTUP);
+	Assert(StartupPMChild != NULL);
 	StartupStatus = STARTUP_RUNNING;
 	pmState = PM_STARTUP;
 
@@ -1660,8 +1629,8 @@ ServerLoop(void)
 		if (avlauncher_needs_signal)
 		{
 			avlauncher_needs_signal = false;
-			if (AutoVacPID != 0)
-				kill(AutoVacPID, SIGUSR2);
+			if (AutoVacLauncherPMChild != NULL)
+				kill(AutoVacLauncherPMChild->pid, SIGUSR2);
 		}
 
 #ifdef HAVE_PTHREAD_IS_THREADED_NP
@@ -1748,7 +1717,7 @@ ServerLoop(void)
  * know whether a NORMAL connection might turn into a walsender.)
  */
 static CAC_state
-canAcceptConnections(int backend_type)
+canAcceptConnections(BackendType backend_type)
 {
 	CAC_state	result = CAC_OK;
 
@@ -1779,21 +1748,6 @@ canAcceptConnections(int backend_type)
 	if (!connsAllowed && backend_type == B_BACKEND)
 		return CAC_SHUTDOWN;	/* shutdown is pending */
 
-	/*
-	 * Don't start too many children.
-	 *
-	 * We allow more connections here than we can have backends because some
-	 * might still be authenticating; they might fail auth, or some existing
-	 * backend might exit before the auth cycle is completed.  The exact
-	 * MaxBackends limit is enforced when a new backend tries to join the
-	 * shared-inval backend array.
-	 *
-	 * The limit here must match the sizes of the per-child-process arrays;
-	 * see comments for MaxLivePostmasterChildren().
-	 */
-	if (CountChildren(BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND)) >= MaxLivePostmasterChildren())
-		result = CAC_TOOMANY;
-
 	return result;
 }
 
@@ -1961,26 +1915,6 @@ process_pm_reload_request(void)
 				(errmsg("received SIGHUP, reloading configuration files")));
 		ProcessConfigFile(PGC_SIGHUP);
 		SignalSomeChildren(SIGHUP, BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND));
-		if (StartupPID != 0)
-			signal_child(StartupPID, SIGHUP);
-		if (BgWriterPID != 0)
-			signal_child(BgWriterPID, SIGHUP);
-		if (CheckpointerPID != 0)
-			signal_child(CheckpointerPID, SIGHUP);
-		if (WalWriterPID != 0)
-			signal_child(WalWriterPID, SIGHUP);
-		if (WalReceiverPID != 0)
-			signal_child(WalReceiverPID, SIGHUP);
-		if (WalSummarizerPID != 0)
-			signal_child(WalSummarizerPID, SIGHUP);
-		if (AutoVacPID != 0)
-			signal_child(AutoVacPID, SIGHUP);
-		if (PgArchPID != 0)
-			signal_child(PgArchPID, SIGHUP);
-		if (SysLoggerPID != 0)
-			signal_child(SysLoggerPID, SIGHUP);
-		if (SlotSyncWorkerPID != 0)
-			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -2218,15 +2152,15 @@ process_pm_child_exit(void)
 
 	while ((pid = waitpid(-1, &exitstatus, WNOHANG)) > 0)
 	{
-		bool		found;
-		dlist_mutable_iter iter;
+		PMChild    *pmchild;
 
 		/*
 		 * Check if this child was a startup process.
 		 */
-		if (pid == StartupPID)
+		if (StartupPMChild && pid == StartupPMChild->pid)
 		{
-			StartupPID = 0;
+			FreePostmasterChildSlot(StartupPMChild);
+			StartupPMChild = NULL;
 
 			/*
 			 * Startup process exited in response to a shutdown request (or it
@@ -2337,9 +2271,10 @@ process_pm_child_exit(void)
 		 * one at the next iteration of the postmaster's main loop, if
 		 * necessary.  Any other exit condition is treated as a crash.
 		 */
-		if (pid == BgWriterPID)
+		if (BgWriterPMChild && pid == BgWriterPMChild->pid)
 		{
-			BgWriterPID = 0;
+			FreePostmasterChildSlot(BgWriterPMChild);
+			BgWriterPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("background writer process"));
@@ -2349,9 +2284,10 @@ process_pm_child_exit(void)
 		/*
 		 * Was it the checkpointer?
 		 */
-		if (pid == CheckpointerPID)
+		if (CheckpointerPMChild && pid == CheckpointerPMChild->pid)
 		{
-			CheckpointerPID = 0;
+			FreePostmasterChildSlot(CheckpointerPMChild);
+			CheckpointerPMChild = NULL;
 			if (EXIT_STATUS_0(exitstatus) && pmState == PM_SHUTDOWN)
 			{
 				/*
@@ -2371,14 +2307,14 @@ process_pm_child_exit(void)
 				Assert(Shutdown > NoShutdown);
 
 				/* Waken archiver for the last time */
-				if (PgArchPID != 0)
-					signal_child(PgArchPID, SIGUSR2);
+				if (PgArchPMChild != NULL)
+					signal_child(PgArchPMChild, SIGUSR2);
 
 				/*
 				 * Waken walsenders for the last time. No regular backends
 				 * should be around anymore.
 				 */
-				SignalSomeChildren(SIGUSR2, BACKEND_TYPE_ALL & (1 << B_DEAD_END_BACKEND));
+				SignalSomeChildren(SIGUSR2, (1 << B_WAL_SENDER));
 
 				pmState = PM_SHUTDOWN_2;
 			}
@@ -2400,9 +2336,10 @@ process_pm_child_exit(void)
 		 * new one at the next iteration of the postmaster's main loop, if
 		 * necessary.  Any other exit condition is treated as a crash.
 		 */
-		if (pid == WalWriterPID)
+		if (WalWriterPMChild && pid == WalWriterPMChild->pid)
 		{
-			WalWriterPID = 0;
+			FreePostmasterChildSlot(WalWriterPMChild);
+			WalWriterPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("WAL writer process"));
@@ -2415,9 +2352,10 @@ process_pm_child_exit(void)
 		 * backends.  (If we need a new wal receiver, we'll start one at the
 		 * next iteration of the postmaster's main loop.)
 		 */
-		if (pid == WalReceiverPID)
+		if (WalReceiverPMChild && pid == WalReceiverPMChild->pid)
 		{
-			WalReceiverPID = 0;
+			FreePostmasterChildSlot(WalReceiverPMChild);
+			WalReceiverPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("WAL receiver process"));
@@ -2429,9 +2367,10 @@ process_pm_child_exit(void)
 		 * a new one at the next iteration of the postmaster's main loop, if
 		 * necessary.  Any other exit condition is treated as a crash.
 		 */
-		if (pid == WalSummarizerPID)
+		if (WalSummarizerPMChild && pid == WalSummarizerPMChild->pid)
 		{
-			WalSummarizerPID = 0;
+			FreePostmasterChildSlot(WalSummarizerPMChild);
+			WalSummarizerPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("WAL summarizer process"));
@@ -2444,9 +2383,10 @@ process_pm_child_exit(void)
 		 * loop, if necessary.  Any other exit condition is treated as a
 		 * crash.
 		 */
-		if (pid == AutoVacPID)
+		if (AutoVacLauncherPMChild && pid == AutoVacLauncherPMChild->pid)
 		{
-			AutoVacPID = 0;
+			FreePostmasterChildSlot(AutoVacLauncherPMChild);
+			AutoVacLauncherPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("autovacuum launcher process"));
@@ -2459,9 +2399,10 @@ process_pm_child_exit(void)
 		 * and just try to start a new one on the next cycle of the
 		 * postmaster's main loop, to retry archiving remaining files.
 		 */
-		if (pid == PgArchPID)
+		if (PgArchPMChild && pid == PgArchPMChild->pid)
 		{
-			PgArchPID = 0;
+			FreePostmasterChildSlot(PgArchPMChild);
+			PgArchPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("archiver process"));
@@ -2469,11 +2410,15 @@ process_pm_child_exit(void)
 		}
 
 		/* Was it the system logger?  If so, try to start a new one */
-		if (pid == SysLoggerPID)
+		if (SysLoggerPMChild && pid == SysLoggerPMChild->pid)
 		{
-			SysLoggerPID = 0;
 			/* for safety's sake, launch new logger *first* */
-			SysLoggerPID = SysLogger_Start();
+			SysLoggerPMChild->pid = SysLogger_Start();
+			if (SysLoggerPMChild->pid == 0)
+			{
+				FreePostmasterChildSlot(SysLoggerPMChild);
+				SysLoggerPMChild = NULL;
+			}
 			if (!EXIT_STATUS_0(exitstatus))
 				LogChildExit(LOG, _("system logger process"),
 							 pid, exitstatus);
@@ -2487,9 +2432,10 @@ process_pm_child_exit(void)
 		 * start a new one at the next iteration of the postmaster's main
 		 * loop, if necessary. Any other exit condition is treated as a crash.
 		 */
-		if (pid == SlotSyncWorkerPID)
+		if (SlotSyncWorkerPMChild && pid == SlotSyncWorkerPMChild->pid)
 		{
-			SlotSyncWorkerPID = 0;
+			FreePostmasterChildSlot(SlotSyncWorkerPMChild);
+			SlotSyncWorkerPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("slot sync worker process"));
@@ -2499,25 +2445,17 @@ process_pm_child_exit(void)
 		/*
 		 * Was it a backend or a background worker?
 		 */
-		found = false;
-		dlist_foreach_modify(iter, &BackendList)
+		pmchild = FindPostmasterChildByPid(pid);
+		if (pmchild)
 		{
-			Backend    *bp = dlist_container(Backend, elem, iter.cur);
-
-			if (bp->pid == pid)
-			{
-				dlist_delete(iter.cur);
-				CleanupBackend(bp, exitstatus);
-				found = true;
-				break;
-			}
+			CleanupBackend(pmchild, exitstatus);
 		}
 
 		/*
 		 * We don't know anything about this child process.  That's highly
 		 * unexpected, as we do track all the child processes that we fork.
 		 */
-		if (!found)
+		else
 		{
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus, _("untracked child process"));
@@ -2540,15 +2478,24 @@ process_pm_child_exit(void)
  * already been unlinked from BackendList, but we will free it here.
  */
 static void
-CleanupBackend(Backend *bp,
+CleanupBackend(PMChild *bp,
 			   int exitstatus)	/* child's exit status. */
 {
 	char		namebuf[MAXPGPATH];
 	char	   *procname;
 	bool		crashed = false;
 	bool		logged = false;
+	pid_t		bp_pid;
+	bool		bp_bgworker_notify;
+	BackendType bp_bkend_type;
+	RegisteredBgWorker *rw;
 
 	/* Construct a process name for log message */
+
+	/*
+	 * FIXME: use GetBackendTypeDesc here? How does the localization of that
+	 * work?
+	 */
 	if (bp->bkend_type == B_DEAD_END_BACKEND)
 	{
 		procname = _("dead end backend");
@@ -2589,25 +2536,28 @@ CleanupBackend(Backend *bp,
 #endif
 
 	/*
-	 * If the process attached to shared memory, check that it detached
-	 * cleanly.
+	 * Release the PMChild entry.
+	 *
+	 * If the process attached to shared memory, this also checks that it
+	 * detached cleanly.
 	 */
-	if (bp->bkend_type != B_DEAD_END_BACKEND)
+	bp_pid = bp->pid;
+	bp_bgworker_notify = bp->bgworker_notify;
+	bp_bkend_type = bp->bkend_type;
+	rw = bp->rw;
+	if (!FreePostmasterChildSlot(bp))
 	{
-		if (!ReleasePostmasterChildSlot(bp->child_slot))
-		{
-			/*
-			 * Uh-oh, the child failed to clean itself up.  Treat as a crash
-			 * after all.
-			 */
-			crashed = true;
-		}
+		/*
+		 * Uh-oh, the child failed to clean itself up.  Treat as a crash after
+		 * all.
+		 */
+		crashed = true;
 	}
+	bp = NULL;
 
 	if (crashed)
 	{
-		HandleChildCrash(bp->pid, exitstatus, namebuf);
-		pfree(bp);
+		HandleChildCrash(bp_pid, exitstatus, namebuf);
 		return;
 	}
 
@@ -2618,16 +2568,14 @@ CleanupBackend(Backend *bp,
 	 * gets skipped in the (probably very common) case where the backend has
 	 * never requested any such notifications.
 	 */
-	if (bp->bgworker_notify)
-		BackgroundWorkerStopNotifications(bp->pid);
+	if (bp_bgworker_notify)
+		BackgroundWorkerStopNotifications(bp_pid);
 
 	/*
 	 * If it was a background worker, also update its RegisteredWorker entry.
 	 */
-	if (bp->bkend_type == B_BG_WORKER)
+	if (bp_bkend_type == B_BG_WORKER)
 	{
-		RegisteredBgWorker *rw = bp->rw;
-
 		if (!EXIT_STATUS_0(exitstatus))
 		{
 			/* Record timestamp, so we know when to restart the worker. */
@@ -2646,7 +2594,7 @@ CleanupBackend(Backend *bp,
 		if (!logged)
 		{
 			LogChildExit(EXIT_STATUS_0(exitstatus) ? DEBUG1 : LOG,
-						 procname, bp->pid, exitstatus);
+						 procname, bp_pid, exitstatus);
 			logged = true;
 		}
 
@@ -2655,9 +2603,7 @@ CleanupBackend(Backend *bp,
 	}
 
 	if (!logged)
-		LogChildExit(DEBUG2, procname, bp->pid, exitstatus);
-
-	pfree(bp);
+		LogChildExit(DEBUG2, procname, bp_pid, exitstatus);
 }
 
 /*
@@ -2697,9 +2643,16 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	{
 		dlist_iter	iter;
 
-		dlist_foreach(iter, &BackendList)
+		dlist_foreach(iter, &ActiveChildList)
 		{
-			Backend    *bp = dlist_container(Backend, elem, iter.cur);
+			PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
+
+			/* We do NOT restart the syslogger */
+			if (bp == SysLoggerPMChild)
+				continue;
+
+			if (bp == StartupPMChild)
+				StartupStatus = STARTUP_SIGNALED;
 
 			/*
 			 * This backend is still alive.  Unless we did so already, tell it
@@ -2708,48 +2661,8 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 			 * We could exclude dead_end children here, but at least when
 			 * sending SIGABRT it seems better to include them.
 			 */
-			sigquit_child(bp->pid);
+			sigquit_child(bp);
 		}
-
-		if (StartupPID != 0)
-		{
-			sigquit_child(StartupPID);
-			StartupStatus = STARTUP_SIGNALED;
-		}
-
-		/* Take care of the bgwriter too */
-		if (BgWriterPID != 0)
-			sigquit_child(BgWriterPID);
-
-		/* Take care of the checkpointer too */
-		if (CheckpointerPID != 0)
-			sigquit_child(CheckpointerPID);
-
-		/* Take care of the walwriter too */
-		if (WalWriterPID != 0)
-			sigquit_child(WalWriterPID);
-
-		/* Take care of the walreceiver too */
-		if (WalReceiverPID != 0)
-			sigquit_child(WalReceiverPID);
-
-		/* Take care of the walsummarizer too */
-		if (WalSummarizerPID != 0)
-			sigquit_child(WalSummarizerPID);
-
-		/* Take care of the autovacuum launcher too */
-		if (AutoVacPID != 0)
-			sigquit_child(AutoVacPID);
-
-		/* Take care of the archiver too */
-		if (PgArchPID != 0)
-			sigquit_child(PgArchPID);
-
-		/* Take care of the slot sync worker too */
-		if (SlotSyncWorkerPID != 0)
-			sigquit_child(SlotSyncWorkerPID);
-
-		/* We do NOT restart the syslogger */
 	}
 
 	if (Shutdown != ImmediateShutdown)
@@ -2864,6 +2777,8 @@ PostmasterStateMachine(void)
 	 */
 	if (pmState == PM_STOP_BACKENDS)
 	{
+		uint32		targetMask;
+
 		/*
 		 * Forget any pending requests for background workers, since we're no
 		 * longer willing to launch any new workers.  (If additional requests
@@ -2871,29 +2786,27 @@ PostmasterStateMachine(void)
 		 */
 		ForgetUnstartedBackgroundWorkers();
 
-		/* Signal all backend children except walsenders and dead-end backends */
-		SignalSomeChildren(SIGTERM,
-						   BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND));
+		/* Signal all backend children except walsenders */
+		/* dead-end children are not signalled yet */
+		targetMask = (1 << B_BACKEND);
+		targetMask |= (1 << B_BG_WORKER);
+
 		/* and the autovac launcher too */
-		if (AutoVacPID != 0)
-			signal_child(AutoVacPID, SIGTERM);
+		targetMask |= (1 << B_AUTOVAC_LAUNCHER);
 		/* and the bgwriter too */
-		if (BgWriterPID != 0)
-			signal_child(BgWriterPID, SIGTERM);
+		targetMask |= (1 << B_BG_WRITER);
 		/* and the walwriter too */
-		if (WalWriterPID != 0)
-			signal_child(WalWriterPID, SIGTERM);
+		targetMask |= (1 << B_WAL_WRITER);
 		/* If we're in recovery, also stop startup and walreceiver procs */
-		if (StartupPID != 0)
-			signal_child(StartupPID, SIGTERM);
-		if (WalReceiverPID != 0)
-			signal_child(WalReceiverPID, SIGTERM);
-		if (WalSummarizerPID != 0)
-			signal_child(WalSummarizerPID, SIGTERM);
-		if (SlotSyncWorkerPID != 0)
-			signal_child(SlotSyncWorkerPID, SIGTERM);
+		targetMask |= (1 << B_STARTUP);
+		targetMask |= (1 << B_WAL_RECEIVER);
+
+		targetMask |= (1 << B_WAL_SUMMARIZER);
+		targetMask |= (1 << B_SLOTSYNC_WORKER);
 		/* checkpointer, archiver, stats, and syslogger may continue for now */
 
+		SignalSomeChildren(SIGTERM, targetMask);
+
 		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
 		pmState = PM_WAIT_BACKENDS;
 	}
@@ -2915,16 +2828,14 @@ PostmasterStateMachine(void)
 		 * here. Walsenders and archiver are also disregarded, they will be
 		 * terminated later after writing the checkpoint record.
 		 */
-		if (CountChildren(BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND)) == 0 &&
-			StartupPID == 0 &&
-			WalReceiverPID == 0 &&
-			WalSummarizerPID == 0 &&
-			BgWriterPID == 0 &&
-			(CheckpointerPID == 0 ||
-			 (!FatalError && Shutdown < ImmediateShutdown)) &&
-			WalWriterPID == 0 &&
-			AutoVacPID == 0 &&
-			SlotSyncWorkerPID == 0)
+		uint32		remaining;
+
+		remaining = (1 << B_WAL_SENDER) | (1 << B_ARCHIVER) | (1 << B_LOGGER);
+		remaining |= (1 << B_DEAD_END_BACKEND);
+		if (!FatalError && Shutdown < ImmediateShutdown)
+			remaining |= (1 << B_CHECKPOINTER);
+
+		if (CountChildren(BACKEND_TYPE_ALL & ~remaining) == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -2950,12 +2861,12 @@ PostmasterStateMachine(void)
 				 */
 				Assert(Shutdown > NoShutdown);
 				/* Start the checkpointer if not running */
-				if (CheckpointerPID == 0)
-					CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
+				if (CheckpointerPMChild == NULL)
+					CheckpointerPMChild = StartChildProcess(B_CHECKPOINTER);
 				/* And tell it to shut down */
-				if (CheckpointerPID != 0)
+				if (CheckpointerPMChild != NULL)
 				{
-					signal_child(CheckpointerPID, SIGUSR2);
+					signal_child(CheckpointerPMChild, SIGUSR2);
 					pmState = PM_SHUTDOWN;
 				}
 				else
@@ -2976,8 +2887,8 @@ PostmasterStateMachine(void)
 
 					/* Kill the walsenders and archiver, too */
 					SignalSomeChildren(SIGQUIT, BACKEND_TYPE_ALL);
-					if (PgArchPID != 0)
-						signal_child(PgArchPID, SIGQUIT);
+					if (PgArchPMChild != NULL)
+						signal_child(PgArchPMChild, SIGQUIT);
 				}
 			}
 		}
@@ -2991,7 +2902,10 @@ PostmasterStateMachine(void)
 		 * left by now anyway; what we're really waiting for is walsenders and
 		 * archiver.
 		 */
-		if (PgArchPID == 0 && CountChildren(BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND)) == 0)
+		uint32		remaining;
+
+		remaining = (1 << B_LOGGER) | (1 << B_DEAD_END_BACKEND);
+		if (CountChildren(BACKEND_TYPE_ALL & ~remaining) == 0)
 		{
 			ConfigurePostmasterWaitSet(false);
 			SignalSomeChildren(SIGTERM, 1 << B_DEAD_END_BACKEND);
@@ -3013,17 +2927,20 @@ PostmasterStateMachine(void)
 		 * normal state transition leading up to PM_WAIT_DEAD_END, or during
 		 * FatalError processing.
 		 */
-		if (dlist_is_empty(&BackendList) && PgArchPID == 0)
+		if (dlist_is_empty(&ActiveChildList) ||
+			(SysLoggerPMChild != NULL &&
+			 dlist_head_node(&ActiveChildList) == &SysLoggerPMChild->elem &&
+			 dlist_tail_node(&ActiveChildList) == &SysLoggerPMChild->elem))
 		{
 			/* These other guys should be dead already */
-			Assert(StartupPID == 0);
-			Assert(WalReceiverPID == 0);
-			Assert(WalSummarizerPID == 0);
-			Assert(BgWriterPID == 0);
-			Assert(CheckpointerPID == 0);
-			Assert(WalWriterPID == 0);
-			Assert(AutoVacPID == 0);
-			Assert(SlotSyncWorkerPID == 0);
+			Assert(StartupPMChild == NULL);
+			Assert(WalReceiverPMChild == NULL);
+			Assert(WalSummarizerPMChild == NULL);
+			Assert(BgWriterPMChild == NULL);
+			Assert(CheckpointerPMChild == NULL);
+			Assert(WalWriterPMChild == NULL);
+			Assert(AutoVacLauncherPMChild == NULL);
+			Assert(SlotSyncWorkerPMChild == NULL);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -3106,8 +3023,8 @@ PostmasterStateMachine(void)
 		/* re-create shared memory and semaphores */
 		CreateSharedMemoryAndSemaphores();
 
-		StartupPID = StartChildProcess(B_STARTUP);
-		Assert(StartupPID != 0);
+		StartupPMChild = StartChildProcess(B_STARTUP);
+		Assert(StartupPMChild != NULL);
 		StartupStatus = STARTUP_RUNNING;
 		pmState = PM_STARTUP;
 		/* crash recovery started, reset SIGKILL flag */
@@ -3130,8 +3047,21 @@ static void
 LaunchMissingBackgroundProcesses(void)
 {
 	/* Syslogger is active in all states */
-	if (SysLoggerPID == 0 && Logging_collector)
-		SysLoggerPID = SysLogger_Start();
+	if (SysLoggerPMChild == NULL && Logging_collector)
+	{
+		SysLoggerPMChild = AssignPostmasterChildSlot(B_LOGGER);
+		if (!SysLoggerPMChild)
+			elog(LOG, "no postmaster child slot available for syslogger");
+		else
+		{
+			SysLoggerPMChild->pid = SysLogger_Start();
+			if (SysLoggerPMChild->pid == 0)
+			{
+				FreePostmasterChildSlot(SysLoggerPMChild);
+				SysLoggerPMChild = NULL;
+			}
+		}
+	}
 
 	/*
 	 * The checkpointer and the background writer are active from the start,
@@ -3144,30 +3074,30 @@ LaunchMissingBackgroundProcesses(void)
 	if (pmState == PM_RUN || pmState == PM_RECOVERY ||
 		pmState == PM_HOT_STANDBY || pmState == PM_STARTUP)
 	{
-		if (CheckpointerPID == 0)
-			CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
-		if (BgWriterPID == 0)
-			BgWriterPID = StartChildProcess(B_BG_WRITER);
+		if (CheckpointerPMChild == NULL)
+			CheckpointerPMChild = StartChildProcess(B_CHECKPOINTER);
+		if (BgWriterPMChild == NULL)
+			BgWriterPMChild = StartChildProcess(B_BG_WRITER);
 	}
 
 	/*
 	 * WAL writer is needed only in normal operation (else we cannot be
 	 * writing any new WAL).
 	 */
-	if (WalWriterPID == 0 && pmState == PM_RUN)
-		WalWriterPID = StartChildProcess(B_WAL_WRITER);
+	if (WalWriterPMChild == NULL && pmState == PM_RUN)
+		WalWriterPMChild = StartChildProcess(B_WAL_WRITER);
 
 	/*
 	 * We don't want autovacuum to run in binary upgrade mode because
 	 * autovacuum might update relfrozenxid for empty tables before the
 	 * physical files are put in place.
 	 */
-	if (!IsBinaryUpgrade && AutoVacPID == 0 &&
+	if (!IsBinaryUpgrade && AutoVacLauncherPMChild == NULL &&
 		(AutoVacuumingActive() || start_autovac_launcher) &&
 		pmState == PM_RUN)
 	{
-		AutoVacPID = StartChildProcess(B_AUTOVAC_LAUNCHER);
-		if (AutoVacPID != 0)
+		AutoVacLauncherPMChild = StartChildProcess(B_AUTOVAC_LAUNCHER);
+		if (AutoVacLauncherPMChild != NULL)
 			start_autovac_launcher = false; /* signal processed */
 	}
 
@@ -3175,11 +3105,11 @@ LaunchMissingBackgroundProcesses(void)
 	 * If WAL archiving is enabled always, we are allowed to start archiver
 	 * even during recovery.
 	 */
-	if (PgArchPID == 0 &&
+	if (PgArchPMChild == NULL &&
 		((XLogArchivingActive() && pmState == PM_RUN) ||
 		 (XLogArchivingAlways() && (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) &&
 		PgArchCanRestart())
-		PgArchPID = StartChildProcess(B_ARCHIVER);
+		PgArchPMChild = StartChildProcess(B_ARCHIVER);
 
 	/*
 	 * If we need to start a slot sync worker, try to do that now
@@ -3189,10 +3119,10 @@ LaunchMissingBackgroundProcesses(void)
 	 * configured correctly, and it is the first time of worker's launch, or
 	 * enough time has passed since the worker was launched last.
 	 */
-	if (SlotSyncWorkerPID == 0 && pmState == PM_HOT_STANDBY &&
+	if (SlotSyncWorkerPMChild == NULL && pmState == PM_HOT_STANDBY &&
 		Shutdown <= SmartShutdown && sync_replication_slots &&
 		ValidateSlotSyncParams(LOG) && SlotSyncWorkerCanRestart())
-		SlotSyncWorkerPID = StartChildProcess(B_SLOTSYNC_WORKER);
+		SlotSyncWorkerPMChild = StartChildProcess(B_SLOTSYNC_WORKER);
 
 	/*
 	 * If we need to start a WAL receiver, try to do that now
@@ -3208,23 +3138,23 @@ LaunchMissingBackgroundProcesses(void)
 	 */
 	if (WalReceiverRequested)
 	{
-		if (WalReceiverPID == 0 &&
+		if (WalReceiverPMChild == NULL &&
 			(pmState == PM_STARTUP || pmState == PM_RECOVERY ||
 			 pmState == PM_HOT_STANDBY) &&
 			Shutdown <= SmartShutdown)
 		{
-			WalReceiverPID = StartChildProcess(B_WAL_RECEIVER);
-			if (WalReceiverPID != 0)
+			WalReceiverPMChild = StartChildProcess(B_WAL_RECEIVER);
+			if (WalReceiverPMChild != 0)
 				WalReceiverRequested = false;
 			/* else leave the flag set, so we'll try again later */
 		}
 	}
 
 	/* If we need to start a WAL summarizer, try to do that now */
-	if (summarize_wal && WalSummarizerPID == 0 &&
+	if (summarize_wal && WalSummarizerPMChild == NULL &&
 		(pmState == PM_RUN || pmState == PM_HOT_STANDBY) &&
 		Shutdown <= SmartShutdown)
-		WalSummarizerPID = StartChildProcess(B_WAL_SUMMARIZER);
+		WalSummarizerPMChild = StartChildProcess(B_WAL_SUMMARIZER);
 
 	/* Get other worker processes running, if needed */
 	if (StartWorkerNeeded || HaveCrashedWorker)
@@ -3248,8 +3178,14 @@ LaunchMissingBackgroundProcesses(void)
  * child twice will not cause any problems.
  */
 static void
-signal_child(pid_t pid, int signal)
+signal_child(PMChild *pmchild, int signal)
 {
+	pid_t		pid;
+
+	if (pmchild == NULL || pmchild->pid == 0)
+		return;
+	pid = pmchild->pid;
+
 	if (kill(pid, signal) < 0)
 		elog(DEBUG3, "kill(%ld,%d) failed: %m", (long) pid, signal);
 #ifdef HAVE_SETSID
@@ -3278,13 +3214,13 @@ signal_child(pid_t pid, int signal)
  * to use SIGABRT to collect per-child core dumps.
  */
 static void
-sigquit_child(pid_t pid)
+sigquit_child(PMChild *pmchild)
 {
 	ereport(DEBUG2,
 			(errmsg_internal("sending %s to process %d",
 							 (send_abort_for_crash ? "SIGABRT" : "SIGQUIT"),
-							 (int) pid)));
-	signal_child(pid, (send_abort_for_crash ? SIGABRT : SIGQUIT));
+							 (int) pmchild->pid)));
+	signal_child(pmchild, (send_abort_for_crash ? SIGABRT : SIGQUIT));
 }
 
 /*
@@ -3296,13 +3232,13 @@ SignalSomeChildren(int signal, uint32 targetMask)
 	dlist_iter	iter;
 	bool		signaled = false;
 
-	dlist_foreach(iter, &BackendList)
+	dlist_foreach(iter, &ActiveChildList)
 	{
-		Backend    *bp = dlist_container(Backend, elem, iter.cur);
+		PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
 
 		/*
-		 * Since target == BACKEND_TYPE_ALL is the most common case, we test
-		 * it first and avoid touching shared memory for every child.
+		 * Since targetMask == BACKEND_TYPE_ALL is the most common case, we
+		 * test it first and avoid touching shared memory for every child.
 		 */
 		if (targetMask != BACKEND_TYPE_ALL)
 		{
@@ -3321,7 +3257,7 @@ SignalSomeChildren(int signal, uint32 targetMask)
 		ereport(DEBUG4,
 				(errmsg_internal("sending signal %d to %s process %d",
 								 signal, GetBackendTypeDesc(bp->bkend_type), (int) bp->pid)));
-		signal_child(bp->pid, signal);
+		signal_child(bp, signal);
 		signaled = true;
 	}
 	return signaled;
@@ -3334,29 +3270,12 @@ SignalSomeChildren(int signal, uint32 targetMask)
 static void
 TerminateChildren(int signal)
 {
-	SignalSomeChildren(signal, BACKEND_TYPE_ALL);
-	if (StartupPID != 0)
+	SignalSomeChildren(signal, BACKEND_TYPE_ALL & ~(1 << B_LOGGER));
+	if (StartupPMChild != NULL)
 	{
-		signal_child(StartupPID, signal);
 		if (signal == SIGQUIT || signal == SIGKILL || signal == SIGABRT)
 			StartupStatus = STARTUP_SIGNALED;
 	}
-	if (BgWriterPID != 0)
-		signal_child(BgWriterPID, signal);
-	if (CheckpointerPID != 0)
-		signal_child(CheckpointerPID, signal);
-	if (WalWriterPID != 0)
-		signal_child(WalWriterPID, signal);
-	if (WalReceiverPID != 0)
-		signal_child(WalReceiverPID, signal);
-	if (WalSummarizerPID != 0)
-		signal_child(WalSummarizerPID, signal);
-	if (AutoVacPID != 0)
-		signal_child(AutoVacPID, signal);
-	if (PgArchPID != 0)
-		signal_child(PgArchPID, signal);
-	if (SlotSyncWorkerPID != 0)
-		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -3369,45 +3288,45 @@ TerminateChildren(int signal)
 static int
 BackendStartup(ClientSocket *client_sock)
 {
-	Backend    *bn;				/* for backend cleanup */
+	PMChild    *bn = NULL;
 	pid_t		pid;
 	BackendStartupData startup_data;
+	CAC_state	cac;
 
-	/*
-	 * Create backend data structure.  Better before the fork() so we can
-	 * handle failure cleanly.
-	 */
-	bn = (Backend *) palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
+	cac = canAcceptConnections(B_BACKEND);
+	if (cac == CAC_OK)
+	{
+		bn = AssignPostmasterChildSlot(B_BACKEND);
+		if (!bn)
+		{
+			/*
+			 * Too many regular child processes; launch a dead-end child
+			 * process instead.
+			 */
+			cac = CAC_TOOMANY;
+		}
+	}
 	if (!bn)
 	{
-		ereport(LOG,
-				(errcode(ERRCODE_OUT_OF_MEMORY),
-				 errmsg("out of memory")));
-		return STATUS_ERROR;
+		bn = AllocDeadEndChild();
+		if (!bn)
+		{
+			ereport(LOG,
+					(errcode(ERRCODE_OUT_OF_MEMORY),
+					 errmsg("out of memory")));
+			return STATUS_ERROR;
+		}
 	}
 
 	/* Pass down canAcceptConnections state */
-	startup_data.canAcceptConnections = canAcceptConnections(B_BACKEND);
+	startup_data.canAcceptConnections = cac;
 	bn->rw = NULL;
 
-	/*
-	 * Unless it's a dead_end child, assign it a child slot number
-	 */
-	if (startup_data.canAcceptConnections == CAC_OK)
-	{
-		bn->bkend_type = B_BACKEND; /* Can change later to WALSND */
-		bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
-	}
-	else
-	{
-		bn->bkend_type = B_DEAD_END_BACKEND;
-		bn->child_slot = 0;
-	}
-
 	/* Hasn't asked to be notified about any bgworkers yet */
 	bn->bgworker_notify = false;
 
-	pid = postmaster_child_launch(B_BACKEND,
+	MyPMChildSlot = bn->child_slot;
+	pid = postmaster_child_launch(bn->bkend_type,
 								  (char *) &startup_data, sizeof(startup_data),
 								  client_sock);
 	if (pid < 0)
@@ -3415,9 +3334,7 @@ BackendStartup(ClientSocket *client_sock)
 		/* in parent, fork failed */
 		int			save_errno = errno;
 
-		if (bn->child_slot != 0)
-			(void) ReleasePostmasterChildSlot(bn->child_slot);
-		pfree(bn);
+		(void) FreePostmasterChildSlot(bn);
 		errno = save_errno;
 		ereport(LOG,
 				(errmsg("could not fork new process for connection: %m")));
@@ -3435,7 +3352,6 @@ BackendStartup(ClientSocket *client_sock)
 	 * of backends.
 	 */
 	bn->pid = pid;
-	dlist_push_head(&BackendList, &bn->elem);
 
 	return STATUS_OK;
 }
@@ -3534,9 +3450,9 @@ process_pm_pmsignal(void)
 		 * Start the archiver if we're responsible for (re-)archiving received
 		 * files.
 		 */
-		Assert(PgArchPID == 0);
+		Assert(PgArchPMChild == NULL);
 		if (XLogArchivingAlways())
-			PgArchPID = StartChildProcess(B_ARCHIVER);
+			PgArchPMChild = StartChildProcess(B_ARCHIVER);
 
 		/*
 		 * If we aren't planning to enter hot standby mode later, treat
@@ -3582,16 +3498,16 @@ process_pm_pmsignal(void)
 	}
 
 	/* Tell syslogger to rotate logfile if requested */
-	if (SysLoggerPID != 0)
+	if (SysLoggerPMChild != NULL)
 	{
 		if (CheckLogrotateSignal())
 		{
-			signal_child(SysLoggerPID, SIGUSR1);
+			signal_child(SysLoggerPMChild, SIGUSR1);
 			RemoveLogrotateSignalFiles();
 		}
 		else if (CheckPostmasterSignal(PMSIGNAL_ROTATE_LOGFILE))
 		{
-			signal_child(SysLoggerPID, SIGUSR1);
+			signal_child(SysLoggerPMChild, SIGUSR1);
 		}
 	}
 
@@ -3638,7 +3554,7 @@ process_pm_pmsignal(void)
 		PostmasterStateMachine();
 	}
 
-	if (StartupPID != 0 &&
+	if (StartupPMChild != NULL &&
 		(pmState == PM_STARTUP || pmState == PM_RECOVERY ||
 		 pmState == PM_HOT_STANDBY) &&
 		CheckPromoteSignal())
@@ -3649,7 +3565,7 @@ process_pm_pmsignal(void)
 		 * Leave the promote signal file in place and let the Startup process
 		 * do the unlink.
 		 */
-		signal_child(StartupPID, SIGUSR2);
+		signal_child(StartupPMChild, SIGUSR2);
 	}
 }
 
@@ -3676,13 +3592,13 @@ CountChildren(uint32 targetMask)
 	dlist_iter	iter;
 	int			cnt = 0;
 
-	dlist_foreach(iter, &BackendList)
+	dlist_foreach(iter, &ActiveChildList)
 	{
-		Backend    *bp = dlist_container(Backend, elem, iter.cur);
+		PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
 
 		/*
-		 * Since target == BACKEND_TYPE_ALL is the most common case, we test
-		 * it first and avoid touching shared memory for every child.
+		 * Since targetMask == BACKEND_TYPE_ALL is the most common case, we
+		 * test it first and avoid touching shared memory for every child.
 		 */
 		if (targetMask != BACKEND_TYPE_ALL)
 		{
@@ -3713,15 +3629,33 @@ CountChildren(uint32 targetMask)
  * Return value of StartChildProcess is subprocess' PID, or 0 if failed
  * to start subprocess.
  */
-static pid_t
+static PMChild *
 StartChildProcess(BackendType type)
 {
+	PMChild    *pmchild;
 	pid_t		pid;
 
+	pmchild = AssignPostmasterChildSlot(type);
+	if (!pmchild)
+	{
+		if (type == B_AUTOVAC_WORKER)
+			ereport(LOG,
+					(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+					 errmsg("no slot available for new autovacuum worker process")));
+		else
+		{
+			/* shouldn't happen because we allocate enough slots */
+			elog(LOG, "no postmaster child slot available for aux process");
+		}
+		return NULL;
+	}
+
+	MyPMChildSlot = pmchild->child_slot;
 	pid = postmaster_child_launch(type, NULL, 0, NULL);
 	if (pid < 0)
 	{
 		/* in parent, fork failed */
+		FreePostmasterChildSlot(pmchild);
 		ereport(LOG,
 				(errmsg("could not fork \"%s\" process: %m", PostmasterChildName(type))));
 
@@ -3731,13 +3665,14 @@ StartChildProcess(BackendType type)
 		 */
 		if (type == B_STARTUP)
 			ExitPostmaster(1);
-		return 0;
+		return NULL;
 	}
 
 	/*
 	 * in parent, successful fork
 	 */
-	return pid;
+	pmchild->pid = pid;
+	return pmchild;
 }
 
 /*
@@ -3752,7 +3687,7 @@ StartChildProcess(BackendType type)
 static void
 StartAutovacuumWorker(void)
 {
-	Backend    *bn;
+	PMChild    *bn;
 
 	/*
 	 * If not in condition to run a process, don't try, but handle it like a
@@ -3763,34 +3698,20 @@ StartAutovacuumWorker(void)
 	 */
 	if (canAcceptConnections(B_AUTOVAC_WORKER) == CAC_OK)
 	{
-		bn = (Backend *) palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
+		bn = StartChildProcess(B_AUTOVAC_WORKER);
 		if (bn)
 		{
-			/* Autovac workers need a child slot */
-			bn->bkend_type = B_AUTOVAC_WORKER;
-			bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
 			bn->bgworker_notify = false;
 			bn->rw = NULL;
-
-			bn->pid = StartChildProcess(B_AUTOVAC_WORKER);
-			if (bn->pid > 0)
-			{
-				dlist_push_head(&BackendList, &bn->elem);
-				/* all OK */
-				return;
-			}
-
+			return;
+		}
+		else
+		{
 			/*
 			 * fork failed, fall through to report -- actual error message was
 			 * logged by StartChildProcess
 			 */
-			(void) ReleasePostmasterChildSlot(bn->child_slot);
-			pfree(bn);
 		}
-		else
-			ereport(LOG,
-					(errcode(ERRCODE_OUT_OF_MEMORY),
-					 errmsg("out of memory")));
 	}
 
 	/*
@@ -3802,7 +3723,7 @@ StartAutovacuumWorker(void)
 	 * quick succession between the autovac launcher and postmaster in case
 	 * things get ugly.
 	 */
-	if (AutoVacPID != 0)
+	if (AutoVacLauncherPMChild != NULL)
 	{
 		AutoVacWorkerFailed();
 		avlauncher_needs_signal = true;
@@ -3846,23 +3767,6 @@ CreateOptsFile(int argc, char *argv[], char *fullprogname)
 }
 
 
-/*
- * MaxLivePostmasterChildren
- *
- * This reports the number of entries needed in the per-child-process array
- * (PMChildFlags).  It includes regular backends, autovac workers, walsenders
- * and background workers, but not special children nor dead_end children.
- * This allows the array to have a fixed maximum size, to wit the same
- * too-many-children limit enforced by canAcceptConnections().  The exact value
- * isn't too critical as long as it's more than MaxBackends.
- */
-int
-MaxLivePostmasterChildren(void)
-{
-	return 2 * (MaxConnections + autovacuum_max_workers + 1 +
-				max_wal_senders + max_worker_processes);
-}
-
 /*
  * Start a new bgworker.
  * Starting time conditions must have been checked already.
@@ -3875,7 +3779,7 @@ MaxLivePostmasterChildren(void)
 static bool
 do_start_bgworker(RegisteredBgWorker *rw)
 {
-	Backend    *bn;
+	PMChild    *bn;
 	pid_t		worker_pid;
 
 	Assert(rw->rw_pid == 0);
@@ -3902,6 +3806,7 @@ do_start_bgworker(RegisteredBgWorker *rw)
 			(errmsg_internal("starting background worker process \"%s\"",
 							 rw->rw_worker.bgw_name)));
 
+	MyPMChildSlot = bn->child_slot;
 	worker_pid = postmaster_child_launch(B_BG_WORKER, (char *) &rw->rw_worker, sizeof(BackgroundWorker), NULL);
 	if (worker_pid == -1)
 	{
@@ -3909,8 +3814,7 @@ do_start_bgworker(RegisteredBgWorker *rw)
 		ereport(LOG,
 				(errmsg("could not fork background worker process: %m")));
 		/* undo what assign_backendlist_entry did */
-		ReleasePostmasterChildSlot(bn->child_slot);
-		pfree(bn);
+		FreePostmasterChildSlot(bn);
 
 		/* mark entry as crashed, so we'll try again later */
 		rw->rw_crashed_at = GetCurrentTimestamp();
@@ -3921,8 +3825,6 @@ do_start_bgworker(RegisteredBgWorker *rw)
 	rw->rw_pid = worker_pid;
 	bn->pid = rw->rw_pid;
 	ReportBackgroundWorkerPID(rw);
-	/* add new worker to lists of backends */
-	dlist_push_head(&BackendList, &bn->elem);
 	return true;
 }
 
@@ -3970,17 +3872,13 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
  *
  * On failure, return NULL.
  */
-static Backend *
+static PMChild *
 assign_backendlist_entry(void)
 {
-	Backend    *bn;
+	PMChild    *bn;
 
-	/*
-	 * Check that database state allows another connection.  Currently the
-	 * only possible failure is CAC_TOOMANY, so we just log an error message
-	 * based on that rather than checking the error code precisely.
-	 */
-	if (canAcceptConnections(B_BG_WORKER) != CAC_OK)
+	bn = AssignPostmasterChildSlot(B_BG_WORKER);
+	if (bn == NULL)
 	{
 		ereport(LOG,
 				(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
@@ -3988,16 +3886,6 @@ assign_backendlist_entry(void)
 		return NULL;
 	}
 
-	bn = palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
-	if (bn == NULL)
-	{
-		ereport(LOG,
-				(errcode(ERRCODE_OUT_OF_MEMORY),
-				 errmsg("out of memory")));
-		return NULL;
-	}
-
-	bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
 	bn->bkend_type = B_BG_WORKER;
 	bn->bgworker_notify = false;
 
@@ -4138,11 +4026,11 @@ bool
 PostmasterMarkPIDForWorkerNotify(int pid)
 {
 	dlist_iter	iter;
-	Backend    *bp;
+	PMChild    *bp;
 
-	dlist_foreach(iter, &BackendList)
+	dlist_foreach(iter, &ActiveChildList)
 	{
-		bp = dlist_container(Backend, elem, iter.cur);
+		bp = dlist_container(PMChild, elem, iter.cur);
 		if (bp->pid == pid)
 		{
 			bp->bgworker_notify = true;
diff --git a/src/backend/storage/ipc/pmsignal.c b/src/backend/storage/ipc/pmsignal.c
index cb99e77476..86970bf69b 100644
--- a/src/backend/storage/ipc/pmsignal.c
+++ b/src/backend/storage/ipc/pmsignal.c
@@ -47,11 +47,11 @@
  * exited without performing proper shutdown.  The per-child-process flags
  * have three possible states: UNUSED, ASSIGNED, ACTIVE.  An UNUSED slot is
  * available for assignment.  An ASSIGNED slot is associated with a postmaster
- * child process, but either the process has not touched shared memory yet,
- * or it has successfully cleaned up after itself.  A ACTIVE slot means the
- * process is actively using shared memory.  The slots are assigned to
- * child processes at random, and postmaster.c is responsible for tracking
- * which one goes with which PID.
+ * child process, but either the process has not touched shared memory yet, or
+ * it has successfully cleaned up after itself.  An ACTIVE slot means the
+ * process is actively using shared memory.  The slots are assigned to child
+ * processes by postmaster, and postmaster.c is responsible for tracking which
+ * one goes with which PID.
  *
  * Actually there is a fourth state, WALSENDER.  This is just like ACTIVE,
  * but carries the extra information that the child is a WAL sender.
@@ -83,15 +83,6 @@ struct PMSignalData
 /* PMSignalState pointer is valid in both postmaster and child processes */
 NON_EXEC_STATIC volatile PMSignalData *PMSignalState = NULL;
 
-/*
- * These static variables are valid only in the postmaster.  We keep a
- * duplicative private array so that we can trust its state even if some
- * failing child has clobbered the PMSignalData struct in shared memory.
- */
-static int	num_child_inuse;	/* # of entries in PMChildInUse[] */
-static int	next_child_inuse;	/* next slot to try to assign */
-static bool *PMChildInUse;		/* true if i'th flag slot is assigned */
-
 /*
  * Signal handler to be notified if postmaster dies.
  */
@@ -155,25 +146,7 @@ PMSignalShmemInit(void)
 	{
 		/* initialize all flags to zeroes */
 		MemSet(unvolatize(PMSignalData *, PMSignalState), 0, PMSignalShmemSize());
-		num_child_inuse = MaxLivePostmasterChildren();
-		PMSignalState->num_child_flags = num_child_inuse;
-
-		/*
-		 * Also allocate postmaster's private PMChildInUse[] array.  We
-		 * might've already done that in a previous shared-memory creation
-		 * cycle, in which case free the old array to avoid a leak.  (Do it
-		 * like this to support the possibility that MaxLivePostmasterChildren
-		 * changed.)  In a standalone backend, we do not need this.
-		 */
-		if (PostmasterContext != NULL)
-		{
-			if (PMChildInUse)
-				pfree(PMChildInUse);
-			PMChildInUse = (bool *)
-				MemoryContextAllocZero(PostmasterContext,
-									   num_child_inuse * sizeof(bool));
-		}
-		next_child_inuse = 0;
+		PMSignalState->num_child_flags = MaxLivePostmasterChildren();
 	}
 }
 
@@ -239,41 +212,22 @@ GetQuitSignalReason(void)
 
 
 /*
- * AssignPostmasterChildSlot - select an unused slot for a new postmaster
- * child process, and set its state to ASSIGNED.  Returns a slot number
- * (one to N).
+ * ReservePostmasterChildSlot - mark the given slot as ASSIGNED for a new
+ * postmaster child process.
  *
  * Only the postmaster is allowed to execute this routine, so we need no
  * special locking.
  */
-int
-AssignPostmasterChildSlot(void)
+void
+ReservePostmasterChildSlot(int slot)
 {
-	int			slot = next_child_inuse;
-	int			n;
+	Assert(slot > 0 && slot <= PMSignalState->num_child_flags);
+	slot--;
 
-	/*
-	 * Scan for a free slot.  Notice that we trust nothing about the contents
-	 * of PMSignalState, but use only postmaster-local data for this decision.
-	 * We track the last slot assigned so as not to waste time repeatedly
-	 * rescanning low-numbered slots.
-	 */
-	for (n = num_child_inuse; n > 0; n--)
-	{
-		if (--slot < 0)
-			slot = num_child_inuse - 1;
-		if (!PMChildInUse[slot])
-		{
-			PMChildInUse[slot] = true;
-			PMSignalState->PMChildFlags[slot] = PM_CHILD_ASSIGNED;
-			next_child_inuse = slot;
-			return slot + 1;
-		}
-	}
+	if (PMSignalState->PMChildFlags[slot] != PM_CHILD_UNUSED)
+		elog(FATAL, "postmaster child slot is already in use");
 
-	/* Out of slots ... should never happen, else postmaster.c messed up */
-	elog(FATAL, "no free slots in PMChildFlags array");
-	return 0;					/* keep compiler quiet */
+	PMSignalState->PMChildFlags[slot] = PM_CHILD_ASSIGNED;
 }
 
 /*
@@ -288,17 +242,18 @@ ReleasePostmasterChildSlot(int slot)
 {
 	bool		result;
 
-	Assert(slot > 0 && slot <= num_child_inuse);
+	Assert(slot > 0 && slot <= PMSignalState->num_child_flags);
 	slot--;
 
 	/*
 	 * Note: the slot state might already be unused, because the logic in
 	 * postmaster.c is such that this might get called twice when a child
 	 * crashes.  So we don't try to Assert anything about the state.
+	 *
+	 * FIXME: does that still happen?
 	 */
 	result = (PMSignalState->PMChildFlags[slot] == PM_CHILD_ASSIGNED);
 	PMSignalState->PMChildFlags[slot] = PM_CHILD_UNUSED;
-	PMChildInUse[slot] = false;
 	return result;
 }
 
@@ -309,7 +264,7 @@ ReleasePostmasterChildSlot(int slot)
 bool
 IsPostmasterChildWalSender(int slot)
 {
-	Assert(slot > 0 && slot <= num_child_inuse);
+	Assert(slot > 0 && slot <= PMSignalState->num_child_flags);
 	slot--;
 
 	if (PMSignalState->PMChildFlags[slot] == PM_CHILD_WALSENDER)
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 9536469e89..2fa709137f 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -311,14 +311,9 @@ InitProcess(void)
 	/*
 	 * Before we start accessing the shared memory in a serious way, mark
 	 * ourselves as an active postmaster child; this is so that the postmaster
-	 * can detect it if we exit without cleaning up.  (XXX autovac launcher
-	 * currently doesn't participate in this; it probably should.)
-	 *
-	 * Slot sync worker also does not participate in it, see comments atop
-	 * 'struct bkend' in postmaster.c.
+	 * can detect it if we exit without cleaning up.
 	 */
-	if (IsUnderPostmaster && !AmAutoVacuumLauncherProcess() &&
-		!AmLogicalSlotSyncWorkerProcess())
+	if (IsUnderPostmaster)
 		MarkPostmasterChildActive();
 
 	/* Decide which list should supply our PGPROC. */
@@ -536,6 +531,9 @@ InitAuxiliaryProcess(void)
 	if (MyProc != NULL)
 		elog(ERROR, "you already exist");
 
+	if (IsUnderPostmaster)
+		MarkPostmasterChildActive();
+
 	/*
 	 * We use the ProcStructLock to protect assignment and releasing of
 	 * AuxiliaryProcs entries.
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 63c12917cf..deca2e8370 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -13,8 +13,39 @@
 #ifndef _POSTMASTER_H
 #define _POSTMASTER_H
 
+#include "lib/ilist.h"
 #include "miscadmin.h"
 
+/*
+ * A struct representing an active postmaster child process.  This is used
+ * mainly to keep track of how many children we have and send them appropriate
+ * signals when necessary.  All postmaster child processes are assigned a
+ * PMChild entry. That includes "normal" client sessions, but also autovacuum
+ * workers, walsenders, background workers, and aux processes.  (Note that at
+ * the time of launch, walsenders are labeled B_BACKEND; we relabel them to
+ * B_WAL_SENDER upon noticing they've changed their PMChildFlags entry.  Hence
+ * that check must be done before any operation that needs to distinguish
+ * walsenders from normal backends.)
+ *
+ * "dead_end" children are also allocated a PMChild entry: these are children
+ * launched just for the purpose of sending a friendly rejection message to a
+ * would-be client.  We must track them because they are attached to shared
+ * memory, but we know they will never become live backends.
+ *
+ * 'child_slot' is an identifier that is unique across all running child
+ * processes.  It is used as an index into the PMChildFlags array. dead_end
+ * children are not assigned a child_slot.
+ */
+typedef struct
+{
+	pid_t		pid;			/* process id of backend */
+	int			child_slot;		/* PMChildSlot for this backend, if any */
+	BackendType bkend_type;		/* child process flavor, see above */
+	struct RegisteredBgWorker *rw;	/* bgworker info, if this is a bgworker */
+	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
+	dlist_node	elem;			/* list link in BackendList */
+} PMChild;
+
 /* GUC options */
 extern PGDLLIMPORT bool EnableSSL;
 extern PGDLLIMPORT int SuperuserReservedConnections;
@@ -80,6 +111,15 @@ const char *PostmasterChildName(BackendType child_type);
 extern void SubPostmasterMain(int argc, char *argv[]) pg_attribute_noreturn();
 #endif
 
+/* prototypes for functions in pmchild.c */
+extern dlist_head ActiveChildList;
+
+extern void InitPostmasterChildSlots(void);
+extern PMChild *AssignPostmasterChildSlot(BackendType btype);
+extern bool FreePostmasterChildSlot(PMChild *pmchild);
+extern PMChild *FindPostmasterChildByPid(int pid);
+extern PMChild *AllocDeadEndChild(void);
+
 /*
  * Note: MAX_BACKENDS is limited to 2^18-1 because that's the width reserved
  * for buffer references in buf_internals.h.  This limitation could be lifted
diff --git a/src/include/storage/pmsignal.h b/src/include/storage/pmsignal.h
index 3b9336b83c..2ab198fc31 100644
--- a/src/include/storage/pmsignal.h
+++ b/src/include/storage/pmsignal.h
@@ -70,7 +70,7 @@ extern void SendPostmasterSignal(PMSignalReason reason);
 extern bool CheckPostmasterSignal(PMSignalReason reason);
 extern void SetQuitSignalReason(QuitSignalReason reason);
 extern QuitSignalReason GetQuitSignalReason(void);
-extern int	AssignPostmasterChildSlot(void);
+extern void ReservePostmasterChildSlot(int slot);
 extern bool ReleasePostmasterChildSlot(int slot);
 extern bool IsPostmasterChildWalSender(int slot);
 extern void MarkPostmasterChildActive(void);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 547d14b3e7..ad39c0741a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -230,7 +230,6 @@ BTWriteState
 BUF_MEM
 BYTE
 BY_HANDLE_FILE_INFORMATION
-Backend
 BackendParameters
 BackendStartupData
 BackendState
@@ -1927,6 +1926,7 @@ PLyTransformToOb
 PLyTupleToOb
 PLyUnicode_FromStringAndSize_t
 PLy_elog_impl_t
+PMChild
 PMINIDUMP_CALLBACK_INFORMATION
 PMINIDUMP_EXCEPTION_INFORMATION
 PMINIDUMP_USER_STREAM_INFORMATION
-- 
2.39.2



  [text/x-patch] v4-0007-Pass-MyPMChildSlot-as-an-explicit-argument-to-chi.patch (10.7K, ../../[email protected]/8-v4-0007-Pass-MyPMChildSlot-as-an-explicit-argument-to-chi.patch)
  download | inline diff:
From 088f7911d2e300bad23e5afe66370340a96275e4 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Thu, 1 Aug 2024 22:58:17 +0300
Subject: [PATCH v4 7/8] Pass MyPMChildSlot as an explicit argument to child
 process

All the other global variables passed from postmaster to child are
have the same value in all the processes, while MyPMChildSlot is more
like a parameter to each child process.
---
 src/backend/postmaster/launch_backend.c | 32 ++++++++++++++++---------
 src/backend/postmaster/pmchild.c        |  3 ---
 src/backend/postmaster/postmaster.c     | 16 ++++++-------
 src/backend/postmaster/syslogger.c      |  8 ++++---
 src/include/postmaster/postmaster.h     |  1 +
 src/include/postmaster/syslogger.h      |  2 +-
 6 files changed, 35 insertions(+), 27 deletions(-)

diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index b0b91dc97f..4e93bd1d94 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -96,7 +96,6 @@ typedef int InheritableSocket;
 typedef struct
 {
 	char		DataDir[MAXPGPATH];
-	int			MyPMChildSlot;
 #ifndef WIN32
 	unsigned long UsedShmemSegID;
 #else
@@ -137,6 +136,8 @@ typedef struct
 	char		my_exec_path[MAXPGPATH];
 	char		pkglib_path[MAXPGPATH];
 
+	int			MyPMChildSlot;
+
 	/*
 	 * These are only used by backend processes, but are here because passing
 	 * a socket needs some special handling on Windows. 'client_sock' is an
@@ -158,13 +159,16 @@ typedef struct
 static void read_backend_variables(char *id, char **startup_data, size_t *startup_data_len);
 static void restore_backend_variables(BackendParameters *param);
 
-static bool save_backend_variables(BackendParameters *param, ClientSocket *client_sock,
+static bool save_backend_variables(BackendParameters *param, int child_slot,
+								   ClientSocket *client_sock,
 #ifdef WIN32
 								   HANDLE childProcess, pid_t childPid,
 #endif
 								   char *startup_data, size_t startup_data_len);
 
-static pid_t internal_forkexec(const char *child_kind, char *startup_data, size_t startup_data_len, ClientSocket *client_sock);
+static pid_t internal_forkexec(const char *child_kind, int child_slot,
+							   char *startup_data, size_t startup_data_len,
+							   ClientSocket *client_sock);
 
 #endif							/* EXEC_BACKEND */
 
@@ -226,7 +230,7 @@ PostmasterChildName(BackendType child_type)
  * the child process.
  */
 pid_t
-postmaster_child_launch(BackendType child_type,
+postmaster_child_launch(BackendType child_type, int child_slot,
 						char *startup_data, size_t startup_data_len,
 						ClientSocket *client_sock)
 {
@@ -235,7 +239,7 @@ postmaster_child_launch(BackendType child_type,
 	Assert(IsPostmasterEnvironment && !IsUnderPostmaster);
 
 #ifdef EXEC_BACKEND
-	pid = internal_forkexec(child_process_kinds[child_type].name,
+	pid = internal_forkexec(child_process_kinds[child_type].name, child_slot,
 							startup_data, startup_data_len, client_sock);
 	/* the child process will arrive in SubPostmasterMain */
 #else							/* !EXEC_BACKEND */
@@ -263,6 +267,7 @@ postmaster_child_launch(BackendType child_type,
 		 */
 		MemoryContextSwitchTo(TopMemoryContext);
 
+		MyPMChildSlot = child_slot;
 		if (client_sock)
 		{
 			MyClientSocket = palloc(sizeof(ClientSocket));
@@ -289,7 +294,8 @@ postmaster_child_launch(BackendType child_type,
  * - fork():s, and then exec():s the child process
  */
 static pid_t
-internal_forkexec(const char *child_kind, char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
+internal_forkexec(const char *child_kind, int child_slot,
+				  char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
 {
 	static unsigned long tmpBackendFileNum = 0;
 	pid_t		pid;
@@ -309,7 +315,7 @@ internal_forkexec(const char *child_kind, char *startup_data, size_t startup_dat
 	 */
 	paramsz = SizeOfBackendParameters(startup_data_len);
 	param = palloc0(paramsz);
-	if (!save_backend_variables(param, client_sock, startup_data, startup_data_len))
+	if (!save_backend_variables(param, child_slot, client_sock, startup_data, startup_data_len))
 	{
 		pfree(param);
 		return -1;				/* log made by save_backend_variables */
@@ -398,7 +404,8 @@ internal_forkexec(const char *child_kind, char *startup_data, size_t startup_dat
  *	 file is complete.
  */
 static pid_t
-internal_forkexec(const char *child_kind, char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
+internal_forkexec(const char *child_kind, int child_slot,
+				  char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
 {
 	int			retry_count = 0;
 	STARTUPINFO si;
@@ -479,7 +486,9 @@ retry:
 		return -1;
 	}
 
-	if (!save_backend_variables(param, client_sock, pi.hProcess, pi.dwProcessId, startup_data, startup_data_len))
+	if (!save_backend_variables(param, child_slot, client_sock,
+								pi.hProcess, pi.dwProcessId,
+								startup_data, startup_data_len))
 	{
 		/*
 		 * log made by save_backend_variables, but we have to clean up the
@@ -691,7 +700,8 @@ static void read_inheritable_socket(SOCKET *dest, InheritableSocket *src);
 
 /* Save critical backend variables into the BackendParameters struct */
 static bool
-save_backend_variables(BackendParameters *param, ClientSocket *client_sock,
+save_backend_variables(BackendParameters *param,
+					   int child_slot, ClientSocket *client_sock,
 #ifdef WIN32
 					   HANDLE childProcess, pid_t childPid,
 #endif
@@ -708,7 +718,7 @@ save_backend_variables(BackendParameters *param, ClientSocket *client_sock,
 
 	strlcpy(param->DataDir, DataDir, MAXPGPATH);
 
-	param->MyPMChildSlot = MyPMChildSlot;
+	param->MyPMChildSlot = child_slot;
 
 #ifdef WIN32
 	param->ShmemProtectiveRegion = ShmemProtectiveRegion;
diff --git a/src/backend/postmaster/pmchild.c b/src/backend/postmaster/pmchild.c
index 735c66f8e7..ac8776bd95 100644
--- a/src/backend/postmaster/pmchild.c
+++ b/src/backend/postmaster/pmchild.c
@@ -209,9 +209,6 @@ AssignPostmasterChildSlot(BackendType btype)
 
 	ReservePostmasterChildSlot(pmchild->child_slot);
 
-	/* FIXME: find a more elegant way to pass this */
-	MyPMChildSlot = pmchild->child_slot;
-
 	elog(DEBUG2, "assigned pm child slot %d for %s", pmchild->child_slot, PostmasterChildName(btype));
 
 	return pmchild;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index a029e28786..798cd330f3 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -983,7 +983,7 @@ PostmasterMain(int argc, char *argv[])
 	SysLoggerPMChild = AssignPostmasterChildSlot(B_LOGGER);
 	if (!SysLoggerPMChild)
 		elog(ERROR, "no postmaster child slot available for syslogger");
-	SysLoggerPMChild->pid = SysLogger_Start();
+	SysLoggerPMChild->pid = SysLogger_Start(SysLoggerPMChild->child_slot);
 	if (SysLoggerPMChild->pid == 0)
 	{
 		FreePostmasterChildSlot(SysLoggerPMChild);
@@ -2413,7 +2413,7 @@ process_pm_child_exit(void)
 		if (SysLoggerPMChild && pid == SysLoggerPMChild->pid)
 		{
 			/* for safety's sake, launch new logger *first* */
-			SysLoggerPMChild->pid = SysLogger_Start();
+			SysLoggerPMChild->pid = SysLogger_Start(SysLoggerPMChild->child_slot);
 			if (SysLoggerPMChild->pid == 0)
 			{
 				FreePostmasterChildSlot(SysLoggerPMChild);
@@ -3054,7 +3054,7 @@ LaunchMissingBackgroundProcesses(void)
 			elog(LOG, "no postmaster child slot available for syslogger");
 		else
 		{
-			SysLoggerPMChild->pid = SysLogger_Start();
+			SysLoggerPMChild->pid = SysLogger_Start(SysLoggerPMChild->child_slot);
 			if (SysLoggerPMChild->pid == 0)
 			{
 				FreePostmasterChildSlot(SysLoggerPMChild);
@@ -3325,8 +3325,7 @@ BackendStartup(ClientSocket *client_sock)
 	/* Hasn't asked to be notified about any bgworkers yet */
 	bn->bgworker_notify = false;
 
-	MyPMChildSlot = bn->child_slot;
-	pid = postmaster_child_launch(bn->bkend_type,
+	pid = postmaster_child_launch(bn->bkend_type, bn->child_slot,
 								  (char *) &startup_data, sizeof(startup_data),
 								  client_sock);
 	if (pid < 0)
@@ -3650,8 +3649,7 @@ StartChildProcess(BackendType type)
 		return NULL;
 	}
 
-	MyPMChildSlot = pmchild->child_slot;
-	pid = postmaster_child_launch(type, NULL, 0, NULL);
+	pid = postmaster_child_launch(type, pmchild->child_slot, NULL, 0, NULL);
 	if (pid < 0)
 	{
 		/* in parent, fork failed */
@@ -3806,8 +3804,8 @@ do_start_bgworker(RegisteredBgWorker *rw)
 			(errmsg_internal("starting background worker process \"%s\"",
 							 rw->rw_worker.bgw_name)));
 
-	MyPMChildSlot = bn->child_slot;
-	worker_pid = postmaster_child_launch(B_BG_WORKER, (char *) &rw->rw_worker, sizeof(BackgroundWorker), NULL);
+	worker_pid = postmaster_child_launch(B_BG_WORKER, bn->child_slot,
+										 (char *) &rw->rw_worker, sizeof(BackgroundWorker), NULL);
 	if (worker_pid == -1)
 	{
 		/* in postmaster, fork failed ... */
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index 7951599fa8..d68853d429 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -590,7 +590,7 @@ SysLoggerMain(char *startup_data, size_t startup_data_len)
  * Postmaster subroutine to start a syslogger subprocess.
  */
 int
-SysLogger_Start(void)
+SysLogger_Start(int child_slot)
 {
 	pid_t		sysloggerPid;
 	char	   *filename;
@@ -699,9 +699,11 @@ SysLogger_Start(void)
 	startup_data.syslogFile = syslogger_fdget(syslogFile);
 	startup_data.csvlogFile = syslogger_fdget(csvlogFile);
 	startup_data.jsonlogFile = syslogger_fdget(jsonlogFile);
-	sysloggerPid = postmaster_child_launch(B_LOGGER, (char *) &startup_data, sizeof(startup_data), NULL);
+	sysloggerPid = postmaster_child_launch(B_LOGGER, child_slot,
+										   (char *) &startup_data, sizeof(startup_data), NULL);
 #else
-	sysloggerPid = postmaster_child_launch(B_LOGGER, NULL, 0, NULL);
+	sysloggerPid = postmaster_child_launch(B_LOGGER, child_slot,
+										   NULL, 0, NULL);
 #endif							/* EXEC_BACKEND */
 
 	if (sysloggerPid == -1)
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index deca2e8370..81a3520021 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -103,6 +103,7 @@ extern PGDLLIMPORT struct ClientSocket *MyClientSocket;
 
 /* prototypes for functions in launch_backend.c */
 extern pid_t postmaster_child_launch(BackendType child_type,
+									 int child_slot,
 									 char *startup_data,
 									 size_t startup_data_len,
 									 struct ClientSocket *client_sock);
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index b5fc239ba9..d72b978b0a 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -86,7 +86,7 @@ extern PGDLLIMPORT HANDLE syslogPipe[2];
 #endif
 
 
-extern int	SysLogger_Start(void);
+extern int	SysLogger_Start(int child_slot);
 
 extern void write_syslogger_file(const char *buffer, int count, int destination);
 
-- 
2.39.2



^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-08-18 08:00  Alexander Lakhin <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  1 sibling, 1 reply; 39+ messages in thread

From: Alexander Lakhin @ 2024-08-18 08:00 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; Thomas Munro <[email protected]>; +Cc: pgsql-hackers

Hello Heikki,

10.08.2024 00:13, Heikki Linnakangas wrote:
>
> Committed the patches up to and including this one, with tiny comment changes.

I've noticed that on the current HEAD server.log contains binary data
(an invalid process name) after a child crash. For example, while playing
with -ftapv, I've got:
SELECT to_date('2024 613566758 1', 'IYYY IW ID');
server closed the connection unexpectedly

grep -a 'was terminated' server.log
2024-08-18 07:07:06.482 UTC|||66c19d96.3482f6|LOG:  `�!x� (PID 3441407) was terminated by signal 6: Aborted

It looks like this was introduced by commit 28a520c0b (IIUC, namebuf in
CleanupBackend() may stay uninitialized in some code paths).

Best regards,
Alexander






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-08-19 06:49  Heikki Linnakangas <[email protected]>
  parent: Alexander Lakhin <[email protected]>
  0 siblings, 0 replies; 39+ messages in thread

From: Heikki Linnakangas @ 2024-08-19 06:49 UTC (permalink / raw)
  To: Alexander Lakhin <[email protected]>; +Cc: pgsql-hackers; Thomas Munro <[email protected]>

On 18/08/2024 11:00, Alexander Lakhin wrote:
> 10.08.2024 00:13, Heikki Linnakangas wrote:
>>
>> Committed the patches up to and including this one, with tiny comment changes.
> 
> I've noticed that on the current HEAD server.log contains binary data
> (an invalid process name) after a child crash. For example, while playing
> with -ftapv, I've got:
> SELECT to_date('2024 613566758 1', 'IYYY IW ID');
> server closed the connection unexpectedly
> 
> grep -a 'was terminated' server.log
> 2024-08-18 07:07:06.482 UTC|||66c19d96.3482f6|LOG:  `�!x� (PID 3441407) was terminated by signal 6: Aborted
> 
> It looks like this was introduced by commit 28a520c0b (IIUC, namebuf in
> CleanupBackend() may stay uninitialized in some code paths).

Fixed, thanks!

-- 
Heikki Linnakangas
Neon (https://neon.tech)







^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-09-04 14:35  Andres Freund <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  1 sibling, 2 replies; 39+ messages in thread

From: Andres Freund @ 2024-09-04 14:35 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers

Hi,

On 2024-08-12 12:55:00 +0300, Heikki Linnakangas wrote:
> While rebasing this today, I spotted another instance of that mistake
> mentioned in the XXX comment above. I called "CountChildren(B_BACKEND)"
> instead of "CountChildren(1 << B_BACKEND)". Some ideas on how to make that
> less error-prone:
> 
> 1. Add a separate typedef for the bitmasks, and macros/functions to work
> with it. Something like:
> 
> typedef struct {
> 	uint32		mask;
> } BackendTypeMask;
> 
> static const BackendTypeMask BTMASK_ALL = { 0xffffffff };
> static const BackendTypeMask BTMASK_NONE = { 0 };
> 
> static inline BackendTypeMask
> BTMASK_ADD(BackendTypeMask mask, BackendType t)
> {
> 	mask.mask |= 1 << t;
> 	return mask;
> }
> 
> static inline BackendTypeMask
> BTMASK_DEL(BackendTypeMask mask, BackendType t)
> {
> 	mask.mask &= ~(1 << t);
> 	return mask;
> }
> 
> Now the compiler will complain if you try to pass a BackendType for the
> mask. We could do this just for BackendType, or we could put this in
> src/include/lib/ with a more generic name, like "bitmask_u32".

I don't like the second suggestion - that just ends up creating a similar
problem in the future because flag values for one thing can be passed to
something else.



> +Running the tests
> +=================
> +
> +NOTE: You must have given the --enable-tap-tests argument to configure.
> +
> +Run
> +    make check
> +or
> +    make installcheck
> +You can use "make installcheck" if you previously did "make install".
> +In that case, the code in the installation tree is tested.  With
> +"make check", a temporary installation tree is built from the current
> +sources and then tested.
> +
> +Either way, this test initializes, starts, and stops a test Postgres
> +cluster.
> +
> +See src/test/perl/README for more info about running these tests.

Is it really useful to have such instructions all over the tree?


> From 93b9e9b6e072f63af9009e0d66ab6d0d62ea8c15 Mon Sep 17 00:00:00 2001
> From: Heikki Linnakangas <[email protected]>
> Date: Mon, 12 Aug 2024 10:55:11 +0300
> Subject: [PATCH v4 2/8] Add test for dead-end backends
> 
> The code path for launching a dead-end backend because we're out of
> slots was not covered by any tests, so add one. (Some tests did hit
> the case of launching a dead-end backend because the server is still
> starting up, though, so the gap in our test coverage wasn't as big as
> it sounds.)
> ---
>  src/test/perl/PostgreSQL/Test/Cluster.pm      | 39 +++++++++++++++++++
>  .../postmaster/t/001_connection_limits.pl     | 17 +++++++-
>  2 files changed, 55 insertions(+), 1 deletion(-)

Why does this need to use "raw" connections?  Can't you just create a bunch of
connections with BackgroundPsql?




> From 88287a2db95e584018f1c7fa9e992feb7d179ce8 Mon Sep 17 00:00:00 2001
> From: Heikki Linnakangas <[email protected]>
> Date: Mon, 12 Aug 2024 10:58:35 +0300
> Subject: [PATCH v4 3/8] Use an shmem_exit callback to remove backend from
>  PMChildFlags on exit
> 
> This seems nicer than having to duplicate the logic between
> InitProcess() and ProcKill() for which child processes have a
> PMChildFlags slot.
> 
> Move the MarkPostmasterChildActive() call earlier in InitProcess(),
> out of the section protected by the spinlock.

> ---
>  src/backend/storage/ipc/pmsignal.c | 10 ++++++--
>  src/backend/storage/lmgr/proc.c    | 38 ++++++++++--------------------
>  src/include/storage/pmsignal.h     |  1 -
>  3 files changed, 21 insertions(+), 28 deletions(-)
> 
> diff --git a/src/backend/storage/ipc/pmsignal.c b/src/backend/storage/ipc/pmsignal.c
> index 27844b46a2..cb99e77476 100644
> --- a/src/backend/storage/ipc/pmsignal.c
> +++ b/src/backend/storage/ipc/pmsignal.c
> @@ -24,6 +24,7 @@
>  #include "miscadmin.h"
>  #include "postmaster/postmaster.h"
>  #include "replication/walsender.h"
> +#include "storage/ipc.h"
>  #include "storage/pmsignal.h"
>  #include "storage/shmem.h"
>  #include "utils/memutils.h"
> @@ -121,6 +122,8 @@ postmaster_death_handler(SIGNAL_ARGS)
>  
>  #endif							/* USE_POSTMASTER_DEATH_SIGNAL */
>  
> +static void MarkPostmasterChildInactive(int code, Datum arg);
> +
>  /*
>   * PMSignalShmemSize
>   *		Compute space needed for pmsignal.c's shared memory
> @@ -328,6 +331,9 @@ MarkPostmasterChildActive(void)
>  	slot--;
>  	Assert(PMSignalState->PMChildFlags[slot] == PM_CHILD_ASSIGNED);
>  	PMSignalState->PMChildFlags[slot] = PM_CHILD_ACTIVE;
> +
> +	/* Arrange to clean up at exit. */
> +	on_shmem_exit(MarkPostmasterChildInactive, 0);
>  }
>  
>  /*
> @@ -352,8 +358,8 @@ MarkPostmasterChildWalSender(void)
>   * MarkPostmasterChildInactive - mark a postmaster child as done using
>   * shared memory.  This is called in the child process.
>   */
> -void
> -MarkPostmasterChildInactive(void)
> +static void
> +MarkPostmasterChildInactive(int code, Datum arg)
>  {
>  	int			slot = MyPMChildSlot;
>  
> diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
> index ac66da8638..9536469e89 100644
> --- a/src/backend/storage/lmgr/proc.c
> +++ b/src/backend/storage/lmgr/proc.c
> @@ -308,6 +308,19 @@ InitProcess(void)
>  	if (MyProc != NULL)
>  		elog(ERROR, "you already exist");
>  
> +	/*
> +	 * Before we start accessing the shared memory in a serious way, mark
> +	 * ourselves as an active postmaster child; this is so that the postmaster
> +	 * can detect it if we exit without cleaning up.  (XXX autovac launcher
> +	 * currently doesn't participate in this; it probably should.)
> +	 *
> +	 * Slot sync worker also does not participate in it, see comments atop
> +	 * 'struct bkend' in postmaster.c.
> +	 */
> +	if (IsUnderPostmaster && !AmAutoVacuumLauncherProcess() &&
> +		!AmLogicalSlotSyncWorkerProcess())
> +		MarkPostmasterChildActive();

I'd not necessarily expect a call to MarkPostmasterChildActive() to register
an shmem exit hook - but I guess it's unlikely to be moved around in a
problematic way. Perhaps something like RegisterPostmasterChild() or such
would be a bit clearer?


> From dc53f89edbeec99f8633def8aa5f47cd98e7a150 Mon Sep 17 00:00:00 2001
> From: Heikki Linnakangas <[email protected]>
> Date: Mon, 12 Aug 2024 10:59:04 +0300
> Subject: [PATCH v4 4/8] Introduce a separate BackendType for dead-end children
> 
> And replace postmaster.c's own "backend type" codes with BackendType

Hm - it seems a bit odd to open-code this when we actually have a "table
driven configuration" available?  Why isn't the type a field in
child_process_kind?

That'd not solve the bitmask confusion issue, but it does seem like a better
direction to me?


> From 9c832ce33667abc5aef128a17fa9c27daaad872a Mon Sep 17 00:00:00 2001
> From: Heikki Linnakangas <[email protected]>
> Date: Mon, 12 Aug 2024 10:59:27 +0300
> Subject: [PATCH v4 5/8] Kill dead-end children when there's nothing else left
> 
> Previously, the postmaster would never try to kill dead-end child
> processes, even if there were no other processes left. A dead-end
> backend will eventually exit, when authentication_timeout expires, but
> if a dead-end backend is the only thing that's preventing the server
> from shutting down, it seems better to kill it immediately. It's
> particularly important, if there was a bug in the early startup code
> that prevented a dead-end child from timing out and exiting normally.

I do wonder if we shouldn't instead get rid of dead end children. We now have
an event based loop in postmaster, it'd perform vastly better to juts handle
these connections in postmaster. And we'd get rid of these weird backend
types.   But I guess this is a worthwhile improvement on its own...


> Includes a test for that case where a dead-end backend previously kept
> the server from shutting down.

The test hardcodes timeouts, I think we've largely come to regret that when we
did. Should probably just be a multiplier based on
PostgreSQL::Test::Utils::timeout_default?



> +/*
> + * MaxLivePostmasterChildren
> + *
> + * This reports the number postmaster child processes that can be active.  It
> + * includes all children except for dead_end children.  This allows the array
> + * in shared memory (PMChildFlags) to have a fixed maximum size.
> + */
> +int
> +MaxLivePostmasterChildren(void)
> +{
> +	int			n = 0;
> +
> +	/* We know exactly how mamy worker and aux processes can be active */
> +	n += autovacuum_max_workers;
> +	n += max_worker_processes;
> +	n += NUM_AUXILIARY_PROCS;
> +
> +	/*
> +	 * We allow more connections here than we can have backends because some
> +	 * might still be authenticating; they might fail auth, or some existing
> +	 * backend might exit before the auth cycle is completed.  The exact
> +	 * MaxBackends limit is enforced when a new backend tries to join the
> +	 * shared-inval backend array.
> +	 */
> +	n += 2 * (MaxConnections + max_wal_senders);
> +
> +	return n;
> +}

I wonder if we could instead maintain at least some of this in
child_process_kinds? Manually listing different types of processes in
different places doesn't seem particularly sustainable.


> +/*
> + * Initialize at postmaster startup
> + */
> +void
> +InitPostmasterChildSlots(void)
> +{
> +	int			num_pmchild_slots;
> +	int			slotno;
> +	PMChild    *slots;
> +
> +	dlist_init(&freeBackendList);
> +	dlist_init(&freeAutoVacWorkerList);
> +	dlist_init(&freeBgWorkerList);
> +	dlist_init(&freeAuxList);
> +	dlist_init(&ActiveChildList);
> +
> +	num_pmchild_slots = MaxLivePostmasterChildren();
> +
> +	slots = palloc(num_pmchild_slots * sizeof(PMChild));
> +
> +	slotno = 0;
> +	for (int i = 0; i < 2 * (MaxConnections + max_wal_senders); i++)
> +	{
> +		init_slot(&slots[slotno], slotno, &freeBackendList);
> +		slotno++;
> +	}
> +	for (int i = 0; i < autovacuum_max_workers; i++)
> +	{
> +		init_slot(&slots[slotno], slotno, &freeAutoVacWorkerList);
> +		slotno++;
> +	}
> +	for (int i = 0; i < max_worker_processes; i++)
> +	{
> +		init_slot(&slots[slotno], slotno, &freeBgWorkerList);
> +		slotno++;
> +	}
> +	for (int i = 0; i < NUM_AUXILIARY_PROCS; i++)
> +	{
> +		init_slot(&slots[slotno], slotno, &freeAuxList);
> +		slotno++;
> +	}
> +	Assert(slotno == num_pmchild_slots);
> +}

Along the same vein - could we generalize this into one array of different
slot types and then loop over that to initialize / acquire the slots?


> +/* Return the appropriate free-list for the given backend type */
> +static dlist_head *
> +GetFreeList(BackendType btype)
> +{
> +	switch (btype)
> +	{
> +		case B_BACKEND:
> +		case B_BG_WORKER:
> +		case B_WAL_SENDER:
> +		case B_SLOTSYNC_WORKER:
> +			return &freeBackendList;

Maybe a daft question - but why are all of these in the same list? Sure,
they're essentially all full backends, but they're covered by different GUCs?


> +			/*
> +			 * Auxiliary processes.  There can be only one of each of these
> +			 * running at a time.
> +			 */
> +		case B_AUTOVAC_LAUNCHER:
> +		case B_ARCHIVER:
> +		case B_BG_WRITER:
> +		case B_CHECKPOINTER:
> +		case B_STARTUP:
> +		case B_WAL_RECEIVER:
> +		case B_WAL_SUMMARIZER:
> +		case B_WAL_WRITER:
> +			return &freeAuxList;
> +
> +			/*
> +			 * Logger is not connected to shared memory, and does not have a
> +			 * PGPROC entry, but we still allocate a child slot for it.
> +			 */

Tangential: Why do we need a freelist for these and why do we choose a random
pgproc for these instead of assigning one statically?

Background: I'd like to not provide AIO workers with "bounce buffers" (for IO
of buffers that can't be done in-place, like writes when checksums are
enabled). The varying proc numbers make that harder than it'd have to be...


> +PMChild *
> +AssignPostmasterChildSlot(BackendType btype)
> +{
> +	dlist_head *freelist;
> +	PMChild    *pmchild;
> +
> +	freelist = GetFreeList(btype);
> +
> +	if (dlist_is_empty(freelist))
> +		return NULL;
> +
> +	pmchild = dlist_container(PMChild, elem, dlist_pop_head_node(freelist));
> +	pmchild->pid = 0;
> +	pmchild->bkend_type = btype;
> +	pmchild->rw = NULL;
> +	pmchild->bgworker_notify = true;
> +
> +	/*
> +	 * pmchild->child_slot for each entry was initialized when the array of
> +	 * slots was allocated.
> +	 */
> +
> +	dlist_push_head(&ActiveChildList, &pmchild->elem);
> +
> +	ReservePostmasterChildSlot(pmchild->child_slot);
> +
> +	/* FIXME: find a more elegant way to pass this */
> +	MyPMChildSlot = pmchild->child_slot;

What if we assigned one offset for each process and assigned its ID here and
also used that for its ProcNumber - that way we wouldn't need to manage
freelists in two places.


> +PMChild *
> +FindPostmasterChildByPid(int pid)
> +{
> +	dlist_iter	iter;
> +
> +	dlist_foreach(iter, &ActiveChildList)
> +	{
> +		PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
> +
> +		if (bp->pid == pid)
> +			return bp;
> +	}
> +	return NULL;
> +}

It's not new, but it's quite sad that postmaster's process exit handling is
effectively O(Backends^2)...



> @@ -1019,7 +980,15 @@ PostmasterMain(int argc, char *argv[])
>  	/*
>  	 * If enabled, start up syslogger collection subprocess
>  	 */
> -	SysLoggerPID = SysLogger_Start();
> +	SysLoggerPMChild = AssignPostmasterChildSlot(B_LOGGER);
> +	if (!SysLoggerPMChild)
> +		elog(ERROR, "no postmaster child slot available for syslogger");
> +	SysLoggerPMChild->pid = SysLogger_Start();
> +	if (SysLoggerPMChild->pid == 0)
> +	{
> +		FreePostmasterChildSlot(SysLoggerPMChild);
> +		SysLoggerPMChild = NULL;
> +	}

Maybe it's a bit obsessive, but this seems long enough to make it worth not
doing inline in the already long PostmasterMain().


>  	/*
>  	 * We're ready to rock and roll...
>  	 */
> -	StartupPID = StartChildProcess(B_STARTUP);
> -	Assert(StartupPID != 0);
> +	StartupPMChild = StartChildProcess(B_STARTUP);
> +	Assert(StartupPMChild != NULL);

This (not new) assertion is ... odd.


> @@ -1779,21 +1748,6 @@ canAcceptConnections(int backend_type)
>  	if (!connsAllowed && backend_type == B_BACKEND)
>  		return CAC_SHUTDOWN;	/* shutdown is pending */
>  
> -	/*
> -	 * Don't start too many children.
> -	 *
> -	 * We allow more connections here than we can have backends because some
> -	 * might still be authenticating; they might fail auth, or some existing
> -	 * backend might exit before the auth cycle is completed.  The exact
> -	 * MaxBackends limit is enforced when a new backend tries to join the
> -	 * shared-inval backend array.
> -	 *
> -	 * The limit here must match the sizes of the per-child-process arrays;
> -	 * see comments for MaxLivePostmasterChildren().
> -	 */
> -	if (CountChildren(BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND)) >= MaxLivePostmasterChildren())
> -		result = CAC_TOOMANY;
> -
>  	return result;
>  }

It's nice to get rid of this source of O(N^2).


> @@ -1961,26 +1915,6 @@ process_pm_reload_request(void)
>  				(errmsg("received SIGHUP, reloading configuration files")));
>  		ProcessConfigFile(PGC_SIGHUP);
>  		SignalSomeChildren(SIGHUP, BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND));
> -		if (StartupPID != 0)
> -			signal_child(StartupPID, SIGHUP);
> -		if (BgWriterPID != 0)
> -			signal_child(BgWriterPID, SIGHUP);
> -		if (CheckpointerPID != 0)
> -			signal_child(CheckpointerPID, SIGHUP);
> -		if (WalWriterPID != 0)
> -			signal_child(WalWriterPID, SIGHUP);
> -		if (WalReceiverPID != 0)
> -			signal_child(WalReceiverPID, SIGHUP);
> -		if (WalSummarizerPID != 0)
> -			signal_child(WalSummarizerPID, SIGHUP);
> -		if (AutoVacPID != 0)
> -			signal_child(AutoVacPID, SIGHUP);
> -		if (PgArchPID != 0)
> -			signal_child(PgArchPID, SIGHUP);
> -		if (SysLoggerPID != 0)
> -			signal_child(SysLoggerPID, SIGHUP);
> -		if (SlotSyncWorkerPID != 0)
> -			signal_child(SlotSyncWorkerPID, SIGHUP);
>  
>  		/* Reload authentication config files too */
>  		if (!load_hba())

For a moment I wondered why this change was part of this commit - but I guess
we didn't have any of these in an array/list before this change...



> @@ -2469,11 +2410,15 @@ process_pm_child_exit(void)
>  		}
>  
>  		/* Was it the system logger?  If so, try to start a new one */
> -		if (pid == SysLoggerPID)
> +		if (SysLoggerPMChild && pid == SysLoggerPMChild->pid)
>  		{
> -			SysLoggerPID = 0;
>  			/* for safety's sake, launch new logger *first* */
> -			SysLoggerPID = SysLogger_Start();
> +			SysLoggerPMChild->pid = SysLogger_Start();
> +			if (SysLoggerPMChild->pid == 0)
> +			{
> +				FreePostmasterChildSlot(SysLoggerPMChild);
> +				SysLoggerPMChild = NULL;
> +			}
>  			if (!EXIT_STATUS_0(exitstatus))
>  				LogChildExit(LOG, _("system logger process"),

Seems a bit weird to have one place with a different memory lifetime handling
than other places. Why don't we just do this the same way as in other places
but continue to defer the logging until after we tried to start the new
logger?

Might be worth having a test ensuring that loggers restart OK.


>  	/* Construct a process name for log message */
> +
> +	/*
> +	 * FIXME: use GetBackendTypeDesc here? How does the localization of that
> +	 * work?
> +	 */
>  	if (bp->bkend_type == B_DEAD_END_BACKEND)
>  	{
>  		procname = _("dead end backend");

Might be worth having a version of GetBackendTypeDesc() that returns a
translated string?


> @@ -2697,9 +2643,16 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
>  	{
>  		dlist_iter	iter;
>  
> -		dlist_foreach(iter, &BackendList)
> +		dlist_foreach(iter, &ActiveChildList)
>  		{
> -			Backend    *bp = dlist_container(Backend, elem, iter.cur);
> +			PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
> +
> +			/* We do NOT restart the syslogger */
> +			if (bp == SysLoggerPMChild)
> +				continue;

That comment seems a bit misleading - we do restart syslogger, we just don't
do it here, no?  I realize it's an old comment, but it still seems like it's
worth fixing given that you touch all the code here...


> @@ -2708,48 +2661,8 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
>  			 * We could exclude dead_end children here, but at least when
>  			 * sending SIGABRT it seems better to include them.
>  			 */
> -			sigquit_child(bp->pid);
> +			sigquit_child(bp);
>  		}
> -
> -		if (StartupPID != 0)
> -		{
> -			sigquit_child(StartupPID);
> -			StartupStatus = STARTUP_SIGNALED;
> -		}
> -
> -		/* Take care of the bgwriter too */
> -		if (BgWriterPID != 0)
> -			sigquit_child(BgWriterPID);
> -
> -		/* Take care of the checkpointer too */
> -		if (CheckpointerPID != 0)
> -			sigquit_child(CheckpointerPID);
> -
> -		/* Take care of the walwriter too */
> -		if (WalWriterPID != 0)
> -			sigquit_child(WalWriterPID);
> -
> -		/* Take care of the walreceiver too */
> -		if (WalReceiverPID != 0)
> -			sigquit_child(WalReceiverPID);
> -
> -		/* Take care of the walsummarizer too */
> -		if (WalSummarizerPID != 0)
> -			sigquit_child(WalSummarizerPID);
> -
> -		/* Take care of the autovacuum launcher too */
> -		if (AutoVacPID != 0)
> -			sigquit_child(AutoVacPID);
> -
> -		/* Take care of the archiver too */
> -		if (PgArchPID != 0)
> -			sigquit_child(PgArchPID);
> -
> -		/* Take care of the slot sync worker too */
> -		if (SlotSyncWorkerPID != 0)
> -			sigquit_child(SlotSyncWorkerPID);
> -
> -		/* We do NOT restart the syslogger */
>  	}

Yay.




> @@ -2871,29 +2786,27 @@ PostmasterStateMachine(void)
> <snip>
> -		if (WalSummarizerPID != 0)
> -			signal_child(WalSummarizerPID, SIGTERM);
> -		if (SlotSyncWorkerPID != 0)
> -			signal_child(SlotSyncWorkerPID, SIGTERM);
> +		targetMask |= (1 << B_STARTUP);
> +		targetMask |= (1 << B_WAL_RECEIVER);
> +
> +		targetMask |= (1 << B_WAL_SUMMARIZER);
> +		targetMask |= (1 << B_SLOTSYNC_WORKER);
>  		/* checkpointer, archiver, stats, and syslogger may continue for now */
>  
> +		SignalSomeChildren(SIGTERM, targetMask);
> +
>  		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
>  		pmState = PM_WAIT_BACKENDS;
> <snip>

It's likely the right thing to not do as one patch, but IMO this really wants
to be a state table. Perhaps as part of child_process_kinds, perhaps separate
from that.


> @@ -3130,8 +3047,21 @@ static void
>  LaunchMissingBackgroundProcesses(void)
>  {
>  	/* Syslogger is active in all states */
> -	if (SysLoggerPID == 0 && Logging_collector)
> -		SysLoggerPID = SysLogger_Start();
> +	if (SysLoggerPMChild == NULL && Logging_collector)
> +	{
> +		SysLoggerPMChild = AssignPostmasterChildSlot(B_LOGGER);
> +		if (!SysLoggerPMChild)
> +			elog(LOG, "no postmaster child slot available for syslogger");

How could this elog() be reached? Seems something seriously would have gone
wrong to get here - in which case a LOG that might not even be visible (due to
logger not working) doesn't seem like the right response.

> @@ -3334,29 +3270,12 @@ SignalSomeChildren(int signal, uint32 targetMask)
>  static void
>  TerminateChildren(int signal)
>  {

The comment for TerminateChildren() says "except syslogger and dead_end
backends." - aren't you including the latter here?




> @@ -311,14 +311,9 @@ InitProcess(void)
>  	/*
>  	 * Before we start accessing the shared memory in a serious way, mark
>  	 * ourselves as an active postmaster child; this is so that the postmaster
> -	 * can detect it if we exit without cleaning up.  (XXX autovac launcher
> -	 * currently doesn't participate in this; it probably should.)
> -	 *
> -	 * Slot sync worker also does not participate in it, see comments atop
> -	 * 'struct bkend' in postmaster.c.
> +	 * can detect it if we exit without cleaning up.
>  	 */
> -	if (IsUnderPostmaster && !AmAutoVacuumLauncherProcess() &&
> -		!AmLogicalSlotSyncWorkerProcess())
> +	if (IsUnderPostmaster)
>  		MarkPostmasterChildActive();
>  
>  	/* Decide which list should supply our PGPROC. */
> @@ -536,6 +531,9 @@ InitAuxiliaryProcess(void)
>  	if (MyProc != NULL)
>  		elog(ERROR, "you already exist");
>  
> +	if (IsUnderPostmaster)
> +		MarkPostmasterChildActive();
> +
>  	/*
>  	 * We use the ProcStructLock to protect assignment and releasing of
>  	 * AuxiliaryProcs entries.

Probably worth, at some point soon, to have an InitProcessCommon() or such.


Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-09-06 09:52  Heikki Linnakangas <[email protected]>
  parent: Andres Freund <[email protected]>
  1 sibling, 1 reply; 39+ messages in thread

From: Heikki Linnakangas @ 2024-09-06 09:52 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers

On 04/09/2024 17:35, Andres Freund wrote:
> On 2024-08-12 12:55:00 +0300, Heikki Linnakangas wrote:
>> +Running the tests
>> +=================
>> +
>> +NOTE: You must have given the --enable-tap-tests argument to configure.
>> +
>> +Run
>> +    make check
>> +or
>> +    make installcheck
>> +You can use "make installcheck" if you previously did "make install".
>> +In that case, the code in the installation tree is tested.  With
>> +"make check", a temporary installation tree is built from the current
>> +sources and then tested.
>> +
>> +Either way, this test initializes, starts, and stops a test Postgres
>> +cluster.
>> +
>> +See src/test/perl/README for more info about running these tests.
> 
> Is it really useful to have such instructions all over the tree?

That's debatable but I didn't want to go down that rabbit hole with this 
patch.

It's repetitive for sure. But there are small variations in which 
PG_TEST_EXTRA options you need, whether "make installcheck" runs against 
a running server or still creates a temporary cluster, etc.

I tried to deduplicate those instructions by moving the above 
boilerplate to src/test/README, and only noting the variations in the 
subdirectory READMEs. I didn't like the result. It's very helpful to 
have full copy-pasteable commands with all the right "PG_TEST_EXTRA" 
options for each test.

These instructions also don't mention how to run the tests with Meson. 
The first time I wanted to run individual tests with Meson, it took me a 
while to figure it out.

I'll think a little more about how to improve these READMEs, but let's 
take that to a separate thread.

>>  From 93b9e9b6e072f63af9009e0d66ab6d0d62ea8c15 Mon Sep 17 00:00:00 2001
>> From: Heikki Linnakangas <[email protected]>
>> Date: Mon, 12 Aug 2024 10:55:11 +0300
>> Subject: [PATCH v4 2/8] Add test for dead-end backends
>>
>> The code path for launching a dead-end backend because we're out of
>> slots was not covered by any tests, so add one. (Some tests did hit
>> the case of launching a dead-end backend because the server is still
>> starting up, though, so the gap in our test coverage wasn't as big as
>> it sounds.)
>> ---
>>   src/test/perl/PostgreSQL/Test/Cluster.pm      | 39 +++++++++++++++++++
>>   .../postmaster/t/001_connection_limits.pl     | 17 +++++++-
>>   2 files changed, 55 insertions(+), 1 deletion(-)
> 
> Why does this need to use "raw" connections?  Can't you just create a bunch of
> connections with BackgroundPsql?

No, these need to be connections that haven't sent the startup packet 
the yet.

With Andrew's PqFFI work [1], we could do better. The latest version on 
that thread doesn't expose the async functions like PQconnectStart() 
PQconnectPoll() though, but they can be added.

[1] 
https://www.postgresql.org/message-id/97d1d1b9-d147-f69d-1991-d8794efed41c%40dunslane.net


Unless you have comments on these first two patches which just add 
tests, I'll commit them shortly. Still processing the rest of your 
comments...

-- 
Heikki Linnakangas
Neon (https://neon.tech)







^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-09-06 13:13  Heikki Linnakangas <[email protected]>
  parent: Andres Freund <[email protected]>
  1 sibling, 2 replies; 39+ messages in thread

From: Heikki Linnakangas @ 2024-09-06 13:13 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers

On 04/09/2024 17:35, Andres Freund wrote:
> On 2024-08-12 12:55:00 +0300, Heikki Linnakangas wrote:
>>  From dc53f89edbeec99f8633def8aa5f47cd98e7a150 Mon Sep 17 00:00:00 2001
>> From: Heikki Linnakangas <[email protected]>
>> Date: Mon, 12 Aug 2024 10:59:04 +0300
>> Subject: [PATCH v4 4/8] Introduce a separate BackendType for dead-end children
>>
>> And replace postmaster.c's own "backend type" codes with BackendType
> 
> Hm - it seems a bit odd to open-code this when we actually have a "table
> driven configuration" available?  Why isn't the type a field in
> child_process_kind?

Sorry, I didn't understand this. What exactly would you add to 
child_process_kind? Where would you use it?

>> +/*
>> + * MaxLivePostmasterChildren
>> + *
>> + * This reports the number postmaster child processes that can be active.  It
>> + * includes all children except for dead_end children.  This allows the array
>> + * in shared memory (PMChildFlags) to have a fixed maximum size.
>> + */
>> +int
>> +MaxLivePostmasterChildren(void)
>> +{
>> +	int			n = 0;
>> +
>> +	/* We know exactly how mamy worker and aux processes can be active */
>> +	n += autovacuum_max_workers;
>> +	n += max_worker_processes;
>> +	n += NUM_AUXILIARY_PROCS;
>> +
>> +	/*
>> +	 * We allow more connections here than we can have backends because some
>> +	 * might still be authenticating; they might fail auth, or some existing
>> +	 * backend might exit before the auth cycle is completed.  The exact
>> +	 * MaxBackends limit is enforced when a new backend tries to join the
>> +	 * shared-inval backend array.
>> +	 */
>> +	n += 2 * (MaxConnections + max_wal_senders);
>> +
>> +	return n;
>> +}
> 
> I wonder if we could instead maintain at least some of this in
> child_process_kinds? Manually listing different types of processes in
> different places doesn't seem particularly sustainable.

Hmm, you mean adding "max this kind of children" field to 
child_process_kinds? Perhaps.

> 
>> +/*
>> + * Initialize at postmaster startup
>> + */
>> +void
>> +InitPostmasterChildSlots(void)
>> +{
>> +	int			num_pmchild_slots;
>> +	int			slotno;
>> +	PMChild    *slots;
>> +
>> +	dlist_init(&freeBackendList);
>> +	dlist_init(&freeAutoVacWorkerList);
>> +	dlist_init(&freeBgWorkerList);
>> +	dlist_init(&freeAuxList);
>> +	dlist_init(&ActiveChildList);
>> +
>> +	num_pmchild_slots = MaxLivePostmasterChildren();
>> +
>> +	slots = palloc(num_pmchild_slots * sizeof(PMChild));
>> +
>> +	slotno = 0;
>> +	for (int i = 0; i < 2 * (MaxConnections + max_wal_senders); i++)
>> +	{
>> +		init_slot(&slots[slotno], slotno, &freeBackendList);
>> +		slotno++;
>> +	}
>> +	for (int i = 0; i < autovacuum_max_workers; i++)
>> +	{
>> +		init_slot(&slots[slotno], slotno, &freeAutoVacWorkerList);
>> +		slotno++;
>> +	}
>> +	for (int i = 0; i < max_worker_processes; i++)
>> +	{
>> +		init_slot(&slots[slotno], slotno, &freeBgWorkerList);
>> +		slotno++;
>> +	}
>> +	for (int i = 0; i < NUM_AUXILIARY_PROCS; i++)
>> +	{
>> +		init_slot(&slots[slotno], slotno, &freeAuxList);
>> +		slotno++;
>> +	}
>> +	Assert(slotno == num_pmchild_slots);
>> +}
> 
> Along the same vein - could we generalize this into one array of different
> slot types and then loop over that to initialize / acquire the slots?

Makes sense.

>> +/* Return the appropriate free-list for the given backend type */
>> +static dlist_head *
>> +GetFreeList(BackendType btype)
>> +{
>> +	switch (btype)
>> +	{
>> +		case B_BACKEND:
>> +		case B_BG_WORKER:
>> +		case B_WAL_SENDER:
>> +		case B_SLOTSYNC_WORKER:
>> +			return &freeBackendList;
> 
> Maybe a daft question - but why are all of these in the same list? Sure,
> they're essentially all full backends, but they're covered by different GUCs?

No reason. No particular reason they should *not* share the same list 
either though.

> 
>> +			/*
>> +			 * Auxiliary processes.  There can be only one of each of these
>> +			 * running at a time.
>> +			 */
>> +		case B_AUTOVAC_LAUNCHER:
>> +		case B_ARCHIVER:
>> +		case B_BG_WRITER:
>> +		case B_CHECKPOINTER:
>> +		case B_STARTUP:
>> +		case B_WAL_RECEIVER:
>> +		case B_WAL_SUMMARIZER:
>> +		case B_WAL_WRITER:
>> +			return &freeAuxList;
>> +
>> +			/*
>> +			 * Logger is not connected to shared memory, and does not have a
>> +			 * PGPROC entry, but we still allocate a child slot for it.
>> +			 */
> 
> Tangential: Why do we need a freelist for these and why do we choose a random
> pgproc for these instead of assigning one statically?
> 
> Background: I'd like to not provide AIO workers with "bounce buffers" (for IO
> of buffers that can't be done in-place, like writes when checksums are
> enabled). The varying proc numbers make that harder than it'd have to be...

Yeah, we can make these fixed.Currently, the # of slots reserved for aux 
processes is sized by NUM_AUXILIARY_PROCS, which is one smaller than the 
number of different aux proces kinds:

> /*
>  * We set aside some extra PGPROC structures for auxiliary processes,
>  * ie things that aren't full-fledged backends but need shmem access.
>  *
>  * Background writer, checkpointer, WAL writer, WAL summarizer, and archiver
>  * run during normal operation.  Startup process and WAL receiver also consume
>  * 2 slots, but WAL writer is launched only after startup has exited, so we
>  * only need 6 slots.
>  */
> #define NUM_AUXILIARY_PROCS		6

For PMChildSlot numbers, we could certainly just allocate one more slot.

It would probably make sense for PGPROCs too, even though PGPROC is a 
much larger struct.

>> +PMChild *
>> +AssignPostmasterChildSlot(BackendType btype)
>> +{
>> +	dlist_head *freelist;
>> +	PMChild    *pmchild;
>> +
>> +	freelist = GetFreeList(btype);
>> +
>> +	if (dlist_is_empty(freelist))
>> +		return NULL;
>> +
>> +	pmchild = dlist_container(PMChild, elem, dlist_pop_head_node(freelist));
>> +	pmchild->pid = 0;
>> +	pmchild->bkend_type = btype;
>> +	pmchild->rw = NULL;
>> +	pmchild->bgworker_notify = true;
>> +
>> +	/*
>> +	 * pmchild->child_slot for each entry was initialized when the array of
>> +	 * slots was allocated.
>> +	 */
>> +
>> +	dlist_push_head(&ActiveChildList, &pmchild->elem);
>> +
>> +	ReservePostmasterChildSlot(pmchild->child_slot);
>> +
>> +	/* FIXME: find a more elegant way to pass this */
>> +	MyPMChildSlot = pmchild->child_slot;
> 
> What if we assigned one offset for each process and assigned its ID here and
> also used that for its ProcNumber - that way we wouldn't need to manage
> freelists in two places.

It's currently possible to have up to 2 * max_connections backends in 
the authentication phase. We would have to change that behaviour, or 
make the PGPROC array 2x larger.

It might well be worth it, I don't know how sensible the current 
behaviour is. But I'd like to punt that to later patch, to keep the 
scope of this patch set reasonable. It's pretty straightforward to do 
later on top of this if we want to.

>> +PMChild *
>> +FindPostmasterChildByPid(int pid)
>> +{
>> +	dlist_iter	iter;
>> +
>> +	dlist_foreach(iter, &ActiveChildList)
>> +	{
>> +		PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
>> +
>> +		if (bp->pid == pid)
>> +			return bp;
>> +	}
>> +	return NULL;
>> +}
> 
> It's not new, but it's quite sad that postmaster's process exit handling is
> effectively O(Backends^2)...

It would be straightforward to turn ActiveChildList into a hash table. 
But I'd like to leave that to a followup patch too.

>>   	/*
>>   	 * We're ready to rock and roll...
>>   	 */
>> -	StartupPID = StartChildProcess(B_STARTUP);
>> -	Assert(StartupPID != 0);
>> +	StartupPMChild = StartChildProcess(B_STARTUP);
>> +	Assert(StartupPMChild != NULL);
> 
> This (not new) assertion is ... odd.

Yeah, it's an assertion because StartChildProcess has this:

> 		/*
> 		 * fork failure is fatal during startup, but there's no need to choke
> 		 * immediately if starting other child types fails.
> 		 */
> 		if (type == B_STARTUP)
> 			ExitPostmaster(1);


>> @@ -1961,26 +1915,6 @@ process_pm_reload_request(void)
>>   				(errmsg("received SIGHUP, reloading configuration files")));
>>   		ProcessConfigFile(PGC_SIGHUP);
>>   		SignalSomeChildren(SIGHUP, BACKEND_TYPE_ALL & ~(1 << B_DEAD_END_BACKEND));
>> -		if (StartupPID != 0)
>> -			signal_child(StartupPID, SIGHUP);
>> -		if (BgWriterPID != 0)
>> -			signal_child(BgWriterPID, SIGHUP);
>> -		if (CheckpointerPID != 0)
>> -			signal_child(CheckpointerPID, SIGHUP);
>> -		if (WalWriterPID != 0)
>> -			signal_child(WalWriterPID, SIGHUP);
>> -		if (WalReceiverPID != 0)
>> -			signal_child(WalReceiverPID, SIGHUP);
>> -		if (WalSummarizerPID != 0)
>> -			signal_child(WalSummarizerPID, SIGHUP);
>> -		if (AutoVacPID != 0)
>> -			signal_child(AutoVacPID, SIGHUP);
>> -		if (PgArchPID != 0)
>> -			signal_child(PgArchPID, SIGHUP);
>> -		if (SysLoggerPID != 0)
>> -			signal_child(SysLoggerPID, SIGHUP);
>> -		if (SlotSyncWorkerPID != 0)
>> -			signal_child(SlotSyncWorkerPID, SIGHUP);
>>   
>>   		/* Reload authentication config files too */
>>   		if (!load_hba())
> 
> For a moment I wondered why this change was part of this commit - but I guess
> we didn't have any of these in an array/list before this change...

Correct.

>> @@ -2469,11 +2410,15 @@ process_pm_child_exit(void)
>>   		}
>>   
>>   		/* Was it the system logger?  If so, try to start a new one */
>> -		if (pid == SysLoggerPID)
>> +		if (SysLoggerPMChild && pid == SysLoggerPMChild->pid)
>>   		{
>> -			SysLoggerPID = 0;
>>   			/* for safety's sake, launch new logger *first* */
>> -			SysLoggerPID = SysLogger_Start();
>> +			SysLoggerPMChild->pid = SysLogger_Start();
>> +			if (SysLoggerPMChild->pid == 0)
>> +			{
>> +				FreePostmasterChildSlot(SysLoggerPMChild);
>> +				SysLoggerPMChild = NULL;
>> +			}
>>   			if (!EXIT_STATUS_0(exitstatus))
>>   				LogChildExit(LOG, _("system logger process"),
> 
> Seems a bit weird to have one place with a different memory lifetime handling
> than other places. Why don't we just do this the same way as in other places
> but continue to defer the logging until after we tried to start the new
> logger?

Hmm, you mean let LaunchMissingBackgroundProcesses() handle the restart?

I'm a little scared of changing the existing logic. We don't have a 
mechanism for deferring logging, so we would have to invent that, or the 
logs would just accumulate in the pipe until syslogger starts up. 
There's some code between here and LaunchMissingBackgroundProcesses(), 
so might postmaster get blocked between writing to the syslogger pipe, 
before having restarted it?

If forking the syslogger process fails, that can happen anyway, though.

> Might be worth having a test ensuring that loggers restart OK.

Yeah..

>>   	/* Construct a process name for log message */
>> +
>> +	/*
>> +	 * FIXME: use GetBackendTypeDesc here? How does the localization of that
>> +	 * work?
>> +	 */
>>   	if (bp->bkend_type == B_DEAD_END_BACKEND)
>>   	{
>>   		procname = _("dead end backend");
> 
> Might be worth having a version of GetBackendTypeDesc() that returns a
> translated string?

Constructing the string for background workers is a little more complicated:

     snprintf(namebuf, MAXPGPATH, _("background worker \"%s\""),
              bp->rw->rw_worker.bgw_type);

We could still do that for background workers and use the transalated 
variant of GetBackendTypeDesc() for everything else though.

>> @@ -2697,9 +2643,16 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
>>   	{
>>   		dlist_iter	iter;
>>   
>> -		dlist_foreach(iter, &BackendList)
>> +		dlist_foreach(iter, &ActiveChildList)
>>   		{
>> -			Backend    *bp = dlist_container(Backend, elem, iter.cur);
>> +			PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
>> +
>> +			/* We do NOT restart the syslogger */
>> +			if (bp == SysLoggerPMChild)
>> +				continue;
> 
> That comment seems a bit misleading - we do restart syslogger, we just don't
> do it here, no?  I realize it's an old comment, but it still seems like it's
> worth fixing given that you touch all the code here...

No, we really do not restart the syslogger. This code runs when 
*another* process has crashed unexpectedly. We kill all other processes, 
reinitialize shared memory and restart, but the old syslogger keeps 
running through all that.

I'll add a note on that to InitPostmasterChildSlots(), as it's a bit 
surprising.

>> @@ -2871,29 +2786,27 @@ PostmasterStateMachine(void)
>> <snip>
>> -		if (WalSummarizerPID != 0)
>> -			signal_child(WalSummarizerPID, SIGTERM);
>> -		if (SlotSyncWorkerPID != 0)
>> -			signal_child(SlotSyncWorkerPID, SIGTERM);
>> +		targetMask |= (1 << B_STARTUP);
>> +		targetMask |= (1 << B_WAL_RECEIVER);
>> +
>> +		targetMask |= (1 << B_WAL_SUMMARIZER);
>> +		targetMask |= (1 << B_SLOTSYNC_WORKER);
>>   		/* checkpointer, archiver, stats, and syslogger may continue for now */
>>   
>> +		SignalSomeChildren(SIGTERM, targetMask);
>> +
>>   		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
>>   		pmState = PM_WAIT_BACKENDS;
>> <snip>
> 
> It's likely the right thing to not do as one patch, but IMO this really wants
> to be a state table. Perhaps as part of child_process_kinds, perhaps separate
> from that.

Yeah. I've tried to refactor this into a table before, but didn't come 
up with anything that I was happy with. I also feel there must be a 
better way to organize this, but not sure what exactly. I hope that will 
become more apparent after these other changes.

>> @@ -3130,8 +3047,21 @@ static void
>>   LaunchMissingBackgroundProcesses(void)
>>   {
>>   	/* Syslogger is active in all states */
>> -	if (SysLoggerPID == 0 && Logging_collector)
>> -		SysLoggerPID = SysLogger_Start();
>> +	if (SysLoggerPMChild == NULL && Logging_collector)
>> +	{
>> +		SysLoggerPMChild = AssignPostmasterChildSlot(B_LOGGER);
>> +		if (!SysLoggerPMChild)
>> +			elog(LOG, "no postmaster child slot available for syslogger");
> 
> How could this elog() be reached? Seems something seriously would have gone
> wrong to get here - in which case a LOG that might not even be visible (due to
> logger not working) doesn't seem like the right response.

I'll turn it into an assertion or PANIC.

>> @@ -3334,29 +3270,12 @@ SignalSomeChildren(int signal, uint32 targetMask)
>>   static void
>>   TerminateChildren(int signal)
>>   {
> 
> The comment for TerminateChildren() says "except syslogger and dead_end
> backends." - aren't you including the latter here?

The comment is adjusted in 
v4-0004-Introduce-a-separate-BackendType-for-dead-end-chi.patch. Before 
that, SignalChildren() does ignore dead-end children.

Thanks for the review!

-- 
Heikki Linnakangas
Neon (https://neon.tech)







^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-09-06 13:32  Robert Haas <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  1 sibling, 0 replies; 39+ messages in thread

From: Robert Haas @ 2024-09-06 13:32 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Andres Freund <[email protected]>; Thomas Munro <[email protected]>; pgsql-hackers

On Fri, Sep 6, 2024 at 9:13 AM Heikki Linnakangas <[email protected]> wrote:
> It's currently possible to have up to 2 * max_connections backends in
> the authentication phase. We would have to change that behaviour, or
> make the PGPROC array 2x larger.

I know I already said this elsewhere, but in case it got lost in the
shuffle, +1 for changing this, unless somebody can make a compelling
argument why 2 * max_connections isn't WAY too many.

-- 
Robert Haas
EDB: http://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-09-10 16:59  Andres Freund <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  1 sibling, 1 reply; 39+ messages in thread

From: Andres Freund @ 2024-09-10 16:59 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers

Hi,

On 2024-09-06 16:13:43 +0300, Heikki Linnakangas wrote:
> On 04/09/2024 17:35, Andres Freund wrote:
> > On 2024-08-12 12:55:00 +0300, Heikki Linnakangas wrote:
> > >  From dc53f89edbeec99f8633def8aa5f47cd98e7a150 Mon Sep 17 00:00:00 2001
> > > From: Heikki Linnakangas <[email protected]>
> > > Date: Mon, 12 Aug 2024 10:59:04 +0300
> > > Subject: [PATCH v4 4/8] Introduce a separate BackendType for dead-end children
> > >
> > > And replace postmaster.c's own "backend type" codes with BackendType
> >
> > Hm - it seems a bit odd to open-code this when we actually have a "table
> > driven configuration" available?  Why isn't the type a field in
> > child_process_kind?
>
> Sorry, I didn't understand this. What exactly would you add to
> child_process_kind? Where would you use it?

I'm not entirely sure what I was thinking of. It might be partially triggering
a prior complaint I had about manually assigning things to MyBackendType,
despite actually having all the information already.

One thing that I just noticed is that this patch orphans comment references to
BACKEND_TYPE_AUTOVAC and BACKEND_TYPE_BGWORKER.

Seems a tad odd to have BACKEND_TYPE_ALL after removing everything else from
the BACKEND_TYPE_* "namespace".


To deal with the issue around bitmasks you had mentioned, I think we should at
least have a static inline function to convert B_* values to the bitmask
index.


> > > +/*
> > > + * MaxLivePostmasterChildren
> > > + *
> > > + * This reports the number postmaster child processes that can be active.  It
> > > + * includes all children except for dead_end children.  This allows the array
> > > + * in shared memory (PMChildFlags) to have a fixed maximum size.
> > > + */
> > > +int
> > > +MaxLivePostmasterChildren(void)
> > > +{
> > > +	int			n = 0;
> > > +
> > > +	/* We know exactly how mamy worker and aux processes can be active */
> > > +	n += autovacuum_max_workers;
> > > +	n += max_worker_processes;
> > > +	n += NUM_AUXILIARY_PROCS;
> > > +
> > > +	/*
> > > +	 * We allow more connections here than we can have backends because some
> > > +	 * might still be authenticating; they might fail auth, or some existing
> > > +	 * backend might exit before the auth cycle is completed.  The exact
> > > +	 * MaxBackends limit is enforced when a new backend tries to join the
> > > +	 * shared-inval backend array.
> > > +	 */
> > > +	n += 2 * (MaxConnections + max_wal_senders);
> > > +
> > > +	return n;
> > > +}
> >
> > I wonder if we could instead maintain at least some of this in
> > child_process_kinds? Manually listing different types of processes in
> > different places doesn't seem particularly sustainable.
>
> Hmm, you mean adding "max this kind of children" field to
> child_process_kinds? Perhaps.

Yep, that's what I meant.



> > > +/* Return the appropriate free-list for the given backend type */
> > > +static dlist_head *
> > > +GetFreeList(BackendType btype)
> > > +{
> > > +	switch (btype)
> > > +	{
> > > +		case B_BACKEND:
> > > +		case B_BG_WORKER:
> > > +		case B_WAL_SENDER:
> > > +		case B_SLOTSYNC_WORKER:
> > > +			return &freeBackendList;
> >
> > Maybe a daft question - but why are all of these in the same list? Sure,
> > they're essentially all full backends, but they're covered by different GUCs?
>
> No reason. No particular reason they should *not* share the same list either
> though.

Aren't they controlled by distinct connection limits? Isn't there a danger
that we could use up entries and fail connections due to that, despite not
actually being above the limit?


> > Tangential: Why do we need a freelist for these and why do we choose a random
> > pgproc for these instead of assigning one statically?
> >
> > Background: I'd like to not provide AIO workers with "bounce buffers" (for IO
> > of buffers that can't be done in-place, like writes when checksums are
> > enabled). The varying proc numbers make that harder than it'd have to be...
>
> Yeah, we can make these fixed.

Cool.


> Currently, the # of slots reserved for aux processes is sized by
> NUM_AUXILIARY_PROCS, which is one smaller than the number of different aux
> proces kinds:

> > /*
> >  * We set aside some extra PGPROC structures for auxiliary processes,
> >  * ie things that aren't full-fledged backends but need shmem access.
> >  *
> >  * Background writer, checkpointer, WAL writer, WAL summarizer, and archiver
> >  * run during normal operation.  Startup process and WAL receiver also consume
> >  * 2 slots, but WAL writer is launched only after startup has exited, so we
> >  * only need 6 slots.
> >  */
> > #define NUM_AUXILIARY_PROCS		6
>
> For PMChildSlot numbers, we could certainly just allocate one more slot.
>
> It would probably make sense for PGPROCs too, even though PGPROC is a much
> larger struct.

I don't think it's worth worrying about that much. PGPROC is large, but not
*that* large. And the robustness win of properly detecting when there's a
problem around starting/stopping aux workers seems to outweigh that to me.



> > > +PMChild *
> > > +AssignPostmasterChildSlot(BackendType btype)
> > > +{
> > > +	dlist_head *freelist;
> > > +	PMChild    *pmchild;
> > > +
> > > +	freelist = GetFreeList(btype);
> > > +
> > > +	if (dlist_is_empty(freelist))
> > > +		return NULL;
> > > +
> > > +	pmchild = dlist_container(PMChild, elem, dlist_pop_head_node(freelist));
> > > +	pmchild->pid = 0;
> > > +	pmchild->bkend_type = btype;
> > > +	pmchild->rw = NULL;
> > > +	pmchild->bgworker_notify = true;
> > > +
> > > +	/*
> > > +	 * pmchild->child_slot for each entry was initialized when the array of
> > > +	 * slots was allocated.
> > > +	 */
> > > +
> > > +	dlist_push_head(&ActiveChildList, &pmchild->elem);
> > > +
> > > +	ReservePostmasterChildSlot(pmchild->child_slot);
> > > +
> > > +	/* FIXME: find a more elegant way to pass this */
> > > +	MyPMChildSlot = pmchild->child_slot;
> >
> > What if we assigned one offset for each process and assigned its ID here and
> > also used that for its ProcNumber - that way we wouldn't need to manage
> > freelists in two places.
>
> It's currently possible to have up to 2 * max_connections backends in the
> authentication phase. We would have to change that behaviour, or make the
> PGPROC array 2x larger.

That however, might be too much...


> It might well be worth it, I don't know how sensible the current behaviour
> is. But I'd like to punt that to later patch, to keep the scope of this
> patch set reasonable. It's pretty straightforward to do later on top of this
> if we want to.

Makes sense.


I still think that we'd be better off to just return an error to the client in
postmaster, rather than deal with this dead-end children mess. That was
perhaps justified at some point, but now it seems to add way more complexity
than it's worth. And it's absurdly expensive to fork to return an error. Way
more expensive than just having postmaster send an error and close the socket.


> > > @@ -2469,11 +2410,15 @@ process_pm_child_exit(void)
> > >   		}
> > >   		/* Was it the system logger?  If so, try to start a new one */
> > > -		if (pid == SysLoggerPID)
> > > +		if (SysLoggerPMChild && pid == SysLoggerPMChild->pid)
> > >   		{
> > > -			SysLoggerPID = 0;
> > >   			/* for safety's sake, launch new logger *first* */
> > > -			SysLoggerPID = SysLogger_Start();
> > > +			SysLoggerPMChild->pid = SysLogger_Start();
> > > +			if (SysLoggerPMChild->pid == 0)
> > > +			{
> > > +				FreePostmasterChildSlot(SysLoggerPMChild);
> > > +				SysLoggerPMChild = NULL;
> > > +			}
> > >   			if (!EXIT_STATUS_0(exitstatus))
> > >   				LogChildExit(LOG, _("system logger process"),
> >
> > Seems a bit weird to have one place with a different memory lifetime handling
> > than other places. Why don't we just do this the same way as in other places
> > but continue to defer the logging until after we tried to start the new
> > logger?
>
> Hmm, you mean let LaunchMissingBackgroundProcesses() handle the restart?

Yea - which it already can do, presumably to handle the case of
logging_collector. It just seems odd to have code to have three places calling
SysLogger_Start() - with some mild variations of the code.

Perhaps we can at least centralize some of that?


But you have a point with:

> I'm a little scared of changing the existing logic. We don't have a
> mechanism for deferring logging, so we would have to invent that, or the
> logs would just accumulate in the pipe until syslogger starts up. There's
> some code between here and LaunchMissingBackgroundProcesses(), so might
> postmaster get blocked between writing to the syslogger pipe, before having
> restarted it?
>
> If forking the syslogger process fails, that can happen anyway, though.




> > >   	/* Construct a process name for log message */
> > > +
> > > +	/*
> > > +	 * FIXME: use GetBackendTypeDesc here? How does the localization of that
> > > +	 * work?
> > > +	 */
> > >   	if (bp->bkend_type == B_DEAD_END_BACKEND)
> > >   	{
> > >   		procname = _("dead end backend");
> >
> > Might be worth having a version of GetBackendTypeDesc() that returns a
> > translated string?
>
> Constructing the string for background workers is a little more complicated:

Random aside: I *hate* that there's no trivial way recognie background workers
in pg_stat_activity, because somebody made pg_stat_activity.backend_type
report something completely under control of extensions...




> > > @@ -2697,9 +2643,16 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
> > >   	{
> > >   		dlist_iter	iter;
> > > -		dlist_foreach(iter, &BackendList)
> > > +		dlist_foreach(iter, &ActiveChildList)
> > >   		{
> > > -			Backend    *bp = dlist_container(Backend, elem, iter.cur);
> > > +			PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
> > > +
> > > +			/* We do NOT restart the syslogger */
> > > +			if (bp == SysLoggerPMChild)
> > > +				continue;
> >
> > That comment seems a bit misleading - we do restart syslogger, we just don't
> > do it here, no?  I realize it's an old comment, but it still seems like it's
> > worth fixing given that you touch all the code here...
>
> No, we really do not restart the syslogger.

Hm?

		/* Was it the system logger?  If so, try to start a new one */
		if (SysLoggerPMChild && pid == SysLoggerPMChild->pid)
		{
			/* for safety's sake, launch new logger *first* */
			SysLoggerPMChild->pid = SysLogger_Start(SysLoggerPMChild->child_slot);
			if (SysLoggerPMChild->pid == 0)
			{
				FreePostmasterChildSlot(SysLoggerPMChild);
				SysLoggerPMChild = NULL;
			}
			if (!EXIT_STATUS_0(exitstatus))
				LogChildExit(LOG, _("system logger process"),
							 pid, exitstatus);
			continue;
		}

We don't do it reaction to other processes crashing, but we still restart it
if it dies. Perhaps it's clear from context - but I had to think aobut it for
a moment.


>
> > > @@ -2871,29 +2786,27 @@ PostmasterStateMachine(void)
> > > <snip>
> > > -		if (WalSummarizerPID != 0)
> > > -			signal_child(WalSummarizerPID, SIGTERM);
> > > -		if (SlotSyncWorkerPID != 0)
> > > -			signal_child(SlotSyncWorkerPID, SIGTERM);
> > > +		targetMask |= (1 << B_STARTUP);
> > > +		targetMask |= (1 << B_WAL_RECEIVER);
> > > +
> > > +		targetMask |= (1 << B_WAL_SUMMARIZER);
> > > +		targetMask |= (1 << B_SLOTSYNC_WORKER);
> > >   		/* checkpointer, archiver, stats, and syslogger may continue for now */
> > > +		SignalSomeChildren(SIGTERM, targetMask);
> > > +
> > >   		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
> > >   		pmState = PM_WAIT_BACKENDS;
> > > <snip>
> >
> > It's likely the right thing to not do as one patch, but IMO this really wants
> > to be a state table. Perhaps as part of child_process_kinds, perhaps separate
> > from that.
>
> Yeah. I've tried to refactor this into a table before, but didn't come up
> with anything that I was happy with. I also feel there must be a better way
> to organize this, but not sure what exactly. I hope that will become more
> apparent after these other changes.

What I'm imagining is something like:
1) Make PMState values each have a distinct bit
2) Move PMState to some (new?) header
3) Add a "uint32 should_run" member to child_process_kind that's a bitmask of
   PMStates
4) Add a new function in launch_backend.c that gets passed the "target"
   PMState and returns a bitmask of the tasks that should be running (or the
   inverse, doesn't really matter).
5) Instead of open-coding the targetMask "computation", use the new function
   from 4).

I think that might not look too bad?

Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-09-10 17:33  Robert Haas <[email protected]>
  parent: Andres Freund <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Robert Haas @ 2024-09-10 17:33 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Thomas Munro <[email protected]>; pgsql-hackers

On Tue, Sep 10, 2024 at 12:59 PM Andres Freund <[email protected]> wrote:
> I still think that we'd be better off to just return an error to the client in
> postmaster, rather than deal with this dead-end children mess. That was
> perhaps justified at some point, but now it seems to add way more complexity
> than it's worth. And it's absurdly expensive to fork to return an error. Way
> more expensive than just having postmaster send an error and close the socket.

The tricky case is the one where the client write() -- or SSL_write() -- blocks.

-- 
Robert Haas
EDB: http://www.enterprisedb.com






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-09-10 17:53  Andres Freund <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  1 sibling, 1 reply; 39+ messages in thread

From: Andres Freund @ 2024-09-10 17:53 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers

Hi,

On 2024-08-12 12:55:00 +0300, Heikki Linnakangas wrote:
> @@ -2864,6 +2777,8 @@ PostmasterStateMachine(void)
>  	 */
>  	if (pmState == PM_STOP_BACKENDS)
>  	{
> +		uint32		targetMask;
> +
>  		/*
>  		 * Forget any pending requests for background workers, since we're no
>  		 * longer willing to launch any new workers.  (If additional requests
> @@ -2871,29 +2786,27 @@ PostmasterStateMachine(void)
>  		 */
>  		ForgetUnstartedBackgroundWorkers();
>  
> -		/* Signal all backend children except walsenders and dead-end backends */
> -		SignalSomeChildren(SIGTERM,
> -						   BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND));
> +		/* Signal all backend children except walsenders */
> +		/* dead-end children are not signalled yet */
> +		targetMask = (1 << B_BACKEND);
> +		targetMask |= (1 << B_BG_WORKER);
> +
>  		/* and the autovac launcher too */
> -		if (AutoVacPID != 0)
> -			signal_child(AutoVacPID, SIGTERM);
> +		targetMask |= (1 << B_AUTOVAC_LAUNCHER);
>  		/* and the bgwriter too */
> -		if (BgWriterPID != 0)
> -			signal_child(BgWriterPID, SIGTERM);
> +		targetMask |= (1 << B_BG_WRITER);
>  		/* and the walwriter too */
> -		if (WalWriterPID != 0)
> -			signal_child(WalWriterPID, SIGTERM);
> +		targetMask |= (1 << B_WAL_WRITER);
>  		/* If we're in recovery, also stop startup and walreceiver procs */
> -		if (StartupPID != 0)
> -			signal_child(StartupPID, SIGTERM);
> -		if (WalReceiverPID != 0)
> -			signal_child(WalReceiverPID, SIGTERM);
> -		if (WalSummarizerPID != 0)
> -			signal_child(WalSummarizerPID, SIGTERM);
> -		if (SlotSyncWorkerPID != 0)
> -			signal_child(SlotSyncWorkerPID, SIGTERM);
> +		targetMask |= (1 << B_STARTUP);
> +		targetMask |= (1 << B_WAL_RECEIVER);
> +
> +		targetMask |= (1 << B_WAL_SUMMARIZER);
> +		targetMask |= (1 << B_SLOTSYNC_WORKER);
>  		/* checkpointer, archiver, stats, and syslogger may continue for now */
>  
> +		SignalSomeChildren(SIGTERM, targetMask);
> +
>  		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
>  		pmState = PM_WAIT_BACKENDS;
>  	}

I think this might now omit shutting down at least autovac workers, which
afaict previously were included.

Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-09-10 22:35  Andres Freund <[email protected]>
  parent: Robert Haas <[email protected]>
  0 siblings, 0 replies; 39+ messages in thread

From: Andres Freund @ 2024-09-10 22:35 UTC (permalink / raw)
  To: Robert Haas <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Thomas Munro <[email protected]>; pgsql-hackers

Hi,

On 2024-09-10 13:33:36 -0400, Robert Haas wrote:
> On Tue, Sep 10, 2024 at 12:59 PM Andres Freund <[email protected]> wrote:
> > I still think that we'd be better off to just return an error to the client in
> > postmaster, rather than deal with this dead-end children mess. That was
> > perhaps justified at some point, but now it seems to add way more complexity
> > than it's worth. And it's absurdly expensive to fork to return an error. Way
> > more expensive than just having postmaster send an error and close the socket.
> 
> The tricky case is the one where the client write() -- or SSL_write() -- blocks.

Yea, SSL definitely does make it harder. But it's not exactly rocket science
to do non-blocking SSL connection establishment. After all, we do manage to
do so in libpq...

Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-10-04 18:41  Heikki Linnakangas <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Heikki Linnakangas @ 2024-10-04 18:41 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers

On 06/09/2024 12:52, Heikki Linnakangas wrote:
> Unless you have comments on these first two patches which just add 
> tests, I'll commit them shortly. Still processing the rest of your 
> comments...

Didn't happen as "shortly" as I thought..

My test for dead-end backends opens 20 TCP (or unix domain) connections 
to the server, in quick succession. That works fine my system, and it 
passed cirrus CI on other platforms, but on FreeBSD it failed 
repeatedly. The behavior in that scenario is apparently 
platform-dependent: it depends on the accept queue size, but what 
happens when you reach the queue size also seems to depend on the 
platform. On my Linux system, the connect() calls in the client are 
blocked, if the server is doesn't call accept() fast enough, but 
apparently you get an error on *BSD systems.

I'm not sure of the exact details, but in any case, platform-dependent 
behavior needs to be avoided in tests. So I changed the test so that it 
sends an SSLRequest packet on each connection and waits for reply (which 
is always 'N' to reject it in this test), before opening the next 
connection. This way, each connection is still left hanging, which is 
what I want in this test, but only after postmaster has successfully 
accept()ed it and forked the backend.

So here are these test patches again, with that addition.

-- 
Heikki Linnakangas
Neon (https://neon.tech)


Attachments:

  [text/x-patch] v5-0001-Add-test-for-connection-limits.patch (6.6K, ../../[email protected]/2-v5-0001-Add-test-for-connection-limits.patch)
  download | inline diff:
From 16993fde8153b4feced18e6a49d8b470e0a49241 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Fri, 4 Oct 2024 20:15:36 +0300
Subject: [PATCH v5 1/3] Add test for connection limits

Reviewed-by: Andres Freund <[email protected]>
Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/test/Makefile                             |  2 +-
 src/test/meson.build                          |  1 +
 src/test/postmaster/Makefile                  | 23 ++++++
 src/test/postmaster/README                    | 27 +++++++
 src/test/postmaster/meson.build               | 12 +++
 .../postmaster/t/001_connection_limits.pl     | 79 +++++++++++++++++++
 6 files changed, 143 insertions(+), 1 deletion(-)
 create mode 100644 src/test/postmaster/Makefile
 create mode 100644 src/test/postmaster/README
 create mode 100644 src/test/postmaster/meson.build
 create mode 100644 src/test/postmaster/t/001_connection_limits.pl

diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..abdd6e5a98 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,7 +12,7 @@ subdir = src/test
 top_builddir = ../..
 include $(top_builddir)/src/Makefile.global
 
-SUBDIRS = perl regress isolation modules authentication recovery subscription
+SUBDIRS = perl postmaster regress isolation modules authentication recovery subscription
 
 ifeq ($(with_icu),yes)
 SUBDIRS += icu
diff --git a/src/test/meson.build b/src/test/meson.build
index c3d0dfedf1..67376e4b7f 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -4,6 +4,7 @@ subdir('regress')
 subdir('isolation')
 
 subdir('authentication')
+subdir('postmaster')
 subdir('recovery')
 subdir('subscription')
 subdir('modules')
diff --git a/src/test/postmaster/Makefile b/src/test/postmaster/Makefile
new file mode 100644
index 0000000000..dfcce9c9ee
--- /dev/null
+++ b/src/test/postmaster/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/postmaster
+#
+# Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/postmaster/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/postmaster
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean:
+	rm -rf tmp_check
diff --git a/src/test/postmaster/README b/src/test/postmaster/README
new file mode 100644
index 0000000000..7e47bf5cff
--- /dev/null
+++ b/src/test/postmaster/README
@@ -0,0 +1,27 @@
+src/test/postmaster/README
+
+Regression tests for postmaster
+===============================
+
+This directory contains a test suite for postmaster's handling of
+connections, connection limits, and startup/shutdown sequence.
+
+
+Running the tests
+=================
+
+NOTE: You must have given the --enable-tap-tests argument to configure.
+
+Run
+    make check
+or
+    make installcheck
+You can use "make installcheck" if you previously did "make install".
+In that case, the code in the installation tree is tested.  With
+"make check", a temporary installation tree is built from the current
+sources and then tested.
+
+Either way, this test initializes, starts, and stops a test Postgres
+cluster.
+
+See src/test/perl/README for more info about running these tests.
diff --git a/src/test/postmaster/meson.build b/src/test/postmaster/meson.build
new file mode 100644
index 0000000000..c2de2e0eb5
--- /dev/null
+++ b/src/test/postmaster/meson.build
@@ -0,0 +1,12 @@
+# Copyright (c) 2022-2024, PostgreSQL Global Development Group
+
+tests += {
+  'name': 'postmaster',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_connection_limits.pl',
+    ],
+  },
+}
diff --git a/src/test/postmaster/t/001_connection_limits.pl b/src/test/postmaster/t/001_connection_limits.pl
new file mode 100644
index 0000000000..f50aae4949
--- /dev/null
+++ b/src/test/postmaster/t/001_connection_limits.pl
@@ -0,0 +1,79 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# Test connection limits, i.e. max_connections, reserved_connections
+# and superuser_reserved_connections.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize the server with specific low connection limits
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->append_conf('postgresql.conf', "max_connections = 6");
+$node->append_conf('postgresql.conf', "reserved_connections = 2");
+$node->append_conf('postgresql.conf', "superuser_reserved_connections = 1");
+$node->append_conf('postgresql.conf', "log_connections = on");
+$node->start;
+
+$node->safe_psql(
+	'postgres', qq{
+CREATE USER regress_regular LOGIN;
+CREATE USER regress_reserved LOGIN;
+GRANT pg_use_reserved_connections TO regress_reserved;
+CREATE USER regress_superuser LOGIN SUPERUSER;
+});
+
+# With the limits we set in postgresql.conf, we can establish:
+# - 3 connections for any user with no special privileges
+# - 2 more connections for users belonging to "pg_use_reserved_connections"
+# - 1 more connection for superuser
+
+sub background_psql_as_user
+{
+	my $user = shift;
+
+	return $node->background_psql(
+		'postgres',
+		on_error_die => 1,
+		extra_params => [ '-U', $user ]);
+}
+
+my @sessions = ();
+
+push(@sessions, background_psql_as_user('regress_regular'));
+push(@sessions, background_psql_as_user('regress_regular'));
+push(@sessions, background_psql_as_user('regress_regular'));
+$node->connect_fails(
+	"dbname=postgres user=regress_regular",
+	"reserved_connections limit",
+	expected_stderr =>
+	  qr/FATAL:  remaining connection slots are reserved for roles with privileges of the "pg_use_reserved_connections" role/
+);
+
+push(@sessions, background_psql_as_user('regress_reserved'));
+push(@sessions, background_psql_as_user('regress_reserved'));
+$node->connect_fails(
+	"dbname=postgres user=regress_regular",
+	"reserved_connections limit",
+	expected_stderr =>
+	  qr/FATAL:  remaining connection slots are reserved for roles with the SUPERUSER attribute/
+);
+
+push(@sessions, background_psql_as_user('regress_superuser'));
+$node->connect_fails(
+	"dbname=postgres user=regress_superuser",
+	"superuser_reserved_connections limit",
+	expected_stderr => qr/FATAL:  sorry, too many clients already/);
+
+# TODO: test that query cancellation is still possible
+
+foreach my $session (@sessions)
+{
+	$session->quit;
+}
+
+done_testing();
-- 
2.39.5



  [text/x-patch] v5-0002-Add-test-for-dead-end-backends.patch (4.1K, ../../[email protected]/3-v5-0002-Add-test-for-dead-end-backends.patch)
  download | inline diff:
From cd53192ea66a5b07c93941bd36e9f38bf8005956 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Fri, 4 Oct 2024 20:15:44 +0300
Subject: [PATCH v5 2/3] Add test for dead-end backends

The code path for launching a dead-end backend because we're out of
slots was not covered by any tests, so add one. (Some tests did hit
the case of launching a dead-end backend because the server is still
starting up, though, so the gap in our test coverage wasn't as big as
it sounds.)

Reviewed-by: Andres Freund <[email protected]>
Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/test/perl/PostgreSQL/Test/Cluster.pm      | 38 +++++++++++++++++++
 .../postmaster/t/001_connection_limits.pl     | 37 +++++++++++++++++-
 2 files changed, 74 insertions(+), 1 deletion(-)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 90a842f96a..c278765fb0 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -104,6 +104,7 @@ use File::Path qw(rmtree mkpath);
 use File::Spec;
 use File::stat qw(stat);
 use File::Temp ();
+use IO::Socket::INET;
 use IPC::Run;
 use PostgreSQL::Version;
 use PostgreSQL::Test::RecursiveCopy;
@@ -286,6 +287,43 @@ sub connstr
 
 =pod
 
+=item $node->raw_connect()
+
+Open a raw TCP or Unix domain socket connection to the server. This is
+used by low-level protocol and connection limit tests.
+
+=cut
+
+sub raw_connect
+{
+	my ($self) = @_;
+	my $pgport = $self->port;
+	my $pghost = $self->host;
+
+	my $socket;
+	if ($PostgreSQL::Test::Utils::use_unix_sockets)
+	{
+		require IO::Socket::UNIX;
+		my $path = "$pghost/.s.PGSQL.$pgport";
+
+		$socket = IO::Socket::UNIX->new(
+			Type => SOCK_STREAM(),
+			Peer => $path,
+		) or die "Cannot create socket - $IO::Socket::errstr\n";
+	}
+	else
+	{
+		$socket = IO::Socket::INET->new(
+			PeerHost => $pghost,
+			PeerPort => $pgport,
+			Proto => 'tcp'
+		) or die "Cannot create socket - $IO::Socket::errstr\n";
+	}
+	return $socket;
+}
+
+=pod
+
 =item $node->group_access()
 
 Does the data dir allow group access?
diff --git a/src/test/postmaster/t/001_connection_limits.pl b/src/test/postmaster/t/001_connection_limits.pl
index f50aae4949..158464fe03 100644
--- a/src/test/postmaster/t/001_connection_limits.pl
+++ b/src/test/postmaster/t/001_connection_limits.pl
@@ -43,6 +43,7 @@ sub background_psql_as_user
 }
 
 my @sessions = ();
+my @raw_connections = ();
 
 push(@sessions, background_psql_as_user('regress_regular'));
 push(@sessions, background_psql_as_user('regress_regular'));
@@ -69,11 +70,45 @@ $node->connect_fails(
 	"superuser_reserved_connections limit",
 	expected_stderr => qr/FATAL:  sorry, too many clients already/);
 
-# TODO: test that query cancellation is still possible
+# We can still open TCP (or Unix domain socket) connections, but
+# beyond a certain number (roughly 2x max_connections), they will be
+# "dead-end backends".
+for (my $i = 0; $i <= 20; $i++)
+{
+	my $sock = $node->raw_connect();
+
+	# On a busy system, the server might reject connections if
+	# postmaster cannot accept() them fast enough. The exact limit and
+	# behavior depends on the platform. To make this reliable, we
+	# attempt SSL negotiation on each connection before opening next
+	# one. The server will reject the SSL negotations, but when it
+	# does so, we know that the backend has been launched and we
+	# should be able to open another connection.
+
+	# SSLRequest packet consists of packet length followed by
+	# NEGOTIATE_SSL_CODE.
+	my $negotiate_ssl_code = pack("Nnn", 8, 1234, 5679);
+	my $sent = $sock->send($negotiate_ssl_code);
 
+	# Read reply. We expect the server to reject it with 'N'
+	my $reply = "";
+	$sock->recv($reply, 1);
+	is($reply, "N", "dead-end connection $i");
+
+	push(@raw_connections, $sock);
+}
+
+# TODO: test that query cancellation is still possible. A dead-end
+# backend can process a query cancellation packet.
+
+# Clean up
 foreach my $session (@sessions)
 {
 	$session->quit;
 }
+foreach my $socket (@raw_connections)
+{
+	$socket->close();
+}
 
 done_testing();
-- 
2.39.5



^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-10-04 22:03  Thomas Munro <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Thomas Munro @ 2024-10-04 22:03 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Andres Freund <[email protected]>; pgsql-hackers

On Sat, Oct 5, 2024 at 7:41 AM Heikki Linnakangas <[email protected]> wrote:
> My test for dead-end backends opens 20 TCP (or unix domain) connections
> to the server, in quick succession. That works fine my system, and it
> passed cirrus CI on other platforms, but on FreeBSD it failed
> repeatedly. The behavior in that scenario is apparently
> platform-dependent: it depends on the accept queue size, but what
> happens when you reach the queue size also seems to depend on the
> platform. On my Linux system, the connect() calls in the client are
> blocked, if the server is doesn't call accept() fast enough, but
> apparently you get an error on *BSD systems.

Right, we've analysed that difference in AF_UNIX implementation
before[1], which shows up in the real world, where client sockets ie
libpq's are usually non-blocking, as EAGAIN on Linux (which is not
valid per POSIX) vs ECONNREFUSED on other OSes.  All fail to connect,
but the error message is different.

For blocking AF_UNIX client sockets like in your test, Linux
effectively has an infinite queue made from two layers.  The listen
queue (a queue of connecting sockets) does respect the requested
backlog size, but when it's full it has an extra trick: the connect()
call waits (in a queue of threads) for space to become free in the
listen queue, so it's effectively unlimited (but only for blocking
sockets), while FreeBSD and I suspect any other implementation
deriving from or reimplementing the BSD socket code gives you
ECONNREFUSED.  macOS behaves just the same as FreeBSD AFAICT, so I
don't know why you didn't see the same thing... I guess it's just
racing against accept() draining the queue.

It's possible that Windows copied the Linux behaviour for AF_UNIX,
given that it probably has something to do with the WSL project for
emulating Linux, but IDK.

[1] https://www.postgresql.org/message-id/flat/CADc_NKg2d%2BoZY9mg4DdQdoUcGzN2kOYXBu-3--RW_hEe0tUV%3Dg%4...

> I'm not sure of the exact details, but in any case, platform-dependent
> behavior needs to be avoided in tests. So I changed the test so that it
> sends an SSLRequest packet on each connection and waits for reply (which
> is always 'N' to reject it in this test), before opening the next
> connection. This way, each connection is still left hanging, which is
> what I want in this test, but only after postmaster has successfully
> accept()ed it and forked the backend.

Makes sense.






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-10-05 11:45  Heikki Linnakangas <[email protected]>
  parent: Thomas Munro <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Heikki Linnakangas @ 2024-10-05 11:45 UTC (permalink / raw)
  To: Thomas Munro <[email protected]>; +Cc: Andres Freund <[email protected]>; pgsql-hackers

On 05/10/2024 01:03, Thomas Munro wrote:
> On Sat, Oct 5, 2024 at 7:41 AM Heikki Linnakangas <[email protected]> wrote:
>> My test for dead-end backends opens 20 TCP (or unix domain) connections
>> to the server, in quick succession. That works fine my system, and it
>> passed cirrus CI on other platforms, but on FreeBSD it failed
>> repeatedly. The behavior in that scenario is apparently
>> platform-dependent: it depends on the accept queue size, but what
>> happens when you reach the queue size also seems to depend on the
>> platform. On my Linux system, the connect() calls in the client are
>> blocked, if the server is doesn't call accept() fast enough, but
>> apparently you get an error on *BSD systems.
> 
> Right, we've analysed that difference in AF_UNIX implementation
> before[1], which shows up in the real world, where client sockets ie
> libpq's are usually non-blocking, as EAGAIN on Linux (which is not
> valid per POSIX) vs ECONNREFUSED on other OSes.  All fail to connect,
> but the error message is different.

Thanks for the pointer!

> For blocking AF_UNIX client sockets like in your test, Linux
> effectively has an infinite queue made from two layers.  The listen
> queue (a queue of connecting sockets) does respect the requested
> backlog size, but when it's full it has an extra trick: the connect()
> call waits (in a queue of threads) for space to become free in the
> listen queue, so it's effectively unlimited (but only for blocking
> sockets), while FreeBSD and I suspect any other implementation
> deriving from or reimplementing the BSD socket code gives you
> ECONNREFUSED.  macOS behaves just the same as FreeBSD AFAICT, so I
> don't know why you didn't see the same thing... I guess it's just
> racing against accept() draining the queue.

In fact I misremembered: the failure happened on macOS, *not* FreeBSD. 
It could be just luck I didn't see it on FreeBSD though.

> It's possible that Windows copied the Linux behaviour for AF_UNIX,
> given that it probably has something to do with the WSL project for
> emulating Linux, but IDK.

Sadly Windows' IO::Socket::UNIX hasn't been implemented on Windows (or 
at least on this perl distribution we're using in Cirrus CI):

Socket::pack_sockaddr_un not implemented on this architecture at 
C:/strawberry/5.26.3.1/perl/lib/Socket.pm line 872.

so I'll have to disable this test on Windows anyway.

-- 
Heikki Linnakangas
Neon (https://neon.tech)







^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-10-05 19:51  Dagfinn Ilmari Mannsåker <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  0 siblings, 2 replies; 39+ messages in thread

From: Dagfinn Ilmari Mannsåker @ 2024-10-05 19:51 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Thomas Munro <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers

Heikki Linnakangas <[email protected]> writes:

> On 05/10/2024 01:03, Thomas Munro wrote:
>
>> It's possible that Windows copied the Linux behaviour for AF_UNIX,
>> given that it probably has something to do with the WSL project for
>> emulating Linux, but IDK.
>
> Sadly Windows' IO::Socket::UNIX hasn't been implemented on Windows (or
> at least on this perl distribution we're using in Cirrus CI):
>
> Socket::pack_sockaddr_un not implemented on this architecture at
> C:/strawberry/5.26.3.1/perl/lib/Socket.pm line 872.
>
> so I'll have to disable this test on Windows anyway.

Socket version 2.028 (included in Perl 5.32) provides pack_sockaddr_un()
on Windows, so that can be fixed by bumping the Perl version in
https://github.com/anarazel/pg-vm-images/blob/main/packer/windows.pkr.hcl
to something more modern (such as 5.40.0.1), and only skipping the test
if on Windows if Socket is too old.

The decision to use 5.26 seems to come from the initial creation of the
CI images in 2021 (when 5.34 was current), with the comment «newer
versions don't currently work correctly for plperl».  That claim is
worth revisiting, and fixing if it's still the case.


- ilmari







^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-10-07 14:19  Andres Freund <[email protected]>
  parent: Dagfinn Ilmari Mannsåker <[email protected]>
  1 sibling, 0 replies; 39+ messages in thread

From: Andres Freund @ 2024-10-07 14:19 UTC (permalink / raw)
  To: Dagfinn Ilmari Mannsåker <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Thomas Munro <[email protected]>; pgsql-hackers; Nazir Bilal Yavuz <[email protected]>

Hi,

On 2024-10-05 20:51:50 +0100, Dagfinn Ilmari Mannsåker wrote:
> Socket version 2.028 (included in Perl 5.32) provides pack_sockaddr_un()
> on Windows, so that can be fixed by bumping the Perl version in
> https://github.com/anarazel/pg-vm-images/blob/main/packer/windows.pkr.hcl
> to something more modern (such as 5.40.0.1), and only skipping the test
> if on Windows if Socket is too old.
> 
> The decision to use 5.26 seems to come from the initial creation of the
> CI images in 2021 (when 5.34 was current), with the comment «newer
> versions don't currently work correctly for plperl».  That claim is
> worth revisiting, and fixing if it's still the case.

I think we fixed the issues that were known at the time. I think I tried
upgrading to something newer at some point and there were some weird, but
fixable, encoding issues. Unfortunately I don't have the bandwidth to tackle
this rn.

Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-10-07 21:55  Heikki Linnakangas <[email protected]>
  parent: Dagfinn Ilmari Mannsåker <[email protected]>
  1 sibling, 3 replies; 39+ messages in thread

From: Heikki Linnakangas @ 2024-10-07 21:55 UTC (permalink / raw)
  To: Dagfinn Ilmari Mannsåker <[email protected]>; +Cc: Thomas Munro <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers

On 05/10/2024 22:51, Dagfinn Ilmari Mannsåker wrote:
> Heikki Linnakangas <[email protected]> writes:
>> Sadly Windows' IO::Socket::UNIX hasn't been implemented on Windows (or
>> at least on this perl distribution we're using in Cirrus CI):
>>
>> Socket::pack_sockaddr_un not implemented on this architecture at
>> C:/strawberry/5.26.3.1/perl/lib/Socket.pm line 872.
>>
>> so I'll have to disable this test on Windows anyway.
> 
> Socket version 2.028 (included in Perl 5.32) provides pack_sockaddr_un()
> on Windows, so that can be fixed by bumping the Perl version in
> https://github.com/anarazel/pg-vm-images/blob/main/packer/windows.pkr.hcl
> to something more modern (such as 5.40.0.1), and only skipping the test
> if on Windows if Socket is too old.
> 
> The decision to use 5.26 seems to come from the initial creation of the
> CI images in 2021 (when 5.34 was current), with the comment «newer
> versions don't currently work correctly for plperl».  That claim is
> worth revisiting, and fixing if it's still the case.

Yeah, it would be nice to update it. I wonder if commit 
341f4e002d461a3c5513cb864490cddae2b43a64 fixed whatever the problem was.

In the meanwhile, here is a one more version of the test patches, with a 
SKIP that checks that IO::Socket::UNIX works.

-- 
Heikki Linnakangas
Neon (https://neon.tech)


Attachments:

  [text/x-patch] v6-0001-Add-test-for-connection-limits.patch (6.6K, ../../[email protected]/2-v6-0001-Add-test-for-connection-limits.patch)
  download | inline diff:
From 9b499e8cdf09a127d7506837d5e2a697dd342820 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Tue, 8 Oct 2024 00:54:30 +0300
Subject: [PATCH v6 1/3] Add test for connection limits

Reviewed-by: Andres Freund <[email protected]>
Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/test/Makefile                             |  2 +-
 src/test/meson.build                          |  1 +
 src/test/postmaster/Makefile                  | 23 ++++++
 src/test/postmaster/README                    | 27 +++++++
 src/test/postmaster/meson.build               | 12 +++
 .../postmaster/t/001_connection_limits.pl     | 79 +++++++++++++++++++
 6 files changed, 143 insertions(+), 1 deletion(-)
 create mode 100644 src/test/postmaster/Makefile
 create mode 100644 src/test/postmaster/README
 create mode 100644 src/test/postmaster/meson.build
 create mode 100644 src/test/postmaster/t/001_connection_limits.pl

diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..abdd6e5a98 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -12,7 +12,7 @@ subdir = src/test
 top_builddir = ../..
 include $(top_builddir)/src/Makefile.global
 
-SUBDIRS = perl regress isolation modules authentication recovery subscription
+SUBDIRS = perl postmaster regress isolation modules authentication recovery subscription
 
 ifeq ($(with_icu),yes)
 SUBDIRS += icu
diff --git a/src/test/meson.build b/src/test/meson.build
index c3d0dfedf1..67376e4b7f 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -4,6 +4,7 @@ subdir('regress')
 subdir('isolation')
 
 subdir('authentication')
+subdir('postmaster')
 subdir('recovery')
 subdir('subscription')
 subdir('modules')
diff --git a/src/test/postmaster/Makefile b/src/test/postmaster/Makefile
new file mode 100644
index 0000000000..dfcce9c9ee
--- /dev/null
+++ b/src/test/postmaster/Makefile
@@ -0,0 +1,23 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/postmaster
+#
+# Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/postmaster/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/postmaster
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean:
+	rm -rf tmp_check
diff --git a/src/test/postmaster/README b/src/test/postmaster/README
new file mode 100644
index 0000000000..7e47bf5cff
--- /dev/null
+++ b/src/test/postmaster/README
@@ -0,0 +1,27 @@
+src/test/postmaster/README
+
+Regression tests for postmaster
+===============================
+
+This directory contains a test suite for postmaster's handling of
+connections, connection limits, and startup/shutdown sequence.
+
+
+Running the tests
+=================
+
+NOTE: You must have given the --enable-tap-tests argument to configure.
+
+Run
+    make check
+or
+    make installcheck
+You can use "make installcheck" if you previously did "make install".
+In that case, the code in the installation tree is tested.  With
+"make check", a temporary installation tree is built from the current
+sources and then tested.
+
+Either way, this test initializes, starts, and stops a test Postgres
+cluster.
+
+See src/test/perl/README for more info about running these tests.
diff --git a/src/test/postmaster/meson.build b/src/test/postmaster/meson.build
new file mode 100644
index 0000000000..c2de2e0eb5
--- /dev/null
+++ b/src/test/postmaster/meson.build
@@ -0,0 +1,12 @@
+# Copyright (c) 2022-2024, PostgreSQL Global Development Group
+
+tests += {
+  'name': 'postmaster',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_connection_limits.pl',
+    ],
+  },
+}
diff --git a/src/test/postmaster/t/001_connection_limits.pl b/src/test/postmaster/t/001_connection_limits.pl
new file mode 100644
index 0000000000..f50aae4949
--- /dev/null
+++ b/src/test/postmaster/t/001_connection_limits.pl
@@ -0,0 +1,79 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# Test connection limits, i.e. max_connections, reserved_connections
+# and superuser_reserved_connections.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize the server with specific low connection limits
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->append_conf('postgresql.conf', "max_connections = 6");
+$node->append_conf('postgresql.conf', "reserved_connections = 2");
+$node->append_conf('postgresql.conf', "superuser_reserved_connections = 1");
+$node->append_conf('postgresql.conf', "log_connections = on");
+$node->start;
+
+$node->safe_psql(
+	'postgres', qq{
+CREATE USER regress_regular LOGIN;
+CREATE USER regress_reserved LOGIN;
+GRANT pg_use_reserved_connections TO regress_reserved;
+CREATE USER regress_superuser LOGIN SUPERUSER;
+});
+
+# With the limits we set in postgresql.conf, we can establish:
+# - 3 connections for any user with no special privileges
+# - 2 more connections for users belonging to "pg_use_reserved_connections"
+# - 1 more connection for superuser
+
+sub background_psql_as_user
+{
+	my $user = shift;
+
+	return $node->background_psql(
+		'postgres',
+		on_error_die => 1,
+		extra_params => [ '-U', $user ]);
+}
+
+my @sessions = ();
+
+push(@sessions, background_psql_as_user('regress_regular'));
+push(@sessions, background_psql_as_user('regress_regular'));
+push(@sessions, background_psql_as_user('regress_regular'));
+$node->connect_fails(
+	"dbname=postgres user=regress_regular",
+	"reserved_connections limit",
+	expected_stderr =>
+	  qr/FATAL:  remaining connection slots are reserved for roles with privileges of the "pg_use_reserved_connections" role/
+);
+
+push(@sessions, background_psql_as_user('regress_reserved'));
+push(@sessions, background_psql_as_user('regress_reserved'));
+$node->connect_fails(
+	"dbname=postgres user=regress_regular",
+	"reserved_connections limit",
+	expected_stderr =>
+	  qr/FATAL:  remaining connection slots are reserved for roles with the SUPERUSER attribute/
+);
+
+push(@sessions, background_psql_as_user('regress_superuser'));
+$node->connect_fails(
+	"dbname=postgres user=regress_superuser",
+	"superuser_reserved_connections limit",
+	expected_stderr => qr/FATAL:  sorry, too many clients already/);
+
+# TODO: test that query cancellation is still possible
+
+foreach my $session (@sessions)
+{
+	$session->quit;
+}
+
+done_testing();
-- 
2.39.5



  [text/x-patch] v6-0002-Add-test-for-dead-end-backends.patch (5.2K, ../../[email protected]/3-v6-0002-Add-test-for-dead-end-backends.patch)
  download | inline diff:
From 5fe25211724859bfa29bff73ac780322ab95181c Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Tue, 8 Oct 2024 00:54:33 +0300
Subject: [PATCH v6 2/3] Add test for dead-end backends

The code path for launching a dead-end backend because we're out of
slots was not covered by any tests, so add one. (Some tests did hit
the case of launching a dead-end backend because the server is still
starting up, though, so the gap in our test coverage wasn't as big as
it sounds.)

Reviewed-by: Andres Freund <[email protected]>
Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/test/perl/PostgreSQL/Test/Cluster.pm      | 78 +++++++++++++++++++
 .../postmaster/t/001_connection_limits.pl     | 42 +++++++++-
 2 files changed, 119 insertions(+), 1 deletion(-)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 30857f34bf..63c25eeb83 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -104,6 +104,7 @@ use File::Path qw(rmtree mkpath);
 use File::Spec;
 use File::stat qw(stat);
 use File::Temp ();
+use IO::Socket::INET;
 use IPC::Run;
 use PostgreSQL::Version;
 use PostgreSQL::Test::RecursiveCopy;
@@ -291,6 +292,83 @@ sub connstr
 
 =pod
 
+=item $node->raw_connect()
+
+Open a raw TCP or Unix domain socket connection to the server. This is
+used by low-level protocol and connection limit tests.
+
+=cut
+
+sub raw_connect
+{
+	my ($self) = @_;
+	my $pgport = $self->port;
+	my $pghost = $self->host;
+
+	my $socket;
+	if ($PostgreSQL::Test::Utils::use_unix_sockets)
+	{
+		require IO::Socket::UNIX;
+		my $path = "$pghost/.s.PGSQL.$pgport";
+
+		$socket = IO::Socket::UNIX->new(
+			Type => SOCK_STREAM(),
+			Peer => $path,
+		) or die "Cannot create socket - $IO::Socket::errstr\n";
+	}
+	else
+	{
+		$socket = IO::Socket::INET->new(
+			PeerHost => $pghost,
+			PeerPort => $pgport,
+			Proto => 'tcp'
+		) or die "Cannot create socket - $IO::Socket::errstr\n";
+	}
+	return $socket;
+}
+
+=pod
+
+=item $node->raw_connect_works()
+
+Check if raw_connect() function works on this platform. This should
+be called to SKIP any tests that require raw_connect().
+
+This tries to connect to the server, to test whether it works or not,,
+so the server is up and running. Otherwise this can return 0 even if
+there's nothing wrong with raw_connect() itself.
+
+Notably, raw_connect() does not work on Unix domain sockets on
+Strawberry perl 5.26.3.1 on Windows, which we use in Cirrus CI images
+as of this writing. It dies with "not implemented on this
+architecture".
+
+=cut
+
+sub raw_connect_works
+{
+	my ($self) = @_;
+
+	# If we're using Unix domain sockets, we need a working
+	# IO::Socket::UNIX implementation.
+	if ($PostgreSQL::Test::Utils::use_unix_sockets)
+	{
+		diag "checking if IO::Socket::UNIX works";
+		eval {
+			my $sock = $self->raw_connect();
+			$sock->close();
+		};
+		if ($@ =~ /not implemented/)
+		{
+			diag "IO::Socket::UNIX does not work: $@";
+			return 0;
+		}
+	}
+	return 1
+}
+
+=pod
+
 =item $node->group_access()
 
 Does the data dir allow group access?
diff --git a/src/test/postmaster/t/001_connection_limits.pl b/src/test/postmaster/t/001_connection_limits.pl
index f50aae4949..f8d24bcf24 100644
--- a/src/test/postmaster/t/001_connection_limits.pl
+++ b/src/test/postmaster/t/001_connection_limits.pl
@@ -43,6 +43,7 @@ sub background_psql_as_user
 }
 
 my @sessions = ();
+my @raw_connections = ();
 
 push(@sessions, background_psql_as_user('regress_regular'));
 push(@sessions, background_psql_as_user('regress_regular'));
@@ -69,11 +70,50 @@ $node->connect_fails(
 	"superuser_reserved_connections limit",
 	expected_stderr => qr/FATAL:  sorry, too many clients already/);
 
-# TODO: test that query cancellation is still possible
+# We can still open TCP (or Unix domain socket) connections, but
+# beyond a certain number (roughly 2x max_connections), they will be
+# "dead-end backends".
+SKIP:
+{
+	skip "this test requies working raw_connect()" unless $node->raw_connect_works();
+
+	for (my $i = 0; $i <= 20; $i++)
+	{
+		my $sock = $node->raw_connect();
+
+		# On a busy system, the server might reject connections if
+		# postmaster cannot accept() them fast enough. The exact limit
+		# and behavior depends on the platform. To make this reliable,
+		# we attempt SSL negotiation on each connection before opening
+		# next one. The server will reject the SSL negotations, but
+		# when it does so, we know that the backend has been launched
+		# and we should be able to open another connection.
+
+		# SSLRequest packet consists of packet length followed by
+		# NEGOTIATE_SSL_CODE.
+		my $negotiate_ssl_code = pack("Nnn", 8, 1234, 5679);
+		my $sent = $sock->send($negotiate_ssl_code);
+
+		# Read reply. We expect the server to reject it with 'N'
+		my $reply = "";
+		$sock->recv($reply, 1);
+		is($reply, "N", "dead-end connection $i");
 
+		push(@raw_connections, $sock);
+	}
+}
+
+# TODO: test that query cancellation is still possible. A dead-end
+# backend can process a query cancellation packet.
+
+# Clean up
 foreach my $session (@sessions)
 {
 	$session->quit;
 }
+foreach my $socket (@raw_connections)
+{
+	$socket->close();
+}
 
 done_testing();
-- 
2.39.5



^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-10-08 17:30  Nazir Bilal Yavuz <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  2 siblings, 0 replies; 39+ messages in thread

From: Nazir Bilal Yavuz @ 2024-10-08 17:30 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Dagfinn Ilmari Mannsåker <[email protected]>; Thomas Munro <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers

Hi,

On Tue, 8 Oct 2024 at 00:55, Heikki Linnakangas <[email protected]> wrote:
>
> On 05/10/2024 22:51, Dagfinn Ilmari Mannsåker wrote:
> > Heikki Linnakangas <[email protected]> writes:
> >> Sadly Windows' IO::Socket::UNIX hasn't been implemented on Windows (or
> >> at least on this perl distribution we're using in Cirrus CI):
> >>
> >> Socket::pack_sockaddr_un not implemented on this architecture at
> >> C:/strawberry/5.26.3.1/perl/lib/Socket.pm line 872.
> >>
> >> so I'll have to disable this test on Windows anyway.
> >
> > Socket version 2.028 (included in Perl 5.32) provides pack_sockaddr_un()
> > on Windows, so that can be fixed by bumping the Perl version in
> > https://github.com/anarazel/pg-vm-images/blob/main/packer/windows.pkr.hcl
> > to something more modern (such as 5.40.0.1), and only skipping the test
> > if on Windows if Socket is too old.
> >
> > The decision to use 5.26 seems to come from the initial creation of the
> > CI images in 2021 (when 5.34 was current), with the comment «newer
> > versions don't currently work correctly for plperl».  That claim is
> > worth revisiting, and fixing if it's still the case.
>
> Yeah, it would be nice to update it. I wonder if commit
> 341f4e002d461a3c5513cb864490cddae2b43a64 fixed whatever the problem was.

The perl version in Windows CI image is bumped to 5.40.0.1 [1]. So,
the related test passes on Windows now [2].

[1] https://github.com/anarazel/pg-vm-images/commit/cbd5d46f2fb7b28efb126ddac64d12711247dfa8
[2] https://cirrus-ci.com/task/5682393120505856?logs=check_world#L241

--
Regards,
Nazir Bilal Yavuz
Microsoft






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-10-09 20:40  Heikki Linnakangas <[email protected]>
  parent: Andres Freund <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Heikki Linnakangas @ 2024-10-09 20:40 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers

I pushed the first three patches, with the new test and one of the small 
refactoring patches. Thanks for all the comments so far! Here is a new 
version of the remaining patches.

Lots of little cleanups and changes here and there since the last 
versions, but the notable bigger changes are:

- There is now a BackendTypeMask datatype, so that if you try to mix up 
bitmasks and plain BackendType values, the compiler will complain.

- pmchild.c has been rewritten per feedback, so that the "pools" of 
PMChild structs are more explicit. The size of each pool is only stated 
once, whereas before the same logic was duplicated in 
MaxLivePostmasterChildren() which calculates the number of slots and in 
InitPostmasterChildSlots() which allocates them.

- In PostmasterStateMachine(), I combined the code to handle 
PM_STOP_BACKENDS and PM_WAIT_BACKENDS. They are essentially the same 
state, except that PM_STOP_BACKENDS first sends the signal to all the 
child processes that it will then wait for. They both needed to build 
the same bitmask of processes to signal or wait for; this eliminates the 
duplication.

Responses to some specific comments below:

On 10/09/2024 20:53, Andres Freund wrote:
> On 2024-08-12 12:55:00 +0300, Heikki Linnakangas wrote:
>> @@ -2864,6 +2777,8 @@ PostmasterStateMachine(void)
>>   	 */
>>   	if (pmState == PM_STOP_BACKENDS)
>>   	{
>> +		uint32		targetMask;
>> +
>>   		/*
>>   		 * Forget any pending requests for background workers, since we're no
>>   		 * longer willing to launch any new workers.  (If additional requests
>> @@ -2871,29 +2786,27 @@ PostmasterStateMachine(void)
>>   		 */
>>   		ForgetUnstartedBackgroundWorkers();
>>   
>> -		/* Signal all backend children except walsenders and dead-end backends */
>> -		SignalSomeChildren(SIGTERM,
>> -						   BACKEND_TYPE_ALL & ~(1 << B_WAL_SENDER | 1 << B_DEAD_END_BACKEND));
>> +		/* Signal all backend children except walsenders */
>> +		/* dead-end children are not signalled yet */
>> +		targetMask = (1 << B_BACKEND);
>> +		targetMask |= (1 << B_BG_WORKER);
>> +
>>   		/* and the autovac launcher too */
>> -		if (AutoVacPID != 0)
>> -			signal_child(AutoVacPID, SIGTERM);
>> +		targetMask |= (1 << B_AUTOVAC_LAUNCHER);
>>   		/* and the bgwriter too */
>> -		if (BgWriterPID != 0)
>> -			signal_child(BgWriterPID, SIGTERM);
>> +		targetMask |= (1 << B_BG_WRITER);
>>   		/* and the walwriter too */
>> -		if (WalWriterPID != 0)
>> -			signal_child(WalWriterPID, SIGTERM);
>> +		targetMask |= (1 << B_WAL_WRITER);
>>   		/* If we're in recovery, also stop startup and walreceiver procs */
>> -		if (StartupPID != 0)
>> -			signal_child(StartupPID, SIGTERM);
>> -		if (WalReceiverPID != 0)
>> -			signal_child(WalReceiverPID, SIGTERM);
>> -		if (WalSummarizerPID != 0)
>> -			signal_child(WalSummarizerPID, SIGTERM);
>> -		if (SlotSyncWorkerPID != 0)
>> -			signal_child(SlotSyncWorkerPID, SIGTERM);
>> +		targetMask |= (1 << B_STARTUP);
>> +		targetMask |= (1 << B_WAL_RECEIVER);
>> +
>> +		targetMask |= (1 << B_WAL_SUMMARIZER);
>> +		targetMask |= (1 << B_SLOTSYNC_WORKER);
>>   		/* checkpointer, archiver, stats, and syslogger may continue for now */
>>   
>> +		SignalSomeChildren(SIGTERM, targetMask);
>> +
>>   		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
>>   		pmState = PM_WAIT_BACKENDS;
>>   	}
> 
> I think this might now omit shutting down at least autovac workers, which
> afaict previously were included.

Fixed. And this code now also explicitly lists backend types that are 
*not* signaled, and there is an assertion that all backend types are 
accounted for. Thanks to that, if someone adds a new backend type, they 
will be forced to decide if the new backend type should be signaled here 
or not. That's not quite table-driven like you suggested, but it's 
closer to that.

>> > > +/* Return the appropriate free-list for the given backend type */
>> > > +static dlist_head *
>> > > +GetFreeList(BackendType btype)
>> > > +{
>> > > +	switch (btype)
>> > > +	{
>> > > +		case B_BACKEND:
>> > > +		case B_BG_WORKER:
>> > > +		case B_WAL_SENDER:
>> > > +		case B_SLOTSYNC_WORKER:
>> > > +			return &freeBackendList;
>> >
>> > Maybe a daft question - but why are all of these in the same list? Sure,
>> > they're essentially all full backends, but they're covered by different GUCs?
>>
>> No reason. No particular reason they should *not* share the same list either
>> though.
> 
> 
> Aren't they controlled by distinct connection limits? Isn't there a danger
> that we could use up entries and fail connections due to that, despite not
> actually being above the limit?

Yes, this was in fact just wrong. Slotsync worker is a special process 
and should not be allocated from the same pool as backends, and 
previously it was not. And indeed bgworkers have a separate connection 
limit, and should have a separate pool. Fixed.

>> > Tangential: Why do we need a freelist for these and why do we choose a random
>> > pgproc for these instead of assigning one statically?
>> >
>> > Background: I'd like to not provide AIO workers with "bounce buffers" (for IO
>> > of buffers that can't be done in-place, like writes when checksums are
>> > enabled). The varying proc numbers make that harder than it'd have to be...
>>
>> Yeah, we can make these fixed.
> 
> Cool.

All the aux processes now have their own "free list" or pool of a single 
entry now, so after postmaster startup, their child_slot never changes. 
They're still not constants across server startups though, because the 
numbering depends on max_connections etc. If it matters, we could 
allocate the aux process slot numbers first, so that they would be truly 
static, but I didn't do that.

I did not change how ProcNumbers are allocated. They are still separate 
from PMChildSlots.

>> Includes a test for that case where a dead-end backend previously kept
>> the server from shutting down.
> 
> The test hardcodes timeouts, I think we've largely come to regret that when we
> did. Should probably just be a multiplier based on
> PostgreSQL::Test::Utils::timeout_default?

Hmm, what the test really needs is that the authentication_timeout is >> 
the "pg_ctl stop" timeout. The idea is that if a dead-end backend isn't 
killed, but needs to wait for authentication_timeout to expire, the test 
should fail. The default pg_ctl stop timeout is actually only 60 s, 
while PostgreSQL::Test::Utils::timeout_default is 180 s.

I changed authentication_timeout in the test to 
PostgreSQL::Test::Utils::timeout_default, but also added an explicit 
timeout to "$node->stop", and set that to authentication_timeout / 2. 
That ensures that the stop timeout is smaller than 
authentication_timeout, regardless of 
PostgreSQL::Test::Utils::timeout_default or the default pg_ctl timeout.

>>  	/* Construct a process name for log message */
>> +
>> +	/*
>> +	 * FIXME: use GetBackendTypeDesc here? How does the localization of that
>> +	 * work?
>> +	 */
>>  	if (bp->bkend_type == B_DEAD_END_BACKEND)
>>  	{
>>  		procname = _("dead end backend");
> 
> Might be worth having a version of GetBackendTypeDesc() that returns a
> translated string?

I made the translation of GetBackendTypeDesc() work the same as for 
error_severity(int elevel).

-- 
Heikki Linnakangas
Neon (https://neon.tech)


Attachments:

  [text/x-patch] 0001-Replace-postmaster.c-s-own-backend-type-codes-with-B.patch (22.7K, ../../[email protected]/2-0001-Replace-postmaster.c-s-own-backend-type-codes-with-B.patch)
  download | inline diff:
From 80c098bd242ab6550be4ebdf9e7bd66c9d32bc55 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Wed, 9 Oct 2024 20:30:16 +0300
Subject: [PATCH 1/4] Replace postmaster.c's own backend type codes with
 BackendType

Introduce a separate BackendType for dead-end children, so that we
don't need a separate dead_end flag.

Reviewed-by: Andres Freund <[email protected]>
Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/backend/postmaster/launch_backend.c |   1 +
 src/backend/postmaster/postmaster.c     | 234 ++++++++++++++----------
 src/backend/utils/activity/pgstat_io.c  |   3 +
 src/backend/utils/init/miscinit.c       |  43 +++--
 src/include/miscadmin.h                 |   1 +
 src/tools/pgindent/typedefs.list        |   1 +
 6 files changed, 167 insertions(+), 116 deletions(-)

diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index 0ae23fdf55..b0b91dc97f 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -182,6 +182,7 @@ static child_process_kind child_process_kinds[] = {
 	[B_INVALID] = {"invalid", NULL, false},
 
 	[B_BACKEND] = {"backend", BackendMain, true},
+	[B_DEAD_END_BACKEND] = {"dead-end backend", BackendMain, true},
 	[B_AUTOVAC_LAUNCHER] = {"autovacuum launcher", AutoVacLauncherMain, true},
 	[B_AUTOVAC_WORKER] = {"autovacuum worker", AutoVacWorkerMain, true},
 	[B_BG_WORKER] = {"bgworker", BackgroundWorkerMain, true},
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 85fd24e828..14a0ce91b2 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -129,15 +129,71 @@
 
 
 /*
- * Possible types of a backend. Beyond being the possible bkend_type values in
- * struct bkend, these are OR-able request flag bits for SignalSomeChildren()
- * and CountChildren().
+ * CountChildren and SignalChildren take a bitmask argument to represent
+ * BackendTypes to count or signal.  Define a separate type and functions to
+ * work with the bitmasks, to avoid accidentally passing a plain BackendType
+ * in place of a bitmask or vice versa.
  */
-#define BACKEND_TYPE_NORMAL		0x0001	/* normal backend */
-#define BACKEND_TYPE_AUTOVAC	0x0002	/* autovacuum worker process */
-#define BACKEND_TYPE_WALSND		0x0004	/* walsender process */
-#define BACKEND_TYPE_BGWORKER	0x0008	/* bgworker process */
-#define BACKEND_TYPE_ALL		0x000F	/* OR of all the above */
+typedef struct
+{
+	uint32		mask;
+} BackendTypeMask;
+
+StaticAssertDecl(BACKEND_NUM_TYPES < 32, "too many backend types for uint32");
+
+static const BackendTypeMask BTYPE_MASK_ALL = {(1 << BACKEND_NUM_TYPES) - 1};
+#if 0							/* unused */
+static const BackendTypeMask BTYPE_MASK_NONE = {0};
+#endif
+
+static inline BackendTypeMask
+btmask(BackendType t)
+{
+	BackendTypeMask mask = {.mask = 1 << t};
+
+	return mask;
+}
+
+#if 0							/* unused */
+static inline BackendTypeMask
+btmask_add(BackendTypeMask mask, BackendType t)
+{
+	mask.mask |= 1 << t;
+	return mask;
+}
+#endif
+
+static inline BackendTypeMask
+btmask_del(BackendTypeMask mask, BackendType t)
+{
+	mask.mask &= ~(1 << t);
+	return mask;
+}
+
+static inline BackendTypeMask
+btmask_all_except(BackendType t)
+{
+	BackendTypeMask mask = BTYPE_MASK_ALL;
+
+	mask = btmask_del(mask, t);
+	return mask;
+}
+
+static inline BackendTypeMask
+btmask_all_except2(BackendType t1, BackendType t2)
+{
+	BackendTypeMask mask = BTYPE_MASK_ALL;
+
+	mask = btmask_del(mask, t1);
+	mask = btmask_del(mask, t2);
+	return mask;
+}
+
+static inline bool
+btmask_contains(BackendTypeMask mask, BackendType t)
+{
+	return (mask.mask & (1 << t)) != 0;
+}
 
 /*
  * List of active backends (or child processes anyway; we don't actually
@@ -148,7 +204,7 @@
  * As shown in the above set of backend types, this list includes not only
  * "normal" client sessions, but also autovacuum workers, walsenders, and
  * background workers.  (Note that at the time of launch, walsenders are
- * labeled BACKEND_TYPE_NORMAL; we relabel them to BACKEND_TYPE_WALSND
+ * labeled B_BACKEND; we relabel them to B_WAL_SENDER
  * upon noticing they've changed their PMChildFlags entry.  Hence that check
  * must be done before any operation that needs to distinguish walsenders
  * from normal backends.)
@@ -157,7 +213,7 @@
  * the purpose of sending a friendly rejection message to a would-be client.
  * We must track them because they are attached to shared memory, but we know
  * they will never become live backends.  dead_end children are not assigned a
- * PMChildSlot.  dead_end children have bkend_type NORMAL.
+ * PMChildSlot.  dead_end children have bkend_type B_DEAD_END_BACKEND.
  *
  * "Special" children such as the startup, bgwriter, autovacuum launcher, and
  * slot sync worker tasks are not in this list.  They are tracked via StartupPID
@@ -169,8 +225,7 @@ typedef struct bkend
 {
 	pid_t		pid;			/* process id of backend */
 	int			child_slot;		/* PMChildSlot for this backend, if any */
-	int			bkend_type;		/* child process flavor, see above */
-	bool		dead_end;		/* is it going to send an error and quit? */
+	BackendType bkend_type;		/* child process flavor, see above */
 	RegisteredBgWorker *rw;		/* bgworker info, if this is a bgworker */
 	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
 	dlist_node	elem;			/* list link in BackendList */
@@ -407,15 +462,12 @@ static void ExitPostmaster(int status) pg_attribute_noreturn();
 static int	ServerLoop(void);
 static int	BackendStartup(ClientSocket *client_sock);
 static void report_fork_failure_to_client(ClientSocket *client_sock, int errnum);
-static CAC_state canAcceptConnections(int backend_type);
+static CAC_state canAcceptConnections(BackendType backend_type);
 static void signal_child(pid_t pid, int signal);
 static void sigquit_child(pid_t pid);
-static bool SignalSomeChildren(int signal, int target);
+static bool SignalChildren(int signal, BackendTypeMask targetMask);
 static void TerminateChildren(int signal);
-
-#define SignalChildren(sig)			   SignalSomeChildren(sig, BACKEND_TYPE_ALL)
-
-static int	CountChildren(int target);
+static int	CountChildren(BackendTypeMask targetMask);
 static Backend *assign_backendlist_entry(void);
 static void LaunchMissingBackgroundProcesses(void);
 static void maybe_start_bgworkers(void);
@@ -1754,12 +1806,12 @@ ServerLoop(void)
 
 /*
  * canAcceptConnections --- check to see if database state allows connections
- * of the specified type.  backend_type can be BACKEND_TYPE_NORMAL,
- * BACKEND_TYPE_AUTOVAC, or BACKEND_TYPE_BGWORKER.  (Note that we don't yet
- * know whether a NORMAL connection might turn into a walsender.)
+ * of the specified type.  backend_type can be B_BACKEND, B_AUTOVAC_WORKER, or
+ * B_BG_WORKER.  (Note that we don't yet know whether a normal B_BACKEND
+ * connection might turn into a walsender.)
  */
 static CAC_state
-canAcceptConnections(int backend_type)
+canAcceptConnections(BackendType backend_type)
 {
 	CAC_state	result = CAC_OK;
 
@@ -1770,7 +1822,7 @@ canAcceptConnections(int backend_type)
 	 * bgworker_should_start_now() decided whether the DB state allows them.
 	 */
 	if (pmState != PM_RUN && pmState != PM_HOT_STANDBY &&
-		backend_type != BACKEND_TYPE_BGWORKER)
+		backend_type != B_BG_WORKER)
 	{
 		if (Shutdown > NoShutdown)
 			return CAC_SHUTDOWN;	/* shutdown is pending */
@@ -1787,7 +1839,7 @@ canAcceptConnections(int backend_type)
 	 * "Smart shutdown" restrictions are applied only to normal connections,
 	 * not to autovac workers or bgworkers.
 	 */
-	if (!connsAllowed && backend_type == BACKEND_TYPE_NORMAL)
+	if (!connsAllowed && backend_type == B_BACKEND)
 		return CAC_SHUTDOWN;	/* shutdown is pending */
 
 	/*
@@ -1802,7 +1854,7 @@ canAcceptConnections(int backend_type)
 	 * The limit here must match the sizes of the per-child-process arrays;
 	 * see comments for MaxLivePostmasterChildren().
 	 */
-	if (CountChildren(BACKEND_TYPE_ALL) >= MaxLivePostmasterChildren())
+	if (CountChildren(btmask_all_except(B_DEAD_END_BACKEND)) >= MaxLivePostmasterChildren())
 		result = CAC_TOOMANY;
 
 	return result;
@@ -1971,7 +2023,7 @@ process_pm_reload_request(void)
 		ereport(LOG,
 				(errmsg("received SIGHUP, reloading configuration files")));
 		ProcessConfigFile(PGC_SIGHUP);
-		SignalChildren(SIGHUP);
+		SignalChildren(SIGHUP, btmask_all_except(B_DEAD_END_BACKEND));
 		if (StartupPID != 0)
 			signal_child(StartupPID, SIGHUP);
 		if (BgWriterPID != 0)
@@ -2389,7 +2441,7 @@ process_pm_child_exit(void)
 				 * Waken walsenders for the last time. No regular backends
 				 * should be around anymore.
 				 */
-				SignalChildren(SIGUSR2);
+				SignalChildren(SIGUSR2, btmask(B_WAL_SENDER));
 
 				pmState = PM_SHUTDOWN_2;
 			}
@@ -2555,23 +2607,19 @@ CleanupBackend(Backend *bp,
 			   int exitstatus)	/* child's exit status. */
 {
 	char		namebuf[MAXPGPATH];
-	char	   *procname;
+	const char *procname;
 	bool		crashed = false;
 	bool		logged = false;
 
-	/* Construct a process name for log message */
-	if (bp->dead_end)
-	{
-		procname = _("dead end backend");
-	}
-	else if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
+	/* Construct a process name for the log message */
+	if (bp->bkend_type == B_BG_WORKER)
 	{
 		snprintf(namebuf, MAXPGPATH, _("background worker \"%s\""),
 				 bp->rw->rw_worker.bgw_type);
 		procname = namebuf;
 	}
 	else
-		procname = _("server process");
+		procname = _(GetBackendTypeDesc(bp->bkend_type));
 
 	/*
 	 * If a backend dies in an ugly way then we must signal all other backends
@@ -2603,7 +2651,7 @@ CleanupBackend(Backend *bp,
 	 * If the process attached to shared memory, check that it detached
 	 * cleanly.
 	 */
-	if (!bp->dead_end)
+	if (bp->bkend_type != B_DEAD_END_BACKEND)
 	{
 		if (!ReleasePostmasterChildSlot(bp->child_slot))
 		{
@@ -2636,7 +2684,7 @@ CleanupBackend(Backend *bp,
 	 * If it was a background worker, also update its RegisteredBgWorker
 	 * entry.
 	 */
-	if (bp->bkend_type == BACKEND_TYPE_BGWORKER)
+	if (bp->bkend_type == B_BG_WORKER)
 	{
 		RegisteredBgWorker *rw = bp->rw;
 
@@ -2864,7 +2912,7 @@ PostmasterStateMachine(void)
 			 * This state ends when we have no normal client backends running.
 			 * Then we're ready to stop other children.
 			 */
-			if (CountChildren(BACKEND_TYPE_NORMAL) == 0)
+			if (CountChildren(btmask(B_BACKEND)) == 0)
 				pmState = PM_STOP_BACKENDS;
 		}
 	}
@@ -2883,9 +2931,8 @@ PostmasterStateMachine(void)
 		 */
 		ForgetUnstartedBackgroundWorkers();
 
-		/* Signal all backend children except walsenders */
-		SignalSomeChildren(SIGTERM,
-						   BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND);
+		/* Signal all backend children except walsenders and dead-end backends */
+		SignalChildren(SIGTERM, btmask_all_except2(B_WAL_SENDER, B_DEAD_END_BACKEND));
 		/* and the autovac launcher too */
 		if (AutoVacPID != 0)
 			signal_child(AutoVacPID, SIGTERM);
@@ -2927,7 +2974,7 @@ PostmasterStateMachine(void)
 		 * here. Walsenders and archiver are also disregarded, they will be
 		 * terminated later after writing the checkpoint record.
 		 */
-		if (CountChildren(BACKEND_TYPE_ALL - BACKEND_TYPE_WALSND) == 0 &&
+		if (CountChildren(btmask_all_except2(B_WAL_SENDER, B_DEAD_END_BACKEND)) == 0 &&
 			StartupPID == 0 &&
 			WalReceiverPID == 0 &&
 			WalSummarizerPID == 0 &&
@@ -2985,7 +3032,7 @@ PostmasterStateMachine(void)
 					pmState = PM_WAIT_DEAD_END;
 
 					/* Kill the walsenders and archiver too */
-					SignalChildren(SIGQUIT);
+					SignalChildren(SIGQUIT, btmask_all_except(B_DEAD_END_BACKEND));
 					if (PgArchPID != 0)
 						signal_child(PgArchPID, SIGQUIT);
 				}
@@ -3001,7 +3048,7 @@ PostmasterStateMachine(void)
 		 * left by now anyway; what we're really waiting for is walsenders and
 		 * archiver.
 		 */
-		if (PgArchPID == 0 && CountChildren(BACKEND_TYPE_ALL) == 0)
+		if (PgArchPID == 0 && CountChildren(btmask_all_except(B_DEAD_END_BACKEND)) == 0)
 		{
 			pmState = PM_WAIT_DEAD_END;
 		}
@@ -3299,11 +3346,10 @@ sigquit_child(pid_t pid)
 }
 
 /*
- * Send a signal to the targeted children (but NOT special children;
- * dead_end children are never signaled, either).
+ * Send a signal to the targeted children (but NOT special children).
  */
 static bool
-SignalSomeChildren(int signal, int target)
+SignalChildren(int signal, BackendTypeMask targetMask)
 {
 	dlist_iter	iter;
 	bool		signaled = false;
@@ -3312,30 +3358,24 @@ SignalSomeChildren(int signal, int target)
 	{
 		Backend    *bp = dlist_container(Backend, elem, iter.cur);
 
-		if (bp->dead_end)
-			continue;
-
 		/*
-		 * Since target == BACKEND_TYPE_ALL is the most common case, we test
-		 * it first and avoid touching shared memory for every child.
+		 * If we need to distinguish between B_BACKEND and B_WAL_SENDER, check
+		 * if any B_BACKEND backends have recently announced that they are
+		 * actually WAL senders.
 		 */
-		if (target != BACKEND_TYPE_ALL)
+		if (btmask_contains(targetMask, B_WAL_SENDER) != btmask_contains(targetMask, B_BACKEND) &&
+			bp->bkend_type == B_BACKEND)
 		{
-			/*
-			 * Assign bkend_type for any recently announced WAL Sender
-			 * processes.
-			 */
-			if (bp->bkend_type == BACKEND_TYPE_NORMAL &&
-				IsPostmasterChildWalSender(bp->child_slot))
-				bp->bkend_type = BACKEND_TYPE_WALSND;
-
-			if (!(target & bp->bkend_type))
-				continue;
+			if (IsPostmasterChildWalSender(bp->child_slot))
+				bp->bkend_type = B_WAL_SENDER;
 		}
 
+		if (!btmask_contains(targetMask, bp->bkend_type))
+			continue;
+
 		ereport(DEBUG4,
-				(errmsg_internal("sending signal %d to process %d",
-								 signal, (int) bp->pid)));
+				(errmsg_internal("sending signal %d to %s process %d",
+								 signal, GetBackendTypeDesc(bp->bkend_type), (int) bp->pid)));
 		signal_child(bp->pid, signal);
 		signaled = true;
 	}
@@ -3349,7 +3389,7 @@ SignalSomeChildren(int signal, int target)
 static void
 TerminateChildren(int signal)
 {
-	SignalChildren(signal);
+	SignalChildren(signal, btmask_all_except(B_DEAD_END_BACKEND));
 	if (StartupPID != 0)
 	{
 		signal_child(StartupPID, signal);
@@ -3402,22 +3442,27 @@ BackendStartup(ClientSocket *client_sock)
 	}
 
 	/* Pass down canAcceptConnections state */
-	startup_data.canAcceptConnections = canAcceptConnections(BACKEND_TYPE_NORMAL);
-	bn->dead_end = (startup_data.canAcceptConnections != CAC_OK);
+	startup_data.canAcceptConnections = canAcceptConnections(B_BACKEND);
 	bn->rw = NULL;
 
 	/*
 	 * Unless it's a dead_end child, assign it a child slot number
 	 */
-	if (!bn->dead_end)
+	if (startup_data.canAcceptConnections == CAC_OK)
+	{
+		bn->bkend_type = B_BACKEND; /* Can change later to B_WAL_SENDER */
 		bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
+	}
 	else
+	{
+		bn->bkend_type = B_DEAD_END_BACKEND;
 		bn->child_slot = 0;
+	}
 
 	/* Hasn't asked to be notified about any bgworkers yet */
 	bn->bgworker_notify = false;
 
-	pid = postmaster_child_launch(B_BACKEND,
+	pid = postmaster_child_launch(bn->bkend_type,
 								  (char *) &startup_data, sizeof(startup_data),
 								  client_sock);
 	if (pid < 0)
@@ -3425,7 +3470,7 @@ BackendStartup(ClientSocket *client_sock)
 		/* in parent, fork failed */
 		int			save_errno = errno;
 
-		if (!bn->dead_end)
+		if (bn->child_slot != 0)
 			(void) ReleasePostmasterChildSlot(bn->child_slot);
 		pfree(bn);
 		errno = save_errno;
@@ -3437,7 +3482,8 @@ BackendStartup(ClientSocket *client_sock)
 
 	/* in parent, successful fork */
 	ereport(DEBUG2,
-			(errmsg_internal("forked new backend, pid=%d socket=%d",
+			(errmsg_internal("forked new %s, pid=%d socket=%d",
+							 GetBackendTypeDesc(bn->bkend_type),
 							 (int) pid, (int) client_sock->sock)));
 
 	/*
@@ -3445,7 +3491,6 @@ BackendStartup(ClientSocket *client_sock)
 	 * of backends.
 	 */
 	bn->pid = pid;
-	bn->bkend_type = BACKEND_TYPE_NORMAL;	/* Can change later to WALSND */
 	dlist_push_head(&BackendList, &bn->elem);
 
 	return STATUS_OK;
@@ -3679,11 +3724,10 @@ dummy_handler(SIGNAL_ARGS)
 }
 
 /*
- * Count up number of child processes of specified types (dead_end children
- * are always excluded).
+ * Count up number of child processes of specified types.
  */
 static int
-CountChildren(int target)
+CountChildren(BackendTypeMask targetMask)
 {
 	dlist_iter	iter;
 	int			cnt = 0;
@@ -3692,27 +3736,21 @@ CountChildren(int target)
 	{
 		Backend    *bp = dlist_container(Backend, elem, iter.cur);
 
-		if (bp->dead_end)
-			continue;
-
 		/*
-		 * Since target == BACKEND_TYPE_ALL is the most common case, we test
-		 * it first and avoid touching shared memory for every child.
+		 * If we need to distinguish between B_BACKEND and B_WAL_SENDER, check
+		 * if any B_BACKEND backends have recently announced that they are
+		 * actually WAL senders.
 		 */
-		if (target != BACKEND_TYPE_ALL)
+		if (btmask_contains(targetMask, B_WAL_SENDER) != btmask_contains(targetMask, B_BACKEND) &&
+			bp->bkend_type == B_BACKEND)
 		{
-			/*
-			 * Assign bkend_type for any recently announced WAL Sender
-			 * processes.
-			 */
-			if (bp->bkend_type == BACKEND_TYPE_NORMAL &&
-				IsPostmasterChildWalSender(bp->child_slot))
-				bp->bkend_type = BACKEND_TYPE_WALSND;
-
-			if (!(target & bp->bkend_type))
-				continue;
+			if (IsPostmasterChildWalSender(bp->child_slot))
+				bp->bkend_type = B_WAL_SENDER;
 		}
 
+		if (!btmask_contains(targetMask, bp->bkend_type))
+			continue;
+
 		cnt++;
 	}
 	return cnt;
@@ -3776,13 +3814,13 @@ StartAutovacuumWorker(void)
 	 * we have to check to avoid race-condition problems during DB state
 	 * changes.
 	 */
-	if (canAcceptConnections(BACKEND_TYPE_AUTOVAC) == CAC_OK)
+	if (canAcceptConnections(B_AUTOVAC_WORKER) == CAC_OK)
 	{
 		bn = (Backend *) palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
 		if (bn)
 		{
-			/* Autovac workers are not dead_end and need a child slot */
-			bn->dead_end = false;
+			/* Autovac workers need a child slot */
+			bn->bkend_type = B_AUTOVAC_WORKER;
 			bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
 			bn->bgworker_notify = false;
 			bn->rw = NULL;
@@ -3790,7 +3828,6 @@ StartAutovacuumWorker(void)
 			bn->pid = StartChildProcess(B_AUTOVAC_WORKER);
 			if (bn->pid > 0)
 			{
-				bn->bkend_type = BACKEND_TYPE_AUTOVAC;
 				dlist_push_head(&BackendList, &bn->elem);
 				/* all OK */
 				return;
@@ -3996,7 +4033,7 @@ assign_backendlist_entry(void)
 	 * only possible failure is CAC_TOOMANY, so we just log an error message
 	 * based on that rather than checking the error code precisely.
 	 */
-	if (canAcceptConnections(BACKEND_TYPE_BGWORKER) != CAC_OK)
+	if (canAcceptConnections(B_BG_WORKER) != CAC_OK)
 	{
 		ereport(LOG,
 				(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
@@ -4014,8 +4051,7 @@ assign_backendlist_entry(void)
 	}
 
 	bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
-	bn->bkend_type = BACKEND_TYPE_BGWORKER;
-	bn->dead_end = false;
+	bn->bkend_type = B_BG_WORKER;
 	bn->bgworker_notify = false;
 
 	return bn;
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index cc2ffc78aa..f9883af2b3 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -330,6 +330,8 @@ pgstat_io_snapshot_cb(void)
 *
 * The following BackendTypes do not participate in the cumulative stats
 * subsystem or do not perform IO on which we currently track:
+* - Dead-end backend because it is not connected to shared memory and
+*   doesn't do any IO
 * - Syslogger because it is not connected to shared memory
 * - Archiver because most relevant archiving IO is delegated to a
 *   specialized command or module
@@ -352,6 +354,7 @@ pgstat_tracks_io_bktype(BackendType bktype)
 	switch (bktype)
 	{
 		case B_INVALID:
+		case B_DEAD_END_BACKEND:
 		case B_ARCHIVER:
 		case B_LOGGER:
 		case B_WAL_RECEIVER:
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index ef60f41b8c..920cf46d66 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -259,60 +259,69 @@ SwitchBackToLocalLatch(void)
 	SetLatch(MyLatch);
 }
 
+/*
+ * Return a human-readable string representation of a BackendType.
+ *
+ * The string is not localized here, but we mark the strings for translation
+ * so that callers can invoke _() on the result.
+ */
 const char *
 GetBackendTypeDesc(BackendType backendType)
 {
-	const char *backendDesc = "unknown process type";
+	const char *backendDesc = gettext_noop("unknown process type");
 
 	switch (backendType)
 	{
 		case B_INVALID:
-			backendDesc = "not initialized";
+			backendDesc = gettext_noop("not initialized");
 			break;
 		case B_ARCHIVER:
-			backendDesc = "archiver";
+			backendDesc = gettext_noop("archiver");
 			break;
 		case B_AUTOVAC_LAUNCHER:
-			backendDesc = "autovacuum launcher";
+			backendDesc = gettext_noop("autovacuum launcher");
 			break;
 		case B_AUTOVAC_WORKER:
-			backendDesc = "autovacuum worker";
+			backendDesc = gettext_noop("autovacuum worker");
 			break;
 		case B_BACKEND:
-			backendDesc = "client backend";
+			backendDesc = gettext_noop("client backend");
+			break;
+		case B_DEAD_END_BACKEND:
+			backendDesc = gettext_noop("dead-end client backend");
 			break;
 		case B_BG_WORKER:
-			backendDesc = "background worker";
+			backendDesc = gettext_noop("background worker");
 			break;
 		case B_BG_WRITER:
-			backendDesc = "background writer";
+			backendDesc = gettext_noop("background writer");
 			break;
 		case B_CHECKPOINTER:
-			backendDesc = "checkpointer";
+			backendDesc = gettext_noop("checkpointer");
 			break;
 		case B_LOGGER:
-			backendDesc = "logger";
+			backendDesc = gettext_noop("logger");
 			break;
 		case B_SLOTSYNC_WORKER:
-			backendDesc = "slotsync worker";
+			backendDesc = gettext_noop("slotsync worker");
 			break;
 		case B_STANDALONE_BACKEND:
-			backendDesc = "standalone backend";
+			backendDesc = gettext_noop("standalone backend");
 			break;
 		case B_STARTUP:
-			backendDesc = "startup";
+			backendDesc = gettext_noop("startup");
 			break;
 		case B_WAL_RECEIVER:
-			backendDesc = "walreceiver";
+			backendDesc = gettext_noop("walreceiver");
 			break;
 		case B_WAL_SENDER:
-			backendDesc = "walsender";
+			backendDesc = gettext_noop("walsender");
 			break;
 		case B_WAL_SUMMARIZER:
-			backendDesc = "walsummarizer";
+			backendDesc = gettext_noop("walsummarizer");
 			break;
 		case B_WAL_WRITER:
-			backendDesc = "walwriter";
+			backendDesc = gettext_noop("walwriter");
 			break;
 	}
 
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index e26d108a47..67e7717d98 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -333,6 +333,7 @@ typedef enum BackendType
 
 	/* Backends and other backend-like processes */
 	B_BACKEND,
+	B_DEAD_END_BACKEND,
 	B_AUTOVAC_LAUNCHER,
 	B_AUTOVAC_WORKER,
 	B_BG_WORKER,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a65e1c07c5..6f98fbc6f5 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -235,6 +235,7 @@ BackendParameters
 BackendStartupData
 BackendState
 BackendType
+BackendTypeMask
 BackgroundWorker
 BackgroundWorkerArray
 BackgroundWorkerHandle
-- 
2.39.5



  [text/x-patch] 0002-Kill-dead-end-children-when-there-s-nothing-else-lef.patch (8.4K, ../../[email protected]/3-0002-Kill-dead-end-children-when-there-s-nothing-else-lef.patch)
  download | inline diff:
From 0e009083262ffca7ad1a6a99dfc03fd3988f76ef Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Wed, 9 Oct 2024 21:28:08 +0300
Subject: [PATCH 2/4] Kill dead-end children when there's nothing else left

Previously, the postmaster would never try to kill dead-end child
processes, even if there were no other processes left. A dead-end
backend will eventually exit, when authentication_timeout expires, but
if a dead-end backend is the only thing that's preventing the server
from shutting down, it seems better to kill it immediately. It's
particularly important, if there was a bug in the early startup code
that prevented a dead-end child from timing out and exiting normally.

Includes a test for that case where a dead-end backend previously
prevented the server from shutting down.

Reviewed-by: Andres Freund <[email protected]>
Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/backend/postmaster/postmaster.c      | 17 ++--
 src/test/perl/PostgreSQL/Test/Cluster.pm | 10 ++-
 src/test/postmaster/meson.build          |  1 +
 src/test/postmaster/t/002_start_stop.pl  | 98 ++++++++++++++++++++++++
 4 files changed, 116 insertions(+), 10 deletions(-)
 create mode 100644 src/test/postmaster/t/002_start_stop.pl

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 14a0ce91b2..7d3074a2a8 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2988,10 +2988,11 @@ PostmasterStateMachine(void)
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
 				/*
-				 * Start waiting for dead_end children to die.  This state
-				 * change causes ServerLoop to stop creating new ones.
+				 * Stop any dead_end children and stop creating new ones.
 				 */
 				pmState = PM_WAIT_DEAD_END;
+				ConfigurePostmasterWaitSet(false);
+				SignalChildren(SIGQUIT, btmask(B_DEAD_END_BACKEND));
 
 				/*
 				 * We already SIGQUIT'd the archiver and stats processes, if
@@ -3030,9 +3031,10 @@ PostmasterStateMachine(void)
 					 */
 					FatalError = true;
 					pmState = PM_WAIT_DEAD_END;
+					ConfigurePostmasterWaitSet(false);
 
 					/* Kill the walsenders and archiver too */
-					SignalChildren(SIGQUIT, btmask_all_except(B_DEAD_END_BACKEND));
+					SignalChildren(SIGQUIT, BTYPE_MASK_ALL);
 					if (PgArchPID != 0)
 						signal_child(PgArchPID, SIGQUIT);
 				}
@@ -3051,14 +3053,13 @@ PostmasterStateMachine(void)
 		if (PgArchPID == 0 && CountChildren(btmask_all_except(B_DEAD_END_BACKEND)) == 0)
 		{
 			pmState = PM_WAIT_DEAD_END;
+			ConfigurePostmasterWaitSet(false);
+			SignalChildren(SIGTERM, BTYPE_MASK_ALL);
 		}
 	}
 
 	if (pmState == PM_WAIT_DEAD_END)
 	{
-		/* Don't allow any new socket connection events. */
-		ConfigurePostmasterWaitSet(false);
-
 		/*
 		 * PM_WAIT_DEAD_END state ends when the BackendList is entirely empty
 		 * (ie, no dead_end children remain), and the archiver is gone too.
@@ -3384,12 +3385,12 @@ SignalChildren(int signal, BackendTypeMask targetMask)
 
 /*
  * Send a termination signal to children.  This considers all of our children
- * processes, except syslogger and dead_end backends.
+ * processes, except syslogger.
  */
 static void
 TerminateChildren(int signal)
 {
-	SignalChildren(signal, btmask_all_except(B_DEAD_END_BACKEND));
+	SignalChildren(signal, BTYPE_MASK_ALL);
 	if (StartupPID != 0)
 	{
 		signal_child(StartupPID, signal);
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index c793f2135d..6b77128db0 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -1186,6 +1186,9 @@ this to fail.  Otherwise, tests might fail to detect server crashes.
 With optional extra param fail_ok => 1, returns 0 for failure
 instead of bailing out.
 
+The optional extra param timeout can be used to pass the pg_ctl
+--timeout option.
+
 =cut
 
 sub stop
@@ -1201,8 +1204,11 @@ sub stop
 	return 1 unless defined $self->{_pid};
 
 	print "### Stopping node \"$name\" using mode $mode\n";
-	$ret = PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $pgdata,
-		'-m', $mode, 'stop');
+	my @cmd = ('pg_ctl', '-D', $pgdata, '-m', $mode, 'stop');
+	if ($params{timeout}) {
+		push(@cmd, ('--timeout', $params{timeout}));
+	}
+	$ret = PostgreSQL::Test::Utils::system_log(@cmd);
 
 	if ($ret != 0)
 	{
diff --git a/src/test/postmaster/meson.build b/src/test/postmaster/meson.build
index c2de2e0eb5..2d89adf520 100644
--- a/src/test/postmaster/meson.build
+++ b/src/test/postmaster/meson.build
@@ -7,6 +7,7 @@ tests += {
   'tap': {
     'tests': [
       't/001_connection_limits.pl',
+      't/002_start_stop.pl',
     ],
   },
 }
diff --git a/src/test/postmaster/t/002_start_stop.pl b/src/test/postmaster/t/002_start_stop.pl
new file mode 100644
index 0000000000..0b956d1184
--- /dev/null
+++ b/src/test/postmaster/t/002_start_stop.pl
@@ -0,0 +1,98 @@
+
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+# Test postmaster start and stop state machine.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+#
+# Test that dead-end backends don't prevent the server from shutting
+# down.
+#
+# Dead-end backends can linger until they reach
+# 'authentication_timeout'. We use a long authentication_timeout and a
+# much shorter timeout for the "pg_ctl stop" operation, to test that
+# if dead-end backends are not killed at fast shut down, "pg_ctl stop"
+# will error out before the authentication timeout kicks in and cleans
+# up the dead-end backends.
+my $authentication_timeout = $PostgreSQL::Test::Utils::timeout_default;
+my $stop_timeout = $authentication_timeout / 2;
+
+# Initialize the server with low connection limits, to test dead-end backends
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf', "max_connections = 5");
+$node->append_conf('postgresql.conf', "max_wal_senders = 0");
+$node->append_conf('postgresql.conf', "autovacuum_max_workers = 1");
+$node->append_conf('postgresql.conf', "max_worker_processes = 1");
+$node->append_conf('postgresql.conf', "log_connections = on");
+$node->append_conf('postgresql.conf', "log_min_messages = debug2");
+$node->append_conf('postgresql.conf',
+	"authentication_timeout = '$authentication_timeout s'");
+$node->append_conf('postgresql.conf', 'trace_connection_negotiation=on');
+$node->start;
+
+if (!$node->raw_connect_works())
+{
+	plan skip_all => "this test requires working raw_connect()";
+}
+
+my @raw_connections = ();
+
+# Open a lot of TCP (or Unix domain socket) connections to use up all
+# the connection slots. Beyond a certain number (roughly 2x
+# max_connections), they will be "dead-end backends".
+for (my $i = 0; $i <= 20; $i++)
+{
+	my $sock = $node->raw_connect();
+
+	# On a busy system, the server might reject connections if
+	# postmaster cannot accept() them fast enough. The exact limit
+	# and behavior depends on the platform. To make this reliable,
+	# we attempt SSL negotiation on each connection before opening
+	# next one. The server will reject the SSL negotations, but
+	# when it does so, we know that the backend has been launched
+	# and we should be able to open another connection.
+
+	# SSLRequest packet consists of packet length followed by
+	# NEGOTIATE_SSL_CODE.
+	my $negotiate_ssl_code = pack("Nnn", 8, 1234, 5679);
+	my $sent = $sock->send($negotiate_ssl_code);
+
+	# Read reply. We expect the server to reject it with 'N'
+	my $reply = "";
+	$sock->recv($reply, 1);
+	is($reply, "N", "dead-end connection $i");
+
+	push(@raw_connections, $sock);
+}
+
+# When all the connection slots are in use, new connections will fail
+# before even looking up the user. Hence you now get "sorry, too many
+# clients already" instead of "role does not exist" error. Test that
+# to ensure that we have used up all the slots.
+$node->connect_fails("dbname=postgres user=invalid_user",
+	"connect ",
+	expected_stderr => qr/FATAL:  sorry, too many clients already/);
+
+# Open one more connection, to really ensure that we have at least one
+# dead-end backend.
+my $sock = $node->raw_connect();
+
+# Test that the dead-end backends don't prevent the server from stopping.
+$node->stop('fast', timeout => $stop_timeout);
+
+$node->start();
+$node->connect_ok("dbname=postgres", "works after restart");
+
+# Clean up
+foreach my $socket (@raw_connections)
+{
+	$socket->close();
+}
+
+done_testing();
-- 
2.39.5



  [text/x-patch] 0003-Assign-a-child-slot-to-every-postmaster-child-proces.patch (72.3K, ../../[email protected]/4-0003-Assign-a-child-slot-to-every-postmaster-child-proces.patch)
  download | inline diff:
From 134128baabf6d99be888acf40ee15770b3252505 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Wed, 9 Oct 2024 22:14:13 +0300
Subject: [PATCH 3/4] Assign a child slot to every postmaster child process

Previously, only backends, autovacuum workers, and background workers
had an entry in the PMChildFlags array. With this commit, all
postmaster child processes, including all the aux processes, have an
entry. Dead-end backends still don't get an entry, though, and other
processes that don't touch shared memory will never mark their
PMChildFlags entry as active.

We now maintain separate free-lists for different kinds of child
processes. That ensures that there are always slots available for
autovacuum and background workers. Previously, pre-authorization
backends could prevent autovacuum or background workers from starting
up, by using up all the slots.

The code to manage the slots in the postmaster process is in a new
pmchild.c source file. Because postmaster.c is just so large.
Assigning pmsignal slot numbers is now pmchild.c's responsibility.
This replaces the PMChildInUse array in pmsignal.c.

Some of the comments in postmaster.c still talked about the "stats
process", but that was removed in commit 5891c7a8ed. Fix those while
we're at it.

Reviewed-by: Andres Freund <[email protected]>
Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/backend/postmaster/Makefile         |   1 +
 src/backend/postmaster/launch_backend.c |   3 +
 src/backend/postmaster/meson.build      |   1 +
 src/backend/postmaster/pmchild.c        | 282 ++++++++
 src/backend/postmaster/postmaster.c     | 815 +++++++++++-------------
 src/backend/postmaster/syslogger.c      |   6 +-
 src/backend/storage/ipc/pmsignal.c      |  83 +--
 src/backend/storage/lmgr/proc.c         |  12 +-
 src/include/postmaster/postmaster.h     |  45 ++
 src/include/postmaster/syslogger.h      |   2 +-
 src/include/storage/pmsignal.h          |   2 +-
 src/tools/pgindent/typedefs.list        |   3 +-
 12 files changed, 725 insertions(+), 530 deletions(-)
 create mode 100644 src/backend/postmaster/pmchild.c

diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index db08543d19..0f4435d2d9 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -22,6 +22,7 @@ OBJS = \
 	interrupt.o \
 	launch_backend.o \
 	pgarch.o \
+	pmchild.o \
 	postmaster.o \
 	startup.o \
 	syslogger.o \
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index b0b91dc97f..02755b6448 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -126,6 +126,7 @@ typedef struct
 	bool		query_id_enabled;
 	int			max_safe_fds;
 	int			MaxBackends;
+	int			num_pmchild_slots;
 #ifdef WIN32
 	HANDLE		PostmasterHandle;
 	HANDLE		initial_signal_pipe;
@@ -743,6 +744,7 @@ save_backend_variables(BackendParameters *param, ClientSocket *client_sock,
 	param->max_safe_fds = max_safe_fds;
 
 	param->MaxBackends = MaxBackends;
+	param->num_pmchild_slots = num_pmchild_slots;
 
 #ifdef WIN32
 	param->PostmasterHandle = PostmasterHandle;
@@ -1002,6 +1004,7 @@ restore_backend_variables(BackendParameters *param)
 	max_safe_fds = param->max_safe_fds;
 
 	MaxBackends = param->MaxBackends;
+	num_pmchild_slots = param->num_pmchild_slots;
 
 #ifdef WIN32
 	PostmasterHandle = param->PostmasterHandle;
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index 0ea4bbe084..0e80f20986 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -10,6 +10,7 @@ backend_sources += files(
   'interrupt.c',
   'launch_backend.c',
   'pgarch.c',
+  'pmchild.c',
   'postmaster.c',
   'startup.c',
   'syslogger.c',
diff --git a/src/backend/postmaster/pmchild.c b/src/backend/postmaster/pmchild.c
new file mode 100644
index 0000000000..849abbf092
--- /dev/null
+++ b/src/backend/postmaster/pmchild.c
@@ -0,0 +1,282 @@
+/*-------------------------------------------------------------------------
+ *
+ * pmchild.c
+ *	  Functions for keeping track of postmaster child processes.
+ *
+ * We keep track of all child processes so that when a process exits, we
+ * know what kind of a process it was and can clean up accordingly.  Every
+ * child process is allocated a PMChild struct from a fixed pool of
+ * structs.  The size of the pool is determined by various settings that
+ * configure how many worker processes and backend connections are
+ * allowed, i.e. autovacuum_max_workers, max_worker_processes,
+ * max_wal_senders, and max_connections.
+ *
+ * Dead-end backends are handled slightly differently.  There is no limit
+ * on the number of dead-end backends, and they do not need unique IDs, so
+ * their PMChild structs are allocated dynamically, not from a pool.
+ *
+ * The structures and functions in this file are private to the postmaster
+ * process.  But note that there is an array in shared memory, managed by
+ * pmsignal.c, that mirrors this.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/postmaster/pmchild.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "miscadmin.h"
+#include "postmaster/autovacuum.h"
+#include "postmaster/postmaster.h"
+#include "replication/walsender.h"
+#include "storage/pmsignal.h"
+#include "storage/proc.h"
+
+/*
+ * Freelists for different kinds of child processes.  We maintain separate
+ * pools for each, so that for example launching a lot of regular backends
+ * cannot prevent autovacuum or an aux process from launching.
+ */
+typedef struct PMChildPool
+{
+	int			size;			/* number of PMChild slots reserved for this
+								 * kind of processes */
+	int			first_slotno;	/* first slot belonging to this pool */
+	dlist_head	freelist;		/* currently unused PMChild entries */
+} PMChildPool;
+
+static PMChildPool pmchildPools[BACKEND_NUM_TYPES];
+NON_EXEC_STATIC int	num_pmchild_slots;
+
+/*
+ * List of active child processes.  This includes dead-end children.
+ */
+dlist_head	ActiveChildList;
+
+/*
+ * MaxLivePostmasterChildren
+ *
+ * This reports the number of postmaster child processes that can be active.
+ * It includes all children except for dead_end children.  This allows the
+ * array in shared memory (PMChildFlags) to have a fixed maximum size.
+ */
+int
+MaxLivePostmasterChildren(void)
+{
+	return num_pmchild_slots;
+}
+
+/*
+ * Initialize at postmaster startup
+ *
+ * Note: This is not called on crash restart.  We rely on PMChild entries to
+ * remain valid through the restart process.  This is important because the
+ * syslogger survives through the crash restart process, so we must not
+ * invalidate its PMChild slot.
+ */
+void
+InitPostmasterChildSlots(void)
+{
+	int			slotno;
+	PMChild    *slots;
+
+	/*
+	 * We allow more connections here than we can have backends because some
+	 * might still be authenticating; they might fail auth, or some existing
+	 * backend might exit before the auth cycle is completed.  The exact
+	 * MaxConnections limit is enforced when a new backend tries to join the
+	 * PGPROC array.
+	 *
+	 * WAL senders start out as regular backends, so they share the same pool.
+	 */
+	pmchildPools[B_BACKEND].size = 2 * (MaxConnections + max_wal_senders);
+
+	pmchildPools[B_AUTOVAC_WORKER].size = autovacuum_max_workers;
+	pmchildPools[B_BG_WORKER].size = max_worker_processes;
+
+	/*
+	 * There can be only one of each of these running at a time.  They each
+	 * get their own pool of just one entry.
+	 */
+	pmchildPools[B_AUTOVAC_LAUNCHER].size = 1;
+	pmchildPools[B_SLOTSYNC_WORKER].size = 1;
+	pmchildPools[B_ARCHIVER].size = 1;
+	pmchildPools[B_BG_WRITER].size = 1;
+	pmchildPools[B_CHECKPOINTER].size = 1;
+	pmchildPools[B_STARTUP].size = 1;
+	pmchildPools[B_WAL_RECEIVER].size = 1;
+	pmchildPools[B_WAL_SUMMARIZER].size = 1;
+	pmchildPools[B_WAL_WRITER].size = 1;
+	pmchildPools[B_LOGGER].size = 1;
+
+	/* The rest of the pmchildPools are left at zero size */
+
+	/* Count the total number of slots */
+	num_pmchild_slots = 0;
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+		num_pmchild_slots += pmchildPools[i].size;
+
+	/* Initialize them */
+	slots = palloc(num_pmchild_slots * sizeof(PMChild));
+	slotno = 0;
+	for (int btype = 0; btype < BACKEND_NUM_TYPES; btype++)
+	{
+		pmchildPools[btype].first_slotno = slotno + 1;
+		dlist_init(&pmchildPools[btype].freelist);
+
+		for (int j = 0; j < pmchildPools[btype].size; j++)
+		{
+			slots[slotno].pid = 0;
+			slots[slotno].child_slot = slotno + 1;
+			slots[slotno].bkend_type = B_INVALID;
+			slots[slotno].rw = NULL;
+			slots[slotno].bgworker_notify = false;
+			dlist_push_tail(&pmchildPools[btype].freelist, &slots[slotno].elem);
+			slotno++;
+		}
+	}
+	Assert(slotno == num_pmchild_slots);
+
+	/* Initialize other structures */
+	dlist_init(&ActiveChildList);
+}
+
+/*
+ * Allocate a PMChild entry for a postmaster child process of given type.
+ *
+ * The entry is taken from the right pool for the type.
+ *
+ * pmchild->child_slot in the returned struct is unique among all active child
+ * processes.
+ */
+PMChild *
+AssignPostmasterChildSlot(BackendType btype)
+{
+	dlist_head *freelist;
+	PMChild    *pmchild;
+
+	if (pmchildPools[btype].size == 0)
+		elog(ERROR, "cannot allocate a PMChild slot for backend type %d", btype);
+
+	freelist = &pmchildPools[btype].freelist;
+	if (dlist_is_empty(freelist))
+		return NULL;
+
+	pmchild = dlist_container(PMChild, elem, dlist_pop_head_node(freelist));
+	pmchild->pid = 0;
+	pmchild->bkend_type = btype;
+	pmchild->rw = NULL;
+	pmchild->bgworker_notify = true;
+
+	/*
+	 * pmchild->child_slot for each entry was initialized when the array of
+	 * slots was allocated.  Sanity check it.
+	 */
+	if (!(pmchild->child_slot >= pmchildPools[btype].first_slotno &&
+		  pmchild->child_slot < pmchildPools[btype].first_slotno + pmchildPools[btype].size))
+	{
+		elog(ERROR, "pmchild freelist for backend type %d is corrupt",
+			 pmchild->bkend_type);
+	}
+
+	dlist_push_head(&ActiveChildList, &pmchild->elem);
+
+	ReservePostmasterChildSlot(pmchild->child_slot);
+
+	elog(DEBUG2, "assigned pm child slot %d for %s",
+		 pmchild->child_slot, PostmasterChildName(btype));
+
+	return pmchild;
+}
+
+/*
+ * Allocate a PMChild struct for a dead-end backend.  Dead-end children are
+ * not assigned a child_slot number.  The struct is palloc'd; returns NULL if
+ * out of memory.
+ */
+PMChild *
+AllocDeadEndChild(void)
+{
+	PMChild    *pmchild;
+
+	elog(DEBUG2, "allocating dead-end child");
+
+	pmchild = (PMChild *) palloc_extended(sizeof(PMChild), MCXT_ALLOC_NO_OOM);
+	if (pmchild)
+	{
+		pmchild->pid = 0;
+		pmchild->child_slot = 0;
+		pmchild->bkend_type = B_DEAD_END_BACKEND;
+		pmchild->rw = NULL;
+		pmchild->bgworker_notify = false;
+
+		dlist_push_head(&ActiveChildList, &pmchild->elem);
+	}
+
+	return pmchild;
+}
+
+/*
+ * Release a PMChild slot, after the child process has exited.
+ *
+ * Returns true if the child detached cleanly from shared memory, false
+ * otherwise (see ReleasePostmasterChildSlot).
+ */
+bool
+FreePostmasterChildSlot(PMChild *pmchild)
+{
+	dlist_delete(&pmchild->elem);
+	if (pmchild->bkend_type == B_DEAD_END_BACKEND)
+	{
+		elog(DEBUG2, "releasing dead-end backend");
+		pfree(pmchild);
+		return true;
+	}
+	else
+	{
+		PMChildPool *pool;
+
+		elog(DEBUG2, "releasing pm child slot %d", pmchild->child_slot);
+
+		/* WAL senders start out as regular backends, and share the pool */
+		if (pmchild->bkend_type == B_WAL_SENDER)
+			pool = &pmchildPools[B_BACKEND];
+		else
+			pool = &pmchildPools[pmchild->bkend_type];
+
+		/* sanity check that we return the entry to the right pool */
+		if (!(pmchild->child_slot >= pool->first_slotno &&
+			  pmchild->child_slot < pool->first_slotno + pool->size))
+		{
+			elog(ERROR, "pmchild freelist for backend type %d is corrupt",
+				 pmchild->bkend_type);
+		}
+
+		dlist_push_head(&pool->freelist, &pmchild->elem);
+		return ReleasePostmasterChildSlot(pmchild->child_slot);
+	}
+}
+
+/*
+ * Find the PMChild entry of a running child process by PID.
+ */
+PMChild *
+FindPostmasterChildByPid(int pid)
+{
+	dlist_iter	iter;
+
+	dlist_foreach(iter, &ActiveChildList)
+	{
+		PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
+
+		if (bp->pid == pid)
+			return bp;
+	}
+	return NULL;
+}
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 7d3074a2a8..97a1b7ae1a 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -142,9 +142,7 @@ typedef struct
 StaticAssertDecl(BACKEND_NUM_TYPES < 32, "too many backend types for uint32");
 
 static const BackendTypeMask BTYPE_MASK_ALL = {(1 << BACKEND_NUM_TYPES) - 1};
-#if 0							/* unused */
 static const BackendTypeMask BTYPE_MASK_NONE = {0};
-#endif
 
 static inline BackendTypeMask
 btmask(BackendType t)
@@ -154,14 +152,12 @@ btmask(BackendType t)
 	return mask;
 }
 
-#if 0							/* unused */
 static inline BackendTypeMask
 btmask_add(BackendTypeMask mask, BackendType t)
 {
 	mask.mask |= 1 << t;
 	return mask;
 }
-#endif
 
 static inline BackendTypeMask
 btmask_del(BackendTypeMask mask, BackendType t)
@@ -195,48 +191,9 @@ btmask_contains(BackendTypeMask mask, BackendType t)
 	return (mask.mask & (1 << t)) != 0;
 }
 
-/*
- * List of active backends (or child processes anyway; we don't actually
- * know whether a given child has become a backend or is still in the
- * authorization phase).  This is used mainly to keep track of how many
- * children we have and send them appropriate signals when necessary.
- *
- * As shown in the above set of backend types, this list includes not only
- * "normal" client sessions, but also autovacuum workers, walsenders, and
- * background workers.  (Note that at the time of launch, walsenders are
- * labeled B_BACKEND; we relabel them to B_WAL_SENDER
- * upon noticing they've changed their PMChildFlags entry.  Hence that check
- * must be done before any operation that needs to distinguish walsenders
- * from normal backends.)
- *
- * Also, "dead_end" children are in it: these are children launched just for
- * the purpose of sending a friendly rejection message to a would-be client.
- * We must track them because they are attached to shared memory, but we know
- * they will never become live backends.  dead_end children are not assigned a
- * PMChildSlot.  dead_end children have bkend_type B_DEAD_END_BACKEND.
- *
- * "Special" children such as the startup, bgwriter, autovacuum launcher, and
- * slot sync worker tasks are not in this list.  They are tracked via StartupPID
- * and other pid_t variables below.  (Thus, there can't be more than one of any
- * given "special" child process type.  We use BackendList entries for any
- * child process there can be more than one of.)
- */
-typedef struct bkend
-{
-	pid_t		pid;			/* process id of backend */
-	int			child_slot;		/* PMChildSlot for this backend, if any */
-	BackendType bkend_type;		/* child process flavor, see above */
-	RegisteredBgWorker *rw;		/* bgworker info, if this is a bgworker */
-	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
-	dlist_node	elem;			/* list link in BackendList */
-} Backend;
-
-static dlist_head BackendList = DLIST_STATIC_INIT(BackendList);
 
 BackgroundWorker *MyBgworkerEntry = NULL;
 
-
-
 /* The socket number we are listening for connections on */
 int			PostPortNumber = DEF_PGPORT;
 
@@ -288,17 +245,17 @@ bool		remove_temp_files_after_crash = true;
 bool		send_abort_for_crash = false;
 bool		send_abort_for_kill = false;
 
-/* PIDs of special child processes; 0 when not running */
-static pid_t StartupPID = 0,
-			BgWriterPID = 0,
-			CheckpointerPID = 0,
-			WalWriterPID = 0,
-			WalReceiverPID = 0,
-			WalSummarizerPID = 0,
-			AutoVacPID = 0,
-			PgArchPID = 0,
-			SysLoggerPID = 0,
-			SlotSyncWorkerPID = 0;
+/* special child processes; NULL when not running */
+static PMChild *StartupPMChild = NULL,
+		   *BgWriterPMChild = NULL,
+		   *CheckpointerPMChild = NULL,
+		   *WalWriterPMChild = NULL,
+		   *WalReceiverPMChild = NULL,
+		   *WalSummarizerPMChild = NULL,
+		   *AutoVacLauncherPMChild = NULL,
+		   *PgArchPMChild = NULL,
+		   *SysLoggerPMChild = NULL,
+		   *SlotSyncWorkerPMChild = NULL;
 
 /* Startup process's status */
 typedef enum
@@ -346,7 +303,7 @@ static bool FatalError = false; /* T if recovering from backend crash */
  * PM_HOT_STANDBY state.  (connsAllowed can also restrict launching.)
  * In other states we handle connection requests by launching "dead_end"
  * child processes, which will simply send the client an error message and
- * quit.  (We track these in the BackendList so that we can know when they
+ * quit.  (We track these in the ActiveChildList so that we can know when they
  * are all gone; this is important because they're still connected to shared
  * memory, and would interfere with an attempt to destroy the shmem segment,
  * possibly leading to SHMALL failure when we try to make a new one.)
@@ -452,7 +409,7 @@ static void process_pm_child_exit(void);
 static void process_pm_reload_request(void);
 static void process_pm_shutdown_request(void);
 static void dummy_handler(SIGNAL_ARGS);
-static void CleanupBackend(Backend *bp, int exitstatus);
+static void CleanupBackend(PMChild *bp, int exitstatus);
 static void HandleChildCrash(int pid, int exitstatus, const char *procname);
 static void LogChildExit(int lev, const char *procname,
 						 int pid, int exitstatus);
@@ -463,16 +420,17 @@ static int	ServerLoop(void);
 static int	BackendStartup(ClientSocket *client_sock);
 static void report_fork_failure_to_client(ClientSocket *client_sock, int errnum);
 static CAC_state canAcceptConnections(BackendType backend_type);
-static void signal_child(pid_t pid, int signal);
-static void sigquit_child(pid_t pid);
+static void signal_child(PMChild *pmchild, int signal);
+static void sigquit_child(PMChild *pmchild);
 static bool SignalChildren(int signal, BackendTypeMask targetMask);
 static void TerminateChildren(int signal);
 static int	CountChildren(BackendTypeMask targetMask);
-static Backend *assign_backendlist_entry(void);
+static PMChild *assign_backendlist_entry(void);
 static void LaunchMissingBackgroundProcesses(void);
 static void maybe_start_bgworkers(void);
 static bool CreateOptsFile(int argc, char *argv[], char *fullprogname);
-static pid_t StartChildProcess(BackendType type);
+static PMChild *StartChildProcess(BackendType type);
+static void StartSysLogger(void);
 static void StartAutovacuumWorker(void);
 static void InitPostmasterDeathWatchHandle(void);
 
@@ -951,9 +909,11 @@ PostmasterMain(int argc, char *argv[])
 
 	/*
 	 * Now that loadable modules have had their chance to alter any GUCs,
-	 * calculate MaxBackends.
+	 * calculate MaxBackends and initialize the machinery to track child
+	 * processes.
 	 */
 	InitializeMaxBackends();
+	InitPostmasterChildSlots();
 
 	/*
 	 * Calculate the size of the PGPROC fast-path lock arrays.
@@ -1082,7 +1042,8 @@ PostmasterMain(int argc, char *argv[])
 	/*
 	 * If enabled, start up syslogger collection subprocess
 	 */
-	SysLoggerPID = SysLogger_Start();
+	if (Logging_collector)
+		StartSysLogger();
 
 	/*
 	 * Reset whereToSendOutput from DestDebug (its starting state) to
@@ -1384,16 +1345,16 @@ PostmasterMain(int argc, char *argv[])
 	AddToDataDirLockFile(LOCK_FILE_LINE_PM_STATUS, PM_STATUS_STARTING);
 
 	/* Start bgwriter and checkpointer so they can help with recovery */
-	if (CheckpointerPID == 0)
-		CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
-	if (BgWriterPID == 0)
-		BgWriterPID = StartChildProcess(B_BG_WRITER);
+	if (CheckpointerPMChild == NULL)
+		CheckpointerPMChild = StartChildProcess(B_CHECKPOINTER);
+	if (BgWriterPMChild == NULL)
+		BgWriterPMChild = StartChildProcess(B_BG_WRITER);
 
 	/*
 	 * We're ready to rock and roll...
 	 */
-	StartupPID = StartChildProcess(B_STARTUP);
-	Assert(StartupPID != 0);
+	StartupPMChild = StartChildProcess(B_STARTUP);
+	Assert(StartupPMChild != NULL);
 	StartupStatus = STARTUP_RUNNING;
 	pmState = PM_STARTUP;
 
@@ -1723,8 +1684,8 @@ ServerLoop(void)
 		if (avlauncher_needs_signal)
 		{
 			avlauncher_needs_signal = false;
-			if (AutoVacPID != 0)
-				kill(AutoVacPID, SIGUSR2);
+			if (AutoVacLauncherPMChild != NULL)
+				kill(AutoVacLauncherPMChild->pid, SIGUSR2);
 		}
 
 #ifdef HAVE_PTHREAD_IS_THREADED_NP
@@ -1842,21 +1803,6 @@ canAcceptConnections(BackendType backend_type)
 	if (!connsAllowed && backend_type == B_BACKEND)
 		return CAC_SHUTDOWN;	/* shutdown is pending */
 
-	/*
-	 * Don't start too many children.
-	 *
-	 * We allow more connections here than we can have backends because some
-	 * might still be authenticating; they might fail auth, or some existing
-	 * backend might exit before the auth cycle is completed.  The exact
-	 * MaxBackends limit is enforced when a new backend tries to join the
-	 * shared-inval backend array.
-	 *
-	 * The limit here must match the sizes of the per-child-process arrays;
-	 * see comments for MaxLivePostmasterChildren().
-	 */
-	if (CountChildren(btmask_all_except(B_DEAD_END_BACKEND)) >= MaxLivePostmasterChildren())
-		result = CAC_TOOMANY;
-
 	return result;
 }
 
@@ -2024,26 +1970,6 @@ process_pm_reload_request(void)
 				(errmsg("received SIGHUP, reloading configuration files")));
 		ProcessConfigFile(PGC_SIGHUP);
 		SignalChildren(SIGHUP, btmask_all_except(B_DEAD_END_BACKEND));
-		if (StartupPID != 0)
-			signal_child(StartupPID, SIGHUP);
-		if (BgWriterPID != 0)
-			signal_child(BgWriterPID, SIGHUP);
-		if (CheckpointerPID != 0)
-			signal_child(CheckpointerPID, SIGHUP);
-		if (WalWriterPID != 0)
-			signal_child(WalWriterPID, SIGHUP);
-		if (WalReceiverPID != 0)
-			signal_child(WalReceiverPID, SIGHUP);
-		if (WalSummarizerPID != 0)
-			signal_child(WalSummarizerPID, SIGHUP);
-		if (AutoVacPID != 0)
-			signal_child(AutoVacPID, SIGHUP);
-		if (PgArchPID != 0)
-			signal_child(PgArchPID, SIGHUP);
-		if (SysLoggerPID != 0)
-			signal_child(SysLoggerPID, SIGHUP);
-		if (SlotSyncWorkerPID != 0)
-			signal_child(SlotSyncWorkerPID, SIGHUP);
 
 		/* Reload authentication config files too */
 		if (!load_hba())
@@ -2281,15 +2207,15 @@ process_pm_child_exit(void)
 
 	while ((pid = waitpid(-1, &exitstatus, WNOHANG)) > 0)
 	{
-		bool		found;
-		dlist_mutable_iter iter;
+		PMChild    *pmchild;
 
 		/*
 		 * Check if this child was a startup process.
 		 */
-		if (pid == StartupPID)
+		if (StartupPMChild && pid == StartupPMChild->pid)
 		{
-			StartupPID = 0;
+			FreePostmasterChildSlot(StartupPMChild);
+			StartupPMChild = NULL;
 
 			/*
 			 * Startup process exited in response to a shutdown request (or it
@@ -2400,9 +2326,10 @@ process_pm_child_exit(void)
 		 * one at the next iteration of the postmaster's main loop, if
 		 * necessary.  Any other exit condition is treated as a crash.
 		 */
-		if (pid == BgWriterPID)
+		if (BgWriterPMChild && pid == BgWriterPMChild->pid)
 		{
-			BgWriterPID = 0;
+			FreePostmasterChildSlot(BgWriterPMChild);
+			BgWriterPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("background writer process"));
@@ -2412,9 +2339,10 @@ process_pm_child_exit(void)
 		/*
 		 * Was it the checkpointer?
 		 */
-		if (pid == CheckpointerPID)
+		if (CheckpointerPMChild && pid == CheckpointerPMChild->pid)
 		{
-			CheckpointerPID = 0;
+			FreePostmasterChildSlot(CheckpointerPMChild);
+			CheckpointerPMChild = NULL;
 			if (EXIT_STATUS_0(exitstatus) && pmState == PM_SHUTDOWN)
 			{
 				/*
@@ -2434,8 +2362,8 @@ process_pm_child_exit(void)
 				Assert(Shutdown > NoShutdown);
 
 				/* Waken archiver for the last time */
-				if (PgArchPID != 0)
-					signal_child(PgArchPID, SIGUSR2);
+				if (PgArchPMChild != NULL)
+					signal_child(PgArchPMChild, SIGUSR2);
 
 				/*
 				 * Waken walsenders for the last time. No regular backends
@@ -2463,9 +2391,10 @@ process_pm_child_exit(void)
 		 * new one at the next iteration of the postmaster's main loop, if
 		 * necessary.  Any other exit condition is treated as a crash.
 		 */
-		if (pid == WalWriterPID)
+		if (WalWriterPMChild && pid == WalWriterPMChild->pid)
 		{
-			WalWriterPID = 0;
+			FreePostmasterChildSlot(WalWriterPMChild);
+			WalWriterPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("WAL writer process"));
@@ -2478,9 +2407,10 @@ process_pm_child_exit(void)
 		 * backends.  (If we need a new wal receiver, we'll start one at the
 		 * next iteration of the postmaster's main loop.)
 		 */
-		if (pid == WalReceiverPID)
+		if (WalReceiverPMChild && pid == WalReceiverPMChild->pid)
 		{
-			WalReceiverPID = 0;
+			FreePostmasterChildSlot(WalReceiverPMChild);
+			WalReceiverPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("WAL receiver process"));
@@ -2492,9 +2422,10 @@ process_pm_child_exit(void)
 		 * a new one at the next iteration of the postmaster's main loop, if
 		 * necessary.  Any other exit condition is treated as a crash.
 		 */
-		if (pid == WalSummarizerPID)
+		if (WalSummarizerPMChild && pid == WalSummarizerPMChild->pid)
 		{
-			WalSummarizerPID = 0;
+			FreePostmasterChildSlot(WalSummarizerPMChild);
+			WalSummarizerPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("WAL summarizer process"));
@@ -2507,9 +2438,10 @@ process_pm_child_exit(void)
 		 * loop, if necessary.  Any other exit condition is treated as a
 		 * crash.
 		 */
-		if (pid == AutoVacPID)
+		if (AutoVacLauncherPMChild && pid == AutoVacLauncherPMChild->pid)
 		{
-			AutoVacPID = 0;
+			FreePostmasterChildSlot(AutoVacLauncherPMChild);
+			AutoVacLauncherPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("autovacuum launcher process"));
@@ -2522,9 +2454,10 @@ process_pm_child_exit(void)
 		 * and just try to start a new one on the next cycle of the
 		 * postmaster's main loop, to retry archiving remaining files.
 		 */
-		if (pid == PgArchPID)
+		if (PgArchPMChild && pid == PgArchPMChild->pid)
 		{
-			PgArchPID = 0;
+			FreePostmasterChildSlot(PgArchPMChild);
+			PgArchPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("archiver process"));
@@ -2532,11 +2465,15 @@ process_pm_child_exit(void)
 		}
 
 		/* Was it the system logger?  If so, try to start a new one */
-		if (pid == SysLoggerPID)
+		if (SysLoggerPMChild && pid == SysLoggerPMChild->pid)
 		{
-			SysLoggerPID = 0;
+			FreePostmasterChildSlot(SysLoggerPMChild);
+			SysLoggerPMChild = NULL;
+
 			/* for safety's sake, launch new logger *first* */
-			SysLoggerPID = SysLogger_Start();
+			if (Logging_collector)
+				StartSysLogger();
+
 			if (!EXIT_STATUS_0(exitstatus))
 				LogChildExit(LOG, _("system logger process"),
 							 pid, exitstatus);
@@ -2550,9 +2487,10 @@ process_pm_child_exit(void)
 		 * start a new one at the next iteration of the postmaster's main
 		 * loop, if necessary. Any other exit condition is treated as a crash.
 		 */
-		if (pid == SlotSyncWorkerPID)
+		if (SlotSyncWorkerPMChild && pid == SlotSyncWorkerPMChild->pid)
 		{
-			SlotSyncWorkerPID = 0;
+			FreePostmasterChildSlot(SlotSyncWorkerPMChild);
+			SlotSyncWorkerPMChild = NULL;
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus,
 								 _("slot sync worker process"));
@@ -2562,25 +2500,17 @@ process_pm_child_exit(void)
 		/*
 		 * Was it a backend or a background worker?
 		 */
-		found = false;
-		dlist_foreach_modify(iter, &BackendList)
+		pmchild = FindPostmasterChildByPid(pid);
+		if (pmchild)
 		{
-			Backend    *bp = dlist_container(Backend, elem, iter.cur);
-
-			if (bp->pid == pid)
-			{
-				dlist_delete(iter.cur);
-				CleanupBackend(bp, exitstatus);
-				found = true;
-				break;
-			}
+			CleanupBackend(pmchild, exitstatus);
 		}
 
 		/*
 		 * We don't know anything about this child process.  That's highly
 		 * unexpected, as we do track all the child processes that we fork.
 		 */
-		if (!found)
+		else
 		{
 			if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus))
 				HandleChildCrash(pid, exitstatus, _("untracked child process"));
@@ -2603,13 +2533,17 @@ process_pm_child_exit(void)
  * already been unlinked from BackendList, but we will free it here.
  */
 static void
-CleanupBackend(Backend *bp,
+CleanupBackend(PMChild *bp,
 			   int exitstatus)	/* child's exit status. */
 {
 	char		namebuf[MAXPGPATH];
 	const char *procname;
 	bool		crashed = false;
 	bool		logged = false;
+	pid_t		bp_pid;
+	bool		bp_bgworker_notify;
+	BackendType bp_bkend_type;
+	RegisteredBgWorker *rw;
 
 	/* Construct a process name for the log message */
 	if (bp->bkend_type == B_BG_WORKER)
@@ -2648,25 +2582,28 @@ CleanupBackend(Backend *bp,
 #endif
 
 	/*
-	 * If the process attached to shared memory, check that it detached
-	 * cleanly.
+	 * Release the PMChild entry.
+	 *
+	 * If the process attached to shared memory, this also checks that it
+	 * detached cleanly.
 	 */
-	if (bp->bkend_type != B_DEAD_END_BACKEND)
+	bp_pid = bp->pid;
+	bp_bgworker_notify = bp->bgworker_notify;
+	bp_bkend_type = bp->bkend_type;
+	rw = bp->rw;
+	if (!FreePostmasterChildSlot(bp))
 	{
-		if (!ReleasePostmasterChildSlot(bp->child_slot))
-		{
-			/*
-			 * Uh-oh, the child failed to clean itself up.  Treat as a crash
-			 * after all.
-			 */
-			crashed = true;
-		}
+		/*
+		 * Uh-oh, the child failed to clean itself up.  Treat as a crash after
+		 * all.
+		 */
+		crashed = true;
 	}
+	bp = NULL;
 
 	if (crashed)
 	{
-		HandleChildCrash(bp->pid, exitstatus, procname);
-		pfree(bp);
+		HandleChildCrash(bp_pid, exitstatus, procname);
 		return;
 	}
 
@@ -2677,17 +2614,15 @@ CleanupBackend(Backend *bp,
 	 * gets skipped in the (probably very common) case where the backend has
 	 * never requested any such notifications.
 	 */
-	if (bp->bgworker_notify)
-		BackgroundWorkerStopNotifications(bp->pid);
+	if (bp_bgworker_notify)
+		BackgroundWorkerStopNotifications(bp_pid);
 
 	/*
 	 * If it was a background worker, also update its RegisteredBgWorker
 	 * entry.
 	 */
-	if (bp->bkend_type == B_BG_WORKER)
+	if (bp_bkend_type == B_BG_WORKER)
 	{
-		RegisteredBgWorker *rw = bp->rw;
-
 		if (!EXIT_STATUS_0(exitstatus))
 		{
 			/* Record timestamp, so we know when to restart the worker. */
@@ -2706,7 +2641,7 @@ CleanupBackend(Backend *bp,
 		if (!logged)
 		{
 			LogChildExit(EXIT_STATUS_0(exitstatus) ? DEBUG1 : LOG,
-						 procname, bp->pid, exitstatus);
+						 procname, bp_pid, exitstatus);
 			logged = true;
 		}
 
@@ -2715,9 +2650,7 @@ CleanupBackend(Backend *bp,
 	}
 
 	if (!logged)
-		LogChildExit(DEBUG2, procname, bp->pid, exitstatus);
-
-	pfree(bp);
+		LogChildExit(DEBUG2, procname, bp_pid, exitstatus);
 }
 
 /*
@@ -2757,9 +2690,16 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 	{
 		dlist_iter	iter;
 
-		dlist_foreach(iter, &BackendList)
+		dlist_foreach(iter, &ActiveChildList)
 		{
-			Backend    *bp = dlist_container(Backend, elem, iter.cur);
+			PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
+
+			/* We do NOT restart the syslogger */
+			if (bp == SysLoggerPMChild)
+				continue;
+
+			if (bp == StartupPMChild)
+				StartupStatus = STARTUP_SIGNALED;
 
 			/*
 			 * This backend is still alive.  Unless we did so already, tell it
@@ -2768,48 +2708,8 @@ HandleChildCrash(int pid, int exitstatus, const char *procname)
 			 * We could exclude dead_end children here, but at least when
 			 * sending SIGABRT it seems better to include them.
 			 */
-			sigquit_child(bp->pid);
-		}
-
-		if (StartupPID != 0)
-		{
-			sigquit_child(StartupPID);
-			StartupStatus = STARTUP_SIGNALED;
+			sigquit_child(bp);
 		}
-
-		/* Take care of the bgwriter too */
-		if (BgWriterPID != 0)
-			sigquit_child(BgWriterPID);
-
-		/* Take care of the checkpointer too */
-		if (CheckpointerPID != 0)
-			sigquit_child(CheckpointerPID);
-
-		/* Take care of the walwriter too */
-		if (WalWriterPID != 0)
-			sigquit_child(WalWriterPID);
-
-		/* Take care of the walreceiver too */
-		if (WalReceiverPID != 0)
-			sigquit_child(WalReceiverPID);
-
-		/* Take care of the walsummarizer too */
-		if (WalSummarizerPID != 0)
-			sigquit_child(WalSummarizerPID);
-
-		/* Take care of the autovacuum launcher too */
-		if (AutoVacPID != 0)
-			sigquit_child(AutoVacPID);
-
-		/* Take care of the archiver too */
-		if (PgArchPID != 0)
-			sigquit_child(PgArchPID);
-
-		/* Take care of the slot sync worker too */
-		if (SlotSyncWorkerPID != 0)
-			sigquit_child(SlotSyncWorkerPID);
-
-		/* We do NOT restart the syslogger */
 	}
 
 	if (Shutdown != ImmediateShutdown)
@@ -2918,72 +2818,94 @@ PostmasterStateMachine(void)
 	}
 
 	/*
-	 * If we're ready to do so, signal child processes to shut down.  (This
-	 * isn't a persistent state, but treating it as a distinct pmState allows
-	 * us to share this code across multiple shutdown code paths.)
+	 * In the PM_WAIT_BACKENDS state, wait for all the regular backends and
+	 * procesess like autovacuum and background workers that are comparable to
+	 * backends to exit.
+	 *
+	 * PM_STOP_BACKENDS is a transient state that means the same as
+	 * PM_WAIT_BACKENDS, but we signal the processes first, before waiting for
+	 * them.  Treating it as a distinct pmState allows us to share this code
+	 * across multiple shutdown code paths.
 	 */
-	if (pmState == PM_STOP_BACKENDS)
+	if (pmState == PM_STOP_BACKENDS || pmState == PM_WAIT_BACKENDS)
 	{
+		BackendTypeMask targetMask = BTYPE_MASK_NONE;
+
 		/*
-		 * Forget any pending requests for background workers, since we're no
-		 * longer willing to launch any new workers.  (If additional requests
-		 * arrive, BackgroundWorkerStateChange will reject them.)
+		 * PM_WAIT_BACKENDS state ends when we have no regular backends, no
+		 * autovac launcher or workers, and no bgworkers (including
+		 * unconnected ones).  No walwriter, bgwriter, slot sync worker, or
+		 * WAL summarizer either.
 		 */
-		ForgetUnstartedBackgroundWorkers();
-
-		/* Signal all backend children except walsenders and dead-end backends */
-		SignalChildren(SIGTERM, btmask_all_except2(B_WAL_SENDER, B_DEAD_END_BACKEND));
-		/* and the autovac launcher too */
-		if (AutoVacPID != 0)
-			signal_child(AutoVacPID, SIGTERM);
-		/* and the bgwriter too */
-		if (BgWriterPID != 0)
-			signal_child(BgWriterPID, SIGTERM);
-		/* and the walwriter too */
-		if (WalWriterPID != 0)
-			signal_child(WalWriterPID, SIGTERM);
+		targetMask = btmask_add(targetMask, B_BACKEND);
+		targetMask = btmask_add(targetMask, B_AUTOVAC_LAUNCHER);
+		targetMask = btmask_add(targetMask, B_AUTOVAC_WORKER);
+		targetMask = btmask_add(targetMask, B_BG_WORKER);
+
+		targetMask = btmask_add(targetMask, B_WAL_WRITER);
+		targetMask = btmask_add(targetMask, B_BG_WRITER);
+		targetMask = btmask_add(targetMask, B_SLOTSYNC_WORKER);
+		targetMask = btmask_add(targetMask, B_WAL_SUMMARIZER);
+
 		/* If we're in recovery, also stop startup and walreceiver procs */
-		if (StartupPID != 0)
-			signal_child(StartupPID, SIGTERM);
-		if (WalReceiverPID != 0)
-			signal_child(WalReceiverPID, SIGTERM);
-		if (WalSummarizerPID != 0)
-			signal_child(WalSummarizerPID, SIGTERM);
-		if (SlotSyncWorkerPID != 0)
-			signal_child(SlotSyncWorkerPID, SIGTERM);
-		/* checkpointer, archiver, stats, and syslogger may continue for now */
-
-		/* Now transition to PM_WAIT_BACKENDS state to wait for them to die */
-		pmState = PM_WAIT_BACKENDS;
-	}
+		targetMask = btmask_add(targetMask, B_STARTUP);
+		targetMask = btmask_add(targetMask, B_WAL_RECEIVER);
 
-	/*
-	 * If we are in a state-machine state that implies waiting for backends to
-	 * exit, see if they're all gone, and change state if so.
-	 */
-	if (pmState == PM_WAIT_BACKENDS)
-	{
 		/*
-		 * PM_WAIT_BACKENDS state ends when we have no regular backends
-		 * (including autovac workers), no bgworkers (including unconnected
-		 * ones), and no walwriter, autovac launcher, bgwriter or slot sync
-		 * worker.  If we are doing crash recovery or an immediate shutdown
-		 * then we expect the checkpointer to exit as well, otherwise not. The
-		 * stats and syslogger processes are disregarded since they are not
-		 * connected to shared memory; we also disregard dead_end children
-		 * here. Walsenders and archiver are also disregarded, they will be
-		 * terminated later after writing the checkpoint record.
+		 * If we are doing crash recovery or an immediate shutdown then we
+		 * expect the checkpointer to exit as well, otherwise not.
 		 */
-		if (CountChildren(btmask_all_except2(B_WAL_SENDER, B_DEAD_END_BACKEND)) == 0 &&
-			StartupPID == 0 &&
-			WalReceiverPID == 0 &&
-			WalSummarizerPID == 0 &&
-			BgWriterPID == 0 &&
-			(CheckpointerPID == 0 ||
-			 (!FatalError && Shutdown < ImmediateShutdown)) &&
-			WalWriterPID == 0 &&
-			AutoVacPID == 0 &&
-			SlotSyncWorkerPID == 0)
+		if (FatalError || Shutdown >= ImmediateShutdown)
+			targetMask = btmask_add(targetMask, B_CHECKPOINTER);
+
+		/*
+		 * Walsenders and archiver will continue running; they will be
+		 * terminated later after writing the checkpoint record.  We also let
+		 * dead_end children to keep running for now.  The syslogger process
+		 * exits last.
+		 *
+		 * This assertion checks that we have covered all backend types,
+		 * either by including them in targetMask, or by noting here that they
+		 * are allowed to continue running.
+		 */
+#ifdef USE_ASSERT_CHECKING
+		{
+			BackendTypeMask remainMask = BTYPE_MASK_NONE;
+
+			remainMask = btmask_add(remainMask, B_WAL_SENDER);
+			remainMask = btmask_add(remainMask, B_ARCHIVER);
+			remainMask = btmask_add(remainMask, B_DEAD_END_BACKEND);
+			remainMask = btmask_add(remainMask, B_LOGGER);
+
+			/* checkpointer may or may not be in targetMask already */
+			remainMask = btmask_add(remainMask, B_CHECKPOINTER);
+
+			/* these are not real postmaster children */
+			remainMask = btmask_add(remainMask, B_INVALID);
+			remainMask = btmask_add(remainMask, B_STANDALONE_BACKEND);
+
+			/* All types should be included in targetMask or remainMask */
+			Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
+		}
+#endif
+
+		/* If we had not yet signaled the processes to exit, do so now */
+		if (pmState == PM_STOP_BACKENDS)
+		{
+			/*
+			 * Forget any pending requests for background workers, since we're
+			 * no longer willing to launch any new workers.  (If additional
+			 * requests arrive, BackgroundWorkerStateChange will reject them.)
+			 */
+			ForgetUnstartedBackgroundWorkers();
+
+			SignalChildren(SIGTERM, targetMask);
+
+			pmState = PM_WAIT_BACKENDS;
+		}
+
+		/* Are any of the target processes still running? */
+		if (CountChildren(targetMask) == 0)
 		{
 			if (Shutdown >= ImmediateShutdown || FatalError)
 			{
@@ -2991,13 +2913,14 @@ PostmasterStateMachine(void)
 				 * Stop any dead_end children and stop creating new ones.
 				 */
 				pmState = PM_WAIT_DEAD_END;
+				elog(DEBUG1, "entering PM_WAIT_DEAD_END");
 				ConfigurePostmasterWaitSet(false);
 				SignalChildren(SIGQUIT, btmask(B_DEAD_END_BACKEND));
 
 				/*
-				 * We already SIGQUIT'd the archiver and stats processes, if
-				 * any, when we started immediate shutdown or entered
-				 * FatalError state.
+				 * We already SIGQUIT'd walsenders and the archiver, if any,
+				 * when we started immediate shutdown or entered FatalError
+				 * state.
 				 */
 			}
 			else
@@ -3009,13 +2932,14 @@ PostmasterStateMachine(void)
 				 */
 				Assert(Shutdown > NoShutdown);
 				/* Start the checkpointer if not running */
-				if (CheckpointerPID == 0)
-					CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
+				if (CheckpointerPMChild == NULL)
+					CheckpointerPMChild = StartChildProcess(B_CHECKPOINTER);
 				/* And tell it to shut down */
-				if (CheckpointerPID != 0)
+				if (CheckpointerPMChild != NULL)
 				{
-					signal_child(CheckpointerPID, SIGUSR2);
+					signal_child(CheckpointerPMChild, SIGUSR2);
 					pmState = PM_SHUTDOWN;
+					elog(DEBUG1, "entering PM_SHUTDOWN");
 				}
 				else
 				{
@@ -3031,12 +2955,11 @@ PostmasterStateMachine(void)
 					 */
 					FatalError = true;
 					pmState = PM_WAIT_DEAD_END;
+					elog(DEBUG1, "entering PM_WAIT_DEAD_END");
 					ConfigurePostmasterWaitSet(false);
 
 					/* Kill the walsenders and archiver too */
-					SignalChildren(SIGQUIT, BTYPE_MASK_ALL);
-					if (PgArchPID != 0)
-						signal_child(PgArchPID, SIGQUIT);
+					SignalChildren(SIGQUIT, btmask_all_except(B_LOGGER));
 				}
 			}
 		}
@@ -3050,39 +2973,40 @@ PostmasterStateMachine(void)
 		 * left by now anyway; what we're really waiting for is walsenders and
 		 * archiver.
 		 */
-		if (PgArchPID == 0 && CountChildren(btmask_all_except(B_DEAD_END_BACKEND)) == 0)
+		if (CountChildren(btmask_all_except2(B_LOGGER, B_DEAD_END_BACKEND)) == 0)
 		{
 			pmState = PM_WAIT_DEAD_END;
 			ConfigurePostmasterWaitSet(false);
-			SignalChildren(SIGTERM, BTYPE_MASK_ALL);
+			SignalChildren(SIGTERM, btmask_all_except(B_LOGGER));
 		}
 	}
 
 	if (pmState == PM_WAIT_DEAD_END)
 	{
 		/*
-		 * PM_WAIT_DEAD_END state ends when the BackendList is entirely empty
-		 * (ie, no dead_end children remain), and the archiver is gone too.
-		 *
-		 * The reason we wait for those two is to protect them against a new
-		 * postmaster starting conflicting subprocesses; this isn't an
-		 * ironclad protection, but it at least helps in the
-		 * shutdown-and-immediately-restart scenario.  Note that they have
-		 * already been sent appropriate shutdown signals, either during a
-		 * normal state transition leading up to PM_WAIT_DEAD_END, or during
+		 * PM_WAIT_DEAD_END state ends when all other children are gone except
+		 * for the logger.  During normal shutdown, all that remains are
+		 * dead-end backends, but in FatalError processing we jump straight
+		 * here with more processes remaining.  Note that they have already
+		 * been sent appropriate shutdown signals, either during a normal
+		 * state transition leading up to PM_WAIT_DEAD_END, or during
 		 * FatalError processing.
+		 *
+		 * The reason we wait is to protect against a new postmaster starting
+		 * conflicting subprocesses; this isn't an ironclad protection, but it
+		 * at least helps in the shutdown-and-immediately-restart scenario.
 		 */
-		if (dlist_is_empty(&BackendList) && PgArchPID == 0)
+		if (CountChildren(btmask_all_except(B_LOGGER)) == 0)
 		{
 			/* These other guys should be dead already */
-			Assert(StartupPID == 0);
-			Assert(WalReceiverPID == 0);
-			Assert(WalSummarizerPID == 0);
-			Assert(BgWriterPID == 0);
-			Assert(CheckpointerPID == 0);
-			Assert(WalWriterPID == 0);
-			Assert(AutoVacPID == 0);
-			Assert(SlotSyncWorkerPID == 0);
+			Assert(StartupPMChild == NULL);
+			Assert(WalReceiverPMChild == NULL);
+			Assert(WalSummarizerPMChild == NULL);
+			Assert(BgWriterPMChild == NULL);
+			Assert(CheckpointerPMChild == NULL);
+			Assert(WalWriterPMChild == NULL);
+			Assert(AutoVacLauncherPMChild == NULL);
+			Assert(SlotSyncWorkerPMChild == NULL);
 			/* syslogger is not considered here */
 			pmState = PM_NO_CHILDREN;
 		}
@@ -3165,8 +3089,8 @@ PostmasterStateMachine(void)
 		/* re-create shared memory and semaphores */
 		CreateSharedMemoryAndSemaphores();
 
-		StartupPID = StartChildProcess(B_STARTUP);
-		Assert(StartupPID != 0);
+		StartupPMChild = StartChildProcess(B_STARTUP);
+		Assert(StartupPMChild != NULL);
 		StartupStatus = STARTUP_RUNNING;
 		pmState = PM_STARTUP;
 		/* crash recovery started, reset SIGKILL flag */
@@ -3189,8 +3113,8 @@ static void
 LaunchMissingBackgroundProcesses(void)
 {
 	/* Syslogger is active in all states */
-	if (SysLoggerPID == 0 && Logging_collector)
-		SysLoggerPID = SysLogger_Start();
+	if (SysLoggerPMChild == NULL && Logging_collector)
+		StartSysLogger();
 
 	/*
 	 * The checkpointer and the background writer are active from the start,
@@ -3203,30 +3127,30 @@ LaunchMissingBackgroundProcesses(void)
 	if (pmState == PM_RUN || pmState == PM_RECOVERY ||
 		pmState == PM_HOT_STANDBY || pmState == PM_STARTUP)
 	{
-		if (CheckpointerPID == 0)
-			CheckpointerPID = StartChildProcess(B_CHECKPOINTER);
-		if (BgWriterPID == 0)
-			BgWriterPID = StartChildProcess(B_BG_WRITER);
+		if (CheckpointerPMChild == NULL)
+			CheckpointerPMChild = StartChildProcess(B_CHECKPOINTER);
+		if (BgWriterPMChild == NULL)
+			BgWriterPMChild = StartChildProcess(B_BG_WRITER);
 	}
 
 	/*
 	 * WAL writer is needed only in normal operation (else we cannot be
 	 * writing any new WAL).
 	 */
-	if (WalWriterPID == 0 && pmState == PM_RUN)
-		WalWriterPID = StartChildProcess(B_WAL_WRITER);
+	if (WalWriterPMChild == NULL && pmState == PM_RUN)
+		WalWriterPMChild = StartChildProcess(B_WAL_WRITER);
 
 	/*
 	 * We don't want autovacuum to run in binary upgrade mode because
 	 * autovacuum might update relfrozenxid for empty tables before the
 	 * physical files are put in place.
 	 */
-	if (!IsBinaryUpgrade && AutoVacPID == 0 &&
+	if (!IsBinaryUpgrade && AutoVacLauncherPMChild == NULL &&
 		(AutoVacuumingActive() || start_autovac_launcher) &&
 		pmState == PM_RUN)
 	{
-		AutoVacPID = StartChildProcess(B_AUTOVAC_LAUNCHER);
-		if (AutoVacPID != 0)
+		AutoVacLauncherPMChild = StartChildProcess(B_AUTOVAC_LAUNCHER);
+		if (AutoVacLauncherPMChild != NULL)
 			start_autovac_launcher = false; /* signal processed */
 	}
 
@@ -3234,11 +3158,11 @@ LaunchMissingBackgroundProcesses(void)
 	 * If WAL archiving is enabled always, we are allowed to start archiver
 	 * even during recovery.
 	 */
-	if (PgArchPID == 0 &&
+	if (PgArchPMChild == NULL &&
 		((XLogArchivingActive() && pmState == PM_RUN) ||
 		 (XLogArchivingAlways() && (pmState == PM_RECOVERY || pmState == PM_HOT_STANDBY))) &&
 		PgArchCanRestart())
-		PgArchPID = StartChildProcess(B_ARCHIVER);
+		PgArchPMChild = StartChildProcess(B_ARCHIVER);
 
 	/*
 	 * If we need to start a slot sync worker, try to do that now
@@ -3248,10 +3172,10 @@ LaunchMissingBackgroundProcesses(void)
 	 * configured correctly, and it is the first time of worker's launch, or
 	 * enough time has passed since the worker was launched last.
 	 */
-	if (SlotSyncWorkerPID == 0 && pmState == PM_HOT_STANDBY &&
+	if (SlotSyncWorkerPMChild == NULL && pmState == PM_HOT_STANDBY &&
 		Shutdown <= SmartShutdown && sync_replication_slots &&
 		ValidateSlotSyncParams(LOG) && SlotSyncWorkerCanRestart())
-		SlotSyncWorkerPID = StartChildProcess(B_SLOTSYNC_WORKER);
+		SlotSyncWorkerPMChild = StartChildProcess(B_SLOTSYNC_WORKER);
 
 	/*
 	 * If we need to start a WAL receiver, try to do that now
@@ -3267,23 +3191,23 @@ LaunchMissingBackgroundProcesses(void)
 	 */
 	if (WalReceiverRequested)
 	{
-		if (WalReceiverPID == 0 &&
+		if (WalReceiverPMChild == NULL &&
 			(pmState == PM_STARTUP || pmState == PM_RECOVERY ||
 			 pmState == PM_HOT_STANDBY) &&
 			Shutdown <= SmartShutdown)
 		{
-			WalReceiverPID = StartChildProcess(B_WAL_RECEIVER);
-			if (WalReceiverPID != 0)
+			WalReceiverPMChild = StartChildProcess(B_WAL_RECEIVER);
+			if (WalReceiverPMChild != 0)
 				WalReceiverRequested = false;
 			/* else leave the flag set, so we'll try again later */
 		}
 	}
 
 	/* If we need to start a WAL summarizer, try to do that now */
-	if (summarize_wal && WalSummarizerPID == 0 &&
+	if (summarize_wal && WalSummarizerPMChild == NULL &&
 		(pmState == PM_RUN || pmState == PM_HOT_STANDBY) &&
 		Shutdown <= SmartShutdown)
-		WalSummarizerPID = StartChildProcess(B_WAL_SUMMARIZER);
+		WalSummarizerPMChild = StartChildProcess(B_WAL_SUMMARIZER);
 
 	/* Get other worker processes running, if needed */
 	if (StartWorkerNeeded || HaveCrashedWorker)
@@ -3307,8 +3231,14 @@ LaunchMissingBackgroundProcesses(void)
  * child twice will not cause any problems.
  */
 static void
-signal_child(pid_t pid, int signal)
+signal_child(PMChild *pmchild, int signal)
 {
+	pid_t		pid;
+
+	if (pmchild == NULL || pmchild->pid == 0)
+		return;
+	pid = pmchild->pid;
+
 	if (kill(pid, signal) < 0)
 		elog(DEBUG3, "kill(%ld,%d) failed: %m", (long) pid, signal);
 #ifdef HAVE_SETSID
@@ -3337,17 +3267,17 @@ signal_child(pid_t pid, int signal)
  * to use SIGABRT to collect per-child core dumps.
  */
 static void
-sigquit_child(pid_t pid)
+sigquit_child(PMChild *pmchild)
 {
 	ereport(DEBUG2,
 			(errmsg_internal("sending %s to process %d",
 							 (send_abort_for_crash ? "SIGABRT" : "SIGQUIT"),
-							 (int) pid)));
-	signal_child(pid, (send_abort_for_crash ? SIGABRT : SIGQUIT));
+							 (int) pmchild->pid)));
+	signal_child(pmchild, (send_abort_for_crash ? SIGABRT : SIGQUIT));
 }
 
 /*
- * Send a signal to the targeted children (but NOT special children).
+ * Send a signal to the targeted children.
  */
 static bool
 SignalChildren(int signal, BackendTypeMask targetMask)
@@ -3355,9 +3285,9 @@ SignalChildren(int signal, BackendTypeMask targetMask)
 	dlist_iter	iter;
 	bool		signaled = false;
 
-	dlist_foreach(iter, &BackendList)
+	dlist_foreach(iter, &ActiveChildList)
 	{
-		Backend    *bp = dlist_container(Backend, elem, iter.cur);
+		PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
 
 		/*
 		 * If we need to distinguish between B_BACKEND and B_WAL_SENDER, check
@@ -3377,7 +3307,7 @@ SignalChildren(int signal, BackendTypeMask targetMask)
 		ereport(DEBUG4,
 				(errmsg_internal("sending signal %d to %s process %d",
 								 signal, GetBackendTypeDesc(bp->bkend_type), (int) bp->pid)));
-		signal_child(bp->pid, signal);
+		signal_child(bp, signal);
 		signaled = true;
 	}
 	return signaled;
@@ -3390,29 +3320,12 @@ SignalChildren(int signal, BackendTypeMask targetMask)
 static void
 TerminateChildren(int signal)
 {
-	SignalChildren(signal, BTYPE_MASK_ALL);
-	if (StartupPID != 0)
+	SignalChildren(signal, btmask_all_except(B_LOGGER));
+	if (StartupPMChild != NULL)
 	{
-		signal_child(StartupPID, signal);
 		if (signal == SIGQUIT || signal == SIGKILL || signal == SIGABRT)
 			StartupStatus = STARTUP_SIGNALED;
 	}
-	if (BgWriterPID != 0)
-		signal_child(BgWriterPID, signal);
-	if (CheckpointerPID != 0)
-		signal_child(CheckpointerPID, signal);
-	if (WalWriterPID != 0)
-		signal_child(WalWriterPID, signal);
-	if (WalReceiverPID != 0)
-		signal_child(WalReceiverPID, signal);
-	if (WalSummarizerPID != 0)
-		signal_child(WalSummarizerPID, signal);
-	if (AutoVacPID != 0)
-		signal_child(AutoVacPID, signal);
-	if (PgArchPID != 0)
-		signal_child(PgArchPID, signal);
-	if (SlotSyncWorkerPID != 0)
-		signal_child(SlotSyncWorkerPID, signal);
 }
 
 /*
@@ -3425,44 +3338,45 @@ TerminateChildren(int signal)
 static int
 BackendStartup(ClientSocket *client_sock)
 {
-	Backend    *bn;				/* for backend cleanup */
+	PMChild    *bn = NULL;
 	pid_t		pid;
 	BackendStartupData startup_data;
+	CAC_state	cac;
 
-	/*
-	 * Create backend data structure.  Better before the fork() so we can
-	 * handle failure cleanly.
-	 */
-	bn = (Backend *) palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
+	cac = canAcceptConnections(B_BACKEND);
+	if (cac == CAC_OK)
+	{
+		/* Can change later to B_WAL_SENDER */
+		bn = AssignPostmasterChildSlot(B_BACKEND);
+		if (!bn)
+		{
+			/*
+			 * Too many regular child processes; launch a dead-end child
+			 * process instead.
+			 */
+			cac = CAC_TOOMANY;
+		}
+	}
 	if (!bn)
 	{
-		ereport(LOG,
-				(errcode(ERRCODE_OUT_OF_MEMORY),
-				 errmsg("out of memory")));
-		return STATUS_ERROR;
+		bn = AllocDeadEndChild();
+		if (!bn)
+		{
+			ereport(LOG,
+					(errcode(ERRCODE_OUT_OF_MEMORY),
+					 errmsg("out of memory")));
+			return STATUS_ERROR;
+		}
 	}
 
 	/* Pass down canAcceptConnections state */
-	startup_data.canAcceptConnections = canAcceptConnections(B_BACKEND);
+	startup_data.canAcceptConnections = cac;
 	bn->rw = NULL;
 
-	/*
-	 * Unless it's a dead_end child, assign it a child slot number
-	 */
-	if (startup_data.canAcceptConnections == CAC_OK)
-	{
-		bn->bkend_type = B_BACKEND; /* Can change later to B_WAL_SENDER */
-		bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
-	}
-	else
-	{
-		bn->bkend_type = B_DEAD_END_BACKEND;
-		bn->child_slot = 0;
-	}
-
 	/* Hasn't asked to be notified about any bgworkers yet */
 	bn->bgworker_notify = false;
 
+	MyPMChildSlot = bn->child_slot;
 	pid = postmaster_child_launch(bn->bkend_type,
 								  (char *) &startup_data, sizeof(startup_data),
 								  client_sock);
@@ -3471,9 +3385,7 @@ BackendStartup(ClientSocket *client_sock)
 		/* in parent, fork failed */
 		int			save_errno = errno;
 
-		if (bn->child_slot != 0)
-			(void) ReleasePostmasterChildSlot(bn->child_slot);
-		pfree(bn);
+		(void) FreePostmasterChildSlot(bn);
 		errno = save_errno;
 		ereport(LOG,
 				(errmsg("could not fork new process for connection: %m")));
@@ -3492,8 +3404,6 @@ BackendStartup(ClientSocket *client_sock)
 	 * of backends.
 	 */
 	bn->pid = pid;
-	dlist_push_head(&BackendList, &bn->elem);
-
 	return STATUS_OK;
 }
 
@@ -3591,9 +3501,9 @@ process_pm_pmsignal(void)
 		 * Start the archiver if we're responsible for (re-)archiving received
 		 * files.
 		 */
-		Assert(PgArchPID == 0);
+		Assert(PgArchPMChild == NULL);
 		if (XLogArchivingAlways())
-			PgArchPID = StartChildProcess(B_ARCHIVER);
+			PgArchPMChild = StartChildProcess(B_ARCHIVER);
 
 		/*
 		 * If we aren't planning to enter hot standby mode later, treat
@@ -3639,16 +3549,16 @@ process_pm_pmsignal(void)
 	}
 
 	/* Tell syslogger to rotate logfile if requested */
-	if (SysLoggerPID != 0)
+	if (SysLoggerPMChild != NULL)
 	{
 		if (CheckLogrotateSignal())
 		{
-			signal_child(SysLoggerPID, SIGUSR1);
+			signal_child(SysLoggerPMChild, SIGUSR1);
 			RemoveLogrotateSignalFiles();
 		}
 		else if (CheckPostmasterSignal(PMSIGNAL_ROTATE_LOGFILE))
 		{
-			signal_child(SysLoggerPID, SIGUSR1);
+			signal_child(SysLoggerPMChild, SIGUSR1);
 		}
 	}
 
@@ -3695,7 +3605,7 @@ process_pm_pmsignal(void)
 		PostmasterStateMachine();
 	}
 
-	if (StartupPID != 0 &&
+	if (StartupPMChild != NULL &&
 		(pmState == PM_STARTUP || pmState == PM_RECOVERY ||
 		 pmState == PM_HOT_STANDBY) &&
 		CheckPromoteSignal())
@@ -3706,7 +3616,7 @@ process_pm_pmsignal(void)
 		 * Leave the promote signal file in place and let the Startup process
 		 * do the unlink.
 		 */
-		signal_child(StartupPID, SIGUSR2);
+		signal_child(StartupPMChild, SIGUSR2);
 	}
 }
 
@@ -3733,9 +3643,9 @@ CountChildren(BackendTypeMask targetMask)
 	dlist_iter	iter;
 	int			cnt = 0;
 
-	dlist_foreach(iter, &BackendList)
+	dlist_foreach(iter, &ActiveChildList)
 	{
-		Backend    *bp = dlist_container(Backend, elem, iter.cur);
+		PMChild    *bp = dlist_container(PMChild, elem, iter.cur);
 
 		/*
 		 * If we need to distinguish between B_BACKEND and B_WAL_SENDER, check
@@ -3752,6 +3662,10 @@ CountChildren(BackendTypeMask targetMask)
 		if (!btmask_contains(targetMask, bp->bkend_type))
 			continue;
 
+		ereport(DEBUG4,
+				(errmsg_internal("%s process %d is still running",
+								 GetBackendTypeDesc(bp->bkend_type), (int) bp->pid)));
+
 		cnt++;
 	}
 	return cnt;
@@ -3764,18 +3678,36 @@ CountChildren(BackendTypeMask targetMask)
  * "type" determines what kind of child will be started.  All child types
  * initially go to AuxiliaryProcessMain, which will handle common setup.
  *
- * Return value of StartChildProcess is subprocess' PID, or 0 if failed
- * to start subprocess.
+ * Return value of StartChildProcess is subprocess' PMChild entry, or NULL on
+ * failure.
  */
-static pid_t
+static PMChild *
 StartChildProcess(BackendType type)
 {
+	PMChild    *pmchild;
 	pid_t		pid;
 
+	pmchild = AssignPostmasterChildSlot(type);
+	if (!pmchild)
+	{
+		if (type == B_AUTOVAC_WORKER)
+			ereport(LOG,
+					(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+					 errmsg("no slot available for new autovacuum worker process")));
+		else
+		{
+			/* shouldn't happen because we allocate enough slots */
+			elog(LOG, "no postmaster child slot available for aux process");
+		}
+		return NULL;
+	}
+
+	MyPMChildSlot = pmchild->child_slot;
 	pid = postmaster_child_launch(type, NULL, 0, NULL);
 	if (pid < 0)
 	{
 		/* in parent, fork failed */
+		FreePostmasterChildSlot(pmchild);
 		ereport(LOG,
 				(errmsg("could not fork \"%s\" process: %m", PostmasterChildName(type))));
 
@@ -3785,13 +3717,31 @@ StartChildProcess(BackendType type)
 		 */
 		if (type == B_STARTUP)
 			ExitPostmaster(1);
-		return 0;
+		return NULL;
 	}
 
-	/*
-	 * in parent, successful fork
-	 */
-	return pid;
+	/* in parent, successful fork */
+	pmchild->pid = pid;
+	return pmchild;
+}
+
+/*
+ * StartSysLogger -- start the syslogger process
+ */
+void
+StartSysLogger(void)
+{
+	Assert(SysLoggerPMChild == NULL);
+
+	SysLoggerPMChild = AssignPostmasterChildSlot(B_LOGGER);
+	if (!SysLoggerPMChild)
+		elog(PANIC, "no postmaster child slot available for syslogger");
+	SysLoggerPMChild->pid = SysLogger_Start(SysLoggerPMChild->child_slot);
+	if (SysLoggerPMChild->pid == 0)
+	{
+		FreePostmasterChildSlot(SysLoggerPMChild);
+		SysLoggerPMChild = NULL;
+	}
 }
 
 /*
@@ -3806,7 +3756,7 @@ StartChildProcess(BackendType type)
 static void
 StartAutovacuumWorker(void)
 {
-	Backend    *bn;
+	PMChild    *bn;
 
 	/*
 	 * If not in condition to run a process, don't try, but handle it like a
@@ -3817,34 +3767,20 @@ StartAutovacuumWorker(void)
 	 */
 	if (canAcceptConnections(B_AUTOVAC_WORKER) == CAC_OK)
 	{
-		bn = (Backend *) palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
+		bn = StartChildProcess(B_AUTOVAC_WORKER);
 		if (bn)
 		{
-			/* Autovac workers need a child slot */
-			bn->bkend_type = B_AUTOVAC_WORKER;
-			bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
 			bn->bgworker_notify = false;
 			bn->rw = NULL;
-
-			bn->pid = StartChildProcess(B_AUTOVAC_WORKER);
-			if (bn->pid > 0)
-			{
-				dlist_push_head(&BackendList, &bn->elem);
-				/* all OK */
-				return;
-			}
-
+			return;
+		}
+		else
+		{
 			/*
 			 * fork failed, fall through to report -- actual error message was
 			 * logged by StartChildProcess
 			 */
-			(void) ReleasePostmasterChildSlot(bn->child_slot);
-			pfree(bn);
 		}
-		else
-			ereport(LOG,
-					(errcode(ERRCODE_OUT_OF_MEMORY),
-					 errmsg("out of memory")));
 	}
 
 	/*
@@ -3856,7 +3792,7 @@ StartAutovacuumWorker(void)
 	 * quick succession between the autovac launcher and postmaster in case
 	 * things get ugly.
 	 */
-	if (AutoVacPID != 0)
+	if (AutoVacLauncherPMChild != NULL)
 	{
 		AutoVacWorkerFailed();
 		avlauncher_needs_signal = true;
@@ -3900,23 +3836,6 @@ CreateOptsFile(int argc, char *argv[], char *fullprogname)
 }
 
 
-/*
- * MaxLivePostmasterChildren
- *
- * This reports the number of entries needed in the per-child-process array
- * (PMChildFlags).  It includes regular backends, autovac workers, walsenders
- * and background workers, but not special children nor dead_end children.
- * This allows the array to have a fixed maximum size, to wit the same
- * too-many-children limit enforced by canAcceptConnections().  The exact value
- * isn't too critical as long as it's more than MaxBackends.
- */
-int
-MaxLivePostmasterChildren(void)
-{
-	return 2 * (MaxConnections + autovacuum_max_workers + 1 +
-				max_wal_senders + max_worker_processes);
-}
-
 /*
  * Start a new bgworker.
  * Starting time conditions must have been checked already.
@@ -3929,7 +3848,7 @@ MaxLivePostmasterChildren(void)
 static bool
 do_start_bgworker(RegisteredBgWorker *rw)
 {
-	Backend    *bn;
+	PMChild    *bn;
 	pid_t		worker_pid;
 
 	Assert(rw->rw_pid == 0);
@@ -3956,6 +3875,7 @@ do_start_bgworker(RegisteredBgWorker *rw)
 			(errmsg_internal("starting background worker process \"%s\"",
 							 rw->rw_worker.bgw_name)));
 
+	MyPMChildSlot = bn->child_slot;
 	worker_pid = postmaster_child_launch(B_BG_WORKER, (char *) &rw->rw_worker, sizeof(BackgroundWorker), NULL);
 	if (worker_pid == -1)
 	{
@@ -3963,8 +3883,7 @@ do_start_bgworker(RegisteredBgWorker *rw)
 		ereport(LOG,
 				(errmsg("could not fork background worker process: %m")));
 		/* undo what assign_backendlist_entry did */
-		ReleasePostmasterChildSlot(bn->child_slot);
-		pfree(bn);
+		FreePostmasterChildSlot(bn);
 
 		/* mark entry as crashed, so we'll try again later */
 		rw->rw_crashed_at = GetCurrentTimestamp();
@@ -3975,8 +3894,6 @@ do_start_bgworker(RegisteredBgWorker *rw)
 	rw->rw_pid = worker_pid;
 	bn->pid = rw->rw_pid;
 	ReportBackgroundWorkerPID(rw);
-	/* add new worker to lists of backends */
-	dlist_push_head(&BackendList, &bn->elem);
 	return true;
 }
 
@@ -4024,17 +3941,13 @@ bgworker_should_start_now(BgWorkerStartTime start_time)
  *
  * On failure, return NULL.
  */
-static Backend *
+static PMChild *
 assign_backendlist_entry(void)
 {
-	Backend    *bn;
+	PMChild    *bn;
 
-	/*
-	 * Check that database state allows another connection.  Currently the
-	 * only possible failure is CAC_TOOMANY, so we just log an error message
-	 * based on that rather than checking the error code precisely.
-	 */
-	if (canAcceptConnections(B_BG_WORKER) != CAC_OK)
+	bn = AssignPostmasterChildSlot(B_BG_WORKER);
+	if (bn == NULL)
 	{
 		ereport(LOG,
 				(errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
@@ -4042,16 +3955,6 @@ assign_backendlist_entry(void)
 		return NULL;
 	}
 
-	bn = palloc_extended(sizeof(Backend), MCXT_ALLOC_NO_OOM);
-	if (bn == NULL)
-	{
-		ereport(LOG,
-				(errcode(ERRCODE_OUT_OF_MEMORY),
-				 errmsg("out of memory")));
-		return NULL;
-	}
-
-	bn->child_slot = MyPMChildSlot = AssignPostmasterChildSlot();
 	bn->bkend_type = B_BG_WORKER;
 	bn->bgworker_notify = false;
 
@@ -4192,11 +4095,11 @@ bool
 PostmasterMarkPIDForWorkerNotify(int pid)
 {
 	dlist_iter	iter;
-	Backend    *bp;
+	PMChild    *bp;
 
-	dlist_foreach(iter, &BackendList)
+	dlist_foreach(iter, &ActiveChildList)
 	{
-		bp = dlist_container(Backend, elem, iter.cur);
+		bp = dlist_container(PMChild, elem, iter.cur);
 		if (bp->pid == pid)
 		{
 			bp->bgworker_notify = true;
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index 7951599fa8..7ca24c6663 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -590,7 +590,7 @@ SysLoggerMain(char *startup_data, size_t startup_data_len)
  * Postmaster subroutine to start a syslogger subprocess.
  */
 int
-SysLogger_Start(void)
+SysLogger_Start(int child_slot)
 {
 	pid_t		sysloggerPid;
 	char	   *filename;
@@ -598,8 +598,7 @@ SysLogger_Start(void)
 	SysloggerStartupData startup_data;
 #endif							/* EXEC_BACKEND */
 
-	if (!Logging_collector)
-		return 0;
+	Assert(Logging_collector);
 
 	/*
 	 * If first time through, create the pipe which will receive stderr
@@ -695,6 +694,7 @@ SysLogger_Start(void)
 		pfree(filename);
 	}
 
+	MyPMChildSlot = child_slot;
 #ifdef EXEC_BACKEND
 	startup_data.syslogFile = syslogger_fdget(syslogFile);
 	startup_data.csvlogFile = syslogger_fdget(csvlogFile);
diff --git a/src/backend/storage/ipc/pmsignal.c b/src/backend/storage/ipc/pmsignal.c
index c801e9bec5..929ab570c1 100644
--- a/src/backend/storage/ipc/pmsignal.c
+++ b/src/backend/storage/ipc/pmsignal.c
@@ -47,11 +47,11 @@
  * exited without performing proper shutdown.  The per-child-process flags
  * have three possible states: UNUSED, ASSIGNED, ACTIVE.  An UNUSED slot is
  * available for assignment.  An ASSIGNED slot is associated with a postmaster
- * child process, but either the process has not touched shared memory yet,
- * or it has successfully cleaned up after itself.  A ACTIVE slot means the
- * process is actively using shared memory.  The slots are assigned to
- * child processes at random, and postmaster.c is responsible for tracking
- * which one goes with which PID.
+ * child process, but either the process has not touched shared memory yet, or
+ * it has successfully cleaned up after itself.  An ACTIVE slot means the
+ * process is actively using shared memory.  The slots are assigned to child
+ * processes by postmaster, and pmchild.c is responsible for tracking which
+ * one goes with which PID.
  *
  * Actually there is a fourth state, WALSENDER.  This is just like ACTIVE,
  * but carries the extra information that the child is a WAL sender.
@@ -84,13 +84,11 @@ struct PMSignalData
 NON_EXEC_STATIC volatile PMSignalData *PMSignalState = NULL;
 
 /*
- * These static variables are valid only in the postmaster.  We keep a
- * duplicative private array so that we can trust its state even if some
- * failing child has clobbered the PMSignalData struct in shared memory.
+ * Local copy of PMSignalState->num_child_flags, only valid in the
+ * postmaster.  Postmaster keeps a local copy so that it doesn't need to
+ * trust the value in shared memory.
  */
-static int	num_child_inuse;	/* # of entries in PMChildInUse[] */
-static int	next_child_inuse;	/* next slot to try to assign */
-static bool *PMChildInUse;		/* true if i'th flag slot is assigned */
+static int     num_child_flags;
 
 /*
  * Signal handler to be notified if postmaster dies.
@@ -155,25 +153,8 @@ PMSignalShmemInit(void)
 	{
 		/* initialize all flags to zeroes */
 		MemSet(unvolatize(PMSignalData *, PMSignalState), 0, PMSignalShmemSize());
-		num_child_inuse = MaxLivePostmasterChildren();
-		PMSignalState->num_child_flags = num_child_inuse;
-
-		/*
-		 * Also allocate postmaster's private PMChildInUse[] array.  We
-		 * might've already done that in a previous shared-memory creation
-		 * cycle, in which case free the old array to avoid a leak.  (Do it
-		 * like this to support the possibility that MaxLivePostmasterChildren
-		 * changed.)  In a standalone backend, we do not need this.
-		 */
-		if (PostmasterContext != NULL)
-		{
-			if (PMChildInUse)
-				pfree(PMChildInUse);
-			PMChildInUse = (bool *)
-				MemoryContextAllocZero(PostmasterContext,
-									   num_child_inuse * sizeof(bool));
-		}
-		next_child_inuse = 0;
+		num_child_flags = MaxLivePostmasterChildren();
+		PMSignalState->num_child_flags = num_child_flags;
 	}
 }
 
@@ -239,41 +220,22 @@ GetQuitSignalReason(void)
 
 
 /*
- * AssignPostmasterChildSlot - select an unused slot for a new postmaster
- * child process, and set its state to ASSIGNED.  Returns a slot number
- * (one to N).
+ * ReservePostmasterChildSlot - mark the given slot as ASSIGNED for a new
+ * postmaster child process.
  *
  * Only the postmaster is allowed to execute this routine, so we need no
  * special locking.
  */
-int
-AssignPostmasterChildSlot(void)
+void
+ReservePostmasterChildSlot(int slot)
 {
-	int			slot = next_child_inuse;
-	int			n;
+	Assert(slot > 0 && slot <= num_child_flags);
+	slot--;
 
-	/*
-	 * Scan for a free slot.  Notice that we trust nothing about the contents
-	 * of PMSignalState, but use only postmaster-local data for this decision.
-	 * We track the last slot assigned so as not to waste time repeatedly
-	 * rescanning low-numbered slots.
-	 */
-	for (n = num_child_inuse; n > 0; n--)
-	{
-		if (--slot < 0)
-			slot = num_child_inuse - 1;
-		if (!PMChildInUse[slot])
-		{
-			PMChildInUse[slot] = true;
-			PMSignalState->PMChildFlags[slot] = PM_CHILD_ASSIGNED;
-			next_child_inuse = slot;
-			return slot + 1;
-		}
-	}
+	if (PMSignalState->PMChildFlags[slot] != PM_CHILD_UNUSED)
+		elog(FATAL, "postmaster child slot is already in use");
 
-	/* Out of slots ... should never happen, else postmaster.c messed up */
-	elog(FATAL, "no free slots in PMChildFlags array");
-	return 0;					/* keep compiler quiet */
+	PMSignalState->PMChildFlags[slot] = PM_CHILD_ASSIGNED;
 }
 
 /*
@@ -288,7 +250,7 @@ ReleasePostmasterChildSlot(int slot)
 {
 	bool		result;
 
-	Assert(slot > 0 && slot <= num_child_inuse);
+	Assert(slot > 0 && slot <= num_child_flags);
 	slot--;
 
 	/*
@@ -298,7 +260,6 @@ ReleasePostmasterChildSlot(int slot)
 	 */
 	result = (PMSignalState->PMChildFlags[slot] == PM_CHILD_ASSIGNED);
 	PMSignalState->PMChildFlags[slot] = PM_CHILD_UNUSED;
-	PMChildInUse[slot] = false;
 	return result;
 }
 
@@ -309,7 +270,7 @@ ReleasePostmasterChildSlot(int slot)
 bool
 IsPostmasterChildWalSender(int slot)
 {
-	Assert(slot > 0 && slot <= num_child_inuse);
+	Assert(slot > 0 && slot <= num_child_flags);
 	slot--;
 
 	if (PMSignalState->PMChildFlags[slot] == PM_CHILD_WALSENDER)
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index eaf3916f28..8b7c1fafc5 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -357,14 +357,9 @@ InitProcess(void)
 	/*
 	 * Before we start accessing the shared memory in a serious way, mark
 	 * ourselves as an active postmaster child; this is so that the postmaster
-	 * can detect it if we exit without cleaning up.  (XXX autovac launcher
-	 * currently doesn't participate in this; it probably should.)
-	 *
-	 * Slot sync worker also does not participate in it, see comments atop
-	 * 'struct bkend' in postmaster.c.
+	 * can detect it if we exit without cleaning up.
 	 */
-	if (IsUnderPostmaster && !AmAutoVacuumLauncherProcess() &&
-		!AmLogicalSlotSyncWorkerProcess())
+	if (IsUnderPostmaster)
 		RegisterPostmasterChildActive();
 
 	/* Decide which list should supply our PGPROC. */
@@ -582,6 +577,9 @@ InitAuxiliaryProcess(void)
 	if (MyProc != NULL)
 		elog(ERROR, "you already exist");
 
+	if (IsUnderPostmaster)
+		RegisterPostmasterChildActive();
+
 	/*
 	 * We use the ProcStructLock to protect assignment and releasing of
 	 * AuxiliaryProcs entries.
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 63c12917cf..5a40aee3ce 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -13,8 +13,44 @@
 #ifndef _POSTMASTER_H
 #define _POSTMASTER_H
 
+#include "lib/ilist.h"
 #include "miscadmin.h"
 
+/*
+ * A struct representing an active postmaster child process.  This is used
+ * mainly to keep track of how many children we have and send them appropriate
+ * signals when necessary.  All postmaster child processes are assigned a
+ * PMChild entry. That includes "normal" client sessions, but also autovacuum
+ * workers, walsenders, background workers, and aux processes.  (Note that at
+ * the time of launch, walsenders are labeled B_BACKEND; we relabel them to
+ * B_WAL_SENDER upon noticing they've changed their PMChildFlags entry.  Hence
+ * that check must be done before any operation that needs to distinguish
+ * walsenders from normal backends.)
+ *
+ * "dead_end" children are also allocated a PMChild entry: these are children
+ * launched just for the purpose of sending a friendly rejection message to a
+ * would-be client.  We must track them because they are attached to shared
+ * memory, but we know they will never become live backends.
+ *
+ * 'child_slot' is an identifier that is unique across all running child
+ * processes.  It is used as an index into the PMChildFlags array. dead_end
+ * children are not assigned a child_slot and have child_slot == 0 (valid
+ * child_slot ids start from 1).
+ */
+typedef struct
+{
+	pid_t		pid;			/* process id of backend */
+	int			child_slot;		/* PMChildSlot for this backend, if any */
+	BackendType bkend_type;		/* child process flavor, see above */
+	struct RegisteredBgWorker *rw;	/* bgworker info, if this is a bgworker */
+	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
+	dlist_node	elem;			/* list link in BackendList */
+} PMChild;
+
+#ifdef EXEC_BACKEND
+extern int	num_pmchild_slots;
+#endif
+
 /* GUC options */
 extern PGDLLIMPORT bool EnableSSL;
 extern PGDLLIMPORT int SuperuserReservedConnections;
@@ -80,6 +116,15 @@ const char *PostmasterChildName(BackendType child_type);
 extern void SubPostmasterMain(int argc, char *argv[]) pg_attribute_noreturn();
 #endif
 
+/* prototypes for functions in pmchild.c */
+extern dlist_head ActiveChildList;
+
+extern void InitPostmasterChildSlots(void);
+extern PMChild *AssignPostmasterChildSlot(BackendType btype);
+extern bool FreePostmasterChildSlot(PMChild *pmchild);
+extern PMChild *FindPostmasterChildByPid(int pid);
+extern PMChild *AllocDeadEndChild(void);
+
 /*
  * Note: MAX_BACKENDS is limited to 2^18-1 because that's the width reserved
  * for buffer references in buf_internals.h.  This limitation could be lifted
diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h
index 94ea263f2b..27bd16ae1d 100644
--- a/src/include/postmaster/syslogger.h
+++ b/src/include/postmaster/syslogger.h
@@ -86,7 +86,7 @@ extern PGDLLIMPORT HANDLE syslogPipe[2];
 #endif
 
 
-extern int	SysLogger_Start(void);
+extern int	SysLogger_Start(int child_slot);
 
 extern void write_syslogger_file(const char *buffer, int count, int destination);
 
diff --git a/src/include/storage/pmsignal.h b/src/include/storage/pmsignal.h
index ce4620af1f..4a32f7fed0 100644
--- a/src/include/storage/pmsignal.h
+++ b/src/include/storage/pmsignal.h
@@ -70,7 +70,7 @@ extern void SendPostmasterSignal(PMSignalReason reason);
 extern bool CheckPostmasterSignal(PMSignalReason reason);
 extern void SetQuitSignalReason(QuitSignalReason reason);
 extern QuitSignalReason GetQuitSignalReason(void);
-extern int	AssignPostmasterChildSlot(void);
+extern void ReservePostmasterChildSlot(int slot);
 extern bool ReleasePostmasterChildSlot(int slot);
 extern bool IsPostmasterChildWalSender(int slot);
 extern void RegisterPostmasterChildActive(void);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6f98fbc6f5..6d77a9fc17 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -230,7 +230,6 @@ BTWriteState
 BUF_MEM
 BYTE
 BY_HANDLE_FILE_INFORMATION
-Backend
 BackendParameters
 BackendStartupData
 BackendState
@@ -1931,6 +1930,8 @@ PLyTransformToOb
 PLyTupleToOb
 PLyUnicode_FromStringAndSize_t
 PLy_elog_impl_t
+PMChild
+PMChildPool
 PMINIDUMP_CALLBACK_INFORMATION
 PMINIDUMP_EXCEPTION_INFORMATION
 PMINIDUMP_USER_STREAM_INFORMATION
-- 
2.39.5



  [text/x-patch] 0004-Pass-MyPMChildSlot-as-an-explicit-argument-to-child-.patch (8.5K, ../../[email protected]/5-0004-Pass-MyPMChildSlot-as-an-explicit-argument-to-child-.patch)
  download | inline diff:
From 935a3708fae9bc721ea23ebb5955705252ab4b37 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Wed, 9 Oct 2024 22:15:41 +0300
Subject: [PATCH 4/4] Pass MyPMChildSlot as an explicit argument to child
 process

All the other global variables passed from postmaster to child have
the same value in all the processes, while MyPMChildSlot is more like
a parameter to each child process.

Reviewed-by: Andres Freund <[email protected]>
Discussion: https://www.postgresql.org/message-id/[email protected]
---
 src/backend/postmaster/launch_backend.c | 32 ++++++++++++++++---------
 src/backend/postmaster/postmaster.c     | 10 ++++----
 src/backend/postmaster/syslogger.c      |  7 +++---
 src/include/postmaster/postmaster.h     |  1 +
 4 files changed, 30 insertions(+), 20 deletions(-)

diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index 02755b6448..20c8d1e038 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -96,7 +96,6 @@ typedef int InheritableSocket;
 typedef struct
 {
 	char		DataDir[MAXPGPATH];
-	int			MyPMChildSlot;
 #ifndef WIN32
 	unsigned long UsedShmemSegID;
 #else
@@ -138,6 +137,8 @@ typedef struct
 	char		my_exec_path[MAXPGPATH];
 	char		pkglib_path[MAXPGPATH];
 
+	int			MyPMChildSlot;
+
 	/*
 	 * These are only used by backend processes, but are here because passing
 	 * a socket needs some special handling on Windows. 'client_sock' is an
@@ -159,13 +160,16 @@ typedef struct
 static void read_backend_variables(char *id, char **startup_data, size_t *startup_data_len);
 static void restore_backend_variables(BackendParameters *param);
 
-static bool save_backend_variables(BackendParameters *param, ClientSocket *client_sock,
+static bool save_backend_variables(BackendParameters *param, int child_slot,
+								   ClientSocket *client_sock,
 #ifdef WIN32
 								   HANDLE childProcess, pid_t childPid,
 #endif
 								   char *startup_data, size_t startup_data_len);
 
-static pid_t internal_forkexec(const char *child_kind, char *startup_data, size_t startup_data_len, ClientSocket *client_sock);
+static pid_t internal_forkexec(const char *child_kind, int child_slot,
+							   char *startup_data, size_t startup_data_len,
+							   ClientSocket *client_sock);
 
 #endif							/* EXEC_BACKEND */
 
@@ -227,7 +231,7 @@ PostmasterChildName(BackendType child_type)
  * the child process.
  */
 pid_t
-postmaster_child_launch(BackendType child_type,
+postmaster_child_launch(BackendType child_type, int child_slot,
 						char *startup_data, size_t startup_data_len,
 						ClientSocket *client_sock)
 {
@@ -236,7 +240,7 @@ postmaster_child_launch(BackendType child_type,
 	Assert(IsPostmasterEnvironment && !IsUnderPostmaster);
 
 #ifdef EXEC_BACKEND
-	pid = internal_forkexec(child_process_kinds[child_type].name,
+	pid = internal_forkexec(child_process_kinds[child_type].name, child_slot,
 							startup_data, startup_data_len, client_sock);
 	/* the child process will arrive in SubPostmasterMain */
 #else							/* !EXEC_BACKEND */
@@ -264,6 +268,7 @@ postmaster_child_launch(BackendType child_type,
 		 */
 		MemoryContextSwitchTo(TopMemoryContext);
 
+		MyPMChildSlot = child_slot;
 		if (client_sock)
 		{
 			MyClientSocket = palloc(sizeof(ClientSocket));
@@ -290,7 +295,8 @@ postmaster_child_launch(BackendType child_type,
  * - fork():s, and then exec():s the child process
  */
 static pid_t
-internal_forkexec(const char *child_kind, char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
+internal_forkexec(const char *child_kind, int child_slot,
+				  char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
 {
 	static unsigned long tmpBackendFileNum = 0;
 	pid_t		pid;
@@ -310,7 +316,7 @@ internal_forkexec(const char *child_kind, char *startup_data, size_t startup_dat
 	 */
 	paramsz = SizeOfBackendParameters(startup_data_len);
 	param = palloc0(paramsz);
-	if (!save_backend_variables(param, client_sock, startup_data, startup_data_len))
+	if (!save_backend_variables(param, child_slot, client_sock, startup_data, startup_data_len))
 	{
 		pfree(param);
 		return -1;				/* log made by save_backend_variables */
@@ -399,7 +405,8 @@ internal_forkexec(const char *child_kind, char *startup_data, size_t startup_dat
  *	 file is complete.
  */
 static pid_t
-internal_forkexec(const char *child_kind, char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
+internal_forkexec(const char *child_kind, int child_slot,
+				  char *startup_data, size_t startup_data_len, ClientSocket *client_sock)
 {
 	int			retry_count = 0;
 	STARTUPINFO si;
@@ -480,7 +487,9 @@ retry:
 		return -1;
 	}
 
-	if (!save_backend_variables(param, client_sock, pi.hProcess, pi.dwProcessId, startup_data, startup_data_len))
+	if (!save_backend_variables(param, child_slot, client_sock,
+								pi.hProcess, pi.dwProcessId,
+								startup_data, startup_data_len))
 	{
 		/*
 		 * log made by save_backend_variables, but we have to clean up the
@@ -692,7 +701,8 @@ static void read_inheritable_socket(SOCKET *dest, InheritableSocket *src);
 
 /* Save critical backend variables into the BackendParameters struct */
 static bool
-save_backend_variables(BackendParameters *param, ClientSocket *client_sock,
+save_backend_variables(BackendParameters *param,
+					   int child_slot, ClientSocket *client_sock,
 #ifdef WIN32
 					   HANDLE childProcess, pid_t childPid,
 #endif
@@ -709,7 +719,7 @@ save_backend_variables(BackendParameters *param, ClientSocket *client_sock,
 
 	strlcpy(param->DataDir, DataDir, MAXPGPATH);
 
-	param->MyPMChildSlot = MyPMChildSlot;
+	param->MyPMChildSlot = child_slot;
 
 #ifdef WIN32
 	param->ShmemProtectiveRegion = ShmemProtectiveRegion;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 97a1b7ae1a..6eab012dc2 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -3376,8 +3376,7 @@ BackendStartup(ClientSocket *client_sock)
 	/* Hasn't asked to be notified about any bgworkers yet */
 	bn->bgworker_notify = false;
 
-	MyPMChildSlot = bn->child_slot;
-	pid = postmaster_child_launch(bn->bkend_type,
+	pid = postmaster_child_launch(bn->bkend_type, bn->child_slot,
 								  (char *) &startup_data, sizeof(startup_data),
 								  client_sock);
 	if (pid < 0)
@@ -3702,8 +3701,7 @@ StartChildProcess(BackendType type)
 		return NULL;
 	}
 
-	MyPMChildSlot = pmchild->child_slot;
-	pid = postmaster_child_launch(type, NULL, 0, NULL);
+	pid = postmaster_child_launch(type, pmchild->child_slot, NULL, 0, NULL);
 	if (pid < 0)
 	{
 		/* in parent, fork failed */
@@ -3875,8 +3873,8 @@ do_start_bgworker(RegisteredBgWorker *rw)
 			(errmsg_internal("starting background worker process \"%s\"",
 							 rw->rw_worker.bgw_name)));
 
-	MyPMChildSlot = bn->child_slot;
-	worker_pid = postmaster_child_launch(B_BG_WORKER, (char *) &rw->rw_worker, sizeof(BackgroundWorker), NULL);
+	worker_pid = postmaster_child_launch(B_BG_WORKER, bn->child_slot,
+										 (char *) &rw->rw_worker, sizeof(BackgroundWorker), NULL);
 	if (worker_pid == -1)
 	{
 		/* in postmaster, fork failed ... */
diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c
index 7ca24c6663..f12639056f 100644
--- a/src/backend/postmaster/syslogger.c
+++ b/src/backend/postmaster/syslogger.c
@@ -694,14 +694,15 @@ SysLogger_Start(int child_slot)
 		pfree(filename);
 	}
 
-	MyPMChildSlot = child_slot;
 #ifdef EXEC_BACKEND
 	startup_data.syslogFile = syslogger_fdget(syslogFile);
 	startup_data.csvlogFile = syslogger_fdget(csvlogFile);
 	startup_data.jsonlogFile = syslogger_fdget(jsonlogFile);
-	sysloggerPid = postmaster_child_launch(B_LOGGER, (char *) &startup_data, sizeof(startup_data), NULL);
+	sysloggerPid = postmaster_child_launch(B_LOGGER, child_slot,
+										   (char *) &startup_data, sizeof(startup_data), NULL);
 #else
-	sysloggerPid = postmaster_child_launch(B_LOGGER, NULL, 0, NULL);
+	sysloggerPid = postmaster_child_launch(B_LOGGER, child_slot,
+										   NULL, 0, NULL);
 #endif							/* EXEC_BACKEND */
 
 	if (sysloggerPid == -1)
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 5a40aee3ce..df18e6cfc5 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -108,6 +108,7 @@ extern PGDLLIMPORT struct ClientSocket *MyClientSocket;
 
 /* prototypes for functions in launch_backend.c */
 extern pid_t postmaster_child_launch(BackendType child_type,
+									 int child_slot,
 									 char *startup_data,
 									 size_t startup_data_len,
 									 struct ClientSocket *client_sock);
-- 
2.39.5



^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-11-14 14:13  Heikki Linnakangas <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Heikki Linnakangas @ 2024-11-14 14:13 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers

On 09/10/2024 23:40, Heikki Linnakangas wrote:
> I pushed the first three patches, with the new test and one of the small
> refactoring patches. Thanks for all the comments so far! Here is a new
> version of the remaining patches.
> 
> Lots of little cleanups and changes here and there since the last
> versions, but the notable bigger changes are:
> 
> - There is now a BackendTypeMask datatype, so that if you try to mix up
> bitmasks and plain BackendType values, the compiler will complain.
> 
> - pmchild.c has been rewritten per feedback, so that the "pools" of
> PMChild structs are more explicit. The size of each pool is only stated
> once, whereas before the same logic was duplicated in
> MaxLivePostmasterChildren() which calculates the number of slots and in
> InitPostmasterChildSlots() which allocates them.
> 
> - In PostmasterStateMachine(), I combined the code to handle
> PM_STOP_BACKENDS and PM_WAIT_BACKENDS. They are essentially the same
> state, except that PM_STOP_BACKENDS first sends the signal to all the
> child processes that it will then wait for. They both needed to build
> the same bitmask of processes to signal or wait for; this eliminates the
> duplication.

Made a few more changes since last patch version:

- Fixed initialization in pmchild.c in single-user and bootstrapping mode
- inlined assign_backendlist_entry() into its only caller; it wasn't 
doing much anymore
- cleaned up some leftovers in canAcceptConnections()
- Renamed some functions for clarity, fixed some leftover comments that 
still talked about Backend structs and BackendList

With those changes, committed. Thanks for the review!

-- 
Heikki Linnakangas
Neon (https://neon.tech)







^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-12-08 23:12  Tomas Vondra <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  0 siblings, 2 replies; 39+ messages in thread

From: Tomas Vondra @ 2024-12-08 23:12 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; Andres Freund <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers

On 11/14/24 15:13, Heikki Linnakangas wrote:
> On 09/10/2024 23:40, Heikki Linnakangas wrote:
>> I pushed the first three patches, with the new test and one of the small
>> refactoring patches. Thanks for all the comments so far! Here is a new
>> version of the remaining patches.
>>
Hi, the TAP test 001_connection_limits.pl introduced by 6a1d0d470e84
seems to have problems with valgrind :-( I reliably get this failure:


t/001_connection_limits.pl .. 3/? # Tests were run but no plan was
declared and done_testing() was not seen.
# Looks like your test exited with 29 just after 4.
t/001_connection_limits.pl .. Dubious, test returned 29 (wstat 7424, 0x1d00)
All 4 subtests passed


and tmp_check/log/regress_log_001_connection_limits says:


[23:48:44.444](1.129s) ok 3 - reserved_connections limit
[23:48:44.445](0.001s) ok 4 - reserved_connections limit: matches
process ended prematurely at
/home/user/work/postgres/src/test/postmaster/../../../src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
line 154.
# Postmaster PID for node "primary" is 198592


That BackgroundPsql.pm line is this in wait_connect()

  $self->{run}->pump()
    until $self->{stdout} =~ /$banner/ || $self->{timeout}->is_expired;

By trial and error I found that it fails on this line 70:

  push(@sessions, background_psql_as_user('regress_superuser'));

but I have no idea idea why. There are multiple similar calls a couple
lines earlier, and those work fine. And various other TAP tests with
background_sql() work fine too.

So what's so special about this particular line?


regards

-- 
Tomas Vondra







^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-12-09 12:30  Heikki Linnakangas <[email protected]>
  parent: Tomas Vondra <[email protected]>
  1 sibling, 1 reply; 39+ messages in thread

From: Heikki Linnakangas @ 2024-12-09 12:30 UTC (permalink / raw)
  To: Tomas Vondra <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers; Andres Freund <[email protected]>

On 09/12/2024 01:12, Tomas Vondra wrote:
> On 11/14/24 15:13, Heikki Linnakangas wrote:
>> On 09/10/2024 23:40, Heikki Linnakangas wrote:
>>> I pushed the first three patches, with the new test and one of the small
>>> refactoring patches. Thanks for all the comments so far! Here is a new
>>> version of the remaining patches.
>>>
> Hi, the TAP test 001_connection_limits.pl introduced by 6a1d0d470e84
> seems to have problems with valgrind :-( I reliably get this failure:

How exactly do you run the test with valgrind? What platform?

It works for me, with this:

(cd build && ninja && rm -rf tmp_install && meson test --suite setup && 
valgrind --leak-check=no --gen-suppressions=all 
--suppressions=/home/heikki/git-sandbox/postgresql/src/tools/valgrind.supp 
--time-stamp=yes 
--error-markers=VALGRINDERROR-BEGIN,VALGRINDERROR-END 
--log-file=$HOME/pg-valgrind/%p.log --trace-children=yes meson test 
--suite postmaster )

> t/001_connection_limits.pl .. 3/? # Tests were run but no plan was
> declared and done_testing() was not seen.
> # Looks like your test exited with 29 just after 4.
> t/001_connection_limits.pl .. Dubious, test returned 29 (wstat 7424, 0x1d00)
> All 4 subtests passed
> 
> 
> and tmp_check/log/regress_log_001_connection_limits says:
> 
> 
> [23:48:44.444](1.129s) ok 3 - reserved_connections limit
> [23:48:44.445](0.001s) ok 4 - reserved_connections limit: matches
> process ended prematurely at
> /home/user/work/postgres/src/test/postmaster/../../../src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
> line 154.
> # Postmaster PID for node "primary" is 198592
> 
> 
> That BackgroundPsql.pm line is this in wait_connect()
> 
>    $self->{run}->pump()
>      until $self->{stdout} =~ /$banner/ || $self->{timeout}->is_expired;
> 
> By trial and error I found that it fails on this line 70:
> 
>    push(@sessions, background_psql_as_user('regress_superuser'));
> 
> but I have no idea idea why. There are multiple similar calls a couple
> lines earlier, and those work fine. And various other TAP tests with
> background_sql() work fine too.
> 
> So what's so special about this particular line?

Weird. Valgrind makes everything slow; is it a timeout? Any other clues 
in the logs?

-- 
Heikki Linnakangas
Neon (https://neon.tech)






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-12-09 12:47  Tomas Vondra <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Tomas Vondra @ 2024-12-09 12:47 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers; Andres Freund <[email protected]>

On 12/9/24 13:30, Heikki Linnakangas wrote:
> On 09/12/2024 01:12, Tomas Vondra wrote:
>> On 11/14/24 15:13, Heikki Linnakangas wrote:
>>> On 09/10/2024 23:40, Heikki Linnakangas wrote:
>>>> I pushed the first three patches, with the new test and one of the
>>>> small
>>>> refactoring patches. Thanks for all the comments so far! Here is a new
>>>> version of the remaining patches.
>>>>
>> Hi, the TAP test 001_connection_limits.pl introduced by 6a1d0d470e84
>> seems to have problems with valgrind :-( I reliably get this failure:
> 
> How exactly do you run the test with valgrind? What platform?
> 

It failed for me on both amd64 (Fedora 41) and rpi5 32/64-bit (Debian).

> It works for me, with this:
> 
> (cd build && ninja && rm -rf tmp_install && meson test --suite setup &&
> valgrind --leak-check=no --gen-suppressions=all --suppressions=/home/
> heikki/git-sandbox/postgresql/src/tools/valgrind.supp --time-stamp=yes
> --error-markers=VALGRINDERROR-BEGIN,VALGRINDERROR-END --log-file=$HOME/
> pg-valgrind/%p.log --trace-children=yes meson test --suite postmaster )
> 

I have a patch that tweaks pg_ctl/pg_regress to execute valgrind, so I
just do

./configure --enable-debug --prefix=/home/user/builds/master
--enable-depend --enable-cassert --enable-tap-tests CPPFLAGS="-O0 -ggdb3
-DUSE_VALGRIND"

and then the usual "make check" or whatever.

The patch has a hardcoded path to the .supp file, and places the
valgrind log into /tmp. It has worked for me fine up until that commit,
and it still seems to be working in every other test directory.

>> t/001_connection_limits.pl .. 3/? # Tests were run but no plan was
>> declared and done_testing() was not seen.
>> # Looks like your test exited with 29 just after 4.
>> t/001_connection_limits.pl .. Dubious, test returned 29 (wstat 7424,
>> 0x1d00)
>> All 4 subtests passed
>>
>>
>> and tmp_check/log/regress_log_001_connection_limits says:
>>
>>
>> [23:48:44.444](1.129s) ok 3 - reserved_connections limit
>> [23:48:44.445](0.001s) ok 4 - reserved_connections limit: matches
>> process ended prematurely at
>> /home/user/work/postgres/src/test/postmaster/../../../src/test/perl/
>> PostgreSQL/Test/BackgroundPsql.pm
>> line 154.
>> # Postmaster PID for node "primary" is 198592
>>
>>
>> That BackgroundPsql.pm line is this in wait_connect()
>>
>>    $self->{run}->pump()
>>      until $self->{stdout} =~ /$banner/ || $self->{timeout}->is_expired;
>>
>> By trial and error I found that it fails on this line 70:
>>
>>    push(@sessions, background_psql_as_user('regress_superuser'));
>>
>> but I have no idea idea why. There are multiple similar calls a couple
>> lines earlier, and those work fine. And various other TAP tests with
>> background_sql() work fine too.
>>
>> So what's so special about this particular line?
> 
> Weird. Valgrind makes everything slow; is it a timeout? Any other clues
> in the logs?
> 

Yeah, weird.

Timeouts were the first thing I thought about, but it fails even if I
set PGCTLTIMEOUT/PG_TEST_TIMEOUT_DEFAULT to 3600. And it doesn't seem to
be waiting for anything for that long :-(


regards

-- 
Tomas Vondra


Attachments:

  [text/x-patch] valgrind-master.patch (2.2K, ../../[email protected]/2-valgrind-master.patch)
  download | inline diff:
diff --git a/src/bin/pg_ctl/pg_ctl.c b/src/bin/pg_ctl/pg_ctl.c
index d6bb2c33119..83544aeb5e2 100644
--- a/src/bin/pg_ctl/pg_ctl.c
+++ b/src/bin/pg_ctl/pg_ctl.c
@@ -487,12 +487,12 @@ start_postmaster(void)
 	 * has the same PID as the current child process.
 	 */
 	if (log_file != NULL)
-		cmd = psprintf("exec \"%s\" %s%s < \"%s\" >> \"%s\" 2>&1",
-					   exec_path, pgdata_opt, post_opts,
+		cmd = psprintf("exec valgrind --quiet --trace-children=yes --track-origins=yes --read-var-info=yes --num-callers=20 --leak-check=no --gen-suppressions=all --suppressions=/home/user/work/postgres/src/tools/valgrind.supp --error-limit=no --log-file=/tmp/valgrind.%d.log \"%s\" %s%s < \"%s\" >> \"%s\" 2>&1",
+					   getpid(), exec_path, pgdata_opt, post_opts,
 					   DEVNULL, log_file);
 	else
-		cmd = psprintf("exec \"%s\" %s%s < \"%s\" 2>&1",
-					   exec_path, pgdata_opt, post_opts, DEVNULL);
+		cmd = psprintf("exec valgrind --quiet --trace-children=yes --track-origins=yes --read-var-info=yes --num-callers=20 --leak-check=no --gen-suppressions=all --suppressions=/home/user/work/postgres/src/tools/valgrind.supp --error-limit=no --log-file=/tmp/valgrind.%d.log \"%s\" %s%s < \"%s\" 2>&1",
+					   getpid(), exec_path, pgdata_opt, post_opts, DEVNULL);
 
 	(void) execl("/bin/sh", "/bin/sh", "-c", cmd, (char *) NULL);
 
diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c
index 0e40ed32a21..97d880c3deb 100644
--- a/src/test/regress/pg_regress.c
+++ b/src/test/regress/pg_regress.c
@@ -2478,10 +2478,10 @@ regression_main(int argc, char *argv[],
 		 * Start the temp postmaster
 		 */
 		snprintf(buf, sizeof(buf),
-				 "\"%s%spostgres\" -D \"%s/data\" -F%s "
+				 "valgrind --quiet --trace-children=yes --track-origins=yes --read-var-info=yes --num-callers=20 --leak-check=no --gen-suppressions=all --suppressions=/home/user/work/postgres/src/tools/valgrind.supp --error-limit=no --log-file=/tmp/valgrind.%d.log \"%s%spostgres\" -D \"%s/data\" -F%s "
 				 "-c \"listen_addresses=%s\" -k \"%s\" "
 				 "> \"%s/log/postmaster.log\" 2>&1",
-				 bindir ? bindir : "",
+				 getpid(), bindir ? bindir : "",
 				 bindir ? "/" : "",
 				 temp_instance, debug ? " -d 5" : "",
 				 hostname ? hostname : "", sockdir ? sockdir : "",


^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-12-09 20:55  Heikki Linnakangas <[email protected]>
  parent: Tomas Vondra <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Heikki Linnakangas @ 2024-12-09 20:55 UTC (permalink / raw)
  To: Tomas Vondra <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers; Andres Freund <[email protected]>

On 09/12/2024 14:47, Tomas Vondra wrote:
> On 12/9/24 13:30, Heikki Linnakangas wrote:
>> On 09/12/2024 01:12, Tomas Vondra wrote:
>>> On 11/14/24 15:13, Heikki Linnakangas wrote:
>>>> On 09/10/2024 23:40, Heikki Linnakangas wrote:
>>>>> I pushed the first three patches, with the new test and one of the
>>>>> small
>>>>> refactoring patches. Thanks for all the comments so far! Here is a new
>>>>> version of the remaining patches.
>>>>>
>>> Hi, the TAP test 001_connection_limits.pl introduced by 6a1d0d470e84
>>> seems to have problems with valgrind :-( I reliably get this failure:
>>
>> How exactly do you run the test with valgrind? What platform?
>>
> 
> It failed for me on both amd64 (Fedora 41) and rpi5 32/64-bit (Debian).
> 
>> It works for me, with this:
>>
>> (cd build && ninja && rm -rf tmp_install && meson test --suite setup &&
>> valgrind --leak-check=no --gen-suppressions=all --suppressions=/home/
>> heikki/git-sandbox/postgresql/src/tools/valgrind.supp --time-stamp=yes
>> --error-markers=VALGRINDERROR-BEGIN,VALGRINDERROR-END --log-file=$HOME/
>> pg-valgrind/%p.log --trace-children=yes meson test --suite postmaster )
>>
> 
> I have a patch that tweaks pg_ctl/pg_regress to execute valgrind, so I
> just do
> 
> ./configure --enable-debug --prefix=/home/user/builds/master
> --enable-depend --enable-cassert --enable-tap-tests CPPFLAGS="-O0 -ggdb3
> -DUSE_VALGRIND"
> 
> and then the usual "make check" or whatever.
> 
> The patch has a hardcoded path to the .supp file, and places the
> valgrind log into /tmp. It has worked for me fine up until that commit,
> and it still seems to be working in every other test directory.

Ok, I was able to reproduce this with that setup.

Unsurprisingly, it's a timing issue. It can be reproduced without 
valgrind by adding this delay:

diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 289059435a9..1eb6bad72ca 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -583,6 +583,7 @@ errfinish(const char *filename, int lineno, const 
char *funcname)
  		 * FATAL termination.  The postmaster may or may not consider this
  		 * worthy of panic, depending on which subprocess returns it.
  		 */
+		sleep(1);
  		proc_exit(1);
  	}

The test opens a connection that is expected to fail with the "remaining 
connection slots are reserved for roles with the SUPERUSER attribute" 
error. Right after that, it opens a new connection as superuser, and 
expects it to succeed. But if the previous backend hasn't exited yet, 
the new connection fails with "too many clients already".

Not sure how to fix this. A small sleep in the test would work, but in 
principle there's no delay that's guaranteed to be enough. A more robust 
solution would be to run a "select count(*) from pg_stat_activity" and 
wait until the number of connections are what's expected. I'll try that 
and see how complicated that gets..

-- 
Heikki Linnakangas
Neon (https://neon.tech)






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-12-10 10:00  Heikki Linnakangas <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  0 siblings, 2 replies; 39+ messages in thread

From: Heikki Linnakangas @ 2024-12-10 10:00 UTC (permalink / raw)
  To: Tomas Vondra <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers; Andres Freund <[email protected]>

On 09/12/2024 22:55, Heikki Linnakangas wrote:
> Not sure how to fix this. A small sleep in the test would work, but in 
> principle there's no delay that's guaranteed to be enough. A more robust 
> solution would be to run a "select count(*) from pg_stat_activity" and 
> wait until the number of connections are what's expected. I'll try that 
> and see how complicated that gets..

Checking pg_stat_activity doesn't help, because the backend doesn't 
register itself in pg_stat_activity until later. A connection that's 
rejected due to connection limits never shows up in pg_stat_activity.

Some options:

0. Do nothing

1. Add a small sleep to the test

2. Move the pgstat_bestart() call earlier in the startup sequence, so 
that a backend shows up in pg_stat_activity before it acquires a PGPROC 
entry, and stays visible until after it has released its PGPROC entry. 
This would give more visibility to backends that are starting up.

3. Rearrange the FATAL error handling so that the process removes itself 
from PGPROC before sending the error to the client. That would be kind 
of nice anyway. Currently, if sending the rejection error message to the 
client blocks, you are holding up a PGPROC slot until the message is 
sent. The error message packet is short, so it's highly unlikely to 
block, but still.

Option 3 seems kind of nice in principle, but looking at the code, it's 
a bit awkward to implement.  Easiest way to implement it would be to 
modify send_message_to_frontend() to not call pq_flush() on FATAL 
errors, and flush the data in socket_close() instead. Not a lot of code, 
but it's a pretty ugly special case.

Option 2 seems nice too, but seems like a lot of work.

-- 
Heikki Linnakangas
Neon (https://neon.tech)







^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2024-12-10 11:39  Tomas Vondra <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  1 sibling, 0 replies; 39+ messages in thread

From: Tomas Vondra @ 2024-12-10 11:39 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Thomas Munro <[email protected]>; pgsql-hackers; Andres Freund <[email protected]>



On 12/10/24 11:00, Heikki Linnakangas wrote:
> On 09/12/2024 22:55, Heikki Linnakangas wrote:
>> Not sure how to fix this. A small sleep in the test would work, but in
>> principle there's no delay that's guaranteed to be enough. A more
>> robust solution would be to run a "select count(*) from
>> pg_stat_activity" and wait until the number of connections are what's
>> expected. I'll try that and see how complicated that gets..
> 
> Checking pg_stat_activity doesn't help, because the backend doesn't
> register itself in pg_stat_activity until later. A connection that's
> rejected due to connection limits never shows up in pg_stat_activity.
> 
> Some options:
> 
> 0. Do nothing
> 
> 1. Add a small sleep to the test
> 

I'd just add a short sleep. Yeah, sleeps are not great, but everything
else seems like a lot of effort just to make this one test pass under
valgrind, and I don't think it's worth it.

Can we make the sleep conditional on valgrind, so that regular builds
are not affected? I guess regular builds could fail too, but I don't
think we've seen such failures until now.


regards

-- 
Tomas Vondra







^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2025-03-04 22:50  Andres Freund <[email protected]>
  parent: Tomas Vondra <[email protected]>
  1 sibling, 0 replies; 39+ messages in thread

From: Andres Freund @ 2025-03-04 22:50 UTC (permalink / raw)
  To: Tomas Vondra <[email protected]>; Noah Misch <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Thomas Munro <[email protected]>; pgsql-hackers

Hi,

On 2024-12-09 00:12:32 +0100, Tomas Vondra wrote:
> Hi, the TAP test 001_connection_limits.pl introduced by 6a1d0d470e84
> seems to have problems with valgrind :-( I reliably get this failure:
> 
> 
> t/001_connection_limits.pl .. 3/? # Tests were run but no plan was
> declared and done_testing() was not seen.
> # Looks like your test exited with 29 just after 4.
> t/001_connection_limits.pl .. Dubious, test returned 29 (wstat 7424, 0x1d00)
> All 4 subtests passed
> 
> 
> and tmp_check/log/regress_log_001_connection_limits says:
> 
> 
> [23:48:44.444](1.129s) ok 3 - reserved_connections limit
> [23:48:44.445](0.001s) ok 4 - reserved_connections limit: matches
> process ended prematurely at
> /home/user/work/postgres/src/test/postmaster/../../../src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
> line 154.
> # Postmaster PID for node "primary" is 198592


I just saw this failure on skink in the BF:
https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=skink&dt=2025-03-04%2015%3A43%3A23

[17:05:56.438](0.247s) ok 3 - reserved_connections limit
[17:05:56.438](0.000s) ok 4 - reserved_connections limit: matches
process ended prematurely at /home/bf/bf-build/skink-master/HEAD/pgsql/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm line 160.


> That BackgroundPsql.pm line is this in wait_connect()
> 
>   $self->{run}->pump()
>     until $self->{stdout} =~ /$banner/ || $self->{timeout}->is_expired;

A big part of the problem here imo is the exception behaviour that
IPC::Run::pump() has:

  If pump() is called after all harnessed activities have completed, a "process
  ended prematurely" exception to be thrown.  This allows for simple scripting
  of external applications without having to add lots of error handling code at
  each step of the script:

Which is, uh, not very compatible with how we use IPC::Run (here and
elsewhere).  Just ending the test because a connection failed is pretty awful.


This behaviour makes it really hard to debug problems. It'd have been a lot
easier to understand the problem if we'd seen psql's stderr before the test
died.

I guess that mean at the very least we'd need to put an eval {} around the
->pump() call., print $self->{stdout}, ->{stderr} and reraise an error?
Presumably not just in in wait_connect(), but also at least in pump_until()?



Will respond downthread to a potential workaround for the issue.

Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2025-03-04 22:58  Andres Freund <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  1 sibling, 1 reply; 39+ messages in thread

From: Andres Freund @ 2025-03-04 22:58 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Thomas Munro <[email protected]>; pgsql-hackers

Hi,

On 2024-12-10 12:00:12 +0200, Heikki Linnakangas wrote:
> On 09/12/2024 22:55, Heikki Linnakangas wrote:
> > Not sure how to fix this. A small sleep in the test would work, but in
> > principle there's no delay that's guaranteed to be enough. A more robust
> > solution would be to run a "select count(*) from pg_stat_activity" and
> > wait until the number of connections are what's expected. I'll try that
> > and see how complicated that gets..
> 
> Checking pg_stat_activity doesn't help, because the backend doesn't register
> itself in pg_stat_activity until later. A connection that's rejected due to
> connection limits never shows up in pg_stat_activity.
> 
> Some options:
> 
> 0. Do nothing
> 
> 1. Add a small sleep to the test
> 
> 2. Move the pgstat_bestart() call earlier in the startup sequence, so that a
> backend shows up in pg_stat_activity before it acquires a PGPROC entry, and
> stays visible until after it has released its PGPROC entry. This would give
> more visibility to backends that are starting up.

We don't necessarily *have* a PGPROC entry for that backend when we run out of
connections, no?



> 3. Rearrange the FATAL error handling so that the process removes itself
> from PGPROC before sending the error to the client. That would be kind of
> nice anyway. Currently, if sending the rejection error message to the client
> blocks, you are holding up a PGPROC slot until the message is sent. The
> error message packet is short, so it's highly unlikely to block, but still.

This is definitely a problem, there was even a recent thread about it. It can
be triggered even with just an ERROR message though :(


For this test, could we perhaps rely on the log messages postmaster logs when
child processes exit?

2025-03-04 17:56:12.528 EST [3509838][not initialized][:0][[unknown]] LOG:  connection received: host=[local]
2025-03-04 17:56:12.528 EST [3509838][client backend][:0][[unknown]] FATAL:  sorry, too many clients already
2025-03-04 17:56:12.529 EST [3509817][postmaster][:0][] DEBUG:  releasing pm child slot 2
2025-03-04 17:56:12.529 EST [3509817][postmaster][:0][] DEBUG:  client backend (PID 3509838) exited with exit code 1

I.e. the test could wait for the 'client backend exited' message using
->wait_for_log()?

Greetings,

Andres Freund






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2025-03-04 23:23  Michael Paquier <[email protected]>
  parent: Andres Freund <[email protected]>
  0 siblings, 1 reply; 39+ messages in thread

From: Michael Paquier @ 2025-03-04 23:23 UTC (permalink / raw)
  To: Andres Freund <[email protected]>; +Cc: Heikki Linnakangas <[email protected]>; Tomas Vondra <[email protected]>; Thomas Munro <[email protected]>; pgsql-hackers

On Tue, Mar 04, 2025 at 05:58:42PM -0500, Andres Freund wrote:
> On 2024-12-10 12:00:12 +0200, Heikki Linnakangas wrote:
>> 2. Move the pgstat_bestart() call earlier in the startup sequence, so that a
>> backend shows up in pg_stat_activity before it acquires a PGPROC entry, and
>> stays visible until after it has released its PGPROC entry. This would give
>> more visibility to backends that are starting up.
> 
> We don't necessarily *have* a PGPROC entry for that backend when we run out of
> connections, no?

Exactly.  If I got this thread's argument right, you cannot have a
PGPROC entry that could be plugged into pg_stat_activity that early
during the startup process when collecting the startup packet.

> For this test, could we perhaps rely on the log messages postmaster logs when
> child processes exit?
> 
> 2025-03-04 17:56:12.528 EST [3509838][not initialized][:0][[unknown]] LOG:  connection received: host=[local]
> 2025-03-04 17:56:12.528 EST [3509838][client backend][:0][[unknown]] FATAL:  sorry, too many clients already
> 2025-03-04 17:56:12.529 EST [3509817][postmaster][:0][] DEBUG:  releasing pm child slot 2
> 2025-03-04 17:56:12.529 EST [3509817][postmaster][:0][] DEBUG:  client backend (PID 3509838) exited with exit code 1
> 
> I.e. the test could wait for the 'client backend exited' message using
> ->wait_for_log()?

Matching expected contents in the server logs is a practice I've found
to be rather reliable, with wait_for_log().  Why not adding an
injection point with a WARNING or a LOG generated, then check the
server logs for the code path taken based on the elog() generated with
the point name?
--
Michael


Attachments:

  [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2025-03-06 20:18  Heikki Linnakangas <[email protected]>
  parent: Michael Paquier <[email protected]>
  0 siblings, 0 replies; 39+ messages in thread

From: Heikki Linnakangas @ 2025-03-06 20:18 UTC (permalink / raw)
  To: Michael Paquier <[email protected]>; Andres Freund <[email protected]>; +Cc: Tomas Vondra <[email protected]>; Thomas Munro <[email protected]>; pgsql-hackers

On 05/03/2025 01:23, Michael Paquier wrote:
> On Tue, Mar 04, 2025 at 05:58:42PM -0500, Andres Freund wrote:
>> On 2024-12-10 12:00:12 +0200, Heikki Linnakangas wrote:
>>> 2. Move the pgstat_bestart() call earlier in the startup sequence, so that a
>>> backend shows up in pg_stat_activity before it acquires a PGPROC entry, and
>>> stays visible until after it has released its PGPROC entry. This would give
>>> more visibility to backends that are starting up.
>>
>> We don't necessarily *have* a PGPROC entry for that backend when we run out of
>> connections, no?
> 
> Exactly.  If I got this thread's argument right, you cannot have a
> PGPROC entry that could be plugged into pg_stat_activity that early
> during the startup process when collecting the startup packet.

That's true in general; once you start running out of connections, you 
can indeed run out PGPROC slots too. In this particular case, though, 
there were still PGPROC slots available, reserved for superuser 
connections, so it would've helped.

We could also have more pg_stat_activity slots than PGPROC slots, or 
just have a few more PGPROC slots than what is required by MaxBackends.

>> For this test, could we perhaps rely on the log messages postmaster logs when
>> child processes exit?
>>
>> 2025-03-04 17:56:12.528 EST [3509838][not initialized][:0][[unknown]] LOG:  connection received: host=[local]
>> 2025-03-04 17:56:12.528 EST [3509838][client backend][:0][[unknown]] FATAL:  sorry, too many clients already
>> 2025-03-04 17:56:12.529 EST [3509817][postmaster][:0][] DEBUG:  releasing pm child slot 2
>> 2025-03-04 17:56:12.529 EST [3509817][postmaster][:0][] DEBUG:  client backend (PID 3509838) exited with exit code 1
>>
>> I.e. the test could wait for the 'client backend exited' message using
>> ->wait_for_log()?
> 
> Matching expected contents in the server logs is a practice I've found
> to be rather reliable, with wait_for_log().  Why not adding an
> injection point with a WARNING or a LOG generated, then check the
> server logs for the code path taken based on the elog() generated with
> the point name?

Hmm, yeah, watching for "releasing pm child slot" or an explicit 
injection point would work.

-- 
Heikki Linnakangas
Neon (https://neon.tech)






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2026-02-15 08:11  Michael Banck <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  2 siblings, 0 replies; 39+ messages in thread

From: Michael Banck @ 2026-02-15 08:11 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Dagfinn Ilmari Mannsåker <[email protected]>; Thomas Munro <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers

Hi,

sorry to bring this old thread back to light.

On Tue, Oct 08, 2024 at 12:55:00AM +0300, Heikki Linnakangas wrote:
> In the meanwhile, here is a one more version of the test patches, with a
> SKIP that checks that IO::Socket::UNIX works.

I've only realized recently, but those postmaster tap tests have been
failing during Debian package build (see e.g. [0]) on hurd-i386/amd64 with

|send: Cannot determine peer address at t/002_connection_limits.pl line 136.

This wasn't widely noticed because both architectures are on the (not
small) list of arches where test suite failures are ignored[1] but I
think nowadays it is the only (or one of the very few) remaining
issue(s). I encountered it now when I tried to turn on
--enable-tap-tests on fruitcrow.

The Perl code run through strace shows it runs connect(), then
getpeername() and then sendto(), as seen here[2]. However, getpeername()
on Unix sockets is not implemented on the Hurd yet[3] (granted, FSVO
"yet", the issue is 20 years old). I've opened an issue in Perl asking
to work around this here: https://github.com/Perl/perl5/issues/24195

Would something like the attached be acceptable in the interim to have
this test be skipped on the Hurd as well?


Michael

[0] https://buildd.debian.org/status/fetch.php?pkg=postgresql-18&arch=hurd-amd64&ver=18.2-1&...
[1] https://salsa.debian.org/postgresql/postgresql-common/-/blob/master/server/postgresql.mk?ref_type=he...
[2] https://github.com/Perl/perl5/blob/blead/dist/IO/lib/IO/Socket.pm#L294
[3] http://savannah.gnu.org/bugs/?func=detailitem&item_id=12434


Attachments:

  [text/x-diff] v1-0001-Add-an-additional-check-in-raw_connect_works-for-.patch (1.2K, ../../[email protected]%3Blightning.p46.dedyn.io/2-v1-0001-Add-an-additional-check-in-raw_connect_works-for-.patch)
  download | inline diff:
From b5638c1959308665486ca9241997d48cae1485eb Mon Sep 17 00:00:00 2001
From: Michael Banck <[email protected]>
Date: Fri, 13 Feb 2026 16:34:45 +0100
Subject: [PATCH v1] Add an additional check in raw_connect_works for the Hurd.

The function raw_connect_works() tests whether raw sockets work for the
postmaster TAP tests. While creating the socket does work on the Hurd,
the TAP test does a subsequent send() on it which fails on the Hurd with
"Cannot determine peer address". To circumvent this, try a send() and
catch this error as well.
---
 src/test/perl/PostgreSQL/Test/Cluster.pm | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index e267ba868fe..ee3170660fc 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -380,9 +380,10 @@ sub raw_connect_works
 	{
 		eval {
 			my $sock = $self->raw_connect();
+			$sock->send('foo');
 			$sock->close();
 		};
-		if ($@ =~ /not implemented/)
+		if ($@ =~ /not implemented/ or $@ =~ /Cannot determine peer address/)
 		{
 			diag "IO::Socket::UNIX does not work: $@";
 			return 0;
-- 
2.39.5



^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2026-02-16 07:51  Heikki Linnakangas <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  2 siblings, 2 replies; 39+ messages in thread

From: Heikki Linnakangas @ 2026-02-16 07:51 UTC (permalink / raw)
  To: Michael Banck <[email protected]>; +Cc: Dagfinn Ilmari Mannsåker <[email protected]>; Thomas Munro <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers

On 15/02/2026 10:11, Michael Banck wrote:
> On Tue, Oct 08, 2024 at 12:55:00AM +0300, Heikki Linnakangas wrote:
>> In the meanwhile, here is a one more version of the test patches, with a
>> SKIP that checks that IO::Socket::UNIX works.
> 
> I've only realized recently, but those postmaster tap tests have been
> failing during Debian package build (see e.g. [0]) on hurd-i386/amd64 with
> 
> |send: Cannot determine peer address at t/002_connection_limits.pl line 136.
> 
> This wasn't widely noticed because both architectures are on the (not
> small) list of arches where test suite failures are ignored[1] but I
> think nowadays it is the only (or one of the very few) remaining
> issue(s). I encountered it now when I tried to turn on
> --enable-tap-tests on fruitcrow.
> 
> The Perl code run through strace shows it runs connect(), then
> getpeername() and then sendto(), as seen here[2]. However, getpeername()
> on Unix sockets is not implemented on the Hurd yet[3] (granted, FSVO
> "yet", the issue is 20 years old). I've opened an issue in Perl asking
> to work around this here: https://github.com/Perl/perl5/issues/24195
> 
> Would something like the attached be acceptable in the interim to have
> this test be skipped on the Hurd as well?

Hmm, so is the "$sock->send('foo');" actually necessary, if the error 
occurs on getpeername() already? Where does the getpeername() call come 
from?

It would be nice to silence that failure one way or another. If we go 
with this approach, would need a comment at least to explain it.

It seems a little awkward to send garbage to the server for this. Could 
we replace the send() with a non-blocking recv() or something?

- Heikki







^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2026-02-16 13:05  Michael Banck <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  1 sibling, 0 replies; 39+ messages in thread

From: Michael Banck @ 2026-02-16 13:05 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Dagfinn Ilmari Mannsåker <[email protected]>; Thomas Munro <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers

Hi Heikki,

On Mon, Feb 16, 2026 at 09:51:00AM +0200, Heikki Linnakangas wrote:
> On 15/02/2026 10:11, Michael Banck wrote:
> > On Tue, Oct 08, 2024 at 12:55:00AM +0300, Heikki Linnakangas wrote:
> > > In the meanwhile, here is a one more version of the test patches, with a
> > > SKIP that checks that IO::Socket::UNIX works.
> > 
> > I've only realized recently, but those postmaster tap tests have been
> > failing during Debian package build (see e.g. [0]) on hurd-i386/amd64 with
> > 
> > |send: Cannot determine peer address at t/002_connection_limits.pl line 136.
> > 
> > This wasn't widely noticed because both architectures are on the (not
> > small) list of arches where test suite failures are ignored[1] but I
> > think nowadays it is the only (or one of the very few) remaining
> > issue(s). I encountered it now when I tried to turn on
> > --enable-tap-tests on fruitcrow.
> > 
> > The Perl code run through strace shows it runs connect(), then
> > getpeername() and then sendto(), as seen here[2]. However, getpeername()
> > on Unix sockets is not implemented on the Hurd yet[3] (granted, FSVO
> > "yet", the issue is 20 years old). I've opened an issue in Perl asking
> > to work around this here: https://github.com/Perl/perl5/issues/24195
> > 
> > Would something like the attached be acceptable in the interim to have
> > this test be skipped on the Hurd as well?
> 
> Hmm, so is the "$sock->send('foo');" actually necessary, if the error occurs
> on getpeername() already? Where does the getpeername() call come from?

I wasn't very clear in my last message, this is the send() code in Perl
(see https://github.com/Perl/perl5/blob/blead/dist/IO/lib/IO/Socket.pm#L294):

|sub send {
|    @_ >= 2 && @_ <= 4 or croak 'usage: $sock->send(BUF, [FLAGS, [TO]])';
|    my $sock  = $_[0];
|    my $flags = $_[2] || 0;
|    my $peer;
|
|    if ($_[3]) {
|        # the caller explicitly requested a TO, so use it
|        # this is non-portable for "connected" UDP sockets
|        $peer = $_[3];
|    }
|    elsif (!defined getpeername($sock)) {
|        # we're not connected, so we require a peer from somewhere
|        $peer = $sock->peername;
|
|	croak 'send: Cannot determine peer address'
|	    unless(defined $peer);
|    }
|
|    my $r = $peer
|      ? send($sock, $_[1], $flags, $peer)
|      : send($sock, $_[1], $flags);
|
|    # remember who we send to, if it was successful
|    ${*$sock}{'io_socket_peername'} = $peer
|	if(@_ == 4 && defined $r);
|
|    $r;
|}

So if we don't give send() a third argument, it runs getpeername() on
the socket. I have the feeling it is unncessary to require a peer for
local Unix sockets and have suggested that now in the Perl issue.

> It would be nice to silence that failure one way or another. If we go with
> this approach, would need a comment at least to explain it.
 
Right.

> It seems a little awkward to send garbage to the server for this. Could we
> replace the send() with a non-blocking recv() or something?

I am not quite sure how this would look like?


Thanks,

Michael






^ permalink  raw  reply  [nested|flat] 39+ messages in thread

* Re: Refactoring postmaster's code to cleanup after child exit
@ 2026-07-07 08:39  Michael Banck <[email protected]>
  parent: Heikki Linnakangas <[email protected]>
  1 sibling, 0 replies; 39+ messages in thread

From: Michael Banck @ 2026-07-07 08:39 UTC (permalink / raw)
  To: Heikki Linnakangas <[email protected]>; +Cc: Dagfinn Ilmari Mannsåker <[email protected]>; Thomas Munro <[email protected]>; Andres Freund <[email protected]>; pgsql-hackers

Hi,

On Mon, Feb 16, 2026 at 02:05:01PM +0100, Michael Banck wrote:
> On Mon, Feb 16, 2026 at 09:51:00AM +0200, Heikki Linnakangas wrote:
> > On 15/02/2026 10:11, Michael Banck wrote:
> > > On Tue, Oct 08, 2024 at 12:55:00AM +0300, Heikki Linnakangas wrote:
> > > > In the meanwhile, here is a one more version of the test patches, with a
> > > > SKIP that checks that IO::Socket::UNIX works.
> > > 
> > > I've only realized recently, but those postmaster tap tests have been
> > > failing during Debian package build (see e.g. [0]) on hurd-i386/amd64 with
> > > 
> > > |send: Cannot determine peer address at t/002_connection_limits.pl line 136.
> > > 
> > > This wasn't widely noticed because both architectures are on the (not
> > > small) list of arches where test suite failures are ignored[1] but I
> > > think nowadays it is the only (or one of the very few) remaining
> > > issue(s). I encountered it now when I tried to turn on
> > > --enable-tap-tests on fruitcrow.
> > > 
> > > The Perl code run through strace shows it runs connect(), then
> > > getpeername() and then sendto(), as seen here[2]. However, getpeername()
> > > on Unix sockets is not implemented on the Hurd yet[3] (granted, FSVO
> > > "yet", the issue is 20 years old). I've opened an issue in Perl asking
> > > to work around this here: https://github.com/Perl/perl5/issues/24195
> > > 
> > > Would something like the attached be acceptable in the interim to have
> > > this test be skipped on the Hurd as well?

We've installed this in the Debian package a while ago[1], and this
seems to have worked so far as the postmaster tests no longer lead to
failure:

|make[2]: Leaving directory
|'/build/reproducible-path/postgresql-18-18.4/build/src/test/perl'
|make -C postmaster check
|make[2]: Entering directory
|'/build/reproducible-path/postgresql-18-18.4/build/src/test/postmaster'
|echo "# +++ tap check in src/test/postmaster +++" && rm -rf
|'/build/reproducible-path/postgresql-18-18.4/build/src/test/postmaster'/tmp_check
|&& /bin/mkdir -p
|'/build/reproducible-path/postgresql-18-18.4/build/src/test/postmaster'/tmp_check
|&& cd
|/build/reproducible-path/postgresql-18-18.4/build/../src/test/postmaster
|&&
|TESTLOGDIR='/build/reproducible-path/postgresql-18-18.4/build/src/test/postmaster/tmp_check/log'
|TESTDATADIR='/build/reproducible-path/postgresql-18-18.4/build/src/test/postmaster/tmp_check'
|PATH="/build/reproducible-path/postgresql-18-18.4/build/tmp_install/usr/lib/postgresql/18/bin:/build/reproducible-path/postgresql-18-18.4/build/src/test/postmaster:$PATH"
|LD_LIBRARY_PATH="/build/reproducible-path/postgresql-18-18.4/build/tmp_install/usr/lib/x86_64-gnu:$LD_LIBRARY_PATH"
|INITDB_TEMPLATE='/build/reproducible-path/postgresql-18-18.4/build'/tmp_install/initdb-template
|PGPORT='65432'
|top_builddir='/build/reproducible-path/postgresql-18-18.4/build/src/test/postmaster/../../..'
|PG_REGRESS='/build/reproducible-path/postgresql-18-18.4/build/src/test/postmaster/../../../src/test/regress/pg_regress'
|share_contrib_dir='/build/reproducible-path/postgresql-18-18.4/build/tmp_install/usr/share/postgresql/18/'
|/usr/bin/prove -I
|/build/reproducible-path/postgresql-18-18.4/build/../src/test/perl/ -I
|/build/reproducible-path/postgresql-18-18.4/build/../src/test/postmaster
|--verbose t/*.pl
|# +++ tap check in src/test/postmaster +++
|t/001_basic.pl .............. 
|ok 1 - postgres --help exit code 0
|ok 2 - postgres --help goes to stdout
|ok 3 - postgres --help nothing to stderr
|ok 4 - postgres --help maximum line length
|ok 5 - postgres --version exit code 0
|ok 6 - postgres --version goes to stdout
|ok 7 - postgres --version nothing to stderr
|ok 8 - postgres with invalid option nonzero exit code
|ok 9 - postgres with invalid option prints error message
|1..9
|ok
|# IO::Socket::UNIX does not work: send: Cannot determine peer address at
|# /build/reproducible-path/postgresql-18-18.4/build/../src/test/perl/PostgreSQL/Test/Cluster.pm
|# line 356.
|t/002_connection_limits.pl .. 
|# initializing database system by copying initdb template
|# connect output:
|# {
|#   'stderr' => 'background_psql: ready
|# ',
|#   'stdout' => 'background_psql: ready
|# '
|# }
|# connect output:
|# {
|#   'stderr' => 'background_psql: ready
|# ',
|#   'stdout' => 'background_psql: ready
|# '
|# }
|# connect output:
|# {
|#   'stderr' => 'background_psql: ready
|# ',
|#   'stdout' => 'background_psql: ready
|# '
|# }
|ok 1 - regular connections limit
|ok 2 - regular connections limit: matches
|ok 3 - regular connections limit: client backend process exited
|# connect output:
|# {
|#   'stderr' => 'background_psql: ready
|# ',
|#   'stdout' => 'background_psql: ready
|# '
|# }
|# connect output:
|# {
|#   'stderr' => 'background_psql: ready
|# ',
|#   'stdout' => 'background_psql: ready
|# '
|# }
|ok 4 - reserved_connections limit
|ok 5 - reserved_connections limit: matches
|ok 6 - reserved_connections limit: client backend process exited
|# connect output:
|# {
|#   'stderr' => 'background_psql: ready
|# ',
|#   'stdout' => 'background_psql: ready
|# '
|# }
|ok 7 - superuser_reserved_connections limit
|ok 8 - superuser_reserved_connections limit: matches
|ok 9 - superuser_reserved_connections limit: client backend process
|exited
|ok 10 # skip this test requires working raw_connect()
|1..10
|ok
|# IO::Socket::UNIX does not work: send: Cannot determine peer address at
|# /build/reproducible-path/postgresql-18-18.4/build/../src/test/perl/PostgreSQL/Test/Cluster.pm
|# line 356.
|t/003_start_stop.pl ......... skipped: this test requires working
|raw_connect()
|# IO::Socket::UNIX does not work: send: Cannot determine peer address at
|# /build/reproducible-path/postgresql-18-18.4/build/../src/test/perl/PostgreSQL/Test/Cluster.pm
|# line 356.
|t/004_negotiate.pl .......... skipped: this test requires working
|raw_connect()
|All tests successful.
|Files=4, Tests=19,  9 wallclock secs ( 0.02 usr +  0.01 sys =  0.03 CPU)
|Result: PASS
|make[2]: Leaving directory
|'/build/reproducible-path/postgresql-18-18.4/build/src/test/postmaster'

It does seem to skip over the other postmaster TAP tests as well though
(t/003_start_stop.pl and t/004_negotiate.pl), is that expected?

> > It would be nice to silence that failure one way or another. If we go with
> > this approach, would need a comment at least to explain it.
>  
> Right.
> 
> > It seems a little awkward to send garbage to the server for this. Could we
> > replace the send() with a non-blocking recv() or something?
> 
> I am not quite sure how this would look like?

Do you have some suggestion here? I am still not sure what exactly you
mean.


Cheers,

Michael

[1] https://salsa.debian.org/postgresql/postgresql/-/blob/18/debian/patches/hurd-postmaster-socket-send?...






^ permalink  raw  reply  [nested|flat] 39+ messages in thread


end of thread, other threads:[~2026-07-07 08:39 UTC | newest]

Thread overview: 39+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2024-03-15 17:26 [PATCH v2 2/3] add avx2 support in simd.h Nathan Bossart <[email protected]>
2024-07-29 20:16 Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-08-01 23:57 ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-08-08 10:47   ` Re: Refactoring postmaster's code to cleanup after child exit Thomas Munro <[email protected]>
2024-08-09 21:13     ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-08-12 09:55       ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-09-04 14:35         ` Re: Refactoring postmaster's code to cleanup after child exit Andres Freund <[email protected]>
2024-09-06 09:52           ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-10-04 18:41             ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-10-04 22:03               ` Re: Refactoring postmaster's code to cleanup after child exit Thomas Munro <[email protected]>
2024-10-05 11:45                 ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-10-05 19:51                   ` Re: Refactoring postmaster's code to cleanup after child exit Dagfinn Ilmari Mannsåker <[email protected]>
2024-10-07 14:19                     ` Re: Refactoring postmaster's code to cleanup after child exit Andres Freund <[email protected]>
2024-10-07 21:55                     ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-10-08 17:30                       ` Re: Refactoring postmaster's code to cleanup after child exit Nazir Bilal Yavuz <[email protected]>
2026-02-15 08:11                       ` Re: Refactoring postmaster's code to cleanup after child exit Michael Banck <[email protected]>
2026-02-16 07:51                       ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2026-02-16 13:05                         ` Re: Refactoring postmaster's code to cleanup after child exit Michael Banck <[email protected]>
2026-07-07 08:39                         ` Re: Refactoring postmaster's code to cleanup after child exit Michael Banck <[email protected]>
2024-09-06 13:13           ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-09-06 13:32             ` Re: Refactoring postmaster's code to cleanup after child exit Robert Haas <[email protected]>
2024-09-10 16:59             ` Re: Refactoring postmaster's code to cleanup after child exit Andres Freund <[email protected]>
2024-09-10 17:33               ` Re: Refactoring postmaster's code to cleanup after child exit Robert Haas <[email protected]>
2024-09-10 22:35                 ` Re: Refactoring postmaster's code to cleanup after child exit Andres Freund <[email protected]>
2024-09-10 17:53         ` Re: Refactoring postmaster's code to cleanup after child exit Andres Freund <[email protected]>
2024-10-09 20:40           ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-11-14 14:13             ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-12-08 23:12               ` Re: Refactoring postmaster's code to cleanup after child exit Tomas Vondra <[email protected]>
2024-12-09 12:30                 ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-12-09 12:47                   ` Re: Refactoring postmaster's code to cleanup after child exit Tomas Vondra <[email protected]>
2024-12-09 20:55                     ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-12-10 10:00                       ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2024-12-10 11:39                         ` Re: Refactoring postmaster's code to cleanup after child exit Tomas Vondra <[email protected]>
2025-03-04 22:58                         ` Re: Refactoring postmaster's code to cleanup after child exit Andres Freund <[email protected]>
2025-03-04 23:23                           ` Re: Refactoring postmaster's code to cleanup after child exit Michael Paquier <[email protected]>
2025-03-06 20:18                             ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[email protected]>
2025-03-04 22:50                 ` Re: Refactoring postmaster's code to cleanup after child exit Andres Freund <[email protected]>
2024-08-18 08:00       ` Re: Refactoring postmaster's code to cleanup after child exit Alexander Lakhin <[email protected]>
2024-08-19 06:49         ` Re: Refactoring postmaster's code to cleanup after child exit Heikki Linnakangas <[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