Support to define custom wait events for extensions

Started by Masahiro Ikedaover 2 years ago77 messages
#1Masahiro Ikeda
ikedamsh@oss.nttdata.com
3 attachment(s)

Hi,

Currently, only one PG_WAIT_EXTENSION event can be used as a
wait event for extensions. Therefore, in environments with multiple
extensions are installed, it could take time to identify which
extension is the bottleneck.

So, I'd like to support new APIs to define custom wait events
for extensions. It's discussed in [1]/messages/by-id/81290a48-b25c-22a5-31a6-3feff5864fe3@gmail.com.

I made patches to realize it. Although I have some TODOs,
I want to know your feedbacks. Please feel free to comment.

# Implementation of new APIs

I implemented 2 new APIs for extensions.
* RequestNamedExtensionWaitEventTranche()
* GetNamedExtensionWaitEventTranche()

Extensions can request custom wait events by calling
RequestNamedExtensionWaitEventTranche(). After that, use
GetNamedExtensionWaitEventTranche() to get the wait event information.

The APIs usage example by extensions are following.

```
shmem_request_hook = shmem_request;
shmem_startup_hook = shmem_startup;

static void
shmem_request(void)
{
/* request a custom wait event */
RequestNamedExtensionWaitEventTranche("custom_wait_event");
}

static void
shmem_startup(void)
{
/* get the wait event information */
custom_wait_event =
GetNamedExtensionWaitEventTranche("custom_wait_event");
}

void
extension_funtion()
{
(void) WaitLatch(MyLatch,
WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
10L * 1000,
custom_wait_event); /* notify core with custom wait event */
ResetLatch(MyLatch);
}
```

# Implementation overview

I referenced the implementation of
RequestNamedLWLockTranche()/GetNamedLWLockTranche().
(0001-Support-to-define-custom-wait-events-for-extensions.patch)

Extensions calls RequestNamedExtensionWaitEventTranche() in
shmem_request_hook() to request wait events to be used by each
extension.

In the core, the requested wait events are dynamically registered in
shared
memory. The extension then obtains the wait event information with
GetNamedExtensionWaitEventTranche() and uses the value to notify the
core
that it is waiting.

When a string representing of the wait event is requested,
it returns the name defined by calling
RequestNamedExtensionWaitEventTranche().

# PoC extension

I created the PoC extension and you can use it, as shown here:
(0002-Add-a-extension-to-test-custom-wait-event.patch)

1. start PostgreSQL with the following configuration
shared_preload_libraries = 'inject_wait_event'

2. check wait events periodically
psql-1=# SELECT query, wait_event_type, wait_event FROM pg_stat_activity
WHERE backend_type = 'client backend' AND pid != pg_backend_pid() ;
psql-1=# \watch

3. execute a function to inject a wait event
psql-2=# CREATE EXTENSION inject_wait_event;
psql-2=# SELECT inject_wait_event();

4. check the custom wait event
You can see the following results of psql-1.

(..snip..)

query | wait_event_type | wait_event
-----------------------------+-----------------+------------
SELECT inject_wait_event(); | Extension | Extension
(1 row)

(..snip..)

(...about 10 seconds later ..)

query | wait_event_type | wait_event
-----------------------------+-----------------+-------------------
SELECT inject_wait_event(); | Extension | custom_wait_event
# requested wait event by the extension!
(1 row)

(..snip..)

# TODOs

* tests on windows (since I tested on Ubuntu 20.04 only)
* add custom wait events for existing contrib modules (ex. postgres_fdw)
* add regression code (but, it seems to be difficult)
* others? (Please let me know)

[1]: /messages/by-id/81290a48-b25c-22a5-31a6-3feff5864fe3@gmail.com
/messages/by-id/81290a48-b25c-22a5-31a6-3feff5864fe3@gmail.com

Regards,

--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

0001-Support-to-define-custom-wait-events-for-extensions.patchtext/x-diff; name=0001-Support-to-define-custom-wait-events-for-extensions.patchDownload
From 59a118402e5e59685fb9e0fb086872e25a405736 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Thu, 15 Jun 2023 12:57:29 +0900
Subject: [PATCH 2/3] Support to define custom wait events for extensions.

Currently, only one PG_WAIT_EXTENSION event can be used as a
wait event for extensions. Therefore, in environments with multiple
extensions are installed, it could take time to identify bottlenecks.

This commit allows defining custom wait events for extensions and
introduces a new API called RequestNamedExtensionWaitEventTranche()/
GetNamedExtensionWaitEventTranche().

These refer to RequestNamedLWLockTranche()/GetNamedLWLockTranche(),
but do not require as much flexibility as LWLock and can be implemented
more simply.

The extension calls RequestNamedExtensionWaitEventTranche() in
shmem_request_hook() to request wait events to be used by each extension.
In the core, the requested wait events are dynamically registered in shared
memory. The extension then obtains the wait event information with
GetNamedExtensionWaitEventTranche() and uses the value to notify the core
that it is waiting.
---
 src/backend/postmaster/postmaster.c     |   6 +
 src/backend/storage/ipc/ipci.c          |   3 +
 src/backend/storage/ipc/shmem.c         |   3 +-
 src/backend/utils/activity/wait_event.c | 303 +++++++++++++++++++++++-
 src/include/utils/wait_event.h          |  32 +++
 5 files changed, 345 insertions(+), 2 deletions(-)

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 4c49393fc5..50afa3aa14 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -516,6 +516,8 @@ typedef struct
 	int			NamedLWLockTrancheRequests;
 	NamedLWLockTranche *NamedLWLockTrancheArray;
 	LWLockPadded *MainLWLockArray;
+	int			NamedExtensionWaitEventTrancheRequests;
+	NamedExtensionWaitEventTranche *NamedExtensionWaitEventTrancheArray;
 	slock_t    *ProcStructLock;
 	PROC_HDR   *ProcGlobal;
 	PGPROC	   *AuxiliaryProcs;
@@ -6087,6 +6089,8 @@ save_backend_variables(BackendParameters *param, Port *port,
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
 	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
 	param->MainLWLockArray = MainLWLockArray;
+	param->NamedExtensionWaitEventTrancheRequests = NamedExtensionWaitEventTrancheRequests;
+	param->NamedExtensionWaitEventTrancheArray = NamedExtensionWaitEventTrancheArray;
 	param->ProcStructLock = ProcStructLock;
 	param->ProcGlobal = ProcGlobal;
 	param->AuxiliaryProcs = AuxiliaryProcs;
@@ -6320,6 +6324,8 @@ restore_backend_variables(BackendParameters *param, Port *port)
 	NamedLWLockTrancheRequests = param->NamedLWLockTrancheRequests;
 	NamedLWLockTrancheArray = param->NamedLWLockTrancheArray;
 	MainLWLockArray = param->MainLWLockArray;
+	NamedExtensionWaitEventTrancheRequests = param->NamedExtensionWaitEventTrancheRequests;
+	NamedExtensionWaitEventTrancheArray = param->NamedExtensionWaitEventTrancheArray;
 	ProcStructLock = param->ProcStructLock;
 	ProcGlobal = param->ProcGlobal;
 	AuxiliaryProcs = param->AuxiliaryProcs;
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 8f1ded7338..ed05121fa3 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -49,6 +49,7 @@
 #include "storage/spin.h"
 #include "utils/guc.h"
 #include "utils/snapmgr.h"
+#include "utils/wait_event.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -142,6 +143,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SyncScanShmemSize());
 	size = add_size(size, AsyncShmemSize());
 	size = add_size(size, StatsShmemSize());
+	size = add_size(size, WaitEventShmemSize());
 #ifdef EXEC_BACKEND
 	size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -294,6 +296,7 @@ CreateSharedMemoryAndSemaphores(void)
 	SyncScanShmemInit();
 	AsyncShmemInit();
 	StatsShmemInit();
+	WaitEventShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/storage/ipc/shmem.c b/src/backend/storage/ipc/shmem.c
index 5465fa1964..02c72ebbb1 100644
--- a/src/backend/storage/ipc/shmem.c
+++ b/src/backend/storage/ipc/shmem.c
@@ -85,7 +85,8 @@ static void *ShmemBase;			/* start address of shared memory */
 
 static void *ShmemEnd;			/* end+1 address of shared memory */
 
-slock_t    *ShmemLock;			/* spinlock for shared memory and LWLock
+slock_t    *ShmemLock;			/* spinlock for shared memory, LWLock
+								 * allocation, and named extension wait event
 								 * allocation */
 
 static HTAB *ShmemIndex = NULL; /* primary index hashtable for shmem */
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 7940d64639..4bf725fbae 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,11 +22,18 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
 #include "storage/lwlock.h"		/* for GetLWLockIdentifier */
+#include "storage/spin.h"
+#include "utils/memutils.h"
 #include "utils/wait_event.h"
 
 
+/* We use the ShmemLock spinlock to protect ExtensionWaitEventCounter */
+extern slock_t *ShmemLock;
+
 static const char *pgstat_get_wait_activity(WaitEventActivity w);
 static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
@@ -38,6 +45,46 @@ static uint32 local_my_wait_event_info;
 uint32	   *my_wait_event_info = &local_my_wait_event_info;
 
 
+/* struct representing the request for named tranche of extension wait event */
+typedef struct NamedExtensionWaitEventTrancheRequest
+{
+	char		tranche_name[NAMEDATALEN];
+}			NamedExtensionWaitEventTrancheRequest;
+
+static NamedExtensionWaitEventTrancheRequest * NamedExtensionWaitEventTrancheRequestArray = NULL;
+static int	NamedExtensionWaitEventTrancheRequestsAllocated = 0;
+
+/*
+ * NamedExtensionWaitEventTrancheRequests is both the valid length of the request array,
+ * and the length of the shared-memory NamedExtensionWaitEventTrancheArray later on.
+ * This variable and NamedExtensionWaitEventTrancheArray are non-static so that
+ * postmaster.c can copy them to child processes in EXEC_BACKEND builds.
+ */
+int			NamedExtensionWaitEventTrancheRequests = 0;
+
+/* points to data in shared memory */
+NamedExtensionWaitEventTranche *NamedExtensionWaitEventTrancheArray = NULL;
+
+static void InitializeExtensionWaitEventTranches(void);
+static void ExtensionWaitEventRegisterTranche(int tranche_id, const char *tranche_name);
+static int	ExtensionWaitEventNewTrancheId(void);
+
+/* first tranche ID for named tranche */
+#define NUM_BUILDIN_WAIT_EVENT_EXTENSION	\
+	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
+
+/*
+ * This is indexed by tranche ID minus NUM_BUILDIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created tranches known to the current
+ * process.  Any unused entries in the array will contain NULL.
+ */
+static const char **ExtensionWaitEventTrancheNames = NULL;
+static int	ExtensionWaitEventTrancheNamesAllocated = 0;
+
+static const char *GetExtensionIdentifier(uint32 classId, uint16 eventId);
+static const char *GetExtensionTrancheName(uint16 trancheId);
+
+
 /*
  * Configure wait event reporting to report wait events to *wait_event_info.
  * *wait_event_info needs to be valid until pgstat_reset_wait_event_storage()
@@ -165,7 +212,7 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				break;
 			}
 		case PG_WAIT_EXTENSION:
-			event_name = "Extension";
+			event_name = GetExtensionIdentifier(classId, eventId);
 			break;
 		case PG_WAIT_IPC:
 			{
@@ -762,3 +809,257 @@ pgstat_get_wait_io(WaitEventIO w)
 
 	return event_name;
 }
+
+/*
+ * RequestNamedExtensionWaitEventTranche
+ *		Request that extra wait events for extensions be allocated
+ *      during postmaster startup.
+ *
+ * This may only be called via the shmem_request_hook of a library that is
+ * loaded into the postmaster via shared_preload_libraries.  Calls from
+ * elsewhere will fail.
+ *
+ * The tranche name will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+RequestNamedExtensionWaitEventTranche(const char *tranche_name)
+{
+	NamedExtensionWaitEventTrancheRequest *request;
+
+	if (!process_shmem_requests_in_progress)
+		elog(FATAL, "cannot request additional wait events outside shmem_request_hook");
+
+	if (NamedExtensionWaitEventTrancheRequestArray == NULL)
+	{
+		NamedExtensionWaitEventTrancheRequestsAllocated = 16;
+		NamedExtensionWaitEventTrancheRequestArray = (NamedExtensionWaitEventTrancheRequest *)
+			MemoryContextAlloc(TopMemoryContext,
+							   NamedExtensionWaitEventTrancheRequestsAllocated
+							   * sizeof(NamedExtensionWaitEventTrancheRequest));
+	}
+
+	if (NamedExtensionWaitEventTrancheRequests >= NamedExtensionWaitEventTrancheRequestsAllocated)
+	{
+		int			i = pg_nextpower2_32(NamedExtensionWaitEventTrancheRequests + 1);
+
+		NamedExtensionWaitEventTrancheRequestArray = (NamedExtensionWaitEventTrancheRequest *)
+			repalloc(NamedExtensionWaitEventTrancheRequestArray,
+					 i * sizeof(NamedExtensionWaitEventTrancheRequest));
+		NamedExtensionWaitEventTrancheRequestsAllocated = i;
+	}
+
+	request = &NamedExtensionWaitEventTrancheRequestArray[NamedExtensionWaitEventTrancheRequests];
+	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
+	strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);
+	NamedExtensionWaitEventTrancheRequests++;
+}
+
+/*
+ * Compute shmem space needed for named wait event tranches.
+ */
+Size
+WaitEventShmemSize(void)
+{
+	Size		size;
+	int			i;
+
+	/* space for named tranches. */
+	size = mul_size(NamedExtensionWaitEventTrancheRequests, sizeof(NamedExtensionWaitEventTranche));
+
+	/* space for name of each tranche. */
+	for (i = 0; i < NamedExtensionWaitEventTrancheRequests; i++)
+		size = add_size(size, strlen(NamedExtensionWaitEventTrancheRequestArray[i].tranche_name) + 1);
+
+	return size;
+}
+
+/*
+ * Allocate shmem space for named wait event tranches and initialize it.
+ * We also register named wait event tranches here.
+ */
+void
+WaitEventShmemInit(void)
+{
+	if (!IsUnderPostmaster)
+	{
+		Size		spaceWaitEvent = WaitEventShmemSize();
+		int		   *ExtensionWaitEventCounter;
+		char	   *ptr;
+
+		/* Allocate space */
+		ptr = (char *) ShmemAlloc(spaceWaitEvent);
+
+		/* Leave room for dynamic allocation of tranches */
+		ptr += sizeof(int);
+
+		NamedExtensionWaitEventTrancheArray = (NamedExtensionWaitEventTranche *) ptr;
+
+		/*
+		 * Initialize the dynamic-allocation counter for tranches, which is
+		 * stored just before the first wait event.
+		 */
+		ExtensionWaitEventCounter = (int *) ((char *) NamedExtensionWaitEventTrancheArray - sizeof(int));
+		*ExtensionWaitEventCounter = NUM_BUILDIN_WAIT_EVENT_EXTENSION;
+
+		/* Initialize requested named wait event tranches */
+		InitializeExtensionWaitEventTranches();
+	}
+
+	/* Register named wait event tranches in the current process. */
+	for (int i = 0; i < NamedExtensionWaitEventTrancheRequests; i++)
+		ExtensionWaitEventRegisterTranche(NamedExtensionWaitEventTrancheArray[i].trancheId,
+										  NamedExtensionWaitEventTrancheArray[i].trancheName);
+}
+
+/*
+ * Initialize requested named wait event tranches.
+ */
+static void
+InitializeExtensionWaitEventTranches(void)
+{
+	/*
+	 * Copy the info about any named tranches into shared memory (so that
+	 * other processes can see it), and initialize the requested wait events.
+	 */
+	if (NamedExtensionWaitEventTrancheRequests > 0)
+	{
+		char	   *trancheNames;
+		int			i;
+
+		trancheNames = (char *) NamedExtensionWaitEventTrancheArray +
+			(NamedExtensionWaitEventTrancheRequests * sizeof(NamedExtensionWaitEventTranche));
+
+		for (i = 0; i < NamedExtensionWaitEventTrancheRequests; i++)
+		{
+			NamedExtensionWaitEventTrancheRequest *request;
+			NamedExtensionWaitEventTranche *tranche;
+			char	   *name;
+
+			request = &NamedExtensionWaitEventTrancheRequestArray[i];
+			tranche = &NamedExtensionWaitEventTrancheArray[i];
+
+			name = trancheNames;
+			trancheNames += strlen(request->tranche_name) + 1;
+			strcpy(name, request->tranche_name);
+			tranche->trancheId = ExtensionWaitEventNewTrancheId();
+			tranche->trancheName = name;
+		}
+	}
+}
+
+/*
+ * Allocate a new tranche ID.
+ */
+int
+ExtensionWaitEventNewTrancheId(void)
+{
+	int			result;
+	int		   *ExtensionWaitEventCounter;
+
+	ExtensionWaitEventCounter = (int *) ((char *) NamedExtensionWaitEventTrancheArray - sizeof(int));
+	SpinLockAcquire(ShmemLock);
+	result = (*ExtensionWaitEventCounter)++;
+	SpinLockRelease(ShmemLock);
+
+	return result;
+}
+
+void
+ExtensionWaitEventRegisterTranche(int tranche_id, const char *tranche_name)
+{
+	/* This should only be called for user-defined tranches. */
+	if (tranche_id < NUM_BUILDIN_WAIT_EVENT_EXTENSION)
+		return;
+
+	/* Convert to array index. */
+	tranche_id -= NUM_BUILDIN_WAIT_EVENT_EXTENSION;
+
+	/* If necessary, create or enlarge array. */
+	if (tranche_id >= ExtensionWaitEventTrancheNamesAllocated)
+	{
+		int			newalloc;
+
+		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+
+		if (ExtensionWaitEventTrancheNames == NULL)
+			ExtensionWaitEventTrancheNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		else
+			ExtensionWaitEventTrancheNames =
+				repalloc0_array(ExtensionWaitEventTrancheNames, const char *, ExtensionWaitEventTrancheNamesAllocated, newalloc);
+		ExtensionWaitEventTrancheNamesAllocated = newalloc;
+	}
+
+	ExtensionWaitEventTrancheNames[tranche_id] = tranche_name;
+}
+
+/*
+ * GetNamedExtensionWaitEventTranche - returns the Extension wait event information
+ *      from the specified tranche.
+ *
+ * Caller needs to retrieve the requested number of Extensions starting from
+ * the base extension address returned by this API.  This can be used for
+ * tranches that are requested by using RequestNamedExtensionWaitEventTranche() API.
+ */
+uint32
+GetNamedExtensionWaitEventTranche(const char *tranche_name)
+{
+	uint32		wait_event_info;
+	int			pos;
+	int			i;
+
+	/* The wait event type is always PG_WAIT_EXTENSION. */
+	wait_event_info = PG_WAIT_EXTENSION;
+
+	/*
+	 * Obtain the name of Extension wait event belonging to requested
+	 * tranche_name in NamedExtensionWaitEventTrancheArray.
+	 */
+	pos = 0;
+	for (i = 0; i < NamedExtensionWaitEventTrancheRequests; i++)
+	{
+		if (strcmp(NamedExtensionWaitEventTrancheRequestArray[i].tranche_name,
+				   tranche_name) == 0)
+		{
+			wait_event_info |= NamedExtensionWaitEventTrancheArray[pos].trancheId;
+			return wait_event_info;
+		}
+
+		pos++;
+	}
+
+	elog(ERROR, "requested tranche is not registered");
+
+	/* just to keep compiler quiet */
+	return wait_event_info;
+}
+
+/*
+ * Return an identifier based on the Extension wait event.
+ */
+static const char *
+GetExtensionIdentifier(uint32 classId, uint16 eventId)
+{
+	Assert(classId == PG_WAIT_EXTENSION);
+	/* The event IDs are just tranche numbers. */
+	return GetExtensionTrancheName(eventId);
+}
+
+/*
+ * Return the name of an Extension tranche.
+ */
+static const char *
+GetExtensionTrancheName(uint16 trancheId)
+{
+	/* Build-in tranche? */
+	if (trancheId < NUM_BUILDIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/* It's an extension tranche, so look in ExtensionWaitEventTrancheNames[]. */
+	trancheId -= NUM_BUILDIN_WAIT_EVENT_EXTENSION;
+	Assert(trancheId < ExtensionWaitEventTrancheNamesAllocated);
+
+	return ExtensionWaitEventTrancheNames[trancheId];
+}
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 518d3b0a1f..fa42671bd5 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -70,6 +70,38 @@ typedef enum
 	WAIT_EVENT_WAL_SENDER_WRITE_DATA,
 } WaitEventClient;
 
+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define custom wait events by calling RequestNamedExtensionWaitEventTranche()
+ * during postmaster startup.  Subsequently, call GetNamedExtensionWaitEventTranche() to
+ * obtain the wait event information requested.
+ * ----------
+ */
+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+}			WaitEventExtension;
+
+extern void WaitEventShmemInit(void);
+extern Size WaitEventShmemSize(void);
+extern void RequestNamedExtensionWaitEventTranche(const char *tranche_name);
+extern uint32 GetNamedExtensionWaitEventTranche(const char *tranche_name);
+
+/* struct for storing named tranche information */
+typedef struct NamedExtensionWaitEventTranche
+{
+	uint16		trancheId;
+	char	   *trancheName;
+}			NamedExtensionWaitEventTranche;
+
+extern PGDLLIMPORT NamedExtensionWaitEventTranche * NamedExtensionWaitEventTrancheArray;
+extern PGDLLIMPORT int NamedExtensionWaitEventTrancheRequests;
+
 /* ----------
  * Wait Events - IPC
  *
-- 
2.25.1

0003-Add-docs-to-define-custom-wait-events.patchtext/x-diff; name=0003-Add-docs-to-define-custom-wait-events.patchDownload
From 65e25d4b27bbb6d0934872310c24ee19f89e9631 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Thu, 15 Jun 2023 13:16:00 +0900
Subject: [PATCH 3/3] Add docs to define custom wait events

---
 doc/src/sgml/monitoring.sgml |  7 +++++++
 doc/src/sgml/xfunc.sgml      | 22 +++++++++++++++++++---
 2 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 5cfdc70c03..aefdc03dbd 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1257,6 +1257,13 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    Extensions can add <literal>Extension</literal> types to the list shown in
+    <xref linkend="wait-event-extension-table"/>.
+   </para>
+  </note>
+
   <table id="wait-event-io-table">
    <title>Wait Events of Type <literal>IO</literal></title>
    <tgroup cols="2">
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 9620ea9ae3..162d50e273 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3397,16 +3397,16 @@ CREATE FUNCTION make_array(anyelement) RETURNS anyarray
    </sect2>
 
    <sect2 id="xfunc-shared-addin">
-    <title>Shared Memory and LWLocks</title>
+    <title>Shared Memory allocated by addins</title>
 
     <para>
-     Add-ins can reserve LWLocks and an allocation of shared memory on server
+     Add-ins can reserve LWLocks, wait events, and an allocation of shared memory on server
      startup.  The add-in's shared library must be preloaded by specifying
      it in
      <xref linkend="guc-shared-preload-libraries"/><indexterm><primary>shared_preload_libraries</primary></indexterm>.
      The shared library should register a <literal>shmem_request_hook</literal>
      in its <function>_PG_init</function> function.  This
-     <literal>shmem_request_hook</literal> can reserve LWLocks or shared memory.
+     <literal>shmem_request_hook</literal> can reserve LWLocks, wait events, or shared memory.
      Shared memory is reserved by calling:
 <programlisting>
 void RequestAddinShmemSpace(int size)
@@ -3451,6 +3451,22 @@ if (!ptr)
 }
 </programlisting>
     </para>
+    <para>
+     wait events are reserved by calling:
+<programlisting>
+void RequestNamedExtensionWaitEventTranche(const char *tranche_name)
+</programlisting>
+     from your <literal>shmem_request_hook</literal>.  This will ensure that 
+     wait event is available under the name <literal>tranche_name</literal>,
+     which the wait event type is <literal>Extension</literal>.
+     Use <function>GetNamedExtensionWaitEventTranche</function>
+     to get a wait event information.
+    </para>
+    <para>
+     To avoid possible race-conditions, each backend should use the LWLock
+     <function>AddinShmemInitLock</function> when connecting to and initializing
+     its allocation of shared memory, same as LWLocks reservations above.
+    </para>
    </sect2>
 
    <sect2 id="extend-cpp">
-- 
2.25.1

0002-Add-a-extension-to-test-custom-wait-event.patchtext/x-diff; name=0002-Add-a-extension-to-test-custom-wait-event.patchDownload
From 97b1bc1ac379038701d1b57f8f64149b13fc351b Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Thu, 15 Jun 2023 11:24:01 +0900
Subject: [PATCH 1/3] Add a extension to test custom wait event

Since the extension is used for only development, it's not intended
to be commited in master branch.
---
 contrib/inject_wait_event/.gitignore          |   1 +
 contrib/inject_wait_event/Makefile            |  24 ++++
 .../inject_wait_event--1.0.sql                |  12 ++
 contrib/inject_wait_event/inject_wait_event.c | 119 ++++++++++++++++++
 .../inject_wait_event.control                 |   4 +
 5 files changed, 160 insertions(+)
 create mode 100644 contrib/inject_wait_event/.gitignore
 create mode 100644 contrib/inject_wait_event/Makefile
 create mode 100644 contrib/inject_wait_event/inject_wait_event--1.0.sql
 create mode 100644 contrib/inject_wait_event/inject_wait_event.c
 create mode 100644 contrib/inject_wait_event/inject_wait_event.control

diff --git a/contrib/inject_wait_event/.gitignore b/contrib/inject_wait_event/.gitignore
new file mode 100644
index 0000000000..43cea6480a
--- /dev/null
+++ b/contrib/inject_wait_event/.gitignore
@@ -0,0 +1 @@
+inject_wait_event
\ No newline at end of file
diff --git a/contrib/inject_wait_event/Makefile b/contrib/inject_wait_event/Makefile
new file mode 100644
index 0000000000..40221e7ac2
--- /dev/null
+++ b/contrib/inject_wait_event/Makefile
@@ -0,0 +1,24 @@
+# contrib/inject_wait_event/Makefile
+
+MODULE_big	= inject_wait_event
+
+OBJS = \
+	$(WIN32RES) \
+	inject_wait_event.o
+
+EXTENSION = inject_wait_event
+DATA = inject_wait_event--1.0.sql
+PGFILEDESC = "inject wait event for test"
+
+TAP_TESTS = 0
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/inject_wait_event
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/inject_wait_event/inject_wait_event--1.0.sql b/contrib/inject_wait_event/inject_wait_event--1.0.sql
new file mode 100644
index 0000000000..8ebab415c3
--- /dev/null
+++ b/contrib/inject_wait_event/inject_wait_event--1.0.sql
@@ -0,0 +1,12 @@
+/* contrib/inject_wait_event/inject_wait_event--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION inject_wait_event" to load this file. \quit
+
+--
+-- inject_wait_event()
+--
+CREATE FUNCTION inject_wait_event()
+RETURNS VOID
+AS 'MODULE_PATHNAME', 'inject_wait_event'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
diff --git a/contrib/inject_wait_event/inject_wait_event.c b/contrib/inject_wait_event/inject_wait_event.c
new file mode 100644
index 0000000000..b9673808d3
--- /dev/null
+++ b/contrib/inject_wait_event/inject_wait_event.c
@@ -0,0 +1,119 @@
+/*-------------------------------------------------------------------------
+ *
+ * inject_wait_event.c
+ *     Support a function to inject wait events for test
+ *
+ * Since the extension assumes to be used for only development,
+ * don't commit the master branch.
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/latch.h"
+#include "storage/shmem.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(inject_wait_event);
+
+typedef struct state
+{
+	uint32		custom_wait_event;	/* custom a wait event */
+}			state;
+
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_request_hook_type prev_shmem_startup_hook = NULL;
+
+static state * ss = NULL;
+
+static void shmem_request(void);
+static void shmem_startup(void);
+static void shmem_startup(void);
+static Size memsize(void);
+
+void
+_PG_init(void)
+{
+	if (!process_shared_preload_libraries_in_progress)
+		return;
+
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = shmem_startup;
+}
+
+static void
+shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(memsize());
+
+	/* define a custom wait event */
+	RequestNamedExtensionWaitEventTranche("custom_wait_event");
+}
+
+static void
+shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	ss = NULL;
+
+	/*
+	 * Create or attach to the shared memory state
+	 */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+	ss = ShmemInitStruct("inject_wait_event",
+						 sizeof(state),
+						 &found);
+
+	if (!found)
+	{
+		ss->custom_wait_event = GetNamedExtensionWaitEventTranche("custom_wait_event");
+	}
+	LWLockRelease(AddinShmemInitLock);
+
+	return;
+}
+
+static Size
+memsize(void)
+{
+	Size		size;
+
+	size = MAXALIGN(sizeof(state));
+
+	return size;
+}
+
+Datum
+inject_wait_event(PG_FUNCTION_ARGS)
+{
+	/* default */
+	(void) WaitLatch(MyLatch,
+					 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					 10L * 1000,	/* 10s */
+					 PG_WAIT_EXTENSION);
+	ResetLatch(MyLatch);
+
+	/* custom wait event */
+	(void) WaitLatch(MyLatch,
+					 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					 10L * 1000,	/* 10s */
+					 ss->custom_wait_event);
+	ResetLatch(MyLatch);
+
+	PG_RETURN_VOID();
+}
diff --git a/contrib/inject_wait_event/inject_wait_event.control b/contrib/inject_wait_event/inject_wait_event.control
new file mode 100644
index 0000000000..3f1132185e
--- /dev/null
+++ b/contrib/inject_wait_event/inject_wait_event.control
@@ -0,0 +1,4 @@
+# inject_wait_event extension
+comment = 'a function for injecting wait event for test'
+default_version = '1.0'
+module_pathname = '$libdir/inject_wait_event'
-- 
2.25.1

#2Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#1)
Re: Support to define custom wait events for extensions

On Thu, Jun 15, 2023 at 03:06:01PM +0900, Masahiro Ikeda wrote:

Currently, only one PG_WAIT_EXTENSION event can be used as a
wait event for extensions. Therefore, in environments with multiple
extensions are installed, it could take time to identify which
extension is the bottleneck.

Thanks for taking the time to implement a patch to do that.

I want to know your feedbacks. Please feel free to comment.

I think that's been cruelly missed.

In the core, the requested wait events are dynamically registered in shared
memory. The extension then obtains the wait event information with
GetNamedExtensionWaitEventTranche() and uses the value to notify the core
that it is waiting.

When a string representing of the wait event is requested,
it returns the name defined by calling
RequestNamedExtensionWaitEventTranche().

So this implements the equivalent of RequestNamedLWLockTranche()
followed by GetNamedLWLockTranche() to get the wait event number,
which can be used only during postmaster startup. Do you think that
we could focus on implementing something more flexible instead, that
can be used dynamically as well as statically? That would be similar
to LWLockNewTrancheId() and LWLockRegisterTranche(), actually, where
we would get one or more tranche IDs, then do initialization actions
in shmem based on the tranche ID(s).

4. check the custom wait event
You can see the following results of psql-1.

query | wait_event_type | wait_event
-----------------------------+-----------------+-------------------
SELECT inject_wait_event(); | Extension | custom_wait_event #
requested wait event by the extension!
(1 row)

(..snip..)

A problem with this approach is that it is expensive as a test. Do we
really need one? There are three places that set PG_WAIT_EXTENSION in
src/test/modules/, more in /contrib, and there are modules like
pg_stat_statements that could gain from events for I/O operations, for
example.

# TODOs

* tests on windows (since I tested on Ubuntu 20.04 only)
* add custom wait events for existing contrib modules (ex. postgres_fdw)
* add regression code (but, it seems to be difficult)
* others? (Please let me know)

Hmm. You would need to maintain a state in a rather stable manner,
and SQL queries can make that difficult in the TAP tests as the wait
event information is reset each time a query finishes. One area where
I think this gets easier is with a background worker loaded with
shared_preload_libraries that has a configurable naptime. Looking at
what's available in the tree, the TAP tests of pg_prewarm could use
one test on pg_stat_activity with a custom wait event name
(pg_prewarm.autoprewarm_interval is 0 hence the bgworker waits
infinitely). Note that in this case, you would need to be careful of
the case where the wait event is loaded dynamically, but like LWLocks
this should be able to work as well?
--
Michael

#3Drouvot, Bertrand
bertranddrouvot.pg@gmail.com
In reply to: Michael Paquier (#2)
Re: Support to define custom wait events for extensions

Hi,

On 6/15/23 10:00 AM, Michael Paquier wrote:

On Thu, Jun 15, 2023 at 03:06:01PM +0900, Masahiro Ikeda wrote:

Currently, only one PG_WAIT_EXTENSION event can be used as a
wait event for extensions. Therefore, in environments with multiple
extensions are installed, it could take time to identify which
extension is the bottleneck.

Thanks for taking the time to implement a patch to do that.

+1 thanks for it!

I want to know your feedbacks. Please feel free to comment.

I think that's been cruelly missed.

Yeah, that would clearly help to diagnose which extension(s) is/are causing the waits (if any).

I did not look at the code yet (will do) but just wanted to chime in to support the idea.

Regards,

--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

#4Tristan Partin
tristan@neon.tech
In reply to: Masahiro Ikeda (#1)
Re: Support to define custom wait events for extensions

We had this on our list of things to do at Neon, so it is a nice
surprise that you brought up an initial patchset :). It was also my
first time looking up the word tranche.

From 59a118402e5e59685fb9e0fb086872e25a405736 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Thu, 15 Jun 2023 12:57:29 +0900
Subject: [PATCH 2/3] Support to define custom wait events for extensions.

Currently, only one PG_WAIT_EXTENSION event can be used as a
wait event for extensions. Therefore, in environments with multiple
extensions are installed, it could take time to identify bottlenecks.

"extensions are installed" should be "extensions installed".

+#define NUM_BUILDIN_WAIT_EVENT_EXTENSION       \
+       (WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)

Should that be NUM_BUILTIN_WAIT_EVENT_EXTENSION?

+               NamedExtensionWaitEventTrancheRequestArray = (NamedExtensionWaitEventTrancheRequest *)
+                       MemoryContextAlloc(TopMemoryContext,
+                                                          NamedExtensionWaitEventTrancheRequestsAllocated
+                                                          * sizeof(NamedExtensionWaitEventTrancheRequest));

I can't tell from reading other Postgres code when one should cast the
return value of MemoryContextAlloc(). Seems unnecessary to me.

+       if (NamedExtensionWaitEventTrancheRequestArray == NULL)
+       {
+               NamedExtensionWaitEventTrancheRequestsAllocated = 16;
+               NamedExtensionWaitEventTrancheRequestArray = (NamedExtensionWaitEventTrancheRequest *)
+                       MemoryContextAlloc(TopMemoryContext,
+                                                          NamedExtensionWaitEventTrancheRequestsAllocated
+                                                          * sizeof(NamedExtensionWaitEventTrancheRequest));
+       }
+
+       if (NamedExtensionWaitEventTrancheRequests >= NamedExtensionWaitEventTrancheRequestsAllocated)
+       {
+               int                     i = pg_nextpower2_32(NamedExtensionWaitEventTrancheRequests + 1);
+
+               NamedExtensionWaitEventTrancheRequestArray = (NamedExtensionWaitEventTrancheRequest *)
+                       repalloc(NamedExtensionWaitEventTrancheRequestArray,
+                                        i * sizeof(NamedExtensionWaitEventTrancheRequest));
+               NamedExtensionWaitEventTrancheRequestsAllocated = i;
+       }

Do you think this code would look better in an if/else if?

+ int i = pg_nextpower2_32(NamedExtensionWaitEventTrancheRequests + 1);

In the Postgres codebase, is an int always guaranteed to be at least 32
bits? I feel like a fixed-width type would be better for tracking the
length of the array, unless Postgres prefers the Size type.

+       Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
+       strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);

A sizeof(request->tranche_name) would keep this code more in-sync if
size of tranche_name were to ever change, though I see sizeof
expressions in the codebase are not too common. Maybe just remove the +1
and make it less than rather than a less than or equal? Seems like it
might be worth noting in the docs of the function that the event name
has to be less than NAMEDATALEN, but maybe this is something extension
authors are inherently aware of?

---

What's the Postgres policy on the following?

for (int i = 0; ...)
for (i = 0; ...)

You are using 2 different patterns in WaitEventShmemInit() and
InitializeExtensionWaitEventTranches().

+       /*
+        * Copy the info about any named tranches into shared memory (so that
+        * other processes can see it), and initialize the requested wait events.
+        */
+       if (NamedExtensionWaitEventTrancheRequests > 0)

Removing this if would allow one less indentation level. Nothing would
have to change about the containing code either since the for loop will
then not run

+ ExtensionWaitEventCounter = (int *) ((char *) NamedExtensionWaitEventTrancheArray - sizeof(int));

From 65e25d4b27bbb6d0934872310c24ee19f89e9631 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Thu, 15 Jun 2023 13:16:00 +0900
Subject: [PATCH 3/3] Add docs to define custom wait events

+    <para>
+     wait events are reserved by calling:
+<programlisting>
+void RequestNamedExtensionWaitEventTranche(const char *tranche_name)
+</programlisting>
+     from your <literal>shmem_request_hook</literal>.  This will ensure that
+     wait event is available under the name <literal>tranche_name</literal>,
+     which the wait event type is <literal>Extension</literal>.
+     Use <function>GetNamedExtensionWaitEventTranche</function>
+     to get a wait event information.
+    </para>
+    <para>
+     To avoid possible race-conditions, each backend should use the LWLock
+     <function>AddinShmemInitLock</function> when connecting to and initializing
+     its allocation of shared memory, same as LWLocks reservations above.
+    </para>

Should "wait" be capitalized in the first sentence?

"This will ensure that wait event is available" should have an "a"
before "wait".

Nice patch.

--
Tristan Partin
Neon (https://neon.tech)

#5Michael Paquier
michael@paquier.xyz
In reply to: Tristan Partin (#4)
Re: Support to define custom wait events for extensions

On Thu, Jun 15, 2023 at 11:13:57AM -0500, Tristan Partin wrote:

What's the Postgres policy on the following?

for (int i = 0; ...)
for (i = 0; ...)

You are using 2 different patterns in WaitEventShmemInit() and
InitializeExtensionWaitEventTranches().

C99 style is OK since v12, so the style of the patch is fine. The
older style has no urgent need to change, either. One argument to let
the code as-is is that it could generate backpatching conflicts, while
it does not hurt as it stands. This also means that bug fixes that
need to be applied down to 12 would be able to use C99 declarations
freely without some of the buildfarm animals running REL_11_STABLE
complaining. I have fallen into this trap recently, actually. See
dbd25dd.
--
Michael

#6Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#2)
Re: Support to define custom wait events for extensions

Thanks for replying and your kind advice!

On 2023-06-15 17:00, Michael Paquier wrote:

On Thu, Jun 15, 2023 at 03:06:01PM +0900, Masahiro Ikeda wrote:

In the core, the requested wait events are dynamically registered in
shared
memory. The extension then obtains the wait event information with
GetNamedExtensionWaitEventTranche() and uses the value to notify the
core
that it is waiting.

When a string representing of the wait event is requested,
it returns the name defined by calling
RequestNamedExtensionWaitEventTranche().

So this implements the equivalent of RequestNamedLWLockTranche()
followed by GetNamedLWLockTranche() to get the wait event number,
which can be used only during postmaster startup. Do you think that
we could focus on implementing something more flexible instead, that
can be used dynamically as well as statically? That would be similar
to LWLockNewTrancheId() and LWLockRegisterTranche(), actually, where
we would get one or more tranche IDs, then do initialization actions
in shmem based on the tranche ID(s).

OK, I agree. I'll make a patch to only support
ExtensionWaitEventNewTrancheId() and ExtensionWaitEventRegisterTranche()
similar to LWLockNewTrancheId() and LWLockRegisterTranche().

4. check the custom wait event
You can see the following results of psql-1.

query | wait_event_type | wait_event
-----------------------------+-----------------+-------------------
SELECT inject_wait_event(); | Extension | custom_wait_event
#
requested wait event by the extension!
(1 row)

(..snip..)

A problem with this approach is that it is expensive as a test. Do we
really need one? There are three places that set PG_WAIT_EXTENSION in
src/test/modules/, more in /contrib, and there are modules like
pg_stat_statements that could gain from events for I/O operations, for
example.

Yes. Since it's hard to test, I thought the PoC extension
should not be committed. But, I couldn't figure out the best
way to test yet.

# TODOs

* tests on windows (since I tested on Ubuntu 20.04 only)
* add custom wait events for existing contrib modules (ex.
postgres_fdw)
* add regression code (but, it seems to be difficult)
* others? (Please let me know)

Hmm. You would need to maintain a state in a rather stable manner,
and SQL queries can make that difficult in the TAP tests as the wait
event information is reset each time a query finishes. One area where
I think this gets easier is with a background worker loaded with
shared_preload_libraries that has a configurable naptime. Looking at
what's available in the tree, the TAP tests of pg_prewarm could use
one test on pg_stat_activity with a custom wait event name
(pg_prewarm.autoprewarm_interval is 0 hence the bgworker waits
infinitely). Note that in this case, you would need to be careful of
the case where the wait event is loaded dynamically, but like LWLocks
this should be able to work as well?

Thanks for your advice!

I tried to query on pg_stat_activity to check the background worker
invoked by pg_prewarm. But, I found that pg_stat_activity doesn't show
it although I may be missing something...

So, I tried to implement TAP tests. But I have a problem with it.
I couldn't find the way to check the status of another backend
while the another backend wait with custom wait events.

```
# TAP test I've implemented.

# wait forever with custom wait events in session1
$session1->query_safe("SELECT test_custom_wait_events_wait()");

# I want to check the wait event from another backend process
# But, the following code is never reached because the above
# query is waiting forever.
$session2->poll_query_until('postgres',
qq[SELECT
(SELECT count(*) FROM pg_stat_activity
WHERE query ~ '^SELECT test_custom_wait_events_wait'
AND wait_event_type = 'Extension'
AND wait_event = 'custom_wait_event'
) > 0;]);
```

If I'm missing something or you have any idea,
please let me know.

Now, I plan to

* find out more the existing tests to check wait events and locks
(though I have already checked a little, but I couldn't find it)
* find another way to check wait event of the background worker invoked
by extension
* look up the reason why pg_stat_activity doesn't show the background
worker
* find a way to implement async queries in TAP tests

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#7Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Drouvot, Bertrand (#3)
Re: Support to define custom wait events for extensions

On 2023-06-15 22:21, Drouvot, Bertrand wrote:

Hi,

On 6/15/23 10:00 AM, Michael Paquier wrote:

On Thu, Jun 15, 2023 at 03:06:01PM +0900, Masahiro Ikeda wrote:

Currently, only one PG_WAIT_EXTENSION event can be used as a
wait event for extensions. Therefore, in environments with multiple
extensions are installed, it could take time to identify which
extension is the bottleneck.

Thanks for taking the time to implement a patch to do that.

+1 thanks for it!

I want to know your feedbacks. Please feel free to comment.

I think that's been cruelly missed.

Yeah, that would clearly help to diagnose which extension(s) is/are
causing the waits (if any).

I did not look at the code yet (will do) but just wanted to chime in
to support the idea.

Great! Thanks.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#8Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Tristan Partin (#4)
Re: Support to define custom wait events for extensions

On 2023-06-16 01:13, Tristan Partin wrote:

We had this on our list of things to do at Neon, so it is a nice
surprise that you brought up an initial patchset :). It was also my
first time looking up the word tranche.

What a coincidence! I came up with the idea when I used Neon with
postgres_fdw. As a Neon user, I also feel the feature is important.

Same as you. Thanks to Michael and Drouvot, I got to know the word
tranche
and the related existing code.

From 59a118402e5e59685fb9e0fb086872e25a405736 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Thu, 15 Jun 2023 12:57:29 +0900
Subject: [PATCH 2/3] Support to define custom wait events for
extensions.

Currently, only one PG_WAIT_EXTENSION event can be used as a
wait event for extensions. Therefore, in environments with multiple
extensions are installed, it could take time to identify bottlenecks.

"extensions are installed" should be "extensions installed".

+#define NUM_BUILDIN_WAIT_EVENT_EXTENSION       \
+       (WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - 
WAIT_EVENT_EXTENSION)

Should that be NUM_BUILTIN_WAIT_EVENT_EXTENSION?

Thanks for your comments.
Yes, I'll fix it.

+               NamedExtensionWaitEventTrancheRequestArray = 
(NamedExtensionWaitEventTrancheRequest *)
+                       MemoryContextAlloc(TopMemoryContext,
+                                                          
NamedExtensionWaitEventTrancheRequestsAllocated
+                                                          * 
sizeof(NamedExtensionWaitEventTrancheRequest));

I can't tell from reading other Postgres code when one should cast the
return value of MemoryContextAlloc(). Seems unnecessary to me.

I referenced RequestNamedLWLockTranche() and it looks ok to me.

```
void
RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
MemoryContextAlloc(TopMemoryContext,
NamedLWLockTrancheRequestsAllocated
* sizeof(NamedLWLockTrancheRequest));
```

+       if (NamedExtensionWaitEventTrancheRequestArray == NULL)
+       {
+               NamedExtensionWaitEventTrancheRequestsAllocated = 16;
+               NamedExtensionWaitEventTrancheRequestArray = 
(NamedExtensionWaitEventTrancheRequest *)
+                       MemoryContextAlloc(TopMemoryContext,
+                                                          
NamedExtensionWaitEventTrancheRequestsAllocated
+                                                          * 
sizeof(NamedExtensionWaitEventTrancheRequest));
+       }
+
+       if (NamedExtensionWaitEventTrancheRequests >= 
NamedExtensionWaitEventTrancheRequestsAllocated)
+       {
+               int                     i = 
pg_nextpower2_32(NamedExtensionWaitEventTrancheRequests + 1);
+
+               NamedExtensionWaitEventTrancheRequestArray = 
(NamedExtensionWaitEventTrancheRequest *)
+                       
repalloc(NamedExtensionWaitEventTrancheRequestArray,
+                                        i * 
sizeof(NamedExtensionWaitEventTrancheRequest));
+               NamedExtensionWaitEventTrancheRequestsAllocated = i;
+       }

Do you think this code would look better in an if/else if?

Same as above. I referenced RequestNamedLWLockTranche().
I don't know if it's a good idea, but it's better to refactor the
existing code separately from this patch.

But I plan to remove the code to focus implementing dynamic allocation
similar to LWLockNewTrancheId() and LWLockRegisterTranche() as
Michael's suggestion. I think it's good idea as a first step. Is it ok
for you?

+ int i =
pg_nextpower2_32(NamedExtensionWaitEventTrancheRequests + 1);

In the Postgres codebase, is an int always guaranteed to be at least 32
bits? I feel like a fixed-width type would be better for tracking the
length of the array, unless Postgres prefers the Size type.

Same as above.

+       Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
+       strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);

A sizeof(request->tranche_name) would keep this code more in-sync if
size of tranche_name were to ever change, though I see sizeof
expressions in the codebase are not too common. Maybe just remove the
+1
and make it less than rather than a less than or equal? Seems like it
might be worth noting in the docs of the function that the event name
has to be less than NAMEDATALEN, but maybe this is something extension
authors are inherently aware of?

Same as above.

---

What's the Postgres policy on the following?

for (int i = 0; ...)
for (i = 0; ...)
You are using 2 different patterns in WaitEventShmemInit() and
InitializeExtensionWaitEventTranches().

I didn't care it. I'll unify it.
Michael's replay is interesting.

+       /*
+        * Copy the info about any named tranches into shared memory 
(so that
+        * other processes can see it), and initialize the requested 
wait events.
+        */
+       if (NamedExtensionWaitEventTrancheRequests > 0)

Removing this if would allow one less indentation level. Nothing would
have to change about the containing code either since the for loop will
then not run

Thanks, but I plan to remove to focus implementing dynamic allocation.

+ ExtensionWaitEventCounter = (int *) ((char *)
NamedExtensionWaitEventTrancheArray - sizeof(int));

From 65e25d4b27bbb6d0934872310c24ee19f89e9631 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Thu, 15 Jun 2023 13:16:00 +0900
Subject: [PATCH 3/3] Add docs to define custom wait events

+    <para>
+     wait events are reserved by calling:
+<programlisting>
+void RequestNamedExtensionWaitEventTranche(const char *tranche_name)
+</programlisting>
+     from your <literal>shmem_request_hook</literal>.  This will 
ensure that
+     wait event is available under the name 
<literal>tranche_name</literal>,
+     which the wait event type is <literal>Extension</literal>.
+     Use <function>GetNamedExtensionWaitEventTranche</function>
+     to get a wait event information.
+    </para>
+    <para>
+     To avoid possible race-conditions, each backend should use the 
LWLock
+     <function>AddinShmemInitLock</function> when connecting to and 
initializing
+     its allocation of shared memory, same as LWLocks reservations 
above.
+    </para>

Should "wait" be capitalized in the first sentence?

Yes, I'll fix it

"This will ensure that wait event is available" should have an "a"
before "wait".

Yes, I'll fix it

Nice patch.

Thanks for your comments too.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#9Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#6)
Re: Support to define custom wait events for extensions

On Fri, Jun 16, 2023 at 11:14:05AM +0900, Masahiro Ikeda wrote:

I tried to query on pg_stat_activity to check the background worker
invoked by pg_prewarm. But, I found that pg_stat_activity doesn't show
it although I may be missing something...

So, I tried to implement TAP tests. But I have a problem with it.
I couldn't find the way to check the status of another backend
while the another backend wait with custom wait events.

Hmm. Right. It seems to me that BGWORKER_BACKEND_DATABASE_CONNECTION
is required in this case, with BackgroundWorkerInitializeConnection()
to connect to a database (or not, like the logical replication
launcher if only access to shared catalogs is wanted).

I have missed that the leader process of pg_prewarm does not use that,
because it has no need to connect to a database, but its workers do.
So it is not going to show up in pg_stat_activity.
--
Michael

#10Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#9)
Re: Support to define custom wait events for extensions

On 2023-06-16 16:46, Michael Paquier wrote:

On Fri, Jun 16, 2023 at 11:14:05AM +0900, Masahiro Ikeda wrote:

I tried to query on pg_stat_activity to check the background worker
invoked by pg_prewarm. But, I found that pg_stat_activity doesn't show
it although I may be missing something...

So, I tried to implement TAP tests. But I have a problem with it.
I couldn't find the way to check the status of another backend
while the another backend wait with custom wait events.

Hmm. Right. It seems to me that BGWORKER_BACKEND_DATABASE_CONNECTION
is required in this case, with BackgroundWorkerInitializeConnection()
to connect to a database (or not, like the logical replication
launcher if only access to shared catalogs is wanted).

I have missed that the leader process of pg_prewarm does not use that,
because it has no need to connect to a database, but its workers do.
So it is not going to show up in pg_stat_activity.

Yes. Thanks to your advice, I understood that
BGWORKER_BACKEND_DATABASE_CONNECTION is the reason.

I could make the TAP test that invokes a background worker waiting
forever
and checks its custom wait event in pg_stat_activity. So, I'll make
patches
including test codes next week.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#11Tristan Partin
tristan@neon.tech
In reply to: Masahiro Ikeda (#8)
Re: Support to define custom wait events for extensions

I will take a look at your V2 when it is ready! I will also pass along
that this is wanted by Neon customers :).

--
Tristan Partin
Neon (https://neon.tech)

#12Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Tristan Partin (#11)
Re: Support to define custom wait events for extensions

On 2023/06/17 1:16, Tristan Partin wrote:

I will take a look at your V2 when it is ready! I will also pass along
that this is wanted by Neon customers :).

Thanks!

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#13Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Masahiro Ikeda (#12)
2 attachment(s)
Re: Support to define custom wait events for extensions

Hi,

I updated the patches. The main changes are
* to support only dynamic wait event allocation
* to add a regression test
I appreciate any feedback.

The followings are TODO items.
* to check that meson.build works since I tested with old command `make`
now
* to make documents
* to add custom wait events for existing contrib modules (ex.
postgres_fdw)

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

0002-Add-test-codes-for-custom-wait-events.patchtext/x-diff; name=0002-Add-test-codes-for-custom-wait-events.patchDownload
From 206ce9e1d74d1449b50cfc765936172de98687e8 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Tue, 20 Jun 2023 17:59:34 +0900
Subject: [PATCH 2/2] Add test codes for custom wait events

---
 src/test/modules/Makefile                     |   1 +
 .../test_custom_wait_events/.gitignore        |   2 +
 .../modules/test_custom_wait_events/Makefile  |  23 +++
 .../test_custom_wait_events/meson.build       |  33 ++++
 .../test_custom_wait_events/t/001_basic.pl    |  34 ++++
 .../test_custom_wait_events--1.0.sql          |  14 ++
 .../test_custom_wait_events.c                 | 182 ++++++++++++++++++
 .../test_custom_wait_events.control           |   3 +
 8 files changed, 292 insertions(+)
 create mode 100644 src/test/modules/test_custom_wait_events/.gitignore
 create mode 100644 src/test/modules/test_custom_wait_events/Makefile
 create mode 100644 src/test/modules/test_custom_wait_events/meson.build
 create mode 100644 src/test/modules/test_custom_wait_events/t/001_basic.pl
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.c
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 6331c976dc..84312b7e57 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -17,6 +17,7 @@ SUBDIRS = \
 		  test_bloomfilter \
 		  test_copy_callbacks \
 		  test_custom_rmgrs \
+		  test_custom_wait_events \
 		  test_ddl_deparse \
 		  test_extensions \
 		  test_ginpostinglist \
diff --git a/src/test/modules/test_custom_wait_events/.gitignore b/src/test/modules/test_custom_wait_events/.gitignore
new file mode 100644
index 0000000000..716e17f5a2
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/.gitignore
@@ -0,0 +1,2 @@
+# Generated subdirectories
+/tmp_check/
diff --git a/src/test/modules/test_custom_wait_events/Makefile b/src/test/modules/test_custom_wait_events/Makefile
new file mode 100644
index 0000000000..dda365d523
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_custom_wait_events/Makefile
+
+MODULE_big = test_custom_wait_events
+OBJS = \
+	$(WIN32RES) \
+	test_custom_wait_events.o
+PGFILEDESC = "test_custom_wait_events - test custom wait events"
+
+EXTENSION = test_custom_wait_events
+DATA = test_custom_wait_events--1.0.sql
+
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_custom_wait_events
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_custom_wait_events/meson.build b/src/test/modules/test_custom_wait_events/meson.build
new file mode 100644
index 0000000000..da2b720f8f
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2022-2023, PostgreSQL Global Development Group
+
+test_custom_wait_events = files(
+  'test_custom_wait_events.c',
+)
+
+if host_system == 'windows'
+  test_custom_wait_events += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_custom_wait_events',
+    '--FILEDESC', 'test_custom_wait_events - test custom wait events',])
+endif
+
+test_custom_wait_events = shared_module('test_custom_wait_events',
+  test_custom_wait_events,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_custom_wait_events
+
+test_install_data += files(
+  'test_custom_wait_events.control',
+  'test_custom_wait_events--1.0.sql',
+)
+
+tests += {
+  'name': 'test_custom_wait_events',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_basic.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_custom_wait_events/t/001_basic.pl b/src/test/modules/test_custom_wait_events/t/001_basic.pl
new file mode 100644
index 0000000000..59ba14919b
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/t/001_basic.pl
@@ -0,0 +1,34 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+
+$node->init;
+$node->append_conf(
+	'postgresql.conf', 
+	"shared_preload_libraries = 'test_custom_wait_events'"
+);
+$node->start;
+
+# setup
+$node->safe_psql('postgres', 'CREATE EXTENSION test_custom_wait_events;');
+
+$node->safe_psql('postgres', 'SELECT wait_worker_launch();');
+
+ok($node->poll_query_until('postgres',
+	qq[SELECT 
+		(SELECT count(*) FROM pg_stat_activity
+	 		WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND wait_event = 'custom_wait_event'
+		) > 0;])
+) or die "Timed out waiting the custom wait event to be showed up";
+
+$node->stop;
+done_testing();
\ No newline at end of file
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
new file mode 100644
index 0000000000..1fc2130f84
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
@@ -0,0 +1,14 @@
+/* src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_custom_wait_events" to load this file. \quit
+
+--
+-- wait_worker_launch()
+--
+-- Launch a background worker to wait forever with a custom wait event.
+--
+CREATE FUNCTION wait_worker_launch()
+RETURNS int
+AS 'MODULE_PATHNAME', 'wait_worker_launch'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.c b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
new file mode 100644
index 0000000000..ba3fe45ad6
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
@@ -0,0 +1,182 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_custom_wait_events.c
+ *		Code for testing custom wait events
+ *
+ * This code initializes a custom wait event in shmem_request_hook() and
+ * provide a function to launch a background worker waiting forever
+ * with the custom wait event.
+ * 
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_custom_wait_events/test_custom_wait_events.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lwlock.h"
+#include "storage/proc.h"
+#include "storage/shmem.h"
+#include "tcop/utility.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(wait_worker_launch);
+
+PGDLLEXPORT void worker_main(Datum main_arg);
+
+/* GUC variables */
+typedef struct state
+{
+	uint32			wait_event_info;  /* a allocated wait event */
+}			state;
+
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_request_hook_type prev_shmem_startup_hook = NULL;
+
+static char * worker_database = "postgres";   /* connected by a background worker */
+static state * ss = NULL;
+
+static void shmem_request(void);
+static void shmem_startup(void);
+static Size memsize(void);
+
+void
+_PG_init(void)
+{
+	if (!process_shared_preload_libraries_in_progress)
+		return;
+
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = shmem_startup;
+}
+
+static void
+shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(memsize());
+}
+
+static Size
+memsize(void)
+{
+	return sizeof(state);
+}
+
+/*
+ * Allocate and register a new wait event.
+ *
+ * This routine allocate a wait event id when it's called first time.
+ * After that, the name of the wait event is associated with the id. It's
+ * registered in the lookup table of each processes dynamically.
+ */
+static void
+shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	ss = NULL;
+
+	/* Create or attach to the shared memory state */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+	ss = ShmemInitStruct("custom_wait_event",
+						 sizeof(state),
+						 &found);
+
+	/* Allocate a new wait event. */
+	if (!found)
+		ss->wait_event_info = ExtensionWaitEventNewTranche();
+
+	LWLockRelease(AddinShmemInitLock);
+
+	/*
+	 * Register the wait event in the lookup table of the current process.
+	 */
+	ExtensionWaitEventRegisterTranche(ss->wait_event_info, "custom_wait_event");
+
+	return;
+}
+
+/*
+ * Connect to the database and wait forever with the custom wait event
+ */
+void
+worker_main(Datum main_arg)
+{
+	pqsignal(SIGTERM, SignalHandlerForShutdownRequest);
+	BackgroundWorkerUnblockSignals();
+
+	/* Connect to our database to show up a wait event in pg_stat_activity */
+	BackgroundWorkerInitializeConnection(worker_database, NULL, 0);
+
+	/* Wait forever with the custom wait event */
+	while (!ShutdownRequestPending)
+	{
+		(void) WaitLatch(MyLatch,
+						 WL_LATCH_SET | WL_EXIT_ON_PM_DEATH,
+						 -1L,
+						 ss->wait_event_info);  /* the custom wait event */
+		ResetLatch(MyLatch);
+	}
+}
+
+/*
+ * Dynamically launch a background worker waiting forever
+ */
+Datum
+wait_worker_launch(PG_FUNCTION_ARGS)
+{
+	BackgroundWorker worker;
+	BackgroundWorkerHandle *handle;
+	BgwHandleStatus status;
+	pid_t		pid;
+
+	memset(&worker, 0, sizeof(worker));
+	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	worker.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	worker.bgw_restart_time = BGW_NEVER_RESTART;
+	sprintf(worker.bgw_function_name, "worker_main");
+	sprintf(worker.bgw_library_name, "test_custom_wait_events");
+	snprintf(worker.bgw_name, BGW_MAXLEN, "test_custom_wait_events worker");
+	snprintf(worker.bgw_type, BGW_MAXLEN, "test_custom_wait_events");
+	worker.bgw_notify_pid = MyProcPid;
+
+	if (!RegisterDynamicBackgroundWorker(&worker, &handle))
+		PG_RETURN_NULL();
+
+	status = WaitForBackgroundWorkerStartup(handle, &pid);
+
+	if (status == BGWH_STOPPED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("could not start background process"),
+				 errhint("More details may be available in the server log.")));
+	if (status == BGWH_POSTMASTER_DIED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("cannot start background processes without postmaster"),
+				 errhint("Kill all remaining database processes and restart the database.")));
+	Assert(status == BGWH_STARTED);
+
+	PG_RETURN_INT32(pid);
+}
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.control b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
new file mode 100644
index 0000000000..8dd875a252
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
@@ -0,0 +1,3 @@
+comment = 'Test code for custom wait events'
+default_version = '1.0'
+module_pathname = '$libdir/test_custom_wait_events'
-- 
2.25.1

0001-Support-custom-wait-events-for-extensions.patchtext/x-diff; name=0001-Support-custom-wait-events-for-extensions.patchDownload
From 8a8093c7cdfff785bdde1d8e830367e5783ab37b Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Fri, 16 Jun 2023 11:53:29 +0900
Subject: [PATCH 1/2] Support custom wait events for extensions.

To support custom wait events, it add 2 APIs to allocate
new wait events dynamically.

The APIs are
* ExtensionWaitEventNewTranche()
* ExtensionWaitEventRegisterTranche()

They similar to existing LWLockNewTrancheId() and
LWLockRegisterTranche().

First, extension calls ExtensionWaitEventNewTranche()
just once to obtain a new wait event tranche; The ID is
allocated from a shared counter. Next, each individual
process using the tranche should call
ExtensionWaitEventRegisterTranche() to associate that
wait event with a name.
---
 src/backend/storage/ipc/ipci.c          |   3 +
 src/backend/storage/ipc/shmem.c         |   3 +-
 src/backend/utils/activity/wait_event.c | 138 +++++++++++++++++++++++-
 src/include/utils/wait_event.h          |  29 +++++
 4 files changed, 171 insertions(+), 2 deletions(-)

diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 8f1ded7338..ed05121fa3 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -49,6 +49,7 @@
 #include "storage/spin.h"
 #include "utils/guc.h"
 #include "utils/snapmgr.h"
+#include "utils/wait_event.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -142,6 +143,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SyncScanShmemSize());
 	size = add_size(size, AsyncShmemSize());
 	size = add_size(size, StatsShmemSize());
+	size = add_size(size, WaitEventShmemSize());
 #ifdef EXEC_BACKEND
 	size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -294,6 +296,7 @@ CreateSharedMemoryAndSemaphores(void)
 	SyncScanShmemInit();
 	AsyncShmemInit();
 	StatsShmemInit();
+	WaitEventShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/storage/ipc/shmem.c b/src/backend/storage/ipc/shmem.c
index 5465fa1964..02c72ebbb1 100644
--- a/src/backend/storage/ipc/shmem.c
+++ b/src/backend/storage/ipc/shmem.c
@@ -85,7 +85,8 @@ static void *ShmemBase;			/* start address of shared memory */
 
 static void *ShmemEnd;			/* end+1 address of shared memory */
 
-slock_t    *ShmemLock;			/* spinlock for shared memory and LWLock
+slock_t    *ShmemLock;			/* spinlock for shared memory, LWLock
+								 * allocation, and named extension wait event
 								 * allocation */
 
 static HTAB *ShmemIndex = NULL; /* primary index hashtable for shmem */
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 7940d64639..724a720f2c 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,11 +22,18 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
 #include "storage/lwlock.h"		/* for GetLWLockIdentifier */
+#include "storage/spin.h"
+#include "utils/memutils.h"
 #include "utils/wait_event.h"
 
 
+/* We use the ShmemLock spinlock to protect ExtensionWaitEventCounter */
+extern slock_t *ShmemLock;
+
 static const char *pgstat_get_wait_activity(WaitEventActivity w);
 static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
@@ -38,6 +45,23 @@ static uint32 local_my_wait_event_info;
 uint32	   *my_wait_event_info = &local_my_wait_event_info;
 
 
+/* dynamic allocation counter in shared memory */
+static uint16 *ExtensionWaitEventCounter;
+
+/* first event ID for named extension wait event tranche */
+#define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
+	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
+
+/*
+ * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event ID known to the current
+ * process.  Any unused entries in the array will contain NULL.
+ */
+static const char **ExtensionWaitEventTrancheNames = NULL;
+static int	ExtensionWaitEventTrancheNamesAllocated = 0;
+
+static const char *GetExtensionWaitEventIdentifier(uint16 eventId);
+
 /*
  * Configure wait event reporting to report wait events to *wait_event_info.
  * *wait_event_info needs to be valid until pgstat_reset_wait_event_storage()
@@ -165,7 +189,7 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				break;
 			}
 		case PG_WAIT_EXTENSION:
-			event_name = "Extension";
+			event_name = GetExtensionWaitEventIdentifier(eventId);
 			break;
 		case PG_WAIT_IPC:
 			{
@@ -762,3 +786,115 @@ pgstat_get_wait_io(WaitEventIO w)
 
 	return event_name;
 }
+
+/*
+ *  Return the space for dynamic allocation counter.
+ */
+Size
+WaitEventShmemSize(void)
+{
+	return sizeof(*ExtensionWaitEventCounter);
+}
+
+/*
+ * Allocate shmem space for dynamic allocation counter.
+ */
+void
+WaitEventShmemInit(void)
+{
+	if (!IsUnderPostmaster)
+	{
+		Size		space = WaitEventShmemSize();
+
+		/* Initialize the dynamic-allocation counter */
+		ExtensionWaitEventCounter = (uint16 *) ShmemAlloc(space);
+		*ExtensionWaitEventCounter = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+	}
+}
+
+/*
+ * Allocate a new event ID and return the wait event info.
+ */
+uint32
+ExtensionWaitEventNewTranche(void)
+{
+	uint16			eventId;
+
+	SpinLockAcquire(ShmemLock);
+	eventId = (*ExtensionWaitEventCounter)++;
+	SpinLockRelease(ShmemLock);
+
+	return PG_WAIT_EXTENSION | eventId;
+}
+
+/*
+ * Register a dynamic tranche name in the lookup table of the current process.
+ *
+ * This routine will save a pointer to the wait event tranche name passed
+ * as an argument, so the name should be allocated in a backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+ExtensionWaitEventRegisterTranche(uint32 wait_event_info, const char *wait_event_name)
+{
+	uint32		classId;
+	uint16		eventId;
+
+	classId = wait_event_info & 0xFF000000;
+	eventId = wait_event_info & 0x0000FFFF;
+	Assert(classId == PG_WAIT_EXTENSION);
+	
+	/* This should only be called for user-defined tranches. */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return;
+
+	/* Convert to array index. */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	/* If necessary, create or enlarge array. */
+	if (eventId >= ExtensionWaitEventTrancheNamesAllocated)
+	{
+		int			newalloc;
+
+		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
+
+		if (ExtensionWaitEventTrancheNames == NULL)
+			ExtensionWaitEventTrancheNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		else
+			ExtensionWaitEventTrancheNames =
+				repalloc0_array(ExtensionWaitEventTrancheNames, const char *, ExtensionWaitEventTrancheNamesAllocated, newalloc);
+		ExtensionWaitEventTrancheNamesAllocated = newalloc;
+	}
+
+	ExtensionWaitEventTrancheNames[eventId] = wait_event_name;
+}
+
+/*
+ * Return the name of an Extension wait event ID.
+ */
+static const char *
+GetExtensionWaitEventIdentifier(uint16 eventId)
+{
+	/* Build-in tranche? */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It's an extension tranche, so look in ExtensionWaitEventTrancheNames[].
+	 * However, it's possible that the tranche has never been registered by
+	 * calling ExtensionWaitEventRegisterTranche() in the current process, in
+	 * which case give up and return "Extension".
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= ExtensionWaitEventTrancheNamesAllocated ||
+		ExtensionWaitEventTrancheNames[eventId] == NULL)
+		return "Extension";
+
+	return ExtensionWaitEventTrancheNames[eventId];
+}
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 518d3b0a1f..789fcbf52a 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -70,6 +70,35 @@ typedef enum
 	WAIT_EVENT_WAL_SENDER_WRITE_DATA,
 } WaitEventClient;
 
+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define custom wait events.  First, call
+ * ExtensionWaitEventNewTranche() just once to obtain a new wait event tranche. 
+ * The ID is allocated from a shared counter. Next, each individual process using
+ * the tranche should call ExtensionWaitEventRegisterTranche() to associate
+ * that wait event with a name.
+ *
+ * It may seem strange that each process using the tranche must register it
+ * separately, but dynamic shared memory segments aren't guaranteed to be
+ * mapped at the same address in all coordinating backends, so storing the
+ * registration in the main shared memory segment wouldn't work for that case.
+ */
+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+}			WaitEventExtension;
+
+extern void WaitEventShmemInit(void);
+extern Size WaitEventShmemSize(void);
+
+extern uint32	ExtensionWaitEventNewTranche(void);
+extern void ExtensionWaitEventRegisterTranche(uint32 wait_event_info, const char *tranche_name);
+
 /* ----------
  * Wait Events - IPC
  *
-- 
2.25.1

#14Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Masahiro Ikeda (#13)
2 attachment(s)
Re: Support to define custom wait events for extensions

On 2023-06-20 18:26, Masahiro Ikeda wrote:

The followings are TODO items.
* to check that meson.build works since I tested with old command
`make` now

I test with meson and I updated the patches to work with it.
My test procedure is the following.

```
export builddir=/mnt/tmp/build
export prefix=/mnt/tmp/master

# setup
meson setup $builddir --prefix=$prefix -Ddebug=true -Dcassert=true
-Dtap_tests=enabled

# build and install with src/test/modules
ninja -C $builddir install install-test-files

# test
meson test -v -C $builddir
meson test -v -C $builddir --suite test_custom_wait_events # run the
test only
```

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

v3-0002-Add-test-codes-for-custom-wait-events.patchtext/x-diff; name=v3-0002-Add-test-codes-for-custom-wait-events.patchDownload
From 901b75d31070ef0029557db6981c98e06f5c16c3 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Tue, 20 Jun 2023 17:59:34 +0900
Subject: [PATCH 2/2] Add test codes for custom wait events

---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 .../test_custom_wait_events/.gitignore        |   2 +
 .../modules/test_custom_wait_events/Makefile  |  23 +++
 .../test_custom_wait_events/meson.build       |  33 ++++
 .../test_custom_wait_events/t/001_basic.pl    |  34 ++++
 .../test_custom_wait_events--1.0.sql          |  14 ++
 .../test_custom_wait_events.c                 | 182 ++++++++++++++++++
 .../test_custom_wait_events.control           |   3 +
 9 files changed, 293 insertions(+)
 create mode 100644 src/test/modules/test_custom_wait_events/.gitignore
 create mode 100644 src/test/modules/test_custom_wait_events/Makefile
 create mode 100644 src/test/modules/test_custom_wait_events/meson.build
 create mode 100644 src/test/modules/test_custom_wait_events/t/001_basic.pl
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.c
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 6331c976dc..84312b7e57 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -17,6 +17,7 @@ SUBDIRS = \
 		  test_bloomfilter \
 		  test_copy_callbacks \
 		  test_custom_rmgrs \
+		  test_custom_wait_events \
 		  test_ddl_deparse \
 		  test_extensions \
 		  test_ginpostinglist \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 17d369e378..83a1d8fd19 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -14,6 +14,7 @@ subdir('ssl_passphrase_callback')
 subdir('test_bloomfilter')
 subdir('test_copy_callbacks')
 subdir('test_custom_rmgrs')
+subdir('test_custom_wait_events')
 subdir('test_ddl_deparse')
 subdir('test_extensions')
 subdir('test_ginpostinglist')
diff --git a/src/test/modules/test_custom_wait_events/.gitignore b/src/test/modules/test_custom_wait_events/.gitignore
new file mode 100644
index 0000000000..716e17f5a2
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/.gitignore
@@ -0,0 +1,2 @@
+# Generated subdirectories
+/tmp_check/
diff --git a/src/test/modules/test_custom_wait_events/Makefile b/src/test/modules/test_custom_wait_events/Makefile
new file mode 100644
index 0000000000..dda365d523
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_custom_wait_events/Makefile
+
+MODULE_big = test_custom_wait_events
+OBJS = \
+	$(WIN32RES) \
+	test_custom_wait_events.o
+PGFILEDESC = "test_custom_wait_events - test custom wait events"
+
+EXTENSION = test_custom_wait_events
+DATA = test_custom_wait_events--1.0.sql
+
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_custom_wait_events
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_custom_wait_events/meson.build b/src/test/modules/test_custom_wait_events/meson.build
new file mode 100644
index 0000000000..8ad073e577
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) PostgreSQL Global Development Group
+
+test_custom_wait_events = files(
+  'test_custom_wait_events.c',
+)
+
+if host_system == 'windows'
+  test_custom_wait_events += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_custom_wait_events',
+    '--FILEDESC', 'test_custom_wait_events - test custom wait events',])
+endif
+
+test_custom_wait_events = shared_module('test_custom_wait_events',
+  test_custom_wait_events,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_custom_wait_events
+
+test_install_data += files(
+  'test_custom_wait_events.control',
+  'test_custom_wait_events--1.0.sql',
+)
+
+tests += {
+  'name': 'test_custom_wait_events',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_basic.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_custom_wait_events/t/001_basic.pl b/src/test/modules/test_custom_wait_events/t/001_basic.pl
new file mode 100644
index 0000000000..2809afbd47
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/t/001_basic.pl
@@ -0,0 +1,34 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+
+$node->init;
+$node->append_conf(
+	'postgresql.conf', 
+	"shared_preload_libraries = 'test_custom_wait_events'"
+);
+$node->start;
+
+# setup
+$node->safe_psql('postgres', 'CREATE EXTENSION test_custom_wait_events;');
+
+$node->safe_psql('postgres', 'SELECT wait_worker_launch();');
+
+ok($node->poll_query_until('postgres',
+	qq[SELECT 
+		(SELECT count(*) FROM pg_stat_activity
+	 		WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND wait_event = 'custom_wait_event'
+		) > 0;])
+) or die "Timed out waiting the custom wait event to be showed up";
+
+$node->stop;
+done_testing();
\ No newline at end of file
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
new file mode 100644
index 0000000000..1fc2130f84
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
@@ -0,0 +1,14 @@
+/* src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_custom_wait_events" to load this file. \quit
+
+--
+-- wait_worker_launch()
+--
+-- Launch a background worker to wait forever with a custom wait event.
+--
+CREATE FUNCTION wait_worker_launch()
+RETURNS int
+AS 'MODULE_PATHNAME', 'wait_worker_launch'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.c b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
new file mode 100644
index 0000000000..ba3fe45ad6
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
@@ -0,0 +1,182 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_custom_wait_events.c
+ *		Code for testing custom wait events
+ *
+ * This code initializes a custom wait event in shmem_request_hook() and
+ * provide a function to launch a background worker waiting forever
+ * with the custom wait event.
+ * 
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_custom_wait_events/test_custom_wait_events.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lwlock.h"
+#include "storage/proc.h"
+#include "storage/shmem.h"
+#include "tcop/utility.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(wait_worker_launch);
+
+PGDLLEXPORT void worker_main(Datum main_arg);
+
+/* GUC variables */
+typedef struct state
+{
+	uint32			wait_event_info;  /* a allocated wait event */
+}			state;
+
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_request_hook_type prev_shmem_startup_hook = NULL;
+
+static char * worker_database = "postgres";   /* connected by a background worker */
+static state * ss = NULL;
+
+static void shmem_request(void);
+static void shmem_startup(void);
+static Size memsize(void);
+
+void
+_PG_init(void)
+{
+	if (!process_shared_preload_libraries_in_progress)
+		return;
+
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = shmem_startup;
+}
+
+static void
+shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(memsize());
+}
+
+static Size
+memsize(void)
+{
+	return sizeof(state);
+}
+
+/*
+ * Allocate and register a new wait event.
+ *
+ * This routine allocate a wait event id when it's called first time.
+ * After that, the name of the wait event is associated with the id. It's
+ * registered in the lookup table of each processes dynamically.
+ */
+static void
+shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	ss = NULL;
+
+	/* Create or attach to the shared memory state */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+	ss = ShmemInitStruct("custom_wait_event",
+						 sizeof(state),
+						 &found);
+
+	/* Allocate a new wait event. */
+	if (!found)
+		ss->wait_event_info = ExtensionWaitEventNewTranche();
+
+	LWLockRelease(AddinShmemInitLock);
+
+	/*
+	 * Register the wait event in the lookup table of the current process.
+	 */
+	ExtensionWaitEventRegisterTranche(ss->wait_event_info, "custom_wait_event");
+
+	return;
+}
+
+/*
+ * Connect to the database and wait forever with the custom wait event
+ */
+void
+worker_main(Datum main_arg)
+{
+	pqsignal(SIGTERM, SignalHandlerForShutdownRequest);
+	BackgroundWorkerUnblockSignals();
+
+	/* Connect to our database to show up a wait event in pg_stat_activity */
+	BackgroundWorkerInitializeConnection(worker_database, NULL, 0);
+
+	/* Wait forever with the custom wait event */
+	while (!ShutdownRequestPending)
+	{
+		(void) WaitLatch(MyLatch,
+						 WL_LATCH_SET | WL_EXIT_ON_PM_DEATH,
+						 -1L,
+						 ss->wait_event_info);  /* the custom wait event */
+		ResetLatch(MyLatch);
+	}
+}
+
+/*
+ * Dynamically launch a background worker waiting forever
+ */
+Datum
+wait_worker_launch(PG_FUNCTION_ARGS)
+{
+	BackgroundWorker worker;
+	BackgroundWorkerHandle *handle;
+	BgwHandleStatus status;
+	pid_t		pid;
+
+	memset(&worker, 0, sizeof(worker));
+	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	worker.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	worker.bgw_restart_time = BGW_NEVER_RESTART;
+	sprintf(worker.bgw_function_name, "worker_main");
+	sprintf(worker.bgw_library_name, "test_custom_wait_events");
+	snprintf(worker.bgw_name, BGW_MAXLEN, "test_custom_wait_events worker");
+	snprintf(worker.bgw_type, BGW_MAXLEN, "test_custom_wait_events");
+	worker.bgw_notify_pid = MyProcPid;
+
+	if (!RegisterDynamicBackgroundWorker(&worker, &handle))
+		PG_RETURN_NULL();
+
+	status = WaitForBackgroundWorkerStartup(handle, &pid);
+
+	if (status == BGWH_STOPPED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("could not start background process"),
+				 errhint("More details may be available in the server log.")));
+	if (status == BGWH_POSTMASTER_DIED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("cannot start background processes without postmaster"),
+				 errhint("Kill all remaining database processes and restart the database.")));
+	Assert(status == BGWH_STARTED);
+
+	PG_RETURN_INT32(pid);
+}
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.control b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
new file mode 100644
index 0000000000..8dd875a252
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
@@ -0,0 +1,3 @@
+comment = 'Test code for custom wait events'
+default_version = '1.0'
+module_pathname = '$libdir/test_custom_wait_events'
-- 
2.25.1

v3-0001-Support-custom-wait-events-for-extensions.patchtext/x-diff; name=v3-0001-Support-custom-wait-events-for-extensions.patchDownload
From bf06b8100cb747031959fe81a2d19baabc4838cf Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Fri, 16 Jun 2023 11:53:29 +0900
Subject: [PATCH 1/2] Support custom wait events for extensions.

To support custom wait events, it add 2 APIs to allocate
new wait events dynamically.

The APIs are
* ExtensionWaitEventNewTranche()
* ExtensionWaitEventRegisterTranche()

They similar to existing LWLockNewTrancheId() and
LWLockRegisterTranche().

First, extension calls ExtensionWaitEventNewTranche()
just once to obtain a new wait event tranche; The ID is
allocated from a shared counter. Next, each individual
process using the tranche should call
ExtensionWaitEventRegisterTranche() to associate that
wait event with a name.
---
 src/backend/storage/ipc/ipci.c          |   3 +
 src/backend/storage/ipc/shmem.c         |   3 +-
 src/backend/utils/activity/wait_event.c | 138 +++++++++++++++++++++++-
 src/include/utils/wait_event.h          |  29 +++++
 4 files changed, 171 insertions(+), 2 deletions(-)

diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 8f1ded7338..ed05121fa3 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -49,6 +49,7 @@
 #include "storage/spin.h"
 #include "utils/guc.h"
 #include "utils/snapmgr.h"
+#include "utils/wait_event.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -142,6 +143,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SyncScanShmemSize());
 	size = add_size(size, AsyncShmemSize());
 	size = add_size(size, StatsShmemSize());
+	size = add_size(size, WaitEventShmemSize());
 #ifdef EXEC_BACKEND
 	size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -294,6 +296,7 @@ CreateSharedMemoryAndSemaphores(void)
 	SyncScanShmemInit();
 	AsyncShmemInit();
 	StatsShmemInit();
+	WaitEventShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/storage/ipc/shmem.c b/src/backend/storage/ipc/shmem.c
index 5465fa1964..02c72ebbb1 100644
--- a/src/backend/storage/ipc/shmem.c
+++ b/src/backend/storage/ipc/shmem.c
@@ -85,7 +85,8 @@ static void *ShmemBase;			/* start address of shared memory */
 
 static void *ShmemEnd;			/* end+1 address of shared memory */
 
-slock_t    *ShmemLock;			/* spinlock for shared memory and LWLock
+slock_t    *ShmemLock;			/* spinlock for shared memory, LWLock
+								 * allocation, and named extension wait event
 								 * allocation */
 
 static HTAB *ShmemIndex = NULL; /* primary index hashtable for shmem */
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 7940d64639..724a720f2c 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,11 +22,18 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
 #include "storage/lwlock.h"		/* for GetLWLockIdentifier */
+#include "storage/spin.h"
+#include "utils/memutils.h"
 #include "utils/wait_event.h"
 
 
+/* We use the ShmemLock spinlock to protect ExtensionWaitEventCounter */
+extern slock_t *ShmemLock;
+
 static const char *pgstat_get_wait_activity(WaitEventActivity w);
 static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
@@ -38,6 +45,23 @@ static uint32 local_my_wait_event_info;
 uint32	   *my_wait_event_info = &local_my_wait_event_info;
 
 
+/* dynamic allocation counter in shared memory */
+static uint16 *ExtensionWaitEventCounter;
+
+/* first event ID for named extension wait event tranche */
+#define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
+	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
+
+/*
+ * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event ID known to the current
+ * process.  Any unused entries in the array will contain NULL.
+ */
+static const char **ExtensionWaitEventTrancheNames = NULL;
+static int	ExtensionWaitEventTrancheNamesAllocated = 0;
+
+static const char *GetExtensionWaitEventIdentifier(uint16 eventId);
+
 /*
  * Configure wait event reporting to report wait events to *wait_event_info.
  * *wait_event_info needs to be valid until pgstat_reset_wait_event_storage()
@@ -165,7 +189,7 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				break;
 			}
 		case PG_WAIT_EXTENSION:
-			event_name = "Extension";
+			event_name = GetExtensionWaitEventIdentifier(eventId);
 			break;
 		case PG_WAIT_IPC:
 			{
@@ -762,3 +786,115 @@ pgstat_get_wait_io(WaitEventIO w)
 
 	return event_name;
 }
+
+/*
+ *  Return the space for dynamic allocation counter.
+ */
+Size
+WaitEventShmemSize(void)
+{
+	return sizeof(*ExtensionWaitEventCounter);
+}
+
+/*
+ * Allocate shmem space for dynamic allocation counter.
+ */
+void
+WaitEventShmemInit(void)
+{
+	if (!IsUnderPostmaster)
+	{
+		Size		space = WaitEventShmemSize();
+
+		/* Initialize the dynamic-allocation counter */
+		ExtensionWaitEventCounter = (uint16 *) ShmemAlloc(space);
+		*ExtensionWaitEventCounter = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+	}
+}
+
+/*
+ * Allocate a new event ID and return the wait event info.
+ */
+uint32
+ExtensionWaitEventNewTranche(void)
+{
+	uint16			eventId;
+
+	SpinLockAcquire(ShmemLock);
+	eventId = (*ExtensionWaitEventCounter)++;
+	SpinLockRelease(ShmemLock);
+
+	return PG_WAIT_EXTENSION | eventId;
+}
+
+/*
+ * Register a dynamic tranche name in the lookup table of the current process.
+ *
+ * This routine will save a pointer to the wait event tranche name passed
+ * as an argument, so the name should be allocated in a backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+ExtensionWaitEventRegisterTranche(uint32 wait_event_info, const char *wait_event_name)
+{
+	uint32		classId;
+	uint16		eventId;
+
+	classId = wait_event_info & 0xFF000000;
+	eventId = wait_event_info & 0x0000FFFF;
+	Assert(classId == PG_WAIT_EXTENSION);
+	
+	/* This should only be called for user-defined tranches. */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return;
+
+	/* Convert to array index. */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	/* If necessary, create or enlarge array. */
+	if (eventId >= ExtensionWaitEventTrancheNamesAllocated)
+	{
+		int			newalloc;
+
+		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
+
+		if (ExtensionWaitEventTrancheNames == NULL)
+			ExtensionWaitEventTrancheNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		else
+			ExtensionWaitEventTrancheNames =
+				repalloc0_array(ExtensionWaitEventTrancheNames, const char *, ExtensionWaitEventTrancheNamesAllocated, newalloc);
+		ExtensionWaitEventTrancheNamesAllocated = newalloc;
+	}
+
+	ExtensionWaitEventTrancheNames[eventId] = wait_event_name;
+}
+
+/*
+ * Return the name of an Extension wait event ID.
+ */
+static const char *
+GetExtensionWaitEventIdentifier(uint16 eventId)
+{
+	/* Build-in tranche? */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It's an extension tranche, so look in ExtensionWaitEventTrancheNames[].
+	 * However, it's possible that the tranche has never been registered by
+	 * calling ExtensionWaitEventRegisterTranche() in the current process, in
+	 * which case give up and return "Extension".
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= ExtensionWaitEventTrancheNamesAllocated ||
+		ExtensionWaitEventTrancheNames[eventId] == NULL)
+		return "Extension";
+
+	return ExtensionWaitEventTrancheNames[eventId];
+}
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 518d3b0a1f..789fcbf52a 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -70,6 +70,35 @@ typedef enum
 	WAIT_EVENT_WAL_SENDER_WRITE_DATA,
 } WaitEventClient;
 
+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define custom wait events.  First, call
+ * ExtensionWaitEventNewTranche() just once to obtain a new wait event tranche. 
+ * The ID is allocated from a shared counter. Next, each individual process using
+ * the tranche should call ExtensionWaitEventRegisterTranche() to associate
+ * that wait event with a name.
+ *
+ * It may seem strange that each process using the tranche must register it
+ * separately, but dynamic shared memory segments aren't guaranteed to be
+ * mapped at the same address in all coordinating backends, so storing the
+ * registration in the main shared memory segment wouldn't work for that case.
+ */
+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+}			WaitEventExtension;
+
+extern void WaitEventShmemInit(void);
+extern Size WaitEventShmemSize(void);
+
+extern uint32	ExtensionWaitEventNewTranche(void);
+extern void ExtensionWaitEventRegisterTranche(uint32 wait_event_info, const char *tranche_name);
+
 /* ----------
  * Wait Events - IPC
  *
-- 
2.25.1

#15Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Masahiro Ikeda (#14)
2 attachment(s)
Re: Support to define custom wait events for extensions

Hi,

I updated the patches to handle the warning mentioned
by PostgreSQL Patch Tester, and removed unnecessary spaces.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

v4-0002-Add-test-codes-for-custom-wait-events.patchtext/x-diff; name=v4-0002-Add-test-codes-for-custom-wait-events.patchDownload
From 1bb78fa2cbe6b030cea7a570bec88bd4d68f314a Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Fri, 23 Jun 2023 17:38:38 +0900
Subject: [PATCH 2/2] Add test codes for custom wait events

---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 .../test_custom_wait_events/.gitignore        |   2 +
 .../modules/test_custom_wait_events/Makefile  |  23 +++
 .../test_custom_wait_events/meson.build       |  33 ++++
 .../test_custom_wait_events/t/001_basic.pl    |  34 ++++
 .../test_custom_wait_events--1.0.sql          |  14 ++
 .../test_custom_wait_events.c                 | 182 ++++++++++++++++++
 .../test_custom_wait_events.control           |   3 +
 9 files changed, 293 insertions(+)
 create mode 100644 src/test/modules/test_custom_wait_events/.gitignore
 create mode 100644 src/test/modules/test_custom_wait_events/Makefile
 create mode 100644 src/test/modules/test_custom_wait_events/meson.build
 create mode 100644 src/test/modules/test_custom_wait_events/t/001_basic.pl
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.c
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 6331c976dc..84312b7e57 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -17,6 +17,7 @@ SUBDIRS = \
 		  test_bloomfilter \
 		  test_copy_callbacks \
 		  test_custom_rmgrs \
+		  test_custom_wait_events \
 		  test_ddl_deparse \
 		  test_extensions \
 		  test_ginpostinglist \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 17d369e378..83a1d8fd19 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -14,6 +14,7 @@ subdir('ssl_passphrase_callback')
 subdir('test_bloomfilter')
 subdir('test_copy_callbacks')
 subdir('test_custom_rmgrs')
+subdir('test_custom_wait_events')
 subdir('test_ddl_deparse')
 subdir('test_extensions')
 subdir('test_ginpostinglist')
diff --git a/src/test/modules/test_custom_wait_events/.gitignore b/src/test/modules/test_custom_wait_events/.gitignore
new file mode 100644
index 0000000000..716e17f5a2
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/.gitignore
@@ -0,0 +1,2 @@
+# Generated subdirectories
+/tmp_check/
diff --git a/src/test/modules/test_custom_wait_events/Makefile b/src/test/modules/test_custom_wait_events/Makefile
new file mode 100644
index 0000000000..dda365d523
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_custom_wait_events/Makefile
+
+MODULE_big = test_custom_wait_events
+OBJS = \
+	$(WIN32RES) \
+	test_custom_wait_events.o
+PGFILEDESC = "test_custom_wait_events - test custom wait events"
+
+EXTENSION = test_custom_wait_events
+DATA = test_custom_wait_events--1.0.sql
+
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_custom_wait_events
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_custom_wait_events/meson.build b/src/test/modules/test_custom_wait_events/meson.build
new file mode 100644
index 0000000000..8ad073e577
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) PostgreSQL Global Development Group
+
+test_custom_wait_events = files(
+  'test_custom_wait_events.c',
+)
+
+if host_system == 'windows'
+  test_custom_wait_events += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_custom_wait_events',
+    '--FILEDESC', 'test_custom_wait_events - test custom wait events',])
+endif
+
+test_custom_wait_events = shared_module('test_custom_wait_events',
+  test_custom_wait_events,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_custom_wait_events
+
+test_install_data += files(
+  'test_custom_wait_events.control',
+  'test_custom_wait_events--1.0.sql',
+)
+
+tests += {
+  'name': 'test_custom_wait_events',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_basic.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_custom_wait_events/t/001_basic.pl b/src/test/modules/test_custom_wait_events/t/001_basic.pl
new file mode 100644
index 0000000000..b43c82a713
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/t/001_basic.pl
@@ -0,0 +1,34 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+
+$node->init;
+$node->append_conf(
+	'postgresql.conf',
+	"shared_preload_libraries = 'test_custom_wait_events'"
+);
+$node->start;
+
+# setup
+$node->safe_psql('postgres', 'CREATE EXTENSION test_custom_wait_events;');
+
+$node->safe_psql('postgres', 'SELECT wait_worker_launch();');
+
+ok($node->poll_query_until('postgres',
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND wait_event = 'custom_wait_event'
+		) > 0;])
+) or die "Timed out waiting the custom wait event to be showed up";
+
+$node->stop;
+done_testing();
\ No newline at end of file
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
new file mode 100644
index 0000000000..1fc2130f84
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
@@ -0,0 +1,14 @@
+/* src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_custom_wait_events" to load this file. \quit
+
+--
+-- wait_worker_launch()
+--
+-- Launch a background worker to wait forever with a custom wait event.
+--
+CREATE FUNCTION wait_worker_launch()
+RETURNS int
+AS 'MODULE_PATHNAME', 'wait_worker_launch'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.c b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
new file mode 100644
index 0000000000..6a47665524
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
@@ -0,0 +1,182 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_custom_wait_events.c
+ *		Code for testing custom wait events
+ *
+ * This code initializes a custom wait event in shmem_request_hook() and
+ * provide a function to launch a background worker waiting forever
+ * with the custom wait event.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_custom_wait_events/test_custom_wait_events.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lwlock.h"
+#include "storage/proc.h"
+#include "storage/shmem.h"
+#include "tcop/utility.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(wait_worker_launch);
+
+PGDLLEXPORT void worker_main(Datum main_arg);
+
+/* GUC variables */
+typedef struct state
+{
+	uint32			wait_event_info;  /* a allocated wait event */
+}			state;
+
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_request_hook_type prev_shmem_startup_hook = NULL;
+
+static char * worker_database = "postgres";   /* connected by a background worker */
+static state * ss = NULL;
+
+static void shmem_request(void);
+static void shmem_startup(void);
+static Size memsize(void);
+
+void
+_PG_init(void)
+{
+	if (!process_shared_preload_libraries_in_progress)
+		return;
+
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = shmem_startup;
+}
+
+static void
+shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(memsize());
+}
+
+static Size
+memsize(void)
+{
+	return sizeof(state);
+}
+
+/*
+ * Allocate and register a new wait event.
+ *
+ * This routine allocate a wait event id when it's called first time.
+ * After that, the name of the wait event is associated with the id. It's
+ * registered in the lookup table of each processes dynamically.
+ */
+static void
+shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	ss = NULL;
+
+	/* Create or attach to the shared memory state */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+	ss = ShmemInitStruct("custom_wait_event",
+						 sizeof(state),
+						 &found);
+
+	/* Allocate a new wait event. */
+	if (!found)
+		ss->wait_event_info = ExtensionWaitEventNewTranche();
+
+	LWLockRelease(AddinShmemInitLock);
+
+	/*
+	 * Register the wait event in the lookup table of the current process.
+	 */
+	ExtensionWaitEventRegisterTranche(ss->wait_event_info, "custom_wait_event");
+
+	return;
+}
+
+/*
+ * Connect to the database and wait forever with the custom wait event
+ */
+void
+worker_main(Datum main_arg)
+{
+	pqsignal(SIGTERM, SignalHandlerForShutdownRequest);
+	BackgroundWorkerUnblockSignals();
+
+	/* Connect to our database to show up a wait event in pg_stat_activity */
+	BackgroundWorkerInitializeConnection(worker_database, NULL, 0);
+
+	/* Wait forever with the custom wait event */
+	while (!ShutdownRequestPending)
+	{
+		(void) WaitLatch(MyLatch,
+						 WL_LATCH_SET | WL_EXIT_ON_PM_DEATH,
+						 -1L,
+						 ss->wait_event_info);  /* the custom wait event */
+		ResetLatch(MyLatch);
+	}
+}
+
+/*
+ * Dynamically launch a background worker waiting forever
+ */
+Datum
+wait_worker_launch(PG_FUNCTION_ARGS)
+{
+	BackgroundWorker worker;
+	BackgroundWorkerHandle *handle;
+	BgwHandleStatus status;
+	pid_t		pid;
+
+	memset(&worker, 0, sizeof(worker));
+	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	worker.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	worker.bgw_restart_time = BGW_NEVER_RESTART;
+	sprintf(worker.bgw_function_name, "worker_main");
+	sprintf(worker.bgw_library_name, "test_custom_wait_events");
+	snprintf(worker.bgw_name, BGW_MAXLEN, "test_custom_wait_events worker");
+	snprintf(worker.bgw_type, BGW_MAXLEN, "test_custom_wait_events");
+	worker.bgw_notify_pid = MyProcPid;
+
+	if (!RegisterDynamicBackgroundWorker(&worker, &handle))
+		PG_RETURN_NULL();
+
+	status = WaitForBackgroundWorkerStartup(handle, &pid);
+
+	if (status == BGWH_STOPPED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("could not start background process"),
+				 errhint("More details may be available in the server log.")));
+	if (status == BGWH_POSTMASTER_DIED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("cannot start background processes without postmaster"),
+				 errhint("Kill all remaining database processes and restart the database.")));
+	Assert(status == BGWH_STARTED);
+
+	PG_RETURN_INT32(pid);
+}
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.control b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
new file mode 100644
index 0000000000..8dd875a252
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
@@ -0,0 +1,3 @@
+comment = 'Test code for custom wait events'
+default_version = '1.0'
+module_pathname = '$libdir/test_custom_wait_events'
-- 
2.25.1

v4-0001-Support-custom-wait-events-for-extensions.patchtext/x-diff; name=v4-0001-Support-custom-wait-events-for-extensions.patchDownload
From 8cf1f026fc04633412f03ace94f24dd4692d7310 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Fri, 16 Jun 2023 11:53:29 +0900
Subject: [PATCH 1/2] Support custom wait events for extensions.

To support custom wait events, it add 2 APIs to allocate
new wait events dynamically.

The APIs are
* ExtensionWaitEventNewTranche()
* ExtensionWaitEventRegisterTranche()

They similar to existing LWLockNewTrancheId() and
LWLockRegisterTranche().

First, extension calls ExtensionWaitEventNewTranche()
just once to obtain a new wait event tranche; The ID is
allocated from a shared counter. Next, each individual
process using the tranche should call
ExtensionWaitEventRegisterTranche() to associate that
wait event with a name.
---
 src/backend/storage/ipc/ipci.c          |   3 +
 src/backend/storage/ipc/shmem.c         |   3 +-
 src/backend/utils/activity/wait_event.c | 138 +++++++++++++++++++++++-
 src/include/utils/wait_event.h          |  29 +++++
 4 files changed, 171 insertions(+), 2 deletions(-)

diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 8f1ded7338..ed05121fa3 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -49,6 +49,7 @@
 #include "storage/spin.h"
 #include "utils/guc.h"
 #include "utils/snapmgr.h"
+#include "utils/wait_event.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -142,6 +143,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SyncScanShmemSize());
 	size = add_size(size, AsyncShmemSize());
 	size = add_size(size, StatsShmemSize());
+	size = add_size(size, WaitEventShmemSize());
 #ifdef EXEC_BACKEND
 	size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -294,6 +296,7 @@ CreateSharedMemoryAndSemaphores(void)
 	SyncScanShmemInit();
 	AsyncShmemInit();
 	StatsShmemInit();
+	WaitEventShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/storage/ipc/shmem.c b/src/backend/storage/ipc/shmem.c
index 5465fa1964..02c72ebbb1 100644
--- a/src/backend/storage/ipc/shmem.c
+++ b/src/backend/storage/ipc/shmem.c
@@ -85,7 +85,8 @@ static void *ShmemBase;			/* start address of shared memory */
 
 static void *ShmemEnd;			/* end+1 address of shared memory */
 
-slock_t    *ShmemLock;			/* spinlock for shared memory and LWLock
+slock_t    *ShmemLock;			/* spinlock for shared memory, LWLock
+								 * allocation, and named extension wait event
 								 * allocation */
 
 static HTAB *ShmemIndex = NULL; /* primary index hashtable for shmem */
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 7940d64639..078bb221b8 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,11 +22,18 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
 #include "storage/lwlock.h"		/* for GetLWLockIdentifier */
+#include "storage/spin.h"
+#include "utils/memutils.h"
 #include "utils/wait_event.h"
 
 
+/* We use the ShmemLock spinlock to protect ExtensionWaitEventCounter */
+extern slock_t *ShmemLock;
+
 static const char *pgstat_get_wait_activity(WaitEventActivity w);
 static const char *pgstat_get_wait_client(WaitEventClient w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
@@ -38,6 +45,23 @@ static uint32 local_my_wait_event_info;
 uint32	   *my_wait_event_info = &local_my_wait_event_info;
 
 
+/* dynamic allocation counter in shared memory */
+static uint16 *ExtensionWaitEventCounter;
+
+/* first event ID for named extension wait event tranche */
+#define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
+	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
+
+/*
+ * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event ID known to the current
+ * process.  Any unused entries in the array will contain NULL.
+ */
+static const char **ExtensionWaitEventTrancheNames = NULL;
+static int	ExtensionWaitEventTrancheNamesAllocated = 0;
+
+static const char *GetExtensionWaitEventIdentifier(uint16 eventId);
+
 /*
  * Configure wait event reporting to report wait events to *wait_event_info.
  * *wait_event_info needs to be valid until pgstat_reset_wait_event_storage()
@@ -165,7 +189,7 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				break;
 			}
 		case PG_WAIT_EXTENSION:
-			event_name = "Extension";
+			event_name = GetExtensionWaitEventIdentifier(eventId);
 			break;
 		case PG_WAIT_IPC:
 			{
@@ -762,3 +786,115 @@ pgstat_get_wait_io(WaitEventIO w)
 
 	return event_name;
 }
+
+/*
+ *  Return the space for dynamic allocation counter.
+ */
+Size
+WaitEventShmemSize(void)
+{
+	return sizeof(*ExtensionWaitEventCounter);
+}
+
+/*
+ * Allocate shmem space for dynamic allocation counter.
+ */
+void
+WaitEventShmemInit(void)
+{
+	if (!IsUnderPostmaster)
+	{
+		Size		space = WaitEventShmemSize();
+
+		/* Initialize the dynamic-allocation counter */
+		ExtensionWaitEventCounter = (uint16 *) ShmemAlloc(space);
+		*ExtensionWaitEventCounter = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+	}
+}
+
+/*
+ * Allocate a new event ID and return the wait event info.
+ */
+uint32
+ExtensionWaitEventNewTranche(void)
+{
+	uint16			eventId;
+
+	SpinLockAcquire(ShmemLock);
+	eventId = (*ExtensionWaitEventCounter)++;
+	SpinLockRelease(ShmemLock);
+
+	return PG_WAIT_EXTENSION | eventId;
+}
+
+/*
+ * Register a dynamic tranche name in the lookup table of the current process.
+ *
+ * This routine will save a pointer to the wait event tranche name passed
+ * as an argument, so the name should be allocated in a backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+ExtensionWaitEventRegisterTranche(uint32 wait_event_info, const char *wait_event_name)
+{
+	uint16		eventId;
+
+	/* Check wait event class. */
+	Assert((wait_event_info & 0xFF000000) == PG_WAIT_EXTENSION);
+
+	eventId = wait_event_info & 0x0000FFFF;
+
+	/* This should only be called for user-defined tranches. */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return;
+
+	/* Convert to array index. */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	/* If necessary, create or enlarge array. */
+	if (eventId >= ExtensionWaitEventTrancheNamesAllocated)
+	{
+		int			newalloc;
+
+		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
+
+		if (ExtensionWaitEventTrancheNames == NULL)
+			ExtensionWaitEventTrancheNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		else
+			ExtensionWaitEventTrancheNames =
+				repalloc0_array(ExtensionWaitEventTrancheNames, const char *, ExtensionWaitEventTrancheNamesAllocated, newalloc);
+		ExtensionWaitEventTrancheNamesAllocated = newalloc;
+	}
+
+	ExtensionWaitEventTrancheNames[eventId] = wait_event_name;
+}
+
+/*
+ * Return the name of an Extension wait event ID.
+ */
+static const char *
+GetExtensionWaitEventIdentifier(uint16 eventId)
+{
+	/* Build-in tranche? */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It's an extension tranche, so look in ExtensionWaitEventTrancheNames[].
+	 * However, it's possible that the tranche has never been registered by
+	 * calling ExtensionWaitEventRegisterTranche() in the current process, in
+	 * which case give up and return "Extension".
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= ExtensionWaitEventTrancheNamesAllocated ||
+		ExtensionWaitEventTrancheNames[eventId] == NULL)
+		return "Extension";
+
+	return ExtensionWaitEventTrancheNames[eventId];
+}
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 518d3b0a1f..9507c4fd71 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -70,6 +70,35 @@ typedef enum
 	WAIT_EVENT_WAL_SENDER_WRITE_DATA,
 } WaitEventClient;
 
+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define custom wait events.  First, call
+ * ExtensionWaitEventNewTranche() just once to obtain a new wait event tranche.
+ * The ID is allocated from a shared counter. Next, each individual process using
+ * the tranche should call ExtensionWaitEventRegisterTranche() to associate
+ * that wait event with a name.
+ *
+ * It may seem strange that each process using the tranche must register it
+ * separately, but dynamic shared memory segments aren't guaranteed to be
+ * mapped at the same address in all coordinating backends, so storing the
+ * registration in the main shared memory segment wouldn't work for that case.
+ */
+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+}			WaitEventExtension;
+
+extern void WaitEventShmemInit(void);
+extern Size WaitEventShmemSize(void);
+
+extern uint32	ExtensionWaitEventNewTranche(void);
+extern void ExtensionWaitEventRegisterTranche(uint32 wait_event_info, const char *tranche_name);
+
 /* ----------
  * Wait Events - IPC
  *
-- 
2.25.1

#16Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#15)
1 attachment(s)
Re: Support to define custom wait events for extensions

On Fri, Jun 23, 2023 at 05:56:26PM +0900, Masahiro Ikeda wrote:

I updated the patches to handle the warning mentioned
by PostgreSQL Patch Tester, and removed unnecessary spaces.

I have begun hacking on that, and the API layer inspired from the
LWLocks is sound. I have been playing with it in my own extensions
and it is nice to be able to plug in custom wait events into
pg_stat_activity, particularly for bgworkers. Finally.

The patch needed a rebase after the recent commit that introduced the
automatic generation of docs and code for wait events. It requires
two tweaks in generate-wait_event_types.pl, feel free to double-check
them.

Some of the new structures and routine names don't quite reflect the
fact that we have wait events for extensions, so I have taken a stab
at that.

Note that the test module test_custom_wait_events would crash if
attempting to launch a worker when not loaded in
shared_preload_libraries, so we'd better have some protection in
wait_worker_launch() (this function should be renamed as well).

Attached is a rebased patch that I have begun tweaking here and
there. For now, the patch is moved as waiting on author. I have
merged the test module with the main patch for the moment, for
simplicity. A split is straight-forward as the code paths touched are
different.

Another and *very* important thing is that we are going to require
some documentation in xfunc.sgml to explain how to use these routines
and what to expect from them. Ikeda-san, could you write some? You
could look at the part about shmem and LWLock to get some
inspiration.
--
Michael

Attachments:

v5-0001-Support-custom-wait-events-for-extensions.patchtext/x-diff; charset=us-asciiDownload
From 630f78822534171bb8839c79d0fdca3bb8268a95 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 11 Jul 2023 16:41:35 +0900
Subject: [PATCH v5] Support custom wait events for extensions.

To support custom wait events, it add 2 APIs to allocate new wait events
dynamically.

The APIs are
* ExtensionWaitEventNewTranche()
* ExtensionWaitEventRegisterTranche()

These are similar to the existing LWLockNewTrancheId() and
LWLockRegisterTranche().

First, extension should call ExtensionWaitEventNewTranche() to get one
or more wait event "tranches", which are ID is allocated from a shared
counter.  Next, each individual process can use the tranche numbers with
ExtensionWaitEventRegisterTranche() to associate that a wait event
string to the associated name.

Note that this includes a test module, which perhaps should be split
later into its own commit.
---
 src/include/utils/wait_event.h                |  31 +++
 src/backend/storage/ipc/ipci.c                |   3 +
 src/backend/storage/ipc/shmem.c               |   3 +-
 .../activity/generate-wait_event_types.pl     |  12 +-
 src/backend/utils/activity/wait_event.c       | 149 +++++++++++++-
 .../utils/activity/wait_event_names.txt       |   2 +-
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 .../test_custom_wait_events/.gitignore        |   2 +
 .../modules/test_custom_wait_events/Makefile  |  23 +++
 .../test_custom_wait_events/meson.build       |  33 +++
 .../test_custom_wait_events/t/001_basic.pl    |  34 ++++
 .../test_custom_wait_events--1.0.sql          |  14 ++
 .../test_custom_wait_events.c                 | 188 ++++++++++++++++++
 .../test_custom_wait_events.control           |   3 +
 15 files changed, 484 insertions(+), 15 deletions(-)
 create mode 100644 src/test/modules/test_custom_wait_events/.gitignore
 create mode 100644 src/test/modules/test_custom_wait_events/Makefile
 create mode 100644 src/test/modules/test_custom_wait_events/meson.build
 create mode 100644 src/test/modules/test_custom_wait_events/t/001_basic.pl
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.c
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.control

diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 4517425f84..dfcaf8c506 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -67,6 +67,37 @@ pgstat_report_wait_start(uint32 wait_event_info)
 	*(volatile uint32 *) my_wait_event_info = wait_event_info;
 }
 
+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define custom wait events.  First, call
+ * WaitEventExtensionNewTranche() just once to obtain a new wait event
+ * tranche.  The ID is allocated from a shared counter.  Next, each
+ * individual process using the tranche should call
+ * WaitEventExtensionRegisterTranche() to associate that wait event with
+ * a name.
+ *
+ * It may seem strange that each process using the tranche must register it
+ * separately, but dynamic shared memory segments aren't guaranteed to be
+ * mapped at the same address in all coordinating backends, so storing the
+ * registration in the main shared memory segment wouldn't work for that case.
+ */
+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+} WaitEventExtension;
+
+extern void WaitEventExtensionShmemInit(void);
+extern Size WaitEventExtensionShmemSize(void);
+
+extern uint32 WaitEventExtensionNewTranche(void);
+extern void WaitEventExtensionRegisterTranche(uint32 wait_event_info,
+											  const char *tranche_name);
+
 /* ----------
  * pgstat_report_wait_end() -
  *
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index cc387c00a1..5551afffc0 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -49,6 +49,7 @@
 #include "storage/spin.h"
 #include "utils/guc.h"
 #include "utils/snapmgr.h"
+#include "utils/wait_event.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -142,6 +143,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SyncScanShmemSize());
 	size = add_size(size, AsyncShmemSize());
 	size = add_size(size, StatsShmemSize());
+	size = add_size(size, WaitEventExtensionShmemSize());
 #ifdef EXEC_BACKEND
 	size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -301,6 +303,7 @@ CreateSharedMemoryAndSemaphores(void)
 	SyncScanShmemInit();
 	AsyncShmemInit();
 	StatsShmemInit();
+	WaitEventExtensionShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/storage/ipc/shmem.c b/src/backend/storage/ipc/shmem.c
index 5465fa1964..02c72ebbb1 100644
--- a/src/backend/storage/ipc/shmem.c
+++ b/src/backend/storage/ipc/shmem.c
@@ -85,7 +85,8 @@ static void *ShmemBase;			/* start address of shared memory */
 
 static void *ShmemEnd;			/* end+1 address of shared memory */
 
-slock_t    *ShmemLock;			/* spinlock for shared memory and LWLock
+slock_t    *ShmemLock;			/* spinlock for shared memory, LWLock
+								 * allocation, and named extension wait event
 								 * allocation */
 
 static HTAB *ShmemIndex = NULL; /* primary index hashtable for shmem */
diff --git a/src/backend/utils/activity/generate-wait_event_types.pl b/src/backend/utils/activity/generate-wait_event_types.pl
index 6d1a2af42a..2cad28fbfe 100644
--- a/src/backend/utils/activity/generate-wait_event_types.pl
+++ b/src/backend/utils/activity/generate-wait_event_types.pl
@@ -85,10 +85,11 @@ foreach my $line (@lines_sorted)
 	my $trimmedwaiteventname = $waiteventenumname;
 	$trimmedwaiteventname =~ s/^WAIT_EVENT_//;
 
-	# An exception is required for LWLock and Lock as these don't require
-	# any C and header files generated.
+	# An exception is required for Extension, LWLock and Lock as these don't
+	# require any C and header files generated.
 	die "wait event names must start with 'WAIT_EVENT_'"
 	  if ( $trimmedwaiteventname eq $waiteventenumname
+		&& $waiteventenumname !~ /^Extension/
 		&& $waiteventenumname !~ /^LWLock/
 		&& $waiteventenumname !~ /^Lock/);
 	$continue = ",\n";
@@ -138,11 +139,12 @@ if ($gen_code)
 	foreach my $waitclass (sort { uc($a) cmp uc($b) } keys %hashwe)
 	{
 
-		# Don't generate .c and .h files for LWLock and Lock, these are
-		# handled independently.
+		# Don't generate .c and .h files for Extension, LWLock and
+		# Lock, these are handled independently.
 		next
 		  if ( $waitclass =~ /^WaitEventLWLock$/
-			|| $waitclass =~ /^WaitEventLock$/);
+			|| $waitclass =~ /^WaitEventLock$/
+			|| $waitclass =~ /^WaitEventExtension$/);
 
 		my $last = $waitclass;
 		$last =~ s/^WaitEvent//;
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 4aad11c111..9d4652fcc9 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,15 +22,21 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
 #include "storage/lwlock.h"		/* for GetLWLockIdentifier */
+#include "storage/spin.h"
+#include "utils/memutils.h"
 #include "utils/wait_event.h"
 
 
+/* We use the ShmemLock spinlock to protect WaitEventExtensionCounter */
+extern slock_t *ShmemLock;
+
 static const char *pgstat_get_wait_activity(WaitEventActivity w);
 static const char *pgstat_get_wait_bufferpin(WaitEventBufferPin w);
 static const char *pgstat_get_wait_client(WaitEventClient w);
-static const char *pgstat_get_wait_extension(WaitEventExtension w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
@@ -40,6 +46,137 @@ static uint32 local_my_wait_event_info;
 uint32	   *my_wait_event_info = &local_my_wait_event_info;
 
 
+/* dynamic allocation counter in shared memory */
+static uint16 *WaitEventExtensionCounter;
+
+/* first event ID for named extension wait event tranche */
+#define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
+	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
+
+/*
+ * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event ID known to the current
+ * process.  Any unused entries in the array will contain NULL.
+ */
+static const char **WaitEventExtensionTrancheNames = NULL;
+static int	WaitEventExtensionTrancheNamesAllocated = 0;
+
+static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
+
+/*
+ *  Return the space for dynamic allocation counter.
+ */
+Size
+WaitEventExtensionShmemSize(void)
+{
+	return sizeof(*WaitEventExtensionCounter);
+}
+
+/*
+ * Allocate shmem space for dynamic allocation counter.
+ */
+void
+WaitEventExtensionShmemInit(void)
+{
+	if (!IsUnderPostmaster)
+	{
+		Size		space = WaitEventExtensionShmemSize();
+
+		/* Initialize the dynamic-allocation counter */
+		WaitEventExtensionCounter = (uint16 *) ShmemAlloc(space);
+		*WaitEventExtensionCounter = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+	}
+}
+
+/*
+ * Allocate a new event ID and return the wait event info.
+ */
+uint32
+WaitEventExtensionNewTranche(void)
+{
+	uint16		eventId;
+
+	SpinLockAcquire(ShmemLock);
+	eventId = (*WaitEventExtensionCounter)++;
+	SpinLockRelease(ShmemLock);
+
+	return PG_WAIT_EXTENSION | eventId;
+}
+
+/*
+ * Register a dynamic tranche name in the lookup table of the current process.
+ *
+ * This routine will save a pointer to the wait event tranche name passed
+ * as an argument, so the name should be allocated in a backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+WaitEventExtensionRegisterTranche(uint32 wait_event_info,
+								  const char *wait_event_name)
+{
+	uint16		eventId;
+
+	/* Check wait event class. */
+	Assert((wait_event_info & 0xFF000000) == PG_WAIT_EXTENSION);
+
+	eventId = wait_event_info & 0x0000FFFF;
+
+	/* This should only be called for user-defined tranches. */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return;
+
+	/* Convert to array index. */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	/* If necessary, create or enlarge array. */
+	if (eventId >= WaitEventExtensionTrancheNamesAllocated)
+	{
+		int			newalloc;
+
+		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
+
+		if (WaitEventExtensionTrancheNames == NULL)
+			WaitEventExtensionTrancheNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		else
+			WaitEventExtensionTrancheNames =
+				repalloc0_array(WaitEventExtensionTrancheNames, const char *, WaitEventExtensionTrancheNamesAllocated, newalloc);
+		WaitEventExtensionTrancheNamesAllocated = newalloc;
+	}
+
+	WaitEventExtensionTrancheNames[eventId] = wait_event_name;
+}
+
+/*
+ * Return the name of an Extension wait event ID.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)
+{
+	/* Build-in tranche? */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It's an extension tranche, so look in WaitEventExtensionTrancheNames[].
+	 * However, it's possible that the tranche has never been registered by
+	 * calling WaitEventExtensionRegisterTranche() in the current process, in
+	 * which case give up and return "Extension".
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= WaitEventExtensionTrancheNamesAllocated ||
+		WaitEventExtensionTrancheNames[eventId] == NULL)
+		return "Extension";
+
+	return WaitEventExtensionTrancheNames[eventId];
+}
+
+
 /*
  * Configure wait event reporting to report wait events to *wait_event_info.
  * *wait_event_info needs to be valid until pgstat_reset_wait_event_storage()
@@ -149,6 +286,9 @@ pgstat_get_wait_event(uint32 wait_event_info)
 		case PG_WAIT_LOCK:
 			event_name = GetLockNameFromTagType(eventId);
 			break;
+		case PG_WAIT_EXTENSION:
+			event_name = GetWaitEventExtensionIdentifier(eventId);
+			break;
 		case PG_WAIT_BUFFERPIN:
 			{
 				WaitEventBufferPin w = (WaitEventBufferPin) wait_event_info;
@@ -170,13 +310,6 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_client(w);
 				break;
 			}
-		case PG_WAIT_EXTENSION:
-			{
-				WaitEventExtension w = (WaitEventExtension) wait_event_info;
-
-				event_name = pgstat_get_wait_extension(w);
-				break;
-			}
 		case PG_WAIT_IPC:
 			{
 				WaitEventIPC w = (WaitEventIPC) wait_event_info;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 7af1a61f95..d3cddde95f 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -261,7 +261,7 @@ WAIT_EVENT_BUFFER_PIN	"BufferPin"	"Waiting to acquire an exclusive pin on a buff
 
 Section: ClassName - WaitEventExtension
 
-WAIT_EVENT_EXTENSION	"Extension"	"Waiting in an extension."
+WAIT_EVENT_DOCONLY	"Extension"	"Waiting in an extension."
 
 #
 # Wait events - LWLock
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 6331c976dc..84312b7e57 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -17,6 +17,7 @@ SUBDIRS = \
 		  test_bloomfilter \
 		  test_copy_callbacks \
 		  test_custom_rmgrs \
+		  test_custom_wait_events \
 		  test_ddl_deparse \
 		  test_extensions \
 		  test_ginpostinglist \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 17d369e378..83a1d8fd19 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -14,6 +14,7 @@ subdir('ssl_passphrase_callback')
 subdir('test_bloomfilter')
 subdir('test_copy_callbacks')
 subdir('test_custom_rmgrs')
+subdir('test_custom_wait_events')
 subdir('test_ddl_deparse')
 subdir('test_extensions')
 subdir('test_ginpostinglist')
diff --git a/src/test/modules/test_custom_wait_events/.gitignore b/src/test/modules/test_custom_wait_events/.gitignore
new file mode 100644
index 0000000000..716e17f5a2
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/.gitignore
@@ -0,0 +1,2 @@
+# Generated subdirectories
+/tmp_check/
diff --git a/src/test/modules/test_custom_wait_events/Makefile b/src/test/modules/test_custom_wait_events/Makefile
new file mode 100644
index 0000000000..dda365d523
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_custom_wait_events/Makefile
+
+MODULE_big = test_custom_wait_events
+OBJS = \
+	$(WIN32RES) \
+	test_custom_wait_events.o
+PGFILEDESC = "test_custom_wait_events - test custom wait events"
+
+EXTENSION = test_custom_wait_events
+DATA = test_custom_wait_events--1.0.sql
+
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_custom_wait_events
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_custom_wait_events/meson.build b/src/test/modules/test_custom_wait_events/meson.build
new file mode 100644
index 0000000000..8ad073e577
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) PostgreSQL Global Development Group
+
+test_custom_wait_events = files(
+  'test_custom_wait_events.c',
+)
+
+if host_system == 'windows'
+  test_custom_wait_events += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_custom_wait_events',
+    '--FILEDESC', 'test_custom_wait_events - test custom wait events',])
+endif
+
+test_custom_wait_events = shared_module('test_custom_wait_events',
+  test_custom_wait_events,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_custom_wait_events
+
+test_install_data += files(
+  'test_custom_wait_events.control',
+  'test_custom_wait_events--1.0.sql',
+)
+
+tests += {
+  'name': 'test_custom_wait_events',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_basic.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_custom_wait_events/t/001_basic.pl b/src/test/modules/test_custom_wait_events/t/001_basic.pl
new file mode 100644
index 0000000000..339a9f66ba
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/t/001_basic.pl
@@ -0,0 +1,34 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+
+$node->init;
+$node->append_conf(
+	'postgresql.conf',
+	"shared_preload_libraries = 'test_custom_wait_events'"
+);
+$node->start;
+
+# setup
+$node->safe_psql('postgres', 'CREATE EXTENSION test_custom_wait_events;');
+
+$node->safe_psql('postgres', 'SELECT wait_worker_launch();');
+
+ok($node->poll_query_until('postgres',
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND wait_event = 'custom_wait_event'
+		) > 0;])
+) or die "Timed out waiting the custom wait event to be showed up";
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
new file mode 100644
index 0000000000..1fc2130f84
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
@@ -0,0 +1,14 @@
+/* src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_custom_wait_events" to load this file. \quit
+
+--
+-- wait_worker_launch()
+--
+-- Launch a background worker to wait forever with a custom wait event.
+--
+CREATE FUNCTION wait_worker_launch()
+RETURNS int
+AS 'MODULE_PATHNAME', 'wait_worker_launch'
+LANGUAGE C STRICT PARALLEL SAFE;
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.c b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
new file mode 100644
index 0000000000..f6d3739411
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
@@ -0,0 +1,188 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_custom_wait_events.c
+ *		Code for testing custom wait events
+ *
+ * This code initializes a custom wait event in shmem_request_hook() and
+ * provide a function to launch a background worker waiting forever
+ * with the custom wait event.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_custom_wait_events/test_custom_wait_events.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lwlock.h"
+#include "storage/proc.h"
+#include "storage/shmem.h"
+#include "tcop/utility.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(wait_worker_launch);
+
+PGDLLEXPORT void custom_we_worker_main(Datum main_arg);
+
+/* GUC variables */
+typedef struct CustomWaitEventState
+{
+	uint32			wait_event_info;  /* a allocated wait event */
+}			CustomWaitEventState;
+
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_request_hook_type prev_shmem_startup_hook = NULL;
+
+static char *worker_database = "postgres";   /* connected by a background worker */
+static CustomWaitEventState *ss = NULL;
+
+static void custom_we_shmem_request(void);
+static void custom_we_shmem_startup(void);
+static Size custom_we_memsize(void);
+
+void
+_PG_init(void)
+{
+	if (!process_shared_preload_libraries_in_progress)
+		return;
+
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = custom_we_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = custom_we_shmem_startup;
+}
+
+static void
+custom_we_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(custom_we_memsize());
+}
+
+static Size
+custom_we_memsize(void)
+{
+	return sizeof(CustomWaitEventState);
+}
+
+/*
+ * Allocate and register a new wait event.
+ *
+ * This routine allocate a wait event id when it's called first time.
+ * After that, the name of the wait event is associated with the id. It's
+ * registered in the lookup table of each processes dynamically.
+ */
+static void
+custom_we_shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	ss = NULL;
+
+	/* Create or attach to the shared memory state */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+	ss = ShmemInitStruct("custom_wait_event",
+						 sizeof(CustomWaitEventState),
+						 &found);
+
+	/* Allocate a new wait event. */
+	if (!found)
+		ss->wait_event_info = WaitEventExtensionNewTranche();
+
+	LWLockRelease(AddinShmemInitLock);
+
+	/*
+	 * Register the wait event in the lookup table of the current process.
+	 */
+	WaitEventExtensionRegisterTranche(ss->wait_event_info,
+									  "custom_wait_event");
+
+	return;
+}
+
+/*
+ * Connect to the database and wait forever with the custom wait event
+ */
+void
+custom_we_worker_main(Datum main_arg)
+{
+	pqsignal(SIGTERM, SignalHandlerForShutdownRequest);
+	BackgroundWorkerUnblockSignals();
+
+	/* Connect to our database to show up a wait event in pg_stat_activity */
+	BackgroundWorkerInitializeConnection(worker_database, NULL, 0);
+
+	/* Wait forever with the custom wait event */
+	while (!ShutdownRequestPending)
+	{
+		(void) WaitLatch(MyLatch,
+						 WL_LATCH_SET | WL_EXIT_ON_PM_DEATH,
+						 -1L,
+						 ss->wait_event_info);  /* the custom wait event */
+		ResetLatch(MyLatch);
+	}
+}
+
+/*
+ * Dynamically launch a background worker waiting forever
+ */
+Datum
+wait_worker_launch(PG_FUNCTION_ARGS)
+{
+	BackgroundWorker worker;
+	BackgroundWorkerHandle *handle;
+	BgwHandleStatus status;
+	pid_t		pid;
+
+	if (ss == NULL)
+		ereport(ERROR,
+				(errmsg("cannot use \"%s\" if \"%s\" has not been loaded with shared_preload_libraries",
+						"wait_worker_launch", "test_custom_wait_events")));
+
+	memset(&worker, 0, sizeof(worker));
+	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	worker.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	worker.bgw_restart_time = BGW_NEVER_RESTART;
+	sprintf(worker.bgw_function_name, "custom_we_worker_main");
+	sprintf(worker.bgw_library_name, "test_custom_wait_events");
+	snprintf(worker.bgw_name, BGW_MAXLEN, "test_custom_wait_events worker");
+	snprintf(worker.bgw_type, BGW_MAXLEN, "test_custom_wait_events");
+	worker.bgw_notify_pid = MyProcPid;
+
+	if (!RegisterDynamicBackgroundWorker(&worker, &handle))
+		PG_RETURN_NULL();
+
+	status = WaitForBackgroundWorkerStartup(handle, &pid);
+
+	if (status == BGWH_STOPPED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("could not start background process"),
+				 errhint("More details may be available in the server log.")));
+	if (status == BGWH_POSTMASTER_DIED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("cannot start background processes without postmaster"),
+				 errhint("Kill all remaining database processes and restart the database.")));
+	Assert(status == BGWH_STARTED);
+
+	PG_RETURN_INT32(pid);
+}
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.control b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
new file mode 100644
index 0000000000..8dd875a252
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
@@ -0,0 +1,3 @@
+comment = 'Test code for custom wait events'
+default_version = '1.0'
+module_pathname = '$libdir/test_custom_wait_events'
-- 
2.40.1

#17Tristan Partin
tristan@neon.tech
In reply to: Masahiro Ikeda (#14)
Re: Support to define custom wait events for extensions

From bf06b8100cb747031959fe81a2d19baabc4838cf Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Fri, 16 Jun 2023 11:53:29 +0900
Subject: [PATCH 1/2] Support custom wait events for extensions.

+ * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event ID known to the current
+ * process.  Any unused entries in the array will contain NULL.

The second ID should be plural.

+       /* If necessary, create or enlarge array. */
+       if (eventId >= ExtensionWaitEventTrancheNamesAllocated)
+       {
+               int                     newalloc;
+
+               newalloc = pg_nextpower2_32(Max(8, eventId + 1));

Given the context of our last conversation, I assume this code was
copied from somewhere else. Since this is new code, I think it would
make more sense if newalloc was a uint16 or size_t.

From what I undersatnd, Neon differs from upstream in some way related
to this patch. I am trying to ascertain how that is. I hope to provide
more feedback when I learn more about it.

--
Tristan Partin
Neon (https://neon.tech)

#18Michael Paquier
michael@paquier.xyz
In reply to: Tristan Partin (#17)
Re: Support to define custom wait events for extensions

On Tue, Jul 11, 2023 at 12:39:52PM -0500, Tristan Partin wrote:

Given the context of our last conversation, I assume this code was
copied from somewhere else. Since this is new code, I think it would
make more sense if newalloc was a uint16 or size_t.

This style comes from LWLockRegisterTranche() in lwlock.c. Do you
think that it would be more adapted to change that to
pg_nextpower2_size_t() with a Size? We could do that for the existing
code on HEAD as an improvement.

From what I understand, Neon differs from upstream in some way related
to this patch. I am trying to ascertain how that is. I hope to provide
more feedback when I learn more about it.

Hmm, okay, that would nice to hear about to make sure that the
approach taken on this thread is able to cover what you are looking
for. So you mean that Neon has been using something similar to
register wait events in the backend? Last time I looked at the Neon
repo, I did not get the impression that there was a custom patch for
Postgres in this area. All the in-core code paths using
WAIT_EVENT_EXTENSION would gain from the APIs added here, FWIW.
--
Michael

#19Andres Freund
andres@anarazel.de
In reply to: Michael Paquier (#16)
Re: Support to define custom wait events for extensions

Hi,

On 2023-07-11 16:45:26 +0900, Michael Paquier wrote:

+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define custom wait events.  First, call
+ * WaitEventExtensionNewTranche() just once to obtain a new wait event
+ * tranche.  The ID is allocated from a shared counter.  Next, each
+ * individual process using the tranche should call
+ * WaitEventExtensionRegisterTranche() to associate that wait event with
+ * a name.

What does "tranche" mean here? For LWLocks it makes some sense, it's used for
a set of lwlocks, not an individual one. But here that doesn't really seem to
apply?

+ * It may seem strange that each process using the tranche must register it
+ * separately, but dynamic shared memory segments aren't guaranteed to be
+ * mapped at the same address in all coordinating backends, so storing the
+ * registration in the main shared memory segment wouldn't work for that case.
+ */

I don't really see how this applies to wait events? There's no pointers
here...

+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+} WaitEventExtension;
+
+extern void WaitEventExtensionShmemInit(void);
+extern Size WaitEventExtensionShmemSize(void);
+
+extern uint32 WaitEventExtensionNewTranche(void);
+extern void WaitEventExtensionRegisterTranche(uint32 wait_event_info,
-slock_t    *ShmemLock;			/* spinlock for shared memory and LWLock
+slock_t    *ShmemLock;			/* spinlock for shared memory, LWLock
+								 * allocation, and named extension wait event
* allocation */

I'm doubtful that it's a good idea to reuse the spinlock further. Given that
the patch adds WaitEventExtensionShmemInit(), why not just have a lock in
there?

+/*
+ * Allocate a new event ID and return the wait event info.
+ */
+uint32
+WaitEventExtensionNewTranche(void)
+{
+	uint16		eventId;
+
+	SpinLockAcquire(ShmemLock);
+	eventId = (*WaitEventExtensionCounter)++;
+	SpinLockRelease(ShmemLock);
+
+	return PG_WAIT_EXTENSION | eventId;
+}

It seems quite possible to run out space in WaitEventExtensionCounter, so this
should error out in that case.

+/*
+ * Register a dynamic tranche name in the lookup table of the current process.
+ *
+ * This routine will save a pointer to the wait event tranche name passed
+ * as an argument, so the name should be allocated in a backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+WaitEventExtensionRegisterTranche(uint32 wait_event_info,
+								  const char *wait_event_name)
+{
+	uint16		eventId;
+
+	/* Check wait event class. */
+	Assert((wait_event_info & 0xFF000000) == PG_WAIT_EXTENSION);
+
+	eventId = wait_event_info & 0x0000FFFF;
+
+	/* This should only be called for user-defined tranches. */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return;

Why not assert out in that case then?

+/*
+ * Return the name of an Extension wait event ID.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)
+{
+	/* Build-in tranche? */

*built

+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It's an extension tranche, so look in WaitEventExtensionTrancheNames[].
+	 * However, it's possible that the tranche has never been registered by
+	 * calling WaitEventExtensionRegisterTranche() in the current process, in
+	 * which case give up and return "Extension".
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= WaitEventExtensionTrancheNamesAllocated ||
+		WaitEventExtensionTrancheNames[eventId] == NULL)
+		return "Extension";

I'd return something different here, otherwise something that's effectively a
bug is not distinguishable from the built

+++ b/src/test/modules/test_custom_wait_events/t/001_basic.pl
@@ -0,0 +1,34 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+
+$node->init;
+$node->append_conf(
+	'postgresql.conf',
+	"shared_preload_libraries = 'test_custom_wait_events'"
+);
+$node->start;

I think this should also test registering a wait event later.

@@ -0,0 +1,188 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_custom_wait_events.c
+ *		Code for testing custom wait events
+ *
+ * This code initializes a custom wait event in shmem_request_hook() and
+ * provide a function to launch a background worker waiting forever
+ * with the custom wait event.

Isn't this vast overkill? Why not just have a simple C function that waits
with a custom wait event, until cancelled? That'd maybe 1/10th of the LOC.

Greetings,

Andres Freund

#20Michael Paquier
michael@paquier.xyz
In reply to: Andres Freund (#19)
Re: Support to define custom wait events for extensions

On Tue, Jul 11, 2023 at 05:36:47PM -0700, Andres Freund wrote:

On 2023-07-11 16:45:26 +0900, Michael Paquier wrote:

+$node->init;
+$node->append_conf(
+	'postgresql.conf',
+	"shared_preload_libraries = 'test_custom_wait_events'"
+);
+$node->start;

I think this should also test registering a wait event later.

Yup, agreed that the coverage is not sufficient.

@@ -0,0 +1,188 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_custom_wait_events.c
+ *		Code for testing custom wait events
+ *
+ * This code initializes a custom wait event in shmem_request_hook() and
+ * provide a function to launch a background worker waiting forever
+ * with the custom wait event.

Isn't this vast overkill? Why not just have a simple C function that waits
with a custom wait event, until cancelled? That'd maybe 1/10th of the LOC.

Hmm. You mean in the shape of a TAP test where a backend registers a
wait event by itself in a SQL function that waits for a certain amount
of time with a WaitLatch(), then we use a second poll_query_until()
that checks if the a wait event is stored in pg_stat_activity? With
something like what's done at the end of 001_stream_rep.pl, that
should be stable, I guess..
--
Michael

#21Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#16)
1 attachment(s)
Re: Support to define custom wait events for extensions

On 2023-07-11 16:45, Michael Paquier wrote:

On Fri, Jun 23, 2023 at 05:56:26PM +0900, Masahiro Ikeda wrote:

I updated the patches to handle the warning mentioned
by PostgreSQL Patch Tester, and removed unnecessary spaces.

I have begun hacking on that, and the API layer inspired from the
LWLocks is sound. I have been playing with it in my own extensions
and it is nice to be able to plug in custom wait events into
pg_stat_activity, particularly for bgworkers. Finally.

Great!

The patch needed a rebase after the recent commit that introduced the
automatic generation of docs and code for wait events. It requires
two tweaks in generate-wait_event_types.pl, feel free to double-check
them.

Thanks for rebasing. I confirmed it works with the current master.

I know this is a little off-topic from what we're talking about here,
but I'm curious about generate-wait_event_types.pl.

  # generate-wait_event_types.pl
  -	# An exception is required for LWLock and Lock as these don't require
  -	# any C and header files generated.
  +	# An exception is required for Extension, LWLock and Lock as these 
don't
  +	# require any C and header files generated.
   	die "wait event names must start with 'WAIT_EVENT_'"
   	  if ( $trimmedwaiteventname eq $waiteventenumname
  +		&& $waiteventenumname !~ /^Extension/
   		&& $waiteventenumname !~ /^LWLock/
   		&& $waiteventenumname !~ /^Lock/);

In my understanding, the first column of the row for WaitEventExtension
in
wait_event_names.txt can be any value and the above code should not die.
But if I use the following input, it falls on the last line.

# wait_event_names.txt
Section: ClassName - WaitEventExtension

WAIT_EVENT_EXTENSION "Extension" "Waiting in an extension."
Extension "Extension" "Waiting in an extension."
EXTENSION "Extension" "Waiting in an extension."

If the behavior is unexpected, we need to change the current code.
I have created a patch for the areas that I felt needed to be changed.
- 0001-change-the-die-condition-in-generate-wait_event_type.patch
(In addition to the above, "$continue = ",\n";" doesn't appear to be
necessary.)

Some of the new structures and routine names don't quite reflect the
fact that we have wait events for extensions, so I have taken a stab
at that.

Sorry. I confirmed the change.

Note that the test module test_custom_wait_events would crash if
attempting to launch a worker when not loaded in
shared_preload_libraries, so we'd better have some protection in
wait_worker_launch() (this function should be renamed as well).

OK, I will handle it. Since Andres gave me other comments for the
test module, I'll think about what is best.

Attached is a rebased patch that I have begun tweaking here and
there. For now, the patch is moved as waiting on author. I have
merged the test module with the main patch for the moment, for
simplicity. A split is straight-forward as the code paths touched are
different.

Another and *very* important thing is that we are going to require
some documentation in xfunc.sgml to explain how to use these routines
and what to expect from them. Ikeda-san, could you write some? You
could look at the part about shmem and LWLock to get some
inspiration.

OK. Yes, I planned to write documentation.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

0001-change-the-die-condition-in-generate-wait_event_type.patchtext/x-diff; name=0001-change-the-die-condition-in-generate-wait_event_type.patchDownload
From 5198fc6302a3bf4232252b23b45d39d987e736bc Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Wed, 12 Jul 2023 16:28:27 +0900
Subject: [PATCH] change the die condition in generate-wait_event_types.pl

---
 src/backend/utils/activity/generate-wait_event_types.pl | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/backend/utils/activity/generate-wait_event_types.pl b/src/backend/utils/activity/generate-wait_event_types.pl
index 6d1a2af42a..fbe011039a 100644
--- a/src/backend/utils/activity/generate-wait_event_types.pl
+++ b/src/backend/utils/activity/generate-wait_event_types.pl
@@ -89,9 +89,8 @@ foreach my $line (@lines_sorted)
 	# any C and header files generated.
 	die "wait event names must start with 'WAIT_EVENT_'"
 	  if ( $trimmedwaiteventname eq $waiteventenumname
-		&& $waiteventenumname !~ /^LWLock/
-		&& $waiteventenumname !~ /^Lock/);
-	$continue = ",\n";
+		&& $waitclassname !~ /^WaitEventLWLock$/
+		&& $waitclassname !~ /^WaitEventLock$/);
 	push(@{ $hashwe{$waitclassname} }, @waiteventlist);
 }
 
-- 
2.25.1

#22Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Tristan Partin (#17)
Re: Support to define custom wait events for extensions

On 2023-07-12 02:39, Tristan Partin wrote:

From bf06b8100cb747031959fe81a2d19baabc4838cf Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Fri, 16 Jun 2023 11:53:29 +0900
Subject: [PATCH 1/2] Support custom wait events for extensions.

+ * This is indexed by event ID minus 
NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event ID known to the 
current
+ * process.  Any unused entries in the array will contain NULL.

The second ID should be plural.

Thanks for reviewing. Yes, I'll fix it.

+       /* If necessary, create or enlarge array. */
+       if (eventId >= ExtensionWaitEventTrancheNamesAllocated)
+       {
+               int                     newalloc;
+
+               newalloc = pg_nextpower2_32(Max(8, eventId + 1));

Given the context of our last conversation, I assume this code was
copied from somewhere else. Since this is new code, I think it would
make more sense if newalloc was a uint16 or size_t.

As Michael-san said, I used LWLockRegisterTranche() as a reference.
I think it is a good idea to fix the current master. I'll modify the
above code accordingly.

From what I undersatnd, Neon differs from upstream in some way related
to this patch. I am trying to ascertain how that is. I hope to provide
more feedback when I learn more about it.

Oh, it was unexpected for me. Thanks for researching the reason.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#23Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Andres Freund (#19)
Re: Support to define custom wait events for extensions

On 2023-07-12 09:36, Andres Freund wrote:

Hi,

On 2023-07-11 16:45:26 +0900, Michael Paquier wrote:

+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some 
condition
+ * defined by an extension module.
+ *
+ * Extensions can define custom wait events.  First, call
+ * WaitEventExtensionNewTranche() just once to obtain a new wait 
event
+ * tranche.  The ID is allocated from a shared counter.  Next, each
+ * individual process using the tranche should call
+ * WaitEventExtensionRegisterTranche() to associate that wait event 
with
+ * a name.

What does "tranche" mean here? For LWLocks it makes some sense, it's
used for
a set of lwlocks, not an individual one. But here that doesn't really
seem to
apply?

Thanks for useful comments.
OK, I will change to WaitEventExtensionNewId() and
WaitEventExtensionRegisterName().

+ * It may seem strange that each process using the tranche must 
register it
+ * separately, but dynamic shared memory segments aren't guaranteed 
to be
+ * mapped at the same address in all coordinating backends, so 
storing the
+ * registration in the main shared memory segment wouldn't work for 
that case.
+ */

I don't really see how this applies to wait events? There's no pointers
here...

Yes, I'll fix the comments.

+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+} WaitEventExtension;
+
+extern void WaitEventExtensionShmemInit(void);
+extern Size WaitEventExtensionShmemSize(void);
+
+extern uint32 WaitEventExtensionNewTranche(void);
+extern void WaitEventExtensionRegisterTranche(uint32 wait_event_info,
-slock_t    *ShmemLock;			/* spinlock for shared memory and LWLock
+slock_t    *ShmemLock;			/* spinlock for shared memory, LWLock
+								 * allocation, and named extension wait event
* allocation */

I'm doubtful that it's a good idea to reuse the spinlock further. Given
that
the patch adds WaitEventExtensionShmemInit(), why not just have a lock
in
there?

OK, I'll create a new spinlock for the purpose.

+/*
+ * Allocate a new event ID and return the wait event info.
+ */
+uint32
+WaitEventExtensionNewTranche(void)
+{
+	uint16		eventId;
+
+	SpinLockAcquire(ShmemLock);
+	eventId = (*WaitEventExtensionCounter)++;
+	SpinLockRelease(ShmemLock);
+
+	return PG_WAIT_EXTENSION | eventId;
+}

It seems quite possible to run out space in WaitEventExtensionCounter,
so this
should error out in that case.

OK, I'll do so.

+/*
+ * Register a dynamic tranche name in the lookup table of the current 
process.
+ *
+ * This routine will save a pointer to the wait event tranche name 
passed
+ * as an argument, so the name should be allocated in a 
backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, 
so try to
+ * use a name that fits the style for those.
+ */
+void
+WaitEventExtensionRegisterTranche(uint32 wait_event_info,
+								  const char *wait_event_name)
+{
+	uint16		eventId;
+
+	/* Check wait event class. */
+	Assert((wait_event_info & 0xFF000000) == PG_WAIT_EXTENSION);
+
+	eventId = wait_event_info & 0x0000FFFF;
+
+	/* This should only be called for user-defined tranches. */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return;

Why not assert out in that case then?

OK, I'll add the assertion for eventID.

+/*
+ * Return the name of an Extension wait event ID.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)
+{
+	/* Build-in tranche? */

*built

I will fix it.

+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It's an extension tranche, so look in 
WaitEventExtensionTrancheNames[].
+	 * However, it's possible that the tranche has never been registered 
by
+	 * calling WaitEventExtensionRegisterTranche() in the current 
process, in
+	 * which case give up and return "Extension".
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= WaitEventExtensionTrancheNamesAllocated ||
+		WaitEventExtensionTrancheNames[eventId] == NULL)
+		return "Extension";

I'd return something different here, otherwise something that's
effectively a
bug is not distinguishable from the built

It is a good idea. It would be good to name it "unknown wait event", the
same as
pgstat_get_wait_activity(), etc.

+++ b/src/test/modules/test_custom_wait_events/t/001_basic.pl
@@ -0,0 +1,34 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+
+$node->init;
+$node->append_conf(
+	'postgresql.conf',
+	"shared_preload_libraries = 'test_custom_wait_events'"
+);
+$node->start;

I think this should also test registering a wait event later.

I see. I wasn't expecting that.

@@ -0,0 +1,188 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_custom_wait_events.c
+ *		Code for testing custom wait events
+ *
+ * This code initializes a custom wait event in shmem_request_hook() 
and
+ * provide a function to launch a background worker waiting forever
+ * with the custom wait event.

Isn't this vast overkill? Why not just have a simple C function that
waits
with a custom wait event, until cancelled? That'd maybe 1/10th of the
LOC.

Are you saying that processing in the background worker is overkill?

If my understanding is correct, it is difficult to implement the test
without a background worker for this purpose. This is because the test
will execute commands serially, while a function waiting is executed in
a backend process, it is not possible to connect to another backend and
check the wait events on pg_stat_activity view.

Please let me know if my understanding is wrong.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#24Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#21)
Re: Support to define custom wait events for extensions

On Wed, Jul 12, 2023 at 04:52:38PM +0900, Masahiro Ikeda wrote:

In my understanding, the first column of the row for WaitEventExtension in
wait_event_names.txt can be any value and the above code should not die.
But if I use the following input, it falls on the last line.

# wait_event_names.txt
Section: ClassName - WaitEventExtension

WAIT_EVENT_EXTENSION "Extension" "Waiting in an extension."
Extension "Extension" "Waiting in an extension."
EXTENSION "Extension" "Waiting in an extension."

If the behavior is unexpected, we need to change the current code.
I have created a patch for the areas that I felt needed to be changed.
- 0001-change-the-die-condition-in-generate-wait_event_type.patch
(In addition to the above, "$continue = ",\n";" doesn't appear to be
necessary.)

    die "wait event names must start with 'WAIT_EVENT_'"
      if ( $trimmedwaiteventname eq $waiteventenumname
-       && $waiteventenumname !~ /^LWLock/
-       && $waiteventenumname !~ /^Lock/);
-   $continue = ",\n";
+       && $waitclassname !~ /^WaitEventLWLock$/
+       && $waitclassname !~ /^WaitEventLock$/);

Indeed, this looks wrong as-is. $waiteventenumname refers to the
names of the enum elements, so we could just apply a filter based on
the class names in full. The second check in for the generation of
the c/h files uses the class names.
--
Michael

#25Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#24)
Re: Support to define custom wait events for extensions

On Wed, Jul 12, 2023 at 05:46:31PM +0900, Michael Paquier wrote:

On Wed, Jul 12, 2023 at 04:52:38PM +0900, Masahiro Ikeda wrote:

If the behavior is unexpected, we need to change the current code.
I have created a patch for the areas that I felt needed to be changed.
- 0001-change-the-die-condition-in-generate-wait_event_type.patch
(In addition to the above, "$continue = ",\n";" doesn't appear to be
necessary.)

die "wait event names must start with 'WAIT_EVENT_'"
if ( $trimmedwaiteventname eq $waiteventenumname
-       && $waiteventenumname !~ /^LWLock/
-       && $waiteventenumname !~ /^Lock/);
-   $continue = ",\n";
+       && $waitclassname !~ /^WaitEventLWLock$/
+       && $waitclassname !~ /^WaitEventLock$/);

Indeed, this looks wrong as-is. $waiteventenumname refers to the
names of the enum elements, so we could just apply a filter based on
the class names in full. The second check in for the generation of
the c/h files uses the class names.

At the end, I have gone with an event simpler way and removed the
checks for LWLock and Lock as their hardcoded values marked as DOCONLY
satisfy this check. The second check when generating the C and header
code has also been simplified a bit to use an exact match with the
class name.
--
Michael

#26Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#25)
Re: Support to define custom wait events for extensions

On 2023-07-13 09:12, Michael Paquier wrote:

On Wed, Jul 12, 2023 at 05:46:31PM +0900, Michael Paquier wrote:

On Wed, Jul 12, 2023 at 04:52:38PM +0900, Masahiro Ikeda wrote:

If the behavior is unexpected, we need to change the current code.
I have created a patch for the areas that I felt needed to be
changed.
- 0001-change-the-die-condition-in-generate-wait_event_type.patch
(In addition to the above, "$continue = ",\n";" doesn't appear to be
necessary.)

die "wait event names must start with 'WAIT_EVENT_'"
if ( $trimmedwaiteventname eq $waiteventenumname
-       && $waiteventenumname !~ /^LWLock/
-       && $waiteventenumname !~ /^Lock/);
-   $continue = ",\n";
+       && $waitclassname !~ /^WaitEventLWLock$/
+       && $waitclassname !~ /^WaitEventLock$/);

Indeed, this looks wrong as-is. $waiteventenumname refers to the
names of the enum elements, so we could just apply a filter based on
the class names in full. The second check in for the generation of
the c/h files uses the class names.

At the end, I have gone with an event simpler way and removed the
checks for LWLock and Lock as their hardcoded values marked as DOCONLY
satisfy this check. The second check when generating the C and header
code has also been simplified a bit to use an exact match with the
class name.

Thanks for your quick response. I'll rebase for the commit.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#27Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#26)
Re: Support to define custom wait events for extensions

On Thu, Jul 13, 2023 at 10:26:35AM +0900, Masahiro Ikeda wrote:

Thanks for your quick response. I'll rebase for the commit.

Okay, thanks. I'll wait for the rebased version before moving on with
the next review, then.
--
Michael

#28Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#27)
1 attachment(s)
Re: Support to define custom wait events for extensions

Hi,

I updated the patches.
* v6-0001-Support-custom-wait-events-for-extensions.patch

The main diffs are

* rebase it atop current HEAD
* update docs to show users how to use the APIs
* rename of functions and variables
* fix typos
* define a new spinlock in shared memory for this purpose
* output an error if the number of wait event for extensions exceeds
uint16
* show the wait event as "extension" if the custom wait event name is
not
registered, which is same as LWLock one.
* add test cases which confirm it works if new wait events for
extensions
are defined in initialize phase and after phase. And add a boundary
condition test.

Please let me know if I forgot to handle something that you commented,
and there are better idea.

Note:
I would like to change the wait event name of contrib modules for
example
postgres_fdw. But, I think it's better to do so after the APIs are
committed.
The example mentioned in docs should be updated to the contrib modules
codes,
not the test module.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

v6-0001-Support-custom-wait-events-for-extensions.patchtext/x-diff; name=v6-0001-Support-custom-wait-events-for-extensions.patchDownload
From 9c5c1f1f4f0be87cc06c7127396e7d228b665b8a Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Wed, 19 Jul 2023 12:28:12 +0900
Subject: [PATCH] Support custom wait events for extensions.

To support custom wait events, it add 2 APIs to define new wait events
for extensions dynamically.

The APIs are
* WaitEventExtensionNew()
* WaitEventExtensionRegisterName()

These are similar to the existing LWLockNewTrancheId() and
LWLockRegisterTranche().

First, extensions should call WaitEventExtensionNew() to get one
or more new wait event, which IDs are allocated from a shared
counter.  Next, each individual process can use the wait event with
WaitEventExtensionRegisterName() to associate that a wait event
string to the associated name.

Note that this includes a test module, which perhaps should be split
later into its own commit.
---
 doc/src/sgml/monitoring.sgml                  |  10 +-
 doc/src/sgml/xfunc.sgml                       |  23 ++
 src/backend/storage/ipc/ipci.c                |   3 +
 .../activity/generate-wait_event_types.pl     |   7 +-
 src/backend/utils/activity/wait_event.c       | 171 +++++++++++-
 .../utils/activity/wait_event_names.txt       |   2 +-
 src/include/utils/wait_event.h                |  25 ++
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 .../test_custom_wait_events/.gitignore        |   4 +
 .../modules/test_custom_wait_events/Makefile  |  23 ++
 .../test_custom_wait_events/meson.build       |  33 +++
 .../test_custom_wait_events/t/001_basic.pl    | 137 ++++++++++
 .../test_custom_wait_events--1.0.sql          |  39 +++
 .../test_custom_wait_events.c                 | 253 ++++++++++++++++++
 .../test_custom_wait_events.control           |   3 +
 16 files changed, 718 insertions(+), 17 deletions(-)
 create mode 100644 src/test/modules/test_custom_wait_events/.gitignore
 create mode 100644 src/test/modules/test_custom_wait_events/Makefile
 create mode 100644 src/test/modules/test_custom_wait_events/meson.build
 create mode 100644 src/test/modules/test_custom_wait_events/t/001_basic.pl
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.c
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.control

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 588b720f57..3aee5c243f 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1117,12 +1117,12 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
    <note>
     <para>
-     Extensions can add <literal>LWLock</literal> types to the list shown in
-     <xref linkend="wait-event-lwlock-table"/>.  In some cases, the name
+     Extensions can add <literal>Extension</literal> and <literal>LWLock</literal> types
+     to the list shown in <xref linkend="wait-event-extension-table"/> and
+     <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
      assigned by an extension will not be available in all server processes;
-     so an <literal>LWLock</literal> wait event might be reported as
-     just <quote><literal>extension</literal></quote> rather than the
-     extension-assigned name.
+     so an wait event might be reported as just <quote><literal>extension</literal></quote>
+     rather than the extension-assigned name.
     </para>
    </note>
  </sect2>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 9620ea9ae3..bc210cd03b 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3453,6 +3453,29 @@ if (!ptr)
     </para>
    </sect2>
 
+   <sect2 id="xfunc-addin-wait-events">
+    <title>Custom Wait Events for Add-ins</title>
+
+    <para>
+    Add-ins can define custom wait events that the wait event type is
+    <literal>Extension</literal>.
+    </para>
+    <para>
+    First, add-ins should get new one or more wait events by calling:
+<programlisting>
+    uint32 WaitEventExtensionNew(void)
+</programlisting>
+    Next, each individual process can use them to associate that
+    a wait event string to the associated name by calling:
+<programlisting>
+    void WaitEventExtensionRegisterName(uint32 wait_event_info, const char *wait_event_name);
+</programlisting>
+    An example can be found in
+    <filename>src/test/modules/test_custom_wait_events/test_custom_wait_events.c</filename>
+    in the PostgreSQL source tree.
+    </para>
+   </sect2>
+
    <sect2 id="extend-cpp">
     <title>Using C++ for Extensibility</title>
 
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index cc387c00a1..5551afffc0 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -49,6 +49,7 @@
 #include "storage/spin.h"
 #include "utils/guc.h"
 #include "utils/snapmgr.h"
+#include "utils/wait_event.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -142,6 +143,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SyncScanShmemSize());
 	size = add_size(size, AsyncShmemSize());
 	size = add_size(size, StatsShmemSize());
+	size = add_size(size, WaitEventExtensionShmemSize());
 #ifdef EXEC_BACKEND
 	size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -301,6 +303,7 @@ CreateSharedMemoryAndSemaphores(void)
 	SyncScanShmemInit();
 	AsyncShmemInit();
 	StatsShmemInit();
+	WaitEventExtensionShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/utils/activity/generate-wait_event_types.pl b/src/backend/utils/activity/generate-wait_event_types.pl
index f63c991051..56335e8730 100644
--- a/src/backend/utils/activity/generate-wait_event_types.pl
+++ b/src/backend/utils/activity/generate-wait_event_types.pl
@@ -133,10 +133,11 @@ if ($gen_code)
 	foreach my $waitclass (sort { uc($a) cmp uc($b) } keys %hashwe)
 	{
 
-		# Don't generate .c and .h files for LWLock and Lock, these are
-		# handled independently.
+		# Don't generate .c and .h files for Extension, LWLock and
+		# Lock, these are handled independently.
 		next
-		  if ( $waitclass eq 'WaitEventLWLock'
+		  if ( $waitclass eq 'WaitEventExtension'
+			|| $waitclass eq 'WaitEventLWLock'
 			|| $waitclass eq 'WaitEventLock');
 
 		my $last = $waitclass;
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 4aad11c111..03acc10407 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,15 +22,18 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
 #include "storage/lwlock.h"		/* for GetLWLockIdentifier */
+#include "storage/spin.h"
+#include "utils/memutils.h"
 #include "utils/wait_event.h"
 
 
 static const char *pgstat_get_wait_activity(WaitEventActivity w);
 static const char *pgstat_get_wait_bufferpin(WaitEventBufferPin w);
 static const char *pgstat_get_wait_client(WaitEventClient w);
-static const char *pgstat_get_wait_extension(WaitEventExtension w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
@@ -40,6 +43,162 @@ static uint32 local_my_wait_event_info;
 uint32	   *my_wait_event_info = &local_my_wait_event_info;
 
 
+
+/* dynamic allocation counter for custom wait events for extensions */
+typedef struct WaitEventExtensionCounter
+{
+	int nextId;		/* next ID to assign */
+	slock_t	mutex;  /* protects the counter only */
+} WaitEventExtensionCounter;
+
+/* pointer to the shared memory */
+static WaitEventExtensionCounter *waitEventExtensionCounter;
+
+/* first event ID of custom wait events for extensions */
+#define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
+	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
+
+/*
+ * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event IDs known to the current
+ * process.  Any unused entries in the array will contain NULL.
+ */
+static const char **WaitEventExtensionNames = NULL;
+static int	WaitEventExtensionNamesAllocated = 0;
+
+static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
+
+/*
+ *  Return the space for dynamic allocation counter.
+ */
+Size
+WaitEventExtensionShmemSize(void)
+{
+	return sizeof(WaitEventExtensionCounter);
+}
+
+/*
+ * Allocate shmem space for dynamic allocation counter.
+ */
+void
+WaitEventExtensionShmemInit(void)
+{
+	bool	found;
+
+	if (!IsUnderPostmaster)
+	{
+		/* Allocate space in shared memory. */
+		waitEventExtensionCounter = (WaitEventExtensionCounter *)
+			ShmemInitStruct("waitEventExtensionCounter", WaitEventExtensionShmemSize(), &found);
+		if (found)
+			return;
+
+		/* Initialize the dynamic-allocation counter and the spinlock. */
+		waitEventExtensionCounter->nextId = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+		SpinLockInit(&waitEventExtensionCounter->mutex);
+	}
+}
+
+/*
+ * Allocate a new event ID and return the wait event.
+ */
+uint32
+WaitEventExtensionNew(void)
+{
+	uint16		eventId;
+
+	SpinLockAcquire(&waitEventExtensionCounter->mutex);
+
+	/* Check for the counter overflow. */
+	if (waitEventExtensionCounter->nextId > PG_UINT16_MAX)
+	{
+		SpinLockRelease(&waitEventExtensionCounter->mutex);
+		ereport(ERROR,
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("too many wait events for extensions"));
+	}
+
+	eventId = waitEventExtensionCounter->nextId++;
+
+	SpinLockRelease(&waitEventExtensionCounter->mutex);
+
+	return PG_WAIT_EXTENSION | eventId;
+}
+
+/*
+ * Register a dynamic wait event name for extension in the lookup table
+ * of the current process.
+ *
+ * This routine will save a pointer to the wait event name passed as an argument,
+ * so the name should be allocated in a backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+WaitEventExtensionRegisterName(uint32 wait_event_info,
+								const char *wait_event_name)
+{
+	uint16		eventId;
+
+	eventId = wait_event_info & 0x0000FFFF;
+
+	/* Check the wait event class. */
+	Assert((wait_event_info & 0xFF000000) == PG_WAIT_EXTENSION);
+
+	/* This should only be called for user-defined wait event. */
+	Assert(eventId >= NUM_BUILTIN_WAIT_EVENT_EXTENSION);
+
+	/* Convert to array index. */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	/* If necessary, create or enlarge array. */
+	if (eventId >= WaitEventExtensionNamesAllocated)
+	{
+		int			newalloc;
+
+		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
+
+		if (WaitEventExtensionNames == NULL)
+			WaitEventExtensionNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		else
+			WaitEventExtensionNames =
+				repalloc0_array(WaitEventExtensionNames, const char *, WaitEventExtensionNamesAllocated, newalloc);
+		WaitEventExtensionNamesAllocated = newalloc;
+	}
+
+	WaitEventExtensionNames[eventId] = wait_event_name;
+}
+
+/*
+ * Return the name of an wait event ID for extension.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)
+{
+	/* Built-in event? */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It's an user-defined wait event, so look in WaitEventExtensionNames[].
+	 * However, it's possible that the name has never been registered by
+	 * calling WaitEventExtensionRegisterName() in the current process, in
+	 * which case give up and return "extension".
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= WaitEventExtensionNamesAllocated ||
+		WaitEventExtensionNames[eventId] == NULL)
+		return "extension";
+
+	return WaitEventExtensionNames[eventId];
+}
+
+
 /*
  * Configure wait event reporting to report wait events to *wait_event_info.
  * *wait_event_info needs to be valid until pgstat_reset_wait_event_storage()
@@ -149,6 +308,9 @@ pgstat_get_wait_event(uint32 wait_event_info)
 		case PG_WAIT_LOCK:
 			event_name = GetLockNameFromTagType(eventId);
 			break;
+		case PG_WAIT_EXTENSION:
+			event_name = GetWaitEventExtensionIdentifier(eventId);
+			break;
 		case PG_WAIT_BUFFERPIN:
 			{
 				WaitEventBufferPin w = (WaitEventBufferPin) wait_event_info;
@@ -170,13 +332,6 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_client(w);
 				break;
 			}
-		case PG_WAIT_EXTENSION:
-			{
-				WaitEventExtension w = (WaitEventExtension) wait_event_info;
-
-				event_name = pgstat_get_wait_extension(w);
-				break;
-			}
 		case PG_WAIT_IPC:
 			{
 				WaitEventIPC w = (WaitEventIPC) wait_event_info;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3fabad96d9..2ea4789b00 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -261,7 +261,7 @@ WAIT_EVENT_BUFFER_PIN	BufferPin	"Waiting to acquire an exclusive pin on a buffer
 
 Section: ClassName - WaitEventExtension
 
-WAIT_EVENT_EXTENSION	Extension	"Waiting in an extension."
+WAIT_EVENT_DOCONLY	Extension	"Waiting in an extension."
 
 #
 # Wait events - LWLock
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 4517425f84..ae3dac88a4 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -67,6 +67,31 @@ pgstat_report_wait_start(uint32 wait_event_info)
 	*(volatile uint32 *) my_wait_event_info = wait_event_info;
 }
 
+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define their wait events. First, extensions should call
+ * WaitEventExtensionNew() to get one or more wait events, which IDs are
+ * allocated from a shared counter.  Next, each individual process can use
+ * them with WaitEventExtensionRegisterName() to associate that a wait
+ * event string to the associated name.
+ */
+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+} WaitEventExtension;
+
+extern void WaitEventExtensionShmemInit(void);
+extern Size WaitEventExtensionShmemSize(void);
+
+extern uint32 WaitEventExtensionNew(void);
+extern void WaitEventExtensionRegisterName(uint32 wait_event_info,
+								const char *wait_event_name);
+
 /* ----------
  * pgstat_report_wait_end() -
  *
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 6331c976dc..84312b7e57 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -17,6 +17,7 @@ SUBDIRS = \
 		  test_bloomfilter \
 		  test_copy_callbacks \
 		  test_custom_rmgrs \
+		  test_custom_wait_events \
 		  test_ddl_deparse \
 		  test_extensions \
 		  test_ginpostinglist \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 17d369e378..83a1d8fd19 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -14,6 +14,7 @@ subdir('ssl_passphrase_callback')
 subdir('test_bloomfilter')
 subdir('test_copy_callbacks')
 subdir('test_custom_rmgrs')
+subdir('test_custom_wait_events')
 subdir('test_ddl_deparse')
 subdir('test_extensions')
 subdir('test_ginpostinglist')
diff --git a/src/test/modules/test_custom_wait_events/.gitignore b/src/test/modules/test_custom_wait_events/.gitignore
new file mode 100644
index 0000000000..5dcb3ff972
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_custom_wait_events/Makefile b/src/test/modules/test_custom_wait_events/Makefile
new file mode 100644
index 0000000000..dda365d523
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_custom_wait_events/Makefile
+
+MODULE_big = test_custom_wait_events
+OBJS = \
+	$(WIN32RES) \
+	test_custom_wait_events.o
+PGFILEDESC = "test_custom_wait_events - test custom wait events"
+
+EXTENSION = test_custom_wait_events
+DATA = test_custom_wait_events--1.0.sql
+
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_custom_wait_events
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_custom_wait_events/meson.build b/src/test/modules/test_custom_wait_events/meson.build
new file mode 100644
index 0000000000..8ad073e577
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) PostgreSQL Global Development Group
+
+test_custom_wait_events = files(
+  'test_custom_wait_events.c',
+)
+
+if host_system == 'windows'
+  test_custom_wait_events += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_custom_wait_events',
+    '--FILEDESC', 'test_custom_wait_events - test custom wait events',])
+endif
+
+test_custom_wait_events = shared_module('test_custom_wait_events',
+  test_custom_wait_events,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_custom_wait_events
+
+test_install_data += files(
+  'test_custom_wait_events.control',
+  'test_custom_wait_events--1.0.sql',
+)
+
+tests += {
+  'name': 'test_custom_wait_events',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_basic.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_custom_wait_events/t/001_basic.pl b/src/test/modules/test_custom_wait_events/t/001_basic.pl
new file mode 100644
index 0000000000..6817eaa2b0
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/t/001_basic.pl
@@ -0,0 +1,137 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+
+# Check that a new wait event can be defined in initialized phase.
+$node->init;
+$node->append_conf(
+	'postgresql.conf',
+	"shared_preload_libraries = 'test_custom_wait_events'"
+);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_custom_wait_events;');
+
+$node->safe_psql('postgres', 'SELECT start_bgworker_waiting_forever();');
+
+ok($node->poll_query_until('postgres',
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND wait_event = 'custom_wait_event'
+		) = 1;]),
+	"registering custom wait event in initialized phase doen't work"
+);
+
+$node->stop;
+$node->clean_node;
+
+
+# Check that multiple new wait events can be defined dynamically.
+$node->init;
+$node->append_conf(
+	'postgresql.conf',
+);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_custom_wait_events;');
+
+# Start new background workers. They wait with new wait events dinamically defined.
+my $wait_event_info_1 = $node->safe_psql('postgres', 'SELECT get_new_wait_event_info();');
+$node->safe_psql('postgres', "SELECT start_bgworker_waiting_forever($wait_event_info_1);");
+my $wait_event_info_2 = $node->safe_psql('postgres', 'SELECT get_new_wait_event_info();');
+$node->safe_psql('postgres', "SELECT start_bgworker_waiting_forever($wait_event_info_2);");
+
+# Since the new wait event names are not registered in current process now,
+# 'wait_event' must be 'extension' which means that it's not registered.
+ok($node->poll_query_until(
+	'postgres',
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND wait_event = 'extension'
+		) = 2;]),
+	"unregisterd wait event names are wrong"
+);
+
+# Register wait event names and check that they will be visible.
+my $new_wait_event_name_1 = 'new_dynamic_wait_event_1';
+my $new_wait_event_name_2 = 'new_dynamic_wait_event_2';
+my $session = $node->background_psql('postgres');
+$session->query_safe(
+	"SELECT register_wait_event_name($wait_event_info_1, '$new_wait_event_name_1');");
+$session->query_safe(
+	"SELECT register_wait_event_name($wait_event_info_2, '$new_wait_event_name_2');");
+my $res = $session->query_safe(
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND (wait_event = '$new_wait_event_name_1')
+		) = 1;
+]);
+like($res, qr/^t$/m, "$new_wait_event_name_1 dynamically registered wasn't found");
+$res = $session->query_safe(
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND (wait_event = '$new_wait_event_name_2')
+		) = 1;
+]);
+like($res, qr/^t$/m, "$new_wait_event_name_2 dynamically registered wasn't found");
+$session->quit;
+
+$node->stop;
+$node->clean_node;
+
+
+# Check the boundary condition. An error should happen if the number of wait
+# events for extension exceeds the limits.
+$node->init;
+$node->append_conf(
+	'postgresql.conf',
+);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_custom_wait_events;');
+
+# skip to the maximum number of wait event for extension. "$uint16_max" can
+# be allocated since the first wait event is reserved by core.
+my $uint16_max=65535;
+$node->safe_psql('postgres',
+	"SELECT COUNT(get_new_wait_event_info()) FROM generate_series(1, $uint16_max - 1)");
+
+# can use the maximum one
+my $wait_event_info = $node->safe_psql('postgres', 'SELECT get_new_wait_event_info();');
+$node->safe_psql('postgres', "SELECT start_bgworker_waiting_forever($wait_event_info);");
+ok($node->poll_query_until(
+	'postgres',
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND wait_event = 'extension'
+		) = 1;]),
+	"unregisterd wait event name is wrong"
+);
+
+# but, next call should output an error.
+my ($ret, $stdout, $stderr) =
+	$node->psql('postgres', 'SELECT get_new_wait_event_info();');
+like(
+	$stderr,
+	qr/ERROR:  too many wait events for extensions/,
+	'expected error when the number of wait events for extensions exceeds the limits'
+);
+
+$node->stop;
+$node->clean_node;
+
+done_testing();
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
new file mode 100644
index 0000000000..993289b1ff
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
@@ -0,0 +1,39 @@
+/* src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_custom_wait_events" to load this file. \quit
+
+--
+-- get_new_wait_event_info()
+--
+-- Dynamically get a new wait event information.
+--
+CREATE FUNCTION get_new_wait_event_info()
+RETURNS bigint
+AS 'MODULE_PATHNAME', 'get_new_wait_event_info'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- register_wait_event_name()
+--
+-- Dynamically register a wait event name.
+--
+CREATE FUNCTION register_wait_event_name(
+    wait_event_info bigint, wait_event_name text
+)
+RETURNS void
+AS 'MODULE_PATHNAME', 'register_wait_event_name'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- start_bgworker_waiting_forever()
+--
+-- Dynamically start a background worker waiting forever
+-- with the specified custom wait event.
+--
+CREATE FUNCTION start_bgworker_waiting_forever(
+    wait_event_info bigint DEFAULT NULL
+)
+RETURNS int4
+AS 'MODULE_PATHNAME', 'start_bgworker_waiting_forever'
+LANGUAGE C PARALLEL SAFE;
\ No newline at end of file
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.c b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
new file mode 100644
index 0000000000..7527d25f8f
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
@@ -0,0 +1,253 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_custom_wait_events.c
+ *		Code for testing custom wait events for extensions
+ *
+ * This code has some functions to define and register new custom wait
+ * events, and to launch a background worker waiting forever with the
+ * specified wait event.
+ *
+ * You can define them not only in initialized phases but also dynamically
+ * via calling user functions. A new wait event "custom_wait_event" is
+ * defined and registered in shmem_startup_hook(). It will be saved in
+ * shared memory. Users can also define and register new custom wait events
+ * via get_new_wait_event_info() and register_wait_event_name() dynamically.
+ *
+ * You can use the wait event for a background worker launched in
+ * start_bgworker_waiting_forever(). The reason why it starts background
+ * workers is for tap tests. It enables to check the wait events on
+ * pg_stat_activity from other processes.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_custom_wait_events/test_custom_wait_events.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lwlock.h"
+#include "storage/proc.h"
+#include "storage/shmem.h"
+#include "tcop/utility.h"
+#include "utils/builtins.h"
+#include "utils/memutils.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(get_new_wait_event_info);
+PG_FUNCTION_INFO_V1(register_wait_event_name);
+PG_FUNCTION_INFO_V1(start_bgworker_waiting_forever);
+
+PGDLLEXPORT void custom_we_worker_main(Datum main_arg);
+
+/* GUC variables */
+typedef struct CustomWaitEventState
+{
+	uint32			wait_event_info; 		 /* the wait event defined in initialized phase */
+} CustomWaitEventState;
+static CustomWaitEventState *ss = NULL;      /* the pointer to the shared memory */
+
+static char *wait_event_name;                /* the wait event name dynamically registered  */
+static char *worker_database = "postgres";   /* connected by a background worker */
+
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_request_hook_type prev_shmem_startup_hook = NULL;
+
+static void custom_we_shmem_request(void);
+static void custom_we_shmem_startup(void);
+static Size custom_we_memsize(void);
+
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = custom_we_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = custom_we_shmem_startup;
+}
+
+static void
+custom_we_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(custom_we_memsize());
+}
+
+static Size
+custom_we_memsize(void)
+{
+	return sizeof(CustomWaitEventState);
+}
+
+/*
+ * Define and register a new wait event.
+ *
+ * This routine defines a new wait event when it's called first time.
+ * After that, the name of the wait event is associated with it. It's
+ * registered in the lookup table of each processes.
+ */
+static void
+custom_we_shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	ss = NULL;
+
+	/* Create or attach to the shared memory state */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+	ss = ShmemInitStruct("custom_wait_event",
+						 sizeof(CustomWaitEventState),
+						 &found);
+
+	/* Define a new wait event. */
+	if (!found)
+		ss->wait_event_info = WaitEventExtensionNew();
+
+	LWLockRelease(AddinShmemInitLock);
+
+	/*
+	 * Register the wait event in the lookup table of the current process.
+	 */
+	WaitEventExtensionRegisterName(ss->wait_event_info,
+									  "custom_wait_event");
+
+	return;
+}
+
+/*
+ * Connect to the database and wait forever with the wait event
+ * specified in first argument.
+ */
+void
+custom_we_worker_main(Datum main_arg)
+{
+	uint32 wait_event_info;
+	wait_event_info = DatumGetUInt32(main_arg);
+
+	pqsignal(SIGTERM, SignalHandlerForShutdownRequest);
+	BackgroundWorkerUnblockSignals();
+
+	/* Connect to our database to show up a wait event in pg_stat_activity */
+	BackgroundWorkerInitializeConnection(worker_database, NULL, 0);
+
+	/* Wait forever with the specified wait event */
+	while (!ShutdownRequestPending)
+	{
+		(void) WaitLatch(MyLatch,
+						 WL_LATCH_SET | WL_EXIT_ON_PM_DEATH,
+						 -1L,
+						 wait_event_info);
+		ResetLatch(MyLatch);
+	}
+}
+
+/*
+ * Dynamically get and return a new wait event.
+ */
+Datum
+get_new_wait_event_info(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_UINT32(WaitEventExtensionNew());
+}
+
+/*
+ * Dynamically register a wait event name in the lookup table of
+ * current process.
+ */
+Datum
+register_wait_event_name(PG_FUNCTION_ARGS)
+{
+	uint32 wait_event_info;
+	char *wait_event_name_arg;
+	int size;
+
+	wait_event_info = PG_GETARG_UINT32(0);
+
+	/*
+	 * Copy to the name in a backend-lifetime context since
+	 * WaitEventExtensionRegisterName() doesn't copy the name.
+	 */
+	wait_event_name_arg = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	size = strlen(wait_event_name_arg) + 1;
+	Assert(size <= NAMEDATALEN);
+	wait_event_name = (char *) MemoryContextAlloc(TopMemoryContext, size);
+	strlcpy(wait_event_name, wait_event_name_arg, size);
+
+	WaitEventExtensionRegisterName(wait_event_info, wait_event_name);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Dynamically start a background worker waiting forever
+ * with the custom wait event specified in the first
+ * argument. If the argument is NULL, it will wait with
+ * the wait event defined in initialized phase.
+ */
+Datum
+start_bgworker_waiting_forever(PG_FUNCTION_ARGS)
+{
+	uint32 wait_event_info;
+	BackgroundWorker worker;
+	BackgroundWorkerHandle *handle;
+	BgwHandleStatus status;
+	pid_t		pid;
+
+	if (PG_ARGISNULL(0))
+	{
+		if (ss == NULL)
+			ereport(ERROR,
+					(errmsg("cannot use \"%s\" if \"%s\" has not been loaded with shared_preload_libraries",
+							"wait_worker_launch", "test_custom_wait_events")));
+
+		wait_event_info = ss->wait_event_info;
+	} else
+		wait_event_info = PG_GETARG_UINT32(0);
+
+	memset(&worker, 0, sizeof(worker));
+	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	worker.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	worker.bgw_restart_time = BGW_NEVER_RESTART;
+	sprintf(worker.bgw_function_name, "custom_we_worker_main");
+	sprintf(worker.bgw_library_name, "test_custom_wait_events");
+	snprintf(worker.bgw_name, BGW_MAXLEN, "test_custom_wait_events worker");
+	snprintf(worker.bgw_type, BGW_MAXLEN, "test_custom_wait_events");
+	worker.bgw_main_arg = wait_event_info;
+	worker.bgw_notify_pid = MyProcPid;
+
+	if (!RegisterDynamicBackgroundWorker(&worker, &handle))
+		PG_RETURN_NULL();
+
+	status = WaitForBackgroundWorkerStartup(handle, &pid);
+
+	if (status == BGWH_STOPPED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("could not start background process"),
+				 errhint("More details may be available in the server log.")));
+	if (status == BGWH_POSTMASTER_DIED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("cannot start background processes without postmaster"),
+				 errhint("Kill all remaining database processes and restart the database.")));
+	Assert(status == BGWH_STARTED);
+
+	PG_RETURN_INT32(pid);
+}
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.control b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
new file mode 100644
index 0000000000..8dd875a252
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
@@ -0,0 +1,3 @@
+comment = 'Test code for custom wait events'
+default_version = '1.0'
+module_pathname = '$libdir/test_custom_wait_events'
-- 
2.25.1

#29Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Masahiro Ikeda (#28)
1 attachment(s)
Re: Support to define custom wait events for extensions

On 2023-07-19 12:52, Masahiro Ikeda wrote:

Hi,

I updated the patches.
* v6-0001-Support-custom-wait-events-for-extensions.patch

I updated the patch since the cfbot found a bug.
* v7-0001-Support-custom-wait-events-for-extensions.patch

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

v7-0001-Support-custom-wait-events-for-extensions.patchtext/x-diff; name=v7-0001-Support-custom-wait-events-for-extensions.patchDownload
From e5d63913cbba9f20098252cc1e415eda4d32f5ad Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Wed, 19 Jul 2023 12:28:12 +0900
Subject: [PATCH] Support custom wait events for extensions.

To support custom wait events, it add 2 APIs to define new wait events
for extensions dynamically.

The APIs are
* WaitEventExtensionNew()
* WaitEventExtensionRegisterName()

These are similar to the existing LWLockNewTrancheId() and
LWLockRegisterTranche().

First, extensions should call WaitEventExtensionNew() to get one
or more new wait event, which IDs are allocated from a shared
counter.  Next, each individual process can use the wait event with
WaitEventExtensionRegisterName() to associate that a wait event
string to the associated name.

Note that this includes a test module, which perhaps should be split
later into its own commit.
---
 doc/src/sgml/monitoring.sgml                  |  10 +-
 doc/src/sgml/xfunc.sgml                       |  23 ++
 src/backend/storage/ipc/ipci.c                |   3 +
 .../activity/generate-wait_event_types.pl     |   7 +-
 src/backend/utils/activity/wait_event.c       | 171 +++++++++++-
 .../utils/activity/wait_event_names.txt       |   2 +-
 src/include/utils/wait_event.h                |  25 ++
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 .../test_custom_wait_events/.gitignore        |   4 +
 .../modules/test_custom_wait_events/Makefile  |  23 ++
 .../test_custom_wait_events/meson.build       |  33 +++
 .../test_custom_wait_events/t/001_basic.pl    | 137 ++++++++++
 .../test_custom_wait_events--1.0.sql          |  39 +++
 .../test_custom_wait_events.c                 | 253 ++++++++++++++++++
 .../test_custom_wait_events.control           |   3 +
 16 files changed, 718 insertions(+), 17 deletions(-)
 create mode 100644 src/test/modules/test_custom_wait_events/.gitignore
 create mode 100644 src/test/modules/test_custom_wait_events/Makefile
 create mode 100644 src/test/modules/test_custom_wait_events/meson.build
 create mode 100644 src/test/modules/test_custom_wait_events/t/001_basic.pl
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.c
 create mode 100644 src/test/modules/test_custom_wait_events/test_custom_wait_events.control

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 588b720f57..3aee5c243f 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1117,12 +1117,12 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
    <note>
     <para>
-     Extensions can add <literal>LWLock</literal> types to the list shown in
-     <xref linkend="wait-event-lwlock-table"/>.  In some cases, the name
+     Extensions can add <literal>Extension</literal> and <literal>LWLock</literal> types
+     to the list shown in <xref linkend="wait-event-extension-table"/> and
+     <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
      assigned by an extension will not be available in all server processes;
-     so an <literal>LWLock</literal> wait event might be reported as
-     just <quote><literal>extension</literal></quote> rather than the
-     extension-assigned name.
+     so an wait event might be reported as just <quote><literal>extension</literal></quote>
+     rather than the extension-assigned name.
     </para>
    </note>
  </sect2>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 9620ea9ae3..bc210cd03b 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3453,6 +3453,29 @@ if (!ptr)
     </para>
    </sect2>
 
+   <sect2 id="xfunc-addin-wait-events">
+    <title>Custom Wait Events for Add-ins</title>
+
+    <para>
+    Add-ins can define custom wait events that the wait event type is
+    <literal>Extension</literal>.
+    </para>
+    <para>
+    First, add-ins should get new one or more wait events by calling:
+<programlisting>
+    uint32 WaitEventExtensionNew(void)
+</programlisting>
+    Next, each individual process can use them to associate that
+    a wait event string to the associated name by calling:
+<programlisting>
+    void WaitEventExtensionRegisterName(uint32 wait_event_info, const char *wait_event_name);
+</programlisting>
+    An example can be found in
+    <filename>src/test/modules/test_custom_wait_events/test_custom_wait_events.c</filename>
+    in the PostgreSQL source tree.
+    </para>
+   </sect2>
+
    <sect2 id="extend-cpp">
     <title>Using C++ for Extensibility</title>
 
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index cc387c00a1..5551afffc0 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -49,6 +49,7 @@
 #include "storage/spin.h"
 #include "utils/guc.h"
 #include "utils/snapmgr.h"
+#include "utils/wait_event.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -142,6 +143,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SyncScanShmemSize());
 	size = add_size(size, AsyncShmemSize());
 	size = add_size(size, StatsShmemSize());
+	size = add_size(size, WaitEventExtensionShmemSize());
 #ifdef EXEC_BACKEND
 	size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -301,6 +303,7 @@ CreateSharedMemoryAndSemaphores(void)
 	SyncScanShmemInit();
 	AsyncShmemInit();
 	StatsShmemInit();
+	WaitEventExtensionShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/utils/activity/generate-wait_event_types.pl b/src/backend/utils/activity/generate-wait_event_types.pl
index f63c991051..56335e8730 100644
--- a/src/backend/utils/activity/generate-wait_event_types.pl
+++ b/src/backend/utils/activity/generate-wait_event_types.pl
@@ -133,10 +133,11 @@ if ($gen_code)
 	foreach my $waitclass (sort { uc($a) cmp uc($b) } keys %hashwe)
 	{
 
-		# Don't generate .c and .h files for LWLock and Lock, these are
-		# handled independently.
+		# Don't generate .c and .h files for Extension, LWLock and
+		# Lock, these are handled independently.
 		next
-		  if ( $waitclass eq 'WaitEventLWLock'
+		  if ( $waitclass eq 'WaitEventExtension'
+			|| $waitclass eq 'WaitEventLWLock'
 			|| $waitclass eq 'WaitEventLock');
 
 		my $last = $waitclass;
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 4aad11c111..03acc10407 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,15 +22,18 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
 #include "storage/lwlock.h"		/* for GetLWLockIdentifier */
+#include "storage/spin.h"
+#include "utils/memutils.h"
 #include "utils/wait_event.h"
 
 
 static const char *pgstat_get_wait_activity(WaitEventActivity w);
 static const char *pgstat_get_wait_bufferpin(WaitEventBufferPin w);
 static const char *pgstat_get_wait_client(WaitEventClient w);
-static const char *pgstat_get_wait_extension(WaitEventExtension w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
@@ -40,6 +43,162 @@ static uint32 local_my_wait_event_info;
 uint32	   *my_wait_event_info = &local_my_wait_event_info;
 
 
+
+/* dynamic allocation counter for custom wait events for extensions */
+typedef struct WaitEventExtensionCounter
+{
+	int nextId;		/* next ID to assign */
+	slock_t	mutex;  /* protects the counter only */
+} WaitEventExtensionCounter;
+
+/* pointer to the shared memory */
+static WaitEventExtensionCounter *waitEventExtensionCounter;
+
+/* first event ID of custom wait events for extensions */
+#define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
+	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
+
+/*
+ * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event IDs known to the current
+ * process.  Any unused entries in the array will contain NULL.
+ */
+static const char **WaitEventExtensionNames = NULL;
+static int	WaitEventExtensionNamesAllocated = 0;
+
+static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
+
+/*
+ *  Return the space for dynamic allocation counter.
+ */
+Size
+WaitEventExtensionShmemSize(void)
+{
+	return sizeof(WaitEventExtensionCounter);
+}
+
+/*
+ * Allocate shmem space for dynamic allocation counter.
+ */
+void
+WaitEventExtensionShmemInit(void)
+{
+	bool	found;
+
+	if (!IsUnderPostmaster)
+	{
+		/* Allocate space in shared memory. */
+		waitEventExtensionCounter = (WaitEventExtensionCounter *)
+			ShmemInitStruct("waitEventExtensionCounter", WaitEventExtensionShmemSize(), &found);
+		if (found)
+			return;
+
+		/* Initialize the dynamic-allocation counter and the spinlock. */
+		waitEventExtensionCounter->nextId = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+		SpinLockInit(&waitEventExtensionCounter->mutex);
+	}
+}
+
+/*
+ * Allocate a new event ID and return the wait event.
+ */
+uint32
+WaitEventExtensionNew(void)
+{
+	uint16		eventId;
+
+	SpinLockAcquire(&waitEventExtensionCounter->mutex);
+
+	/* Check for the counter overflow. */
+	if (waitEventExtensionCounter->nextId > PG_UINT16_MAX)
+	{
+		SpinLockRelease(&waitEventExtensionCounter->mutex);
+		ereport(ERROR,
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("too many wait events for extensions"));
+	}
+
+	eventId = waitEventExtensionCounter->nextId++;
+
+	SpinLockRelease(&waitEventExtensionCounter->mutex);
+
+	return PG_WAIT_EXTENSION | eventId;
+}
+
+/*
+ * Register a dynamic wait event name for extension in the lookup table
+ * of the current process.
+ *
+ * This routine will save a pointer to the wait event name passed as an argument,
+ * so the name should be allocated in a backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+WaitEventExtensionRegisterName(uint32 wait_event_info,
+								const char *wait_event_name)
+{
+	uint16		eventId;
+
+	eventId = wait_event_info & 0x0000FFFF;
+
+	/* Check the wait event class. */
+	Assert((wait_event_info & 0xFF000000) == PG_WAIT_EXTENSION);
+
+	/* This should only be called for user-defined wait event. */
+	Assert(eventId >= NUM_BUILTIN_WAIT_EVENT_EXTENSION);
+
+	/* Convert to array index. */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	/* If necessary, create or enlarge array. */
+	if (eventId >= WaitEventExtensionNamesAllocated)
+	{
+		int			newalloc;
+
+		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
+
+		if (WaitEventExtensionNames == NULL)
+			WaitEventExtensionNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		else
+			WaitEventExtensionNames =
+				repalloc0_array(WaitEventExtensionNames, const char *, WaitEventExtensionNamesAllocated, newalloc);
+		WaitEventExtensionNamesAllocated = newalloc;
+	}
+
+	WaitEventExtensionNames[eventId] = wait_event_name;
+}
+
+/*
+ * Return the name of an wait event ID for extension.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)
+{
+	/* Built-in event? */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It's an user-defined wait event, so look in WaitEventExtensionNames[].
+	 * However, it's possible that the name has never been registered by
+	 * calling WaitEventExtensionRegisterName() in the current process, in
+	 * which case give up and return "extension".
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= WaitEventExtensionNamesAllocated ||
+		WaitEventExtensionNames[eventId] == NULL)
+		return "extension";
+
+	return WaitEventExtensionNames[eventId];
+}
+
+
 /*
  * Configure wait event reporting to report wait events to *wait_event_info.
  * *wait_event_info needs to be valid until pgstat_reset_wait_event_storage()
@@ -149,6 +308,9 @@ pgstat_get_wait_event(uint32 wait_event_info)
 		case PG_WAIT_LOCK:
 			event_name = GetLockNameFromTagType(eventId);
 			break;
+		case PG_WAIT_EXTENSION:
+			event_name = GetWaitEventExtensionIdentifier(eventId);
+			break;
 		case PG_WAIT_BUFFERPIN:
 			{
 				WaitEventBufferPin w = (WaitEventBufferPin) wait_event_info;
@@ -170,13 +332,6 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_client(w);
 				break;
 			}
-		case PG_WAIT_EXTENSION:
-			{
-				WaitEventExtension w = (WaitEventExtension) wait_event_info;
-
-				event_name = pgstat_get_wait_extension(w);
-				break;
-			}
 		case PG_WAIT_IPC:
 			{
 				WaitEventIPC w = (WaitEventIPC) wait_event_info;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3fabad96d9..2ea4789b00 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -261,7 +261,7 @@ WAIT_EVENT_BUFFER_PIN	BufferPin	"Waiting to acquire an exclusive pin on a buffer
 
 Section: ClassName - WaitEventExtension
 
-WAIT_EVENT_EXTENSION	Extension	"Waiting in an extension."
+WAIT_EVENT_DOCONLY	Extension	"Waiting in an extension."
 
 #
 # Wait events - LWLock
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 4517425f84..ae3dac88a4 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -67,6 +67,31 @@ pgstat_report_wait_start(uint32 wait_event_info)
 	*(volatile uint32 *) my_wait_event_info = wait_event_info;
 }
 
+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define their wait events. First, extensions should call
+ * WaitEventExtensionNew() to get one or more wait events, which IDs are
+ * allocated from a shared counter.  Next, each individual process can use
+ * them with WaitEventExtensionRegisterName() to associate that a wait
+ * event string to the associated name.
+ */
+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+} WaitEventExtension;
+
+extern void WaitEventExtensionShmemInit(void);
+extern Size WaitEventExtensionShmemSize(void);
+
+extern uint32 WaitEventExtensionNew(void);
+extern void WaitEventExtensionRegisterName(uint32 wait_event_info,
+								const char *wait_event_name);
+
 /* ----------
  * pgstat_report_wait_end() -
  *
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 6331c976dc..84312b7e57 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -17,6 +17,7 @@ SUBDIRS = \
 		  test_bloomfilter \
 		  test_copy_callbacks \
 		  test_custom_rmgrs \
+		  test_custom_wait_events \
 		  test_ddl_deparse \
 		  test_extensions \
 		  test_ginpostinglist \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 17d369e378..83a1d8fd19 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -14,6 +14,7 @@ subdir('ssl_passphrase_callback')
 subdir('test_bloomfilter')
 subdir('test_copy_callbacks')
 subdir('test_custom_rmgrs')
+subdir('test_custom_wait_events')
 subdir('test_ddl_deparse')
 subdir('test_extensions')
 subdir('test_ginpostinglist')
diff --git a/src/test/modules/test_custom_wait_events/.gitignore b/src/test/modules/test_custom_wait_events/.gitignore
new file mode 100644
index 0000000000..5dcb3ff972
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_custom_wait_events/Makefile b/src/test/modules/test_custom_wait_events/Makefile
new file mode 100644
index 0000000000..dda365d523
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_custom_wait_events/Makefile
+
+MODULE_big = test_custom_wait_events
+OBJS = \
+	$(WIN32RES) \
+	test_custom_wait_events.o
+PGFILEDESC = "test_custom_wait_events - test custom wait events"
+
+EXTENSION = test_custom_wait_events
+DATA = test_custom_wait_events--1.0.sql
+
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_custom_wait_events
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_custom_wait_events/meson.build b/src/test/modules/test_custom_wait_events/meson.build
new file mode 100644
index 0000000000..8ad073e577
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) PostgreSQL Global Development Group
+
+test_custom_wait_events = files(
+  'test_custom_wait_events.c',
+)
+
+if host_system == 'windows'
+  test_custom_wait_events += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_custom_wait_events',
+    '--FILEDESC', 'test_custom_wait_events - test custom wait events',])
+endif
+
+test_custom_wait_events = shared_module('test_custom_wait_events',
+  test_custom_wait_events,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_custom_wait_events
+
+test_install_data += files(
+  'test_custom_wait_events.control',
+  'test_custom_wait_events--1.0.sql',
+)
+
+tests += {
+  'name': 'test_custom_wait_events',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_basic.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_custom_wait_events/t/001_basic.pl b/src/test/modules/test_custom_wait_events/t/001_basic.pl
new file mode 100644
index 0000000000..6817eaa2b0
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/t/001_basic.pl
@@ -0,0 +1,137 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+
+# Check that a new wait event can be defined in initialized phase.
+$node->init;
+$node->append_conf(
+	'postgresql.conf',
+	"shared_preload_libraries = 'test_custom_wait_events'"
+);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_custom_wait_events;');
+
+$node->safe_psql('postgres', 'SELECT start_bgworker_waiting_forever();');
+
+ok($node->poll_query_until('postgres',
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND wait_event = 'custom_wait_event'
+		) = 1;]),
+	"registering custom wait event in initialized phase doen't work"
+);
+
+$node->stop;
+$node->clean_node;
+
+
+# Check that multiple new wait events can be defined dynamically.
+$node->init;
+$node->append_conf(
+	'postgresql.conf',
+);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_custom_wait_events;');
+
+# Start new background workers. They wait with new wait events dinamically defined.
+my $wait_event_info_1 = $node->safe_psql('postgres', 'SELECT get_new_wait_event_info();');
+$node->safe_psql('postgres', "SELECT start_bgworker_waiting_forever($wait_event_info_1);");
+my $wait_event_info_2 = $node->safe_psql('postgres', 'SELECT get_new_wait_event_info();');
+$node->safe_psql('postgres', "SELECT start_bgworker_waiting_forever($wait_event_info_2);");
+
+# Since the new wait event names are not registered in current process now,
+# 'wait_event' must be 'extension' which means that it's not registered.
+ok($node->poll_query_until(
+	'postgres',
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND wait_event = 'extension'
+		) = 2;]),
+	"unregisterd wait event names are wrong"
+);
+
+# Register wait event names and check that they will be visible.
+my $new_wait_event_name_1 = 'new_dynamic_wait_event_1';
+my $new_wait_event_name_2 = 'new_dynamic_wait_event_2';
+my $session = $node->background_psql('postgres');
+$session->query_safe(
+	"SELECT register_wait_event_name($wait_event_info_1, '$new_wait_event_name_1');");
+$session->query_safe(
+	"SELECT register_wait_event_name($wait_event_info_2, '$new_wait_event_name_2');");
+my $res = $session->query_safe(
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND (wait_event = '$new_wait_event_name_1')
+		) = 1;
+]);
+like($res, qr/^t$/m, "$new_wait_event_name_1 dynamically registered wasn't found");
+$res = $session->query_safe(
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND (wait_event = '$new_wait_event_name_2')
+		) = 1;
+]);
+like($res, qr/^t$/m, "$new_wait_event_name_2 dynamically registered wasn't found");
+$session->quit;
+
+$node->stop;
+$node->clean_node;
+
+
+# Check the boundary condition. An error should happen if the number of wait
+# events for extension exceeds the limits.
+$node->init;
+$node->append_conf(
+	'postgresql.conf',
+);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_custom_wait_events;');
+
+# skip to the maximum number of wait event for extension. "$uint16_max" can
+# be allocated since the first wait event is reserved by core.
+my $uint16_max=65535;
+$node->safe_psql('postgres',
+	"SELECT COUNT(get_new_wait_event_info()) FROM generate_series(1, $uint16_max - 1)");
+
+# can use the maximum one
+my $wait_event_info = $node->safe_psql('postgres', 'SELECT get_new_wait_event_info();');
+$node->safe_psql('postgres', "SELECT start_bgworker_waiting_forever($wait_event_info);");
+ok($node->poll_query_until(
+	'postgres',
+	qq[SELECT
+		(SELECT count(*) FROM pg_stat_activity
+			WHERE backend_type = 'test_custom_wait_events'
+			  AND wait_event_type = 'Extension'
+			  AND wait_event = 'extension'
+		) = 1;]),
+	"unregisterd wait event name is wrong"
+);
+
+# but, next call should output an error.
+my ($ret, $stdout, $stderr) =
+	$node->psql('postgres', 'SELECT get_new_wait_event_info();');
+like(
+	$stderr,
+	qr/ERROR:  too many wait events for extensions/,
+	'expected error when the number of wait events for extensions exceeds the limits'
+);
+
+$node->stop;
+$node->clean_node;
+
+done_testing();
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
new file mode 100644
index 0000000000..df916d4d5e
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql
@@ -0,0 +1,39 @@
+/* src/test/modules/test_custom_wait_events/test_custom_wait_events--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_custom_wait_events" to load this file. \quit
+
+--
+-- get_new_wait_event_info()
+--
+-- Dynamically get a new wait event information.
+--
+CREATE FUNCTION get_new_wait_event_info()
+RETURNS int4
+AS 'MODULE_PATHNAME', 'get_new_wait_event_info'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- register_wait_event_name()
+--
+-- Dynamically register a wait event name.
+--
+CREATE FUNCTION register_wait_event_name(
+    wait_event_info int4, wait_event_name text
+)
+RETURNS void
+AS 'MODULE_PATHNAME', 'register_wait_event_name'
+LANGUAGE C STRICT PARALLEL SAFE;
+
+--
+-- start_bgworker_waiting_forever()
+--
+-- Dynamically start a background worker waiting forever
+-- with the specified custom wait event.
+--
+CREATE FUNCTION start_bgworker_waiting_forever(
+    wait_event_info int4 DEFAULT NULL
+)
+RETURNS int4
+AS 'MODULE_PATHNAME', 'start_bgworker_waiting_forever'
+LANGUAGE C PARALLEL SAFE;
\ No newline at end of file
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.c b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
new file mode 100644
index 0000000000..7527d25f8f
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.c
@@ -0,0 +1,253 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_custom_wait_events.c
+ *		Code for testing custom wait events for extensions
+ *
+ * This code has some functions to define and register new custom wait
+ * events, and to launch a background worker waiting forever with the
+ * specified wait event.
+ *
+ * You can define them not only in initialized phases but also dynamically
+ * via calling user functions. A new wait event "custom_wait_event" is
+ * defined and registered in shmem_startup_hook(). It will be saved in
+ * shared memory. Users can also define and register new custom wait events
+ * via get_new_wait_event_info() and register_wait_event_name() dynamically.
+ *
+ * You can use the wait event for a background worker launched in
+ * start_bgworker_waiting_forever(). The reason why it starts background
+ * workers is for tap tests. It enables to check the wait events on
+ * pg_stat_activity from other processes.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_custom_wait_events/test_custom_wait_events.c
+ *
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/interrupt.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "storage/lwlock.h"
+#include "storage/proc.h"
+#include "storage/shmem.h"
+#include "tcop/utility.h"
+#include "utils/builtins.h"
+#include "utils/memutils.h"
+#include "utils/wait_event.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(get_new_wait_event_info);
+PG_FUNCTION_INFO_V1(register_wait_event_name);
+PG_FUNCTION_INFO_V1(start_bgworker_waiting_forever);
+
+PGDLLEXPORT void custom_we_worker_main(Datum main_arg);
+
+/* GUC variables */
+typedef struct CustomWaitEventState
+{
+	uint32			wait_event_info; 		 /* the wait event defined in initialized phase */
+} CustomWaitEventState;
+static CustomWaitEventState *ss = NULL;      /* the pointer to the shared memory */
+
+static char *wait_event_name;                /* the wait event name dynamically registered  */
+static char *worker_database = "postgres";   /* connected by a background worker */
+
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_request_hook_type prev_shmem_startup_hook = NULL;
+
+static void custom_we_shmem_request(void);
+static void custom_we_shmem_startup(void);
+static Size custom_we_memsize(void);
+
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = custom_we_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = custom_we_shmem_startup;
+}
+
+static void
+custom_we_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(custom_we_memsize());
+}
+
+static Size
+custom_we_memsize(void)
+{
+	return sizeof(CustomWaitEventState);
+}
+
+/*
+ * Define and register a new wait event.
+ *
+ * This routine defines a new wait event when it's called first time.
+ * After that, the name of the wait event is associated with it. It's
+ * registered in the lookup table of each processes.
+ */
+static void
+custom_we_shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	ss = NULL;
+
+	/* Create or attach to the shared memory state */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+	ss = ShmemInitStruct("custom_wait_event",
+						 sizeof(CustomWaitEventState),
+						 &found);
+
+	/* Define a new wait event. */
+	if (!found)
+		ss->wait_event_info = WaitEventExtensionNew();
+
+	LWLockRelease(AddinShmemInitLock);
+
+	/*
+	 * Register the wait event in the lookup table of the current process.
+	 */
+	WaitEventExtensionRegisterName(ss->wait_event_info,
+									  "custom_wait_event");
+
+	return;
+}
+
+/*
+ * Connect to the database and wait forever with the wait event
+ * specified in first argument.
+ */
+void
+custom_we_worker_main(Datum main_arg)
+{
+	uint32 wait_event_info;
+	wait_event_info = DatumGetUInt32(main_arg);
+
+	pqsignal(SIGTERM, SignalHandlerForShutdownRequest);
+	BackgroundWorkerUnblockSignals();
+
+	/* Connect to our database to show up a wait event in pg_stat_activity */
+	BackgroundWorkerInitializeConnection(worker_database, NULL, 0);
+
+	/* Wait forever with the specified wait event */
+	while (!ShutdownRequestPending)
+	{
+		(void) WaitLatch(MyLatch,
+						 WL_LATCH_SET | WL_EXIT_ON_PM_DEATH,
+						 -1L,
+						 wait_event_info);
+		ResetLatch(MyLatch);
+	}
+}
+
+/*
+ * Dynamically get and return a new wait event.
+ */
+Datum
+get_new_wait_event_info(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_UINT32(WaitEventExtensionNew());
+}
+
+/*
+ * Dynamically register a wait event name in the lookup table of
+ * current process.
+ */
+Datum
+register_wait_event_name(PG_FUNCTION_ARGS)
+{
+	uint32 wait_event_info;
+	char *wait_event_name_arg;
+	int size;
+
+	wait_event_info = PG_GETARG_UINT32(0);
+
+	/*
+	 * Copy to the name in a backend-lifetime context since
+	 * WaitEventExtensionRegisterName() doesn't copy the name.
+	 */
+	wait_event_name_arg = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	size = strlen(wait_event_name_arg) + 1;
+	Assert(size <= NAMEDATALEN);
+	wait_event_name = (char *) MemoryContextAlloc(TopMemoryContext, size);
+	strlcpy(wait_event_name, wait_event_name_arg, size);
+
+	WaitEventExtensionRegisterName(wait_event_info, wait_event_name);
+
+	PG_RETURN_VOID();
+}
+
+/*
+ * Dynamically start a background worker waiting forever
+ * with the custom wait event specified in the first
+ * argument. If the argument is NULL, it will wait with
+ * the wait event defined in initialized phase.
+ */
+Datum
+start_bgworker_waiting_forever(PG_FUNCTION_ARGS)
+{
+	uint32 wait_event_info;
+	BackgroundWorker worker;
+	BackgroundWorkerHandle *handle;
+	BgwHandleStatus status;
+	pid_t		pid;
+
+	if (PG_ARGISNULL(0))
+	{
+		if (ss == NULL)
+			ereport(ERROR,
+					(errmsg("cannot use \"%s\" if \"%s\" has not been loaded with shared_preload_libraries",
+							"wait_worker_launch", "test_custom_wait_events")));
+
+		wait_event_info = ss->wait_event_info;
+	} else
+		wait_event_info = PG_GETARG_UINT32(0);
+
+	memset(&worker, 0, sizeof(worker));
+	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	worker.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	worker.bgw_restart_time = BGW_NEVER_RESTART;
+	sprintf(worker.bgw_function_name, "custom_we_worker_main");
+	sprintf(worker.bgw_library_name, "test_custom_wait_events");
+	snprintf(worker.bgw_name, BGW_MAXLEN, "test_custom_wait_events worker");
+	snprintf(worker.bgw_type, BGW_MAXLEN, "test_custom_wait_events");
+	worker.bgw_main_arg = wait_event_info;
+	worker.bgw_notify_pid = MyProcPid;
+
+	if (!RegisterDynamicBackgroundWorker(&worker, &handle))
+		PG_RETURN_NULL();
+
+	status = WaitForBackgroundWorkerStartup(handle, &pid);
+
+	if (status == BGWH_STOPPED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("could not start background process"),
+				 errhint("More details may be available in the server log.")));
+	if (status == BGWH_POSTMASTER_DIED)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+				 errmsg("cannot start background processes without postmaster"),
+				 errhint("Kill all remaining database processes and restart the database.")));
+	Assert(status == BGWH_STARTED);
+
+	PG_RETURN_INT32(pid);
+}
diff --git a/src/test/modules/test_custom_wait_events/test_custom_wait_events.control b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
new file mode 100644
index 0000000000..8dd875a252
--- /dev/null
+++ b/src/test/modules/test_custom_wait_events/test_custom_wait_events.control
@@ -0,0 +1,3 @@
+comment = 'Test code for custom wait events'
+default_version = '1.0'
+module_pathname = '$libdir/test_custom_wait_events'
-- 
2.25.1

#30Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#28)
Re: Support to define custom wait events for extensions

On Wed, Jul 19, 2023 at 12:52:10PM +0900, Masahiro Ikeda wrote:

I would like to change the wait event name of contrib modules for example
postgres_fdw. But, I think it's better to do so after the APIs are
committed.

Agreed to do things one step at a time here. Let's focus on the core
APIs and facilities first.
--
Michael

#31Bharath Rupireddy
bharath.rupireddyforpostgres@gmail.com
In reply to: Masahiro Ikeda (#29)
Re: Support to define custom wait events for extensions

On Wed, Jul 19, 2023 at 11:49 AM Masahiro Ikeda
<ikedamsh@oss.nttdata.com> wrote:

I updated the patch since the cfbot found a bug.
* v7-0001-Support-custom-wait-events-for-extensions.patch

Thanks for working on this feature. +1. I've wanted this capability
for a while because extensions have many different wait loops for
different reasons, a single wait event type isn't enough.

I think we don't need a separate test extension for demonstrating use
of custom wait events, you can leverage the sample extension
worker_spi because that's where extension authors look for while
writing a new extension. Also, it simplifies your patch a lot. I don't
mind adding a few things to worker_spi for the sake of demonstrating
the use and testing for custom wait events.

--
Bharath Rupireddy
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

#32Tristan Partin
tristan@neon.tech
In reply to: Masahiro Ikeda (#29)
Re: Support to define custom wait events for extensions

Thanks for continuing to work on this patchset. I only have
prose-related comments.

To support custom wait events, it add 2 APIs to define new wait events
for extensions dynamically.

Remove the "it" here.

The APIs are
* WaitEventExtensionNew()
* WaitEventExtensionRegisterName()

These are similar to the existing LWLockNewTrancheId() and
LWLockRegisterTranche().

This sentence seems like it could be removed given the API names have
changed during the development of this patch.

First, extensions should call WaitEventExtensionNew() to get one
or more new wait event, which IDs are allocated from a shared
counter. Next, each individual process can use the wait event with
WaitEventExtensionRegisterName() to associate that a wait event
string to the associated name.

This portion of the commit message is a copy-paste of the function
comment. Whatever you do in the function comment (which I commented on
below), just do here as well.

+     so an wait event might be reported as just <quote><literal>extension</literal></quote>
+     rather than the extension-assigned name.

s/an/a

+   <sect2 id="xfunc-addin-wait-events">
+    <title>Custom Wait Events for Add-ins</title>

This would be the second use of "Add-ins" ever, according to my search.
Should this be "Extensions" instead?

+    <para>
+    Add-ins can define custom wait events that the wait event type is

s/that/where

+    <literal>Extension</literal>.
+    </para>
+    <para>
+    First, add-ins should get new one or more wait events by calling:

"one or more" doesn't seem to make sense grammatically here.

+<programlisting>
+    uint32 WaitEventExtensionNew(void)
+</programlisting>
+    Next, each individual process can use them to associate that

Remove "that".

+    a wait event string to the associated name by calling:
+<programlisting>
+    void WaitEventExtensionRegisterName(uint32 wait_event_info, const char *wait_event_name);
+</programlisting>
+    An example can be found in
+    <filename>src/test/modules/test_custom_wait_events/test_custom_wait_events.c</filename>
+    in the PostgreSQL source tree.
+    </para>
+   </sect2>
+ * Register a dynamic wait event name for extension in the lookup table
+ * of the current process.

Inserting an "an" before "extension" would make this read better.

+/*
+ * Return the name of an wait event ID for extension.
+ */

s/an/a

+       /*
+        * It's an user-defined wait event, so look in WaitEventExtensionNames[].
+        * However, it's possible that the name has never been registered by
+        * calling WaitEventExtensionRegisterName() in the current process, in
+        * which case give up and return "extension".
+        */

s/an/a

"extension" seems very similar to "Extension". Instead of returning a
string here, could we just error? This seems like a programmer error on
the part of the extension author.

+ * Extensions can define their wait events. First, extensions should call
+ * WaitEventExtensionNew() to get one or more wait events, which IDs are
+ * allocated from a shared counter.  Next, each individual process can use
+ * them with WaitEventExtensionRegisterName() to associate that a wait
+ * event string to the associated name.

An "own" before "wait events" in the first sentence would increase
clarity. "where" instead of "which" in the next sentence. Remove "that"
after "associate" in the third sentence.

--
Tristan Partin
Neon (https://neon.tech)

#33Tristan Partin
tristan@neon.tech
In reply to: Michael Paquier (#18)
Re: Support to define custom wait events for extensions

On Tue Jul 11, 2023 at 6:29 PM CDT, Michael Paquier wrote:

On Tue, Jul 11, 2023 at 12:39:52PM -0500, Tristan Partin wrote:

Given the context of our last conversation, I assume this code was
copied from somewhere else. Since this is new code, I think it would
make more sense if newalloc was a uint16 or size_t.

This style comes from LWLockRegisterTranche() in lwlock.c. Do you
think that it would be more adapted to change that to
pg_nextpower2_size_t() with a Size? We could do that for the existing
code on HEAD as an improvement.

Yes, I think that would be the most correct code. At the very least,
using a uint32 instead of an int, would be preferrable.

From what I understand, Neon differs from upstream in some way related
to this patch. I am trying to ascertain how that is. I hope to provide
more feedback when I learn more about it.

Hmm, okay, that would nice to hear about to make sure that the
approach taken on this thread is able to cover what you are looking
for. So you mean that Neon has been using something similar to
register wait events in the backend? Last time I looked at the Neon
repo, I did not get the impression that there was a custom patch for
Postgres in this area. All the in-core code paths using
WAIT_EVENT_EXTENSION would gain from the APIs added here, FWIW.

I did some investigation into the Neon fork[0]https://github.com/neondatabase/postgres, and couldn't find any
commits that seemed related. Perhaps this is on our wishlist instead of
something we already have implemented. I have CCed Heikki to talk some
more about how this would fit in at Neon.

[0]: https://github.com/neondatabase/postgres

--
Tristan Partin
Neon (https://neon.tech)

#34Michael Paquier
michael@paquier.xyz
In reply to: Tristan Partin (#33)
Re: Support to define custom wait events for extensions

On Wed, Jul 19, 2023 at 11:16:34AM -0500, Tristan Partin wrote:

On Tue Jul 11, 2023 at 6:29 PM CDT, Michael Paquier wrote:

This style comes from LWLockRegisterTranche() in lwlock.c. Do you
think that it would be more adapted to change that to
pg_nextpower2_size_t() with a Size? We could do that for the existing
code on HEAD as an improvement.

Yes, I think that would be the most correct code. At the very least,
using a uint32 instead of an int, would be preferrable.

Would you like to send a patch on a new thread about that?

Hmm, okay, that would nice to hear about to make sure that the
approach taken on this thread is able to cover what you are looking
for. So you mean that Neon has been using something similar to
register wait events in the backend? Last time I looked at the Neon
repo, I did not get the impression that there was a custom patch for
Postgres in this area. All the in-core code paths using
WAIT_EVENT_EXTENSION would gain from the APIs added here, FWIW.

I did some investigation into the Neon fork[0], and couldn't find any
commits that seemed related. Perhaps this is on our wishlist instead of
something we already have implemented. I have CCed Heikki to talk some
more about how this would fit in at Neon.

[0]: https://github.com/neondatabase/postgres

Anybody with complex out-of-core extensions have wanted more
monitoring capabilities for wait events without relying on the core
backend. To be honest, I would not be surprised to see this stuff on
more than one wishlist.
--
Michael

#35Michael Paquier
michael@paquier.xyz
In reply to: Tristan Partin (#32)
1 attachment(s)
Re: Support to define custom wait events for extensions

On Wed, Jul 19, 2023 at 10:57:39AM -0500, Tristan Partin wrote:

+   <sect2 id="xfunc-addin-wait-events">
+    <title>Custom Wait Events for Add-ins</title>

This would be the second use of "Add-ins" ever, according to my search.
Should this be "Extensions" instead?

Yes, I would think that just "Custom Wait Events" is enough here.
And I'd recommend to also use Shared Memory here. The case of
dynamically loaded things is possible, more advanced and can work, but
I am not sure we really need to do down to that as long as we mention
to use shared_preload_libraries.

I've rewritten the docs in their entirety, but honestly I still need
to spend more time polishing that.

Another part of the patch that has been itching me a lot are the
regression tests. I have spent some time today migrating the tests of
worker_spi to TAP for the sake of this thread, resulting in commit
320c311, and concluded that we need to care about three new cases:
- For custom wait events where the shmem state is not loaded, check
that we report the default of 'extension'.
- Check that it is possible to allocate and load a custom wait event
dynamically. Here, I have used a new SQL function in worker_spi,
called worker_spi_init(). That feels a bit hack-ish but for a test in
a template module that works great.
- Check that wait events loaded through shared_preload_libraries work
correctly.

The tests of worker_spi can take care easily of all these cases, once
a few things for the shmem handling are put in place for the dynamic
and preloading cases.

+Datum
+get_new_wait_event_info(PG_FUNCTION_ARGS)
+{
+       PG_RETURN_UINT32(WaitEventExtensionNew());
+}

While looking at the previous patch and the test, I've noticed this
pattern. WaitEventExtensionNew() should not be called without holding
AddinShmemInitLock, or that opens the door to race conditions.

I am still mid-way through the review of the core APIs, but attached
is my current version in progress, labelled v8. I'll continue
tomorrow. I'm aware of some typos in the commit message of this
patch, and the dynamic bgworker launch is failing in the CI for
VS2019 (just too tired to finish debugging that today).

Thoughts are welcome.
--
Michael

Attachments:

v8-0001-Support-custom-wait-events-for-extensions.patchtext/x-diff; charset=us-asciiDownload
From deb7cab66671e0277cc34236c7515f4ea18fac65 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <masahiro.ikeda.us@hco.ntt.co.jp>
Date: Wed, 19 Jul 2023 12:28:12 +0900
Subject: [PATCH v8] Support custom wait events for extensions.

To support custom wait events, it add 2 APIs to define new wait events
for extensions dynamically.

The APIs are
* WaitEventExtensionNew()
* WaitEventExtensionRegisterName()

These are similar to the existing LWLockNewTrancheId() and
LWLockRegisterTranche().

First, extensions should call WaitEventExtensionNew() to get one
or more new wait event, which IDs are allocated from a shared
counter.  Next, each individual process can use the wait event with
WaitEventExtensionRegisterName() to associate that a wait event
string to the associated name.

Note that this includes an example of how to use this new facility in
worker_spi.
---
 src/include/utils/wait_event.h                |  25 +++
 src/backend/storage/ipc/ipci.c                |   3 +
 .../activity/generate-wait_event_types.pl     |   7 +-
 src/backend/utils/activity/wait_event.c       | 171 +++++++++++++++++-
 .../utils/activity/wait_event_names.txt       |   2 +-
 .../modules/worker_spi/t/001_worker_spi.pl    |  31 +++-
 .../modules/worker_spi/worker_spi--1.0.sql    |   5 +
 src/test/modules/worker_spi/worker_spi.c      | 105 ++++++++++-
 doc/src/sgml/monitoring.sgml                  |  13 +-
 doc/src/sgml/xfunc.sgml                       |  34 ++++
 10 files changed, 370 insertions(+), 26 deletions(-)

diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 4517425f84..d405398b75 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -67,6 +67,31 @@ pgstat_report_wait_start(uint32 wait_event_info)
 	*(volatile uint32 *) my_wait_event_info = wait_event_info;
 }
 
+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define their wait events. First, extensions should call
+ * WaitEventExtensionNew() to get one or more wait events, which IDs are
+ * allocated from a shared counter.  Next, each individual process can use
+ * them with WaitEventExtensionRegisterName() to associate that a wait
+ * event string to the associated name.
+ */
+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+} WaitEventExtension;
+
+extern void WaitEventExtensionShmemInit(void);
+extern Size WaitEventExtensionShmemSize(void);
+
+extern uint32 WaitEventExtensionNew(void);
+extern void WaitEventExtensionRegisterName(uint32 wait_event_info,
+										   const char *wait_event_name);
+
 /* ----------
  * pgstat_report_wait_end() -
  *
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index cc387c00a1..5551afffc0 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -49,6 +49,7 @@
 #include "storage/spin.h"
 #include "utils/guc.h"
 #include "utils/snapmgr.h"
+#include "utils/wait_event.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -142,6 +143,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SyncScanShmemSize());
 	size = add_size(size, AsyncShmemSize());
 	size = add_size(size, StatsShmemSize());
+	size = add_size(size, WaitEventExtensionShmemSize());
 #ifdef EXEC_BACKEND
 	size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -301,6 +303,7 @@ CreateSharedMemoryAndSemaphores(void)
 	SyncScanShmemInit();
 	AsyncShmemInit();
 	StatsShmemInit();
+	WaitEventExtensionShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/utils/activity/generate-wait_event_types.pl b/src/backend/utils/activity/generate-wait_event_types.pl
index f63c991051..56335e8730 100644
--- a/src/backend/utils/activity/generate-wait_event_types.pl
+++ b/src/backend/utils/activity/generate-wait_event_types.pl
@@ -133,10 +133,11 @@ if ($gen_code)
 	foreach my $waitclass (sort { uc($a) cmp uc($b) } keys %hashwe)
 	{
 
-		# Don't generate .c and .h files for LWLock and Lock, these are
-		# handled independently.
+		# Don't generate .c and .h files for Extension, LWLock and
+		# Lock, these are handled independently.
 		next
-		  if ( $waitclass eq 'WaitEventLWLock'
+		  if ( $waitclass eq 'WaitEventExtension'
+			|| $waitclass eq 'WaitEventLWLock'
 			|| $waitclass eq 'WaitEventLock');
 
 		my $last = $waitclass;
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 4aad11c111..9e7e65bb01 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,15 +22,18 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
 #include "storage/lwlock.h"		/* for GetLWLockIdentifier */
+#include "storage/spin.h"
+#include "utils/memutils.h"
 #include "utils/wait_event.h"
 
 
 static const char *pgstat_get_wait_activity(WaitEventActivity w);
 static const char *pgstat_get_wait_bufferpin(WaitEventBufferPin w);
 static const char *pgstat_get_wait_client(WaitEventClient w);
-static const char *pgstat_get_wait_extension(WaitEventExtension w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
@@ -40,6 +43,162 @@ static uint32 local_my_wait_event_info;
 uint32	   *my_wait_event_info = &local_my_wait_event_info;
 
 
+
+/* dynamic allocation counter for custom wait events for extensions */
+typedef struct WaitEventExtensionCounter
+{
+	int			nextId;			/* next ID to assign */
+	slock_t		mutex;			/* protects the counter only */
+}			WaitEventExtensionCounter;
+
+/* pointer to the shared memory */
+static WaitEventExtensionCounter * waitEventExtensionCounter;
+
+/* first event ID of custom wait events for extensions */
+#define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
+	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
+
+/*
+ * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event IDs known to the current
+ * process.  Any unused entries in the array will contain NULL.
+ */
+static const char **WaitEventExtensionNames = NULL;
+static int	WaitEventExtensionNamesAllocated = 0;
+
+static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
+
+/*
+ *  Return the space for dynamic allocation counter.
+ */
+Size
+WaitEventExtensionShmemSize(void)
+{
+	return sizeof(WaitEventExtensionCounter);
+}
+
+/*
+ * Allocate shmem space for dynamic allocation counter.
+ */
+void
+WaitEventExtensionShmemInit(void)
+{
+	bool		found;
+
+	if (!IsUnderPostmaster)
+	{
+		/* Allocate space in shared memory. */
+		waitEventExtensionCounter = (WaitEventExtensionCounter *)
+			ShmemInitStruct("waitEventExtensionCounter", WaitEventExtensionShmemSize(), &found);
+		if (found)
+			return;
+
+		/* Initialize the dynamic-allocation counter and the spinlock. */
+		waitEventExtensionCounter->nextId = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+		SpinLockInit(&waitEventExtensionCounter->mutex);
+	}
+}
+
+/*
+ * Allocate a new event ID and return the wait event.
+ */
+uint32
+WaitEventExtensionNew(void)
+{
+	uint16		eventId;
+
+	SpinLockAcquire(&waitEventExtensionCounter->mutex);
+
+	/* Check for the counter overflow. */
+	if (waitEventExtensionCounter->nextId > PG_UINT16_MAX)
+	{
+		SpinLockRelease(&waitEventExtensionCounter->mutex);
+		ereport(ERROR,
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("too many wait events for extensions"));
+	}
+
+	eventId = waitEventExtensionCounter->nextId++;
+
+	SpinLockRelease(&waitEventExtensionCounter->mutex);
+
+	return PG_WAIT_EXTENSION | eventId;
+}
+
+/*
+ * Register a dynamic wait event name for extension in the lookup table
+ * of the current process.
+ *
+ * This routine will save a pointer to the wait event name passed as an argument,
+ * so the name should be allocated in a backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+WaitEventExtensionRegisterName(uint32 wait_event_info,
+							   const char *wait_event_name)
+{
+	uint16		eventId;
+
+	eventId = wait_event_info & 0x0000FFFF;
+
+	/* Check the wait event class. */
+	Assert((wait_event_info & 0xFF000000) == PG_WAIT_EXTENSION);
+
+	/* This should only be called for user-defined wait event. */
+	Assert(eventId >= NUM_BUILTIN_WAIT_EVENT_EXTENSION);
+
+	/* Convert to array index. */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	/* If necessary, create or enlarge array. */
+	if (eventId >= WaitEventExtensionNamesAllocated)
+	{
+		int			newalloc;
+
+		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
+
+		if (WaitEventExtensionNames == NULL)
+			WaitEventExtensionNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		else
+			WaitEventExtensionNames =
+				repalloc0_array(WaitEventExtensionNames, const char *, WaitEventExtensionNamesAllocated, newalloc);
+		WaitEventExtensionNamesAllocated = newalloc;
+	}
+
+	WaitEventExtensionNames[eventId] = wait_event_name;
+}
+
+/*
+ * Return the name of an wait event ID for extension.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)
+{
+	/* Built-in event? */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It's an user-defined wait event, so look in WaitEventExtensionNames[].
+	 * However, it's possible that the name has never been registered by
+	 * calling WaitEventExtensionRegisterName() in the current process, in
+	 * which case give up and return "extension".
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= WaitEventExtensionNamesAllocated ||
+		WaitEventExtensionNames[eventId] == NULL)
+		return "extension";
+
+	return WaitEventExtensionNames[eventId];
+}
+
+
 /*
  * Configure wait event reporting to report wait events to *wait_event_info.
  * *wait_event_info needs to be valid until pgstat_reset_wait_event_storage()
@@ -149,6 +308,9 @@ pgstat_get_wait_event(uint32 wait_event_info)
 		case PG_WAIT_LOCK:
 			event_name = GetLockNameFromTagType(eventId);
 			break;
+		case PG_WAIT_EXTENSION:
+			event_name = GetWaitEventExtensionIdentifier(eventId);
+			break;
 		case PG_WAIT_BUFFERPIN:
 			{
 				WaitEventBufferPin w = (WaitEventBufferPin) wait_event_info;
@@ -170,13 +332,6 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_client(w);
 				break;
 			}
-		case PG_WAIT_EXTENSION:
-			{
-				WaitEventExtension w = (WaitEventExtension) wait_event_info;
-
-				event_name = pgstat_get_wait_extension(w);
-				break;
-			}
 		case PG_WAIT_IPC:
 			{
 				WaitEventIPC w = (WaitEventIPC) wait_event_info;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3fabad96d9..2ea4789b00 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -261,7 +261,7 @@ WAIT_EVENT_BUFFER_PIN	BufferPin	"Waiting to acquire an exclusive pin on a buffer
 
 Section: ClassName - WaitEventExtension
 
-WAIT_EVENT_EXTENSION	Extension	"Waiting in an extension."
+WAIT_EVENT_DOCONLY	Extension	"Waiting in an extension."
 
 #
 # Wait events - LWLock
diff --git a/src/test/modules/worker_spi/t/001_worker_spi.pl b/src/test/modules/worker_spi/t/001_worker_spi.pl
index c293871313..f914791a64 100644
--- a/src/test/modules/worker_spi/t/001_worker_spi.pl
+++ b/src/test/modules/worker_spi/t/001_worker_spi.pl
@@ -39,6 +39,25 @@ $node->poll_query_until('postgres',
 $result = $node->safe_psql('postgres', 'SELECT * FROM schema4.counted;');
 is($result, qq(total|1), 'dynamic bgworker correctly consumed tuple data');
 
+# Check that the wait event used by the dynamic bgworker.  For a session
+# not without the state in shared memory known, the default of "Extension"
+# is the value waited on.
+$result = $node->poll_query_until(
+	'postgres',
+	qq[SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
+	'extension');
+is($result, 1, 'dynamic bgworker has reported "extension" as wait event');
+# If the shared memory state is loaded, the wait event is a custom one.
+# The expected result is a special pattern here with a newline being first
+# as the initialization of the shared memory state is enforced.
+$result = $node->poll_query_until(
+	'postgres',
+	qq[SELECT worker_spi_init(); SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
+	qq[
+worker_spi_main]);
+is($result, 1,
+	'dynamic bgworker has reported "worker_spi_main" as wait event');
+
 note "testing bgworkers loaded with shared_preload_libraries";
 
 # Create the database first so as the workers can connect to it when
@@ -58,9 +77,9 @@ $node->restart;
 # Check that bgworkers have been registered and launched.
 ok( $node->poll_query_until(
 		'mydb',
-		qq[SELECT datname, count(datname) FROM pg_stat_activity
-            WHERE backend_type = 'worker_spi' GROUP BY datname;],
-		'mydb|3'),
+		qq[SELECT datname, count(datname), wait_event FROM pg_stat_activity
+            WHERE backend_type = 'worker_spi' GROUP BY datname, wait_event;],
+		'mydb|3|worker_spi_main'),
 	'bgworkers all launched'
 ) or die "Timed out while waiting for bgworkers to be launched";
 
@@ -70,10 +89,10 @@ my $worker1_pid = $node->safe_psql('mydb', 'SELECT worker_spi_launch(1);');
 my $worker2_pid = $node->safe_psql('mydb', 'SELECT worker_spi_launch(2);');
 ok( $node->poll_query_until(
 		'mydb',
-		qq[SELECT datname, count(datname)  FROM pg_stat_activity
+		qq[SELECT datname, count(datname), wait_event FROM pg_stat_activity
             WHERE backend_type = 'worker_spi dynamic' AND
-            pid IN ($worker1_pid, $worker2_pid) GROUP BY datname;],
-		'mydb|2'),
+            pid IN ($worker1_pid, $worker2_pid) GROUP BY datname, wait_event;],
+		'mydb|2|worker_spi_main'),
 	'dynamic bgworkers all launched'
 ) or die "Timed out while waiting for dynamic bgworkers to be launched";
 
diff --git a/src/test/modules/worker_spi/worker_spi--1.0.sql b/src/test/modules/worker_spi/worker_spi--1.0.sql
index e9d5b07373..f13f7e0f98 100644
--- a/src/test/modules/worker_spi/worker_spi--1.0.sql
+++ b/src/test/modules/worker_spi/worker_spi--1.0.sql
@@ -7,3 +7,8 @@ CREATE FUNCTION worker_spi_launch(pg_catalog.int4)
 RETURNS pg_catalog.int4 STRICT
 AS 'MODULE_PATHNAME'
 LANGUAGE C;
+
+CREATE FUNCTION worker_spi_init()
+RETURNS VOID STRICT
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
diff --git a/src/test/modules/worker_spi/worker_spi.c b/src/test/modules/worker_spi/worker_spi.c
index 903dcddef9..48b5aad022 100644
--- a/src/test/modules/worker_spi/worker_spi.c
+++ b/src/test/modules/worker_spi/worker_spi.c
@@ -44,22 +44,97 @@
 
 PG_MODULE_MAGIC;
 
+PG_FUNCTION_INFO_V1(worker_spi_init);
 PG_FUNCTION_INFO_V1(worker_spi_launch);
 
 PGDLLEXPORT void worker_spi_main(Datum main_arg) pg_attribute_noreturn();
 
+/* Shared memory state */
+typedef struct worker_spi_state
+{
+	/* the wait event defined in initialized phase */
+	uint32		wait_event;
+}			worker_spi_state;
+
+static worker_spi_state * wsstate = NULL;	/* pointer to shared memory */
+
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_request_hook_type prev_shmem_startup_hook = NULL;
+
+static void worker_spi_shmem_request(void);
+static void worker_spi_shmem_startup(void);
+static void worker_spi_shmem_init(void);
+static Size worker_spi_memsize(void);
+
 /* GUC variables */
 static int	worker_spi_naptime = 10;
 static int	worker_spi_total_workers = 2;
 static char *worker_spi_database = NULL;
 
-
 typedef struct worktable
 {
 	const char *schema;
 	const char *name;
 } worktable;
 
+static void
+worker_spi_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(worker_spi_memsize());
+}
+
+static void
+worker_spi_shmem_startup(void)
+{
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	worker_spi_shmem_init();
+}
+
+static Size
+worker_spi_memsize(void)
+{
+	return MAXALIGN(sizeof(worker_spi_state));
+}
+
+/*
+ * Initialize the shared memory state of worker_spi.
+ *
+ * This routine allocates a new wait event when called the first time.
+ * On follow-up calls, the name of the wait event associated with the
+ * existing shared memory state is registered.
+ */
+static void
+worker_spi_shmem_init(void)
+{
+	bool		found;
+
+	wsstate = NULL;
+
+	/* Create or attach to the shared memory state */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+	wsstate = ShmemInitStruct("custom_wait_event",
+							  sizeof(worker_spi_state),
+							  &found);
+
+	/* Define a new wait event */
+	if (!found)
+		wsstate->wait_event = WaitEventExtensionNew();
+
+	LWLockRelease(AddinShmemInitLock);
+
+	/*
+	 * Register the wait event in the lookup table of the current process.
+	 */
+	WaitEventExtensionRegisterName(wsstate->wait_event, "worker_spi_main");
+
+	return;
+}
+
 /*
  * Initialize workspace for a worker process: create the schema if it doesn't
  * already exist.
@@ -149,6 +224,9 @@ worker_spi_main(Datum main_arg)
 	/* We're now ready to receive signals */
 	BackgroundWorkerUnblockSignals();
 
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
 	/* Connect to our database */
 	BackgroundWorkerInitializeConnection(worker_spi_database, NULL, 0);
 
@@ -199,7 +277,7 @@ worker_spi_main(Datum main_arg)
 		(void) WaitLatch(MyLatch,
 						 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
 						 worker_spi_naptime * 1000L,
-						 WAIT_EVENT_EXTENSION);
+						 wsstate->wait_event);
 		ResetLatch(MyLatch);
 
 		CHECK_FOR_INTERRUPTS();
@@ -328,6 +406,11 @@ _PG_init(void)
 
 	MarkGUCPrefixReserved("worker_spi");
 
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = worker_spi_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = worker_spi_shmem_startup;
+
 	/* set up common data for all our workers */
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
@@ -351,6 +434,21 @@ _PG_init(void)
 	}
 }
 
+/*
+ * Wrapper to initialize a session with the shared memory state
+ * used by this module.  This is a convenience routine to be able to
+ * see the custom wait event stored in shared memory without loading
+ * through shared_preload_libraries.
+ */
+Datum
+worker_spi_init(PG_FUNCTION_ARGS)
+{
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
+	PG_RETURN_VOID();
+}
+
 /*
  * Dynamically launch an SPI worker.
  */
@@ -363,6 +461,9 @@ worker_spi_launch(PG_FUNCTION_ARGS)
 	BgwHandleStatus status;
 	pid_t		pid;
 
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 588b720f57..f8c0220d47 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1117,12 +1117,13 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
    <note>
     <para>
-     Extensions can add <literal>LWLock</literal> types to the list shown in
-     <xref linkend="wait-event-lwlock-table"/>.  In some cases, the name
-     assigned by an extension will not be available in all server processes;
-     so an <literal>LWLock</literal> wait event might be reported as
-     just <quote><literal>extension</literal></quote> rather than the
-     extension-assigned name.
+     Extensions can add <literal>Extension</literal> and <literal>LWLock</literal> types
+     to the list shown in <xref linkend="wait-event-extension-table"/> and
+     <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
+     assigned by an extension will not be available in all server processes
+     if the extension's library is not loaded; so a custom wait event might
+     be reported as just <quote><literal>extension</literal></quote>
+     rather than the assigned, custom, name.
     </para>
    </note>
  </sect2>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 9620ea9ae3..97344b219a 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3453,6 +3453,40 @@ if (!ptr)
     </para>
    </sect2>
 
+   <sect2 id="xfunc-addin-wait-events">
+    <title>Shared Memory and Custom Wait Events</title>
+
+    <para>
+     Add-ins can define Custom Wait Events under the wait event type
+     <literal>Extension</literal>. The add-in's shared library must be
+     preloaded by specifying it in <literal>shared_preload_libraries</literal>,
+     and register a <literal>shmem_request_hook</literal> and a
+     <literal>shmem_startup_hook</literal> in its
+     <function>_PG_init</function> function.
+     <literal>shmem_request_hook</literal> can request a shared memory size
+     to be later used at startup by calling from the
+     <literal>shmem_request_hook</literal>:
+<programlisting>
+void RequestAddinShmemSpace(int size)
+</programlisting>
+    </para>
+    <para>
+     Custom Wait Events are allocated in shared memory by calling:
+<programlisting>
+uint32 WaitEventExtensionNew(void)
+</programlisting>
+     in the <literal>shmem_startup_hook</literal> while holding the LWLock
+     <function>AddinShmemInitLock</function> to avoid any race conditions.
+     Next, each process needs to associate the wait event allocated previously
+     to a string, which is something done by calling:
+<programlisting>
+void WaitEventExtensionRegisterName(uint32 wait_event_info, const char *wait_event_name);
+</programlisting>
+     An example can be found in <filename>src/test/modules/worker_spi</filename>
+     in the PostgreSQL source tree.
+    </para>
+   </sect2>
+
    <sect2 id="extend-cpp">
     <title>Using C++ for Extensibility</title>
 
-- 
2.40.1

#36Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#35)
Re: Support to define custom wait events for extensions

Hi, all.

Sorry for late reply.

I am still mid-way through the review of the core APIs, but attached
is my current version in progress, labelled v8. I'll continue
tomorrow. I'm aware of some typos in the commit message of this
patch, and the dynamic bgworker launch is failing in the CI for
VS2019 (just too tired to finish debugging that today).

I suspect that I forgot to specify "volatile" to the variable
for the spinlock.

+/* dynamic allocation counter for custom wait events for extensions */
+typedef struct WaitEventExtensionCounter
+{
+	int			nextId;			/* next ID to assign */
+	slock_t		mutex;			/* protects the counter only */
+}			WaitEventExtensionCounter;
+
+/* pointer to the shared memory */
+static WaitEventExtensionCounter * waitEventExtensionCounter;

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#37Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#36)
1 attachment(s)
Re: Support to define custom wait events for extensions

On Thu, Jul 27, 2023 at 06:29:22PM +0900, Masahiro Ikeda wrote:

I suspect that I forgot to specify "volatile" to the variable
for the spinlock.

+   if (!IsUnderPostmaster)
+   {
+       /* Allocate space in shared memory. */
+       waitEventExtensionCounter = (WaitEventExtensionCounter *)
+           ShmemInitStruct("waitEventExtensionCounter", WaitEventExtensionShmemSize(), &found);
+       if (found)
+           return;

I think that your error is here. WaitEventExtensionShmemInit() is
forgetting to set the pointer to waitEventExtensionCounter for
processes where IsUnderPostmaster is true, which impacts things not
forked like in -DEXEC_BACKEND (the crash is reproducible on Linux with
-DEXEC_BACKEND in CFLAGS, as well). The correct thing to do is to
always call ShmemInitStruct, but only initialize the contents of the
shared memory area if ShmemInitStruct() has *not* found the shmem
contents.

WaitEventExtensionNew() could be easily incorrectly used, so I'd
rather add a LWLockHeldByMeInMode() on AddinShmemInitLock as safety
measure. Perhaps we should do the same for the LWLocks, subject for a
different thread..

+       int         newalloc;
+
+       newalloc = pg_nextpower2_32(Max(8, eventId + 1));

This should be a uint32.

+   if (eventId >= WaitEventExtensionNamesAllocated ||
+       WaitEventExtensionNames[eventId] == NULL)
+       return "extension";
That's too close to the default of "Extension".  It would be cleaner
to use "unknown", but we've been using "???" as well in many default
paths where an ID cannot be mapped to a string, so I would recommend
to just use that.

I have spent more time polishing the docs and the comments. This v9
looks in a rather committable shape now with docs, tests and core
routines in place.
--
Michael

Attachments:

v9-0001-Support-custom-wait-events-for-wait-event-type-Ex.patchtext/x-diff; charset=us-asciiDownload
From 766d1db13035eefd87a02adb88853f575abe75cb Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 28 Jul 2023 10:01:43 +0900
Subject: [PATCH v9] Support custom wait events for wait event type "Extension"

Two backend routines are added to allow extension to allocate and define
custom wait events, all of these being allocated as of type "Extension":
* WaitEventExtensionNew(), that allocates a wait event ID computed from
a counter in shared memory.
* WaitEventExtensionRegisterName(), to associate a custom string to the
wait event registered.

Note that this includes an example of how to use this new facility in
worker_spi, with tests for various scenarios.

The use of these routines should be extended across more in-core
modules, but this is left as work for future patches.
---
 src/include/utils/wait_event.h                |  26 +++
 src/backend/storage/ipc/ipci.c                |   3 +
 .../activity/generate-wait_event_types.pl     |   7 +-
 src/backend/utils/activity/wait_event.c       | 171 +++++++++++++++++-
 .../utils/activity/wait_event_names.txt       |   2 +-
 .../modules/worker_spi/t/001_worker_spi.pl    |  31 +++-
 .../modules/worker_spi/worker_spi--1.0.sql    |   5 +
 src/test/modules/worker_spi/worker_spi.c      | 104 ++++++++++-
 doc/src/sgml/monitoring.sgml                  |  13 +-
 doc/src/sgml/xfunc.sgml                       |  33 ++++
 10 files changed, 369 insertions(+), 26 deletions(-)

diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 4517425f84..3e38215c2d 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -38,6 +38,32 @@ extern void pgstat_reset_wait_event_storage(void);
 extern PGDLLIMPORT uint32 *my_wait_event_info;
 
 
+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define their own wait events in this categiry.  First,
+ * they should call WaitEventExtensionNew() to get one or more wait event
+ * IDs that are allocated from a shared counter.  These can be used directly
+ * with pgstat_report_wait_start() or equivalent.  Next, each individual
+ * process should call WaitEventExtensionRegisterName() to associate a wait
+ * event string to the number allocated previously.
+ */
+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+} WaitEventExtension;
+
+extern void WaitEventExtensionShmemInit(void);
+extern Size WaitEventExtensionShmemSize(void);
+
+extern uint32 WaitEventExtensionNew(void);
+extern void WaitEventExtensionRegisterName(uint32 wait_event_info,
+										   const char *wait_event_name);
+
 /* ----------
  * pgstat_report_wait_start() -
  *
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index cc387c00a1..5551afffc0 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -49,6 +49,7 @@
 #include "storage/spin.h"
 #include "utils/guc.h"
 #include "utils/snapmgr.h"
+#include "utils/wait_event.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -142,6 +143,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SyncScanShmemSize());
 	size = add_size(size, AsyncShmemSize());
 	size = add_size(size, StatsShmemSize());
+	size = add_size(size, WaitEventExtensionShmemSize());
 #ifdef EXEC_BACKEND
 	size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -301,6 +303,7 @@ CreateSharedMemoryAndSemaphores(void)
 	SyncScanShmemInit();
 	AsyncShmemInit();
 	StatsShmemInit();
+	WaitEventExtensionShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/utils/activity/generate-wait_event_types.pl b/src/backend/utils/activity/generate-wait_event_types.pl
index f63c991051..56335e8730 100644
--- a/src/backend/utils/activity/generate-wait_event_types.pl
+++ b/src/backend/utils/activity/generate-wait_event_types.pl
@@ -133,10 +133,11 @@ if ($gen_code)
 	foreach my $waitclass (sort { uc($a) cmp uc($b) } keys %hashwe)
 	{
 
-		# Don't generate .c and .h files for LWLock and Lock, these are
-		# handled independently.
+		# Don't generate .c and .h files for Extension, LWLock and
+		# Lock, these are handled independently.
 		next
-		  if ( $waitclass eq 'WaitEventLWLock'
+		  if ( $waitclass eq 'WaitEventExtension'
+			|| $waitclass eq 'WaitEventLWLock'
 			|| $waitclass eq 'WaitEventLock');
 
 		my $last = $waitclass;
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 4aad11c111..7754ba0e07 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,15 +22,18 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
 #include "storage/lwlock.h"		/* for GetLWLockIdentifier */
+#include "storage/spin.h"
+#include "utils/memutils.h"
 #include "utils/wait_event.h"
 
 
 static const char *pgstat_get_wait_activity(WaitEventActivity w);
 static const char *pgstat_get_wait_bufferpin(WaitEventBufferPin w);
 static const char *pgstat_get_wait_client(WaitEventClient w);
-static const char *pgstat_get_wait_extension(WaitEventExtension w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
@@ -40,6 +43,162 @@ static uint32 local_my_wait_event_info;
 uint32	   *my_wait_event_info = &local_my_wait_event_info;
 
 
+
+/* dynamic allocation counter for custom wait events in extensions */
+typedef struct WaitEventExtensionCounter
+{
+	int			nextId;			/* next ID to assign */
+	slock_t		mutex;			/* protects the counter */
+} WaitEventExtensionCounter;
+
+/* pointer to the shared memory */
+static WaitEventExtensionCounter *waitEventExtensionCounter;
+
+/* first event ID of custom wait events for extensions */
+#define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
+	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
+
+/*
+ * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event IDs known to the current
+ * process.  Any unused entries in the array will contain NULL.
+ */
+static const char **WaitEventExtensionNames = NULL;
+static int	WaitEventExtensionNamesAllocated = 0;
+
+static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
+
+/*
+ *  Return the space for dynamic allocation counter.
+ */
+Size
+WaitEventExtensionShmemSize(void)
+{
+	return sizeof(WaitEventExtensionCounter);
+}
+
+/*
+ * Allocate shmem space for dynamic allocation counter.
+ */
+void
+WaitEventExtensionShmemInit(void)
+{
+	bool		found;
+
+	waitEventExtensionCounter = (WaitEventExtensionCounter *)
+		ShmemInitStruct("waitEventExtensionCounter",
+						WaitEventExtensionShmemSize(), &found);
+
+	if (!found)
+	{
+		/* initialize the allocation counter and its spinlock. */
+		waitEventExtensionCounter->nextId = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+		SpinLockInit(&waitEventExtensionCounter->mutex);
+	}
+}
+
+/*
+ * Allocate a new event ID and return the wait event.
+ */
+uint32
+WaitEventExtensionNew(void)
+{
+	uint16		eventId;
+
+	Assert(LWLockHeldByMeInMode(AddinShmemInitLock, LW_EXCLUSIVE));
+
+	SpinLockAcquire(&waitEventExtensionCounter->mutex);
+
+	if (waitEventExtensionCounter->nextId > PG_UINT16_MAX)
+	{
+		SpinLockRelease(&waitEventExtensionCounter->mutex);
+		ereport(ERROR,
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("too many wait events for extensions"));
+	}
+
+	eventId = waitEventExtensionCounter->nextId++;
+
+	SpinLockRelease(&waitEventExtensionCounter->mutex);
+
+	return PG_WAIT_EXTENSION | eventId;
+}
+
+/*
+ * Register a dynamic wait event name for extension in the lookup table
+ * of the current process.
+ *
+ * This routine will save a pointer to the wait event name passed as an argument,
+ * so the name should be allocated in a backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+WaitEventExtensionRegisterName(uint32 wait_event_info,
+							   const char *wait_event_name)
+{
+	uint16		eventId;
+
+	eventId = wait_event_info & 0x0000FFFF;
+
+	/* Check the wait event class. */
+	Assert((wait_event_info & 0xFF000000) == PG_WAIT_EXTENSION);
+
+	/* This should only be called for user-defined wait event. */
+	Assert(eventId >= NUM_BUILTIN_WAIT_EVENT_EXTENSION);
+
+	/* Convert to array index. */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	/* If necessary, create or enlarge array. */
+	if (eventId >= WaitEventExtensionNamesAllocated)
+	{
+		uint32		newalloc;
+
+		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
+
+		if (WaitEventExtensionNames == NULL)
+			WaitEventExtensionNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		else
+			WaitEventExtensionNames =
+				repalloc0_array(WaitEventExtensionNames, const char *,
+								WaitEventExtensionNamesAllocated, newalloc);
+		WaitEventExtensionNamesAllocated = newalloc;
+	}
+
+	WaitEventExtensionNames[eventId] = wait_event_name;
+}
+
+/*
+ * Return the name of an wait event ID for extension.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)
+{
+	/* Built-in event? */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It is a user-defined wait event, so look at WaitEventExtensionNames[].
+	 * However, it is possible that the name has never been registered by
+	 * calling WaitEventExtensionRegisterName() in the current process, in
+	 * which case give up and return an unknown state.
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= WaitEventExtensionNamesAllocated ||
+		WaitEventExtensionNames[eventId] == NULL)
+		return "???";
+
+	return WaitEventExtensionNames[eventId];
+}
+
+
 /*
  * Configure wait event reporting to report wait events to *wait_event_info.
  * *wait_event_info needs to be valid until pgstat_reset_wait_event_storage()
@@ -149,6 +308,9 @@ pgstat_get_wait_event(uint32 wait_event_info)
 		case PG_WAIT_LOCK:
 			event_name = GetLockNameFromTagType(eventId);
 			break;
+		case PG_WAIT_EXTENSION:
+			event_name = GetWaitEventExtensionIdentifier(eventId);
+			break;
 		case PG_WAIT_BUFFERPIN:
 			{
 				WaitEventBufferPin w = (WaitEventBufferPin) wait_event_info;
@@ -170,13 +332,6 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_client(w);
 				break;
 			}
-		case PG_WAIT_EXTENSION:
-			{
-				WaitEventExtension w = (WaitEventExtension) wait_event_info;
-
-				event_name = pgstat_get_wait_extension(w);
-				break;
-			}
 		case PG_WAIT_IPC:
 			{
 				WaitEventIPC w = (WaitEventIPC) wait_event_info;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3fabad96d9..2ea4789b00 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -261,7 +261,7 @@ WAIT_EVENT_BUFFER_PIN	BufferPin	"Waiting to acquire an exclusive pin on a buffer
 
 Section: ClassName - WaitEventExtension
 
-WAIT_EVENT_EXTENSION	Extension	"Waiting in an extension."
+WAIT_EVENT_DOCONLY	Extension	"Waiting in an extension."
 
 #
 # Wait events - LWLock
diff --git a/src/test/modules/worker_spi/t/001_worker_spi.pl b/src/test/modules/worker_spi/t/001_worker_spi.pl
index c293871313..44764d4bf4 100644
--- a/src/test/modules/worker_spi/t/001_worker_spi.pl
+++ b/src/test/modules/worker_spi/t/001_worker_spi.pl
@@ -39,6 +39,25 @@ $node->poll_query_until('postgres',
 $result = $node->safe_psql('postgres', 'SELECT * FROM schema4.counted;');
 is($result, qq(total|1), 'dynamic bgworker correctly consumed tuple data');
 
+# Check the wait event used by the dynamic bgworker.  For a session
+# without the state in shared memory known, the default of "???" is the
+# value waited on.
+$result = $node->poll_query_until(
+	'postgres',
+	qq[SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
+	'???');
+is($result, 1, 'dynamic bgworker has reported "???" as wait event');
+# If the shared memory state is loaded, the wait event is the custom one.
+# The expected result is a special pattern here with a newline coming from the
+# first query where the shared memory state is set.
+$result = $node->poll_query_until(
+	'postgres',
+	qq[SELECT worker_spi_init(); SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
+	qq[
+worker_spi_main]);
+is($result, 1,
+	'dynamic bgworker has reported "worker_spi_main" as wait event');
+
 note "testing bgworkers loaded with shared_preload_libraries";
 
 # Create the database first so as the workers can connect to it when
@@ -58,9 +77,9 @@ $node->restart;
 # Check that bgworkers have been registered and launched.
 ok( $node->poll_query_until(
 		'mydb',
-		qq[SELECT datname, count(datname) FROM pg_stat_activity
-            WHERE backend_type = 'worker_spi' GROUP BY datname;],
-		'mydb|3'),
+		qq[SELECT datname, count(datname), wait_event FROM pg_stat_activity
+            WHERE backend_type = 'worker_spi' GROUP BY datname, wait_event;],
+		'mydb|3|worker_spi_main'),
 	'bgworkers all launched'
 ) or die "Timed out while waiting for bgworkers to be launched";
 
@@ -70,10 +89,10 @@ my $worker1_pid = $node->safe_psql('mydb', 'SELECT worker_spi_launch(1);');
 my $worker2_pid = $node->safe_psql('mydb', 'SELECT worker_spi_launch(2);');
 ok( $node->poll_query_until(
 		'mydb',
-		qq[SELECT datname, count(datname)  FROM pg_stat_activity
+		qq[SELECT datname, count(datname), wait_event FROM pg_stat_activity
             WHERE backend_type = 'worker_spi dynamic' AND
-            pid IN ($worker1_pid, $worker2_pid) GROUP BY datname;],
-		'mydb|2'),
+            pid IN ($worker1_pid, $worker2_pid) GROUP BY datname, wait_event;],
+		'mydb|2|worker_spi_main'),
 	'dynamic bgworkers all launched'
 ) or die "Timed out while waiting for dynamic bgworkers to be launched";
 
diff --git a/src/test/modules/worker_spi/worker_spi--1.0.sql b/src/test/modules/worker_spi/worker_spi--1.0.sql
index e9d5b07373..f13f7e0f98 100644
--- a/src/test/modules/worker_spi/worker_spi--1.0.sql
+++ b/src/test/modules/worker_spi/worker_spi--1.0.sql
@@ -7,3 +7,8 @@ CREATE FUNCTION worker_spi_launch(pg_catalog.int4)
 RETURNS pg_catalog.int4 STRICT
 AS 'MODULE_PATHNAME'
 LANGUAGE C;
+
+CREATE FUNCTION worker_spi_init()
+RETURNS VOID STRICT
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
diff --git a/src/test/modules/worker_spi/worker_spi.c b/src/test/modules/worker_spi/worker_spi.c
index 903dcddef9..8e234f912c 100644
--- a/src/test/modules/worker_spi/worker_spi.c
+++ b/src/test/modules/worker_spi/worker_spi.c
@@ -44,22 +44,96 @@
 
 PG_MODULE_MAGIC;
 
+PG_FUNCTION_INFO_V1(worker_spi_init);
 PG_FUNCTION_INFO_V1(worker_spi_launch);
 
 PGDLLEXPORT void worker_spi_main(Datum main_arg) pg_attribute_noreturn();
 
+/* Shared memory state */
+typedef struct worker_spi_state
+{
+	/* the wait event defined during initialization phase */
+	uint32		wait_event;
+} worker_spi_state;
+
+static worker_spi_state *wsstate = NULL;	/* pointer to shared memory */
+
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_request_hook_type prev_shmem_startup_hook = NULL;
+
+static void worker_spi_shmem_request(void);
+static void worker_spi_shmem_startup(void);
+static void worker_spi_shmem_init(void);
+static Size worker_spi_memsize(void);
+
 /* GUC variables */
 static int	worker_spi_naptime = 10;
 static int	worker_spi_total_workers = 2;
 static char *worker_spi_database = NULL;
 
-
 typedef struct worktable
 {
 	const char *schema;
 	const char *name;
 } worktable;
 
+static void
+worker_spi_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(worker_spi_memsize());
+}
+
+static void
+worker_spi_shmem_startup(void)
+{
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	worker_spi_shmem_init();
+}
+
+static Size
+worker_spi_memsize(void)
+{
+	return MAXALIGN(sizeof(worker_spi_state));
+}
+
+/*
+ * Initialize the shared memory state of worker_spi.
+ *
+ * This routine allocates a new wait event when called the first time.
+ * On follow-up calls, the name of the wait event associated with the
+ * existing shared memory state is registered.
+ */
+static void
+worker_spi_shmem_init(void)
+{
+	bool		found;
+
+	wsstate = NULL;
+
+	/* Create or attach to the shared memory state */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+	wsstate = ShmemInitStruct("custom_wait_event",
+							  sizeof(worker_spi_state),
+							  &found);
+
+	/* Define a new wait event */
+	if (!found)
+		wsstate->wait_event = WaitEventExtensionNew();
+
+	LWLockRelease(AddinShmemInitLock);
+
+	/*
+	 * Register the wait event in the lookup table of the current process.
+	 */
+	WaitEventExtensionRegisterName(wsstate->wait_event, "worker_spi_main");
+	return;
+}
+
 /*
  * Initialize workspace for a worker process: create the schema if it doesn't
  * already exist.
@@ -149,6 +223,9 @@ worker_spi_main(Datum main_arg)
 	/* We're now ready to receive signals */
 	BackgroundWorkerUnblockSignals();
 
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
 	/* Connect to our database */
 	BackgroundWorkerInitializeConnection(worker_spi_database, NULL, 0);
 
@@ -199,7 +276,7 @@ worker_spi_main(Datum main_arg)
 		(void) WaitLatch(MyLatch,
 						 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
 						 worker_spi_naptime * 1000L,
-						 WAIT_EVENT_EXTENSION);
+						 wsstate->wait_event);
 		ResetLatch(MyLatch);
 
 		CHECK_FOR_INTERRUPTS();
@@ -328,6 +405,11 @@ _PG_init(void)
 
 	MarkGUCPrefixReserved("worker_spi");
 
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = worker_spi_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = worker_spi_shmem_startup;
+
 	/* set up common data for all our workers */
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
@@ -351,6 +433,21 @@ _PG_init(void)
 	}
 }
 
+/*
+ * Wrapper to initialize a session with the shared memory state
+ * used by this module.  This is a convenience routine to be able to
+ * see the custom wait event stored in shared memory without loading
+ * through shared_preload_libraries.
+ */
+Datum
+worker_spi_init(PG_FUNCTION_ARGS)
+{
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
+	PG_RETURN_VOID();
+}
+
 /*
  * Dynamically launch an SPI worker.
  */
@@ -363,6 +460,9 @@ worker_spi_launch(PG_FUNCTION_ARGS)
 	BgwHandleStatus status;
 	pid_t		pid;
 
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 588b720f57..023761dacb 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1117,12 +1117,13 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
    <note>
     <para>
-     Extensions can add <literal>LWLock</literal> types to the list shown in
-     <xref linkend="wait-event-lwlock-table"/>.  In some cases, the name
-     assigned by an extension will not be available in all server processes;
-     so an <literal>LWLock</literal> wait event might be reported as
-     just <quote><literal>extension</literal></quote> rather than the
-     extension-assigned name.
+     Extensions can add <literal>Extension</literal> and <literal>LWLock</literal> types
+     to the list shown in <xref linkend="wait-event-extension-table"/> and
+     <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
+     assigned by an extension will not be available in all server processes
+     if the extension's library is not loaded; so a custom wait event might
+     be reported as just <quote><literal>???</literal></quote>
+     rather than the custom name assigned.
     </para>
    </note>
  </sect2>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 9620ea9ae3..315afef2a5 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3453,6 +3453,39 @@ if (!ptr)
     </para>
    </sect2>
 
+   <sect2 id="xfunc-addin-wait-events">
+    <title>Shared Memory and Custom Wait Events</title>
+
+    <para>
+     Add-ins can define custom wait events under the wait event type
+     <literal>Extension</literal>. The add-in's shared library must be
+     preloaded by specifying it in <literal>shared_preload_libraries</literal>,
+     and register a <literal>shmem_request_hook</literal> and a
+     <literal>shmem_startup_hook</literal> in its
+     <function>_PG_init</function> function.
+     <literal>shmem_request_hook</literal> can request a shared memory size
+     to be later used at startup by calling:
+<programlisting>
+void RequestAddinShmemSpace(int size)
+</programlisting>
+    </para>
+    <para>
+     <literal>shmem_startup_hook</literal> can allocate in shared memory
+     custom wait events by calling while holding the LWLock
+     <function>AddinShmemInitLock</function> to avoid any race conditions:
+<programlisting>
+uint32 WaitEventExtensionNew(void)
+</programlisting>
+     Next, each process needs to associate the wait event allocated previously
+     to a user-facing custom string, which is something done by calling:
+<programlisting>
+void WaitEventExtensionRegisterName(uint32 wait_event_info, const char *wait_event_name)
+</programlisting>
+     An example can be found in <filename>src/test/modules/worker_spi</filename>
+     in the PostgreSQL source tree.
+    </para>
+   </sect2>
+
    <sect2 id="extend-cpp">
     <title>Using C++ for Extensibility</title>
 
-- 
2.40.1

#38Bharath Rupireddy
bharath.rupireddyforpostgres@gmail.com
In reply to: Michael Paquier (#37)
Re: Support to define custom wait events for extensions

On Fri, Jul 28, 2023 at 6:36 AM Michael Paquier <michael@paquier.xyz> wrote:

I have spent more time polishing the docs and the comments. This v9
looks in a rather committable shape now with docs, tests and core
routines in place.

Thanks. Here are some comments on v9 patch:

1.
-     so an <literal>LWLock</literal> wait event might be reported as
-     just <quote><literal>extension</literal></quote> rather than the
-     extension-assigned name.
+     if the extension's library is not loaded; so a custom wait event might
+     be reported as just <quote><literal>???</literal></quote>
+     rather than the custom name assigned.

Trying to understand why '???' is any better than 'extension' for a
registered custom wait event of an unloaded extension?

PS: Looked at other instances where '???' is being used for
representing an unknown "thing".

2. Have an example of how a custom wait event is displayed in the
example in the docs "Here is an example of how wait events can be
viewed:". We can use the worker_spi wait event type there.

3.
- so an <literal>LWLock</literal> wait event might be reported as
- just <quote><literal>extension</literal></quote> rather than the
- extension-assigned name.

+     <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
+     assigned by an extension will not be available in all server processes
+     if the extension's library is not loaded; so a custom wait event might
+     be reported as just <quote><literal>???</literal></quote>

Are we missing to explicitly say what wait event will be reported for
an LWLock when the extension library is not loaded?

4.
+ Add-ins can define custom wait events under the wait event type

I see a few instances of Add-ins/add-in in xfunc.sgml. Isn't it better
to use the word extension given that glossary defines what an
extension is https://www.postgresql.org/docs/current/glossary.html#GLOSSARY-EXTENSION?

5.
+} WaitEventExtensionCounter;
+
+/* pointer to the shared memory */
+static WaitEventExtensionCounter *waitEventExtensionCounter;

How about naming the structure variable as
WaitEventExtensionCounterData and pointer as
WaitEventExtensionCounter? This keeps all the static variable names
consistent WaitEventExtensionNames, WaitEventExtensionNamesAllocated
and WaitEventExtensionCounter.

6.
+    /* Check the wait event class. */
+    Assert((wait_event_info & 0xFF000000) == PG_WAIT_EXTENSION);
+
+    /* This should only be called for user-defined wait event. */
+    Assert(eventId >= NUM_BUILTIN_WAIT_EVENT_EXTENSION);

Maybe, we must turn the above asserts into ereport(ERROR) to protect
against an extension sending in an unregistered wait_event_info?
Especially, the first Assert((wait_event_info & 0xFF000000) ==
PG_WAIT_EXTENSION); checks that the passed in wait_event_info is
previously returned by WaitEventExtensionNew. IMO, these assertions
better fit for errors.

7.
+ * Extensions can define their own wait events in this categiry. First,
Typo - s/categiry/category

8.
+ First,
+ * they should call WaitEventExtensionNew() to get one or more wait event
+ * IDs that are allocated from a shared counter.

Can WaitEventExtensionNew() be WaitEventExtensionNew(int num_ids, int
*result) to get the required number of wait event IDs in one call
similar to RequestNamedLWLockTranche? Currently, an extension needs to
call WaitEventExtensionNew() N number of times to get N wait event
IDs. Maybe the existing WaitEventExtensionNew() is good, but just a
thought.

9.
# The expected result is a special pattern here with a newline coming from the
# first query where the shared memory state is set.
$result = $node->poll_query_until(
'postgres',
qq[SELECT worker_spi_init(); SELECT wait_event FROM
pg_stat_activity WHERE backend_type ~ 'worker_spi';],
qq[
worker_spi_main]);

This test doesn't have to be that complex with the result being a
special pattern, SELECT worker_spi_init(); can just be within a
separate safe_psql.

10.
+ wsstate = ShmemInitStruct("custom_wait_event",

Name the shared memory just "worker_spi" to make it generic and
extensible. Essentially, it is a woker_spi shared memory area part of
it is for custom wait event id.

--
Bharath Rupireddy
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

#39Michael Paquier
michael@paquier.xyz
In reply to: Bharath Rupireddy (#38)
1 attachment(s)
Re: Support to define custom wait events for extensions

On Fri, Jul 28, 2023 at 12:43:36PM +0530, Bharath Rupireddy wrote:

1.
-     so an <literal>LWLock</literal> wait event might be reported as
-     just <quote><literal>extension</literal></quote> rather than the
-     extension-assigned name.
+     if the extension's library is not loaded; so a custom wait event might
+     be reported as just <quote><literal>???</literal></quote>
+     rather than the custom name assigned.

Trying to understand why '???' is any better than 'extension' for a
registered custom wait event of an unloaded extension?

PS: Looked at other instances where '???' is being used for
representing an unknown "thing".

You are right that I am making things inconsistent here. Having a
behavior close to the existing LWLock and use "extension" when the
event cannot be found makes the most sense. I have been a bit
confused with the wording though of this part of the docs, though, as
LWLocks don't directly use the custom wait event APIs.

2. Have an example of how a custom wait event is displayed in the
example in the docs "Here is an example of how wait events can be
viewed:". We can use the worker_spi wait event type there.

Fine by me, added one.

3.
- so an <literal>LWLock</literal> wait event might be reported as
- just <quote><literal>extension</literal></quote> rather than the
- extension-assigned name.

+     <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
+     assigned by an extension will not be available in all server processes
+     if the extension's library is not loaded; so a custom wait event might
+     be reported as just <quote><literal>???</literal></quote>

Are we missing to explicitly say what wait event will be reported for
an LWLock when the extension library is not loaded?

Yes, see answer to point 1.

4.
+ Add-ins can define custom wait events under the wait event type

I see a few instances of Add-ins/add-in in xfunc.sgml. Isn't it better
to use the word extension given that glossary defines what an
extension is https://www.postgresql.org/docs/current/glossary.html#GLOSSARY-EXTENSION?

An extension is an Add-in, and may be loaded, but it is possible to
have modules that just need to be LOAD'ed (with command line or just
shared_preload_libraries) so an add-in may not always be an extension.
I am not completely sure if add-ins is the best term, but it covers
both, and that's consistent with the existing docs. Perhaps the same
area of the docs should be refreshed, but that looks like a separate
patch for me. For now, I'd rather use a consistent term, and this one
sounds OK to me.

[1]: https://www.postgresql.org/docs/devel/extend-extensions.html.

5.
+} WaitEventExtensionCounter;
+
+/* pointer to the shared memory */
+static WaitEventExtensionCounter *waitEventExtensionCounter;

How about naming the structure variable as
WaitEventExtensionCounterData and pointer as
WaitEventExtensionCounter? This keeps all the static variable names
consistent WaitEventExtensionNames, WaitEventExtensionNamesAllocated
and WaitEventExtensionCounter.

Hmm, good point on consistency here, especially to use an upper-case
character for the first characters of waitEventExtensionCounter..
Err.. WaitEventExtensionCounter.

6.
+    /* Check the wait event class. */
+    Assert((wait_event_info & 0xFF000000) == PG_WAIT_EXTENSION);
+
+    /* This should only be called for user-defined wait event. */
+    Assert(eventId >= NUM_BUILTIN_WAIT_EVENT_EXTENSION);

Maybe, we must turn the above asserts into ereport(ERROR) to protect
against an extension sending in an unregistered wait_event_info?
Especially, the first Assert((wait_event_info & 0xFF000000) ==
PG_WAIT_EXTENSION); checks that the passed in wait_event_info is
previously returned by WaitEventExtensionNew. IMO, these assertions
better fit for errors.

Okay by me that it may be a better idea to use ereport(ERROR) in the
long run, so changed this way. I have introduced a
WAIT_EVENT_CLASS_MASK and a WAIT_EVENT_ID_MASK as we now use
0xFF000000 and 0x0000FFFF in three places of this file. This should
just be a patch on its own.

7.
+ * Extensions can define their own wait events in this categiry. First,
Typo - s/categiry/category

Thanks, missed that.

8.
+ First,
+ * they should call WaitEventExtensionNew() to get one or more wait event
+ * IDs that are allocated from a shared counter.

Can WaitEventExtensionNew() be WaitEventExtensionNew(int num_ids, int
*result) to get the required number of wait event IDs in one call
similar to RequestNamedLWLockTranche? Currently, an extension needs to
call WaitEventExtensionNew() N number of times to get N wait event
IDs. Maybe the existing WaitEventExtensionNew() is good, but just a
thought.

Yes, this was mentioned upthread. I am not completely sure yet how
much we need to do for this interface, but surely it would be faster
to have a Multiple() interface that returns an array made of N numbers
requested (rather than a rank of them). For now, I'd rather just aim
for simplicity for the basics.

9.
# The expected result is a special pattern here with a newline coming from the
# first query where the shared memory state is set.
$result = $node->poll_query_until(
'postgres',
qq[SELECT worker_spi_init(); SELECT wait_event FROM
pg_stat_activity WHERE backend_type ~ 'worker_spi';],
qq[
worker_spi_main]);

This test doesn't have to be that complex with the result being a
special pattern, SELECT worker_spi_init(); can just be within a
separate safe_psql.

No, it cannot because we need the custom wait event string to be
loaded in the same connection as the one querying pg_stat_activity.
A different thing that can be done here is to use background_psql()
with a query_until(), though I am not sure that this is worth doing
here.

10.
+ wsstate = ShmemInitStruct("custom_wait_event",

Name the shared memory just "worker_spi" to make it generic and
extensible. Essentially, it is a woker_spi shared memory area part of
it is for custom wait event id.

Right, this is misleading. This could be something like a "worker_spi
State", for instance. I have switched to this term.

Attached is a new version.
--
Michael

Attachments:

v10-0001-Support-custom-wait-events-for-wait-event-type-E.patchtext/x-diff; charset=us-asciiDownload
From 7ab64c42814d36800f53d532a567ec55a5ff7174 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 31 Jul 2023 09:51:02 +0900
Subject: [PATCH v10] Support custom wait events for wait event type
 "Extension"

Two backend routines are added to allow extension to allocate and define
custom wait events, all of these being allocated as of type "Extension":
* WaitEventExtensionNew(), that allocates a wait event ID computed from
a counter in shared memory.
* WaitEventExtensionRegisterName(), to associate a custom string to the
wait event registered.

Note that this includes an example of how to use this new facility in
worker_spi, with tests for various scenarios.

The use of these routines should be extended across more in-core
modules, but this is left as work for future patches.
---
 src/include/utils/wait_event.h                |  26 +++
 src/backend/storage/ipc/ipci.c                |   3 +
 .../activity/generate-wait_event_types.pl     |   7 +-
 src/backend/utils/activity/wait_event.c       | 186 ++++++++++++++++--
 .../utils/activity/wait_event_names.txt       |   2 +-
 .../modules/worker_spi/t/001_worker_spi.pl    |  31 ++-
 .../modules/worker_spi/worker_spi--1.0.sql    |   5 +
 src/test/modules/worker_spi/worker_spi.c      | 104 +++++++++-
 doc/src/sgml/monitoring.sgml                  |  11 +-
 doc/src/sgml/xfunc.sgml                       |  45 +++++
 src/tools/pgindent/typedefs.list              |   2 +
 11 files changed, 395 insertions(+), 27 deletions(-)

diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 4517425f84..aad8bc08fa 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -38,6 +38,32 @@ extern void pgstat_reset_wait_event_storage(void);
 extern PGDLLIMPORT uint32 *my_wait_event_info;
 
 
+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define their own wait events in this category.  First,
+ * they should call WaitEventExtensionNew() to get one or more wait event
+ * IDs that are allocated from a shared counter.  These can be used directly
+ * with pgstat_report_wait_start() or equivalent.  Next, each individual
+ * process should call WaitEventExtensionRegisterName() to associate a wait
+ * event string to the number allocated previously.
+ */
+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+} WaitEventExtension;
+
+extern void WaitEventExtensionShmemInit(void);
+extern Size WaitEventExtensionShmemSize(void);
+
+extern uint32 WaitEventExtensionNew(void);
+extern void WaitEventExtensionRegisterName(uint32 wait_event_info,
+										   const char *wait_event_name);
+
 /* ----------
  * pgstat_report_wait_start() -
  *
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index cc387c00a1..5551afffc0 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -49,6 +49,7 @@
 #include "storage/spin.h"
 #include "utils/guc.h"
 #include "utils/snapmgr.h"
+#include "utils/wait_event.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -142,6 +143,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SyncScanShmemSize());
 	size = add_size(size, AsyncShmemSize());
 	size = add_size(size, StatsShmemSize());
+	size = add_size(size, WaitEventExtensionShmemSize());
 #ifdef EXEC_BACKEND
 	size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -301,6 +303,7 @@ CreateSharedMemoryAndSemaphores(void)
 	SyncScanShmemInit();
 	AsyncShmemInit();
 	StatsShmemInit();
+	WaitEventExtensionShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/utils/activity/generate-wait_event_types.pl b/src/backend/utils/activity/generate-wait_event_types.pl
index f63c991051..56335e8730 100644
--- a/src/backend/utils/activity/generate-wait_event_types.pl
+++ b/src/backend/utils/activity/generate-wait_event_types.pl
@@ -133,10 +133,11 @@ if ($gen_code)
 	foreach my $waitclass (sort { uc($a) cmp uc($b) } keys %hashwe)
 	{
 
-		# Don't generate .c and .h files for LWLock and Lock, these are
-		# handled independently.
+		# Don't generate .c and .h files for Extension, LWLock and
+		# Lock, these are handled independently.
 		next
-		  if ( $waitclass eq 'WaitEventLWLock'
+		  if ( $waitclass eq 'WaitEventExtension'
+			|| $waitclass eq 'WaitEventLWLock'
 			|| $waitclass eq 'WaitEventLock');
 
 		my $last = $waitclass;
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 4aad11c111..eaf3d0515a 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,15 +22,18 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
 #include "storage/lwlock.h"		/* for GetLWLockIdentifier */
+#include "storage/spin.h"
+#include "utils/memutils.h"
 #include "utils/wait_event.h"
 
 
 static const char *pgstat_get_wait_activity(WaitEventActivity w);
 static const char *pgstat_get_wait_bufferpin(WaitEventBufferPin w);
 static const char *pgstat_get_wait_client(WaitEventClient w);
-static const char *pgstat_get_wait_extension(WaitEventExtension w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
@@ -39,6 +42,171 @@ static const char *pgstat_get_wait_io(WaitEventIO w);
 static uint32 local_my_wait_event_info;
 uint32	   *my_wait_event_info = &local_my_wait_event_info;
 
+#define WAIT_EVENT_ID_MASK		0x0000FFFF
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+
+/* dynamic allocation counter for custom wait events in extensions */
+typedef struct WaitEventExtensionCounterData
+{
+	int			nextId;			/* next ID to assign */
+	slock_t		mutex;			/* protects the counter */
+} WaitEventExtensionCounterData;
+
+/* pointer to the shared memory */
+static WaitEventExtensionCounterData *WaitEventExtensionCounter;
+
+/* first event ID of custom wait events for extensions */
+#define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
+	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
+
+/*
+ * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event IDs known to the current
+ * process.  Any unused entries in the array will contain NULL.
+ */
+static const char **WaitEventExtensionNames = NULL;
+static int	WaitEventExtensionNamesAllocated = 0;
+
+static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
+
+/*
+ *  Return the space for dynamic allocation counter.
+ */
+Size
+WaitEventExtensionShmemSize(void)
+{
+	return sizeof(WaitEventExtensionCounterData);
+}
+
+/*
+ * Allocate shmem space for dynamic allocation counter.
+ */
+void
+WaitEventExtensionShmemInit(void)
+{
+	bool		found;
+
+	WaitEventExtensionCounter = (WaitEventExtensionCounterData *)
+		ShmemInitStruct("WaitEventExtensionCounterData",
+						WaitEventExtensionShmemSize(), &found);
+
+	if (!found)
+	{
+		/* initialize the allocation counter and its spinlock. */
+		WaitEventExtensionCounter->nextId = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+		SpinLockInit(&WaitEventExtensionCounter->mutex);
+	}
+}
+
+/*
+ * Allocate a new event ID and return the wait event.
+ */
+uint32
+WaitEventExtensionNew(void)
+{
+	uint16		eventId;
+
+	Assert(LWLockHeldByMeInMode(AddinShmemInitLock, LW_EXCLUSIVE));
+
+	SpinLockAcquire(&WaitEventExtensionCounter->mutex);
+
+	if (WaitEventExtensionCounter->nextId > PG_UINT16_MAX)
+	{
+		SpinLockRelease(&WaitEventExtensionCounter->mutex);
+		ereport(ERROR,
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("too many wait events for extensions"));
+	}
+
+	eventId = WaitEventExtensionCounter->nextId++;
+
+	SpinLockRelease(&WaitEventExtensionCounter->mutex);
+
+	return PG_WAIT_EXTENSION | eventId;
+}
+
+/*
+ * Register a dynamic wait event name for extension in the lookup table
+ * of the current process.
+ *
+ * This routine will save a pointer to the wait event name passed as an argument,
+ * so the name should be allocated in a backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+WaitEventExtensionRegisterName(uint32 wait_event_info,
+							   const char *wait_event_name)
+{
+	uint32		classId;
+	uint16		eventId;
+
+	classId = wait_event_info & WAIT_EVENT_CLASS_MASK;
+	eventId = wait_event_info & WAIT_EVENT_ID_MASK;
+
+	/* Check the wait event class. */
+	if (classId != PG_WAIT_EXTENSION)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid wait event class %u", classId));
+
+	/* This should only be called for user-defined wait event. */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid wait event ID %u", eventId));
+
+	/* Convert to array index. */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	/* If necessary, create or enlarge array. */
+	if (eventId >= WaitEventExtensionNamesAllocated)
+	{
+		uint32		newalloc;
+
+		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
+
+		if (WaitEventExtensionNames == NULL)
+			WaitEventExtensionNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		else
+			WaitEventExtensionNames =
+				repalloc0_array(WaitEventExtensionNames, const char *,
+								WaitEventExtensionNamesAllocated, newalloc);
+		WaitEventExtensionNamesAllocated = newalloc;
+	}
+
+	WaitEventExtensionNames[eventId] = wait_event_name;
+}
+
+/*
+ * Return the name of an wait event ID for extension.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)
+{
+	/* Built-in event? */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It is a user-defined wait event, so look at WaitEventExtensionNames[].
+	 * However, it is possible that the name has never been registered by
+	 * calling WaitEventExtensionRegisterName() in the current process, in
+	 * which case give up and return an unknown state.
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= WaitEventExtensionNamesAllocated ||
+		WaitEventExtensionNames[eventId] == NULL)
+		return "extension";
+
+	return WaitEventExtensionNames[eventId];
+}
+
 
 /*
  * Configure wait event reporting to report wait events to *wait_event_info.
@@ -82,7 +250,7 @@ pgstat_get_wait_event_type(uint32 wait_event_info)
 	if (wait_event_info == 0)
 		return NULL;
 
-	classId = wait_event_info & 0xFF000000;
+	classId = wait_event_info & WAIT_EVENT_CLASS_MASK;
 
 	switch (classId)
 	{
@@ -138,8 +306,8 @@ pgstat_get_wait_event(uint32 wait_event_info)
 	if (wait_event_info == 0)
 		return NULL;
 
-	classId = wait_event_info & 0xFF000000;
-	eventId = wait_event_info & 0x0000FFFF;
+	classId = wait_event_info & WAIT_EVENT_CLASS_MASK;
+	eventId = wait_event_info & WAIT_EVENT_ID_MASK;
 
 	switch (classId)
 	{
@@ -149,6 +317,9 @@ pgstat_get_wait_event(uint32 wait_event_info)
 		case PG_WAIT_LOCK:
 			event_name = GetLockNameFromTagType(eventId);
 			break;
+		case PG_WAIT_EXTENSION:
+			event_name = GetWaitEventExtensionIdentifier(eventId);
+			break;
 		case PG_WAIT_BUFFERPIN:
 			{
 				WaitEventBufferPin w = (WaitEventBufferPin) wait_event_info;
@@ -170,13 +341,6 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_client(w);
 				break;
 			}
-		case PG_WAIT_EXTENSION:
-			{
-				WaitEventExtension w = (WaitEventExtension) wait_event_info;
-
-				event_name = pgstat_get_wait_extension(w);
-				break;
-			}
 		case PG_WAIT_IPC:
 			{
 				WaitEventIPC w = (WaitEventIPC) wait_event_info;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3fabad96d9..2ea4789b00 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -261,7 +261,7 @@ WAIT_EVENT_BUFFER_PIN	BufferPin	"Waiting to acquire an exclusive pin on a buffer
 
 Section: ClassName - WaitEventExtension
 
-WAIT_EVENT_EXTENSION	Extension	"Waiting in an extension."
+WAIT_EVENT_DOCONLY	Extension	"Waiting in an extension."
 
 #
 # Wait events - LWLock
diff --git a/src/test/modules/worker_spi/t/001_worker_spi.pl b/src/test/modules/worker_spi/t/001_worker_spi.pl
index 74e109f9a1..0798653d00 100644
--- a/src/test/modules/worker_spi/t/001_worker_spi.pl
+++ b/src/test/modules/worker_spi/t/001_worker_spi.pl
@@ -39,6 +39,25 @@ $node->poll_query_until('postgres',
 $result = $node->safe_psql('postgres', 'SELECT * FROM schema4.counted;');
 is($result, qq(total|1), 'dynamic bgworker correctly consumed tuple data');
 
+# Check the wait event used by the dynamic bgworker.  For a session without
+# the state in shared memory known, the default of "extension" is the value
+# waited on.
+$result = $node->poll_query_until(
+	'postgres',
+	qq[SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
+	'extension');
+is($result, 1, 'dynamic bgworker has reported "extension" as wait event');
+# If the shared memory state is loaded, the wait event is the custom one.
+# The expected result is a special pattern here with a newline coming from the
+# first query where the shared memory state is set.
+$result = $node->poll_query_until(
+	'postgres',
+	qq[SELECT worker_spi_init(); SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
+	qq[
+worker_spi_main]);
+is($result, 1,
+	'dynamic bgworker has reported "worker_spi_main" as wait event');
+
 note "testing bgworkers loaded with shared_preload_libraries";
 
 # Create the database first so as the workers can connect to it when
@@ -58,9 +77,9 @@ $node->restart;
 # Check that bgworkers have been registered and launched.
 ok( $node->poll_query_until(
 		'mydb',
-		qq[SELECT datname, count(datname) FROM pg_stat_activity
-            WHERE backend_type = 'worker_spi' GROUP BY datname;],
-		'mydb|3'),
+		qq[SELECT datname, count(datname), wait_event FROM pg_stat_activity
+            WHERE backend_type = 'worker_spi' GROUP BY datname, wait_event;],
+		'mydb|3|worker_spi_main'),
 	'bgworkers all launched'
 ) or die "Timed out while waiting for bgworkers to be launched";
 
@@ -72,10 +91,10 @@ my $worker2_pid = $node->safe_psql('mydb', 'SELECT worker_spi_launch(11);');
 
 ok( $node->poll_query_until(
 		'mydb',
-		qq[SELECT datname, count(datname)  FROM pg_stat_activity
+		qq[SELECT datname, count(datname), wait_event FROM pg_stat_activity
             WHERE backend_type = 'worker_spi dynamic' AND
-            pid IN ($worker1_pid, $worker2_pid) GROUP BY datname;],
-		'mydb|2'),
+            pid IN ($worker1_pid, $worker2_pid) GROUP BY datname, wait_event;],
+		'mydb|2|worker_spi_main'),
 	'dynamic bgworkers all launched'
 ) or die "Timed out while waiting for dynamic bgworkers to be launched";
 
diff --git a/src/test/modules/worker_spi/worker_spi--1.0.sql b/src/test/modules/worker_spi/worker_spi--1.0.sql
index e9d5b07373..f13f7e0f98 100644
--- a/src/test/modules/worker_spi/worker_spi--1.0.sql
+++ b/src/test/modules/worker_spi/worker_spi--1.0.sql
@@ -7,3 +7,8 @@ CREATE FUNCTION worker_spi_launch(pg_catalog.int4)
 RETURNS pg_catalog.int4 STRICT
 AS 'MODULE_PATHNAME'
 LANGUAGE C;
+
+CREATE FUNCTION worker_spi_init()
+RETURNS VOID STRICT
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
diff --git a/src/test/modules/worker_spi/worker_spi.c b/src/test/modules/worker_spi/worker_spi.c
index 903dcddef9..e030d4b1de 100644
--- a/src/test/modules/worker_spi/worker_spi.c
+++ b/src/test/modules/worker_spi/worker_spi.c
@@ -44,22 +44,96 @@
 
 PG_MODULE_MAGIC;
 
+PG_FUNCTION_INFO_V1(worker_spi_init);
 PG_FUNCTION_INFO_V1(worker_spi_launch);
 
 PGDLLEXPORT void worker_spi_main(Datum main_arg) pg_attribute_noreturn();
 
+/* Shared memory state */
+typedef struct worker_spi_state
+{
+	/* the wait event defined during initialization phase */
+	uint32		wait_event;
+} worker_spi_state;
+
+static worker_spi_state *wsstate = NULL;	/* pointer to shared memory */
+
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_request_hook_type prev_shmem_startup_hook = NULL;
+
+static void worker_spi_shmem_request(void);
+static void worker_spi_shmem_startup(void);
+static void worker_spi_shmem_init(void);
+static Size worker_spi_memsize(void);
+
 /* GUC variables */
 static int	worker_spi_naptime = 10;
 static int	worker_spi_total_workers = 2;
 static char *worker_spi_database = NULL;
 
-
 typedef struct worktable
 {
 	const char *schema;
 	const char *name;
 } worktable;
 
+static void
+worker_spi_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(worker_spi_memsize());
+}
+
+static void
+worker_spi_shmem_startup(void)
+{
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	worker_spi_shmem_init();
+}
+
+static Size
+worker_spi_memsize(void)
+{
+	return MAXALIGN(sizeof(worker_spi_state));
+}
+
+/*
+ * Initialize the shared memory state of worker_spi.
+ *
+ * This routine allocates a new wait event when called the first time.
+ * On follow-up calls, the name of the wait event associated with the
+ * existing shared memory state is registered.
+ */
+static void
+worker_spi_shmem_init(void)
+{
+	bool		found;
+
+	wsstate = NULL;
+
+	/* Create or attach to the shared memory state */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+	wsstate = ShmemInitStruct("worker_spi State",
+							  sizeof(worker_spi_state),
+							  &found);
+
+	/* Define a new wait event */
+	if (!found)
+		wsstate->wait_event = WaitEventExtensionNew();
+
+	LWLockRelease(AddinShmemInitLock);
+
+	/*
+	 * Register the wait event in the lookup table of the current process.
+	 */
+	WaitEventExtensionRegisterName(wsstate->wait_event, "worker_spi_main");
+	return;
+}
+
 /*
  * Initialize workspace for a worker process: create the schema if it doesn't
  * already exist.
@@ -149,6 +223,9 @@ worker_spi_main(Datum main_arg)
 	/* We're now ready to receive signals */
 	BackgroundWorkerUnblockSignals();
 
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
 	/* Connect to our database */
 	BackgroundWorkerInitializeConnection(worker_spi_database, NULL, 0);
 
@@ -199,7 +276,7 @@ worker_spi_main(Datum main_arg)
 		(void) WaitLatch(MyLatch,
 						 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
 						 worker_spi_naptime * 1000L,
-						 WAIT_EVENT_EXTENSION);
+						 wsstate->wait_event);
 		ResetLatch(MyLatch);
 
 		CHECK_FOR_INTERRUPTS();
@@ -328,6 +405,11 @@ _PG_init(void)
 
 	MarkGUCPrefixReserved("worker_spi");
 
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = worker_spi_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = worker_spi_shmem_startup;
+
 	/* set up common data for all our workers */
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
@@ -351,6 +433,21 @@ _PG_init(void)
 	}
 }
 
+/*
+ * Wrapper to initialize a session with the shared memory state
+ * used by this module.  This is a convenience routine to be able to
+ * see the custom wait event stored in shared memory without loading
+ * through shared_preload_libraries.
+ */
+Datum
+worker_spi_init(PG_FUNCTION_ARGS)
+{
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
+	PG_RETURN_VOID();
+}
+
 /*
  * Dynamically launch an SPI worker.
  */
@@ -363,6 +460,9 @@ worker_spi_launch(PG_FUNCTION_ARGS)
 	BgwHandleStatus status;
 	pid_t		pid;
 
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 588b720f57..c97008eeb3 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1117,11 +1117,14 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
    <note>
     <para>
-     Extensions can add <literal>LWLock</literal> types to the list shown in
-     <xref linkend="wait-event-lwlock-table"/>.  In some cases, the name
+     Extensions can add <literal>Extension</literal> and
+     <literal>LWLock</literal> types
+     to the list shown in <xref linkend="wait-event-extension-table"/> and
+     <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
      assigned by an extension will not be available in all server processes;
-     so an <literal>LWLock</literal> wait event might be reported as
-     just <quote><literal>extension</literal></quote> rather than the
+     so an <literal>LWLock</literal> or <literal>Extension</literal> wait
+     event might be reported as just
+     <quote><literal>extension</literal></quote> rather than the
      extension-assigned name.
     </para>
    </note>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 9620ea9ae3..d6345a775b 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3453,6 +3453,51 @@ if (!ptr)
     </para>
    </sect2>
 
+   <sect2 id="xfunc-addin-wait-events">
+    <title>Shared Memory and Custom Wait Events</title>
+
+    <para>
+     Add-ins can define custom wait events under the wait event type
+     <literal>Extension</literal>. The add-in's shared library must be
+     preloaded by specifying it in <literal>shared_preload_libraries</literal>,
+     and register a <literal>shmem_request_hook</literal> and a
+     <literal>shmem_startup_hook</literal> in its
+     <function>_PG_init</function> function.
+     <literal>shmem_request_hook</literal> can request a shared memory size
+     to be later used at startup by calling:
+<programlisting>
+void RequestAddinShmemSpace(int size)
+</programlisting>
+    </para>
+    <para>
+     <literal>shmem_startup_hook</literal> can allocate in shared memory
+     custom wait events by calling while holding the LWLock
+     <function>AddinShmemInitLock</function> to avoid any race conditions:
+<programlisting>
+uint32 WaitEventExtensionNew(void)
+</programlisting>
+     Next, each process needs to associate the wait event allocated previously
+     to a user-facing custom string, which is something done by calling:
+<programlisting>
+void WaitEventExtensionRegisterName(uint32 wait_event_info, const char *wait_event_name)
+</programlisting>
+     An example can be found in <filename>src/test/modules/worker_spi</filename>
+     in the PostgreSQL source tree.
+    </para>
+    <para>
+     Custom wait events can be viewed in
+     <link linkend="monitoring-pg-stat-activity-view"><structname>pg_stat_activity</structname></link>:
+<screen>
+=# SELECT wait_event_type, wait_event FROM pg_stat_activity
+     WHERE backend_type ~ 'worker_spi';
+ wait_event_type |   wait_event
+-----------------+-----------------
+ Extension       | worker_spi_main
+(1 row)
+</screen>
+    </para>
+   </sect2>
+
    <sect2 id="extend-cpp">
     <title>Using C++ for Extensibility</title>
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 11d47294cf..ab97f1794a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2992,6 +2992,7 @@ WaitEventActivity
 WaitEventBufferPin
 WaitEventClient
 WaitEventExtension
+WaitEventExtensionCounterData
 WaitEventIO
 WaitEventIPC
 WaitEventSet
@@ -3862,6 +3863,7 @@ wchar2mb_with_len_converter
 wchar_t
 win32_deadchild_waitinfo
 wint_t
+worker_spi_state
 worker_state
 worktable
 wrap
-- 
2.40.1

#40Bharath Rupireddy
bharath.rupireddyforpostgres@gmail.com
In reply to: Michael Paquier (#39)
Re: Support to define custom wait events for extensions

On Mon, Jul 31, 2023 at 6:40 AM Michael Paquier <michael@paquier.xyz> wrote:

You are right that I am making things inconsistent here. Having a
behavior close to the existing LWLock and use "extension" when the
event cannot be found makes the most sense. I have been a bit
confused with the wording though of this part of the docs, though, as
LWLocks don't directly use the custom wait event APIs.

+     * calling WaitEventExtensionRegisterName() in the current process, in
+     * which case give up and return an unknown state.

We're not giving up and returning an unknown state in the v10 patch
unlike v9, no? This comment needs to change.

4.
+ Add-ins can define custom wait events under the wait event type

I see a few instances of Add-ins/add-in in xfunc.sgml. Isn't it better
to use the word extension given that glossary defines what an
extension is https://www.postgresql.org/docs/current/glossary.html#GLOSSARY-EXTENSION?

An extension is an Add-in, and may be loaded, but it is possible to
have modules that just need to be LOAD'ed (with command line or just
shared_preload_libraries) so an add-in may not always be an extension.
I am not completely sure if add-ins is the best term, but it covers
both, and that's consistent with the existing docs. Perhaps the same
area of the docs should be refreshed, but that looks like a separate
patch for me. For now, I'd rather use a consistent term, and this one
sounds OK to me.

[1]: https://www.postgresql.org/docs/devel/extend-extensions.html.

The "external module" seems the right wording here. Use of "add-ins"
is fine by me for this patch.

Okay by me that it may be a better idea to use ereport(ERROR) in the
long run, so changed this way. I have introduced a
WAIT_EVENT_CLASS_MASK and a WAIT_EVENT_ID_MASK as we now use
0xFF000000 and 0x0000FFFF in three places of this file. This should
just be a patch on its own.

Yeah, I don't mind these macros going along or before or after the
custom wait events feature.

Yes, this was mentioned upthread. I am not completely sure yet how
much we need to do for this interface, but surely it would be faster
to have a Multiple() interface that returns an array made of N numbers
requested (rather than a rank of them). For now, I'd rather just aim
for simplicity for the basics.

+1 to be simple for now. If any such requests come in future, I'm sure
we can always get back to it.

9.
# The expected result is a special pattern here with a newline coming from the
# first query where the shared memory state is set.
$result = $node->poll_query_until(
'postgres',
qq[SELECT worker_spi_init(); SELECT wait_event FROM
pg_stat_activity WHERE backend_type ~ 'worker_spi';],
qq[
worker_spi_main]);

This test doesn't have to be that complex with the result being a
special pattern, SELECT worker_spi_init(); can just be within a
separate safe_psql.

No, it cannot because we need the custom wait event string to be
loaded in the same connection as the one querying pg_stat_activity.
A different thing that can be done here is to use background_psql()
with a query_until(), though I am not sure that this is worth doing
here.

-1 to go to the background_psql and query_until route. However,
enhancing the comment might help "we need the custom wait event string
to be loaded in the same connection as .....". Having said this, I
don't quite understand the point of having worker_spi_init() when we
say clearly how to use shmem hooks and custom wait events. If its
intention is to show loading of shared memory to a backend via a
function, do we really need it in worker_spi? I don't mind removing it
if it's not adding any significant value.

--
Bharath Rupireddy
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

#41Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#39)
Re: Support to define custom wait events for extensions

On 2023-07-31 10:10, Michael Paquier wrote:

Attached is a new version.

Thanks for all the improvements.
I have some comments for v10.

(1)

     <note>
      <para>
-     Extensions can add <literal>LWLock</literal> types to the list 
shown in
-     <xref linkend="wait-event-lwlock-table"/>.  In some cases, the 
name
+     Extensions can add <literal>Extension</literal> and
+     <literal>LWLock</literal> types
+     to the list shown in <xref linkend="wait-event-extension-table"/> 
and
+     <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
       assigned by an extension will not be available in all server 
processes;
-     so an <literal>LWLock</literal> wait event might be reported as
-     just <quote><literal>extension</literal></quote> rather than the
+     so an <literal>LWLock</literal> or <literal>Extension</literal> 
wait
+     event might be reported as just
+     <quote><literal>extension</literal></quote> rather than the
       extension-assigned name.
      </para>
     </note>
I think the order in which they are mentioned should be matched. I mean 
that
-     so an <literal>LWLock</literal> or <literal>Extension</literal> 
wait
+     so an <literal>Extension</literal> or <literal>LWLock</literal> 
wait

(2)

/* This should only be called for user-defined wait event. */
if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
ereport(ERROR,
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("invalid wait event ID %u", eventId));

I was just wondering if it should also check the eventId
that has been allocated though it needs to take the spinlock
and GetWaitEventExtensionIdentifier() doesn't take it into account.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#42Michael Paquier
michael@paquier.xyz
In reply to: Bharath Rupireddy (#40)
Re: Support to define custom wait events for extensions

On Mon, Jul 31, 2023 at 12:07:40PM +0530, Bharath Rupireddy wrote:

We're not giving up and returning an unknown state in the v10 patch
unlike v9, no? This comment needs to change.

Right. Better to be consistent with lwlock.c here.

No, it cannot because we need the custom wait event string to be
loaded in the same connection as the one querying pg_stat_activity.
A different thing that can be done here is to use background_psql()
with a query_until(), though I am not sure that this is worth doing
here.

-1 to go to the background_psql and query_until route. However,
enhancing the comment might help "we need the custom wait event string
to be loaded in the same connection as .....". Having said this, I
don't quite understand the point of having worker_spi_init() when we
say clearly how to use shmem hooks and custom wait events. If its
intention is to show loading of shared memory to a backend via a
function, do we really need it in worker_spi? I don't mind removing it
if it's not adding any significant value.

It seems to initialize the state of the worker_spi, so attaching a
function to this stuff makes sense to me, just for the sake of testing
all that.
--
Michael

#43Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#41)
1 attachment(s)
Re: Support to define custom wait events for extensions

On Mon, Jul 31, 2023 at 03:53:27PM +0900, Masahiro Ikeda wrote:

I think the order in which they are mentioned should be matched. I mean that
-     so an <literal>LWLock</literal> or <literal>Extension</literal> wait
+     so an <literal>Extension</literal> or <literal>LWLock</literal> wait

Makes sense.

/* This should only be called for user-defined wait event. */
if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
ereport(ERROR,
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("invalid wait event ID %u", eventId));

I was just wondering if it should also check the eventId
that has been allocated though it needs to take the spinlock
and GetWaitEventExtensionIdentifier() doesn't take it into account.

What kind of extra check do you have in mind? Once WAIT_EVENT_ID_MASK
is applied, we already know that we don't have something larger than
PG_UNIT16_MAX, or perhaps you want to cross-check this number with
what nextId holds in shared memory and that we don't have a number
between nextId and PG_UNIT16_MAX? I am not sure that we need to care
much about that this much in this code path, and I'd rather avoid
taking an extra time the spinlock just for a cross-check.

Attaching a v11 based on Bharath's feedback and yours, for now. I
have also applied the addition of the two masking variables in
wait_event.c separately with 7395a90.
--
Michael

Attachments:

v11-0001-Support-custom-wait-events-for-wait-event-type-E.patchtext/x-diff; charset=us-asciiDownload
From 23b6c4c36fbf23ca9c6fb4f42a3f943be44689a6 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 31 Jul 2023 16:04:20 +0900
Subject: [PATCH v11] Support custom wait events for wait event type
 "Extension"

Two backend routines are added to allow extension to allocate and define
custom wait events, all of these being allocated as of type "Extension":
* WaitEventExtensionNew(), that allocates a wait event ID computed from
a counter in shared memory.
* WaitEventExtensionRegisterName(), to associate a custom string to the
wait event registered.

Note that this includes an example of how to use this new facility in
worker_spi, with tests for various scenarios.

The use of these routines should be extended across more in-core
modules, but this is left as work for future patches.
---
 src/include/utils/wait_event.h                |  26 +++
 src/backend/storage/ipc/ipci.c                |   3 +
 .../activity/generate-wait_event_types.pl     |   7 +-
 src/backend/utils/activity/wait_event.c       | 178 +++++++++++++++++-
 .../utils/activity/wait_event_names.txt       |   2 +-
 .../modules/worker_spi/t/001_worker_spi.pl    |  34 +++-
 .../modules/worker_spi/worker_spi--1.0.sql    |   5 +
 src/test/modules/worker_spi/worker_spi.c      | 104 +++++++++-
 doc/src/sgml/monitoring.sgml                  |  11 +-
 doc/src/sgml/xfunc.sgml                       |  45 +++++
 src/tools/pgindent/typedefs.list              |   2 +
 11 files changed, 393 insertions(+), 24 deletions(-)

diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index 4517425f84..aad8bc08fa 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -38,6 +38,32 @@ extern void pgstat_reset_wait_event_storage(void);
 extern PGDLLIMPORT uint32 *my_wait_event_info;
 
 
+/* ----------
+ * Wait Events - Extension
+ *
+ * Use this category when the server process is waiting for some condition
+ * defined by an extension module.
+ *
+ * Extensions can define their own wait events in this category.  First,
+ * they should call WaitEventExtensionNew() to get one or more wait event
+ * IDs that are allocated from a shared counter.  These can be used directly
+ * with pgstat_report_wait_start() or equivalent.  Next, each individual
+ * process should call WaitEventExtensionRegisterName() to associate a wait
+ * event string to the number allocated previously.
+ */
+typedef enum
+{
+	WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
+	WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED
+} WaitEventExtension;
+
+extern void WaitEventExtensionShmemInit(void);
+extern Size WaitEventExtensionShmemSize(void);
+
+extern uint32 WaitEventExtensionNew(void);
+extern void WaitEventExtensionRegisterName(uint32 wait_event_info,
+										   const char *wait_event_name);
+
 /* ----------
  * pgstat_report_wait_start() -
  *
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index cc387c00a1..5551afffc0 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -49,6 +49,7 @@
 #include "storage/spin.h"
 #include "utils/guc.h"
 #include "utils/snapmgr.h"
+#include "utils/wait_event.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -142,6 +143,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, SyncScanShmemSize());
 	size = add_size(size, AsyncShmemSize());
 	size = add_size(size, StatsShmemSize());
+	size = add_size(size, WaitEventExtensionShmemSize());
 #ifdef EXEC_BACKEND
 	size = add_size(size, ShmemBackendArraySize());
 #endif
@@ -301,6 +303,7 @@ CreateSharedMemoryAndSemaphores(void)
 	SyncScanShmemInit();
 	AsyncShmemInit();
 	StatsShmemInit();
+	WaitEventExtensionShmemInit();
 
 #ifdef EXEC_BACKEND
 
diff --git a/src/backend/utils/activity/generate-wait_event_types.pl b/src/backend/utils/activity/generate-wait_event_types.pl
index f63c991051..56335e8730 100644
--- a/src/backend/utils/activity/generate-wait_event_types.pl
+++ b/src/backend/utils/activity/generate-wait_event_types.pl
@@ -133,10 +133,11 @@ if ($gen_code)
 	foreach my $waitclass (sort { uc($a) cmp uc($b) } keys %hashwe)
 	{
 
-		# Don't generate .c and .h files for LWLock and Lock, these are
-		# handled independently.
+		# Don't generate .c and .h files for Extension, LWLock and
+		# Lock, these are handled independently.
 		next
-		  if ( $waitclass eq 'WaitEventLWLock'
+		  if ( $waitclass eq 'WaitEventExtension'
+			|| $waitclass eq 'WaitEventLWLock'
 			|| $waitclass eq 'WaitEventLock');
 
 		my $last = $waitclass;
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index 59177de7a0..b3596ece80 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,15 +22,18 @@
  */
 #include "postgres.h"
 
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
 #include "storage/lwlock.h"		/* for GetLWLockIdentifier */
+#include "storage/spin.h"
+#include "utils/memutils.h"
 #include "utils/wait_event.h"
 
 
 static const char *pgstat_get_wait_activity(WaitEventActivity w);
 static const char *pgstat_get_wait_bufferpin(WaitEventBufferPin w);
 static const char *pgstat_get_wait_client(WaitEventClient w);
-static const char *pgstat_get_wait_extension(WaitEventExtension w);
 static const char *pgstat_get_wait_ipc(WaitEventIPC w);
 static const char *pgstat_get_wait_timeout(WaitEventTimeout w);
 static const char *pgstat_get_wait_io(WaitEventIO w);
@@ -42,6 +45,169 @@ uint32	   *my_wait_event_info = &local_my_wait_event_info;
 #define WAIT_EVENT_CLASS_MASK	0xFF000000
 #define WAIT_EVENT_ID_MASK		0x0000FFFF
 
+/* dynamic allocation counter for custom wait events in extensions */
+typedef struct WaitEventExtensionCounterData
+{
+	int			nextId;			/* next ID to assign */
+	slock_t		mutex;			/* protects the counter */
+} WaitEventExtensionCounterData;
+
+/* pointer to the shared memory */
+static WaitEventExtensionCounterData *WaitEventExtensionCounter;
+
+/* first event ID of custom wait events for extensions */
+#define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
+	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
+
+/*
+ * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
+ * stores the names of all dynamically-created event IDs known to the current
+ * process.  Any unused entries in the array will contain NULL.
+ */
+static const char **WaitEventExtensionNames = NULL;
+static int	WaitEventExtensionNamesAllocated = 0;
+
+static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
+
+/*
+ *  Return the space for dynamic allocation counter.
+ */
+Size
+WaitEventExtensionShmemSize(void)
+{
+	return sizeof(WaitEventExtensionCounterData);
+}
+
+/*
+ * Allocate shmem space for dynamic allocation counter.
+ */
+void
+WaitEventExtensionShmemInit(void)
+{
+	bool		found;
+
+	WaitEventExtensionCounter = (WaitEventExtensionCounterData *)
+		ShmemInitStruct("WaitEventExtensionCounterData",
+						WaitEventExtensionShmemSize(), &found);
+
+	if (!found)
+	{
+		/* initialize the allocation counter and its spinlock. */
+		WaitEventExtensionCounter->nextId = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+		SpinLockInit(&WaitEventExtensionCounter->mutex);
+	}
+}
+
+/*
+ * Allocate a new event ID and return the wait event.
+ */
+uint32
+WaitEventExtensionNew(void)
+{
+	uint16		eventId;
+
+	Assert(LWLockHeldByMeInMode(AddinShmemInitLock, LW_EXCLUSIVE));
+
+	SpinLockAcquire(&WaitEventExtensionCounter->mutex);
+
+	if (WaitEventExtensionCounter->nextId > PG_UINT16_MAX)
+	{
+		SpinLockRelease(&WaitEventExtensionCounter->mutex);
+		ereport(ERROR,
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("too many wait events for extensions"));
+	}
+
+	eventId = WaitEventExtensionCounter->nextId++;
+
+	SpinLockRelease(&WaitEventExtensionCounter->mutex);
+
+	return PG_WAIT_EXTENSION | eventId;
+}
+
+/*
+ * Register a dynamic wait event name for extension in the lookup table
+ * of the current process.
+ *
+ * This routine will save a pointer to the wait event name passed as an argument,
+ * so the name should be allocated in a backend-lifetime context
+ * (shared memory, TopMemoryContext, static constant, or similar).
+ *
+ * The "wait_event_name" will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
+ */
+void
+WaitEventExtensionRegisterName(uint32 wait_event_info,
+							   const char *wait_event_name)
+{
+	uint32		classId;
+	uint16		eventId;
+
+	classId = wait_event_info & WAIT_EVENT_CLASS_MASK;
+	eventId = wait_event_info & WAIT_EVENT_ID_MASK;
+
+	/* Check the wait event class. */
+	if (classId != PG_WAIT_EXTENSION)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid wait event class %u", classId));
+
+	/* This should only be called for user-defined wait event. */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid wait event ID %u", eventId));
+
+	/* Convert to array index. */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	/* If necessary, create or enlarge array. */
+	if (eventId >= WaitEventExtensionNamesAllocated)
+	{
+		uint32		newalloc;
+
+		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
+
+		if (WaitEventExtensionNames == NULL)
+			WaitEventExtensionNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		else
+			WaitEventExtensionNames =
+				repalloc0_array(WaitEventExtensionNames, const char *,
+								WaitEventExtensionNamesAllocated, newalloc);
+		WaitEventExtensionNamesAllocated = newalloc;
+	}
+
+	WaitEventExtensionNames[eventId] = wait_event_name;
+}
+
+/*
+ * Return the name of an wait event ID for extension.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)
+{
+	/* Built-in event? */
+	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
+		return "Extension";
+
+	/*
+	 * It is a user-defined wait event, so look at WaitEventExtensionNames[].
+	 * However, it is possible that the name has never been registered by
+	 * calling WaitEventExtensionRegisterName() in the current process, in
+	 * which case give up and return "extension".
+	 */
+	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+
+	if (eventId >= WaitEventExtensionNamesAllocated ||
+		WaitEventExtensionNames[eventId] == NULL)
+		return "extension";
+
+	return WaitEventExtensionNames[eventId];
+}
+
+
 /*
  * Configure wait event reporting to report wait events to *wait_event_info.
  * *wait_event_info needs to be valid until pgstat_reset_wait_event_storage()
@@ -151,6 +317,9 @@ pgstat_get_wait_event(uint32 wait_event_info)
 		case PG_WAIT_LOCK:
 			event_name = GetLockNameFromTagType(eventId);
 			break;
+		case PG_WAIT_EXTENSION:
+			event_name = GetWaitEventExtensionIdentifier(eventId);
+			break;
 		case PG_WAIT_BUFFERPIN:
 			{
 				WaitEventBufferPin w = (WaitEventBufferPin) wait_event_info;
@@ -172,13 +341,6 @@ pgstat_get_wait_event(uint32 wait_event_info)
 				event_name = pgstat_get_wait_client(w);
 				break;
 			}
-		case PG_WAIT_EXTENSION:
-			{
-				WaitEventExtension w = (WaitEventExtension) wait_event_info;
-
-				event_name = pgstat_get_wait_extension(w);
-				break;
-			}
 		case PG_WAIT_IPC:
 			{
 				WaitEventIPC w = (WaitEventIPC) wait_event_info;
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3fabad96d9..2ea4789b00 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -261,7 +261,7 @@ WAIT_EVENT_BUFFER_PIN	BufferPin	"Waiting to acquire an exclusive pin on a buffer
 
 Section: ClassName - WaitEventExtension
 
-WAIT_EVENT_EXTENSION	Extension	"Waiting in an extension."
+WAIT_EVENT_DOCONLY	Extension	"Waiting in an extension."
 
 #
 # Wait events - LWLock
diff --git a/src/test/modules/worker_spi/t/001_worker_spi.pl b/src/test/modules/worker_spi/t/001_worker_spi.pl
index 74e109f9a1..c3e7f5fbe6 100644
--- a/src/test/modules/worker_spi/t/001_worker_spi.pl
+++ b/src/test/modules/worker_spi/t/001_worker_spi.pl
@@ -39,6 +39,28 @@ $node->poll_query_until('postgres',
 $result = $node->safe_psql('postgres', 'SELECT * FROM schema4.counted;');
 is($result, qq(total|1), 'dynamic bgworker correctly consumed tuple data');
 
+# Check the wait event used by the dynamic bgworker.  For a session without
+# the state in shared memory known, the default of "extension" is the value
+# waited on.
+$result = $node->poll_query_until(
+	'postgres',
+	qq[SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
+	'extension');
+is($result, 1, 'dynamic bgworker has reported "extension" as wait event');
+
+# If the shared memory state is loaded (here with worker_spi_init within
+# the same connection as the one querying pg_stat_activity), the wait
+# event is the custom one.
+# The expected result is a special pattern here with a newline coming from the
+# first query where the shared memory state is set.
+$result = $node->poll_query_until(
+	'postgres',
+	qq[SELECT worker_spi_init(); SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
+	qq[
+worker_spi_main]);
+is($result, 1,
+	'dynamic bgworker has reported "worker_spi_main" as wait event');
+
 note "testing bgworkers loaded with shared_preload_libraries";
 
 # Create the database first so as the workers can connect to it when
@@ -58,9 +80,9 @@ $node->restart;
 # Check that bgworkers have been registered and launched.
 ok( $node->poll_query_until(
 		'mydb',
-		qq[SELECT datname, count(datname) FROM pg_stat_activity
-            WHERE backend_type = 'worker_spi' GROUP BY datname;],
-		'mydb|3'),
+		qq[SELECT datname, count(datname), wait_event FROM pg_stat_activity
+            WHERE backend_type = 'worker_spi' GROUP BY datname, wait_event;],
+		'mydb|3|worker_spi_main'),
 	'bgworkers all launched'
 ) or die "Timed out while waiting for bgworkers to be launched";
 
@@ -72,10 +94,10 @@ my $worker2_pid = $node->safe_psql('mydb', 'SELECT worker_spi_launch(11);');
 
 ok( $node->poll_query_until(
 		'mydb',
-		qq[SELECT datname, count(datname)  FROM pg_stat_activity
+		qq[SELECT datname, count(datname), wait_event FROM pg_stat_activity
             WHERE backend_type = 'worker_spi dynamic' AND
-            pid IN ($worker1_pid, $worker2_pid) GROUP BY datname;],
-		'mydb|2'),
+            pid IN ($worker1_pid, $worker2_pid) GROUP BY datname, wait_event;],
+		'mydb|2|worker_spi_main'),
 	'dynamic bgworkers all launched'
 ) or die "Timed out while waiting for dynamic bgworkers to be launched";
 
diff --git a/src/test/modules/worker_spi/worker_spi--1.0.sql b/src/test/modules/worker_spi/worker_spi--1.0.sql
index e9d5b07373..f13f7e0f98 100644
--- a/src/test/modules/worker_spi/worker_spi--1.0.sql
+++ b/src/test/modules/worker_spi/worker_spi--1.0.sql
@@ -7,3 +7,8 @@ CREATE FUNCTION worker_spi_launch(pg_catalog.int4)
 RETURNS pg_catalog.int4 STRICT
 AS 'MODULE_PATHNAME'
 LANGUAGE C;
+
+CREATE FUNCTION worker_spi_init()
+RETURNS VOID STRICT
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
diff --git a/src/test/modules/worker_spi/worker_spi.c b/src/test/modules/worker_spi/worker_spi.c
index 903dcddef9..e030d4b1de 100644
--- a/src/test/modules/worker_spi/worker_spi.c
+++ b/src/test/modules/worker_spi/worker_spi.c
@@ -44,22 +44,96 @@
 
 PG_MODULE_MAGIC;
 
+PG_FUNCTION_INFO_V1(worker_spi_init);
 PG_FUNCTION_INFO_V1(worker_spi_launch);
 
 PGDLLEXPORT void worker_spi_main(Datum main_arg) pg_attribute_noreturn();
 
+/* Shared memory state */
+typedef struct worker_spi_state
+{
+	/* the wait event defined during initialization phase */
+	uint32		wait_event;
+} worker_spi_state;
+
+static worker_spi_state *wsstate = NULL;	/* pointer to shared memory */
+
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_request_hook_type prev_shmem_startup_hook = NULL;
+
+static void worker_spi_shmem_request(void);
+static void worker_spi_shmem_startup(void);
+static void worker_spi_shmem_init(void);
+static Size worker_spi_memsize(void);
+
 /* GUC variables */
 static int	worker_spi_naptime = 10;
 static int	worker_spi_total_workers = 2;
 static char *worker_spi_database = NULL;
 
-
 typedef struct worktable
 {
 	const char *schema;
 	const char *name;
 } worktable;
 
+static void
+worker_spi_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(worker_spi_memsize());
+}
+
+static void
+worker_spi_shmem_startup(void)
+{
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	worker_spi_shmem_init();
+}
+
+static Size
+worker_spi_memsize(void)
+{
+	return MAXALIGN(sizeof(worker_spi_state));
+}
+
+/*
+ * Initialize the shared memory state of worker_spi.
+ *
+ * This routine allocates a new wait event when called the first time.
+ * On follow-up calls, the name of the wait event associated with the
+ * existing shared memory state is registered.
+ */
+static void
+worker_spi_shmem_init(void)
+{
+	bool		found;
+
+	wsstate = NULL;
+
+	/* Create or attach to the shared memory state */
+	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+	wsstate = ShmemInitStruct("worker_spi State",
+							  sizeof(worker_spi_state),
+							  &found);
+
+	/* Define a new wait event */
+	if (!found)
+		wsstate->wait_event = WaitEventExtensionNew();
+
+	LWLockRelease(AddinShmemInitLock);
+
+	/*
+	 * Register the wait event in the lookup table of the current process.
+	 */
+	WaitEventExtensionRegisterName(wsstate->wait_event, "worker_spi_main");
+	return;
+}
+
 /*
  * Initialize workspace for a worker process: create the schema if it doesn't
  * already exist.
@@ -149,6 +223,9 @@ worker_spi_main(Datum main_arg)
 	/* We're now ready to receive signals */
 	BackgroundWorkerUnblockSignals();
 
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
 	/* Connect to our database */
 	BackgroundWorkerInitializeConnection(worker_spi_database, NULL, 0);
 
@@ -199,7 +276,7 @@ worker_spi_main(Datum main_arg)
 		(void) WaitLatch(MyLatch,
 						 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
 						 worker_spi_naptime * 1000L,
-						 WAIT_EVENT_EXTENSION);
+						 wsstate->wait_event);
 		ResetLatch(MyLatch);
 
 		CHECK_FOR_INTERRUPTS();
@@ -328,6 +405,11 @@ _PG_init(void)
 
 	MarkGUCPrefixReserved("worker_spi");
 
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = worker_spi_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = worker_spi_shmem_startup;
+
 	/* set up common data for all our workers */
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
@@ -351,6 +433,21 @@ _PG_init(void)
 	}
 }
 
+/*
+ * Wrapper to initialize a session with the shared memory state
+ * used by this module.  This is a convenience routine to be able to
+ * see the custom wait event stored in shared memory without loading
+ * through shared_preload_libraries.
+ */
+Datum
+worker_spi_init(PG_FUNCTION_ARGS)
+{
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
+	PG_RETURN_VOID();
+}
+
 /*
  * Dynamically launch an SPI worker.
  */
@@ -363,6 +460,9 @@ worker_spi_launch(PG_FUNCTION_ARGS)
 	BgwHandleStatus status;
 	pid_t		pid;
 
+	/* Create (if necessary) and attach to our shared memory area. */
+	worker_spi_shmem_init();
+
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 588b720f57..f4fc5d814f 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1117,11 +1117,14 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
    <note>
     <para>
-     Extensions can add <literal>LWLock</literal> types to the list shown in
-     <xref linkend="wait-event-lwlock-table"/>.  In some cases, the name
+     Extensions can add <literal>Extension</literal> and
+     <literal>LWLock</literal> types
+     to the list shown in <xref linkend="wait-event-extension-table"/> and
+     <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
      assigned by an extension will not be available in all server processes;
-     so an <literal>LWLock</literal> wait event might be reported as
-     just <quote><literal>extension</literal></quote> rather than the
+     so an <literal>Extension</literal> or <literal>LWLock</literal> wait
+     event might be reported as just
+     <quote><literal>extension</literal></quote> rather than the
      extension-assigned name.
     </para>
    </note>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 9620ea9ae3..d6345a775b 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3453,6 +3453,51 @@ if (!ptr)
     </para>
    </sect2>
 
+   <sect2 id="xfunc-addin-wait-events">
+    <title>Shared Memory and Custom Wait Events</title>
+
+    <para>
+     Add-ins can define custom wait events under the wait event type
+     <literal>Extension</literal>. The add-in's shared library must be
+     preloaded by specifying it in <literal>shared_preload_libraries</literal>,
+     and register a <literal>shmem_request_hook</literal> and a
+     <literal>shmem_startup_hook</literal> in its
+     <function>_PG_init</function> function.
+     <literal>shmem_request_hook</literal> can request a shared memory size
+     to be later used at startup by calling:
+<programlisting>
+void RequestAddinShmemSpace(int size)
+</programlisting>
+    </para>
+    <para>
+     <literal>shmem_startup_hook</literal> can allocate in shared memory
+     custom wait events by calling while holding the LWLock
+     <function>AddinShmemInitLock</function> to avoid any race conditions:
+<programlisting>
+uint32 WaitEventExtensionNew(void)
+</programlisting>
+     Next, each process needs to associate the wait event allocated previously
+     to a user-facing custom string, which is something done by calling:
+<programlisting>
+void WaitEventExtensionRegisterName(uint32 wait_event_info, const char *wait_event_name)
+</programlisting>
+     An example can be found in <filename>src/test/modules/worker_spi</filename>
+     in the PostgreSQL source tree.
+    </para>
+    <para>
+     Custom wait events can be viewed in
+     <link linkend="monitoring-pg-stat-activity-view"><structname>pg_stat_activity</structname></link>:
+<screen>
+=# SELECT wait_event_type, wait_event FROM pg_stat_activity
+     WHERE backend_type ~ 'worker_spi';
+ wait_event_type |   wait_event
+-----------------+-----------------
+ Extension       | worker_spi_main
+(1 row)
+</screen>
+    </para>
+   </sect2>
+
    <sect2 id="extend-cpp">
     <title>Using C++ for Extensibility</title>
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 11d47294cf..ab97f1794a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2992,6 +2992,7 @@ WaitEventActivity
 WaitEventBufferPin
 WaitEventClient
 WaitEventExtension
+WaitEventExtensionCounterData
 WaitEventIO
 WaitEventIPC
 WaitEventSet
@@ -3862,6 +3863,7 @@ wchar2mb_with_len_converter
 wchar_t
 win32_deadchild_waitinfo
 wint_t
+worker_spi_state
 worker_state
 worktable
 wrap
-- 
2.40.1

#44Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#43)
Re: Support to define custom wait events for extensions

On 2023-07-31 16:28, Michael Paquier wrote:

On Mon, Jul 31, 2023 at 03:53:27PM +0900, Masahiro Ikeda wrote:

/* This should only be called for user-defined wait event. */
if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
ereport(ERROR,
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("invalid wait event ID %u", eventId));

I was just wondering if it should also check the eventId
that has been allocated though it needs to take the spinlock
and GetWaitEventExtensionIdentifier() doesn't take it into account.

What kind of extra check do you have in mind? Once WAIT_EVENT_ID_MASK
is applied, we already know that we don't have something larger than
PG_UNIT16_MAX, or perhaps you want to cross-check this number with
what nextId holds in shared memory and that we don't have a number
between nextId and PG_UNIT16_MAX? I am not sure that we need to care
much about that this much in this code path, and I'd rather avoid
taking an extra time the spinlock just for a cross-check.

OK. I assumed to check that we don't have a number between nextId and
PG_UNIT16_MAX.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#45Bharath Rupireddy
bharath.rupireddyforpostgres@gmail.com
In reply to: Michael Paquier (#43)
Re: Support to define custom wait events for extensions

On Mon, Jul 31, 2023 at 12:58 PM Michael Paquier <michael@paquier.xyz> wrote:

Attaching a v11 based on Bharath's feedback and yours, for now. I
have also applied the addition of the two masking variables in
wait_event.c separately with 7395a90.

+uint32 WaitEventExtensionNew(void)
+</programlisting>
+     Next, each process needs to associate the wait event allocated previously
+     to a user-facing custom string, which is something done by calling:
+<programlisting>
+void WaitEventExtensionRegisterName(uint32 wait_event_info, const
char *wait_event_name)
+</programlisting>
+     An example can be found in
<filename>src/test/modules/worker_spi</filename>
+     in the PostgreSQL source tree.
+    </para>

Do you think it's worth adding a note here in the docs about an
external module defining more than one custom wait event? A pseudo
code if possible or just a note? Also, how about a XXX comment atop
WaitEventExtensionNew and/or WaitEventExtensionRegisterName on the
possibility of extending the functions to support allocation of more
than one custom wait events?

--
Bharath Rupireddy
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

#46Kyotaro Horiguchi
horikyota.ntt@gmail.com
In reply to: Michael Paquier (#43)
Re: Support to define custom wait events for extensions

At Mon, 31 Jul 2023 16:28:16 +0900, Michael Paquier <michael@paquier.xyz> wrote in

Attaching a v11 based on Bharath's feedback and yours, for now. I
have also applied the addition of the two masking variables in
wait_event.c separately with 7395a90.

+/*
+ * Return the name of an wait event ID for extension.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)

This looks inconsistent. Shouldn't it be GetWaitEventExtentionName()?

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#47Michael Paquier
michael@paquier.xyz
In reply to: Bharath Rupireddy (#45)
Re: Support to define custom wait events for extensions

On Mon, Jul 31, 2023 at 01:37:49PM +0530, Bharath Rupireddy wrote:

Do you think it's worth adding a note here in the docs about an
external module defining more than one custom wait event? A pseudo
code if possible or just a note? Also, how about a XXX comment atop
WaitEventExtensionNew and/or WaitEventExtensionRegisterName on the
possibility of extending the functions to support allocation of more
than one custom wait events?

I am not sure that any of that is necessary. Anyway, I have applied
v11 to get the basics done.

Now, I agree that a WaitEventExtensionMultiple() may come in handy,
particularly for postgres_fdw that uses WAIT_EVENT_EXTENSION three
times.
--
Michael

#48Michael Paquier
michael@paquier.xyz
In reply to: Kyotaro Horiguchi (#46)
Re: Support to define custom wait events for extensions

On Mon, Jul 31, 2023 at 05:10:21PM +0900, Kyotaro Horiguchi wrote:

+/*
+ * Return the name of an wait event ID for extension.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)

This looks inconsistent. Shouldn't it be GetWaitEventExtentionName()?

This is an inspiration from GetLWLockIdentifier(), which is kind of OK
by me. If there is a consensus in changing that, fine by me, of
course.
--
Michael

#49Bharath Rupireddy
bharath.rupireddyforpostgres@gmail.com
In reply to: Michael Paquier (#48)
Re: Support to define custom wait events for extensions

On Mon, Jul 31, 2023 at 3:54 PM Michael Paquier <michael@paquier.xyz> wrote:

On Mon, Jul 31, 2023 at 05:10:21PM +0900, Kyotaro Horiguchi wrote:

+/*
+ * Return the name of an wait event ID for extension.
+ */
+static const char *
+GetWaitEventExtensionIdentifier(uint16 eventId)

This looks inconsistent. Shouldn't it be GetWaitEventExtentionName()?

This is an inspiration from GetLWLockIdentifier(), which is kind of OK
by me. If there is a consensus in changing that, fine by me, of
course.

+1 to GetWaitEventExtensionIdentifier for consistency with LWLock's counterpart.

--
Bharath Rupireddy
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

#50Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#47)
Re: Support to define custom wait events for extensions

On 2023-07-31 19:22, Michael Paquier wrote:

I am not sure that any of that is necessary. Anyway, I have applied
v11 to get the basics done.

Thanks for committing the main patch.

In my understanding, the rest works are
* to support WaitEventExtensionMultiple()
* to replace WAIT_EVENT_EXTENSION to custom wait events

Do someone already works for them? If not, I'll consider
how to realize them.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#51Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#50)
Re: Support to define custom wait events for extensions

On Tue, Aug 01, 2023 at 11:51:35AM +0900, Masahiro Ikeda wrote:

Thanks for committing the main patch.

In my understanding, the rest works are
* to support WaitEventExtensionMultiple()
* to replace WAIT_EVENT_EXTENSION to custom wait events

Do someone already works for them? If not, I'll consider
how to realize them.

Note that postgres_fdw and dblink use WAIT_EVENT_EXTENSION, but have
no dependency to shared_preload_libraries. Perhaps these could just
use a dynamic handling but that deserves a separate discussion because
of the fact that they'd need shared memory without being able to
request it. autoprewarm.c is much simpler.
--
Michael

#52Andres Freund
andres@anarazel.de
In reply to: Michael Paquier (#51)
Re: Support to define custom wait events for extensions

Hi,

On 2023-08-01 12:14:49 +0900, Michael Paquier wrote:

On Tue, Aug 01, 2023 at 11:51:35AM +0900, Masahiro Ikeda wrote:

Thanks for committing the main patch.

In my understanding, the rest works are
* to support WaitEventExtensionMultiple()
* to replace WAIT_EVENT_EXTENSION to custom wait events

Do someone already works for them? If not, I'll consider
how to realize them.

Note that postgres_fdw and dblink use WAIT_EVENT_EXTENSION, but have
no dependency to shared_preload_libraries. Perhaps these could just
use a dynamic handling but that deserves a separate discussion because
of the fact that they'd need shared memory without being able to
request it. autoprewarm.c is much simpler.

This is why the scheme as implemented doesn't really make sense to me. It'd be
much easier to use if we had a shared hashtable with the identifiers than
what's been merged now.

In plenty of cases it's not realistic for an extension library to run in each
backend, while still needing to wait for things.

Greetings,

Andres Freund

#53Ranier Vilela
ranier.vf@gmail.com
In reply to: Andres Freund (#52)
Re: Support to define custom wait events for extensions

Hi,

On Tue, Aug 01, 2023 at 11:51:35AM +0900, Masahiro Ikeda wrote:

Thanks for committing the main patch.

Latest head
Ubuntu 64 bits
gcc 13 64 bits

./configure --without-icu
make clean
make

In file included from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event.h:56:9: error: redeclaration of
enumerator ‘WAIT_EVENT_EXTENSION’
56 | WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
| ^~~~~~~~~~~~~~~~~~~~
In file included from ../../src/include/utils/wait_event.h:29,
from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event_types.h:60:9: note: previous definition
of ‘WAIT_EVENT_EXTENSION’ with type ‘enum <anonymous>’
60 | WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION
| ^~~~~~~~~~~~~~~~~~~~
In file included from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event.h:58:3: error: conflicting types for
‘WaitEventExtension’; have ‘enum <anonymous>’
58 | } WaitEventExtension;
| ^~~~~~~~~~~~~~~~~~
In file included from ../../src/include/utils/wait_event.h:29,
from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event_types.h:61:3: note: previous declaration
of ‘WaitEventExtension’ with type ‘WaitEventExtension’
61 | } WaitEventExtension;
| ^~~~~~~~~~~~~~~~~~
make[2]: *** [Makefile:174: controldata_utils_srv.o] Erro 1
make[2]: Saindo do diretório '/usr/src/postgres_commits/src/common'
make[1]: *** [Makefile:42: all-common-recurse] Erro 2
make[1]: Saindo do diretório '/usr/src/postgres_commits/src'
make: *** [GNUmakefile:11: all-src-recurse] Erro 2
ranier@notebook2:/usr/src/postgres_commits$ grep -r --include '*.c'
"WAIT_EVENT_EXTESION" .
ranier@notebook2:/usr/src/postgres_commits$ grep -r --include '*.c'
"WAIT_EVENT_EXTENSION" .

grep -r --include '*.h' "WAIT_EVENT_EXTENSION" .
./src/backend/utils/activity/wait_event_types.h: WAIT_EVENT_EXTENSION =
PG_WAIT_EXTENSION
./src/include/utils/wait_event.h: WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
./src/include/utils/wait_event.h: WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED

Not sure if this is really a mistake in my environment.

regards,
Ranier Vilela

#54Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Ranier Vilela (#53)
Re: Support to define custom wait events for extensions

On 2023-08-02 08:38, Ranier Vilela wrote:

Hi,

On Tue, Aug 01, 2023 at 11:51:35AM +0900, Masahiro Ikeda wrote:

Thanks for committing the main patch.

Latest head
Ubuntu 64 bits
gcc 13 64 bits

./configure --without-icu
make clean

make

In file included from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event.h:56:9: error: redeclaration of
enumerator ‘WAIT_EVENT_EXTENSION’
56 | WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
| ^~~~~~~~~~~~~~~~~~~~
In file included from ../../src/include/utils/wait_event.h:29,
from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event_types.h:60:9: note: previous
definition of ‘WAIT_EVENT_EXTENSION’ with type ‘enum
<anonymous>’
60 | WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION
| ^~~~~~~~~~~~~~~~~~~~
In file included from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event.h:58:3: error: conflicting types
for ‘WaitEventExtension’; have ‘enum <anonymous>’
58 | } WaitEventExtension;
| ^~~~~~~~~~~~~~~~~~
In file included from ../../src/include/utils/wait_event.h:29,
from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event_types.h:61:3: note: previous
declaration of ‘WaitEventExtension’ with type
‘WaitEventExtension’
61 | } WaitEventExtension;
| ^~~~~~~~~~~~~~~~~~
make[2]: *** [Makefile:174: controldata_utils_srv.o] Erro 1
make[2]: Saindo do diretório '/usr/src/postgres_commits/src/common'
make[1]: *** [Makefile:42: all-common-recurse] Erro 2
make[1]: Saindo do diretório '/usr/src/postgres_commits/src'
make: *** [GNUmakefile:11: all-src-recurse] Erro 2
ranier@notebook2:/usr/src/postgres_commits$ grep -r --include '*.c'
"WAIT_EVENT_EXTESION" .
ranier@notebook2:/usr/src/postgres_commits$ grep -r --include '*.c'
"WAIT_EVENT_EXTENSION" .

grep -r --include '*.h' "WAIT_EVENT_EXTENSION" .
./src/backend/utils/activity/wait_event_types.h: WAIT_EVENT_EXTENSION
= PG_WAIT_EXTENSION
./src/include/utils/wait_event.h: WAIT_EVENT_EXTENSION =
PG_WAIT_EXTENSION,
./src/include/utils/wait_event.h:
WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED

Not sure if this is really a mistake in my environment.

I can build without error.
* latest commit(3845577cb55eab3e06b3724e307ebbcac31f4841)
* gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)

The result in my environment is
$ grep -r --include '*.h' "WAIT_EVENT_EXTENSION" .
./src/include/utils/wait_event.h: WAIT_EVENT_EXTENSION =
PG_WAIT_EXTENSION,
./src/include/utils/wait_event.h:
WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED

I think the error occurred because the old wait_event_types.h still
exists.
Can you remove it before building and try again?

$ make maintainer-clean # remove wait_event_types.h
$ ./configure --without-icu
$ make

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#55Ranier Vilela
ranier.vf@gmail.com
In reply to: Masahiro Ikeda (#54)
Re: Support to define custom wait events for extensions

Em ter., 1 de ago. de 2023 às 21:34, Masahiro Ikeda <
ikedamsh@oss.nttdata.com> escreveu:

On 2023-08-02 08:38, Ranier Vilela wrote:

Hi,

On Tue, Aug 01, 2023 at 11:51:35AM +0900, Masahiro Ikeda wrote:

Thanks for committing the main patch.

Latest head
Ubuntu 64 bits
gcc 13 64 bits

./configure --without-icu
make clean

make

In file included from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event.h:56:9: error: redeclaration of
enumerator ‘WAIT_EVENT_EXTENSION’
56 | WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION,
| ^~~~~~~~~~~~~~~~~~~~
In file included from ../../src/include/utils/wait_event.h:29,
from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event_types.h:60:9: note: previous
definition of ‘WAIT_EVENT_EXTENSION’ with type ‘enum
<anonymous>’
60 | WAIT_EVENT_EXTENSION = PG_WAIT_EXTENSION
| ^~~~~~~~~~~~~~~~~~~~
In file included from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event.h:58:3: error: conflicting types
for ‘WaitEventExtension’; have ‘enum <anonymous>’
58 | } WaitEventExtension;
| ^~~~~~~~~~~~~~~~~~
In file included from ../../src/include/utils/wait_event.h:29,
from ../../src/include/pgstat.h:20,
from controldata_utils.c:38:
../../src/include/utils/wait_event_types.h:61:3: note: previous
declaration of ‘WaitEventExtension’ with type
‘WaitEventExtension’
61 | } WaitEventExtension;
| ^~~~~~~~~~~~~~~~~~
make[2]: *** [Makefile:174: controldata_utils_srv.o] Erro 1
make[2]: Saindo do diretório '/usr/src/postgres_commits/src/common'
make[1]: *** [Makefile:42: all-common-recurse] Erro 2
make[1]: Saindo do diretório '/usr/src/postgres_commits/src'
make: *** [GNUmakefile:11: all-src-recurse] Erro 2
ranier@notebook2:/usr/src/postgres_commits$ grep -r --include '*.c'
"WAIT_EVENT_EXTESION" .
ranier@notebook2:/usr/src/postgres_commits$ grep -r --include '*.c'
"WAIT_EVENT_EXTENSION" .

grep -r --include '*.h' "WAIT_EVENT_EXTENSION" .
./src/backend/utils/activity/wait_event_types.h: WAIT_EVENT_EXTENSION
= PG_WAIT_EXTENSION
./src/include/utils/wait_event.h: WAIT_EVENT_EXTENSION =
PG_WAIT_EXTENSION,
./src/include/utils/wait_event.h:
WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED

Not sure if this is really a mistake in my environment.

I can build without error.
* latest commit(3845577cb55eab3e06b3724e307ebbcac31f4841)
* gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)

The result in my environment is
$ grep -r --include '*.h' "WAIT_EVENT_EXTENSION" .
./src/include/utils/wait_event.h: WAIT_EVENT_EXTENSION =
PG_WAIT_EXTENSION,
./src/include/utils/wait_event.h:
WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED

I think the error occurred because the old wait_event_types.h still
exists.
Can you remove it before building and try again?

$ make maintainer-clean # remove wait_event_types.h
$ ./configure --without-icu
$ make

Yeah, removing wait_event_types.h works.

Thanks.

regards,
Ranier Vilela

#56Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Andres Freund (#52)
Re: Support to define custom wait events for extensions

On 2023-08-01 12:23, Andres Freund wrote:

Hi,

On 2023-08-01 12:14:49 +0900, Michael Paquier wrote:

On Tue, Aug 01, 2023 at 11:51:35AM +0900, Masahiro Ikeda wrote:

Thanks for committing the main patch.

In my understanding, the rest works are
* to support WaitEventExtensionMultiple()
* to replace WAIT_EVENT_EXTENSION to custom wait events

Do someone already works for them? If not, I'll consider
how to realize them.

Note that postgres_fdw and dblink use WAIT_EVENT_EXTENSION, but have
no dependency to shared_preload_libraries. Perhaps these could just
use a dynamic handling but that deserves a separate discussion because
of the fact that they'd need shared memory without being able to
request it. autoprewarm.c is much simpler.

This is why the scheme as implemented doesn't really make sense to me.
It'd be
much easier to use if we had a shared hashtable with the identifiers
than
what's been merged now.

In plenty of cases it's not realistic for an extension library to run
in each
backend, while still needing to wait for things.

OK, I'll try to make a PoC patch.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#57Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#56)
Re: Support to define custom wait events for extensions

On Wed, Aug 02, 2023 at 06:34:15PM +0900, Masahiro Ikeda wrote:

On 2023-08-01 12:23, Andres Freund wrote:

This is why the scheme as implemented doesn't really make sense to me.
It'd be
much easier to use if we had a shared hashtable with the identifiers
than
what's been merged now.

In plenty of cases it's not realistic for an extension library to run in
each
backend, while still needing to wait for things.

OK, I'll try to make a PoC patch.

Hmm. There are a few things to take into account here:
- WaitEventExtensionShmemInit() should gain a dshash_create(), to make
sure that the shared table is around, and we are going to have a
reference to it in WaitEventExtensionCounterData, saved from
dshash_get_hash_table_handle().
- The hash table entries could just use nextId as key to look at the
entries, with entries added during WaitEventExtensionNew(), and use as
contents the name of the wait event. We are going to need a fixed
size for these custom strings, but perhaps a hard limit of 256
characters for each entry of the hash table is more than enough for
most users?
- WaitEventExtensionRegisterName() could be removed, I guess, replaced
by a single WaitEventExtensionNew(), as of:
uint32 WaitEventExtensionNew(const char *event_name);
- GetWaitEventExtensionIdentifier() needs to switch to a lookup of the
shared hash table, based on the eventId.

All that would save from the extra WaitEventExtensionRegisterName()
needed in the backends to keep a track of the names, indeed.
--
Michael

#58Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#57)
Re: Support to define custom wait events for extensions

On 2023-08-08 08:54, Michael Paquier wrote:

On Wed, Aug 02, 2023 at 06:34:15PM +0900, Masahiro Ikeda wrote:

On 2023-08-01 12:23, Andres Freund wrote:

This is why the scheme as implemented doesn't really make sense to
me.
It'd be
much easier to use if we had a shared hashtable with the identifiers
than
what's been merged now.

In plenty of cases it's not realistic for an extension library to run
in
each
backend, while still needing to wait for things.

OK, I'll try to make a PoC patch.

Hmm. There are a few things to take into account here:
- WaitEventExtensionShmemInit() should gain a dshash_create(), to make
sure that the shared table is around, and we are going to have a
reference to it in WaitEventExtensionCounterData, saved from
dshash_get_hash_table_handle().
- The hash table entries could just use nextId as key to look at the
entries, with entries added during WaitEventExtensionNew(), and use as
contents the name of the wait event. We are going to need a fixed
size for these custom strings, but perhaps a hard limit of 256
characters for each entry of the hash table is more than enough for
most users?
- WaitEventExtensionRegisterName() could be removed, I guess, replaced
by a single WaitEventExtensionNew(), as of:
uint32 WaitEventExtensionNew(const char *event_name);
- GetWaitEventExtensionIdentifier() needs to switch to a lookup of the
shared hash table, based on the eventId.

All that would save from the extra WaitEventExtensionRegisterName()
needed in the backends to keep a track of the names, indeed.

Thank you for pointing the direction of implementation.

I am thinking a bit that we also need another hash where the key
is a custom string. For extensions that have no dependencies
with shared_preload_libraries, I think we need to avoid that
WaitEventExtensionNew() is called repeatedly and a new eventId
is issued each time.

So, is it better to have another hash where the key is
a custom string and uniqueness is identified by it to determine
if a new eventId should be issued?

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#59Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#58)
Re: Support to define custom wait events for extensions

On Tue, Aug 08, 2023 at 09:39:02AM +0900, Masahiro Ikeda wrote:

I am thinking a bit that we also need another hash where the key
is a custom string. For extensions that have no dependencies
with shared_preload_libraries, I think we need to avoid that
WaitEventExtensionNew() is called repeatedly and a new eventId
is issued each time.

So, is it better to have another hash where the key is
a custom string and uniqueness is identified by it to determine
if a new eventId should be issued?

Yeah, I was also considering if something like that is really
necessary, but I cannot stop worrying about adding more contention to
the hash table lookup each time an extention needs to retrieve an
event ID to use for WaitLatch() or such. The results of the hash
table lookups could be cached in each backend, still it creates an
extra cost when combined with queries running in parallel on
pg_stat_activity that do the opposite lookup event_id -> event_name.
My suggestion adds more load to AddinShmemInitLock instead.

Hence, I was just thinking about relying on AddinShmemInitLock to
insert new entries in the hash table, only if its shmem state is not
found when calling ShmemInitStruct(). Or perhaps it is just OK to not
care about the impact event_name -> event_id lookup for fresh
connections, and just bite the bullet with two lookup keys instead of
relying on AddinShmemInitLock for the addition of new entries in the
hash table? Hmm, perhaps you're right with your approach here, at the
end.
--
Michael

#60Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#59)
1 attachment(s)
Re: Support to define custom wait events for extensions

On 2023-08-08 10:05, Michael Paquier wrote:

On Tue, Aug 08, 2023 at 09:39:02AM +0900, Masahiro Ikeda wrote:

I am thinking a bit that we also need another hash where the key
is a custom string. For extensions that have no dependencies
with shared_preload_libraries, I think we need to avoid that
WaitEventExtensionNew() is called repeatedly and a new eventId
is issued each time.

So, is it better to have another hash where the key is
a custom string and uniqueness is identified by it to determine
if a new eventId should be issued?

Yeah, I was also considering if something like that is really
necessary, but I cannot stop worrying about adding more contention to
the hash table lookup each time an extention needs to retrieve an
event ID to use for WaitLatch() or such. The results of the hash
table lookups could be cached in each backend, still it creates an
extra cost when combined with queries running in parallel on
pg_stat_activity that do the opposite lookup event_id -> event_name.

My suggestion adds more load to AddinShmemInitLock instead.
Hence, I was just thinking about relying on AddinShmemInitLock to
insert new entries in the hash table, only if its shmem state is not
found when calling ShmemInitStruct(). Or perhaps it is just OK to not
care about the impact event_name -> event_id lookup for fresh
connections, and just bite the bullet with two lookup keys instead of
relying on AddinShmemInitLock for the addition of new entries in the
hash table? Hmm, perhaps you're right with your approach here, at the
end.

For the first idea, I agree that if a lot of new connections come in,
it is easy to leads many conflicts. The only solution I can think of
is to use connection pooling now.

IIUC, the second idea is based on the premise of allocating their shared
memory for each extension. In that case, the cons of the first idea can
be solved because the wait event infos are saved in their shared memory
and
they don't need call WaitEventExtensionNew() anymore. Is that right?

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

v1-0001-Change-to-manage-custom-wait-events-in-shared-hash.patchtext/x-diff; charset=us-ascii; name=v1-0001-Change-to-manage-custom-wait-events-in-shared-hash.patchDownload
From 0e3ccc6474bfcc51114d9363b7819b68f37fcad3 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <mshr.ikeda@ntt.com>
Date: Tue, 8 Aug 2023 19:24:32 +0900
Subject: [PATCH] Change to manage custom wait events in shared hash

Currently, names of the custom wait event must be
registered per backends. This patch relaxes the
constraints to store the wait events and their names
in the dynamic shared hashtable. So, all backends can
look up the wait event names on pg_stat_activity view
without additional processing by extensions.
---
 doc/src/sgml/monitoring.sgml                  |   7 +-
 doc/src/sgml/xfunc.sgml                       |  10 +-
 src/backend/storage/ipc/ipci.c                |   6 +
 src/backend/storage/lmgr/lwlock.c             |   2 +
 src/backend/utils/activity/wait_event.c       | 275 ++++++++++++------
 src/backend/utils/init/postinit.c             |   3 +
 src/include/storage/lwlock.h                  |   2 +
 src/include/utils/wait_event.h                |  17 +-
 .../modules/worker_spi/t/001_worker_spi.pl    |  18 +-
 src/test/modules/worker_spi/worker_spi.c      |   6 +-
 src/tools/pgindent/typedefs.list              |   2 +-
 11 files changed, 216 insertions(+), 132 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index f4fc5d814f..19181832d7 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1121,10 +1121,9 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
      <literal>LWLock</literal> types
      to the list shown in <xref linkend="wait-event-extension-table"/> and
      <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
-     assigned by an extension will not be available in all server processes;
-     so an <literal>Extension</literal> or <literal>LWLock</literal> wait
-     event might be reported as just
-     <quote><literal>extension</literal></quote> rather than the
+     of <literal>LWLock</literal> assigned by an extension will not be
+     available in all server processes; so an wait event might be reported
+     as just <quote><literal>extension</literal></quote> rather than the
      extension-assigned name.
     </para>
    </note>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index d6345a775b..7fec034db4 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3470,17 +3470,13 @@ void RequestAddinShmemSpace(int size)
 </programlisting>
     </para>
     <para>
-     <literal>shmem_startup_hook</literal> can allocate in shared memory
+     <literal>shmem_startup_hook</literal> can allocate in dynamic shared memory
      custom wait events by calling while holding the LWLock
      <function>AddinShmemInitLock</function> to avoid any race conditions:
 <programlisting>
-uint32 WaitEventExtensionNew(void)
-</programlisting>
-     Next, each process needs to associate the wait event allocated previously
-     to a user-facing custom string, which is something done by calling:
-<programlisting>
-void WaitEventExtensionRegisterName(uint32 wait_event_info, const char *wait_event_name)
+uint32 WaitEventExtensionNew(const char *wait_event_name)
 </programlisting>
+     The wait event is associated to a user-facing custom string.
      An example can be found in <filename>src/test/modules/worker_spi</filename>
      in the PostgreSQL source tree.
     </para>
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 5551afffc0..b3d2a0b252 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -323,6 +323,12 @@ CreateSharedMemoryAndSemaphores(void)
 	 */
 	if (shmem_startup_hook)
 		shmem_startup_hook();
+
+	/*
+	 * Detach from the dynamic shared hash because postmaster will never
+	 * access these again
+	 */
+	WaitEventExtensionDetachShmem();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 315a78cda9..cf1bdbed85 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -190,6 +190,8 @@ static const char *const BuiltinTrancheNames[] = {
 	"LogicalRepLauncherDSA",
 	/* LWTRANCHE_LAUNCHER_HASH: */
 	"LogicalRepLauncherHash",
+	"LWTRANCHE_WAITEVENTEXTENSION_DSA",
+	"LWTRANCHE_WAITEVENTEXTENSION_HASH",
 };
 
 StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index b3596ece80..dbdf681110 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,6 +22,7 @@
  */
 #include "postgres.h"
 
+#include "lib/dshash.h"
 #include "miscadmin.h"
 #include "port/pg_bitutils.h"
 #include "storage/lmgr.h"		/* for GetLockNameFromTagType */
@@ -45,141 +46,235 @@ uint32	   *my_wait_event_info = &local_my_wait_event_info;
 #define WAIT_EVENT_CLASS_MASK	0xFF000000
 #define WAIT_EVENT_ID_MASK		0x0000FFFF
 
-/* dynamic allocation counter for custom wait events in extensions */
-typedef struct WaitEventExtensionCounterData
+/* hash table entry for finding the custom wait event name for a key */
+#define MAX_WAIT_EVENT_EXTENSION_NAME		256
+typedef struct WaitEventExtensionNameEntry
 {
-	int			nextId;			/* next ID to assign */
-	slock_t		mutex;			/* protects the counter */
-} WaitEventExtensionCounterData;
+	uint16	event_id;	/* hash key */
+	uint32	status;		/* hash status */
+	char	wait_event_name[MAX_WAIT_EVENT_EXTENSION_NAME]; /* custom wait event name */
+} WaitEventExtensionNameEntry;
+
+/* for references to shared custom wait event entries */
+#define SH_PREFIX		wait_event_extension_name_hash
+#define SH_ELEMENT_TYPE	WaitEventExtensionNameEntry
+#define SH_KEY_TYPE		uint16
+#define	SH_KEY			event_id
+#define SH_HASH_KEY(tb, key)	dshash_memhash(&key, sizeof(uint16), NULL)
+#define SH_EQUAL(tb, a, b)		dshash_memcmp(&a, &b, sizeof(uint16), NULL) == 0
+#define	SH_SCOPE		static inline
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
+
+/* parameter for the shared hash */
+static const dshash_parameters dsh_params = {
+	sizeof(uint16),
+	sizeof(WaitEventExtensionNameEntry),
+	dshash_memcmp,
+	dshash_memhash,
+	LWTRANCHE_WAITEVENTEXTENSION_HASH
+};
+
+/*
+ * Shared state information for custom wait events in extensions.
+ * This manages a dynamic shared hash and a dynamic allocation counter.
+ */
+typedef struct WaitEventExtensionSharedState
+{
+	/*
+	 * Custom wait event names are registered in shared custom wait event hash.
+	 * Other backends can look up them without additional processing per backend
+	 * like LWLockRegisterTranche().
+	 */
+	void		*raw_dsa_area;
+	dshash_table_handle hash_handle;
+
+	int			nextId;					/* next event ID to assign */
+	slock_t		mutex;					/* protects the counter */
+} WaitEventExtensionSharedState;
 
 /* pointer to the shared memory */
-static WaitEventExtensionCounterData *WaitEventExtensionCounter;
+static WaitEventExtensionSharedState *wee_state;
+
+/* backend-local state to manage dynamic shared hash */
+typedef struct WaitEventExtension_LocalState
+{
+	dsa_area   *dsa;
+	dshash_table *dsh;
+} WaitEventExtension_LocalState;
+
+static WaitEventExtension_LocalState wee_localstate;
 
 /* first event ID of custom wait events for extensions */
 #define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
 	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
 
+static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
+
 /*
- * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
- * stores the names of all dynamically-created event IDs known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * The size of the shared memory allocation for custom wait events stored in
+ * the shared hash table. This allocation will be done as part of the main shared
+ * memory, rather than dynamic shared memory, allowing it to be initialized in
+ * postmaster.
  */
-static const char **WaitEventExtensionNames = NULL;
-static int	WaitEventExtensionNamesAllocated = 0;
+static Size
+wait_event_extension_dsa_init_size(void)
+{
+	Size		sz;
 
-static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
+	/* Same size as pgstat_dsa_init_size() for dshash */
+	sz = 256 * 1024;
+	Assert(dsa_minimum_size() <= sz);
+	return MAXALIGN(sz);
+}
 
 /*
- *  Return the space for dynamic allocation counter.
+ *  Return the space for dynamic shared hash and dynamic allocation counter.
  */
 Size
 WaitEventExtensionShmemSize(void)
 {
-	return sizeof(WaitEventExtensionCounterData);
+	Size	sz;
+	sz = MAXALIGN(sizeof(WaitEventExtensionSharedState));
+	sz = add_size(sz, wait_event_extension_dsa_init_size());
+	return sz;
 }
 
 /*
- * Allocate shmem space for dynamic allocation counter.
+ * Allocate shmem space for dynamic shared hash and dynamic allocation counter.
  */
 void
 WaitEventExtensionShmemInit(void)
 {
 	bool		found;
 
-	WaitEventExtensionCounter = (WaitEventExtensionCounterData *)
-		ShmemInitStruct("WaitEventExtensionCounterData",
+	wee_state = (WaitEventExtensionSharedState *)
+		ShmemInitStruct("WaitEventExtensionSharedState",
 						WaitEventExtensionShmemSize(), &found);
 
 	if (!found)
 	{
+		char	*p = (char *) wee_state;
+
 		/* initialize the allocation counter and its spinlock. */
-		WaitEventExtensionCounter->nextId = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
-		SpinLockInit(&WaitEventExtensionCounter->mutex);
+		wee_state->nextId = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+		SpinLockInit(&wee_state->mutex);
+
+		/* initialize dsa and dshash */
+		p += (MAXALIGN(sizeof(WaitEventExtensionSharedState)));
+		wee_state->raw_dsa_area = (void *) p;
+		p += MAXALIGN(wait_event_extension_dsa_init_size());
+		wee_localstate.dsa = dsa_create_in_place(wee_state->raw_dsa_area,
+								  wait_event_extension_dsa_init_size(),
+								  LWTRANCHE_WAITEVENTEXTENSION_DSA,
+								  0);
+		dsa_pin(wee_localstate.dsa);
+
+		/*
+		 * To ensure dshash is created in "plain" shared memory, temporarily
+		 * limit size of dsa to the initial size of the dsa.
+		 */
+		dsa_set_size_limit(wee_localstate.dsa, wait_event_extension_dsa_init_size());
+
+		/*
+		 * With the limit in place, create the dshash table. XXX: It'd be nice
+		 * if there were dshash_create_in_place().
+		 */
+		wee_localstate.dsh = dshash_create(wee_localstate.dsa, &dsh_params, 0);
+		wee_state->hash_handle = dshash_get_hash_table_handle(wee_localstate.dsh);
+
+		dsa_set_size_limit(wee_localstate.dsa, -1);
+
+		/*
+		 * Don't detach from a shared hash table because postmaster can access
+		 * these again via shmem_startup_hooks by extensions.
+		 */
+		Assert(wee_localstate.dsh != NULL);
 	}
 }
 
 /*
- * Allocate a new event ID and return the wait event.
+ * Detach from the dynamic shared hash.
  */
-uint32
-WaitEventExtensionNew(void)
+void
+WaitEventExtensionDetachShmem(void)
 {
-	uint16		eventId;
+	Assert(wee_localstate.dsa);
 
-	Assert(LWLockHeldByMeInMode(AddinShmemInitLock, LW_EXCLUSIVE));
+	dshash_detach(wee_localstate.dsh);
+	wee_localstate.dsh = NULL;
 
-	SpinLockAcquire(&WaitEventExtensionCounter->mutex);
+	dsa_detach(wee_localstate.dsa);
+	wee_localstate.dsa = NULL;
+}
 
-	if (WaitEventExtensionCounter->nextId > PG_UINT16_MAX)
-	{
-		SpinLockRelease(&WaitEventExtensionCounter->mutex);
-		ereport(ERROR,
-				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-				errmsg("too many wait events for extensions"));
-	}
+/*
+ * Attach the dynamic shared hash for backends. Called From BaseInit().
+ */
+void
+WaitEventExtensionAttachShmem(void)
+{
+	MemoryContext oldcontext;
 
-	eventId = WaitEventExtensionCounter->nextId++;
+	Assert(wee_localstate.dsa == NULL);
 
-	SpinLockRelease(&WaitEventExtensionCounter->mutex);
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
 
-	return PG_WAIT_EXTENSION | eventId;
+	wee_localstate.dsa =
+			dsa_attach_in_place(wee_state->raw_dsa_area,
+								  NULL);
+	dsa_pin_mapping(wee_localstate.dsa);
+
+	wee_localstate.dsh =
+			dshash_attach(wee_localstate.dsa, &dsh_params,
+							wee_state->hash_handle, NULL);
+
+	MemoryContextSwitchTo(oldcontext);
 }
 
 /*
- * Register a dynamic wait event name for extension in the lookup table
- * of the current process.
- *
- * This routine will save a pointer to the wait event name passed as an argument,
- * so the name should be allocated in a backend-lifetime context
- * (shared memory, TopMemoryContext, static constant, or similar).
- *
- * The "wait_event_name" will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
+ * Allocate a new event ID and return the wait event info.
  */
-void
-WaitEventExtensionRegisterName(uint32 wait_event_info,
-							   const char *wait_event_name)
+uint32
+WaitEventExtensionNew(const char *wait_event_name)
 {
-	uint32		classId;
 	uint16		eventId;
+	bool		found;
+	WaitEventExtensionNameEntry	*entry;
 
-	classId = wait_event_info & WAIT_EVENT_CLASS_MASK;
-	eventId = wait_event_info & WAIT_EVENT_ID_MASK;
+	Assert(LWLockHeldByMeInMode(AddinShmemInitLock, LW_EXCLUSIVE));
+	Assert(wee_localstate.dsh != NULL);
 
-	/* Check the wait event class. */
-	if (classId != PG_WAIT_EXTENSION)
+	/* Check the limit of the length of the event name */
+	if (strlen(wait_event_name) >= MAX_WAIT_EVENT_EXTENSION_NAME)
 		ereport(ERROR,
-				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("invalid wait event class %u", classId));
-
-	/* This should only be called for user-defined wait event. */
-	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
-		ereport(ERROR,
-				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("invalid wait event ID %u", eventId));
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("wait event name is too long"));
 
-	/* Convert to array index. */
-	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+	/* Allocate a new event Id */
+	SpinLockAcquire(&wee_state->mutex);
 
-	/* If necessary, create or enlarge array. */
-	if (eventId >= WaitEventExtensionNamesAllocated)
+	if (wee_state->nextId > PG_UINT16_MAX)
 	{
-		uint32		newalloc;
-
-		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
-
-		if (WaitEventExtensionNames == NULL)
-			WaitEventExtensionNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			WaitEventExtensionNames =
-				repalloc0_array(WaitEventExtensionNames, const char *,
-								WaitEventExtensionNamesAllocated, newalloc);
-		WaitEventExtensionNamesAllocated = newalloc;
+		SpinLockRelease(&wee_state->mutex);
+		ereport(ERROR,
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("too many wait events for extensions"));
 	}
 
-	WaitEventExtensionNames[eventId] = wait_event_name;
+	eventId = wee_state->nextId++;
+
+	SpinLockRelease(&wee_state->mutex);
+
+	/* Register a new custom wait event in the shared hash */
+	entry = dshash_find_or_insert(wee_localstate.dsh,
+									&eventId, &found);
+	Assert(!found);
+	strlcpy(entry->wait_event_name, wait_event_name, sizeof(entry->wait_event_name));
+	dshash_release_lock(wee_localstate.dsh, entry);
+
+	return PG_WAIT_EXTENSION | eventId;
 }
 
 /*
@@ -188,23 +283,23 @@ WaitEventExtensionRegisterName(uint32 wait_event_info,
 static const char *
 GetWaitEventExtensionIdentifier(uint16 eventId)
 {
+	WaitEventExtensionNameEntry	*entry;
+
 	/* Built-in event? */
 	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
 		return "Extension";
 
-	/*
-	 * It is a user-defined wait event, so look at WaitEventExtensionNames[].
-	 * However, it is possible that the name has never been registered by
-	 * calling WaitEventExtensionRegisterName() in the current process, in
-	 * which case give up and return "extension".
-	 */
-	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
-
-	if (eventId >= WaitEventExtensionNamesAllocated ||
-		WaitEventExtensionNames[eventId] == NULL)
-		return "extension";
+	/* It is a user-defined wait event, so look at the dynamic shared hash */
+	entry = dshash_find(wee_localstate.dsh,
+						&eventId,
+						false);
+	if (entry)
+		dshash_release_lock(wee_localstate.dsh, entry);
+	else
+		ereport(ERROR,
+				errmsg("could not find the name for custom wait event ID %u", eventId));
 
-	return WaitEventExtensionNames[eventId];
+	return entry->wait_event_name;
 }
 
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index f31b85c013..3dea5afd8f 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -671,6 +671,9 @@ BaseInit(void)
 	 * drop ephemeral slots, which in turn triggers stats reporting.
 	 */
 	ReplicationSlotInitialize();
+
+    /* Attach the dynamic shared hash for custom wait events */
+	WaitEventExtensionAttachShmem();
 }
 
 
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index d77410bdea..451c406e47 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -207,6 +207,8 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_PGSTATS_DATA,
 	LWTRANCHE_LAUNCHER_DSA,
 	LWTRANCHE_LAUNCHER_HASH,
+	LWTRANCHE_WAITEVENTEXTENSION_DSA,
+	LWTRANCHE_WAITEVENTEXTENSION_HASH,
 	LWTRANCHE_FIRST_USER_DEFINED
 }			BuiltinTrancheIds;
 
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index aad8bc08fa..4c2aa25b78 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -44,12 +44,11 @@ extern PGDLLIMPORT uint32 *my_wait_event_info;
  * Use this category when the server process is waiting for some condition
  * defined by an extension module.
  *
- * Extensions can define their own wait events in this category.  First,
- * they should call WaitEventExtensionNew() to get one or more wait event
- * IDs that are allocated from a shared counter.  These can be used directly
- * with pgstat_report_wait_start() or equivalent.  Next, each individual
- * process should call WaitEventExtensionRegisterName() to associate a wait
- * event string to the number allocated previously.
+ * Extensions can define their own wait events in this category. They should
+ * call WaitEventExtensionNew() with a wait event string. It will get one wait
+ * event ID that is allocated from a shared counter, associate the string to
+ * the number in the shared dynamic hash and return the wait event info. It
+ * can be used directly with pgstat_report_wait_start() or equivalent.
  */
 typedef enum
 {
@@ -59,10 +58,10 @@ typedef enum
 
 extern void WaitEventExtensionShmemInit(void);
 extern Size WaitEventExtensionShmemSize(void);
+extern void WaitEventExtensionDetachShmem(void);
+extern void WaitEventExtensionAttachShmem(void);
 
-extern uint32 WaitEventExtensionNew(void);
-extern void WaitEventExtensionRegisterName(uint32 wait_event_info,
-										   const char *wait_event_name);
+extern uint32 WaitEventExtensionNew(const char *wait_event_name);
 
 /* ----------
  * pgstat_report_wait_start() -
diff --git a/src/test/modules/worker_spi/t/001_worker_spi.pl b/src/test/modules/worker_spi/t/001_worker_spi.pl
index c3e7f5fbe6..26b8a49bec 100644
--- a/src/test/modules/worker_spi/t/001_worker_spi.pl
+++ b/src/test/modules/worker_spi/t/001_worker_spi.pl
@@ -39,25 +39,11 @@ $node->poll_query_until('postgres',
 $result = $node->safe_psql('postgres', 'SELECT * FROM schema4.counted;');
 is($result, qq(total|1), 'dynamic bgworker correctly consumed tuple data');
 
-# Check the wait event used by the dynamic bgworker.  For a session without
-# the state in shared memory known, the default of "extension" is the value
-# waited on.
+# Check the wait event used by the dynamic bgworker.
 $result = $node->poll_query_until(
 	'postgres',
 	qq[SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
-	'extension');
-is($result, 1, 'dynamic bgworker has reported "extension" as wait event');
-
-# If the shared memory state is loaded (here with worker_spi_init within
-# the same connection as the one querying pg_stat_activity), the wait
-# event is the custom one.
-# The expected result is a special pattern here with a newline coming from the
-# first query where the shared memory state is set.
-$result = $node->poll_query_until(
-	'postgres',
-	qq[SELECT worker_spi_init(); SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
-	qq[
-worker_spi_main]);
+	qq[worker_spi_main]);
 is($result, 1,
 	'dynamic bgworker has reported "worker_spi_main" as wait event');
 
diff --git a/src/test/modules/worker_spi/worker_spi.c b/src/test/modules/worker_spi/worker_spi.c
index c4317351ce..90a762b5e4 100644
--- a/src/test/modules/worker_spi/worker_spi.c
+++ b/src/test/modules/worker_spi/worker_spi.c
@@ -124,14 +124,10 @@ worker_spi_shmem_init(void)
 
 	/* Define a new wait event */
 	if (!found)
-		wsstate->wait_event = WaitEventExtensionNew();
+		wsstate->wait_event = WaitEventExtensionNew("worker_spi_main");
 
 	LWLockRelease(AddinShmemInitLock);
 
-	/*
-	 * Register the wait event in the lookup table of the current process.
-	 */
-	WaitEventExtensionRegisterName(wsstate->wait_event, "worker_spi_main");
 	return;
 }
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 66823bc2a7..0809a28b8f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2991,7 +2991,7 @@ WaitEventActivity
 WaitEventBufferPin
 WaitEventClient
 WaitEventExtension
-WaitEventExtensionCounterData
+WaitEventExtensionSharedState
 WaitEventIO
 WaitEventIPC
 WaitEventSet
-- 
2.25.1

#61Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Masahiro Ikeda (#60)
Re: Support to define custom wait events for extensions

I accidentally attached a patch in one previous email.
But, you don't need to check it, sorry.
(v1-0001-Change-to-manage-custom-wait-events-in-shared-hash.patch)

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#62Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#61)
Re: Support to define custom wait events for extensions

On Tue, Aug 08, 2023 at 08:40:53PM +0900, Masahiro Ikeda wrote:

I accidentally attached a patch in one previous email.
But, you don't need to check it, sorry.
(v1-0001-Change-to-manage-custom-wait-events-in-shared-hash.patch)

Sure, no worries. With that in place, the init function in worker_spi
can be removed.
--
Michael

#63Andres Freund
andres@anarazel.de
In reply to: Michael Paquier (#57)
Re: Support to define custom wait events for extensions

Hi,

On 2023-08-08 08:54:10 +0900, Michael Paquier wrote:

- WaitEventExtensionShmemInit() should gain a dshash_create(), to make
sure that the shared table is around, and we are going to have a
reference to it in WaitEventExtensionCounterData, saved from
dshash_get_hash_table_handle().

I'm not even sure it's worth using dshash here. Why don't we just create a
decently sized dynahash (say 128 enties) in shared memory? We overallocate
shared memory by enough that there's a lot of headroom for further entries, in
the rare cases they're needed.

We are going to need a fixed size for these custom strings, but perhaps a
hard limit of 256 characters for each entry of the hash table is more than
enough for most users?

I'd just use NAMEDATALEN.

- Andres

#64Michael Paquier
michael@paquier.xyz
In reply to: Andres Freund (#63)
Re: Support to define custom wait events for extensions

On Tue, Aug 08, 2023 at 03:59:54PM -0700, Andres Freund wrote:

On 2023-08-08 08:54:10 +0900, Michael Paquier wrote:

- WaitEventExtensionShmemInit() should gain a dshash_create(), to make
sure that the shared table is around, and we are going to have a
reference to it in WaitEventExtensionCounterData, saved from
dshash_get_hash_table_handle().

I'm not even sure it's worth using dshash here. Why don't we just create a
decently sized dynahash (say 128 enties) in shared memory? We overallocate
shared memory by enough that there's a lot of headroom for further entries, in
the rare cases they're needed.

The question here would be how many slots the most popular extensions
actually need, but that could always be sized up based on the
feedback.

We are going to need a fixed size for these custom strings, but perhaps a
hard limit of 256 characters for each entry of the hash table is more than
enough for most users?

I'd just use NAMEDATALEN.

Both suggestions WFM.
--
Michael

#65Andres Freund
andres@anarazel.de
In reply to: Michael Paquier (#64)
Re: Support to define custom wait events for extensions

Hi,

On 2023-08-09 08:03:29 +0900, Michael Paquier wrote:

On Tue, Aug 08, 2023 at 03:59:54PM -0700, Andres Freund wrote:

On 2023-08-08 08:54:10 +0900, Michael Paquier wrote:

- WaitEventExtensionShmemInit() should gain a dshash_create(), to make
sure that the shared table is around, and we are going to have a
reference to it in WaitEventExtensionCounterData, saved from
dshash_get_hash_table_handle().

I'm not even sure it's worth using dshash here. Why don't we just create a
decently sized dynahash (say 128 enties) in shared memory? We overallocate
shared memory by enough that there's a lot of headroom for further entries, in
the rare cases they're needed.

The question here would be how many slots the most popular extensions
actually need, but that could always be sized up based on the
feedback.

On a default initdb (i.e. 128MB s_b), after explicitly disabling huge pages,
we over-allocate shared memory by by 1922304 bytes, according to
pg_shmem_allocations. We allow that memory to be used for things like shared
hashtables that grow beyond their initial size. So even if the hash table's
static size is too small, there's lots of room to grow, even on small systems.

Just because it's somewhat interesting: With huge pages available and not
disabled, we over-allocate by 3364096 bytes.

Greetings,

Andres Freund

#66Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Andres Freund (#65)
2 attachment(s)
Re: Support to define custom wait events for extensions

Hi,

Thanks for your comments to v1 patch.

I made v2 patch. Main changes are
* change to NAMEDATALEN
* change to use dynahash from dshash
* remove worker_spi_init()
* create second hash table to find a event id from a name to
identify uniquness. It enable extensions which don't use share
memory for their use to define custom wait events because
WaitEventExtensionNew() will not allocate duplicate wait events.
* create PoC patch to show that extensions, which don't use shared
memory for their use, can define custom wait events.
(v2-0002-poc-custom-wait-event-for-dblink.patch)

I'm worrying about
* Is 512(wee_hash_max_size) the maximum number of the custom wait
events sufficient?
* Is there any way to not force extensions that don't use shared
memory for their use like dblink to acquire AddinShmemInitLock?;

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

v2-0001-Change-to-manage-custom-wait-events-in-shared-hash.patchtext/x-diff; name=v2-0001-Change-to-manage-custom-wait-events-in-shared-hash.patchDownload
From ec7d2b0ae47be9bedabb4a4127c914bfb8c361b5 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <mshr.ikeda@ntt.com>
Date: Wed, 9 Aug 2023 19:19:58 +0900
Subject: [PATCH 1/2] Change to manage custom wait events in shared hash

Currently, names of the custom wait event must be registered
per backends. This patch relaxes the constraints by store the
wait events and their names in hash tables in shared memory.

So, all backends can look up the wait event names on
pg_stat_activity view without additional processing by extensions.

In addition, extensions which do not use shared memory for their
use can define custom wait events because WaitEventExtensionNew()
will never allocate new one if the wait event associated to the
name is already allocated. It use hash table to identify uniquness.
---
 doc/src/sgml/monitoring.sgml                  |   7 +-
 doc/src/sgml/xfunc.sgml                       |  10 +-
 src/backend/storage/lmgr/lwlocknames.txt      |   1 +
 src/backend/utils/activity/wait_event.c       | 198 +++++++++++-------
 src/include/utils/wait_event.h                |  17 +-
 .../modules/worker_spi/t/001_worker_spi.pl    |  18 +-
 .../modules/worker_spi/worker_spi--1.0.sql    |   5 -
 src/test/modules/worker_spi/worker_spi.c      |  22 +-
 8 files changed, 137 insertions(+), 141 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index f4fc5d814f..19181832d7 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1121,10 +1121,9 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
      <literal>LWLock</literal> types
      to the list shown in <xref linkend="wait-event-extension-table"/> and
      <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
-     assigned by an extension will not be available in all server processes;
-     so an <literal>Extension</literal> or <literal>LWLock</literal> wait
-     event might be reported as just
-     <quote><literal>extension</literal></quote> rather than the
+     of <literal>LWLock</literal> assigned by an extension will not be
+     available in all server processes; so an wait event might be reported
+     as just <quote><literal>extension</literal></quote> rather than the
      extension-assigned name.
     </para>
    </note>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index d6345a775b..7fec034db4 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3470,17 +3470,13 @@ void RequestAddinShmemSpace(int size)
 </programlisting>
     </para>
     <para>
-     <literal>shmem_startup_hook</literal> can allocate in shared memory
+     <literal>shmem_startup_hook</literal> can allocate in dynamic shared memory
      custom wait events by calling while holding the LWLock
      <function>AddinShmemInitLock</function> to avoid any race conditions:
 <programlisting>
-uint32 WaitEventExtensionNew(void)
-</programlisting>
-     Next, each process needs to associate the wait event allocated previously
-     to a user-facing custom string, which is something done by calling:
-<programlisting>
-void WaitEventExtensionRegisterName(uint32 wait_event_info, const char *wait_event_name)
+uint32 WaitEventExtensionNew(const char *wait_event_name)
 </programlisting>
+     The wait event is associated to a user-facing custom string.
      An example can be found in <filename>src/test/modules/worker_spi</filename>
      in the PostgreSQL source tree.
     </para>
diff --git a/src/backend/storage/lmgr/lwlocknames.txt b/src/backend/storage/lmgr/lwlocknames.txt
index b34b6afecd..7af3e0ba1a 100644
--- a/src/backend/storage/lmgr/lwlocknames.txt
+++ b/src/backend/storage/lmgr/lwlocknames.txt
@@ -53,3 +53,4 @@ XactTruncationLock					44
 # 45 was XactTruncationLock until removal of BackendRandomLock
 WrapLimitsVacuumLock				46
 NotifyQueueTailLock					47
+WaitEventExtensionLock              48
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index b3596ece80..15b2dd06cc 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -45,6 +45,42 @@ uint32	   *my_wait_event_info = &local_my_wait_event_info;
 #define WAIT_EVENT_CLASS_MASK	0xFF000000
 #define WAIT_EVENT_ID_MASK		0x0000FFFF
 
+/*
+ * Hash tables for storing custom wait event ids and their names in
+ * shared memory.
+ *
+ * WaitEventExtensionNameHash is used to find the name from a event id.
+ * It enables all backends look up them without additional processing
+ * per backend like LWLockRegisterTranche().
+ *
+ * WaitEventExtensionIdHash is used to find the event id from a name.
+ * Since it can identify uniquness by the names, extensions that do not
+ * use shared memory also be able to define custom wait events without
+ * defining duplicate wait events.
+ *
+ * The size of hash table is based on assumption. In most of the case,
+ * 16 seem to be sufficient. It seems unlikely that the number of entries
+ * will reach 512.
+ */
+static HTAB *WaitEventExtensionNameHash;	/* find Ids from names */
+static HTAB *WaitEventExtensionIdHash;		/* find names from Ids */
+static const int wee_hash_init_size = 128;
+static const int wee_hash_max_size = 512;
+
+/* hash table entres */
+typedef struct WaitEventExtensionNameEntry
+{
+	uint16	event_id;	/* hash key */
+	char	wait_event_name[NAMEDATALEN]; /* custom wait event name */
+} WaitEventExtensionNameEntry;
+
+typedef struct WaitEventExtensionIdEntry
+{
+	char	wait_event_name[NAMEDATALEN]; /* hash key */
+	uint16	event_id;	/* wait event id */
+} WaitEventExtensionIdEntry;
+
+
 /* dynamic allocation counter for custom wait events in extensions */
 typedef struct WaitEventExtensionCounterData
 {
@@ -59,36 +95,35 @@ static WaitEventExtensionCounterData *WaitEventExtensionCounter;
 #define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
 	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
 
-/*
- * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
- * stores the names of all dynamically-created event IDs known to the current
- * process.  Any unused entries in the array will contain NULL.
- */
-static const char **WaitEventExtensionNames = NULL;
-static int	WaitEventExtensionNamesAllocated = 0;
-
 static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
 
 /*
- *  Return the space for dynamic allocation counter.
+ *  Return the space for dynamic shared hash tables and dynamic allocation counter.
  */
 Size
 WaitEventExtensionShmemSize(void)
 {
-	return sizeof(WaitEventExtensionCounterData);
+	Size	sz;
+	sz = MAXALIGN(sizeof(WaitEventExtensionCounterData));
+	sz = add_size(sz, hash_estimate_size(wee_hash_init_size,
+										sizeof(WaitEventExtensionNameEntry)));
+	sz = add_size(sz, hash_estimate_size(wee_hash_init_size,
+										sizeof(WaitEventExtensionIdEntry)));
+	return sz;
 }
 
 /*
- * Allocate shmem space for dynamic allocation counter.
+ * Allocate shmem space for dynamic shared hash and dynamic allocation counter.
  */
 void
 WaitEventExtensionShmemInit(void)
 {
 	bool		found;
+	HASHCTL		info;
 
 	WaitEventExtensionCounter = (WaitEventExtensionCounterData *)
 		ShmemInitStruct("WaitEventExtensionCounterData",
-						WaitEventExtensionShmemSize(), &found);
+						MAXALIGN(sizeof(WaitEventExtensionCounterData)), &found);
 
 	if (!found)
 	{
@@ -96,21 +131,63 @@ WaitEventExtensionShmemInit(void)
 		WaitEventExtensionCounter->nextId = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
 		SpinLockInit(&WaitEventExtensionCounter->mutex);
 	}
+
+	/* initialize or atatch the hash tables to store custom wait events */
+	info.keysize = sizeof(uint16);
+	info.entrysize = sizeof(WaitEventExtensionNameEntry);
+	WaitEventExtensionNameHash = ShmemInitHash("Wait Event Extension name hash",
+					wee_hash_init_size,
+					wee_hash_max_size,
+					&info,
+					HASH_ELEM | HASH_BLOBS);
+
+	info.keysize = sizeof(char[NAMEDATALEN]);
+	info.entrysize = sizeof(WaitEventExtensionIdEntry);
+	WaitEventExtensionIdHash = ShmemInitHash("Wait Event Extension id hash",
+					wee_hash_init_size,
+					wee_hash_max_size,
+					&info,
+					HASH_ELEM | HASH_BLOBS);
 }
 
 /*
- * Allocate a new event ID and return the wait event.
+ * Allocate a new event ID and return the wait event info.
+ *
+ * If the wait event name is already defined, this don't
+ * allocate new one, but it return the wait event info
+ * associated to the name.
  */
 uint32
-WaitEventExtensionNew(void)
+WaitEventExtensionNew(const char *wait_event_name)
 {
 	uint16		eventId;
+	bool		found;
+	WaitEventExtensionIdEntry	*id_entry;
+	WaitEventExtensionNameEntry	*name_entry;
 
 	Assert(LWLockHeldByMeInMode(AddinShmemInitLock, LW_EXCLUSIVE));
 
+	/* Check the limit of the length of the event name */
+	if (strlen(wait_event_name) >= NAMEDATALEN)
+		ereport(ERROR,
+				errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				errmsg("wait event name is too long"));
+
+	/*
+	 * First, check the wait event info associated to the name is
+	 * already defined. Return it if so.
+	 */
+	LWLockAcquire(WaitEventExtensionLock, LW_SHARED);
+	id_entry = (WaitEventExtensionIdEntry *)
+				hash_search(WaitEventExtensionIdHash, &wait_event_name, HASH_FIND, &found);
+	LWLockRelease(WaitEventExtensionLock);
+	if (found)
+		return PG_WAIT_EXTENSION | id_entry->event_id;
+
+	/* Allocate a new event Id */
 	SpinLockAcquire(&WaitEventExtensionCounter->mutex);
 
-	if (WaitEventExtensionCounter->nextId > PG_UINT16_MAX)
+	if (WaitEventExtensionCounter->nextId >= wee_hash_max_size)
 	{
 		SpinLockRelease(&WaitEventExtensionCounter->mutex);
 		ereport(ERROR,
@@ -122,64 +199,22 @@ WaitEventExtensionNew(void)
 
 	SpinLockRelease(&WaitEventExtensionCounter->mutex);
 
-	return PG_WAIT_EXTENSION | eventId;
-}
+	/* Register the new custom wait event in the shared hash table */
+	LWLockAcquire(WaitEventExtensionLock, LW_EXCLUSIVE);
 
-/*
- * Register a dynamic wait event name for extension in the lookup table
- * of the current process.
- *
- * This routine will save a pointer to the wait event name passed as an argument,
- * so the name should be allocated in a backend-lifetime context
- * (shared memory, TopMemoryContext, static constant, or similar).
- *
- * The "wait_event_name" will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-WaitEventExtensionRegisterName(uint32 wait_event_info,
-							   const char *wait_event_name)
-{
-	uint32		classId;
-	uint16		eventId;
+	name_entry = (WaitEventExtensionNameEntry *)
+				hash_search(WaitEventExtensionNameHash, &eventId, HASH_ENTER, &found);
+	Assert(!found);
+	strlcpy(name_entry->wait_event_name, wait_event_name, sizeof(name_entry->wait_event_name));
 
-	classId = wait_event_info & WAIT_EVENT_CLASS_MASK;
-	eventId = wait_event_info & WAIT_EVENT_ID_MASK;
+	id_entry = (WaitEventExtensionIdEntry *)
+				hash_search(WaitEventExtensionIdHash, &wait_event_name, HASH_ENTER, &found);
+	Assert(!found);
+	id_entry->event_id = eventId;
 
-	/* Check the wait event class. */
-	if (classId != PG_WAIT_EXTENSION)
-		ereport(ERROR,
-				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("invalid wait event class %u", classId));
+	LWLockRelease(WaitEventExtensionLock);
 
-	/* This should only be called for user-defined wait event. */
-	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
-		ereport(ERROR,
-				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("invalid wait event ID %u", eventId));
-
-	/* Convert to array index. */
-	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
-
-	/* If necessary, create or enlarge array. */
-	if (eventId >= WaitEventExtensionNamesAllocated)
-	{
-		uint32		newalloc;
-
-		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
-
-		if (WaitEventExtensionNames == NULL)
-			WaitEventExtensionNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			WaitEventExtensionNames =
-				repalloc0_array(WaitEventExtensionNames, const char *,
-								WaitEventExtensionNamesAllocated, newalloc);
-		WaitEventExtensionNamesAllocated = newalloc;
-	}
-
-	WaitEventExtensionNames[eventId] = wait_event_name;
+	return PG_WAIT_EXTENSION | eventId;
 }
 
 /*
@@ -188,23 +223,28 @@ WaitEventExtensionRegisterName(uint32 wait_event_info,
 static const char *
 GetWaitEventExtensionIdentifier(uint16 eventId)
 {
+	bool found;
+	WaitEventExtensionNameEntry	*entry;
+
 	/* Built-in event? */
 	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
 		return "Extension";
 
+	/* It is a user-defined wait event, so lookup hash table. */
+	LWLockAcquire(WaitEventExtensionLock, LW_SHARED);
+	entry = (WaitEventExtensionNameEntry *)
+				hash_search(WaitEventExtensionNameHash, &eventId, HASH_FIND, &found);
+	LWLockRelease(WaitEventExtensionLock);
+
 	/*
-	 * It is a user-defined wait event, so look at WaitEventExtensionNames[].
-	 * However, it is possible that the name has never been registered by
-	 * calling WaitEventExtensionRegisterName() in the current process, in
-	 * which case give up and return "extension".
+	 * The entry must be stored because it's registered in
+	 * WaitEventExtensionNew().
 	 */
-	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
-
-	if (eventId >= WaitEventExtensionNamesAllocated ||
-		WaitEventExtensionNames[eventId] == NULL)
-		return "extension";
+	if (!entry)
+		ereport(ERROR,
+				errmsg("could not find the name for custom wait event ID %u", eventId));
 
-	return WaitEventExtensionNames[eventId];
+	return entry->wait_event_name;
 }
 
 
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index aad8bc08fa..51a0b83b79 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -44,12 +44,13 @@ extern PGDLLIMPORT uint32 *my_wait_event_info;
  * Use this category when the server process is waiting for some condition
  * defined by an extension module.
  *
- * Extensions can define their own wait events in this category.  First,
- * they should call WaitEventExtensionNew() to get one or more wait event
- * IDs that are allocated from a shared counter.  These can be used directly
- * with pgstat_report_wait_start() or equivalent.  Next, each individual
- * process should call WaitEventExtensionRegisterName() to associate a wait
- * event string to the number allocated previously.
+ * Extensions can define their own wait events in this category. They should
+ * call WaitEventExtensionNew() with a wait event string. If the wait event
+ * associated the string is already allocated, it returns the wait event info.
+ * If not, it will get one wait event ID that is allocated from a shared counter,
+ * associate the string to the number in the shared dynamic hash and return the
+ * wait event info. The string can be used directly with pgstat_report_wait_start()
+ * or equivalent.
  */
 typedef enum
 {
@@ -60,9 +61,7 @@ typedef enum
 extern void WaitEventExtensionShmemInit(void);
 extern Size WaitEventExtensionShmemSize(void);
 
-extern uint32 WaitEventExtensionNew(void);
-extern void WaitEventExtensionRegisterName(uint32 wait_event_info,
-										   const char *wait_event_name);
+extern uint32 WaitEventExtensionNew(const char *wait_event_name);
 
 /* ----------
  * pgstat_report_wait_start() -
diff --git a/src/test/modules/worker_spi/t/001_worker_spi.pl b/src/test/modules/worker_spi/t/001_worker_spi.pl
index c3e7f5fbe6..26b8a49bec 100644
--- a/src/test/modules/worker_spi/t/001_worker_spi.pl
+++ b/src/test/modules/worker_spi/t/001_worker_spi.pl
@@ -39,25 +39,11 @@ $node->poll_query_until('postgres',
 $result = $node->safe_psql('postgres', 'SELECT * FROM schema4.counted;');
 is($result, qq(total|1), 'dynamic bgworker correctly consumed tuple data');
 
-# Check the wait event used by the dynamic bgworker.  For a session without
-# the state in shared memory known, the default of "extension" is the value
-# waited on.
+# Check the wait event used by the dynamic bgworker.
 $result = $node->poll_query_until(
 	'postgres',
 	qq[SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
-	'extension');
-is($result, 1, 'dynamic bgworker has reported "extension" as wait event');
-
-# If the shared memory state is loaded (here with worker_spi_init within
-# the same connection as the one querying pg_stat_activity), the wait
-# event is the custom one.
-# The expected result is a special pattern here with a newline coming from the
-# first query where the shared memory state is set.
-$result = $node->poll_query_until(
-	'postgres',
-	qq[SELECT worker_spi_init(); SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
-	qq[
-worker_spi_main]);
+	qq[worker_spi_main]);
 is($result, 1,
 	'dynamic bgworker has reported "worker_spi_main" as wait event');
 
diff --git a/src/test/modules/worker_spi/worker_spi--1.0.sql b/src/test/modules/worker_spi/worker_spi--1.0.sql
index f13f7e0f98..e9d5b07373 100644
--- a/src/test/modules/worker_spi/worker_spi--1.0.sql
+++ b/src/test/modules/worker_spi/worker_spi--1.0.sql
@@ -7,8 +7,3 @@ CREATE FUNCTION worker_spi_launch(pg_catalog.int4)
 RETURNS pg_catalog.int4 STRICT
 AS 'MODULE_PATHNAME'
 LANGUAGE C;
-
-CREATE FUNCTION worker_spi_init()
-RETURNS VOID STRICT
-AS 'MODULE_PATHNAME'
-LANGUAGE C;
diff --git a/src/test/modules/worker_spi/worker_spi.c b/src/test/modules/worker_spi/worker_spi.c
index c4317351ce..ee1ce13d50 100644
--- a/src/test/modules/worker_spi/worker_spi.c
+++ b/src/test/modules/worker_spi/worker_spi.c
@@ -44,7 +44,6 @@
 
 PG_MODULE_MAGIC;
 
-PG_FUNCTION_INFO_V1(worker_spi_init);
 PG_FUNCTION_INFO_V1(worker_spi_launch);
 
 PGDLLEXPORT void worker_spi_main(Datum main_arg) pg_attribute_noreturn();
@@ -124,14 +123,10 @@ worker_spi_shmem_init(void)
 
 	/* Define a new wait event */
 	if (!found)
-		wsstate->wait_event = WaitEventExtensionNew();
+		wsstate->wait_event = WaitEventExtensionNew("worker_spi_main");
 
 	LWLockRelease(AddinShmemInitLock);
 
-	/*
-	 * Register the wait event in the lookup table of the current process.
-	 */
-	WaitEventExtensionRegisterName(wsstate->wait_event, "worker_spi_main");
 	return;
 }
 
@@ -434,21 +429,6 @@ _PG_init(void)
 	}
 }
 
-/*
- * Wrapper to initialize a session with the shared memory state
- * used by this module.  This is a convenience routine to be able to
- * see the custom wait event stored in shared memory without loading
- * through shared_preload_libraries.
- */
-Datum
-worker_spi_init(PG_FUNCTION_ARGS)
-{
-	/* Create (if necessary) and attach to our shared memory area. */
-	worker_spi_shmem_init();
-
-	PG_RETURN_VOID();
-}
-
 /*
  * Dynamically launch an SPI worker.
  */
-- 
2.25.1

v2-0002-poc-custom-wait-event-for-dblink.patchtext/x-diff; name=v2-0002-poc-custom-wait-event-for-dblink.patchDownload
From 13bfa30d804d84502fd5de40c9ab735869a731d8 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <mshr.ikeda@ntt.com>
Date: Wed, 9 Aug 2023 19:40:02 +0900
Subject: [PATCH 2/2] poc: custom wait event for dblink

---
 contrib/dblink/dblink.c | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c
index 41e1f6c91d..1b3dd3736a 100644
--- a/contrib/dblink/dblink.c
+++ b/contrib/dblink/dblink.c
@@ -129,6 +129,7 @@ static void restoreLocalGucs(int nestlevel);
 /* Global */
 static remoteConn *pconn = NULL;
 static HTAB *remoteConnHash = NULL;
+static uint32 *wait_event_info = NULL;
 
 /*
  *	Following is list that holds multiple remote connections.
@@ -203,7 +204,14 @@ dblink_get_conn(char *conname_or_str,
 		dblink_connstr_check(connstr);
 
 		/* OK to make connection */
-		conn = libpqsrv_connect(connstr, WAIT_EVENT_EXTENSION);
+		if (wait_event_info == NULL)
+		{
+			wait_event_info = (uint32 *) MemoryContextAlloc(TopMemoryContext, sizeof(uint32));
+			LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+			*wait_event_info = WaitEventExtensionNew("dblink_get_con");
+			LWLockRelease(AddinShmemInitLock);
+		}
+		conn = libpqsrv_connect(connstr, *wait_event_info);
 
 		if (PQstatus(conn) == CONNECTION_BAD)
 		{
-- 
2.25.1

#67Andres Freund
andres@anarazel.de
In reply to: Masahiro Ikeda (#66)
Re: Support to define custom wait events for extensions

Hi,

On 2023-08-09 20:10:42 +0900, Masahiro Ikeda wrote:

* Is there any way to not force extensions that don't use shared
memory for their use like dblink to acquire AddinShmemInitLock?;

I think the caller shouldn't need to do deal with AddinShmemInitLock at all.

Greetings,

Andres Freund

#68Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#66)
Re: Support to define custom wait events for extensions

On Wed, Aug 09, 2023 at 08:10:42PM +0900, Masahiro Ikeda wrote:

* create second hash table to find a event id from a name to
identify uniquness. It enable extensions which don't use share
memory for their use to define custom wait events because
WaitEventExtensionNew() will not allocate duplicate wait events.

Okay, a second hash table to check if events are registered works for
me.

* create PoC patch to show that extensions, which don't use shared
memory for their use, can define custom wait events.
(v2-0002-poc-custom-wait-event-for-dblink.patch)

I'm worrying about
* Is 512(wee_hash_max_size) the maximum number of the custom wait
events sufficient?

Thanks for sending a patch!

I'm OK to start with that. This could always be revisited later, but
even for a server loaded with a bunch of extensions that looks more
than enough to me.

* Is there any way to not force extensions that don't use shared
memory for their use like dblink to acquire AddinShmemInitLock?;

Yes, they don't need it at all as the dynahashes are protected with
their own LWLocks.

+++ b/src/backend/storage/lmgr/lwlocknames.txt
@@ -53,3 +53,4 @@ XactTruncationLock                    44
 # 45 was XactTruncationLock until removal of BackendRandomLock
 WrapLimitsVacuumLock               46
 NotifyQueueTailLock                    47
+WaitEventExtensionLock              48

This new LWLock needs to be added to wait_event_names.txt, or it won't
be reported to pg_stat_activity and it would not be documented when
the sgml docs are generated from the txt data.

-extern uint32 WaitEventExtensionNew(void);
-extern void WaitEventExtensionRegisterName(uint32 wait_event_info,
-                                          const char *wait_event_name);
+extern uint32 WaitEventExtensionNew(const char *wait_event_name);
Looks about right, and the docs are refreshed.
+static const int wee_hash_init_size = 128;
+static const int wee_hash_max_size = 512;
I would use a few #defines with upper-case characters here instead as
these are constants for us.
Now that it is possible to rely on LWLocks for the hash tables, more
cleanup is possible in worker_spi, with the removal of
worker_spi_state, the shmem hooks and their routines.  The only thing
that should be needed is something like that at the start of
worker_spi_main() (same position as worker_spi_shmem_init now):
+static uint32 wait_event = 0;
[...]
+   if (wait_event == 0)
+       wait_event = WaitEventExtensionNew("worker_spi_main");

The updates in 001_worker_spi.pl look OK.

+    * The entry must be stored because it's registered in
+    * WaitEventExtensionNew().
     */
-   eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+   if (!entry)
+       ereport(ERROR,
+               errmsg("could not find the name for custom wait event ID %u", eventId));
Yeah, I think that's better than just falling back to "extension".  An
ID reported in pg_stat_activity should always have an entry, or we
have race conditions.  This should be an elog(ERROR), as in
this-error-shall-never-happen.  No need to translate the error string,
as well (the docs have been updated with this change. thanks!).

Additionally, LWLockHeldByMeInMode(AddinShmemInitLock) in
WaitEventExtensionNew() should not be needed, thanks to
WaitEventExtensionLock.

+ * WaitEventExtensionNameHash is used to find the name from a event id.
+ * It enables all backends look up them without additional processing
+ * per backend like LWLockRegisterTranche().

It does not seem necessary to mention LWLockRegisterTranche().

+ * WaitEventExtensionIdHash is used to find the event id from a name.
+ * Since it can identify uniquness by the names, extensions that do not
+ * use shared memory also be able to define custom wait events without
+ * defining duplicate wait events.

Perhaps this could just say that this table is necessary to ensure
that we don't have duplicated entries when registering new strings
with their IDs? s/uniquness/uniqueness/. The second part of the
sentence about shared memory does not seem necessary now.

+   sz = add_size(sz, hash_estimate_size(wee_hash_init_size,
+                                       sizeof(WaitEventExtensionNameEntry)));
+   sz = add_size(sz, hash_estimate_size(wee_hash_init_size,
+                                       sizeof(WaitEventExtensionIdEntry)));

Err, this should use the max size, and not the init size for the size
estimation, no?

+   if (strlen(wait_event_name) >= NAMEDATALEN)
+       ereport(ERROR,
+               errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+               errmsg("wait event name is too long"));
This could just be an elog(ERROR), I assume, as that could only be
reached by developers.  The string needs to be rewritten, like "cannot
use custom wait event string longer than %u characters", or something
like that.
+       if (wait_event_info == NULL)
+       {
+           wait_event_info = (uint32 *) MemoryContextAlloc(TopMemoryContext, sizeof(uint32));
+           LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
+           *wait_event_info = WaitEventExtensionNew("dblink_get_con");
+           LWLockRelease(AddinShmemInitLock);
+       }
+       conn = libpqsrv_connect(connstr, *wait_event_info)

In 0002. Caching the value statically in the backend is what you
should do, but a pointer, an allocation to the TopMemoryContext and a
dependency to AddinShmemInitLock should not be necessary when dealing
with a uint32. You could use an initial value of 0, for example, or
just PG_WAIT_EXTENSION but the latter is not really necessary and
would bypass the sanity checks.

+   /* Register the new custom wait event in the shared hash table */
+   LWLockAcquire(WaitEventExtensionLock, LW_EXCLUSIVE);
+
+   name_entry = (WaitEventExtensionNameEntry *)
+               hash_search(WaitEventExtensionNameHash, &eventId, HASH_ENTER, &found);
+   Assert(!found);
+   strlcpy(name_entry->wait_event_name, wait_event_name, sizeof(name_entry->wait_event_name));
+
+   id_entry = (WaitEventExtensionIdEntry *)
+               hash_search(WaitEventExtensionIdHash, &wait_event_name, HASH_ENTER, &found);
+   Assert(!found);
+   id_entry->event_id = eventId;
+
+   LWLockRelease(WaitEventExtensionLock);

The logic added to WaitEventExtensionNew() is a bit racy, where it
would be possible with the same entry to be added multiple times.
Imagine for example the following:
- Process 1 does WaitEventExtensionNew("foo1"), does not find the
entry by name in hash_search, gets an eventId of 1, releases the
spinlock.
- Process 2 calls as well WaitEventExtensionNew("foo1"), does not find
the entry by name because it has not been added by process 1 yet,
allocates an eventId of 2
- Process 2 takes first WaitEventExtensionLock in LW_EXCLUSIVE to add
entry "foo1", there is no entry by name, so one is added for the ID.
WaitEventExtensionLock is released
- Process 1, that was waiting on WaitEventExtensionLock, can now take
it in exclusive mode. It finds an entry by name for "foo1", fails the
assertion because an entry is found.

I think that the ordering of WaitEventExtensionNew() should be
reworked a bit. This order should be safer.
- Take WaitEventExtensionLock in shared mode, look if there's an entry
by name, release the lock. The patch does that.
- If an entry is found, return, we're OK. The patch does that.
- Take again WaitEventExtensionLock in exclusive mode.
- Look again at the hash table with the name given, in case somebody
has inserted an equivalent entry in the short window where the lock
was not held.
-- If an entry is found, release the lock and leave, we're OK.
-- If an entry is not found, keep the lock.
- Acquire the spinlock, and get a new event ID. Release spinlock.
- Add the new entries to both tables, both assertions on found are OK
to have.
- Release LWLock and leave.
--
Michael

#69Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#68)
2 attachment(s)
Re: Support to define custom wait events for extensions

Hi,

Thanks for your comments about the v2 patches. I updated to v3 patches.

The main changes are:
* remove the AddinShmemInitLock assertion
* add the new lock (WaitEventExtensionLock) to wait_event_names.txt
* change "static const int wee_hash_XXX_size" to "#define XXX"
* simplify worker_spi. I removed codes related to share memory and
try to allocate the new wait event before waiting per background
worker.
* change to elog from ereport because the errors are for developers.
* revise comments as advised.
* fix the request size for shared memory correctly
* simplify dblink.c
* fix process ordering of WaitEventExtensionNew() as advised to
avoid leading illegal state.

In addition, I change the followings:
* update about custom wait events in sgml. we don't need to use
shmem_startup_hook.
* change the hash names for readability.
(ex. WaitEventExtensionNameHash -> WaitEventExtensionHashById)
* fix a bug to fail to get already defined events by names
because HASH_BLOBS was specified. HASH_STRINGS is right since
the key is C strings.

I create a new entry in commitfest for CI testing.
(https://commitfest.postgresql.org/44/4494/)

Thanks for closing the former entry.
(https://commitfest.postgresql.org/43/4368/)

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

Attachments:

v3-0002-poc-custom-wait-event-for-dblink.patchtext/x-diff; name=v3-0002-poc-custom-wait-event-for-dblink.patchDownload
From 234cc7d852aebf78285ccde383f27ea35a27dad2 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <mshr.ikeda@ntt.com>
Date: Thu, 10 Aug 2023 10:44:41 +0900
Subject: [PATCH 2/2] poc: custom wait event for dblink

---
 contrib/dblink/dblink.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c
index 41e1f6c91d..b7158ecd4f 100644
--- a/contrib/dblink/dblink.c
+++ b/contrib/dblink/dblink.c
@@ -129,6 +129,7 @@ static void restoreLocalGucs(int nestlevel);
 /* Global */
 static remoteConn *pconn = NULL;
 static HTAB *remoteConnHash = NULL;
+static uint32 wait_event_info = 0;
 
 /*
  *	Following is list that holds multiple remote connections.
@@ -203,7 +204,9 @@ dblink_get_conn(char *conname_or_str,
 		dblink_connstr_check(connstr);
 
 		/* OK to make connection */
-		conn = libpqsrv_connect(connstr, WAIT_EVENT_EXTENSION);
+		if (wait_event_info == 0)
+			wait_event_info = WaitEventExtensionNew("dblink_get_con");
+		conn = libpqsrv_connect(connstr, wait_event_info);
 
 		if (PQstatus(conn) == CONNECTION_BAD)
 		{
-- 
2.25.1

v3-0001-Change-to-manage-custom-wait-events-in-shared-hash.patchtext/x-diff; name=v3-0001-Change-to-manage-custom-wait-events-in-shared-hash.patchDownload
From 07bbff06a0f58f08d313c02cbb4104ad1db2a0e9 Mon Sep 17 00:00:00 2001
From: Masahiro Ikeda <mshr.ikeda@ntt.com>
Date: Thu, 10 Aug 2023 10:43:34 +0900
Subject: [PATCH 1/2] Change to manage custom wait events in shared hash

Currently, names of the custom wait event must be registered
per backends. This patch relaxes the constraints by store the
wait events and their names in hash tables in shared memory.

So, all backends can look up the wait event names on
pg_stat_activity view without additional processing by extensions.

In addition, extensions which do not use shared memory for their
use can define custom wait events because WaitEventExtensionNew()
will never allocate new one if the wait event associated to the
name is already allocated. It use hash table to identify uniqueness.
---
 doc/src/sgml/monitoring.sgml                  |   7 +-
 doc/src/sgml/xfunc.sgml                       |  26 +--
 src/backend/storage/lmgr/lwlocknames.txt      |   1 +
 src/backend/utils/activity/wait_event.c       | 219 +++++++++++-------
 .../utils/activity/wait_event_names.txt       |   1 +
 src/include/utils/wait_event.h                |  17 +-
 .../modules/worker_spi/t/001_worker_spi.pl    |  18 +-
 .../modules/worker_spi/worker_spi--1.0.sql    |   5 -
 src/test/modules/worker_spi/worker_spi.c      | 109 +--------
 9 files changed, 165 insertions(+), 238 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index f4fc5d814f..19181832d7 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1121,10 +1121,9 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
      <literal>LWLock</literal> types
      to the list shown in <xref linkend="wait-event-extension-table"/> and
      <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
-     assigned by an extension will not be available in all server processes;
-     so an <literal>Extension</literal> or <literal>LWLock</literal> wait
-     event might be reported as just
-     <quote><literal>extension</literal></quote> rather than the
+     of <literal>LWLock</literal> assigned by an extension will not be
+     available in all server processes; so an wait event might be reported
+     as just <quote><literal>extension</literal></quote> rather than the
      extension-assigned name.
     </para>
    </note>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index d6345a775b..281c178b0e 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3454,33 +3454,15 @@ if (!ptr)
    </sect2>
 
    <sect2 id="xfunc-addin-wait-events">
-    <title>Shared Memory and Custom Wait Events</title>
+    <title>Custom Wait Events</title>
 
     <para>
      Add-ins can define custom wait events under the wait event type
-     <literal>Extension</literal>. The add-in's shared library must be
-     preloaded by specifying it in <literal>shared_preload_libraries</literal>,
-     and register a <literal>shmem_request_hook</literal> and a
-     <literal>shmem_startup_hook</literal> in its
-     <function>_PG_init</function> function.
-     <literal>shmem_request_hook</literal> can request a shared memory size
-     to be later used at startup by calling:
+     <literal>Extension</literal> by calling:
 <programlisting>
-void RequestAddinShmemSpace(int size)
-</programlisting>
-    </para>
-    <para>
-     <literal>shmem_startup_hook</literal> can allocate in shared memory
-     custom wait events by calling while holding the LWLock
-     <function>AddinShmemInitLock</function> to avoid any race conditions:
-<programlisting>
-uint32 WaitEventExtensionNew(void)
-</programlisting>
-     Next, each process needs to associate the wait event allocated previously
-     to a user-facing custom string, which is something done by calling:
-<programlisting>
-void WaitEventExtensionRegisterName(uint32 wait_event_info, const char *wait_event_name)
+uint32 WaitEventExtensionNew(const char *wait_event_name)
 </programlisting>
+     The wait event is associated to a user-facing custom string.
      An example can be found in <filename>src/test/modules/worker_spi</filename>
      in the PostgreSQL source tree.
     </para>
diff --git a/src/backend/storage/lmgr/lwlocknames.txt b/src/backend/storage/lmgr/lwlocknames.txt
index b34b6afecd..7af3e0ba1a 100644
--- a/src/backend/storage/lmgr/lwlocknames.txt
+++ b/src/backend/storage/lmgr/lwlocknames.txt
@@ -53,3 +53,4 @@ XactTruncationLock					44
 # 45 was XactTruncationLock until removal of BackendRandomLock
 WrapLimitsVacuumLock				46
 NotifyQueueTailLock					47
+WaitEventExtensionLock              48
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index b3596ece80..d2b21d54a6 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -45,6 +45,42 @@ uint32	   *my_wait_event_info = &local_my_wait_event_info;
 #define WAIT_EVENT_CLASS_MASK	0xFF000000
 #define WAIT_EVENT_ID_MASK		0x0000FFFF
 
+/*
+ * Hash tables for storing custom wait event ids and their names in
+ * shared memory.
+ *
+ * WaitEventExtensionHashById is used to find the name from a event id.
+ * It enables all backends look up them without additional processing
+ * per backend.
+ *
+ * WaitEventExtensionHashByName is used to find the event id from a name.
+ * It's used to ensure that duplicated entries won't be registered
+ * by identify uniqueness by the names
+ *
+ * The size of hash table is based on assumption. In most of the case,
+ * 16 seem to be sufficient. It seems unlikely that the number of entries
+ * will reach 512.
+ */
+static HTAB *WaitEventExtensionHashById;	/* find names from ids */
+static HTAB *WaitEventExtensionHashByName;	/* find ids from names */
+
+#define WaitEventExtensionHashInitSize 128
+#define WaitEventExtensionHashMaxSize 512
+
+/* hash table entres */
+typedef struct WaitEventExtensionEntryById
+{
+	uint16	event_id;						/* hash key */
+	char	wait_event_name[NAMEDATALEN];	/* custom wait event name */
+} WaitEventExtensionEntryById;
+
+typedef struct WaitEventExtensionEntryByName
+{
+	char	wait_event_name[NAMEDATALEN];	/* hash key */
+	uint16	event_id;						/* wait event id */
+} WaitEventExtensionEntryByName;
+
+
 /* dynamic allocation counter for custom wait events in extensions */
 typedef struct WaitEventExtensionCounterData
 {
@@ -59,36 +95,38 @@ static WaitEventExtensionCounterData *WaitEventExtensionCounter;
 #define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
 	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
 
-/*
- * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
- * stores the names of all dynamically-created event IDs known to the current
- * process.  Any unused entries in the array will contain NULL.
- */
-static const char **WaitEventExtensionNames = NULL;
-static int	WaitEventExtensionNamesAllocated = 0;
+/* wait event info for extensions */
+#define WAIT_EVENT_EXTENSION_INFO(eventId)	(PG_WAIT_EXTENSION | eventId)
 
 static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
 
 /*
- *  Return the space for dynamic allocation counter.
+ *  Return the space for dynamic shared hash tables and dynamic allocation counter.
  */
 Size
 WaitEventExtensionShmemSize(void)
 {
-	return sizeof(WaitEventExtensionCounterData);
+	Size	sz;
+	sz = MAXALIGN(sizeof(WaitEventExtensionCounterData));
+	sz = add_size(sz, hash_estimate_size(WaitEventExtensionHashMaxSize,
+										sizeof(WaitEventExtensionEntryById)));
+	sz = add_size(sz, hash_estimate_size(WaitEventExtensionHashMaxSize,
+										sizeof(WaitEventExtensionEntryByName)));
+	return sz;
 }
 
 /*
- * Allocate shmem space for dynamic allocation counter.
+ * Allocate shmem space for dynamic shared hash and dynamic allocation counter.
  */
 void
 WaitEventExtensionShmemInit(void)
 {
 	bool		found;
+	HASHCTL		info;
 
 	WaitEventExtensionCounter = (WaitEventExtensionCounterData *)
 		ShmemInitStruct("WaitEventExtensionCounterData",
-						WaitEventExtensionShmemSize(), &found);
+						MAXALIGN(sizeof(WaitEventExtensionCounterData)), &found);
 
 	if (!found)
 	{
@@ -96,21 +134,77 @@ WaitEventExtensionShmemInit(void)
 		WaitEventExtensionCounter->nextId = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
 		SpinLockInit(&WaitEventExtensionCounter->mutex);
 	}
+
+	/* initialize or atatch the hash tables to store custom wait events */
+	info.keysize = sizeof(uint16);
+	info.entrysize = sizeof(WaitEventExtensionEntryById);
+	WaitEventExtensionHashById = ShmemInitHash("Wait Event Extension hash by id",
+					WaitEventExtensionHashInitSize,
+					WaitEventExtensionHashMaxSize,
+					&info,
+					HASH_ELEM | HASH_BLOBS);
+
+	info.keysize = sizeof(char[NAMEDATALEN]);
+	info.entrysize = sizeof(WaitEventExtensionEntryByName);
+	WaitEventExtensionHashByName = ShmemInitHash("Wait Event Extension hash by name",
+					WaitEventExtensionHashInitSize,
+					WaitEventExtensionHashMaxSize,
+					&info,
+					HASH_ELEM | HASH_STRINGS);   /* key is Null-terminated C strings */
 }
 
 /*
- * Allocate a new event ID and return the wait event.
+ * Allocate a new event ID and return the wait event info.
+ *
+ * If the wait event name is already defined, this don't
+ * allocate new one, but it return the wait event info
+ * associated to the name.
  */
 uint32
-WaitEventExtensionNew(void)
+WaitEventExtensionNew(const char *wait_event_name)
 {
 	uint16		eventId;
+	bool		found;
+	WaitEventExtensionEntryByName	*entry_by_name;
+	WaitEventExtensionEntryById	*entry_by_id;
+
+	/* Check the limit of the length of the event name */
+	if (strlen(wait_event_name) >= NAMEDATALEN)
+		elog(ERROR,
+			"cannot use custom wait event string longer than %u characters",
+			NAMEDATALEN - 1);
 
-	Assert(LWLockHeldByMeInMode(AddinShmemInitLock, LW_EXCLUSIVE));
+	/*
+	 * Check the wait event info associated to the name is already defined.
+	 * Return it if so.
+	 */
+	LWLockAcquire(WaitEventExtensionLock, LW_SHARED);
+	entry_by_name = (WaitEventExtensionEntryByName *)
+				hash_search(WaitEventExtensionHashByName, wait_event_name,
+							HASH_FIND, &found);
+	LWLockRelease(WaitEventExtensionLock);
+	if (found)
+		return WAIT_EVENT_EXTENSION_INFO(entry_by_name->event_id);
+
+	/*
+	 * Allocate and register a new wait event. But, we need to recheck because
+	 * other processes could already do so while releasing the lock.
+	 */
+	LWLockAcquire(WaitEventExtensionLock, LW_EXCLUSIVE);
+	entry_by_name = (WaitEventExtensionEntryByName *)
+				hash_search(WaitEventExtensionHashByName, wait_event_name,
+							HASH_FIND, &found);
+	/* Recheck */
+	if (found)
+	{
+		LWLockRelease(WaitEventExtensionLock);
+		return WAIT_EVENT_EXTENSION_INFO(entry_by_name->event_id);
+	}
 
+	/* Allocate a new event Id */
 	SpinLockAcquire(&WaitEventExtensionCounter->mutex);
 
-	if (WaitEventExtensionCounter->nextId > PG_UINT16_MAX)
+	if (WaitEventExtensionCounter->nextId >= WaitEventExtensionHashMaxSize)
 	{
 		SpinLockRelease(&WaitEventExtensionCounter->mutex);
 		ereport(ERROR,
@@ -122,64 +216,23 @@ WaitEventExtensionNew(void)
 
 	SpinLockRelease(&WaitEventExtensionCounter->mutex);
 
-	return PG_WAIT_EXTENSION | eventId;
-}
-
-/*
- * Register a dynamic wait event name for extension in the lookup table
- * of the current process.
- *
- * This routine will save a pointer to the wait event name passed as an argument,
- * so the name should be allocated in a backend-lifetime context
- * (shared memory, TopMemoryContext, static constant, or similar).
- *
- * The "wait_event_name" will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-WaitEventExtensionRegisterName(uint32 wait_event_info,
-							   const char *wait_event_name)
-{
-	uint32		classId;
-	uint16		eventId;
-
-	classId = wait_event_info & WAIT_EVENT_CLASS_MASK;
-	eventId = wait_event_info & WAIT_EVENT_ID_MASK;
-
-	/* Check the wait event class. */
-	if (classId != PG_WAIT_EXTENSION)
-		ereport(ERROR,
-				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("invalid wait event class %u", classId));
-
-	/* This should only be called for user-defined wait event. */
-	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
-		ereport(ERROR,
-				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("invalid wait event ID %u", eventId));
+	/* Register the new wait event */
+	entry_by_id = (WaitEventExtensionEntryById *)
+				hash_search(WaitEventExtensionHashById, &eventId,
+							HASH_ENTER, &found);
+	Assert(!found);
+	strlcpy(entry_by_id->wait_event_name, wait_event_name,
+			sizeof(entry_by_id->wait_event_name));
 
-	/* Convert to array index. */
-	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+	entry_by_name = (WaitEventExtensionEntryByName *)
+				hash_search(WaitEventExtensionHashByName, wait_event_name,
+							HASH_ENTER, &found);
+	Assert(!found);
+	entry_by_name->event_id = eventId;
 
-	/* If necessary, create or enlarge array. */
-	if (eventId >= WaitEventExtensionNamesAllocated)
-	{
-		uint32		newalloc;
-
-		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
-
-		if (WaitEventExtensionNames == NULL)
-			WaitEventExtensionNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			WaitEventExtensionNames =
-				repalloc0_array(WaitEventExtensionNames, const char *,
-								WaitEventExtensionNamesAllocated, newalloc);
-		WaitEventExtensionNamesAllocated = newalloc;
-	}
+	LWLockRelease(WaitEventExtensionLock);
 
-	WaitEventExtensionNames[eventId] = wait_event_name;
+	return WAIT_EVENT_EXTENSION_INFO(eventId);
 }
 
 /*
@@ -188,23 +241,29 @@ WaitEventExtensionRegisterName(uint32 wait_event_info,
 static const char *
 GetWaitEventExtensionIdentifier(uint16 eventId)
 {
+	bool found;
+	WaitEventExtensionEntryById	*entry;
+
 	/* Built-in event? */
 	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
 		return "Extension";
 
+	/* It is a user-defined wait event, so lookup hash table. */
+	LWLockAcquire(WaitEventExtensionLock, LW_SHARED);
+	entry = (WaitEventExtensionEntryById *)
+				hash_search(WaitEventExtensionHashById, &eventId,
+							HASH_FIND, &found);
+	LWLockRelease(WaitEventExtensionLock);
+
 	/*
-	 * It is a user-defined wait event, so look at WaitEventExtensionNames[].
-	 * However, it is possible that the name has never been registered by
-	 * calling WaitEventExtensionRegisterName() in the current process, in
-	 * which case give up and return "extension".
+	 * The entry must be stored because it's registered in
+	 * WaitEventExtensionNew().
 	 */
-	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
-
-	if (eventId >= WaitEventExtensionNamesAllocated ||
-		WaitEventExtensionNames[eventId] == NULL)
-		return "extension";
+	if (!entry)
+		elog(ERROR, "could not find the name for custom wait event ID %u",
+			eventId);
 
-	return WaitEventExtensionNames[eventId];
+	return entry->wait_event_name;
 }
 
 
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 2ea4789b00..a4e5c80bc0 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -317,6 +317,7 @@ WAIT_EVENT_DOCONLY	LogicalRepWorker	"Waiting to read or update the state of logi
 WAIT_EVENT_DOCONLY	XactTruncation	"Waiting to execute <function>pg_xact_status</function> or update the oldest transaction ID available to it."
 WAIT_EVENT_DOCONLY	WrapLimitsVacuum	"Waiting to update limits on transaction id and multixact consumption."
 WAIT_EVENT_DOCONLY	NotifyQueueTail	"Waiting to update limit on <command>NOTIFY</command> message storage."
+WAIT_EVENT_DOCONLY	WaitEventExtension	"Waiting to read or update custom wait events information for extensions."
 
 WAIT_EVENT_DOCONLY	XactBuffer	"Waiting for I/O on a transaction status SLRU buffer."
 WAIT_EVENT_DOCONLY	CommitTsBuffer	"Waiting for I/O on a commit timestamp SLRU buffer."
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index aad8bc08fa..51a0b83b79 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -44,12 +44,13 @@ extern PGDLLIMPORT uint32 *my_wait_event_info;
  * Use this category when the server process is waiting for some condition
  * defined by an extension module.
  *
- * Extensions can define their own wait events in this category.  First,
- * they should call WaitEventExtensionNew() to get one or more wait event
- * IDs that are allocated from a shared counter.  These can be used directly
- * with pgstat_report_wait_start() or equivalent.  Next, each individual
- * process should call WaitEventExtensionRegisterName() to associate a wait
- * event string to the number allocated previously.
+ * Extensions can define their own wait events in this category. They should
+ * call WaitEventExtensionNew() with a wait event string. If the wait event
+ * associated the string is already allocated, it returns the wait event info.
+ * If not, it will get one wait event ID that is allocated from a shared counter,
+ * associate the string to the number in the shared dynamic hash and return the
+ * wait event info. The string can be used directly with pgstat_report_wait_start()
+ * or equivalent.
  */
 typedef enum
 {
@@ -60,9 +61,7 @@ typedef enum
 extern void WaitEventExtensionShmemInit(void);
 extern Size WaitEventExtensionShmemSize(void);
 
-extern uint32 WaitEventExtensionNew(void);
-extern void WaitEventExtensionRegisterName(uint32 wait_event_info,
-										   const char *wait_event_name);
+extern uint32 WaitEventExtensionNew(const char *wait_event_name);
 
 /* ----------
  * pgstat_report_wait_start() -
diff --git a/src/test/modules/worker_spi/t/001_worker_spi.pl b/src/test/modules/worker_spi/t/001_worker_spi.pl
index c3e7f5fbe6..26b8a49bec 100644
--- a/src/test/modules/worker_spi/t/001_worker_spi.pl
+++ b/src/test/modules/worker_spi/t/001_worker_spi.pl
@@ -39,25 +39,11 @@ $node->poll_query_until('postgres',
 $result = $node->safe_psql('postgres', 'SELECT * FROM schema4.counted;');
 is($result, qq(total|1), 'dynamic bgworker correctly consumed tuple data');
 
-# Check the wait event used by the dynamic bgworker.  For a session without
-# the state in shared memory known, the default of "extension" is the value
-# waited on.
+# Check the wait event used by the dynamic bgworker.
 $result = $node->poll_query_until(
 	'postgres',
 	qq[SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
-	'extension');
-is($result, 1, 'dynamic bgworker has reported "extension" as wait event');
-
-# If the shared memory state is loaded (here with worker_spi_init within
-# the same connection as the one querying pg_stat_activity), the wait
-# event is the custom one.
-# The expected result is a special pattern here with a newline coming from the
-# first query where the shared memory state is set.
-$result = $node->poll_query_until(
-	'postgres',
-	qq[SELECT worker_spi_init(); SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
-	qq[
-worker_spi_main]);
+	qq[worker_spi_main]);
 is($result, 1,
 	'dynamic bgworker has reported "worker_spi_main" as wait event');
 
diff --git a/src/test/modules/worker_spi/worker_spi--1.0.sql b/src/test/modules/worker_spi/worker_spi--1.0.sql
index f13f7e0f98..e9d5b07373 100644
--- a/src/test/modules/worker_spi/worker_spi--1.0.sql
+++ b/src/test/modules/worker_spi/worker_spi--1.0.sql
@@ -7,8 +7,3 @@ CREATE FUNCTION worker_spi_launch(pg_catalog.int4)
 RETURNS pg_catalog.int4 STRICT
 AS 'MODULE_PATHNAME'
 LANGUAGE C;
-
-CREATE FUNCTION worker_spi_init()
-RETURNS VOID STRICT
-AS 'MODULE_PATHNAME'
-LANGUAGE C;
diff --git a/src/test/modules/worker_spi/worker_spi.c b/src/test/modules/worker_spi/worker_spi.c
index c4317351ce..56e492dd4b 100644
--- a/src/test/modules/worker_spi/worker_spi.c
+++ b/src/test/modules/worker_spi/worker_spi.c
@@ -44,33 +44,17 @@
 
 PG_MODULE_MAGIC;
 
-PG_FUNCTION_INFO_V1(worker_spi_init);
 PG_FUNCTION_INFO_V1(worker_spi_launch);
 
 PGDLLEXPORT void worker_spi_main(Datum main_arg) pg_attribute_noreturn();
 
-/* Shared memory state */
-typedef struct worker_spi_state
-{
-	/* the wait event defined during initialization phase */
-	uint32		wait_event;
-} worker_spi_state;
-
-static worker_spi_state *wsstate = NULL;	/* pointer to shared memory */
-
-static shmem_request_hook_type prev_shmem_request_hook = NULL;
-static shmem_request_hook_type prev_shmem_startup_hook = NULL;
-
-static void worker_spi_shmem_request(void);
-static void worker_spi_shmem_startup(void);
-static void worker_spi_shmem_init(void);
-static Size worker_spi_memsize(void);
-
 /* GUC variables */
 static int	worker_spi_naptime = 10;
 static int	worker_spi_total_workers = 2;
 static char *worker_spi_database = NULL;
 
+/* Local variables */
+static uint32 worker_spi_wait_event = 0;
 
 typedef struct worktable
 {
@@ -78,63 +62,6 @@ typedef struct worktable
 	const char *name;
 } worktable;
 
-static void
-worker_spi_shmem_request(void)
-{
-	if (prev_shmem_request_hook)
-		prev_shmem_request_hook();
-
-	RequestAddinShmemSpace(worker_spi_memsize());
-}
-
-static void
-worker_spi_shmem_startup(void)
-{
-	if (prev_shmem_startup_hook)
-		prev_shmem_startup_hook();
-
-	worker_spi_shmem_init();
-}
-
-static Size
-worker_spi_memsize(void)
-{
-	return MAXALIGN(sizeof(worker_spi_state));
-}
-
-/*
- * Initialize the shared memory state of worker_spi.
- *
- * This routine allocates a new wait event when called the first time.
- * On follow-up calls, the name of the wait event associated with the
- * existing shared memory state is registered.
- */
-static void
-worker_spi_shmem_init(void)
-{
-	bool		found;
-
-	wsstate = NULL;
-
-	/* Create or attach to the shared memory state */
-	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
-	wsstate = ShmemInitStruct("worker_spi State",
-							  sizeof(worker_spi_state),
-							  &found);
-
-	/* Define a new wait event */
-	if (!found)
-		wsstate->wait_event = WaitEventExtensionNew();
-
-	LWLockRelease(AddinShmemInitLock);
-
-	/*
-	 * Register the wait event in the lookup table of the current process.
-	 */
-	WaitEventExtensionRegisterName(wsstate->wait_event, "worker_spi_main");
-	return;
-}
-
 /*
  * Initialize workspace for a worker process: create the schema if it doesn't
  * already exist.
@@ -224,9 +151,6 @@ worker_spi_main(Datum main_arg)
 	/* We're now ready to receive signals */
 	BackgroundWorkerUnblockSignals();
 
-	/* Create (if necessary) and attach to our shared memory area. */
-	worker_spi_shmem_init();
-
 	/* Connect to our database */
 	BackgroundWorkerInitializeConnection(worker_spi_database, NULL, 0);
 
@@ -268,6 +192,10 @@ worker_spi_main(Datum main_arg)
 	{
 		int			ret;
 
+		/* First time, allocate or get the custom wait event */
+		if (worker_spi_wait_event == 0)
+			worker_spi_wait_event = WaitEventExtensionNew("worker_spi_main");
+
 		/*
 		 * Background workers mustn't call usleep() or any direct equivalent:
 		 * instead, they may wait on their process latch, which sleeps as
@@ -277,7 +205,7 @@ worker_spi_main(Datum main_arg)
 		(void) WaitLatch(MyLatch,
 						 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
 						 worker_spi_naptime * 1000L,
-						 wsstate->wait_event);
+						 worker_spi_wait_event);
 		ResetLatch(MyLatch);
 
 		CHECK_FOR_INTERRUPTS();
@@ -406,11 +334,6 @@ _PG_init(void)
 
 	MarkGUCPrefixReserved("worker_spi");
 
-	prev_shmem_request_hook = shmem_request_hook;
-	shmem_request_hook = worker_spi_shmem_request;
-	prev_shmem_startup_hook = shmem_startup_hook;
-	shmem_startup_hook = worker_spi_shmem_startup;
-
 	/* set up common data for all our workers */
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
@@ -434,21 +357,6 @@ _PG_init(void)
 	}
 }
 
-/*
- * Wrapper to initialize a session with the shared memory state
- * used by this module.  This is a convenience routine to be able to
- * see the custom wait event stored in shared memory without loading
- * through shared_preload_libraries.
- */
-Datum
-worker_spi_init(PG_FUNCTION_ARGS)
-{
-	/* Create (if necessary) and attach to our shared memory area. */
-	worker_spi_shmem_init();
-
-	PG_RETURN_VOID();
-}
-
 /*
  * Dynamically launch an SPI worker.
  */
@@ -461,9 +369,6 @@ worker_spi_launch(PG_FUNCTION_ARGS)
 	BgwHandleStatus status;
 	pid_t		pid;
 
-	/* Create (if necessary) and attach to our shared memory area. */
-	worker_spi_shmem_init();
-
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
-- 
2.25.1

#70Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#69)
Re: Support to define custom wait events for extensions

On Thu, Aug 10, 2023 at 01:08:39PM +0900, Masahiro Ikeda wrote:

In addition, I change the followings:
* update about custom wait events in sgml. we don't need to use
shmem_startup_hook.
* change the hash names for readability.
(ex. WaitEventExtensionNameHash -> WaitEventExtensionHashById)
* fix a bug to fail to get already defined events by names
because HASH_BLOBS was specified. HASH_STRINGS is right since
the key is C strings.

That's what I get based on what ShmemInitHash() relies on.

I got a few more comments about v3. Overall this looks much better.

This time the ordering of the operations in WaitEventExtensionNew()
looks much better.

+    * The entry must be stored because it's registered in
+    * WaitEventExtensionNew().
Not sure of the value added by this comment, I would remove it.
+   if (!entry)
+       elog(ERROR, "could not find the name for custom wait event ID %u",
+           eventId);

Or a simpler "could not find custom wait event name for ID %u"?

+static HTAB *WaitEventExtensionHashById;   /* find names from ids */
+static HTAB *WaitEventExtensionHashByName; /* find ids from names */

These names are OK here.

+/* Local variables */
+static uint32 worker_spi_wait_event = 0;
That's a cached value, used as a placeholder for the custom wait event
ID found from the table.

+ HASH_ELEM | HASH_STRINGS); /* key is Null-terminated C strings */
Looks obvious to me based on the code, I would remove this note.

+/* hash table entres */
s/entres/entries/

+   /*
+    * Allocate and register a new wait event. But, we need to recheck because
+    * other processes could already do so while releasing the lock.
+    */

Could be reworked for the second sentence, like "Recheck if the event
exists, as it could be possible that a concurrent process has inserted
one with the same name while the lock was previously released."

+ /* Recheck */
Duplicate.

        /* OK to make connection */
-       conn = libpqsrv_connect(connstr, WAIT_EVENT_EXTENSION);
+       if (wait_event_info == 0)
+           wait_event_info = WaitEventExtensionNew("dblink_get_con");
+       conn = libpqsrv_connect(connstr, wait_event_info);

It is going to be difficult to make that simpler ;)

This looks correct, but perhaps we need to think harder about the
custom event names and define a convention when more of this stuff is
added to the core modules.
--
Michael

#71Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#70)
1 attachment(s)
Re: Support to define custom wait events for extensions

On Thu, Aug 10, 2023 at 05:37:55PM +0900, Michael Paquier wrote:

This looks correct, but perhaps we need to think harder about the
custom event names and define a convention when more of this stuff is
added to the core modules.

Okay, I have put my hands on that, fixing a couple of typos, polishing
a couple of comments, clarifying the docs and applying an indentation.
And here is a v4.

Any thoughts or comments? I'd like to apply that soon, so as we are
able to move on with the wait event catalog and assigning custom wait
events to the other in-core modules.
--
Michael

Attachments:

v4-0001-Change-custom-wait-events-to-use-shared-hash-tabl.patchtext/x-diff; charset=us-asciiDownload
From 83e40dace75d0df91e8b7f617bd03d02bd8450d4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 14 Aug 2023 07:48:15 +0900
Subject: [PATCH v4] Change custom wait events to use shared hash tables

Currently, names of the custom wait event must be registered for each
backends, requiring all these to link to the shared memory area of an
extention, even if not loaded with shared_preload_libraries.

This patch relaxes the constraints by storing the wait events and their
names in hash tables in shared memory.  First, this has the advantage to
simplify the registration of custom wait events to a single routine
call that returns an event ID, either existing or incremented:
uint32 WaitEventExtensionNew(const char *wait_event_name);

Any other backend looking at the wait event information, for instance
via pg_stat_activity, is then able to automatically know about the new
event name.

The code changes done in worker_spi show how things are simplified:
- worker_spi_init() is gone.
- No more shmem hooks.
- The custom wait event ID is cached in the process that needs to set
it, with one single call to WaitEventExtensionNew() to retrieve it.

Per suggestion from Andres Freund.

Author: Masahiro Ikeda
Discussion: https://postgr.es/m/20230801032349.aaiuvhtrcvvcwzcx@awork3.anarazel.de
---
 src/include/utils/wait_event.h                |  18 +-
 src/backend/storage/lmgr/lwlocknames.txt      |   1 +
 src/backend/utils/activity/wait_event.c       | 217 +++++++++++-------
 .../utils/activity/wait_event_names.txt       |   1 +
 .../modules/worker_spi/t/001_worker_spi.pl    |  18 +-
 .../modules/worker_spi/worker_spi--1.0.sql    |   5 -
 src/test/modules/worker_spi/worker_spi.c      | 109 +--------
 doc/src/sgml/monitoring.sgml                  |   5 +-
 doc/src/sgml/xfunc.sgml                       |  26 +--
 src/tools/pgindent/typedefs.list              |   2 +
 10 files changed, 164 insertions(+), 238 deletions(-)

diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index aad8bc08fa..3cffae23e1 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -44,12 +44,14 @@ extern PGDLLIMPORT uint32 *my_wait_event_info;
  * Use this category when the server process is waiting for some condition
  * defined by an extension module.
  *
- * Extensions can define their own wait events in this category.  First,
- * they should call WaitEventExtensionNew() to get one or more wait event
- * IDs that are allocated from a shared counter.  These can be used directly
- * with pgstat_report_wait_start() or equivalent.  Next, each individual
- * process should call WaitEventExtensionRegisterName() to associate a wait
- * event string to the number allocated previously.
+ * Extensions can define their own wait events in this category.  They should
+ * call WaitEventExtensionNew() with a wait event string.  If the wait event
+ * associated the string is already allocated, it returns the wait event info.
+ * If not, it will get one wait event ID that is allocated from a shared
+ + counter, associate the string to the number in the shared dynamic hash and
+ * return the wait event info.
+ *
+ * The ID retrieved can be used with pgstat_report_wait_start() or equivalent.
  */
 typedef enum
 {
@@ -60,9 +62,7 @@ typedef enum
 extern void WaitEventExtensionShmemInit(void);
 extern Size WaitEventExtensionShmemSize(void);
 
-extern uint32 WaitEventExtensionNew(void);
-extern void WaitEventExtensionRegisterName(uint32 wait_event_info,
-										   const char *wait_event_name);
+extern uint32 WaitEventExtensionNew(const char *wait_event_name);
 
 /* ----------
  * pgstat_report_wait_start() -
diff --git a/src/backend/storage/lmgr/lwlocknames.txt b/src/backend/storage/lmgr/lwlocknames.txt
index b34b6afecd..7af3e0ba1a 100644
--- a/src/backend/storage/lmgr/lwlocknames.txt
+++ b/src/backend/storage/lmgr/lwlocknames.txt
@@ -53,3 +53,4 @@ XactTruncationLock					44
 # 45 was XactTruncationLock until removal of BackendRandomLock
 WrapLimitsVacuumLock				46
 NotifyQueueTailLock					47
+WaitEventExtensionLock              48
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index b3596ece80..14750233d4 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -45,6 +45,41 @@ uint32	   *my_wait_event_info = &local_my_wait_event_info;
 #define WAIT_EVENT_CLASS_MASK	0xFF000000
 #define WAIT_EVENT_ID_MASK		0x0000FFFF
 
+/*
+ * Hash tables for storing custom wait event ids and their names in
+ * shared memory.
+ *
+ * WaitEventExtensionHashById is used to find the name from a event id.
+ * Any backend can search it to find custom wait events.
+ *
+ * WaitEventExtensionHashByName is used to find the event ID from a name.
+ * It is used to ensure that no duplicated entries are registered.
+ *
+ * The size of the hash table is based on the assumption that
+ * WAIT_EVENT_EXTENSION_BASH_INIT_SIZE is enough for most cases, and it seems
+ * unlikely that the number of entries will reach
+ * WAIT_EVENT_EXTENSION_BASH_MAX_SIZE.
+ */
+static HTAB *WaitEventExtensionHashById;	/* find names from IDs */
+static HTAB *WaitEventExtensionHashByName;	/* find IDs from names */
+
+#define WAIT_EVENT_EXTENSION_HASH_INIT_SIZE	16
+#define WAIT_EVENT_EXTENSION_HASH_MAX_SIZE	128
+
+/* hash table entries */
+typedef struct WaitEventExtensionEntryById
+{
+	uint16		event_id;		/* hash key */
+	char		wait_event_name[NAMEDATALEN];	/* custom wait event name */
+} WaitEventExtensionEntryById;
+
+typedef struct WaitEventExtensionEntryByName
+{
+	char		wait_event_name[NAMEDATALEN];	/* hash key */
+	uint16		event_id;		/* wait event ID */
+} WaitEventExtensionEntryByName;
+
+
 /* dynamic allocation counter for custom wait events in extensions */
 typedef struct WaitEventExtensionCounterData
 {
@@ -59,36 +94,39 @@ static WaitEventExtensionCounterData *WaitEventExtensionCounter;
 #define NUM_BUILTIN_WAIT_EVENT_EXTENSION	\
 	(WAIT_EVENT_EXTENSION_FIRST_USER_DEFINED - WAIT_EVENT_EXTENSION)
 
-/*
- * This is indexed by event ID minus NUM_BUILTIN_WAIT_EVENT_EXTENSION, and
- * stores the names of all dynamically-created event IDs known to the current
- * process.  Any unused entries in the array will contain NULL.
- */
-static const char **WaitEventExtensionNames = NULL;
-static int	WaitEventExtensionNamesAllocated = 0;
+/* wait event info for extensions */
+#define WAIT_EVENT_EXTENSION_INFO(eventId)	(PG_WAIT_EXTENSION | eventId)
 
 static const char *GetWaitEventExtensionIdentifier(uint16 eventId);
 
 /*
- *  Return the space for dynamic allocation counter.
+ *  Return the space for dynamic shared hash tables and dynamic allocation counter.
  */
 Size
 WaitEventExtensionShmemSize(void)
 {
-	return sizeof(WaitEventExtensionCounterData);
+	Size		sz;
+
+	sz = MAXALIGN(sizeof(WaitEventExtensionCounterData));
+	sz = add_size(sz, hash_estimate_size(WAIT_EVENT_EXTENSION_HASH_MAX_SIZE,
+										 sizeof(WaitEventExtensionEntryById)));
+	sz = add_size(sz, hash_estimate_size(WAIT_EVENT_EXTENSION_HASH_MAX_SIZE,
+										 sizeof(WaitEventExtensionEntryByName)));
+	return sz;
 }
 
 /*
- * Allocate shmem space for dynamic allocation counter.
+ * Allocate shmem space for dynamic shared hash and dynamic allocation counter.
  */
 void
 WaitEventExtensionShmemInit(void)
 {
 	bool		found;
+	HASHCTL		info;
 
 	WaitEventExtensionCounter = (WaitEventExtensionCounterData *)
 		ShmemInitStruct("WaitEventExtensionCounterData",
-						WaitEventExtensionShmemSize(), &found);
+						MAXALIGN(sizeof(WaitEventExtensionCounterData)), &found);
 
 	if (!found)
 	{
@@ -96,21 +134,77 @@ WaitEventExtensionShmemInit(void)
 		WaitEventExtensionCounter->nextId = NUM_BUILTIN_WAIT_EVENT_EXTENSION;
 		SpinLockInit(&WaitEventExtensionCounter->mutex);
 	}
+
+	/* initialize or attach the hash tables to store custom wait events */
+	info.keysize = sizeof(uint16);
+	info.entrysize = sizeof(WaitEventExtensionEntryById);
+	WaitEventExtensionHashById = ShmemInitHash("WaitEventExtension hash by id",
+											   WAIT_EVENT_EXTENSION_HASH_INIT_SIZE,
+											   WAIT_EVENT_EXTENSION_HASH_MAX_SIZE,
+											   &info,
+											   HASH_ELEM | HASH_BLOBS);
+
+	info.keysize = sizeof(char[NAMEDATALEN]);
+	info.entrysize = sizeof(WaitEventExtensionEntryByName);
+	/* key is a NULL-terminated string */
+	WaitEventExtensionHashByName = ShmemInitHash("WaitEventExtension hash by name",
+												 WAIT_EVENT_EXTENSION_HASH_INIT_SIZE,
+												 WAIT_EVENT_EXTENSION_HASH_MAX_SIZE,
+												 &info,
+												 HASH_ELEM | HASH_STRINGS);
 }
 
 /*
- * Allocate a new event ID and return the wait event.
+ * Allocate a new event ID and return the wait event info.
+ *
+ * If the wait event name is already defined, this does not allocate a new
+ * entry; it returns the wait event information associated to the name.
  */
 uint32
-WaitEventExtensionNew(void)
+WaitEventExtensionNew(const char *wait_event_name)
 {
 	uint16		eventId;
+	bool		found;
+	WaitEventExtensionEntryByName *entry_by_name;
+	WaitEventExtensionEntryById *entry_by_id;
 
-	Assert(LWLockHeldByMeInMode(AddinShmemInitLock, LW_EXCLUSIVE));
+	/* Check the limit of the length of the event name */
+	if (strlen(wait_event_name) >= NAMEDATALEN)
+		elog(ERROR,
+			 "cannot use custom wait event string longer than %u characters",
+			 NAMEDATALEN - 1);
 
+	/*
+	 * Check the wait event info associated to the name is already defined.
+	 * Return it if so.
+	 */
+	LWLockAcquire(WaitEventExtensionLock, LW_SHARED);
+	entry_by_name = (WaitEventExtensionEntryByName *)
+		hash_search(WaitEventExtensionHashByName, wait_event_name,
+					HASH_FIND, &found);
+	LWLockRelease(WaitEventExtensionLock);
+	if (found)
+		return WAIT_EVENT_EXTENSION_INFO(entry_by_name->event_id);
+
+	/*
+	 * Allocate and register a new wait event.  Recheck if the event name
+	 * exists, as it could be possible that a concurrent process has inserted
+	 * one with the same name while the lock was previously released.
+	 */
+	LWLockAcquire(WaitEventExtensionLock, LW_EXCLUSIVE);
+	entry_by_name = (WaitEventExtensionEntryByName *)
+		hash_search(WaitEventExtensionHashByName, wait_event_name,
+					HASH_FIND, &found);
+	if (found)
+	{
+		LWLockRelease(WaitEventExtensionLock);
+		return WAIT_EVENT_EXTENSION_INFO(entry_by_name->event_id);
+	}
+
+	/* Allocate a new event Id */
 	SpinLockAcquire(&WaitEventExtensionCounter->mutex);
 
-	if (WaitEventExtensionCounter->nextId > PG_UINT16_MAX)
+	if (WaitEventExtensionCounter->nextId >= WAIT_EVENT_EXTENSION_HASH_MAX_SIZE)
 	{
 		SpinLockRelease(&WaitEventExtensionCounter->mutex);
 		ereport(ERROR,
@@ -122,64 +216,23 @@ WaitEventExtensionNew(void)
 
 	SpinLockRelease(&WaitEventExtensionCounter->mutex);
 
-	return PG_WAIT_EXTENSION | eventId;
-}
+	/* Register the new wait event */
+	entry_by_id = (WaitEventExtensionEntryById *)
+		hash_search(WaitEventExtensionHashById, &eventId,
+					HASH_ENTER, &found);
+	Assert(!found);
+	strlcpy(entry_by_id->wait_event_name, wait_event_name,
+			sizeof(entry_by_id->wait_event_name));
 
-/*
- * Register a dynamic wait event name for extension in the lookup table
- * of the current process.
- *
- * This routine will save a pointer to the wait event name passed as an argument,
- * so the name should be allocated in a backend-lifetime context
- * (shared memory, TopMemoryContext, static constant, or similar).
- *
- * The "wait_event_name" will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-WaitEventExtensionRegisterName(uint32 wait_event_info,
-							   const char *wait_event_name)
-{
-	uint32		classId;
-	uint16		eventId;
+	entry_by_name = (WaitEventExtensionEntryByName *)
+		hash_search(WaitEventExtensionHashByName, wait_event_name,
+					HASH_ENTER, &found);
+	Assert(!found);
+	entry_by_name->event_id = eventId;
 
-	classId = wait_event_info & WAIT_EVENT_CLASS_MASK;
-	eventId = wait_event_info & WAIT_EVENT_ID_MASK;
+	LWLockRelease(WaitEventExtensionLock);
 
-	/* Check the wait event class. */
-	if (classId != PG_WAIT_EXTENSION)
-		ereport(ERROR,
-				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("invalid wait event class %u", classId));
-
-	/* This should only be called for user-defined wait event. */
-	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
-		ereport(ERROR,
-				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				errmsg("invalid wait event ID %u", eventId));
-
-	/* Convert to array index. */
-	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
-
-	/* If necessary, create or enlarge array. */
-	if (eventId >= WaitEventExtensionNamesAllocated)
-	{
-		uint32		newalloc;
-
-		newalloc = pg_nextpower2_32(Max(8, eventId + 1));
-
-		if (WaitEventExtensionNames == NULL)
-			WaitEventExtensionNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			WaitEventExtensionNames =
-				repalloc0_array(WaitEventExtensionNames, const char *,
-								WaitEventExtensionNamesAllocated, newalloc);
-		WaitEventExtensionNamesAllocated = newalloc;
-	}
-
-	WaitEventExtensionNames[eventId] = wait_event_name;
+	return WAIT_EVENT_EXTENSION_INFO(eventId);
 }
 
 /*
@@ -188,23 +241,25 @@ WaitEventExtensionRegisterName(uint32 wait_event_info,
 static const char *
 GetWaitEventExtensionIdentifier(uint16 eventId)
 {
+	bool		found;
+	WaitEventExtensionEntryById *entry;
+
 	/* Built-in event? */
 	if (eventId < NUM_BUILTIN_WAIT_EVENT_EXTENSION)
 		return "Extension";
 
-	/*
-	 * It is a user-defined wait event, so look at WaitEventExtensionNames[].
-	 * However, it is possible that the name has never been registered by
-	 * calling WaitEventExtensionRegisterName() in the current process, in
-	 * which case give up and return "extension".
-	 */
-	eventId -= NUM_BUILTIN_WAIT_EVENT_EXTENSION;
+	/* It is a user-defined wait event, so lookup hash table. */
+	LWLockAcquire(WaitEventExtensionLock, LW_SHARED);
+	entry = (WaitEventExtensionEntryById *)
+		hash_search(WaitEventExtensionHashById, &eventId,
+					HASH_FIND, &found);
+	LWLockRelease(WaitEventExtensionLock);
 
-	if (eventId >= WaitEventExtensionNamesAllocated ||
-		WaitEventExtensionNames[eventId] == NULL)
-		return "extension";
+	if (!entry)
+		elog(ERROR, "could not find custom wait event name for ID %u",
+			 eventId);
 
-	return WaitEventExtensionNames[eventId];
+	return entry->wait_event_name;
 }
 
 
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index fcd9d2c63c..f9e01e33b1 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -317,6 +317,7 @@ WAIT_EVENT_DOCONLY	LogicalRepWorker	"Waiting to read or update the state of logi
 WAIT_EVENT_DOCONLY	XactTruncation	"Waiting to execute <function>pg_xact_status</function> or update the oldest transaction ID available to it."
 WAIT_EVENT_DOCONLY	WrapLimitsVacuum	"Waiting to update limits on transaction id and multixact consumption."
 WAIT_EVENT_DOCONLY	NotifyQueueTail	"Waiting to update limit on <command>NOTIFY</command> message storage."
+WAIT_EVENT_DOCONLY	WaitEventExtension	"Waiting to read or update custom wait events information for extensions."
 
 WAIT_EVENT_DOCONLY	XactBuffer	"Waiting for I/O on a transaction status SLRU buffer."
 WAIT_EVENT_DOCONLY	CommitTsBuffer	"Waiting for I/O on a commit timestamp SLRU buffer."
diff --git a/src/test/modules/worker_spi/t/001_worker_spi.pl b/src/test/modules/worker_spi/t/001_worker_spi.pl
index c3e7f5fbe6..26b8a49bec 100644
--- a/src/test/modules/worker_spi/t/001_worker_spi.pl
+++ b/src/test/modules/worker_spi/t/001_worker_spi.pl
@@ -39,25 +39,11 @@ $node->poll_query_until('postgres',
 $result = $node->safe_psql('postgres', 'SELECT * FROM schema4.counted;');
 is($result, qq(total|1), 'dynamic bgworker correctly consumed tuple data');
 
-# Check the wait event used by the dynamic bgworker.  For a session without
-# the state in shared memory known, the default of "extension" is the value
-# waited on.
+# Check the wait event used by the dynamic bgworker.
 $result = $node->poll_query_until(
 	'postgres',
 	qq[SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
-	'extension');
-is($result, 1, 'dynamic bgworker has reported "extension" as wait event');
-
-# If the shared memory state is loaded (here with worker_spi_init within
-# the same connection as the one querying pg_stat_activity), the wait
-# event is the custom one.
-# The expected result is a special pattern here with a newline coming from the
-# first query where the shared memory state is set.
-$result = $node->poll_query_until(
-	'postgres',
-	qq[SELECT worker_spi_init(); SELECT wait_event FROM pg_stat_activity WHERE backend_type ~ 'worker_spi';],
-	qq[
-worker_spi_main]);
+	qq[worker_spi_main]);
 is($result, 1,
 	'dynamic bgworker has reported "worker_spi_main" as wait event');
 
diff --git a/src/test/modules/worker_spi/worker_spi--1.0.sql b/src/test/modules/worker_spi/worker_spi--1.0.sql
index f13f7e0f98..e9d5b07373 100644
--- a/src/test/modules/worker_spi/worker_spi--1.0.sql
+++ b/src/test/modules/worker_spi/worker_spi--1.0.sql
@@ -7,8 +7,3 @@ CREATE FUNCTION worker_spi_launch(pg_catalog.int4)
 RETURNS pg_catalog.int4 STRICT
 AS 'MODULE_PATHNAME'
 LANGUAGE C;
-
-CREATE FUNCTION worker_spi_init()
-RETURNS VOID STRICT
-AS 'MODULE_PATHNAME'
-LANGUAGE C;
diff --git a/src/test/modules/worker_spi/worker_spi.c b/src/test/modules/worker_spi/worker_spi.c
index c4317351ce..98f8d4194b 100644
--- a/src/test/modules/worker_spi/worker_spi.c
+++ b/src/test/modules/worker_spi/worker_spi.c
@@ -44,33 +44,17 @@
 
 PG_MODULE_MAGIC;
 
-PG_FUNCTION_INFO_V1(worker_spi_init);
 PG_FUNCTION_INFO_V1(worker_spi_launch);
 
 PGDLLEXPORT void worker_spi_main(Datum main_arg) pg_attribute_noreturn();
 
-/* Shared memory state */
-typedef struct worker_spi_state
-{
-	/* the wait event defined during initialization phase */
-	uint32		wait_event;
-} worker_spi_state;
-
-static worker_spi_state *wsstate = NULL;	/* pointer to shared memory */
-
-static shmem_request_hook_type prev_shmem_request_hook = NULL;
-static shmem_request_hook_type prev_shmem_startup_hook = NULL;
-
-static void worker_spi_shmem_request(void);
-static void worker_spi_shmem_startup(void);
-static void worker_spi_shmem_init(void);
-static Size worker_spi_memsize(void);
-
 /* GUC variables */
 static int	worker_spi_naptime = 10;
 static int	worker_spi_total_workers = 2;
 static char *worker_spi_database = NULL;
 
+/* value cached, fetched from shared memory */
+static uint32 worker_spi_wait_event_main = 0;
 
 typedef struct worktable
 {
@@ -78,63 +62,6 @@ typedef struct worktable
 	const char *name;
 } worktable;
 
-static void
-worker_spi_shmem_request(void)
-{
-	if (prev_shmem_request_hook)
-		prev_shmem_request_hook();
-
-	RequestAddinShmemSpace(worker_spi_memsize());
-}
-
-static void
-worker_spi_shmem_startup(void)
-{
-	if (prev_shmem_startup_hook)
-		prev_shmem_startup_hook();
-
-	worker_spi_shmem_init();
-}
-
-static Size
-worker_spi_memsize(void)
-{
-	return MAXALIGN(sizeof(worker_spi_state));
-}
-
-/*
- * Initialize the shared memory state of worker_spi.
- *
- * This routine allocates a new wait event when called the first time.
- * On follow-up calls, the name of the wait event associated with the
- * existing shared memory state is registered.
- */
-static void
-worker_spi_shmem_init(void)
-{
-	bool		found;
-
-	wsstate = NULL;
-
-	/* Create or attach to the shared memory state */
-	LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
-	wsstate = ShmemInitStruct("worker_spi State",
-							  sizeof(worker_spi_state),
-							  &found);
-
-	/* Define a new wait event */
-	if (!found)
-		wsstate->wait_event = WaitEventExtensionNew();
-
-	LWLockRelease(AddinShmemInitLock);
-
-	/*
-	 * Register the wait event in the lookup table of the current process.
-	 */
-	WaitEventExtensionRegisterName(wsstate->wait_event, "worker_spi_main");
-	return;
-}
-
 /*
  * Initialize workspace for a worker process: create the schema if it doesn't
  * already exist.
@@ -224,9 +151,6 @@ worker_spi_main(Datum main_arg)
 	/* We're now ready to receive signals */
 	BackgroundWorkerUnblockSignals();
 
-	/* Create (if necessary) and attach to our shared memory area. */
-	worker_spi_shmem_init();
-
 	/* Connect to our database */
 	BackgroundWorkerInitializeConnection(worker_spi_database, NULL, 0);
 
@@ -268,6 +192,10 @@ worker_spi_main(Datum main_arg)
 	{
 		int			ret;
 
+		/* First time, allocate or get the custom wait event */
+		if (worker_spi_wait_event_main == 0)
+			worker_spi_wait_event_main = WaitEventExtensionNew("worker_spi_main");
+
 		/*
 		 * Background workers mustn't call usleep() or any direct equivalent:
 		 * instead, they may wait on their process latch, which sleeps as
@@ -277,7 +205,7 @@ worker_spi_main(Datum main_arg)
 		(void) WaitLatch(MyLatch,
 						 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
 						 worker_spi_naptime * 1000L,
-						 wsstate->wait_event);
+						 worker_spi_wait_event_main);
 		ResetLatch(MyLatch);
 
 		CHECK_FOR_INTERRUPTS();
@@ -406,11 +334,6 @@ _PG_init(void)
 
 	MarkGUCPrefixReserved("worker_spi");
 
-	prev_shmem_request_hook = shmem_request_hook;
-	shmem_request_hook = worker_spi_shmem_request;
-	prev_shmem_startup_hook = shmem_startup_hook;
-	shmem_startup_hook = worker_spi_shmem_startup;
-
 	/* set up common data for all our workers */
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
@@ -434,21 +357,6 @@ _PG_init(void)
 	}
 }
 
-/*
- * Wrapper to initialize a session with the shared memory state
- * used by this module.  This is a convenience routine to be able to
- * see the custom wait event stored in shared memory without loading
- * through shared_preload_libraries.
- */
-Datum
-worker_spi_init(PG_FUNCTION_ARGS)
-{
-	/* Create (if necessary) and attach to our shared memory area. */
-	worker_spi_shmem_init();
-
-	PG_RETURN_VOID();
-}
-
 /*
  * Dynamically launch an SPI worker.
  */
@@ -461,9 +369,6 @@ worker_spi_launch(PG_FUNCTION_ARGS)
 	BgwHandleStatus status;
 	pid_t		pid;
 
-	/* Create (if necessary) and attach to our shared memory area. */
-	worker_spi_shmem_init();
-
 	memset(&worker, 0, sizeof(worker));
 	worker.bgw_flags = BGWORKER_SHMEM_ACCESS |
 		BGWORKER_BACKEND_DATABASE_CONNECTION;
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index f4fc5d814f..70511a2388 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1121,9 +1121,8 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
      <literal>LWLock</literal> types
      to the list shown in <xref linkend="wait-event-extension-table"/> and
      <xref linkend="wait-event-lwlock-table"/>. In some cases, the name
-     assigned by an extension will not be available in all server processes;
-     so an <literal>Extension</literal> or <literal>LWLock</literal> wait
-     event might be reported as just
+     of <literal>LWLock</literal> assigned by an extension will not be
+     available in all server processes; It might be reported as just
      <quote><literal>extension</literal></quote> rather than the
      extension-assigned name.
     </para>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index d6345a775b..281c178b0e 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3454,33 +3454,15 @@ if (!ptr)
    </sect2>
 
    <sect2 id="xfunc-addin-wait-events">
-    <title>Shared Memory and Custom Wait Events</title>
+    <title>Custom Wait Events</title>
 
     <para>
      Add-ins can define custom wait events under the wait event type
-     <literal>Extension</literal>. The add-in's shared library must be
-     preloaded by specifying it in <literal>shared_preload_libraries</literal>,
-     and register a <literal>shmem_request_hook</literal> and a
-     <literal>shmem_startup_hook</literal> in its
-     <function>_PG_init</function> function.
-     <literal>shmem_request_hook</literal> can request a shared memory size
-     to be later used at startup by calling:
+     <literal>Extension</literal> by calling:
 <programlisting>
-void RequestAddinShmemSpace(int size)
-</programlisting>
-    </para>
-    <para>
-     <literal>shmem_startup_hook</literal> can allocate in shared memory
-     custom wait events by calling while holding the LWLock
-     <function>AddinShmemInitLock</function> to avoid any race conditions:
-<programlisting>
-uint32 WaitEventExtensionNew(void)
-</programlisting>
-     Next, each process needs to associate the wait event allocated previously
-     to a user-facing custom string, which is something done by calling:
-<programlisting>
-void WaitEventExtensionRegisterName(uint32 wait_event_info, const char *wait_event_name)
+uint32 WaitEventExtensionNew(const char *wait_event_name)
 </programlisting>
+     The wait event is associated to a user-facing custom string.
      An example can be found in <filename>src/test/modules/worker_spi</filename>
      in the PostgreSQL source tree.
     </para>
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 66823bc2a7..87feb432a0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2992,6 +2992,8 @@ WaitEventBufferPin
 WaitEventClient
 WaitEventExtension
 WaitEventExtensionCounterData
+WaitEventExtensionEntryById
+WaitEventExtensionEntryByName
 WaitEventIO
 WaitEventIPC
 WaitEventSet
-- 
2.40.1

#72Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#71)
Re: Support to define custom wait events for extensions

On 2023-08-14 08:06, Michael Paquier wrote:

On Thu, Aug 10, 2023 at 05:37:55PM +0900, Michael Paquier wrote:

This looks correct, but perhaps we need to think harder about the
custom event names and define a convention when more of this stuff is
added to the core modules.

Okay, I have put my hands on that, fixing a couple of typos, polishing
a couple of comments, clarifying the docs and applying an indentation.
And here is a v4.

Any thoughts or comments? I'd like to apply that soon, so as we are
able to move on with the wait event catalog and assigning custom wait
events to the other in-core modules.

Thanks! I confirmed the changes, and all tests passed.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#73Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#72)
Re: Support to define custom wait events for extensions

On Mon, Aug 14, 2023 at 12:31:05PM +0900, Masahiro Ikeda wrote:

Thanks! I confirmed the changes, and all tests passed.

Okay, cool. I got some extra time today and applied that, with a few
more tweaks.
--
Michael

#74Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#73)
Re: Support to define custom wait events for extensions

On 2023-08-14 15:28, Michael Paquier wrote:

On Mon, Aug 14, 2023 at 12:31:05PM +0900, Masahiro Ikeda wrote:

Thanks! I confirmed the changes, and all tests passed.

Okay, cool. I got some extra time today and applied that, with a few
more tweaks.

Thanks for applying master branch!

This looks correct, but perhaps we need to think harder about the
custom event names and define a convention when more of this stuff is
added to the core modules.

I checked the source code how many functions use WAIT_EVENT_EXTENSION.
There are 3 contrib modules and a test module use WAIT_EVENT_EXTENSION
and
there are 8 places where it is called as an argument.

* dblink
- dblink_get_conn(): the wait event is set until the connection
establishment succeeded
- dblink_connect(): same as above

* autoprewarm
- autoprewarm_main(): the wait event is set until shutdown request is
received
- autoprewarm_main(): the wait event is set until the next dump time

* postgres_fdw
- connect_pg_server(): the wait event is set until connection
establishment succeeded
- pgfdw_get_result(): the wait event is set until the results are
received
- pgfdw_get_cleanup_result(): same as above except for abort cleanup

* test_sh_mq
- wait_for_workers_to_become_ready(): the wait event is set until the
workers become ready

I'm thinking a name like "contrib name + description summary" would
be nice. The "contrib name" is namespace-like and the "description
summary"
is the same as the name of the waiting event name in core. For example,
"DblinkConnect" for dblink. In the same as the core one, I thought the
name
should be the camel case.

BTW, is it better to discuss this in a new thread because other
developers
might be interested in user-facing wait event names? I also would like
to add
documentation on the wait events for each modules, as they are not
mentioned now.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#75Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#74)
Re: Support to define custom wait events for extensions

On Mon, Aug 14, 2023 at 05:55:42PM +0900, Masahiro Ikeda wrote:

I'm thinking a name like "contrib name + description summary" would
be nice. The "contrib name" is namespace-like and the "description summary"
is the same as the name of the waiting event name in core. For example,
"DblinkConnect" for dblink. In the same as the core one, I thought the name
should be the camel case.

Or you could use something more in line with the other in-core wait
events formatted as camel-case, like DblinkConnect, etc.

BTW, is it better to discuss this in a new thread because other developers
might be interested in user-facing wait event names? I also would like to
add documentation on the wait events for each modules, as they are not mentioned
now.

Saying that, having some documentation on the page of each extension
is mandatory once these can be customized, in my opinion. All that
should be discussed on a new, separate thread, to attract the correct
audience.
--
Michael

#76Masahiro Ikeda
ikedamsh@oss.nttdata.com
In reply to: Michael Paquier (#75)
Re: Support to define custom wait events for extensions

On 2023-08-14 18:26, Michael Paquier wrote:

On Mon, Aug 14, 2023 at 05:55:42PM +0900, Masahiro Ikeda wrote:

I'm thinking a name like "contrib name + description summary" would
be nice. The "contrib name" is namespace-like and the "description
summary"
is the same as the name of the waiting event name in core. For
example,
"DblinkConnect" for dblink. In the same as the core one, I thought the
name
should be the camel case.

Or you could use something more in line with the other in-core wait
events formatted as camel-case, like DblinkConnect, etc.

BTW, is it better to discuss this in a new thread because other
developers
might be interested in user-facing wait event names? I also would like
to
add documentation on the wait events for each modules, as they are not
mentioned
now.

Saying that, having some documentation on the page of each extension
is mandatory once these can be customized, in my opinion. All that
should be discussed on a new, separate thread, to attract the correct
audience.

OK. I'll make a new patch and start a new thread.

Regards,
--
Masahiro Ikeda
NTT DATA CORPORATION

#77Michael Paquier
michael@paquier.xyz
In reply to: Masahiro Ikeda (#76)
Re: Support to define custom wait events for extensions

On Tue, Aug 15, 2023 at 09:14:15AM +0900, Masahiro Ikeda wrote:

OK. I'll make a new patch and start a new thread.

Cool, thanks!
--
Michael