Improve LWLock tranche name visibility across backends

Started by Sami Imseih6 months ago107 messages
#1Sami Imseih
samimseih@gmail.com
1 attachment(s)

Hi,

This is a follow-up to a discussion started in [0]/messages/by-id/aEiTzmndOVPmA6Mm@nathan.

LWLocks in PostgreSQL are categorized into tranches, and the tranche name
appears as the wait_event in pg_stat_activity. There are both built-in
tranche names and tranche names that can be registered by extensions using
RequestNamedLWLockTranche() or LWLockRegisterTranche().

Tranche names are stored in process-local memory when registered. If a
tranche is registered during postmaster startup, such as with built-in
tranches or those registered via RequestNamedLWLockTranche(), its name is
inherited by backend processes via fork(). However, if a tranche is
registered dynamically by a backend using LWLockRegisterTranche(), other
backends will not be aware of it unless they explicitly register it as well.

Consider a case in which an extension allows a backend to attach a new
dshash via the GetNamedDSHash API and supplies a tranche name like
"MyUsefulExtension". The first backend to call GetNamedDSHash will
initialize an LWLock using the extension-defined tranche name and associate
it with a tranche ID in local memory. Other backends that later attach to
the same dshash will also learn about the tranche name and ID. Backends
that do not attach the dshash will not know this tranche name. This
results in differences in how wait events are reported in pg_stat_activity.

When querying pg_stat_activity, the function pgstat_get_wait_event is
called, which internally uses GetLWLockIdentifier and GetLWTrancheName
to map the LWLock to its tranche name. If the backend does not recognize
the tranche ID, a fallback name "extension" is used. Therefore, backends
that have registered the tranche will report the correct extension-defined
tranche name, while others will report the generic fallback of "extension".

i.e.
````
postgres=# select wait_event, wait_event_type from pg_stat_activity;
-[ RECORD 1 ]---+--------------------
wait_event | extension
wait_event_type | LWLock
```
instead of
```
postgres=# select wait_event, wait_event_type from pg_stat_activity;
-[ RECORD 1 ]---+--------------------
wait_event | MyUsefulExtension
wait_event_type | LWLock
```

This is the current design, but I think we can do better to avoid inconsitencies
this my lead for monitoring tools and diagnostics.

To improve this, we could store tranche names registered by a normal backend
in shared memory, for example in a dshash, allowing tranche names to be
resolved even by backends that have not explicitly registered them. This
would lead to more consistent behavior, particularly as more extensions
adopt APIs like GetNamedDSHash, where tranche names are registered by the
backend rather than the postmaster.

Attached is a proof of concept that does not alter the
LWLockRegisterTranche API. Instead, it detects when a registration is
performed by a normal backend and stores the tranche name in shared memory,
using a dshash keyed by tranche ID. Tranche name lookup now proceeds in
the order of built-in names, the local list, and finally the shared memory.
The fallback name "extension" can still be returned if an extension does
not register a tranche.

An exclusive lock is taken when adding a new tranche, which should be a rare
occurrence. A shared lock is taken when looking up a tranche name via
GetLWTrancheName.

There are still some open questions I have:

1/ There is currently no mechanism for deleting entries. I am not sure whether
this is a concern, since the size of the table would grow only with the
number of extensions and the number of LWLocks they initialize, which is
typically small. That said, others may have different thoughts on this.

2/ What is the appropriate size limit for a tranche name. The work done
in [0]/messages/by-id/aEiTzmndOVPmA6Mm@nathan caps the tranche name to 128 bytes for the dshash tranche, and
128 bytes + length of " DSA" suffix for the dsa tranche. Also, the
existing RequestNamedLWLockTranche caps the name to NAMEDATALEN. Currently,
LWLockRegisterTranche does not have a limit on the tranche name. I wonder
if we also need to take care of this and implement some common limit that
applies to tranch names regardless of how they're created?

[0]: /messages/by-id/aEiTzmndOVPmA6Mm@nathan

--

Sami Imseih
Amazon Web Services (AWS)

Attachments:

0001-Improve-LWLock-tranche-name-visibility-across-backen.patchapplication/octet-stream; name=0001-Improve-LWLock-tranche-name-visibility-across-backen.patchDownload
From ba4a22746478dceff955943f8e0c10361c86a0b4 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 8 Jul 2025 20:36:47 -0500
Subject: [PATCH 1/1] Improve LWLock tranche name visibility across backends

LWLocks in PostgreSQL are grouped into tranches, with tranche names shown
as the wait_event in pg_stat_activity. Built-in tranche names and those
registered during postmaster startup are inherited by backend processes
via fork(). However, tranche names registered by individual backends using
LWLockRegisterTranche() are stored only in local memory and unknown to
other backends.

This causes inconsistent wait event reporting in multi-backend scenarios,
especially when extensions dynamically register tranche names via APIs
like GetNamedDSHash. Backends unaware of these tranche names fallback to
reporting a generic "extension" name.

This patch improves this situation by storing tranche names registered by
normal backends in shared memory (using a dynamic shared hash table keyed
by tranche ID). Lookup of tranche names now proceeds through built-in names,
local lists, and finally shared memory, improving consistency of wait event
reporting across all backends.

The implementation  preserves the existing LWLockRegisterTranche() API and
maintains the fallback to "extension" if the extension never registers
a tranche name.

There is no removal mechanism for tranche entries in shared memory;
this is deemed acceptable given the typically small number of extensions and
LWLocks involved.
---
 src/backend/storage/ipc/ipci.c                |   6 +
 src/backend/storage/lmgr/lwlock.c             | 191 +++++++++++++++---
 .../utils/activity/wait_event_names.txt       |   1 +
 src/include/storage/lwlock.h                  |  15 ++
 src/include/storage/lwlocklist.h              |   1 +
 src/tools/pgindent/typedefs.list              |   1 +
 6 files changed, 190 insertions(+), 25 deletions(-)

diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..d99224324db 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -150,6 +150,7 @@ CalculateShmemSize(int *num_semaphores)
 	size = add_size(size, InjectionPointShmemSize());
 	size = add_size(size, SlotSyncShmemSize());
 	size = add_size(size, AioShmemSize());
+	size = add_size(size, LWLockTrancheShmemSize());
 
 	/* include additional requested shmem from preload libraries */
 	size = add_size(size, total_addin_request);
@@ -343,6 +344,11 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+
+	/*
+	 * LWLock Named Tranches
+	 */
+	LWLockTrancheShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 46f44bc4511..64291e69e0f 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -127,8 +127,10 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * appear in BuiltinTrancheNames[] below.
  *
  * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * or LWLockRegisterTranche. Tranche names registered during postmaster startup
+ * are inherited by backends at fork time and stored in LWLockTrancheNames[].
+ * Tranche names registered by a normal backend are tracked in the dshash table
+ * lwlock_tranche_names_dsh, ensuring they are visible to all backends.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -224,6 +226,28 @@ typedef struct NamedLWLockTrancheRequest
 	int			num_lwlocks;
 } NamedLWLockTrancheRequest;
 
+/* struct representing stated for shared named tranches */
+typedef struct LWLockTrancheNamesShared
+{
+	dsa_handle	lwlock_tranche_names_dsa;
+	dshash_table_handle lwlock_tranche_names_dsh;
+
+} LWLockTrancheNamesShared;
+
+/* parameters for shared tranches hash table */
+static const dshash_parameters dsh_params = {
+	sizeof(int),
+	sizeof(LWLockTracheNamesEntry),
+	dshash_memcmp,
+	dshash_memhash,
+	dshash_memcpy,
+	LWTRANCHE_FIRST_USER_DEFINED
+};
+
+static LWLockTrancheNamesShared *NamedLWLockTrancheShared;
+static dsa_area *lwlock_tranche_names_dsa = NULL;
+static dshash_table *lwlock_tranche_names_dsh = NULL;
+
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
 static int	NamedLWLockTrancheRequestsAllocated = 0;
 
@@ -241,7 +265,8 @@ NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
-static const char *GetLWTrancheName(uint16 trancheId);
+static const char *GetLWTrancheName(int trancheId);
+static void LWLockAttachNamedTranches(void);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -627,7 +652,10 @@ LWLockNewTrancheId(void)
 }
 
 /*
- * Register a dynamic tranche name in the lookup table of the current process.
+ * Register a dynamic tranche name in the lookup table of the current process
+ * or in shared memory. The former is used if the tranche is registered during
+ * postmaster startup. The latter is used when the tranche is registered by
+ * a normal backend.
  *
  * This routine will save a pointer to the tranche name passed as an argument,
  * so the name should be allocated in a backend-lifetime context
@@ -646,24 +674,48 @@ LWLockRegisterTranche(int tranche_id, const char *tranche_name)
 	/* Convert to array index. */
 	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (!IsUnderPostmaster)
 	{
-		int			newalloc;
+		/* If necessary, create or enlarge array. */
+		if (tranche_id >= LWLockTrancheNamesAllocated)
+		{
+			int			newalloc;
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+			newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+			if (LWLockTrancheNames == NULL)
+				LWLockTrancheNames = (const char **)
+					MemoryContextAllocZero(TopMemoryContext,
+										   newalloc * sizeof(char *));
+			else
+				LWLockTrancheNames =
+					repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
+			LWLockTrancheNamesAllocated = newalloc;
+		}
+
+		LWLockTrancheNames[tranche_id] = tranche_name;
 	}
+	else
+	{
+		bool		found;
+		LWLockTracheNamesEntry *entry;
+
+		if (strlen(tranche_name) + 1 > LWLOCK_SHARED_TRANCHE_NAME_LEN)
+			ereport(ERROR,
+					(errmsg("Tranche name too long")));
+
+		LWLockAttachNamedTranches();
+
+		entry = dshash_find_or_insert(lwlock_tranche_names_dsh, &tranche_id, &found);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+		if (entry)
+		{
+			if (!found)
+				strcpy(entry->trancheName, tranche_name);
+
+			dshash_release_lock(lwlock_tranche_names_dsh, entry);
+		}
+	}
 }
 
 /*
@@ -752,24 +804,44 @@ LWLockReportWaitEnd(void)
  * Return the name of an LWLock tranche.
  */
 static const char *
-GetLWTrancheName(uint16 trancheId)
+GetLWTrancheName(int trancheId)
 {
+	LWLockTracheNamesEntry *entry;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in LWLockTrancheNames[] first, and
+	 * then lwlock_tranche_names_dsh if the tranche was registered by a normal
+	 * backend.
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (LWLockTrancheNames[trancheId])
+		return LWLockTrancheNames[trancheId];
+
+	LWLockAttachNamedTranches();
 
-	return LWLockTrancheNames[trancheId];
+	entry = dshash_find(lwlock_tranche_names_dsh, &trancheId, false);
+
+	if (entry)
+	{
+		char	   *name = (char *) palloc(LWLOCK_SHARED_TRANCHE_NAME_LEN);
+
+		Assert(strlen(entry->trancheName) + 1 <= LWLOCK_SHARED_TRANCHE_NAME_LEN);
+		strcpy(name, entry->trancheName);
+		dshash_release_lock(lwlock_tranche_names_dsh, entry);
+
+		return name;
+	}
+
+	/*
+	 * The extension did not register a tranche, so let's return the fallback
+	 * name.
+	 */
+	return "extension";
 }
 
 /*
@@ -2035,3 +2107,72 @@ LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode)
 	}
 	return false;
 }
+
+Size
+LWLockTrancheShmemSize(void)
+{
+	Size		size = 0;
+
+	size = sizeof(LWLockTrancheNamesShared);
+
+	return size;
+}
+
+/* Allocate and initialize LWLock tranche names shared memory */
+void
+LWLockTrancheShmemInit(void)
+{
+	bool		found;
+
+	NamedLWLockTrancheShared = (LWLockTrancheNamesShared *)
+		ShmemInitStruct("LWLock Tranches", LWLockTrancheShmemSize(), &found);
+
+	if (!found)
+	{
+		/* First time through, so initialize */
+		MemSet(NamedLWLockTrancheShared, 0, LWLockTrancheShmemSize());
+		NamedLWLockTrancheShared->lwlock_tranche_names_dsa = DSA_HANDLE_INVALID;
+		NamedLWLockTrancheShared->lwlock_tranche_names_dsh = DSHASH_HANDLE_INVALID;
+	}
+}
+
+void
+LWLockAttachNamedTranches(void)
+{
+	MemoryContext oldcontext;
+
+	/* Quick exit if we already did this. */
+	if (NamedLWLockTrancheShared->lwlock_tranche_names_dsh != DSHASH_HANDLE_INVALID &&
+		lwlock_tranche_names_dsh != NULL)
+		return;
+
+	/* Otherwise, use a lock to ensure only one process creates the table. */
+	LWLockAcquire(LWLockNamedTranchesLock, LW_EXCLUSIVE);
+
+	/* Be sure any local memory allocated by DSA routines is persistent. */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	if (NamedLWLockTrancheShared->lwlock_tranche_names_dsh == DSHASH_HANDLE_INVALID)
+	{
+		/* Initialize dynamic shared hash table for last-start times. */
+		lwlock_tranche_names_dsa = dsa_create(LWTRANCHE_LAUNCHER_DSA);
+		dsa_pin(lwlock_tranche_names_dsa);
+		dsa_pin_mapping(lwlock_tranche_names_dsa);
+		lwlock_tranche_names_dsh = dshash_create(lwlock_tranche_names_dsa, &dsh_params, NULL);
+
+		/* Store handles in shared memory for other backends to use. */
+		NamedLWLockTrancheShared->lwlock_tranche_names_dsa = dsa_get_handle(lwlock_tranche_names_dsa);
+		NamedLWLockTrancheShared->lwlock_tranche_names_dsh = dshash_get_hash_table_handle(lwlock_tranche_names_dsh);
+	}
+	else if (!lwlock_tranche_names_dsh)
+	{
+		/* Attach to existing dynamic shared hash table. */
+		lwlock_tranche_names_dsa = dsa_attach(NamedLWLockTrancheShared->lwlock_tranche_names_dsa);
+		dsa_pin_mapping(lwlock_tranche_names_dsa);
+		lwlock_tranche_names_dsh = dshash_attach(lwlock_tranche_names_dsa, &dsh_params,
+												 NamedLWLockTrancheShared->lwlock_tranche_names_dsh, NULL);
+	}
+
+	MemoryContextSwitchTo(oldcontext);
+	LWLockRelease(LWLockNamedTranchesLock);
+}
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4da68312b5f..620cc1f8c97 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -352,6 +352,7 @@ DSMRegistry	"Waiting to read or update the dynamic shared memory registry."
 InjectionPoint	"Waiting to read or update information related to injection points."
 SerialControl	"Waiting to read or update shared <filename>pg_serial</filename> state."
 AioWorkerSubmissionQueue	"Waiting to access AIO worker submission queue."
+LWLockNamedTranches	"Waiting to read or update shared lwlock named tranches state."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 08a72569ae5..46ca48b5755 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -18,6 +18,7 @@
 #error "lwlock.h may not be included from frontend code"
 #endif
 
+#include "lib/dshash.h"
 #include "port/atomics.h"
 #include "storage/lwlocknames.h"
 #include "storage/proclist_types.h"
@@ -80,6 +81,16 @@ typedef struct NamedLWLockTranche
 	char	   *trancheName;
 } NamedLWLockTranche;
 
+/* length of a lwlock shared tranche name */
+#define LWLOCK_SHARED_TRANCHE_NAME_LEN  256
+
+/* struct for storing trache information in shared memory */
+typedef struct LWLockTracheNamesEntry
+{
+	int			trancheId;
+	char		trancheName[LWLOCK_SHARED_TRANCHE_NAME_LEN];
+}			LWLockTracheNamesEntry;
+
 extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
@@ -172,6 +183,10 @@ extern int	LWLockNewTrancheId(void);
 extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
+/* Creates the shared memory state for tracking tranche names */
+extern Size LWLockTrancheShmemSize(void);
+extern void LWLockTrancheShmemInit(void);
+
 /*
  * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
  * we reserve additional tranche IDs for builtin tranches not included in
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index a9681738146..029bed98988 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -84,3 +84,4 @@ PG_LWLOCK(50, DSMRegistry)
 PG_LWLOCK(51, InjectionPoint)
 PG_LWLOCK(52, SerialControl)
 PG_LWLOCK(53, AioWorkerSubmissionQueue)
+PG_LWLOCK(54, LWLockNamedTranches)
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 83192038571..c65fa454104 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4343,3 +4343,4 @@ yyscan_t
 z_stream
 z_streamp
 zic_t
+LWLockTrancheNamesShared
-- 
2.43.0

#2Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#1)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Wed, Jul 09, 2025 at 04:39:48PM -0500, Sami Imseih wrote:

Hi,

When querying pg_stat_activity, the function pgstat_get_wait_event is
called, which internally uses GetLWLockIdentifier and GetLWTrancheName
to map the LWLock to its tranche name. If the backend does not recognize
the tranche ID, a fallback name "extension" is used. Therefore, backends
that have registered the tranche will report the correct extension-defined
tranche name, while others will report the generic fallback of "extension".

i.e.
````
postgres=# select wait_event, wait_event_type from pg_stat_activity;
-[ RECORD 1 ]---+--------------------
wait_event | extension
wait_event_type | LWLock
```
instead of
```
postgres=# select wait_event, wait_event_type from pg_stat_activity;
-[ RECORD 1 ]---+--------------------
wait_event | MyUsefulExtension
wait_event_type | LWLock
```

This is the current design, but I think we can do better to avoid inconsitencies
this my lead for monitoring tools and diagnostics.

+1 on finding a way to improve this, thanks for looking at it.

Attached is a proof of concept that does not alter the
LWLockRegisterTranche API. Instead, it detects when a registration is
performed by a normal backend and stores the tranche name in shared memory,
using a dshash keyed by tranche ID. Tranche name lookup now proceeds in
the order of built-in names, the local list, and finally the shared memory.
The fallback name "extension" can still be returned if an extension does
not register a tranche.

I did not look in details, but do you think we could make use of
WaitEventCustomNew()?

Regards,

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

#3Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#2)
Re: Improve LWLock tranche name visibility across backends

Thanks for the feedback!

Attached is a proof of concept that does not alter the
LWLockRegisterTranche API. Instead, it detects when a registration is
performed by a normal backend and stores the tranche name in shared memory,
using a dshash keyed by tranche ID. Tranche name lookup now proceeds in
the order of built-in names, the local list, and finally the shared memory.
The fallback name "extension" can still be returned if an extension does
not register a tranche.

I did not look in details, but do you think we could make use of
WaitEventCustomNew()?

It looks like I overlooked the custom wait event, so I didn’t take it into
account initially. That said, I do think it’s reasonable to consider
piggybacking on this infrastructure.

After all, LWLockRegisterTranche is already creating a custom wait event
defined by the extension. The advantage here is that we can avoid creating
new shared memory and instead reuse the existing static hash table, which is
capped at 128 custom wait events:

```
#define WAIT_EVENT_CUSTOM_HASH_MAX_SIZE 128
```

However, WaitEventCustomNew as it currently stands won’t work for our use
case, since it assigns an eventId automatically. The API currently takes a
classId and wait_event_name, but in our case, we’d actually want to pass in a
trancheId.

So, we might need a new API, something like:
```
WaitEventCustomNewWithEventId(uint32 classId, uint16 eventId,
const char *wait_event_name);
```
eventId in the LWLock case will be a tracheId that was generated
by the user in some earlier step, like LWLockInitialize

This would behave the same as the existing WaitEventCustomNew API,
except that it uses the provided eventId.

or maybe we can just allow WaitEventCustomNew to take in the eventId, and
if it's > 0, then use the passed in value, otherwise generate the next eventId.

I do like the latter approach more, what do you think?

With this API, we can then teach LWLockRegisterTranche to register the
custom wait event.

--
Sami

#4Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#3)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Thu, Jul 10, 2025 at 04:34:34PM -0500, Sami Imseih wrote:

Thanks for the feedback!

Attached is a proof of concept that does not alter the
LWLockRegisterTranche API. Instead, it detects when a registration is
performed by a normal backend and stores the tranche name in shared memory,
using a dshash keyed by tranche ID. Tranche name lookup now proceeds in
the order of built-in names, the local list, and finally the shared memory.
The fallback name "extension" can still be returned if an extension does
not register a tranche.

I did not look in details, but do you think we could make use of
WaitEventCustomNew()?

It looks like I overlooked the custom wait event, so I didn’t take it into
account initially. That said, I do think it’s reasonable to consider
piggybacking on this infrastructure.

After all, LWLockRegisterTranche is already creating a custom wait event
defined by the extension.

Right, the tranche is nothing but an eventID (from a wait_event_info point of
view).

The advantage here is that we can avoid creating
new shared memory

Right, I think it's good to rely on this existing machinery.

and instead reuse the existing static hash table, which is
capped at 128 custom wait events:

```
#define WAIT_EVENT_CUSTOM_HASH_MAX_SIZE 128
```

That's probably still high enough, thoughts?

However, WaitEventCustomNew as it currently stands won’t work for our use
case, since it assigns an eventId automatically. The API currently takes a
classId and wait_event_name, but in our case, we’d actually want to pass in a
trancheId.

So, we might need a new API, something like:
```
WaitEventCustomNewWithEventId(uint32 classId, uint16 eventId,
const char *wait_event_name);
```
eventId in the LWLock case will be a tracheId that was generated
by the user in some earlier step, like LWLockInitialize

This would behave the same as the existing WaitEventCustomNew API,
except that it uses the provided eventId.

or maybe we can just allow WaitEventCustomNew to take in the eventId, and
if it's > 0, then use the passed in value, otherwise generate the next eventId.

I do like the latter approach more, what do you think?

I think I do prefer it too, but in both cases we'll have to make sure there
is no collision on the eventID (LWTRANCHE_FIRST_USER_DEFINED is currently
95).

Regards,

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

#5Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#1)
Re: Improve LWLock tranche name visibility across backends

On Wed, Jul 09, 2025 at 04:39:48PM -0500, Sami Imseih wrote:

Attached is a proof of concept that does not alter the
LWLockRegisterTranche API.

IMHO we should consider modifying the API, because right now you have to
call LWLockRegisterTranche() in each backend. Why not accept the name as
an argument for LWLockNewTrancheId() and only require it to be called once
during your shared memory initialization? In any case, a lot of existing
code will continue to call it in each backend unless the API changes.

Instead, it detects when a registration is
performed by a normal backend and stores the tranche name in shared memory,
using a dshash keyed by tranche ID. Tranche name lookup now proceeds in
the order of built-in names, the local list, and finally the shared memory.
The fallback name "extension" can still be returned if an extension does
not register a tranche.

Why do we need three different places for the lock names? Is there a
reason we can't put it all in shared memory?

1/ There is currently no mechanism for deleting entries. I am not sure whether
this is a concern, since the size of the table would grow only with the
number of extensions and the number of LWLocks they initialize, which is
typically small. That said, others may have different thoughts on this.

I don't see any strong reason to allow deletion unless we started to
reclaim tranche IDs, and I don't see any strong reason for that, either.

2/ What is the appropriate size limit for a tranche name. The work done
in [0] caps the tranche name to 128 bytes for the dshash tranche, and
128 bytes + length of " DSA" suffix for the dsa tranche. Also, the
existing RequestNamedLWLockTranche caps the name to NAMEDATALEN. Currently,
LWLockRegisterTranche does not have a limit on the tranche name. I wonder
if we also need to take care of this and implement some common limit that
applies to tranch names regardless of how they're created?

Do we need to set a limit? If we're using a DSA and dshash, we could let
folks use arbitrary long tranche names, right? The reason for the limit in
the DSM registry is because the name is used as the key for the dshash
table.

--
nathan

#6Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#4)
1 attachment(s)
Re: Improve LWLock tranche name visibility across backends

and instead reuse the existing static hash table, which is
capped at 128 custom wait events:

```
#define WAIT_EVENT_CUSTOM_HASH_MAX_SIZE 128
```

That's probably still high enough, thoughts?

I have no reason to believe that this number could be too low.
I am not aware of an extension that will initialize more than a
couple of LWLocks.

or maybe we can just allow WaitEventCustomNew to take in the eventId, and
if it's > 0, then use the passed in value, otherwise generate the next eventId.

I do like the latter approach more, what do you think?

I think I do prefer it too, but in both cases we'll have to make sure there
is no collision on the eventID (LWTRANCHE_FIRST_USER_DEFINED is currently
95).

As far as collisions are concerned, the key of the hash is the wait_event_info,
which is a bitwise OR of classId and eventId
```
wait_event_info = classId | eventId;
```
Do you think collision can still be possible?

Now, what I think will be a good API is to provide an alternative to
LWLockRegisterTranche,
which now takes in both a tranche ID and tranche_name. The new API I propose is
LWLockRegisterTrancheWaitEventCustom which takes only a tranche_name
and internally
calls WaitEventCustomNew to add a new wait_event_info to the hash
table. The wait_event_info
is made up of classId = PG_WAIT_LWLOCK and LWLockNewTrancheId().

I prefer we implement a new API for this to make it explicit that this
API will both register
a tranche and create a custom wait event. I do not think we should get
rid of LWLockRegisterTranche
because it is used by CreateLWLocks during startup and I don't see a
reason to change that.
See the attached v1.

What is missing from the attached v1 is:

1/ the documentation changes required in [0]https://www.postgresql.org/docs/current/xfunc-c.html.

2/ in dsm_registry.c, we are going to have to modify GetNamedDSHash and
GetNamedDSA to use the new API. Here is an example of how that looks like

```
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const
dshash_parameters *params, bool *found)
entry->type = DSMR_ENTRY_TYPE_DSH;

                /* Initialize the LWLock tranche for the DSA. */
-               dsa_state->tranche = LWLockNewTrancheId();
                sprintf(dsa_state->tranche_name, "%s%s", name,
DSMR_DSA_TRANCHE_SUFFIX);
-               LWLockRegisterTranche(dsa_state->tranche,
dsa_state->tranche_name);
+               dsa_state->tranche =
LWLockRegisterTrancheWaitEventCustom(dsa_state->tranche_name);
 ```

Is there a concern with a custom wait event to be created implicitly
via the GetNamed* APIs?

3/ I still did not workout the max length requirement of the tranche
name, but I think
it would have to be as large as the GetNamed* APIs from the dsm
registry support.

I hope with v1, we can agree to the general direction of this change.

[0]: https://www.postgresql.org/docs/current/xfunc-c.html

--
Sami

Attachments:

v1-0001-Create-LWLock-tranche-in-shared-memory.patchapplication/octet-stream; name=v1-0001-Create-LWLock-tranche-in-shared-memory.patchDownload
From 5fc6bc07f2f870f590bd86ac2924aac6a15cd317 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 11 Jul 2025 13:51:00 -0500
Subject: [PATCH v1 1/1] Create LWLock tranche in shared memory

---
 src/backend/storage/lmgr/lwlock.c       | 37 +++++++++++++++++-----
 src/backend/utils/activity/wait_event.c | 41 ++++++++++++++++++-------
 src/include/storage/lwlock.h            |  1 +
 src/include/utils/wait_event.h          |  5 +++
 4 files changed, 66 insertions(+), 18 deletions(-)

diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 46f44bc4511..eeb3c4501c1 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -130,6 +130,12 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * or LWLockRegisterTranche.  The names of these that are known in the current
  * process appear in LWLockTrancheNames[].
  *
+ * 4. Extensions can also create a new tranche and define a custom wait event
+ * using LWLockRegisterTrancheWaitEventCustom. This function takes only a
+ * tranche name and returns a trancheId to the caller. Unlike
+ * RequestNamedLWLockTranche or LWLockRegisterTranche, the wait events are
+ * stored in shared memory and are visible to all backends.
+ *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
  */
@@ -666,6 +672,12 @@ LWLockRegisterTranche(int tranche_id, const char *tranche_name)
 	LWLockTrancheNames[tranche_id] = tranche_name;
 }
 
+uint16
+LWLockRegisterTrancheWaitEventCustom(const char *tranche_name)
+{
+	return WaitEventLWLockNew(tranche_name) & WAIT_EVENT_ID_MASK;
+}
+
 /*
  * RequestNamedLWLockTranche
  *		Request that extra LWLocks be allocated during postmaster
@@ -754,22 +766,33 @@ LWLockReportWaitEnd(void)
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	const char *wait_event_name = NULL;
+	uint32		wait_event_info;
+	uint16		trancheIdSaved = trancheId;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so first look in LWLockTrancheNames[] and
+	 * then in custom wait events, in that order, ensuring shared memory is
+	 * only checked if necessary. However, it is possible that the tranche has
+	 * never been registered in the current process; in that case, give up and
+	 * return "extension".
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (trancheId < LWLockTrancheNamesAllocated &&
+		LWLockTrancheNames[trancheId] != NULL)
+		return LWLockTrancheNames[trancheId];
+
+	wait_event_info = PG_WAIT_LWLOCK | trancheIdSaved;
+	wait_event_name = GetWaitEventCustomIdentifier(wait_event_info, false);
+	if (wait_event_name)
+		return wait_event_name;
 
-	return LWLockTrancheNames[trancheId];
+	return "extension";
 }
 
 /*
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d9b8f34a355..5e0bda8714a 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -39,9 +39,6 @@ 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_CLASS_MASK	0xFF000000
-#define WAIT_EVENT_ID_MASK		0x0000FFFF
-
 /*
  * Hash tables for storing custom wait event ids and their names in
  * shared memory.
@@ -94,7 +91,6 @@ static WaitEventCustomCounterData *WaitEventCustomCounter;
 #define WAIT_EVENT_CUSTOM_INITIAL_ID	1
 
 static uint32 WaitEventCustomNew(uint32 classId, const char *wait_event_name);
-static const char *GetWaitEventCustomIdentifier(uint32 wait_event_info);
 
 /*
  *  Return the space for dynamic shared hash tables and dynamic allocation counter.
@@ -171,6 +167,12 @@ WaitEventInjectionPointNew(const char *wait_event_name)
 	return WaitEventCustomNew(PG_WAIT_INJECTIONPOINT, wait_event_name);
 }
 
+uint32
+WaitEventLWLockNew(const char *wait_event_name)
+{
+	return WaitEventCustomNew(PG_WAIT_LWLOCK, wait_event_name);
+}
+
 static uint32
 WaitEventCustomNew(uint32 classId, const char *wait_event_name)
 {
@@ -245,7 +247,19 @@ WaitEventCustomNew(uint32 classId, const char *wait_event_name)
 				errmsg("too many custom wait events"));
 	}
 
-	eventId = WaitEventCustomCounter->nextId++;
+	/*
+	 * If classId refers to an LWLock, the corresponding eventId should be a
+	 * trancheId derived from the global LWLockCounter. In this case, we
+	 * increment the WaitEventCustomCounter for tracking and assign the next
+	 * trancheId using LWLockNewTrancheId().
+	 */
+	if (classId == PG_WAIT_LWLOCK)
+	{
+		WaitEventCustomCounter->nextId++;
+		eventId = LWLockNewTrancheId();
+	}
+	else
+		eventId = WaitEventCustomCounter->nextId++;
 
 	SpinLockRelease(&WaitEventCustomCounter->mutex);
 
@@ -272,8 +286,8 @@ WaitEventCustomNew(uint32 classId, const char *wait_event_name)
 /*
  * Return the name of a custom wait event information.
  */
-static const char *
-GetWaitEventCustomIdentifier(uint32 wait_event_info)
+const char *
+GetWaitEventCustomIdentifier(uint32 wait_event_info, bool error_if_not_found)
 {
 	bool		found;
 	WaitEventCustomEntryByInfo *entry;
@@ -290,9 +304,14 @@ GetWaitEventCustomIdentifier(uint32 wait_event_info)
 	LWLockRelease(WaitEventCustomLock);
 
 	if (!entry)
-		elog(ERROR,
-			 "could not find custom name for wait event information %u",
-			 wait_event_info);
+	{
+		if (error_if_not_found)
+			elog(ERROR,
+				 "could not find custom name for wait event information %u",
+				 wait_event_info);
+		else
+			return NULL;
+	}
 
 	return entry->wait_event_name;
 }
@@ -451,7 +470,7 @@ pgstat_get_wait_event(uint32 wait_event_info)
 			break;
 		case PG_WAIT_EXTENSION:
 		case PG_WAIT_INJECTIONPOINT:
-			event_name = GetWaitEventCustomIdentifier(wait_event_info);
+			event_name = GetWaitEventCustomIdentifier(wait_event_info, true);
 			break;
 		case PG_WAIT_BUFFERPIN:
 			{
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 08a72569ae5..26067568be6 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -170,6 +170,7 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
  */
 extern int	LWLockNewTrancheId(void);
 extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern uint16 LWLockRegisterTrancheWaitEventCustom(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
 /*
diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h
index f5815b4994a..e1205cd9375 100644
--- a/src/include/utils/wait_event.h
+++ b/src/include/utils/wait_event.h
@@ -22,6 +22,8 @@ extern void pgstat_reset_wait_event_storage(void);
 
 extern PGDLLIMPORT uint32 *my_wait_event_info;
 
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+#define WAIT_EVENT_ID_MASK		0x0000FFFF
 
 /*
  * Wait Events - Extension, InjectionPoint
@@ -41,10 +43,13 @@ extern PGDLLIMPORT uint32 *my_wait_event_info;
  */
 extern uint32 WaitEventExtensionNew(const char *wait_event_name);
 extern uint32 WaitEventInjectionPointNew(const char *wait_event_name);
+extern uint32 WaitEventLWLockNew(const char *wait_event_name);
 
 extern void WaitEventCustomShmemInit(void);
 extern Size WaitEventCustomShmemSize(void);
 extern char **GetWaitEventCustomNames(uint32 classId, int *nwaitevents);
+extern const char *GetWaitEventCustomIdentifier(uint32 wait_event_info,
+												bool error_if_not_found);
 
 /* ----------
  * pgstat_report_wait_start() -
-- 
2.39.5 (Apple Git-154)

#7Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#6)
Re: Improve LWLock tranche name visibility across backends

On Fri, Jul 11, 2025 at 04:32:13PM -0500, Sami Imseih wrote:

Now, what I think will be a good API is to provide an alternative to
LWLockRegisterTranche,
which now takes in both a tranche ID and tranche_name. The new API I propose is
LWLockRegisterTrancheWaitEventCustom which takes only a tranche_name
and internally
calls WaitEventCustomNew to add a new wait_event_info to the hash
table. The wait_event_info
is made up of classId = PG_WAIT_LWLOCK and LWLockNewTrancheId().

I prefer we implement a new API for this to make it explicit that this
API will both register
a tranche and create a custom wait event. I do not think we should get
rid of LWLockRegisterTranche
because it is used by CreateLWLocks during startup and I don't see a
reason to change that.
See the attached v1.

Hm. I was thinking we could have LWLockNewTrancheId() take care of
registering the name. The CreateLWLocks() case strikes me as a special
path. IMHO LWLockRegisterTranche() should go away.

Is there a concern with a custom wait event to be created implicitly
via the GetNamed* APIs?

I'm not sure I see any particular advantage to using custom wait events
versus a dedicated LWLock tranche name table. If anything, the limits on
the number of tranches and the lengths of the names gives me pause.

--
nathan

#8Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#7)
Re: Improve LWLock tranche name visibility across backends

Attached is a proof of concept that does not alter the
LWLockRegisterTranche API.

IMHO we should consider modifying the API, because right now you have to
call LWLockRegisterTranche() in each backend. Why not accept the name as
an argument for LWLockNewTrancheId() and only require it to be called once
during your shared memory initialization?

Yes, we could do that, and this will simplify the tranche registration from the
current 2 step process of LWLockNewTrancheId() followed up with a
LWLockRegisterTranche(), to just simply LWLockNewTrancheId("my tranche").
I agree.

Instead, it detects when a registration is
performed by a normal backend and stores the tranche name in shared memory,
using a dshash keyed by tranche ID. Tranche name lookup now proceeds in
the order of built-in names, the local list, and finally the shared memory.
The fallback name "extension" can still be returned if an extension does
not register a tranche.

Why do we need three different places for the lock names? Is there a
reason we can't put it all in shared memory?

The real reason I felt it was better to keep three separate locations is that
it allows for a clear separation between user-defined tranches registered
during postmaster startup and those registered during a normal backend. The
tranches registered during postmaster are inherited by the backend via
fork() (or EXEC_BACKEND), and therefore, the dshash table will only be used
by a normal backend.

Since DSM is not available during postmaster, if we were to create a DSA
segment in place, similar to what's done in StatsShmemInit(), we would also
need to ensure that the initial shared memory is sized appropriately. This is
because it would need to be large enough to accommodate all user-defined
tranches registered during postmaster, without having to rely on new
dsm segments.
From my experimentation, this sizing is not as straightforward as simply
calculating # of tranches * size of a tranche entry.

I still think we should create the dsa during postmaster, as we do with
StatsShmemInit, but it would be better if postmaster keeps its hands off this
dshash and only normal backends can use them.

Thoughts?

2/ What is the appropriate size limit for a tranche name. The work done
in [0] caps the tranche name to 128 bytes for the dshash tranche, and
128 bytes + length of " DSA" suffix for the dsa tranche. Also, the
existing RequestNamedLWLockTranche caps the name to NAMEDATALEN. Currently,
LWLockRegisterTranche does not have a limit on the tranche name. I wonder
if we also need to take care of this and implement some common limit that
applies to tranch names regardless of how they're created?

Do we need to set a limit? If we're using a DSA and dshash, we could let
folks use arbitrary long tranche names, right? The reason for the limit in
the DSM registry is because the name is used as the key for the dshash
table.

Sure that is a good point. The dshash entry could be like below, without a limit
on the tranche_name.

```
typedef struct LWLockTracheNamesEntry
{
int trancheId;
const char *tranche_name;
} LWLockTracheNamesEntry;
```

Is there a concern with a custom wait event to be created implicitly
via the GetNamed* APIs?

I'm not sure I see any particular advantage to using custom wait events
versus a dedicated LWLock tranche name table. If anything, the limits on
the number of tranches and the lengths of the names gives me pause.

Sure, after contemplating on this a bit, I prefer a separate shared memory
as well. Custom wait events, while could work, will also be a bit of a confusing
user experience.

--
Sami

#9Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#8)
Re: Improve LWLock tranche name visibility across backends

On Mon, Jul 14, 2025 at 02:34:00PM -0500, Sami Imseih wrote:

Why do we need three different places for the lock names? Is there a
reason we can't put it all in shared memory?

The real reason I felt it was better to keep three separate locations is that
it allows for a clear separation between user-defined tranches registered
during postmaster startup and those registered during a normal backend. The
tranches registered during postmaster are inherited by the backend via
fork() (or EXEC_BACKEND), and therefore, the dshash table will only be used
by a normal backend.

Since DSM is not available during postmaster, if we were to create a DSA
segment in place, similar to what's done in StatsShmemInit(), we would also
need to ensure that the initial shared memory is sized appropriately. This is
because it would need to be large enough to accommodate all user-defined
tranches registered during postmaster, without having to rely on new
dsm segments.
From my experimentation, this sizing is not as straightforward as simply
calculating # of tranches * size of a tranche entry.

I still think we should create the dsa during postmaster, as we do with
StatsShmemInit, but it would be better if postmaster keeps its hands off this
dshash and only normal backends can use them.

Ah, I missed the problem with postmaster. Could we have the first backend
that needs to access the table be responsible for creating it and
populating it with the built-in/requested-at-startup entries? Also, is
there any chance that postmaster might need to access the tranche names?

--
nathan

#10Tom Lane
tgl@sss.pgh.pa.us
In reply to: Nathan Bossart (#9)
Re: Improve LWLock tranche name visibility across backends

Nathan Bossart <nathandbossart@gmail.com> writes:

Ah, I missed the problem with postmaster. Could we have the first backend
that needs to access the table be responsible for creating it and
populating it with the built-in/requested-at-startup entries? Also, is
there any chance that postmaster might need to access the tranche names?

Seems quite hazardous to let the postmaster get involved with such
a data structure. If it seems to need to, we'd better rethink
where to put the functionality that needs the access.

regards, tom lane

#11Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#9)
Re: Improve LWLock tranche name visibility across backends

Ah, I missed the problem with postmaster. Could we have the first backend
that needs to access the table be responsible for creating it and
populating it with the built-in/requested-at-startup entries?

We can certainly maintain a flag in the shared state that is set once
the first backend loads all the tranches in shared memory. That did not
cross my mind, but it feels wrong to offload such responsibility to a
normal backend.

Also, is there any chance that postmaster might need to access the
tranche names?

A postmaster does not currently have a reason to lookup
a tranche name, afaict. This only occurs when looking up wait events
or if lwlock tracing is enabled.

--
Sami

#12Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#11)
Re: Improve LWLock tranche name visibility across backends

On Mon, Jul 14, 2025 at 03:45:02PM -0500, Sami Imseih wrote:

Ah, I missed the problem with postmaster. Could we have the first backend
that needs to access the table be responsible for creating it and
populating it with the built-in/requested-at-startup entries?

We can certainly maintain a flag in the shared state that is set once
the first backend loads all the tranches in shared memory. That did not
cross my mind, but it feels wrong to offload such responsibility to a
normal backend.

Well, we already need each backend to either initialize or attach to the
dshash table, and the initialization would only ever happen once on a
running server. Adding a new initialization step to bootstrap the built-in
and registered-at-startup tranche names doesn't seem like that much of a
leap to me.

Another random thought: I worry that the dshash approach might be quite a
bit slower, and IIUC we just need to map an integer to a string. Maybe we
should just use a DSA for LWLockTrancheNames. IOW we'd leave it as a char
** but put it in shared memory.

--
nathan

#13Rahila Syed
rahilasyed90@gmail.com
In reply to: Sami Imseih (#8)
Re: Improve LWLock tranche name visibility across backends

Hi,

Since DSM is not available during postmaster, if we were to create a DSA
segment in place, similar to what's done in StatsShmemInit(), we would also
need to ensure that the initial shared memory is sized appropriately. This
is
because it would need to be large enough to accommodate all user-defined
tranches registered during postmaster, without having to rely on new
dsm segments.
From my experimentation, this sizing is not as straightforward as simply
calculating # of tranches * size of a tranche entry.

I still think we should create the dsa during postmaster, as we do with
StatsShmemInit, but it would be better if postmaster keeps its hands off
this
dshash and only normal backends can use them.

Thoughts?

Since creating a DSA segment in place during Postmaster startup still
requires calculating the size of
the tranche names table including the user-defined tranches, why not use
static shared memory to
pre-allocate a fixed sized table and arrays of size NAMEDATALEN to store
the names?

If a dshash table is used to store tranche names and IDs, where would the
tranche name for this table
be registered?

Thank you,
Rahila Syed

#14Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#6)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Fri, Jul 11, 2025 at 04:32:13PM -0500, Sami Imseih wrote:

and instead reuse the existing static hash table, which is
capped at 128 custom wait events:

```
#define WAIT_EVENT_CUSTOM_HASH_MAX_SIZE 128
```

That's probably still high enough, thoughts?

I have no reason to believe that this number could be too low.
I am not aware of an extension that will initialize more than a
couple of LWLocks.

or maybe we can just allow WaitEventCustomNew to take in the eventId, and
if it's > 0, then use the passed in value, otherwise generate the next eventId.

I do like the latter approach more, what do you think?

I think I do prefer it too, but in both cases we'll have to make sure there
is no collision on the eventID (LWTRANCHE_FIRST_USER_DEFINED is currently
95).

As far as collisions are concerned, the key of the hash is the wait_event_info,
which is a bitwise OR of classId and eventId
```
wait_event_info = classId | eventId;
```
Do you think collision can still be possible?

I meant to say collision between the trancheID and WaitEventCustomCounter->nextId

Regards,

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

#15Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#12)
Re: Improve LWLock tranche name visibility across backends

Another random thought: I worry that the dshash approach might be quite a
bit slower, and IIUC we just need to map an integer to a string. Maybe we
should just use a DSA for LWLockTrancheNames. IOW we'd leave it as a char**
but put it in shared memory.

To use DSA just for this purpose, we would need to maintain an array of
dsa_pointers that reference the string(s), right? I am not clear what you
mean by using dsa to put the char**

--
Sami

#16Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#15)
Re: Improve LWLock tranche name visibility across backends

On Tue, Jul 15, 2025 at 11:52:19AM -0500, Sami Imseih wrote:

Another random thought: I worry that the dshash approach might be quite a
bit slower, and IIUC we just need to map an integer to a string. Maybe we
should just use a DSA for LWLockTrancheNames. IOW we'd leave it as a char**
but put it in shared memory.

To use DSA just for this purpose, we would need to maintain an array of
dsa_pointers that reference the string(s), right? I am not clear what you
mean by using dsa to put the char**

I was imagining putting the array in one big DSA allocation instead of
carting around a pointer for each tranche name. (Sorry, I realize I am
hand-waving over some of the details.)

--
nathan

#17Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#16)
Re: Improve LWLock tranche name visibility across backends

On Tue, Jul 15, 2025 at 11:57 AM Nathan Bossart
<nathandbossart@gmail.com> wrote:

On Tue, Jul 15, 2025 at 11:52:19AM -0500, Sami Imseih wrote:

Another random thought: I worry that the dshash approach might be quite a
bit slower, and IIUC we just need to map an integer to a string. Maybe we
should just use a DSA for LWLockTrancheNames. IOW we'd leave it as a char**
but put it in shared memory.

To use DSA just for this purpose, we would need to maintain an array of
dsa_pointers that reference the string(s), right? I am not clear what you
mean by using dsa to put the char**

I was imagining putting the array in one big DSA allocation instead of
carting around a pointer for each tranche name. (Sorry, I realize I am
hand-waving over some of the details.)

I understood it like this. Here is a sketch:

```
dsa_pointer p;

dsa = dsa_create(....)

p = dsa_allocate(dsa, LWLockTranchesInitialSize());
tranche_names = (char **) dsa_get_address(dsa, p);
tranche_names[0] = "my tranche";
tranche_names[1] = "my tranche";
```

We will need to track the size and resize if needed.

Is this what you mean, from a high level?

--
Sami

#18Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#17)
Re: Improve LWLock tranche name visibility across backends

On Tue, Jul 15, 2025 at 12:06:00PM -0500, Sami Imseih wrote:

On Tue, Jul 15, 2025 at 11:57 AM Nathan Bossart
<nathandbossart@gmail.com> wrote:

I was imagining putting the array in one big DSA allocation instead of
carting around a pointer for each tranche name. (Sorry, I realize I am
hand-waving over some of the details.)

I understood it like this. Here is a sketch:

```
dsa_pointer p;

dsa = dsa_create(....)

p = dsa_allocate(dsa, LWLockTranchesInitialSize());
tranche_names = (char **) dsa_get_address(dsa, p);
tranche_names[0] = "my tranche";
tranche_names[1] = "my tranche";
```

We will need to track the size and resize if needed.

Is this what you mean, from a high level?

Yes, that's roughly what I had in mind. We might need to employ some
tricks to avoid a limit on tranche name length, but maybe that's not worth
the energy.

--
nathan

#19Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Rahila Syed (#13)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Tue, Jul 15, 2025 at 12:59:04PM +0530, Rahila Syed wrote:

Hi,

If a dshash table is used to store tranche names and IDs, where would the
tranche name for this table
be registered?

I guess it could be a new BuiltinTrancheId for this dsa but not sure what Nathan
and Sami have in mind.

Regards,

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

#20Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#19)
Re: Improve LWLock tranche name visibility across backends

Hi,

If a dshash table is used to store tranche names and IDs, where would the
tranche name for this table
be registered?

I guess it could be a new BuiltinTrancheId for this dsa but not sure what
Nathan
and Sami have in mind.

Yes, it will be a BuiltinTrancheId for a shared memory that is allocated
during postmaster for tracking tranches. The shared memory will then
only be used by normal backends to register tranches. Any tranche
registered during postmaster is inherited by the backends.

Regards,

Sami

Show quoted text
#21Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#18)
1 attachment(s)
Re: Improve LWLock tranche name visibility across backends

I was imagining putting the array in one big DSA allocation instead of
carting around a pointer for each tranche name. (Sorry, I realize I am
hand-waving over some of the details.)

I understood it like this. Here is a sketch:

```
dsa_pointer p;

dsa = dsa_create(....)

p = dsa_allocate(dsa, LWLockTranchesInitialSize());
tranche_names = (char **) dsa_get_address(dsa, p);
tranche_names[0] = "my tranche";
tranche_names[1] = "my tranche";
```

We will need to track the size and resize if needed.

Is this what you mean, from a high level?

Yes, that's roughly what I had in mind. We might need to employ some
tricks to avoid a limit on tranche name length, but maybe that's not worth
the energy.

After spending some time looking at this, I don't think it's a good idea to
do as mentioned above, since it's unclear when to resize the dsa pointer.
It's better to have a single dsa pointer that holds a list of dsa
pointers, each tracking a tranche name. We can also track the size of this
list and grow it when needed. Essentially we should only be manipulating
a DSA area via dsa_pointers.

Attached is what I have in mind, if we want to avoid using dshash.
This patch initializes a DSA with create_in_place at startup ( similar to what
we do for pgstat, see pgstat_shmem.c) , and provides
a routine to append a tranche name to the list, as well as a routine to
look up a tranche name by index. The index is the tranche ID offset by
LWTRANCHE_FIRST_USER_DEFINED.

Additionally, this patch removes the step in [0]https://www.postgresql.org/docs/current/xfunc-c.html#XFUNC-ADDIN-LWLOCKS-AFTER-STARTUP to call LWLockRegisterTranche.
Instead, LWLockNewTrancheId now takes a tranche_name and returns the
tranche ID. This new API is what we discussed earlier, and I agree with it,
as it simplifies the workflow. LWLockRegisterTranche is still kept, but it's
a private routine.

To support the above, LWLockTrancheNames is now a struct that tracks both a
local list of tranche names (populated during postmaster startup) and
pointers to shared memory, for tranche names registered after postmaster.

GetLWTrancheName now first checks BuiltinTrancheNames, then the local
list in LWLockTrancheNames, and finally the shared memory. This allows
tranche names registered after postmaster startup to be visible to all
backends.

Unlike the local list of tranche names, appending to and searching the
shared memory list requires an LWLock; in exclusive mode to append, and
shared mode to search.

--

Sami

[0]: https://www.postgresql.org/docs/current/xfunc-c.html#XFUNC-ADDIN-LWLOCKS-AFTER-STARTUP

Attachments:

0001-Store-LWLock-tranche-names-registered-after-postmast.patchapplication/octet-stream; name=0001-Store-LWLock-tranche-names-registered-after-postmast.patchDownload
From 8c47c55105b7f3daf2c116cb9ce39542d2e5e0d7 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Mon, 21 Jul 2025 19:02:47 -0500
Subject: [PATCH 1/1] Store LWLock tranche names registered after postmaster
 startup in dynamic shared memory.

---
 contrib/pg_prewarm/autoprewarm.c              |   4 +-
 doc/src/sgml/xfunc.sgml                       |  18 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/ipc/ipci.c                |   2 +
 src/backend/storage/lmgr/lwlock.c             | 326 +++++++++++++++---
 .../utils/activity/wait_event_names.txt       |   2 +
 src/backend/utils/init/postinit.c             |   2 +
 src/include/storage/lwlock.h                  |  24 +-
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 13 files changed, 314 insertions(+), 107 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..b623589b430 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,8 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
-
 	return found;
 }
 
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 2d81afce8cb..649531d9ef8 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3758,9 +3758,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name<literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3778,17 +3779,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..c8e4d55cf80 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -343,6 +343,8 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+
+	LWLockTrancheNamesShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 2d43bf2cc13..a4313d8af7d 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -80,6 +80,7 @@
 #include "pg_trace.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
+#include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/proclist.h"
 #include "storage/procnumber.h"
@@ -127,8 +128,9 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * appear in BuiltinTrancheNames[] below.
  *
  * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * or LWLockNewTrancheId.  The names of these are registered in LWLockTrancheNames
+ * either by during postmaster startup or by after. For the latter case,
+ * there is a dedicated dynamic shared memory to store the tranche named.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -178,20 +180,14 @@ static const char *const BuiltinTrancheNames[] = {
 	[LWTRANCHE_XACT_SLRU] = "XactSLRU",
 	[LWTRANCHE_PARALLEL_VACUUM_DSA] = "ParallelVacuumDSA",
 	[LWTRANCHE_AIO_URING_COMPLETION] = "AioUringCompletion",
+	[LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA] = "LWLockDynamicTranchesDSA",
+	[LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST] = "LWLockDynamicTranchesList",
 };
 
 StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 				 LWTRANCHE_FIRST_USER_DEFINED,
 				 "missing entries in BuiltinTrancheNames[]");
 
-/*
- * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, 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 **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
-
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
  * the pointer by fork from the postmaster (except in the EXEC_BACKEND case,
@@ -227,6 +223,45 @@ typedef struct NamedLWLockTrancheRequest
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
 static int	NamedLWLockTrancheRequestsAllocated = 0;
 
+typedef struct LWLockTrancheNamesShmem
+{
+	/* pointer to the DSA shared memory, created in-place */
+	void	   *raw_dsa_area;
+	/* DSA Pointer to a list of DSA pointers, each storing a tranche name */
+	dsa_pointer list_ptr;
+	/* number of tranche names stored in shared memory */
+	int			count;
+
+	/*
+	 * The number of slots allocated for tranche names. The allocated size
+	 * will grow geometrically to reduce resizing of the list_ptr.
+	 */
+	int			allocated;
+	/* lock to protect access to the list */
+	LWLock		lock;
+}			LWLockTrancheNamesShmem;
+
+typedef struct LWLockTrancheNamesStruct
+{
+	/* Fields for tranche names reated during ostmaster startup */
+	const char **local;
+	int			allocated;
+
+	/*
+	 * Fields for shared memory tranche names ( those created after postmaster
+	 * startup )
+	 */
+	LWLockTrancheNamesShmem *shmem;
+	dsa_area   *dsa;
+}			LWLockTrancheNamesStruct;
+
+static LWLockTrancheNamesStruct LWLockTrancheNames =
+{
+	NULL, 0, NULL, NULL
+};
+
+#define LWLOCK_TRANCHE_NAMES_INIT_SIZE 16
+
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
  * and the length of the shared-memory NamedLWLockTrancheArray later on.
@@ -242,6 +277,10 @@ static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
 static const char *GetLWTrancheName(uint16 trancheId);
+static void LWLockRegisterTranche(int tranche_id,
+								  const char *tranche_name);
+static void LWLockTrancheNamesAttach(void);
+static void LWLockTrancheNamesDetach(void);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -487,12 +526,12 @@ CreateLWLocks(void)
 
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
-	}
 
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
+		/* Register named extension LWLock tranches in the current process. */
+		for (int i = 0; i < NamedLWLockTrancheRequests; i++)
+			LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
+								  NamedLWLockTrancheArray[i].trancheName);
+	}
 }
 
 /*
@@ -553,7 +592,7 @@ InitializeLWLocks(void)
 			name = trancheNames;
 			trancheNames += strlen(request->tranche_name) + 1;
 			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
+			tranche->trancheId = LWLockNewTrancheId(name);
 			tranche->trancheName = name;
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
@@ -609,21 +648,30 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID and register it with the associated tranche
+ * name.
+ *
+ * The tranche name will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
+	int			tranche_id;
+	int			tranche_index;
 	int		   *LWLockCounter;
 
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 	/* We use the ShmemLock spinlock to protect LWLockCounter */
 	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
+	tranche_id = (*LWLockCounter)++;
 	SpinLockRelease(ShmemLock);
 
-	return result;
+	tranche_index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
+
+	LWLockRegisterTranche(tranche_index, tranche_name);
+
+	return tranche_id;
 }
 
 /*
@@ -633,37 +681,79 @@ LWLockNewTrancheId(void)
  * so the name should be allocated in a backend-lifetime context
  * (shared memory, TopMemoryContext, static constant, or similar).
  *
- * 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
+static void
 LWLockRegisterTranche(int tranche_id, const char *tranche_name)
 {
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
-
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	int			newalloc;
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (!IsUnderPostmaster)
 	{
-		int			newalloc;
+		/* If necessary, create or enlarge array. */
+		if (tranche_id >= LWLockTrancheNames.allocated)
+		{
+			newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+			if (LWLockTrancheNames.local == NULL)
+				LWLockTrancheNames.local = (const char **)
+					MemoryContextAllocZero(TopMemoryContext,
+										   newalloc * sizeof(char *));
+			else
+				LWLockTrancheNames.local =
+					repalloc0_array(LWLockTrancheNames.local, const char *, LWLockTrancheNames.allocated, newalloc);
+			LWLockTrancheNames.allocated = newalloc;
+		}
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+		LWLockTrancheNames.local[tranche_id] = tranche_name;
 	}
+	else
+	{
+		dsa_pointer *current_ptrs;
+		dsa_pointer new_list_ptr;
+		dsa_pointer *new_name_ptrs;
+		size_t		len;
+		dsa_pointer str_ptr;
+		char	   *str_addr;
+
+		LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
+
+		/*
+		 * The first time adding a name to the shmem, so let's allocate the
+		 * list now.
+		 */
+		if (LWLockTrancheNames.shmem->list_ptr == InvalidDsaPointer)
+		{
+			LWLockTrancheNames.shmem->list_ptr = dsa_allocate(LWLockTrancheNames.dsa,
+															  LWLOCK_TRANCHE_NAMES_INIT_SIZE * sizeof(dsa_pointer));
+			LWLockTrancheNames.shmem->allocated = LWLOCK_TRANCHE_NAMES_INIT_SIZE;
+		}
+
+		current_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+		/* Resize if needed */
+		if (LWLockTrancheNames.shmem->count >= LWLockTrancheNames.shmem->allocated)
+		{
+			newalloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE, LWLockTrancheNames.shmem->allocated + 1));
+			new_list_ptr = dsa_allocate(LWLockTrancheNames.dsa, newalloc * sizeof(dsa_pointer));
+			new_name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list_ptr);
+			memcpy(new_name_ptrs, current_ptrs, LWLockTrancheNames.shmem->allocated * sizeof(dsa_pointer));
+			dsa_free(LWLockTrancheNames.dsa, *current_ptrs);
+			LWLockTrancheNames.shmem->list_ptr = new_list_ptr;
+			LWLockTrancheNames.shmem->allocated = newalloc;
+			current_ptrs = new_name_ptrs;
+		}
+
+		/* Allocate and copy string */
+		len = strlen(tranche_name) + 1;
+		str_ptr = dsa_allocate(LWLockTrancheNames.dsa, len);
+		str_addr = dsa_get_address(LWLockTrancheNames.dsa, str_ptr);
+		memcpy(str_addr, tranche_name, len);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+		current_ptrs[tranche_id] = str_ptr;
+		LWLockTrancheNames.shmem->count++;
+
+		LWLockRelease(&LWLockTrancheNames.shmem->lock);
+	}
 }
 
 /*
@@ -748,12 +838,32 @@ LWLockReportWaitEnd(void)
 	pgstat_report_wait_end();
 }
 
+
+static const char *
+GetLWTrancheNameByID(int tranche_id)
+{
+	dsa_pointer *pointers;
+	char	   *str = NULL;
+
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_SHARED);
+
+	pointers = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+	str = dsa_get_address(LWLockTrancheNames.dsa, pointers[tranche_id]);
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+
+	return str;
+}
+
 /*
  * Return the name of an LWLock tranche.
  */
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	const char *tranche_name = NULL;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
@@ -765,11 +875,15 @@ GetLWTrancheName(uint16 trancheId)
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (trancheId < LWLockTrancheNames.allocated &&
+		LWLockTrancheNames.local[trancheId] != NULL)
+		return LWLockTrancheNames.local[trancheId];
 
-	return LWLockTrancheNames[trancheId];
+	tranche_name = GetLWTrancheNameByID(trancheId);
+
+	Assert(tranche_name);
+
+	return tranche_name;
 }
 
 /*
@@ -2035,3 +2149,121 @@ LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode)
 	}
 	return false;
 }
+
+static Size
+LWLockTrancheNamesInitSize()
+{
+	Size		sz;
+
+	sz = 256 * 1024;
+
+	Assert(dsa_minimum_size() <= sz);
+
+	return MAXALIGN(sz);
+}
+
+Size
+LWLockTrancheNamesShmemSize(void)
+{
+	Size		sz;
+
+	sz = MAXALIGN(sizeof(LWLockTrancheNamesStruct));
+	sz = add_size(sz, LWLockTrancheNamesInitSize());
+
+	return sz;
+}
+
+void
+LWLockTrancheNamesShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize or attach to shared memory structure */
+	LWLockTrancheNames.shmem = (LWLockTrancheNamesShmem *)
+		ShmemInitStruct("Dynamic LWLock tranche names",
+						LWLockTrancheNamesShmemSize(),
+						&found);
+
+	if (!IsUnderPostmaster)
+	{
+		dsa_area   *dsa;
+		LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+		char	   *p = (char *) ctl;
+
+		/* Calculate layout within the shared memory region */
+		p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+		ctl->raw_dsa_area = p;
+		p += MAXALIGN(LWLockTrancheNamesInitSize());
+
+		LWLockInitialize(&ctl->lock, LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST);
+
+		/* Create a dynamic shared memory area in-place */
+		dsa = dsa_create_in_place(ctl->raw_dsa_area,
+								  LWLockTrancheNamesInitSize(),
+								  LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA,
+								  NULL);
+
+		/*
+		 * Postmaster will never access these again, thus free the local
+		 * dsa/dshash references.
+		 */
+		dsa_pin(dsa);
+		dsa_detach(dsa);
+
+		ctl->allocated = 0;
+		ctl->count = 0;
+		ctl->list_ptr = InvalidDsaPointer;
+	}
+	else
+	{
+		Assert(found);
+	}
+}
+
+static void
+LWLockTrancheNamesAttach(void)
+{
+	MemoryContext oldcontext;
+
+	Assert(LWLockTrancheNames.dsa == NULL);
+
+	/* stats shared memory persists for the backend lifetime */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	LWLockTrancheNames.dsa = dsa_attach_in_place(LWLockTrancheNames.shmem->raw_dsa_area,
+												 NULL);
+	dsa_pin_mapping(LWLockTrancheNames.dsa);
+
+	Assert(LWLockTrancheNames.dsa);
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+static void
+LWLockTrancheNamesDetach(void)
+{
+	Assert(LWLockTrancheNames.dsa);
+
+	dsa_detach(LWLockTrancheNames.dsa);
+
+	dsa_release_in_place(LWLockTrancheNames.shmem->raw_dsa_area);
+
+	LWLockTrancheNames.dsa = NULL;
+}
+
+static void
+LWLockTrancheNamesShutdownHook(int code, Datum arg)
+{
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	LWLockTrancheNamesDetach();
+}
+
+void
+LWLockTrancheNamesBEInit(void)
+{
+	LWLockTrancheNamesAttach();
+
+	/* Set up a process-exit hook to clean up */
+	before_shmem_exit(LWLockTrancheNamesShutdownHook, 0);
+}
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4da68312b5f..3785377e6bc 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -402,6 +402,8 @@ SubtransSLRU	"Waiting to access the sub-transaction SLRU cache."
 XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
+LWLockDynamicTranchesDSA	"Waiting to access LWLock tranche named dynamic shared memory."
+LWLockDynamicTranchesList	"Waiting to access the list of LWLock tranche names in shared memory."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 641e535a73c..1ef04c6b5ec 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -662,6 +662,8 @@ BaseInit(void)
 	 * drop ephemeral slots, which in turn triggers stats reporting.
 	 */
 	ReplicationSlotInitialize();
+
+	LWLockTrancheNamesBEInit();
 }
 
 
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 08a72569ae5..c8d5020c19f 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -156,22 +156,18 @@ extern void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
- * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * An alternative, more flexible method of obtaining lwlocks is available.
+ * First, call LWLockNewTrancheId once, providing an associated tranche name.
+ * This function allocates a tranche ID from a shared counter. Then, for each
+ * lwlock, call LWLockInitialize once, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
+extern void LWLockTrancheNamesShmemInit(void);
+extern Size LWLockTrancheNamesShmemSize(void);
+extern void LWLockTrancheNamesBEInit(void);
+
 /*
  * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
  * we reserve additional tranche IDs for builtin tranches not included in
@@ -221,6 +217,8 @@ typedef enum BuiltinTrancheIds
 	LWTRANCHE_XACT_SLRU,
 	LWTRANCHE_PARALLEL_VACUUM_DSA,
 	LWTRANCHE_AIO_URING_COMPLETION,
+	LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA,
+	LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST,
 	LWTRANCHE_FIRST_USER_DEFINED,
 }			BuiltinTrancheIds;
 
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 32de6a3123e..4ae826d08a0 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

#22Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#21)
1 attachment(s)
Re: Improve LWLock tranche name visibility across backends

I've attached a rebased version of the patch.

On Mon, Jul 21, 2025 at 11:26:44PM -0500, Sami Imseih wrote:

Unlike the local list of tranche names, appending to and searching the
shared memory list requires an LWLock; in exclusive mode to append, and
shared mode to search.

The thing that stood out the most to me is how much more expensive looking
up the lock name is. At the risk of suggesting even more complexity, I'm
wondering if we should add some sort of per-backend cache to avoid the need
for locking and dsa_get_address() calls every time you need to look up a
lock name.

Also, I suspect that there might be some concerns about the API changes.
While it should be a very easy fix, that seems likely to break a lot of
extensions. I don't know if it's possible to make this stuff backward
compatible, and I also don't know if we really want to, as that'll both
introduce more complexity and keep folks using the old API. (That being
said, I just looked on GitHub and Debian Code Search and was surprised at
how few uses of LWLockNewTrancheId() and LWLockRegisterTranche() there
are...)

--
nathan

Attachments:

v3-0001-Store-LWLock-tranche-names-registered-after-postm.patchtext/plain; charset=us-asciiDownload
From c06e8ef0599bda5f2fe77c46087a768c47ea23d4 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Mon, 21 Jul 2025 19:02:47 -0500
Subject: [PATCH v3 1/1] Store LWLock tranche names registered after postmaster
 startup in dynamic shared memory.

---
 contrib/pg_prewarm/autoprewarm.c              |   4 +-
 doc/src/sgml/xfunc.sgml                       |  18 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/ipc/ipci.c                |   2 +
 src/backend/storage/lmgr/lwlock.c             | 324 +++++++++++++++---
 .../utils/activity/wait_event_names.txt       |   2 +
 src/backend/utils/init/postinit.c             |   2 +
 src/include/storage/lwlock.h                  |  22 +-
 src/include/storage/lwlocklist.h              |   2 +
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 14 files changed, 312 insertions(+), 107 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..b623589b430 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,8 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
-
 	return found;
 }
 
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 2d81afce8cb..a45d3039b47 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3758,9 +3758,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3778,17 +3779,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..c8e4d55cf80 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -343,6 +343,8 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+
+	LWLockTrancheNamesShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index ec9c345ffdf..e777d23d098 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -80,6 +80,7 @@
 #include "pg_trace.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
+#include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/proclist.h"
 #include "storage/procnumber.h"
@@ -126,8 +127,9 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
  * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * or LWLockNewTrancheId.  The names of these are registered in LWLockTrancheNames
+ * either by during postmaster startup or by after. For the latter case,
+ * there is a dedicated dynamic shared memory to store the tranche named.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -144,14 +146,6 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 				 LWTRANCHE_FIRST_USER_DEFINED,
 				 "missing entries in BuiltinTrancheNames[]");
 
-/*
- * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, 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 **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
-
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
  * the pointer by fork from the postmaster (except in the EXEC_BACKEND case,
@@ -187,6 +181,45 @@ typedef struct NamedLWLockTrancheRequest
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
 static int	NamedLWLockTrancheRequestsAllocated = 0;
 
+typedef struct LWLockTrancheNamesShmem
+{
+	/* pointer to the DSA shared memory, created in-place */
+	void	   *raw_dsa_area;
+	/* DSA Pointer to a list of DSA pointers, each storing a tranche name */
+	dsa_pointer list_ptr;
+	/* number of tranche names stored in shared memory */
+	int			count;
+
+	/*
+	 * The number of slots allocated for tranche names. The allocated size
+	 * will grow geometrically to reduce resizing of the list_ptr.
+	 */
+	int			allocated;
+	/* lock to protect access to the list */
+	LWLock		lock;
+}			LWLockTrancheNamesShmem;
+
+typedef struct LWLockTrancheNamesStruct
+{
+	/* Fields for tranche names reated during ostmaster startup */
+	const char **local;
+	int			allocated;
+
+	/*
+	 * Fields for shared memory tranche names ( those created after postmaster
+	 * startup )
+	 */
+	LWLockTrancheNamesShmem *shmem;
+	dsa_area   *dsa;
+}			LWLockTrancheNamesStruct;
+
+static LWLockTrancheNamesStruct LWLockTrancheNames =
+{
+	NULL, 0, NULL, NULL
+};
+
+#define LWLOCK_TRANCHE_NAMES_INIT_SIZE 16
+
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
  * and the length of the shared-memory NamedLWLockTrancheArray later on.
@@ -202,6 +235,10 @@ static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
 static const char *GetLWTrancheName(uint16 trancheId);
+static void LWLockRegisterTranche(int tranche_id,
+								  const char *tranche_name);
+static void LWLockTrancheNamesAttach(void);
+static void LWLockTrancheNamesDetach(void);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -447,12 +484,12 @@ CreateLWLocks(void)
 
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
-	}
 
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
+		/* Register named extension LWLock tranches in the current process. */
+		for (int i = 0; i < NamedLWLockTrancheRequests; i++)
+			LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
+								  NamedLWLockTrancheArray[i].trancheName);
+	}
 }
 
 /*
@@ -513,7 +550,7 @@ InitializeLWLocks(void)
 			name = trancheNames;
 			trancheNames += strlen(request->tranche_name) + 1;
 			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
+			tranche->trancheId = LWLockNewTrancheId(name);
 			tranche->trancheName = name;
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
@@ -569,21 +606,30 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID and register it with the associated tranche
+ * name.
+ *
+ * The tranche name will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
+	int			tranche_id;
+	int			tranche_index;
 	int		   *LWLockCounter;
 
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 	/* We use the ShmemLock spinlock to protect LWLockCounter */
 	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
+	tranche_id = (*LWLockCounter)++;
 	SpinLockRelease(ShmemLock);
 
-	return result;
+	tranche_index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
+
+	LWLockRegisterTranche(tranche_index, tranche_name);
+
+	return tranche_id;
 }
 
 /*
@@ -593,37 +639,79 @@ LWLockNewTrancheId(void)
  * so the name should be allocated in a backend-lifetime context
  * (shared memory, TopMemoryContext, static constant, or similar).
  *
- * 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
+static void
 LWLockRegisterTranche(int tranche_id, const char *tranche_name)
 {
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
-
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	int			newalloc;
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (!IsUnderPostmaster)
 	{
-		int			newalloc;
+		/* If necessary, create or enlarge array. */
+		if (tranche_id >= LWLockTrancheNames.allocated)
+		{
+			newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+			if (LWLockTrancheNames.local == NULL)
+				LWLockTrancheNames.local = (const char **)
+					MemoryContextAllocZero(TopMemoryContext,
+										   newalloc * sizeof(char *));
+			else
+				LWLockTrancheNames.local =
+					repalloc0_array(LWLockTrancheNames.local, const char *, LWLockTrancheNames.allocated, newalloc);
+			LWLockTrancheNames.allocated = newalloc;
+		}
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+		LWLockTrancheNames.local[tranche_id] = tranche_name;
 	}
+	else
+	{
+		dsa_pointer *current_ptrs;
+		dsa_pointer new_list_ptr;
+		dsa_pointer *new_name_ptrs;
+		size_t		len;
+		dsa_pointer str_ptr;
+		char	   *str_addr;
+
+		LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
+
+		/*
+		 * The first time adding a name to the shmem, so let's allocate the
+		 * list now.
+		 */
+		if (LWLockTrancheNames.shmem->list_ptr == InvalidDsaPointer)
+		{
+			LWLockTrancheNames.shmem->list_ptr = dsa_allocate(LWLockTrancheNames.dsa,
+															  LWLOCK_TRANCHE_NAMES_INIT_SIZE * sizeof(dsa_pointer));
+			LWLockTrancheNames.shmem->allocated = LWLOCK_TRANCHE_NAMES_INIT_SIZE;
+		}
+
+		current_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+		/* Resize if needed */
+		if (LWLockTrancheNames.shmem->count >= LWLockTrancheNames.shmem->allocated)
+		{
+			newalloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE, LWLockTrancheNames.shmem->allocated + 1));
+			new_list_ptr = dsa_allocate(LWLockTrancheNames.dsa, newalloc * sizeof(dsa_pointer));
+			new_name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list_ptr);
+			memcpy(new_name_ptrs, current_ptrs, LWLockTrancheNames.shmem->allocated * sizeof(dsa_pointer));
+			dsa_free(LWLockTrancheNames.dsa, *current_ptrs);
+			LWLockTrancheNames.shmem->list_ptr = new_list_ptr;
+			LWLockTrancheNames.shmem->allocated = newalloc;
+			current_ptrs = new_name_ptrs;
+		}
+
+		/* Allocate and copy string */
+		len = strlen(tranche_name) + 1;
+		str_ptr = dsa_allocate(LWLockTrancheNames.dsa, len);
+		str_addr = dsa_get_address(LWLockTrancheNames.dsa, str_ptr);
+		memcpy(str_addr, tranche_name, len);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+		current_ptrs[tranche_id] = str_ptr;
+		LWLockTrancheNames.shmem->count++;
+
+		LWLockRelease(&LWLockTrancheNames.shmem->lock);
+	}
 }
 
 /*
@@ -708,12 +796,32 @@ LWLockReportWaitEnd(void)
 	pgstat_report_wait_end();
 }
 
+
+static const char *
+GetLWTrancheNameByID(int tranche_id)
+{
+	dsa_pointer *pointers;
+	char	   *str = NULL;
+
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_SHARED);
+
+	pointers = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+	str = dsa_get_address(LWLockTrancheNames.dsa, pointers[tranche_id]);
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+
+	return str;
+}
+
 /*
  * Return the name of an LWLock tranche.
  */
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	const char *tranche_name = NULL;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
@@ -725,11 +833,15 @@ GetLWTrancheName(uint16 trancheId)
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (trancheId < LWLockTrancheNames.allocated &&
+		LWLockTrancheNames.local[trancheId] != NULL)
+		return LWLockTrancheNames.local[trancheId];
 
-	return LWLockTrancheNames[trancheId];
+	tranche_name = GetLWTrancheNameByID(trancheId);
+
+	Assert(tranche_name);
+
+	return tranche_name;
 }
 
 /*
@@ -1995,3 +2107,121 @@ LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode)
 	}
 	return false;
 }
+
+static Size
+LWLockTrancheNamesInitSize()
+{
+	Size		sz;
+
+	sz = 256 * 1024;
+
+	Assert(dsa_minimum_size() <= sz);
+
+	return MAXALIGN(sz);
+}
+
+Size
+LWLockTrancheNamesShmemSize(void)
+{
+	Size		sz;
+
+	sz = MAXALIGN(sizeof(LWLockTrancheNamesStruct));
+	sz = add_size(sz, LWLockTrancheNamesInitSize());
+
+	return sz;
+}
+
+void
+LWLockTrancheNamesShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize or attach to shared memory structure */
+	LWLockTrancheNames.shmem = (LWLockTrancheNamesShmem *)
+		ShmemInitStruct("Dynamic LWLock tranche names",
+						LWLockTrancheNamesShmemSize(),
+						&found);
+
+	if (!IsUnderPostmaster)
+	{
+		dsa_area   *dsa;
+		LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+		char	   *p = (char *) ctl;
+
+		/* Calculate layout within the shared memory region */
+		p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+		ctl->raw_dsa_area = p;
+		p += MAXALIGN(LWLockTrancheNamesInitSize());
+
+		LWLockInitialize(&ctl->lock, LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST);
+
+		/* Create a dynamic shared memory area in-place */
+		dsa = dsa_create_in_place(ctl->raw_dsa_area,
+								  LWLockTrancheNamesInitSize(),
+								  LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA,
+								  NULL);
+
+		/*
+		 * Postmaster will never access these again, thus free the local
+		 * dsa/dshash references.
+		 */
+		dsa_pin(dsa);
+		dsa_detach(dsa);
+
+		ctl->allocated = 0;
+		ctl->count = 0;
+		ctl->list_ptr = InvalidDsaPointer;
+	}
+	else
+	{
+		Assert(found);
+	}
+}
+
+static void
+LWLockTrancheNamesAttach(void)
+{
+	MemoryContext oldcontext;
+
+	Assert(LWLockTrancheNames.dsa == NULL);
+
+	/* stats shared memory persists for the backend lifetime */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	LWLockTrancheNames.dsa = dsa_attach_in_place(LWLockTrancheNames.shmem->raw_dsa_area,
+												 NULL);
+	dsa_pin_mapping(LWLockTrancheNames.dsa);
+
+	Assert(LWLockTrancheNames.dsa);
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+static void
+LWLockTrancheNamesDetach(void)
+{
+	Assert(LWLockTrancheNames.dsa);
+
+	dsa_detach(LWLockTrancheNames.dsa);
+
+	dsa_release_in_place(LWLockTrancheNames.shmem->raw_dsa_area);
+
+	LWLockTrancheNames.dsa = NULL;
+}
+
+static void
+LWLockTrancheNamesShutdownHook(int code, Datum arg)
+{
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	LWLockTrancheNamesDetach();
+}
+
+void
+LWLockTrancheNamesBEInit(void)
+{
+	LWLockTrancheNamesAttach();
+
+	/* Set up a process-exit hook to clean up */
+	before_shmem_exit(LWLockTrancheNamesShutdownHook, 0);
+}
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0be307d2ca0..ac827f96ad6 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -406,6 +406,8 @@ SubtransSLRU	"Waiting to access the sub-transaction SLRU cache."
 XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
+LWLockDynamicTranchesDSA	"Waiting to access LWLock tranche named dynamic shared memory."
+LWLockDynamicTranchesList	"Waiting to access the list of LWLock tranche names in shared memory."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 641e535a73c..1ef04c6b5ec 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -662,6 +662,8 @@ BaseInit(void)
 	 * drop ephemeral slots, which in turn triggers stats reporting.
 	 */
 	ReplicationSlotInitialize();
+
+	LWLockTrancheNamesBEInit();
 }
 
 
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..8a008fc67dd 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -156,22 +156,18 @@ extern void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
- * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * An alternative, more flexible method of obtaining lwlocks is available.
+ * First, call LWLockNewTrancheId once, providing an associated tranche name.
+ * This function allocates a tranche ID from a shared counter. Then, for each
+ * lwlock, call LWLockInitialize once, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
+extern void LWLockTrancheNamesShmemInit(void);
+extern Size LWLockTrancheNamesShmemSize(void);
+extern void LWLockTrancheNamesBEInit(void);
+
 /*
  * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
  * we reserve additional tranche IDs for builtin tranches not included in
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 208d2e3a8ed..5978648c09d 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -135,3 +135,5 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU)
 PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
 PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
 PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_DSA, LWLockDynamicTranchesDSA)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_LIST, LWLockDynamicTranchesList)
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 32de6a3123e..4ae826d08a0 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

#23Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Nathan Bossart (#22)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Thu, Jul 31, 2025 at 04:24:38PM -0500, Nathan Bossart wrote:

I've attached a rebased version of the patch.

On Mon, Jul 21, 2025 at 11:26:44PM -0500, Sami Imseih wrote:

Unlike the local list of tranche names, appending to and searching the
shared memory list requires an LWLock; in exclusive mode to append, and
shared mode to search.

The thing that stood out the most to me is how much more expensive looking
up the lock name is. At the risk of suggesting even more complexity, I'm
wondering if we should add some sort of per-backend cache to avoid the need
for locking and dsa_get_address() calls every time you need to look up a
lock name.

Yeah, I 've the same concern that GetLWTrancheNameByID() might be too costly.
OTOH it's not really used in a hot path, "only" when displaying wait events.

But still, I agree that the extra overhead (and possible contention) is
concerning and that, for example, a per-backend cache (with fallback to the
dsa if needed) could help.

Also, I suspect that there might be some concerns about the API changes.
While it should be a very easy fix, that seems likely to break a lot of
extensions.
I don't know if it's possible to make this stuff backward
compatible, and I also don't know if we really want to, as that'll both
introduce more complexity and keep folks using the old API.

Yeah, I'm not sure that would be worth the extra complexity and using the old API
would "keep" the issue we're trying to solve here.

I don't think we should be worried that much by the number of extensions impacted
but more about the change complexity and it looks pretty simple.

So, I don't think we should worry that much in that regard.

Regards,

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

#24Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#23)
Re: Improve LWLock tranche name visibility across backends

Unlike the local list of tranche names, appending to and searching the
shared memory list requires an LWLock; in exclusive mode to append, and
shared mode to search.

The thing that stood out the most to me is how much more expensive looking
up the lock name is. At the risk of suggesting even more complexity, I'm
wondering if we should add some sort of per-backend cache to avoid the need
for locking and dsa_get_address() calls every time you need to look up a
lock name.

Yeah, I 've the same concern that GetLWTrancheNameByID() might be too costly.
OTOH it's not really used in a hot path, "only" when displaying wait events.

LWLockTrancheGetName also gets called, via T_NAME, when
trace_lwlock is enabled. This is a developer option that requires
LOCK_DEBUG to be set at compile time.
So, that path may become even slower than it currently
is under high concurrency.

Also, if dtrace is enabled and the specific LWLock probe is used.
```
if (TRACE_POSTGRESQL_LWLOCK_WAIT_START_ENABLED())
TRACE_POSTGRESQL_LWLOCK_WAIT_START(T_NAME(lock), mode);
```
But both of the above cases are not used in normal production activity.

For the case of dtrace, b94409a02f612 added the _ENABLED macros to ensure that
GetLWTrancheName ( via T_NAME) is only called when necessary.

"
If dtrace is compiled in but disabled, the lwlock dtrace probes still
evaluate their arguments. Since PostgreSQL 13, T_NAME(lock) does
nontrivial work, so it should be avoided if not needed. To fix, make
these calls conditional on the *_ENABLED() macro corresponding to each
probe.
"

The normal case is when pg_stat_get_activity calls
pgstat_get_wait_event to retrieve the wait event via
GetLWLockIdentifier. However, as mentioned, this is not on the hot path.

I think we could add a local backend copy that stays up to date with the
DSA. One idea would be to use an atomic counter to track the number of
entries in the DSA and compare it with a local backend counter whenever the
tranche name lookup occurs. If the atomic counter is higher (since we
don't have deletions),
we can update the local copy. Updating the local table should be a
rare occurrence, but it would
require an additional atomic fetch every time the name lookup occurs, in all the
above code paths.

Perhaps there's a better approach?

--
Sami

#25Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#23)
Re: Improve LWLock tranche name visibility across backends

Also, I suspect that there might be some concerns about the API changes.
While it should be a very easy fix, that seems likely to break a lot of
extensions.
I don't know if it's possible to make this stuff backward
compatible, and I also don't know if we really want to, as that'll both
introduce more complexity and keep folks using the old API.

Yeah, I'm not sure that would be worth the extra complexity and using the
old API
would "keep" the issue we're trying to solve here.

I don't think we should be worried that much by the number of extensions
impacted
but more about the change complexity and it looks pretty simple.

So, I don't think we should worry that much in that regard.

I agree. I don’t think the API changes are a big
deal, especially when they reduce the number of
of steps.

--
Sami

#26Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#24)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Fri, Aug 01, 2025 at 01:15:24PM -0500, Sami Imseih wrote:

I think we could add a local backend copy that stays up to date with the
DSA. One idea would be to use an atomic counter to track the number of
entries in the DSA and compare it with a local backend counter whenever the
tranche name lookup occurs. If the atomic counter is higher (since we
don't have deletions),
we can update the local copy. Updating the local table should be a
rare occurrence, but it would
require an additional atomic fetch every time the name lookup occurs, in all the
above code paths.

Perhaps there's a better approach?

I was thinking to switch to the DSA (and update local copy) when a name is
not found in the local copy. That way there is no need to maintain a counter and
the DSA overhead should be rare enough.

Regards,

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

#27Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#26)
Re: Improve LWLock tranche name visibility across backends

I think we could add a local backend copy that stays up to date with the
DSA. One idea would be to use an atomic counter to track the number of
entries in the DSA and compare it with a local backend counter whenever the
tranche name lookup occurs. If the atomic counter is higher (since we
don't have deletions),
we can update the local copy. Updating the local table should be a
rare occurrence, but it would
require an additional atomic fetch every time the name lookup occurs, in all the
above code paths.

Perhaps there's a better approach?

I was thinking to switch to the DSA (and update local copy) when a name is
not found in the local copy. That way there is no need to maintain a counter and
the DSA overhead should be rare enough.

Regards,

That should work as well. good idea.

--
Sami

#28Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#27)
Re: Improve LWLock tranche name visibility across backends

I think we could add a local backend copy that stays up to date with the
DSA. One idea would be to use an atomic counter to track the number of
entries in the DSA and compare it with a local backend counter whenever the
tranche name lookup occurs. If the atomic counter is higher (since we
don't have deletions),
we can update the local copy. Updating the local table should be a
rare occurrence, but it would
require an additional atomic fetch every time the name lookup occurs, in all the
above code paths.

Perhaps there's a better approach?

I was thinking to switch to the DSA (and update local copy) when a name is
not found in the local copy. That way there is no need to maintain a counter and
the DSA overhead should be rare enough.

Regards,

That should work as well. good idea.

With a local hash table, I don't think it's necessary to introduce new
code for managing
a DSA based list of tranche names as is done in v3. We can go back to
storing the shared
trance names in dshash.

What do you think?

--
Sami

#29Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#28)
Re: Improve LWLock tranche name visibility across backends

On Mon, Aug 04, 2025 at 11:32:19AM -0500, Sami Imseih wrote:

With a local hash table, I don't think it's necessary to introduce new
code for managing
a DSA based list of tranche names as is done in v3. We can go back to
storing the shared
trance names in dshash.

What do you think?

My first thought is that a per-backend hash table seems too
expensive/complicated for this. Couldn't it just be an array like we have
now?

--
nathan

#30Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#29)
Re: Improve LWLock tranche name visibility across backends

On Mon, Aug 04, 2025 at 11:32:19AM -0500, Sami Imseih wrote:

With a local hash table, I don't think it's necessary to introduce new
code for managing
a DSA based list of tranche names as is done in v3. We can go back to
storing the shared
trance names in dshash.

What do you think?

My first thought is that a per-backend hash table seems too
expensive/complicated for this. Couldn't it just be an array like we have
now?

We can, but I was considering simplicity of implementation, and using a
local hash table is slightly simpler.

That said, since we're dealing with an append-only data structure, a hash
table is probably more than we need. All we need is index-based lookup,
so I’ll go with the local array to mirror the shared ( dsa ) array.

--
Sami

#31Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#30)
1 attachment(s)
Re: Improve LWLock tranche name visibility across backends

With a local hash table, I don't think it's necessary to introduce new
code for managing
a DSA based list of tranche names as is done in v3. We can go back to
storing the shared
trance names in dshash.

What do you think?

My first thought is that a per-backend hash table seems too
expensive/complicated for this. Couldn't it just be an array like we have
now?

We can, but I was considering simplicity of implementation, and using a
local hash table is slightly simpler.

That said, since we're dealing with an append-only data structure, a hash
table is probably more than we need. All we need is index-based lookup,
so I’ll go with the local array to mirror the shared ( dsa ) array.

Here is v4. We can keep using the local array as the backend local array.
It will already include the tranches registered during postmaster startup,
and it will be updated during tranche name lookup from the dsa based
array. I added a new routine UpdateLocalTrancheName which can be used
in both LWLockRegisterTranche and GetLWTrancheName to append to the
local tranche during both postmaster startup and afterwards.

Also, initially I thought we can get rid of returning the "extension" wait event
name, but it's still possible for an "extension" tranche name to be returned
if LWLockInitialize is called with a tranche_id that was never registered
with LWLockNewTrancheId. We don't prevent that, and I can't see how
we can without changing the LWLockInitialize API, but it is something
that should probably be improved. For now, I added a mention of this
behavior in the documentation. This is existing behavior, so I could
also separate this doc change into a separate patch if it makes more
sense to do so.

--
Sami

Attachments:

v4-0001-Implement-a-DSA-for-LWLock-tranche-names.patchapplication/octet-stream; name=v4-0001-Implement-a-DSA-for-LWLock-tranche-names.patchDownload
From ce9e9f777327ab9d1381907851706207c265bb91 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Mon, 21 Jul 2025 19:02:47 -0500
Subject: [PATCH v4 1/1] Implement a DSA for LWLock tranche names

Previously, only LWLock tranches registered during postmaster startup were
available to all backends via a tranche name array inherited through fork.
Other backends that wanted to know about a tranche name had to register it
using LWLockRegisterTranche. While this design worked as intended, it was
not ideal because if a backend did not call LWLockRegisterTranche, it would
show a generic "extension" wait_event name in pg_stat_activity instead of
the actual tranche name. This caused inconsistencies with other backends
that registered the tranche name.

Commit fe07100e82b0 made dynamic creation of named DSA areas (and dshash)
much simpler for a backend, so it is anticipated that fewer tranche names
will be registered during postmaster startup in the future, with more
registered dynamically. This will make the above issue more common.

To address this, this change introduces a DSA to store LWLock tranche names
after postmaster startup. When a backend looks up a tranche name by tranche ID,
it first checks its local cache; if the name is not found locally, it fetches
it from the DSA and caches it for future lookups.

This also simplifies the API for registering tranche names: all registration
work is now done via LWLockNewTrancheId, which accepts a tranche name. The
LWLockRegisterTranche call is no longer required.

Additionally, while users should not pass arbitrary tranche IDs (that is,
IDs not created via LWLockNewTrancheId) to LWLockInitialize, nothing
technically prevents them from doing so. Therefore, we must continue to
handle such cases gracefully by returning a default "extension" tranche name.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   4 +-
 doc/src/sgml/xfunc.sgml                       |  20 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/ipc/ipci.c                |   2 +
 src/backend/storage/lmgr/lwlock.c             | 373 +++++++++++++++---
 .../utils/activity/wait_event_names.txt       |   2 +
 src/backend/utils/init/postinit.c             |   2 +
 src/include/storage/lwlock.h                  |  22 +-
 src/include/storage/lwlocklist.h              |   2 +
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 14 files changed, 358 insertions(+), 112 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..b623589b430 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,8 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
-
 	return found;
 }
 
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 2d81afce8cb..d14061af3be 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3758,9 +3758,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3778,17 +3779,16 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
+      It is possible to use a <literal>tranche_id</literal> that was not retrieved
+      using <function>LWLockNewTrancheId</function>, but this is not recommended.
+      The ID may clash with an already registered tranche name, or the specified
+      name may not be found. In such cases, looking up the name will return a generic
+      "extension" tranche name.
      </para>
 
      <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..c8e4d55cf80 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -343,6 +343,8 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+
+	LWLockTrancheNamesShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index ec9c345ffdf..b12253fdc77 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -80,6 +80,7 @@
 #include "pg_trace.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
+#include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/proclist.h"
 #include "storage/procnumber.h"
@@ -125,9 +126,12 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. Tranche names are registered in LWLockTrancheNames in
+ * two ways: if registration happens during postmaster startup, the names are
+ * stored directly in the local array; if registration happens afterward, the
+ * names are first stored in shared memory and later cached into the local array
+ * during lookup.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -144,14 +148,6 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 				 LWTRANCHE_FIRST_USER_DEFINED,
 				 "missing entries in BuiltinTrancheNames[]");
 
-/*
- * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, 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 **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
-
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
  * the pointer by fork from the postmaster (except in the EXEC_BACKEND case,
@@ -187,6 +183,48 @@ typedef struct NamedLWLockTrancheRequest
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
 static int	NamedLWLockTrancheRequestsAllocated = 0;
 
+typedef struct LWLockTrancheNamesShmem
+{
+	/* pointer to the DSA shared memory, created in-place */
+	void	   *raw_dsa_area;
+	/* DSA Pointer to a list of DSA pointers, each storing a tranche name */
+	dsa_pointer list_ptr;
+	/* number of tranche names stored in shared memory */
+	int			count;
+
+	/*
+	 * The number of slots allocated for tranche names. The allocated size
+	 * will grow geometrically to reduce resizing of the list_ptr.
+	 */
+	int			allocated;
+	/* lock to protect access to the list */
+	LWLock		lock;
+}			LWLockTrancheNamesShmem;
+
+typedef struct LWLockTrancheNamesStruct
+{
+	/*
+	 * Tranche names allocated in shared memory after postmaster startup, when
+	 * dynamic allocation is allowed.
+	 */
+	LWLockTrancheNamesShmem *shmem;
+	dsa_area   *dsa;
+
+	/*
+	 * Tranche names stored in local backend memory, either during postmaster
+	 * startup or as a local cache afterward.
+	 */
+	const char **local;
+	int			allocated;
+}			LWLockTrancheNamesStruct;
+
+static LWLockTrancheNamesStruct LWLockTrancheNames =
+{
+	NULL, NULL, NULL, 0
+};
+
+#define LWLOCK_TRANCHE_NAMES_INIT_SIZE 16
+
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
  * and the length of the shared-memory NamedLWLockTrancheArray later on.
@@ -199,9 +237,14 @@ int			NamedLWLockTrancheRequests = 0;
 NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 
 static void InitializeLWLocks(void);
+static void UpdateLocalTrancheName(int tranche_id, const char *tranche_name);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
 static const char *GetLWTrancheName(uint16 trancheId);
+static void LWLockRegisterTranche(int tranche_id,
+								  const char *tranche_name);
+static void LWLockTrancheNamesAttach(void);
+static void LWLockTrancheNamesDetach(void);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -447,12 +490,12 @@ CreateLWLocks(void)
 
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
-	}
 
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
+		/* Register named extension LWLock tranches in the current process. */
+		for (int i = 0; i < NamedLWLockTrancheRequests; i++)
+			LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
+								  NamedLWLockTrancheArray[i].trancheName);
+	}
 }
 
 /*
@@ -513,7 +556,7 @@ InitializeLWLocks(void)
 			name = trancheNames;
 			trancheNames += strlen(request->tranche_name) + 1;
 			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
+			tranche->trancheId = LWLockNewTrancheId(name);
 			tranche->trancheName = name;
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
@@ -569,61 +612,122 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID and register it with the associated tranche
+ * name.
+ *
+ * The tranche name will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
+	int			tranche_id;
+	int			tranche_index;
 	int		   *LWLockCounter;
 
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 	/* We use the ShmemLock spinlock to protect LWLockCounter */
 	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
+	tranche_id = (*LWLockCounter)++;
 	SpinLockRelease(ShmemLock);
 
-	return result;
+	tranche_index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
+
+	LWLockRegisterTranche(tranche_index, tranche_name);
+
+	return tranche_id;
+}
+
+static void
+UpdateLocalTrancheName(int tranche_id, const char *tranche_name)
+{
+	int			newalloc;
+
+	/* If necessary, create or enlarge array. */
+	if (tranche_id >= LWLockTrancheNames.allocated)
+	{
+		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+
+		if (LWLockTrancheNames.local == NULL)
+		{
+			LWLockTrancheNames.local = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   newalloc * sizeof(char *));
+		}
+		else
+		{
+			LWLockTrancheNames.local =
+				repalloc0_array(LWLockTrancheNames.local, const char *, LWLockTrancheNames.allocated, newalloc);
+		}
+
+		LWLockTrancheNames.allocated = newalloc;
+	}
+
+	LWLockTrancheNames.local[tranche_id] = tranche_name;
 }
 
 /*
  * Register a dynamic tranche name in the lookup table of the current process.
  *
  * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
+ * so the name must be allocated in a backend-lifetime context, either shared
+ * memory or TopMemoryContext.
  */
-void
+static void
 LWLockRegisterTranche(int tranche_id, const char *tranche_name)
 {
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	int			newalloc;
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
-
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (!IsUnderPostmaster)
+		UpdateLocalTrancheName(tranche_id, tranche_name);
+	else
 	{
-		int			newalloc;
+		dsa_pointer *current_ptrs;
+		dsa_pointer new_list_ptr;
+		dsa_pointer *new_name_ptrs;
+		size_t		len;
+		dsa_pointer str_ptr;
+		char	   *str_addr;
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+		LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
-	}
+		/*
+		 * The first time adding a name to the shmem, so let's allocate the
+		 * list now.
+		 */
+		if (LWLockTrancheNames.shmem->list_ptr == InvalidDsaPointer)
+		{
+			LWLockTrancheNames.shmem->list_ptr = dsa_allocate(LWLockTrancheNames.dsa,
+															  LWLOCK_TRANCHE_NAMES_INIT_SIZE * sizeof(dsa_pointer));
+			LWLockTrancheNames.shmem->allocated = LWLOCK_TRANCHE_NAMES_INIT_SIZE;
+		}
+
+		current_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+		/* Resize if needed */
+		if (LWLockTrancheNames.shmem->count >= LWLockTrancheNames.shmem->allocated)
+		{
+			newalloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE, LWLockTrancheNames.shmem->allocated + 1));
+			new_list_ptr = dsa_allocate(LWLockTrancheNames.dsa, newalloc * sizeof(dsa_pointer));
+			new_name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list_ptr);
+			memcpy(new_name_ptrs, current_ptrs, LWLockTrancheNames.shmem->allocated * sizeof(dsa_pointer));
+			dsa_free(LWLockTrancheNames.dsa, *current_ptrs);
+			LWLockTrancheNames.shmem->list_ptr = new_list_ptr;
+			LWLockTrancheNames.shmem->allocated = newalloc;
+			current_ptrs = new_name_ptrs;
+		}
+
+		/* Allocate and copy string */
+		len = strlen(tranche_name) + 1;
+		str_ptr = dsa_allocate(LWLockTrancheNames.dsa, len);
+		str_addr = dsa_get_address(LWLockTrancheNames.dsa, str_ptr);
+		memcpy(str_addr, tranche_name, len);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+		current_ptrs[tranche_id] = str_ptr;
+		LWLockTrancheNames.shmem->count++;
+
+		LWLockRelease(&LWLockTrancheNames.shmem->lock);
+	}
 }
 
 /*
@@ -708,28 +812,69 @@ LWLockReportWaitEnd(void)
 	pgstat_report_wait_end();
 }
 
+
+static const char *
+GetLWTrancheNameByID(int tranche_id)
+{
+	dsa_pointer *pointers;
+	char	   *str = NULL;
+
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_SHARED);
+
+	pointers = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+	str = dsa_get_address(LWLockTrancheNames.dsa, pointers[tranche_id]);
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+
+	return str;
+}
+
 /*
  * Return the name of an LWLock tranche.
  */
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	const char *tranche_name = NULL;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in LWLockTrancheNames[]; first
+	 * locally and then in shared memory.  However, it's possible that the
+	 * tranche has never been registered (with LWLockNewTrancheId), in which
+	 * case give up and return "extension".
+	 *
+	 * XXX: It might be worth enforcing a call to LWLockNewTrancheId before
+	 * allowing LWLock initialization with a user-defined tranche.
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (trancheId < LWLockTrancheNames.allocated)
+	{
+		/*
+		 * First, check if the tranche name is already cached locally. If so,
+		 * return it directly.
+		 */
+		tranche_name = LWLockTrancheNames.local[trancheId];
 
-	return LWLockTrancheNames[trancheId];
+		/*
+		 * If not cached, try fetching the name from shared memory. If found,
+		 * store it in the local cache for future lookups.
+		 */
+		if (tranche_name == NULL)
+		{
+			tranche_name = GetLWTrancheNameByID(trancheId);
+			if (tranche_name != NULL)
+				UpdateLocalTrancheName(trancheId, tranche_name);
+		}
+	}
+
+	/* Return the found name, or "extension" if not found */
+	return tranche_name ? tranche_name : "extension";
 }
 
 /*
@@ -1995,3 +2140,121 @@ LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode)
 	}
 	return false;
 }
+
+static Size
+LWLockTrancheNamesInitSize()
+{
+	Size		sz;
+
+	sz = 256 * 1024;
+
+	Assert(dsa_minimum_size() <= sz);
+
+	return MAXALIGN(sz);
+}
+
+Size
+LWLockTrancheNamesShmemSize(void)
+{
+	Size		sz;
+
+	sz = MAXALIGN(sizeof(LWLockTrancheNamesStruct));
+	sz = add_size(sz, LWLockTrancheNamesInitSize());
+
+	return sz;
+}
+
+void
+LWLockTrancheNamesShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize or attach to shared memory structure */
+	LWLockTrancheNames.shmem = (LWLockTrancheNamesShmem *)
+		ShmemInitStruct("Dynamic LWLock tranche names",
+						LWLockTrancheNamesShmemSize(),
+						&found);
+
+	if (!IsUnderPostmaster)
+	{
+		dsa_area   *dsa;
+		LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+		char	   *p = (char *) ctl;
+
+		/* Calculate layout within the shared memory region */
+		p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+		ctl->raw_dsa_area = p;
+		p += MAXALIGN(LWLockTrancheNamesInitSize());
+
+		LWLockInitialize(&ctl->lock, LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST);
+
+		/* Create a dynamic shared memory area in-place */
+		dsa = dsa_create_in_place(ctl->raw_dsa_area,
+								  LWLockTrancheNamesInitSize(),
+								  LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA,
+								  NULL);
+
+		/*
+		 * Postmaster will never access these again, thus free the local
+		 * dsa/dshash references.
+		 */
+		dsa_pin(dsa);
+		dsa_detach(dsa);
+
+		ctl->allocated = 0;
+		ctl->count = 0;
+		ctl->list_ptr = InvalidDsaPointer;
+	}
+	else
+	{
+		Assert(found);
+	}
+}
+
+static void
+LWLockTrancheNamesAttach(void)
+{
+	MemoryContext oldcontext;
+
+	Assert(LWLockTrancheNames.dsa == NULL);
+
+	/* stats shared memory persists for the backend lifetime */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	LWLockTrancheNames.dsa = dsa_attach_in_place(LWLockTrancheNames.shmem->raw_dsa_area,
+												 NULL);
+	dsa_pin_mapping(LWLockTrancheNames.dsa);
+
+	Assert(LWLockTrancheNames.dsa);
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+static void
+LWLockTrancheNamesDetach(void)
+{
+	Assert(LWLockTrancheNames.dsa);
+
+	dsa_detach(LWLockTrancheNames.dsa);
+
+	dsa_release_in_place(LWLockTrancheNames.shmem->raw_dsa_area);
+
+	LWLockTrancheNames.dsa = NULL;
+}
+
+static void
+LWLockTrancheNamesShutdownHook(int code, Datum arg)
+{
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	LWLockTrancheNamesDetach();
+}
+
+void
+LWLockTrancheNamesBEInit(void)
+{
+	LWLockTrancheNamesAttach();
+
+	/* Set up a process-exit hook to clean up */
+	before_shmem_exit(LWLockTrancheNamesShutdownHook, 0);
+}
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0be307d2ca0..f60ed927892 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -406,6 +406,8 @@ SubtransSLRU	"Waiting to access the sub-transaction SLRU cache."
 XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
+LWLockDynamicTranchesDSA	"Waiting to access the LWLock tranche names DSA area."
+LWLockDynamicTranchesList	"Waiting to access the list of LWLock tranche names in DSA."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 641e535a73c..1ef04c6b5ec 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -662,6 +662,8 @@ BaseInit(void)
 	 * drop ephemeral slots, which in turn triggers stats reporting.
 	 */
 	ReplicationSlotInitialize();
+
+	LWLockTrancheNamesBEInit();
 }
 
 
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..8a008fc67dd 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -156,22 +156,18 @@ extern void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
- * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * An alternative, more flexible method of obtaining lwlocks is available.
+ * First, call LWLockNewTrancheId once, providing an associated tranche name.
+ * This function allocates a tranche ID from a shared counter. Then, for each
+ * lwlock, call LWLockInitialize once, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
+extern void LWLockTrancheNamesShmemInit(void);
+extern Size LWLockTrancheNamesShmemSize(void);
+extern void LWLockTrancheNamesBEInit(void);
+
 /*
  * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
  * we reserve additional tranche IDs for builtin tranches not included in
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 208d2e3a8ed..5978648c09d 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -135,3 +135,5 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU)
 PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
 PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
 PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_DSA, LWLockDynamicTranchesDSA)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_LIST, LWLockDynamicTranchesList)
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 32de6a3123e..4ae826d08a0 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

#32Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#31)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Mon, Aug 04, 2025 at 10:47:45PM -0500, Sami Imseih wrote:

With a local hash table, I don't think it's necessary to introduce new
code for managing
a DSA based list of tranche names as is done in v3. We can go back to
storing the shared
trance names in dshash.

What do you think?

My first thought is that a per-backend hash table seems too
expensive/complicated for this. Couldn't it just be an array like we have
now?

We can, but I was considering simplicity of implementation, and using a
local hash table is slightly simpler.

That said, since we're dealing with an append-only data structure, a hash
table is probably more than we need. All we need is index-based lookup,
so I’ll go with the local array to mirror the shared ( dsa ) array.

+1 for the local array.

Here is v4.

Thanks for the patch update!

We can keep using the local array as the backend local array.
It will already include the tranches registered during postmaster startup,
and it will be updated during tranche name lookup from the dsa based
array.

I added a new routine UpdateLocalTrancheName which can be used
in both LWLockRegisterTranche and GetLWTrancheName to append to the
local tranche during both postmaster startup and afterwards.

I did not look at the code in details, just played a bit with it and found
some issues.

Issue 1 --

If I register enough tranches to go to:

+               /* Resize if needed */
+               if (LWLockTrancheNames.shmem->count >= LWLockTrancheNames.shmem->allocated)
+               {
+                       newalloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE, LWLockTrancheNames.shmem->allocated + 1));
+                       new_list_ptr = dsa_allocate(LWLockTrancheNames.dsa, newalloc * sizeof(dsa_pointer));
+                       new_name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list_ptr);
+                       memcpy(new_name_ptrs, current_ptrs, LWLockTrancheNames.shmem->allocated * sizeof(dsa_pointer));
+                       dsa_free(LWLockTrancheNames.dsa, *current_ptrs);

then I get:

ERROR: dsa_area could not attach to a segment that has been freed

I think we should

"
dsa_free(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr)
"

instead.

Issue 2 --

If an extension calls RequestNamedLWLockTranche() it will register the same
tranche twice:

(gdb) p LWLockTrancheNames.local[0]
$1 = 0x7acf5a40c420 "pg_playmem"
(gdb) p LWLockTrancheNames.local[97]
$2 = 0x7acf5a40c420 "pg_playmem"

First (local[0]) during LWLockNewTrancheId() during InitializeLWLocks()
Second (local[97]) during LWLockRegisterTranche() during CreateLWLocks()

Maybe we should put this back?

- /* This should only be called for user-defined tranches. */
- if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
- return;

- /* Convert to array index. */
- tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;

and remove:

+ tranche_index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;

from LWLockNewTrancheId()?

Regards,

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

#33Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#32)
Re: Improve LWLock tranche name visibility across backends

Thanks for reviewing!

Issue 1 --

If I register enough tranches to go to:

+               /* Resize if needed */
+               if (LWLockTrancheNames.shmem->count >= LWLockTrancheNames.shmem->allocated)
+               {
+                       newalloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE, LWLockTrancheNames.shmem->allocated + 1));
+                       new_list_ptr = dsa_allocate(LWLockTrancheNames.dsa, newalloc * sizeof(dsa_pointer));
+                       new_name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list_ptr);
+                       memcpy(new_name_ptrs, current_ptrs, LWLockTrancheNames.shmem->allocated * sizeof(dsa_pointer));
+                       dsa_free(LWLockTrancheNames.dsa, *current_ptrs);

then I get:

ERROR: dsa_area could not attach to a segment that has been freed

Will investigate this one and correct in the next rev.

Issue 2 --

If an extension calls RequestNamedLWLockTranche() it will register the same
tranche twice:

(gdb) p LWLockTrancheNames.local[0]
$1 = 0x7acf5a40c420 "pg_playmem"
(gdb) p LWLockTrancheNames.local[97]
$2 = 0x7acf5a40c420 "pg_playmem"

Thanks for catching. This one is clear, there is an extra call to
register inside RequestNamedLWLockTranche.
I'll fix this in the next rev.

--
Sami

#34Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#33)
1 attachment(s)
Re: Improve LWLock tranche name visibility across backends

I'll fix this in the next rev.

v5 fixes the above 2 issues found above.

For the issue that was throwing ".... segment that has been freed",
indeed we should be freeing LWLockTrancheNames.shmem->list_ptr,
list_ptr is a dsa_pointer that stores an array of dsa_pointers, which
then made me realize that I was not freeing the actual dsa_pointers
holding the tranche names. I fixed that as well.

I also made some improvements to the code that copies the old
list to the new list and fixed the lookup in
GetLWTrancheName.

--
Sami

Attachments:

v5-0001-Implement-a-DSA-for-LWLock-tranche-names.patchapplication/octet-stream; name=v5-0001-Implement-a-DSA-for-LWLock-tranche-names.patchDownload
From 59fe1de71ab57bf8f5a87f7137066f0914f9ef45 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Mon, 21 Jul 2025 19:02:47 -0500
Subject: [PATCH v5 1/1] Implement a DSA for LWLock tranche names

Previously, only LWLock tranches registered during postmaster startup were
available to all backends via a tranche name array inherited through fork.
Other backends that wanted to know about a tranche name had to register it
using LWLockRegisterTranche. While this design worked as intended, it was
not ideal because if a backend did not call LWLockRegisterTranche, it would
show a generic "extension" wait_event name in pg_stat_activity instead of
the actual tranche name. This caused inconsistencies with other backends
that registered the tranche name.

Commit fe07100e82b0 made dynamic creation of named DSA areas (and dshash)
much simpler for a backend, so it is anticipated that fewer tranche names
will be registered during postmaster startup in the future, with more
registered dynamically. This will make the above issue more common.

To address this, this change introduces a DSA to store LWLock tranche names
after postmaster startup. When a backend looks up a tranche name by tranche ID,
it first checks its local cache; if the name is not found locally, it fetches
it from the DSA and caches it for future lookups.

This also simplifies the API for registering tranche names: all registration
work is now done via LWLockNewTrancheId, which accepts a tranche name. The
LWLockRegisterTranche call is no longer required.

Additionally, while users should not pass arbitrary tranche IDs (that is,
IDs not created via LWLockNewTrancheId) to LWLockInitialize, nothing
technically prevents them from doing so. Therefore, we must continue to
handle such cases gracefully by returning a default "extension" tranche name.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   4 +-
 doc/src/sgml/xfunc.sgml                       |  20 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/ipc/ipci.c                |   2 +
 src/backend/storage/lmgr/lwlock.c             | 387 +++++++++++++++---
 .../utils/activity/wait_event_names.txt       |   2 +
 src/backend/utils/init/postinit.c             |   2 +
 src/include/storage/lwlock.h                  |  22 +-
 src/include/storage/lwlocklist.h              |   2 +
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 14 files changed, 372 insertions(+), 112 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..b623589b430 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,8 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
-
 	return found;
 }
 
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 30219f432d9..32ea6c87be8 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3758,9 +3758,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3778,17 +3779,16 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
+      It is possible to use a <literal>tranche_id</literal> that was not retrieved
+      using <function>LWLockNewTrancheId</function>, but this is not recommended.
+      The ID may clash with an already registered tranche name, or the specified
+      name may not be found. In such cases, looking up the name will return a generic
+      "extension" tranche name.
      </para>
 
      <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..c8e4d55cf80 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -343,6 +343,8 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+
+	LWLockTrancheNamesShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index ec9c345ffdf..c86693aab09 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -80,6 +80,7 @@
 #include "pg_trace.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
+#include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/proclist.h"
 #include "storage/procnumber.h"
@@ -125,9 +126,12 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. Tranche names are registered in LWLockTrancheNames in
+ * two ways: if registration happens during postmaster startup, the names are
+ * stored directly in the local array; if registration happens afterward, the
+ * names are first stored in shared memory and later cached into the local array
+ * during lookup.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -144,14 +148,6 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 				 LWTRANCHE_FIRST_USER_DEFINED,
 				 "missing entries in BuiltinTrancheNames[]");
 
-/*
- * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, 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 **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
-
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
  * the pointer by fork from the postmaster (except in the EXEC_BACKEND case,
@@ -187,6 +183,46 @@ typedef struct NamedLWLockTrancheRequest
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
 static int	NamedLWLockTrancheRequestsAllocated = 0;
 
+typedef struct LWLockTrancheNamesShmem
+{
+	/* pointer to the DSA shared memory, created in-place */
+	void	   *raw_dsa_area;
+	/* DSA Pointer to a list of DSA pointers, each storing a tranche name */
+	dsa_pointer list_ptr;
+
+	/*
+	 * The number of slots allocated for tranche names. The allocated size
+	 * will grow geometrically to reduce resizing of the list_ptr.
+	 */
+	int			allocated;
+	/* lock to protect access to the list */
+	LWLock		lock;
+}			LWLockTrancheNamesShmem;
+
+typedef struct LWLockTrancheNamesStruct
+{
+	/*
+	 * Tranche names allocated in shared memory after postmaster startup, when
+	 * dynamic allocation is allowed.
+	 */
+	LWLockTrancheNamesShmem *shmem;
+	dsa_area   *dsa;
+
+	/*
+	 * Tranche names stored in local backend memory, either during postmaster
+	 * startup or as a local cache afterward.
+	 */
+	const char **local;
+	int			allocated;
+}			LWLockTrancheNamesStruct;
+
+static LWLockTrancheNamesStruct LWLockTrancheNames =
+{
+	NULL, NULL, NULL, 0
+};
+
+#define LWLOCK_TRANCHE_NAMES_INIT_SIZE 16
+
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
  * and the length of the shared-memory NamedLWLockTrancheArray later on.
@@ -201,7 +237,11 @@ NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
+static void SetLocalTrancheName(int tranche_id, const char *tranche_name);
+static void SetSharedTrancheName(int tranche_id, const char *tranche_name);
 static const char *GetLWTrancheName(uint16 trancheId);
+static void LWLockTrancheNamesAttach(void);
+static void LWLockTrancheNamesDetach(void);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -448,11 +488,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -513,7 +548,7 @@ InitializeLWLocks(void)
 			name = trancheNames;
 			trancheNames += strlen(request->tranche_name) + 1;
 			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
+			tranche->trancheId = LWLockNewTrancheId(name);
 			tranche->trancheName = name;
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
@@ -569,61 +604,153 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID and register it with the associated tranche
+ * name.
+ *
+ * The tranche name will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
+	int			tranche_id;
+	int			tranche_index;
 	int		   *LWLockCounter;
 
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 	/* We use the ShmemLock spinlock to protect LWLockCounter */
 	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
+	tranche_id = (*LWLockCounter)++;
 	SpinLockRelease(ShmemLock);
 
-	return result;
+	tranche_index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
+
+	if (!IsUnderPostmaster)
+		SetLocalTrancheName(tranche_index, tranche_name);
+	else
+		SetSharedTrancheName(tranche_index, tranche_name);
+
+	return tranche_id;
 }
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
+static void
+SetSharedTrancheName(int tranche_id, const char *tranche_name)
 {
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	dsa_pointer *name_ptrs;
+	dsa_pointer str_ptr;
+	char	   *str_addr;
+	int			len;
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	/* Tranche array does not exist, initialize it */
+	if (!DsaPointerIsValid(LWLockTrancheNames.shmem->list_ptr))
 	{
-		int			newalloc;
+		int			alloc = LWLOCK_TRANCHE_NAMES_INIT_SIZE;
+
+		LWLockTrancheNames.shmem->allocated = alloc;
+		LWLockTrancheNames.shmem->list_ptr =
+			dsa_allocate(LWLockTrancheNames.dsa, alloc * sizeof(dsa_pointer));
 
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+		for (int i = 0; i < alloc; i++)
+			name_ptrs[i] = InvalidDsaPointer;
+	}
+
+	/*
+	 * We need to enlarge the tranche array. So, we deep copy the old tranche
+	 * names into a new list and free the old list. The shared memory is
+	 * updated to point to the new list.
+	 */
+	else if (tranche_id >= LWLockTrancheNames.shmem->allocated)
+	{
+		int			newalloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE,
+													tranche_id + 1));
+		dsa_pointer new_list = dsa_allocate(LWLockTrancheNames.dsa,
+											newalloc * sizeof(dsa_pointer));
+		dsa_pointer *old_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+												LWLockTrancheNames.shmem->list_ptr);
+		dsa_pointer *new_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list);
+
+		/* Copy existing names into new list, freeing old ones. */
+		for (int i = 0; i < newalloc; i++)
+		{
+			if (i < LWLockTrancheNames.shmem->allocated &&
+				DsaPointerIsValid(old_ptrs[i]))
+			{
+				char	   *old_name = (char *) dsa_get_address(LWLockTrancheNames.dsa, old_ptrs[i]);
+				dsa_pointer copied_ptr;
+				char	   *copied_addr;
+
+				len = strlen(old_name) + 1;
+				copied_ptr = dsa_allocate(LWLockTrancheNames.dsa, len);
+
+				copied_addr = dsa_get_address(LWLockTrancheNames.dsa, copied_ptr);
+				memcpy(copied_addr, old_name, len);
+
+				new_ptrs[i] = copied_ptr;
+
+				/* free old tranche names */
+				dsa_free(LWLockTrancheNames.dsa, old_ptrs[i]);
+			}
+			else
+			{
+				new_ptrs[i] = InvalidDsaPointer;
+			}
+		}
+
+		/* Replace old list */
+		dsa_free(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+		LWLockTrancheNames.shmem->list_ptr = new_list;
+		LWLockTrancheNames.shmem->allocated = newalloc;
+
+		name_ptrs = new_ptrs;
+	}
+	else
+	{
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+	}
+
+	/* Replace existing name for tranche_id, freeing if present */
+	if (DsaPointerIsValid(name_ptrs[tranche_id]))
+		dsa_free(LWLockTrancheNames.dsa, name_ptrs[tranche_id]);
+
+	/* Allocate and copy new string */
+	len = strlen(tranche_name) + 1;
+	str_ptr = dsa_allocate(LWLockTrancheNames.dsa, len);
+	str_addr = dsa_get_address(LWLockTrancheNames.dsa, str_ptr);
+	memcpy(str_addr, tranche_name, len);
+	name_ptrs[tranche_id] = str_ptr;
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
+static void
+SetLocalTrancheName(int tranche_id, const char *tranche_name)
+{
+	int			newalloc;
+
+	/* If necessary, create or enlarge array. */
+	if (tranche_id >= LWLockTrancheNames.allocated)
+	{
 		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
+		if (LWLockTrancheNames.local == NULL)
+		{
+			LWLockTrancheNames.local = (const char **)
 				MemoryContextAllocZero(TopMemoryContext,
 									   newalloc * sizeof(char *));
+		}
 		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+		{
+			LWLockTrancheNames.local =
+				repalloc0_array(LWLockTrancheNames.local, const char *, LWLockTrancheNames.allocated, newalloc);
+		}
+
+		LWLockTrancheNames.allocated = newalloc;
 	}
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	LWLockTrancheNames.local[tranche_id] = tranche_name;
 }
 
 /*
@@ -708,28 +835,61 @@ LWLockReportWaitEnd(void)
 	pgstat_report_wait_end();
 }
 
+
+static const char *
+GetLWTrancheNameByID(int tranche_id)
+{
+	dsa_pointer *pointers;
+	char	   *str = NULL;
+
+	if (tranche_id >= LWLockTrancheNames.shmem->allocated)
+		return NULL;
+
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_SHARED);
+
+	pointers = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+	str = dsa_get_address(LWLockTrancheNames.dsa, pointers[tranche_id]);
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+
+	return str;
+}
+
 /*
  * Return the name of an LWLock tranche.
  */
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	const char *tranche_name = NULL;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in LWLockTrancheNames[]. If the
+	 * trancheId is higher than our locally cached one, look it up in shared
+	 * memory and update the local cache. Otherwise, check the local cache.
+	 * However, it is also possible that the tranche has never been registered
+	 * (with LWLockNewTrancheId), in which case we give up and return
+	 * "extension".
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (trancheId >= LWLockTrancheNames.allocated)
+	{
+		tranche_name = GetLWTrancheNameByID(trancheId);
 
-	return LWLockTrancheNames[trancheId];
+		if (tranche_name != NULL)
+			SetLocalTrancheName(trancheId, tranche_name);
+	}
+	else
+		tranche_name = LWLockTrancheNames.local[trancheId];
+
+	/* Return the found name, or "extension" if not found */
+	return tranche_name ? tranche_name : "extension";
 }
 
 /*
@@ -1995,3 +2155,120 @@ LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode)
 	}
 	return false;
 }
+
+static Size
+LWLockTrancheNamesInitSize()
+{
+	Size		sz;
+
+	sz = 256 * 1024;
+
+	Assert(dsa_minimum_size() <= sz);
+
+	return MAXALIGN(sz);
+}
+
+Size
+LWLockTrancheNamesShmemSize(void)
+{
+	Size		sz;
+
+	sz = MAXALIGN(sizeof(LWLockTrancheNamesStruct));
+	sz = add_size(sz, LWLockTrancheNamesInitSize());
+
+	return sz;
+}
+
+void
+LWLockTrancheNamesShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize or attach to shared memory structure */
+	LWLockTrancheNames.shmem = (LWLockTrancheNamesShmem *)
+		ShmemInitStruct("Dynamic LWLock tranche names",
+						LWLockTrancheNamesShmemSize(),
+						&found);
+
+	if (!IsUnderPostmaster)
+	{
+		dsa_area   *dsa;
+		LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+		char	   *p = (char *) ctl;
+
+		/* Calculate layout within the shared memory region */
+		p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+		ctl->raw_dsa_area = p;
+		p += MAXALIGN(LWLockTrancheNamesInitSize());
+
+		LWLockInitialize(&ctl->lock, LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST);
+
+		/* Create a dynamic shared memory area in-place */
+		dsa = dsa_create_in_place(ctl->raw_dsa_area,
+								  LWLockTrancheNamesInitSize(),
+								  LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA,
+								  NULL);
+
+		/*
+		 * Postmaster will never access these again, thus free the local
+		 * dsa/dshash references.
+		 */
+		dsa_pin(dsa);
+		dsa_detach(dsa);
+
+		ctl->allocated = 0;
+		ctl->list_ptr = InvalidDsaPointer;
+	}
+	else
+	{
+		Assert(found);
+	}
+}
+
+static void
+LWLockTrancheNamesAttach(void)
+{
+	MemoryContext oldcontext;
+
+	Assert(LWLockTrancheNames.dsa == NULL);
+
+	/* stats shared memory persists for the backend lifetime */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	LWLockTrancheNames.dsa = dsa_attach_in_place(LWLockTrancheNames.shmem->raw_dsa_area,
+												 NULL);
+	dsa_pin_mapping(LWLockTrancheNames.dsa);
+
+	Assert(LWLockTrancheNames.dsa);
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+static void
+LWLockTrancheNamesDetach(void)
+{
+	Assert(LWLockTrancheNames.dsa);
+
+	dsa_detach(LWLockTrancheNames.dsa);
+
+	dsa_release_in_place(LWLockTrancheNames.shmem->raw_dsa_area);
+
+	LWLockTrancheNames.dsa = NULL;
+}
+
+static void
+LWLockTrancheNamesShutdownHook(int code, Datum arg)
+{
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	LWLockTrancheNamesDetach();
+}
+
+void
+LWLockTrancheNamesBEInit(void)
+{
+	LWLockTrancheNamesAttach();
+
+	/* Set up a process-exit hook to clean up */
+	before_shmem_exit(LWLockTrancheNamesShutdownHook, 0);
+}
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0be307d2ca0..f60ed927892 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -406,6 +406,8 @@ SubtransSLRU	"Waiting to access the sub-transaction SLRU cache."
 XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
+LWLockDynamicTranchesDSA	"Waiting to access the LWLock tranche names DSA area."
+LWLockDynamicTranchesList	"Waiting to access the list of LWLock tranche names in DSA."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 641e535a73c..1ef04c6b5ec 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -662,6 +662,8 @@ BaseInit(void)
 	 * drop ephemeral slots, which in turn triggers stats reporting.
 	 */
 	ReplicationSlotInitialize();
+
+	LWLockTrancheNamesBEInit();
 }
 
 
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..8a008fc67dd 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -156,22 +156,18 @@ extern void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
- * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * An alternative, more flexible method of obtaining lwlocks is available.
+ * First, call LWLockNewTrancheId once, providing an associated tranche name.
+ * This function allocates a tranche ID from a shared counter. Then, for each
+ * lwlock, call LWLockInitialize once, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
+extern void LWLockTrancheNamesShmemInit(void);
+extern Size LWLockTrancheNamesShmemSize(void);
+extern void LWLockTrancheNamesBEInit(void);
+
 /*
  * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
  * we reserve additional tranche IDs for builtin tranches not included in
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 208d2e3a8ed..5978648c09d 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -135,3 +135,5 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU)
 PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
 PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
 PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_DSA, LWLockDynamicTranchesDSA)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_LIST, LWLockDynamicTranchesList)
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 32de6a3123e..4ae826d08a0 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

#35Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#34)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Tue, Aug 05, 2025 at 11:24:04PM -0500, Sami Imseih wrote:

I'll fix this in the next rev.

v5 fixes the above 2 issues found above.

Thanks!

For the issue that was throwing ".... segment that has been freed",
indeed we should be freeing LWLockTrancheNames.shmem->list_ptr,

Yeah.

list_ptr is a dsa_pointer that stores an array of dsa_pointers, which
then made me realize that I was not freeing the actual dsa_pointers
holding the tranche names. I fixed that as well.

I also made some improvements to the code that copies the old
list to the new list and fixed the lookup in
GetLWTrancheName.

Still doing a bit of testing without looking closely to the code.

Issue 1 --

If I:

LWLockInitialize(&lck, LWLockNewTrancheId("BDT_Lck"));
LWLockAcquire(&lck, LW_EXCLUSIVE);
LWLockAcquire(&lck, LW_EXCLUSIVE);

So that this process is locked on the LWLock BDT_Lck and if I query pg_stat_activity
in another backend, then I get:

postgres=# select wait_event from pg_stat_activity where wait_event_type = 'LWLock';
wait_event
------------
BDT_Lck
(1 row)

which is what we expect.

But if I look at LWLockTrancheNames.local (for the process that queried pg_stat_activity):

(gdb) p LWLockTrancheNames.local[0]
$1 = 0x0
(gdb) p LWLockTrancheNames.local[1]
$2 = 0x7b759a81b038 "BDT_Lck"

It looks like there is an indexing issue, as we should start at index 0.

Issue 2 / question --

If I call LWLockNewTrancheId("BDT_play2") multiple times to ensure that

trancheId >= LWLockTrancheNames.allocated and then ensure that a backend is
locked doing:

"
LWLockInitialize(&lck, LWLockNewTrancheId("BDT_Lck"));
LWLockAcquire(&lck, LW_EXCLUSIVE);
LWLockAcquire(&lck, LW_EXCLUSIVE);
"

Then if, in another backend, I call GetLWTrancheName by querying pg_stat_activity
then I see "BDT_Lck" being reported as wait event name (which is good).

The thing that worries me a bit is that the local cache is populated only for
"BDT_Lck", but not for all the other "BDT_play2".

(gdb) p LWLockTrancheNames.local[2]
$9 = 0x0
(gdb) p LWLockTrancheNames.local[3]
$10 = 0x0
(gdb) p LWLockTrancheNames.local[12]
$11 = 0x0
(gdb) p LWLockTrancheNames.local[13]
$12 = 0x780593a1b138 "BDT_Lck"

When we need to populate the local cache, would it be better to populate
it with the whole dsa content instead of just the current missing ID as done
in GetLWTrancheName():

+               if (tranche_name != NULL)
+                       SetLocalTrancheName(trancheId, tranche_name);?

I think that would make more sense, as that would avoid to take the
LWLockTrancheNames.shmem->lock in GetLWTrancheNameByID() multiple times
should the wait event change, thoughts?

Issue 3 --

If I call LWLockNewTrancheId("BDT_play2") only one time to ensure that
trancheId < LWLockTrancheNames.allocated and then ensure that a backend is
locked doing:

"
LWLockInitialize(&lck, LWLockNewTrancheId("BDT_Lck"));
LWLockAcquire(&lck, LW_EXCLUSIVE);
LWLockAcquire(&lck, LW_EXCLUSIVE);
"

Then if, in another backend, I call GetLWTrancheName by querying pg_stat_activity
then I see "extension" being reported as wait event name (which is not good).

The issue is that in GetLWTrancheName(), the patch does:

+       if (trancheId >= LWLockTrancheNames.allocated)
+       {
+               tranche_name = GetLWTrancheNameByID(trancheId);
+               if (tranche_name != NULL)
+                       SetLocalTrancheName(trancheId, tranche_name);
+       }
+       else
+               tranche_name = LWLockTrancheNames.local[trancheId];

Then does not find the lock in the local cache and then returns "extension".

I think that in that case we should fall back to querying the DSA (and
populate the local cache), thoughts?

Regards,

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

#36Rahila Syed
rahilasyed90@gmail.com
In reply to: Bertrand Drouvot (#35)
Re: Improve LWLock tranche name visibility across backends

Hi,

I've begun reviewing this patch and have a few questions listed below:

1. + if (i < LWLockTrancheNames.shmem->allocated &&
DsaPointerIsValid(old_ptrs[i]))

Should an assert be used for the second condition instead?
Since for i < LWLockTrancheNames.shmem->allocated, the dsa pointer is
expected to be valid.

2.                                copied_ptr =
dsa_allocate(LWLockTrancheNames.dsa, len);
+
+                               copied_addr =
dsa_get_address(LWLockTrancheNames.dsa, copied_ptr);
+                               memcpy(copied_addr, old_name, len);
+
+                               new_ptrs[i] = copied_ptr;
+
+                               /* free old tranche names */
+                               dsa_free(LWLockTrancheNames.dsa,
old_ptrs[i]);

Why is it necessary to allocate a new dsa_pointer for tranche names that
are the same size and then
free the old one?
Is there a reason we can't just assign new_ptrs[i] = old_ptrs[i]?

3.

Additionally, while users should not pass arbitrary tranche IDs (that is,
IDs not created via LWLockNewTrancheId) to LWLockInitialize, nothing
technically prevents them from doing so. Therefore, we must continue to
handle such cases gracefully by returning a default "extension" tranche

name.

Would it be possible to update LWLockInitialize so that it checks if
tranche_id is
already registered in the dsa, and if not, registers it during the
LWLockInitialize() process?

Thank you,
Rahila Syed

#37Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#35)
Re: Improve LWLock tranche name visibility across backends

Thanks for testing!

which is what we expect.

But if I look at LWLockTrancheNames.local (for the process that queried pg_stat_activity):

(gdb) p LWLockTrancheNames.local[0]
$1 = 0x0
(gdb) p LWLockTrancheNames.local[1]
$2 = 0x7b759a81b038 "BDT_Lck"

It looks like there is an indexing issue, as we should start at index 0.

Issue 2 / question --

If I call LWLockNewTrancheId("BDT_play2") multiple times to ensure that

Then if, in another backend, I call GetLWTrancheName by querying pg_stat_activity
then I see "BDT_Lck" being reported as wait event name (which is good).

The thing that worries me a bit is that the local cache is populated only for
"BDT_Lck", but not for all the other "BDT_play2".

Issue #1 and issue #2 are related. In both cases you have a registered wait
event that only lives in shared memory, but was never required to be
accessed by pg_stat_activity, so it was never cached locally.

When we need to populate the local cache, would it be better to populate
it with the whole dsa content instead of just the current missing ID as done
in GetLWTrancheName():

I do agree that we should just sync the local cache totally whenever we
can't find the tranche name in the local cache. Will fix in the next rev.

--
Sami

#38Sami Imseih
samimseih@gmail.com
In reply to: Rahila Syed (#36)
Re: Improve LWLock tranche name visibility across backends

Thanks for testing!

Why is it necessary to allocate a new dsa_pointer for tranche names that are the same size and then
free the old one?
Is there a reason we can't just assign new_ptrs[i] = old_ptrs[i]?

Fair point. I will updated in the next rev. We don't need to free the'
existing tranche name pointers, only the list.

Would it be possible to update LWLockInitialize so that it checks if tranche_id is
already registered in the dsa, and if not, registers it during the LWLockInitialize() process?

We could. I do think this will need a separate discussion as a follow-up to this
thread.

--
Sami

#39Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#38)
Re: Improve LWLock tranche name visibility across backends

From this discussion, and the fact that the tranche name could come from
either local or shared memory, I think we should have tests.
So, I am working on adding tests using INJECTION_POINTS.

Some of the tests I have in mind now are:

1/ Does the shared memory grow correctly,
2/ Is the tranche name being returned the correct one
3/ Is the local cache being updated correctly.

I will have an updated patch with the new test
module early next week.

--
Sami

#40Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#39)
1 attachment(s)
Re: Improve LWLock tranche name visibility across backends

Hi,

Attached v6 which addresses the feedback from the last review.

1/ Rahila raised a point about the necessity to allocate new dsa pointers
for tranche names when copying ( during resize). That is correct. We can simply
memcpy those dsa pointers at the time of copy. All we need to
do is free the dsa pointer that tracks the list.

2/ I also implemented a full sync of the local cache when necessary.
That is the tranche name is not found locally and the tranche index is
not higher than the max used index. To do that we do have to track the
highest index used as the allocated counter is not sufficient to do this.
Allocation will grow geometrically to avoid resize operations.

3/ I also added tests using INJECTION_POINTS. Although the same tests
could be done with DEBUG, I felt INJECTION_POINTS
are better to use for this purpose to avoid unnecessary logging.
I created a test module called test_lwlock_tranches which has
a perl test because we do need to set shared_preload_libraries.

I did have to define the following in wait_classes.h:

+#define WAIT_EVENT_CLASS_MASK  0xFF000000
+#define WAIT_EVENT_ID_MASK             0x0000FFFF

This is because the extension calls
GetLWLockIdentifier ( to mimic pg_stat_activity) and
needs to calculate a classId.

--
Sami

Attachments:

v6-0001-Implement-a-DSA-for-LWLock-tranche-names.patchapplication/octet-stream; name=v6-0001-Implement-a-DSA-for-LWLock-tranche-names.patchDownload
From e8a507f75b8402bb8f452734df70c38973b0efdc Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Mon, 21 Jul 2025 19:02:47 -0500
Subject: [PATCH v6 1/1] Implement a DSA for LWLock tranche names

Previously, only LWLock tranches registered during postmaster startup were
available to all backends via a tranche name array inherited through fork.
Other backends that wanted to know about a tranche name had to register it
using LWLockRegisterTranche. While this design worked as intended, it was
not ideal because if a backend did not call LWLockRegisterTranche, it would
show a generic "extension" wait_event name in pg_stat_activity instead of
the actual tranche name. This caused inconsistencies with other backends
that registered the tranche name.

Commit fe07100e82b0 made dynamic creation of named DSA areas (and dshash)
much simpler for a backend, so it is anticipated that fewer tranche names
will be registered during postmaster startup in the future, with more
registered dynamically. This will make the above issue more common.

To address this, this change introduces a DSA to store LWLock tranche names
after postmaster startup. When a backend looks up a tranche name by tranche ID,
it first checks its local cache; if the name is not found locally, it fetches
it from the DSA and caches it for future lookups.

This also simplifies the API for registering tranche names: all registration
work is now done via LWLockNewTrancheId, which accepts a tranche name. The
LWLockRegisterTranche call is no longer required.

Additionally, while users should not pass arbitrary tranche IDs (that is,
IDs not created via LWLockNewTrancheId) to LWLockInitialize, nothing
technically prevents them from doing so. Therefore, we must continue to
handle such cases gracefully by returning a default "extension" tranche name.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   4 +-
 doc/src/sgml/xfunc.sgml                       |  20 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/ipc/ipci.c                |   2 +
 src/backend/storage/lmgr/lwlock.c             | 416 +++++++++++++++---
 src/backend/utils/activity/wait_event.c       |   3 -
 .../utils/activity/wait_event_names.txt       |   2 +
 src/backend/utils/init/postinit.c             |   2 +
 src/include/storage/lwlock.h                  |  46 +-
 src/include/storage/lwlocklist.h              |   2 +
 src/include/utils/wait_classes.h              |   2 +
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_lwlock_tranches/Makefile     |  25 ++
 .../modules/test_lwlock_tranches/meson.build  |  36 ++
 .../t/001_test_lwlock_tranches.pl             | 115 +++++
 .../test_lwlock_tranches--1.0.sql             |  16 +
 .../test_lwlock_tranches.c                    | 144 ++++++
 .../test_lwlock_tranches.control              |   6 +
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 24 files changed, 770 insertions(+), 118 deletions(-)
 create mode 100644 src/test/modules/test_lwlock_tranches/Makefile
 create mode 100644 src/test/modules/test_lwlock_tranches/meson.build
 create mode 100644 src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..b623589b430 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,8 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
-
 	return found;
 }
 
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 30219f432d9..32ea6c87be8 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3758,9 +3758,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3778,17 +3779,16 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
+      It is possible to use a <literal>tranche_id</literal> that was not retrieved
+      using <function>LWLockNewTrancheId</function>, but this is not recommended.
+      The ID may clash with an already registered tranche name, or the specified
+      name may not be found. In such cases, looking up the name will return a generic
+      "extension" tranche name.
      </para>
 
      <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..c8e4d55cf80 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -343,6 +343,8 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+
+	LWLockTrancheNamesShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index ec9c345ffdf..80d8514c364 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -80,10 +80,12 @@
 #include "pg_trace.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
+#include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/proclist.h"
 #include "storage/procnumber.h"
 #include "storage/spin.h"
+#include "utils/injection_point.h"
 #include "utils/memutils.h"
 
 #ifdef LWLOCK_STATS
@@ -125,9 +127,12 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. Tranche names are registered in LWLockTrancheNames in
+ * two ways: if registration happens during postmaster startup, the names are
+ * stored directly in the local array; if registration happens afterward, the
+ * names are first stored in shared memory and later cached into the local array
+ * during lookup.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -144,14 +149,6 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 				 LWTRANCHE_FIRST_USER_DEFINED,
 				 "missing entries in BuiltinTrancheNames[]");
 
-/*
- * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, 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 **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
-
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
  * the pointer by fork from the postmaster (except in the EXEC_BACKEND case,
@@ -187,6 +184,52 @@ typedef struct NamedLWLockTrancheRequest
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
 static int	NamedLWLockTrancheRequestsAllocated = 0;
 
+typedef struct LWLockTrancheNamesShmem
+{
+	/* pointer to the DSA shared memory, created in-place */
+	void	   *raw_dsa_area;
+	/* DSA Pointer to a list of DSA pointers, each storing a tranche name */
+	dsa_pointer list_ptr;
+
+	/*
+	 * The number of slots allocated for tranche names. The allocated size
+	 * will grow geometrically to reduce resizing of the list_ptr.
+	 */
+	int			allocated;
+
+	/*
+	 * The highest tranche index currently in use. This is used to determine
+	 * whether local memory can be synchronized with shared memory.
+	 */
+	int			max_tranche_index;
+	/* lock to protect access to the list */
+	LWLock		lock;
+}			LWLockTrancheNamesShmem;
+
+typedef struct LWLockTrancheNamesStruct
+{
+	/*
+	 * Tranche names allocated in shared memory after postmaster startup, when
+	 * dynamic allocation is allowed.
+	 */
+	LWLockTrancheNamesShmem *shmem;
+	dsa_area   *dsa;
+
+	/*
+	 * Tranche names stored in local backend memory, either during postmaster
+	 * startup or as a local cache afterward.
+	 */
+	const char **local;
+	int			allocated;
+}			LWLockTrancheNamesStruct;
+
+static LWLockTrancheNamesStruct LWLockTrancheNames =
+{
+	NULL, NULL, NULL, 0
+};
+
+#define LWLOCK_TRANCHE_NAMES_INIT_SIZE 16
+
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
  * and the length of the shared-memory NamedLWLockTrancheArray later on.
@@ -201,7 +244,11 @@ NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
+static void SetLocalTrancheName(int tranche_index, const char *tranche_name);
+static void SetSharedTrancheName(int tranche_index, const char *tranche_name);
 static const char *GetLWTrancheName(uint16 trancheId);
+static void LWLockTrancheNamesAttach(void);
+static void LWLockTrancheNamesDetach(void);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -448,11 +495,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -513,7 +555,7 @@ InitializeLWLocks(void)
 			name = trancheNames;
 			trancheNames += strlen(request->tranche_name) + 1;
 			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
+			tranche->trancheId = LWLockNewTrancheId(name);
 			tranche->trancheName = name;
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
@@ -569,61 +611,155 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID and register it with the associated tranche
+ * name.
+ *
+ * The tranche name will be user-visible as a wait event name, so try to
+ * use a name that fits the style for those.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
+	int			tranche_id;
+	int			tranche_index;
 	int		   *LWLockCounter;
 
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 	/* We use the ShmemLock spinlock to protect LWLockCounter */
 	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
+	tranche_id = (*LWLockCounter)++;
 	SpinLockRelease(ShmemLock);
 
-	return result;
+	tranche_index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
+
+	if (!IsUnderPostmaster)
+		SetLocalTrancheName(tranche_index, tranche_name);
+	else
+		SetSharedTrancheName(tranche_index, tranche_name);
+
+	return tranche_id;
 }
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
+static void
+SetSharedTrancheName(int tranche_index, const char *tranche_name)
 {
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	dsa_pointer *name_ptrs;
+	dsa_pointer str_ptr;
+	char	   *str_addr;
+	int			len;
+	int			current_allocated;
+#ifdef USE_INJECTION_POINTS
+	LWLockTranchesInjectionPoint condition = {SET_SHARED, false, NULL, 0, false, false};
+#endif
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	current_allocated = LWLockTrancheNames.shmem->allocated;
+
+	/* Tranche array does not exist, initialize it */
+	if (!DsaPointerIsValid(LWLockTrancheNames.shmem->list_ptr))
 	{
-		int			newalloc;
+		int			init_alloc = LWLOCK_TRANCHE_NAMES_INIT_SIZE;
+
+		LWLockTrancheNames.shmem->allocated = init_alloc;
+		LWLockTrancheNames.shmem->list_ptr =
+			dsa_allocate0(LWLockTrancheNames.dsa, init_alloc * sizeof(dsa_pointer));
+
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+		memset(name_ptrs, InvalidDsaPointer, init_alloc);
+#ifdef USE_INJECTION_POINTS
+		condition.initialized = true;
+		condition.allocated = init_alloc;
+#endif
+	}
+
+	/*
+	 * Need to enlarge the tranche array. Allocate a new list, copy the
+	 * current tranche name pointers into it, and update shared memory to
+	 * point to the new list.
+	 */
+	else if (tranche_index >= current_allocated)
+	{
+		int			new_alloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE,
+													 tranche_index + 1));
+
+		dsa_pointer new_list = dsa_allocate(LWLockTrancheNames.dsa,
+											new_alloc * sizeof(dsa_pointer));
+
+		dsa_pointer *old_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+												LWLockTrancheNames.shmem->list_ptr);
+
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list);
+
+		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+		memcpy(name_ptrs, old_ptrs, sizeof(dsa_pointer) * current_allocated);
+
+		dsa_free(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+		LWLockTrancheNames.shmem->list_ptr = new_list;
+		LWLockTrancheNames.shmem->allocated = new_alloc;
+#ifdef USE_INJECTION_POINTS
+		condition.resized = true;
+		condition.allocated = new_alloc;
+#endif
+	}
+	/* Use the current list */
+	else
+	{
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+									LWLockTrancheNames.shmem->list_ptr);
+	}
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	/* Allocate and copy new string */
+	len = strlen(tranche_name) + 1;
+	str_ptr = dsa_allocate(LWLockTrancheNames.dsa, len);
+	str_addr = dsa_get_address(LWLockTrancheNames.dsa, str_ptr);
+	memcpy(str_addr, tranche_name, len);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
+	name_ptrs[tranche_index] = str_ptr;
+
+	LWLockTrancheNames.shmem->max_tranche_index = tranche_index;
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+#ifdef USE_INJECTION_POINTS
+	if (condition.resized || condition.initialized)
+		INJECTION_POINT("lwlock-sync-tranche-names", &condition);
+#endif
+}
+
+static void
+SetLocalTrancheName(int tranche_index, const char *tranche_name)
+{
+	int			newalloc;
+
+	/* If necessary, create or enlarge array. */
+	if (tranche_index >= LWLockTrancheNames.allocated)
+	{
+		newalloc = pg_nextpower2_32(Max(8, tranche_index + 1));
+
+		if (LWLockTrancheNames.local == NULL)
+		{
+			LWLockTrancheNames.local = (const char **)
 				MemoryContextAllocZero(TopMemoryContext,
 									   newalloc * sizeof(char *));
+		}
 		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+		{
+			LWLockTrancheNames.local =
+				repalloc0_array(LWLockTrancheNames.local, const char *, LWLockTrancheNames.allocated, newalloc);
+		}
+
+		LWLockTrancheNames.allocated = newalloc;
 	}
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	/*
+	 * Don't override an existing name. Local cache may be ahead of shared
+	 * memory if tranche names were added locally during postmaster startup.
+	 */
+	if (LWLockTrancheNames.local[tranche_index])
+		return;
+
+	LWLockTrancheNames.local[tranche_index] = tranche_name;
 }
 
 /*
@@ -708,28 +844,76 @@ LWLockReportWaitEnd(void)
 	pgstat_report_wait_end();
 }
 
+static void
+SyncLWLockTrancheNames()
+{
+	dsa_pointer *pointers;
+
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_SHARED);
+
+	pointers = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+	for (int i = 0; i <= LWLockTrancheNames.shmem->max_tranche_index; i++)
+	{
+		const char *tranche_name = (char *) dsa_get_address(LWLockTrancheNames.dsa, pointers[i]);
+
+		if (tranche_name)
+			SetLocalTrancheName(i, tranche_name);
+	}
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
 /*
  * Return the name of an LWLock tranche.
  */
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	const char *tranche_name = NULL;
+#ifdef USE_INJECTION_POINTS
+	LWLockTranchesInjectionPoint condition = {SYNC_LOCAL, false, NULL};
+#endif
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
+	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * This is an extension tranche, so check LWLockTrancheNames[]. First,
+	 * synchronize the local list with shared memory if necessary, then
+	 * perform the lookup. If the tranche hasn't been registered by the
+	 * extension, return "extension".
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	if (trancheId >= LWLockTrancheNames.allocated ||
+		LWLockTrancheNames.local[trancheId] == NULL)
+	{
+		/*
+		 * If it's not possible for the tranche to be in shared memory, skip
+		 * synchronization.
+		 */
+		if (trancheId <= LWLockTrancheNames.shmem->max_tranche_index)
+		{
+#ifdef USE_INJECTION_POINTS
+			condition.synced_local = true;
+#endif
+			SyncLWLockTrancheNames();
+		}
+	}
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (trancheId < LWLockTrancheNames.allocated)
+		tranche_name = LWLockTrancheNames.local[trancheId];
 
-	return LWLockTrancheNames[trancheId];
+	tranche_name = tranche_name ? tranche_name : "extension";
+#ifdef USE_INJECTION_POINTS
+	condition.tranche_name = tranche_name;
+	condition.tranche_index = trancheId;
+	condition.tranche_id = trancheId + LWTRANCHE_FIRST_USER_DEFINED;
+	INJECTION_POINT("lwlock-sync-tranche-names", &condition);
+#endif
+	return tranche_name;
 }
 
 /*
@@ -1995,3 +2179,121 @@ LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode)
 	}
 	return false;
 }
+
+static Size
+LWLockTrancheNamesInitSize()
+{
+	Size		sz;
+
+	sz = 256 * 1024;
+
+	Assert(dsa_minimum_size() <= sz);
+
+	return MAXALIGN(sz);
+}
+
+Size
+LWLockTrancheNamesShmemSize(void)
+{
+	Size		sz;
+
+	sz = MAXALIGN(sizeof(LWLockTrancheNamesStruct));
+	sz = add_size(sz, LWLockTrancheNamesInitSize());
+
+	return sz;
+}
+
+void
+LWLockTrancheNamesShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize or attach to shared memory structure */
+	LWLockTrancheNames.shmem = (LWLockTrancheNamesShmem *)
+		ShmemInitStruct("Dynamic LWLock tranche names",
+						LWLockTrancheNamesShmemSize(),
+						&found);
+
+	if (!IsUnderPostmaster)
+	{
+		dsa_area   *dsa;
+		LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+		char	   *p = (char *) ctl;
+
+		/* Calculate layout within the shared memory region */
+		p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+		ctl->raw_dsa_area = p;
+		p += MAXALIGN(LWLockTrancheNamesInitSize());
+
+		LWLockInitialize(&ctl->lock, LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST);
+
+		/* Create a dynamic shared memory area in-place */
+		dsa = dsa_create_in_place(ctl->raw_dsa_area,
+								  LWLockTrancheNamesInitSize(),
+								  LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA,
+								  NULL);
+
+		/*
+		 * Postmaster will never access these again, thus free the local
+		 * dsa/dshash references.
+		 */
+		dsa_pin(dsa);
+		dsa_detach(dsa);
+
+		ctl->allocated = 0;
+		ctl->max_tranche_index = 0;
+		ctl->list_ptr = InvalidDsaPointer;
+	}
+	else
+	{
+		Assert(found);
+	}
+}
+
+static void
+LWLockTrancheNamesAttach(void)
+{
+	MemoryContext oldcontext;
+
+	Assert(LWLockTrancheNames.dsa == NULL);
+
+	/* stats shared memory persists for the backend lifetime */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	LWLockTrancheNames.dsa = dsa_attach_in_place(LWLockTrancheNames.shmem->raw_dsa_area,
+												 NULL);
+	dsa_pin_mapping(LWLockTrancheNames.dsa);
+
+	Assert(LWLockTrancheNames.dsa);
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+static void
+LWLockTrancheNamesDetach(void)
+{
+	Assert(LWLockTrancheNames.dsa);
+
+	dsa_detach(LWLockTrancheNames.dsa);
+
+	dsa_release_in_place(LWLockTrancheNames.shmem->raw_dsa_area);
+
+	LWLockTrancheNames.dsa = NULL;
+}
+
+static void
+LWLockTrancheNamesShutdownHook(int code, Datum arg)
+{
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	LWLockTrancheNamesDetach();
+}
+
+void
+LWLockTrancheNamesBEInit(void)
+{
+	LWLockTrancheNamesAttach();
+
+	/* Set up a process-exit hook to clean up */
+	before_shmem_exit(LWLockTrancheNamesShutdownHook, 0);
+}
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d9b8f34a355..eba7d338c1f 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -39,9 +39,6 @@ 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_CLASS_MASK	0xFF000000
-#define WAIT_EVENT_ID_MASK		0x0000FFFF
-
 /*
  * Hash tables for storing custom wait event ids and their names in
  * shared memory.
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0be307d2ca0..f60ed927892 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -406,6 +406,8 @@ SubtransSLRU	"Waiting to access the sub-transaction SLRU cache."
 XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
+LWLockDynamicTranchesDSA	"Waiting to access the LWLock tranche names DSA area."
+LWLockDynamicTranchesList	"Waiting to access the list of LWLock tranche names in DSA."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 641e535a73c..1ef04c6b5ec 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -662,6 +662,8 @@ BaseInit(void)
 	 * drop ephemeral slots, which in turn triggers stats reporting.
 	 */
 	ReplicationSlotInitialize();
+
+	LWLockTrancheNamesBEInit();
 }
 
 
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..8cf0fa7256a 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -80,6 +80,30 @@ typedef struct NamedLWLockTranche
 	char	   *trancheName;
 } NamedLWLockTranche;
 
+#ifdef USE_INJECTION_POINTS
+typedef enum LWLockTranchesInjectionPointTests
+{
+	SYNC_LOCAL = 0,
+	SET_SHARED,
+}			LWLockTranchesInjectionPointTests;
+
+typedef struct LWLockTranchesInjectionPoint
+{
+	int			test_type;
+
+	/* for SYNC_LOCAL tests */
+	bool		synced_local;
+	const char *tranche_name;
+	int			tranche_index;
+	int			tranche_id;
+
+	/* for SET_SHARED tests */
+	int			allocated;
+	bool		initialized;
+	bool		resized;
+}			LWLockTranchesInjectionPoint;
+#endif
+
 extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
@@ -156,22 +180,18 @@ extern void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
- * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * An alternative, more flexible method of obtaining lwlocks is available.
+ * First, call LWLockNewTrancheId once, providing an associated tranche name.
+ * This function allocates a tranche ID from a shared counter. Then, for each
+ * lwlock, call LWLockInitialize once, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
+extern void LWLockTrancheNamesShmemInit(void);
+extern Size LWLockTrancheNamesShmemSize(void);
+extern void LWLockTrancheNamesBEInit(void);
+
 /*
  * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
  * we reserve additional tranche IDs for builtin tranches not included in
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 208d2e3a8ed..5978648c09d 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -135,3 +135,5 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU)
 PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
 PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
 PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_DSA, LWLockDynamicTranchesDSA)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_LIST, LWLockDynamicTranchesList)
diff --git a/src/include/utils/wait_classes.h b/src/include/utils/wait_classes.h
index 51ee68397d5..6ca0504cee1 100644
--- a/src/include/utils/wait_classes.h
+++ b/src/include/utils/wait_classes.h
@@ -10,6 +10,8 @@
 #ifndef WAIT_CLASSES_H
 #define WAIT_CLASSES_H
 
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+#define WAIT_EVENT_ID_MASK		0x0000FFFF
 
 /* ----------
  * Wait Classes
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..119d4708357 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_lwlock_tranches \
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..f04910d13d7 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_lwlock_tranches')
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_lwlock_tranches/Makefile b/src/test/modules/test_lwlock_tranches/Makefile
new file mode 100644
index 00000000000..902867229c1
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/Makefile
@@ -0,0 +1,25 @@
+# src/test/modules/test_lwlock_tranches/Makefile
+
+MODULE_big = test_lwlock_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_lwlock_tranches.o
+PGFILEDESC = "test_lwlock_tranches - test code LWLock tranche management"
+
+EXTENSION = test_lwlock_tranches
+DATA = test_lwlock_tranches--1.0.sql
+
+TAP_TESTS = 1
+
+export enable_injection_points
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_lwlock_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_lwlock_tranches/meson.build b/src/test/modules/test_lwlock_tranches/meson.build
new file mode 100644
index 00000000000..f32417a4143
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/meson.build
@@ -0,0 +1,36 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_lwlock_tranches_sources = files(
+  'test_lwlock_tranches.c',
+)
+
+if host_system == 'windows'
+  test_lwlock_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_lwlock_tranches',
+    '--FILEDESC', 'test_lwlock_tranches - test code LWLock tranche management',])
+endif
+
+test_lwlock_tranches = shared_module('test_lwlock_tranches',
+  test_lwlock_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_lwlock_tranches
+
+test_install_data += files(
+  'test_lwlock_tranches.control',
+  'test_lwlock_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_lwlock_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
+    'tests': [
+      't/001_test_lwlock_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
new file mode 100644
index 00000000000..1d39cbdf1ac
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
@@ -0,0 +1,115 @@
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $initial_size = 16;
+my $final_allocation = 64;
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+$node->append_conf(
+	'postgresql.conf', qq(
+shared_preload_libraries=test_lwlock_tranches
+));
+$node->append_conf(
+	'postgresql.conf', qq(
+test_lwlock_tranches.requested_named_tranches=2
+));
+$node->start();
+
+$node->safe_psql('postgres', "CREATE EXTENSION injection_points");
+$node->safe_psql('postgres', "CREATE EXTENSION test_lwlock_tranches");
+
+my $first_user_defined = $node->safe_psql(
+	'postgres',
+	"SELECT test_lwlock_tranches_get_first_user_defined()");
+
+my $requested_named_tranches = $node->safe_psql(
+	'postgres',
+	"SELECT setting FROM pg_settings WHERE name = \'test_lwlock_tranches.requested_named_tranches\'");
+
+my $next_index;
+my $lookup_tranche_id = 0;
+my $log_location = -s $node->logfile;
+my $current_size = $initial_size;
+
+while ($current_size < $final_allocation) {
+	$current_size *= 2;
+}
+
+#
+# Create tranches using LWLockNewTrancheId. Note that tranches created
+# with RequestNamedLWLockTranche occur during startup.
+#
+$node->safe_psql('postgres',
+	qq{select test_lwlock_new_tranche_id($current_size - $requested_named_tranches)});
+
+$log_location = $node->wait_for_log(qr/ LOG:  allocation_reason = resize allocation_size = $current_size/, $log_location);
+ok(1, "resize up to $current_size tranche names");
+
+# Tests: Lookup of tranches created with RequestNamedLWLockTranche
+for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+	$lookup_tranche_id = $next_index + $first_user_defined;
+
+	$node->safe_psql('postgres',
+		qq{select test_get_lwlock_identifier($next_index)});
+	$log_location = $node->wait_for_log(qr/ LOG:  tranche_name = test_lock_$next_index tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = no/, $log_location);
+}
+ok(1, "requested_named_tranches looked up from local");
+
+#
+# Tests: Lookup for tranches created directly with LWLockNewTrancheId.
+#
+# Two scenarios are tested:
+#  1. Test lookup for tranches created directly with LWLockNewTrancheId.
+#     We lookup twice to ensure the 2nd lookup occurs from the local
+#     cache.
+#  2. Test lookup to make sure that tranches created during postmaster startup,
+#     and are already in local memory are not overwritten during synchronization
+#     of local memory.
+#
+
+$lookup_tranche_id = $next_index + $first_user_defined;
+$node->safe_psql('postgres',
+	qq{select test_get_lwlock_identifier($next_index);
+	   select test_get_lwlock_identifier($next_index);
+	   select test_get_lwlock_identifier($next_index);
+	   select test_get_lwlock_identifier(0)});
+$node->wait_for_log(qr/ LOG:  tranche_name = test_lock__$next_index tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = yes/, $log_location);
+$node->wait_for_log(qr/ LOG:  tranche_name = test_lock__$next_index tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = no/, $log_location);
+$node->wait_for_log(qr/ LOG:  tranche_name = test_lock__$next_index tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = no/, $log_location);
+$log_location = $node->wait_for_log(qr/ LOG:  tranche_name = test_lock_0 tranche_id = $first_user_defined tranche_index = 0 synced_local = no/, $log_location);
+ok(1, "lookup with synchronization is successful");
+
+#
+# Tests: Lookup behavior for tranches never registered
+#        (via RequestNamedLWLockTranche or LWLockNewTrancheId).
+#
+# Two scenarios are tested:
+#  1. Tranche index within shared memory bounds.
+#     - shared memory is never synced in this case.
+#
+#  2. Tranche index outside shared memory bounds.
+#     - First lookup skips syncing local cache from shared memory.
+#
+
+# --- Scenario 1: Index within bounds ---
+$next_index = $current_size - $requested_named_tranches;
+$lookup_tranche_id = $next_index + $first_user_defined;
+$node->safe_psql('postgres',
+		qq{select test_get_lwlock_identifier($next_index)});
+$node->wait_for_log(qr/ LOG:  tranche_name = extension tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = no/, $log_location);
+ok(1, "tranche_id is within allocated amount. Returns \"extension\"");
+
+# --- Scenario 2: Index outside bounds ---
+$next_index = $current_size;
+$lookup_tranche_id = $next_index + $first_user_defined;
+$node->safe_psql('postgres',
+	qq{select test_get_lwlock_identifier($next_index)});
+$log_location = $node->wait_for_log(qr/LOG:  tranche_name = extension tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = no/, $log_location);
+ok(1, "tranche_id is outside allocated amount. Returns \"extension\"");
+
+done_testing();
\ No newline at end of file
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
new file mode 100644
index 00000000000..7fdf90e3e23
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
@@ -0,0 +1,16 @@
+-- test_lwlock_tranches--1.0.sql
+
+CREATE FUNCTION test_lwlock_new_tranche_id(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_lwlock_new_tranche_id'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_get_lwlock_identifier(int)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_get_lwlock_identifier'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_lwlock_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_lwlock_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
new file mode 100644
index 00000000000..bd900be856c
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
@@ -0,0 +1,144 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+#ifdef USE_INJECTION_POINTS
+extern PGDLLEXPORT void lwlock_tranches_injection_callback(const char *name,
+														   const void *private_data,
+														   void *arg);
+#else
+elog(ERROR, "injection points not supported");
+#endif
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_lwlock_tranches_shmem_request(void);
+static void test_lwlock_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_lwlock_tranches_requested_named_tranches = 0;
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_lwlock_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_lwlock_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_lwlock_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_lwlock_tranches_requested_named_tranches,
+							2,
+							1,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_lwlock_tranches_shmem_startup(void)
+{
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+#ifdef USE_INJECTION_POINTS
+	InjectionPointAttach("lwlock-sync-tranche-names",
+						 "test_lwlock_tranches",
+						 "lwlock_tranches_injection_callback",
+						 NULL,
+						 0);
+#else
+	elog(ERROR, "injection points not supported");
+#endif
+}
+
+static void
+test_lwlock_tranches_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	for (int i = 0; i < test_lwlock_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_new_tranche_id);
+Datum
+test_lwlock_new_tranche_id(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+
+	for (int i = test_lwlock_tranches_requested_named_tranches; i < num; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	PG_RETURN_VOID();
+}
+
+#ifdef USE_INJECTION_POINTS
+void
+lwlock_tranches_injection_callback(const char *name, const void *private_data, void *arg)
+{
+	LWLockTranchesInjectionPoint *condition = (LWLockTranchesInjectionPoint *) arg;
+
+	if (condition->test_type == SYNC_LOCAL)
+		elog(LOG, "tranche_name = %s tranche_id = %d tranche_index = %d synced_local = %s",
+			 condition->tranche_name,
+			 condition->tranche_id,
+			 condition->tranche_index,
+			 condition->synced_local ? "yes" : "no");
+	else if (condition->test_type == SET_SHARED)
+		elog(LOG, "allocation_reason = %s allocation_size = %d",
+			 condition->initialized ? "initial" : "resize",
+			 condition->allocated);
+}
+#else
+elog(ERROR, "injection points not supported");
+#endif
+
+PG_FUNCTION_INFO_V1(test_get_lwlock_identifier);
+Datum
+test_get_lwlock_identifier(PG_FUNCTION_ARGS)
+{
+	int			id = PG_GETARG_INT32(0);
+	uint16		eventid = id + LWTRANCHE_FIRST_USER_DEFINED;
+
+	GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, eventid);
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_tranches_get_first_user_defined);
+Datum
+test_lwlock_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
new file mode 100644
index 00000000000..bf0ecf64376
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
@@ -0,0 +1,6 @@
+# test_lwlock_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_lwlock_tranches'
\ No newline at end of file
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

#41Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#40)
Re: Improve LWLock tranche name visibility across backends

I haven't followed the latest discussion, but I took a look at the patch.

+      It is possible to use a <literal>tranche_id</literal> that was not retrieved
+      using <function>LWLockNewTrancheId</function>, but this is not recommended.
+      The ID may clash with an already registered tranche name, or the specified
+      name may not be found. In such cases, looking up the name will return a generic
+      "extension" tranche name.

Is there any reason to continue allowing this? For example, maybe we could
ERROR if LWLockInitialize()/GetLWTrancheName() are given a tranche_id
greater than the number allocated. I guess I'm not following why we should
gracefully handle these kinds of coding errors, especially when they result
in unhelpful behavior like an "extension" tranche.

+#else
+elog(ERROR, "injection points not supported");
+#endif

This causes compilation to fail when injection points are not enabled.

I haven't combed through the patch character-by-character, but upon a
read-through, the general shape looks reasonable to me. As a general note,
I'd suggest adding more commentary throughout and finding opportunities to
simplify and/or clean up the code as much as possible.

--
nathan

#42Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#41)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

I haven't followed the latest discussion, but I took a look at the patch.

+      It is possible to use a <literal>tranche_id</literal> that was not retrieved
+      using <function>LWLockNewTrancheId</function>, but this is not recommended.
+      The ID may clash with an already registered tranche name, or the specified
+      name may not be found. In such cases, looking up the name will return a generic
+      "extension" tranche name.

Is there any reason to continue allowing this? For example, maybe we could
ERROR if LWLockInitialize()/GetLWTrancheName() are given a tranche_id
greater than the number allocated. I guess I'm not following why we should
gracefully handle these kinds of coding errors, especially when they result
in unhelpful behavior like an "extension" tranche.

We could definitely error out LWLockInitialize in this case. It may
break things out
there, so that is why I have been hesitant.

I don’t think we can use allocated in this case, since that value refers to
pre-allocated slots. Instead, we will need to do a lookup, and if no
tranche_name is found, then we should error out.

Essentially, we can call GetLWTrancheName within LWLockInitialize, but
GetLWTrancheName can no longer have a fallback value such as "extension".
It should error out instead. I believe that is what you mean. correct?

One thought I had was to change the signature of LWLockInitialize to take a
tranche name and no longer should LWLockNewTrancheId be no longer be global.
That will slightly complicate things with built-in tranches a bit (but
is doable).

On the other hand, an error seems like a reasonable approach.

Attached v6 which addresses the feedback from the last review.

I spoke offline with Bertrand and discussed the synchronization of
the local memory from shared memory. After that discussion it is clear
we don't need to track the highest used index in shared memory. Because
the tranche_id comes from a shared counter, it is not possible that
the same tranche_id can be used twice, so we will not have overlap.
That means, when we sync, we just need to know the highest used
index in the local memory ( since that could be populated during
postmaster startup and inherited via fork ) and start syncing local
memory from that point. We can also cut off the sync once we encounter
an invalid dsa pointer, since there can't be any tranche names in shared
memory after that point. I did change SyncLWLockTrancheNames to
accept the tranche_id that triggered the sync, so we can make sure it
gets added to local memory, either as part of the sync or added with
the default "extension" tranche name.

I also corrected the INJECTION_POINT tests. I was missing a skip
if INJECTION_POINT is not enabled and removed

+elog(ERROR, "injection points not supported");

mentioned by Nathan earlier. To simplify the review, I also split the
test patch. It is not yet clear if there is consensus to include tests
( I think we should ), but having the tests will help the review.

More commentary was added to the code for the point Nathan
also raised.

See v7; which could be simplified further depending on the handling
of LWLockInitialize as mentioned at the top of this message, since we
don't need to account for out of range tranche IDs.

--
Sami

Attachments:

v7-0002-lwlock-shared-tranche-names-test.patchapplication/octet-stream; name=v7-0002-lwlock-shared-tranche-names-test.patchDownload
From 04727b3f141de4923df6beebd2664321f132269f Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Tue, 12 Aug 2025 19:20:47 +0000
Subject: [PATCH v7 2/2] lwlock shared tranche names test

---
 src/backend/storage/lmgr/lwlock.c             |  33 ++++-
 src/backend/utils/activity/wait_event.c       |   3 -
 src/include/storage/lwlock.h                  |  24 +++
 src/include/utils/wait_classes.h              |   2 +
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 .../modules/test_lwlock_tranches/Makefile     |  25 ++++
 .../modules/test_lwlock_tranches/meson.build  |  36 +++++
 .../t/001_test_lwlock_tranches.pl             | 117 +++++++++++++++
 .../test_lwlock_tranches--1.0.sql             |  16 ++
 .../test_lwlock_tranches.c                    | 138 ++++++++++++++++++
 .../test_lwlock_tranches.control              |   6 +
 12 files changed, 399 insertions(+), 5 deletions(-)
 create mode 100644 src/test/modules/test_lwlock_tranches/Makefile
 create mode 100644 src/test/modules/test_lwlock_tranches/meson.build
 create mode 100644 src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control

diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 7bf455beda4..aa99df36feb 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -85,6 +85,7 @@
 #include "storage/proclist.h"
 #include "storage/procnumber.h"
 #include "storage/spin.h"
+#include "utils/injection_point.h"
 #include "utils/memutils.h"
 
 #ifdef LWLOCK_STATS
@@ -652,6 +653,9 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 	char	   *str_addr;
 	int			len;
 	int			current_allocated;
+#ifdef USE_INJECTION_POINTS
+	LWLockTranchesInjectionPoint condition = {SET_SHARED, false, NULL, 0, false, false};
+#endif
 
 	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
 
@@ -668,6 +672,10 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 
 		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
 		memset(name_ptrs, InvalidDsaPointer, init_alloc);
+#ifdef USE_INJECTION_POINTS
+		condition.initialized = true;
+		condition.allocated = init_alloc;
+#endif
 	}
 
 	/*
@@ -695,6 +703,10 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 
 		LWLockTrancheNames.shmem->list_ptr = new_list;
 		LWLockTrancheNames.shmem->allocated = new_alloc;
+#ifdef USE_INJECTION_POINTS
+		condition.resized = true;
+		condition.allocated = new_alloc;
+#endif
 	}
 	/* Use the current list */
 	else
@@ -712,6 +724,10 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 	name_ptrs[tranche_index] = str_ptr;
 
 	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+#ifdef USE_INJECTION_POINTS
+	if (condition.resized || condition.initialized)
+		INJECTION_POINT("lwlock-sync-tranche-names", &condition);
+#endif
 }
 
 /*
@@ -900,6 +916,9 @@ static const char *
 GetLWTrancheName(uint16 trancheId)
 {
 	const char *tranche_name = NULL;
+#ifdef USE_INJECTION_POINTS
+	LWLockTranchesInjectionPoint condition = {SYNC_LOCAL, false, NULL};
+#endif
 
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
@@ -915,12 +934,24 @@ GetLWTrancheName(uint16 trancheId)
 	 */
 	if (trancheId >= LWLockTrancheNames.allocated ||
 		LWLockTrancheNames.local[trancheId] == NULL)
+	{
+#ifdef USE_INJECTION_POINTS
+		condition.synced_local = true;
+#endif
 		SyncLWLockTrancheNames(trancheId);
+	}
 
 	if (trancheId < LWLockTrancheNames.allocated)
 		tranche_name = LWLockTrancheNames.local[trancheId];
 
-	return tranche_name ? tranche_name : "extension";
+	tranche_name = tranche_name ? tranche_name : "extension";
+#ifdef USE_INJECTION_POINTS
+	condition.tranche_name = tranche_name;
+	condition.tranche_index = trancheId;
+	condition.tranche_id = trancheId + LWTRANCHE_FIRST_USER_DEFINED;
+	INJECTION_POINT("lwlock-sync-tranche-names", &condition);
+#endif
+	return tranche_name;
 }
 
 /*
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d9b8f34a355..eba7d338c1f 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -39,9 +39,6 @@ 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_CLASS_MASK	0xFF000000
-#define WAIT_EVENT_ID_MASK		0x0000FFFF
-
 /*
  * Hash tables for storing custom wait event ids and their names in
  * shared memory.
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 8a008fc67dd..8cf0fa7256a 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -80,6 +80,30 @@ typedef struct NamedLWLockTranche
 	char	   *trancheName;
 } NamedLWLockTranche;
 
+#ifdef USE_INJECTION_POINTS
+typedef enum LWLockTranchesInjectionPointTests
+{
+	SYNC_LOCAL = 0,
+	SET_SHARED,
+}			LWLockTranchesInjectionPointTests;
+
+typedef struct LWLockTranchesInjectionPoint
+{
+	int			test_type;
+
+	/* for SYNC_LOCAL tests */
+	bool		synced_local;
+	const char *tranche_name;
+	int			tranche_index;
+	int			tranche_id;
+
+	/* for SET_SHARED tests */
+	int			allocated;
+	bool		initialized;
+	bool		resized;
+}			LWLockTranchesInjectionPoint;
+#endif
+
 extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
diff --git a/src/include/utils/wait_classes.h b/src/include/utils/wait_classes.h
index 51ee68397d5..6ca0504cee1 100644
--- a/src/include/utils/wait_classes.h
+++ b/src/include/utils/wait_classes.h
@@ -10,6 +10,8 @@
 #ifndef WAIT_CLASSES_H
 #define WAIT_CLASSES_H
 
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+#define WAIT_EVENT_ID_MASK		0x0000FFFF
 
 /* ----------
  * Wait Classes
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..119d4708357 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_lwlock_tranches \
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..f04910d13d7 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_lwlock_tranches')
diff --git a/src/test/modules/test_lwlock_tranches/Makefile b/src/test/modules/test_lwlock_tranches/Makefile
new file mode 100644
index 00000000000..902867229c1
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/Makefile
@@ -0,0 +1,25 @@
+# src/test/modules/test_lwlock_tranches/Makefile
+
+MODULE_big = test_lwlock_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_lwlock_tranches.o
+PGFILEDESC = "test_lwlock_tranches - test code LWLock tranche management"
+
+EXTENSION = test_lwlock_tranches
+DATA = test_lwlock_tranches--1.0.sql
+
+TAP_TESTS = 1
+
+export enable_injection_points
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_lwlock_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_lwlock_tranches/meson.build b/src/test/modules/test_lwlock_tranches/meson.build
new file mode 100644
index 00000000000..f32417a4143
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/meson.build
@@ -0,0 +1,36 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_lwlock_tranches_sources = files(
+  'test_lwlock_tranches.c',
+)
+
+if host_system == 'windows'
+  test_lwlock_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_lwlock_tranches',
+    '--FILEDESC', 'test_lwlock_tranches - test code LWLock tranche management',])
+endif
+
+test_lwlock_tranches = shared_module('test_lwlock_tranches',
+  test_lwlock_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_lwlock_tranches
+
+test_install_data += files(
+  'test_lwlock_tranches.control',
+  'test_lwlock_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_lwlock_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
+    'tests': [
+      't/001_test_lwlock_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
new file mode 100644
index 00000000000..843485bf939
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
@@ -0,0 +1,117 @@
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $initial_size = 16;
+my $final_allocation = 64;
+
+SKIP:
+{
+	skip 'Injection points not supported by this build', 1
+		unless $ENV{enable_injection_points} eq 'yes';
+
+	my $node = PostgreSQL::Test::Cluster->new('primary');
+	$node->init();
+	$node->append_conf('postgresql.conf',
+		qq(shared_preload_libraries=test_lwlock_tranches));
+	$node->append_conf('postgresql.conf',
+		qq(test_lwlock_tranches.requested_named_tranches=2));
+	$node->start();
+
+	$node->safe_psql('postgres', q(CREATE EXTENSION injection_points));
+	$node->safe_psql('postgres', q(CREATE EXTENSION test_lwlock_tranches));
+
+	my $first_user_defined = $node->safe_psql(
+		'postgres',
+		"SELECT test_lwlock_tranches_get_first_user_defined()");
+
+	my $requested_named_tranches = $node->safe_psql(
+		'postgres',
+		"SELECT setting FROM pg_settings WHERE name = \'test_lwlock_tranches.requested_named_tranches\'");
+
+	my $next_index;
+	my $lookup_tranche_id = 0;
+	my $log_location = -s $node->logfile;
+	my $current_size = $initial_size;
+
+	while ($current_size < $final_allocation) {
+		$current_size *= 2;
+	}
+
+	#
+	# Create tranches using LWLockNewTrancheId. Note that tranches created
+	# with RequestNamedLWLockTranche occur during startup, based on
+	# test_lwlock_tranches.requested_named_tranches.
+	#
+	$node->safe_psql('postgres',
+		qq{select test_lwlock_new_tranche_id($current_size - $requested_named_tranches)});
+
+	$log_location = $node->wait_for_log(
+		qr/ LOG:  allocation_reason = resize allocation_size = $current_size/, $log_location);
+	ok(1, "resize shared memory with allocation up to $current_size tranche names");
+
+	# Tests: Lookup of tranches created with RequestNamedLWLockTranche
+	for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+		$lookup_tranche_id = $next_index + $first_user_defined;
+
+		$node->safe_psql('postgres',
+			qq{select test_get_lwlock_identifier($next_index)});
+		$log_location = $node->wait_for_log(
+			qr/ LOG:  tranche_name = test_lock_$next_index tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = no/, $log_location);
+	}
+	ok(1, "requested_named_tranches looked up from local cache");
+
+	#
+	# Lookup for tranches created directly with LWLockNewTrancheId.
+	#
+	# Two scenarios are tested. First, the lookup for tranches created directly with
+	# LWLockNewTrancheId is performed twice to ensure the second lookup occurs from the
+	# local cache. Second, the lookup verifies that tranches created during postmaster
+	# startup, which are already in local memory, are not overwritten by synchronization
+	# of local memory.
+	#
+	$lookup_tranche_id = $next_index + $first_user_defined;
+	$node->safe_psql('postgres',
+		qq{select  test_get_lwlock_identifier($next_index);
+		select test_get_lwlock_identifier($next_index);
+		select test_get_lwlock_identifier($next_index);
+		select test_get_lwlock_identifier(0);
+		select test_get_lwlock_identifier(1)});
+	$node->wait_for_log(qr/ LOG:  tranche_name = test_lock__$next_index tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = yes/, $log_location);
+	$node->wait_for_log(qr/ LOG:  tranche_name = test_lock__$next_index tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = no/, $log_location);
+	$node->wait_for_log(qr/ LOG:  tranche_name = test_lock__$next_index tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = no/, $log_location);
+	$lookup_tranche_id = $first_user_defined;
+	$node->wait_for_log(qr/ LOG:  tranche_name = test_lock_0 tranche_id = $lookup_tranche_id tranche_index = 0 synced_local = no/, $log_location);
+	$lookup_tranche_id = $first_user_defined + 1;
+	$log_location = $node->wait_for_log(qr/ LOG:  tranche_name = test_lock_1 tranche_id = $lookup_tranche_id tranche_index = 1 synced_local = no/, $log_location);
+	ok(1, "lookup with synchronization is successful");
+
+	#
+	# Lookup tranches that have not been registered with RequestNamedLWLockTranche
+	# or LWLockNewTrancheId. They should return "extension". We want to test when the
+	# tranche index is within the allocated slots in shared memory and outisde of the
+	# allocated slots.
+	#
+	$next_index = $current_size - $requested_named_tranches;
+	$lookup_tranche_id = $next_index + $first_user_defined;
+	$node->safe_psql('postgres',
+		qq{select test_get_lwlock_identifier($next_index);
+		select test_get_lwlock_identifier($next_index);});
+	$node->wait_for_log(qr/ LOG:  tranche_name = extension tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = yes/, $log_location);
+	$log_location = $node->wait_for_log(qr/ LOG:  tranche_name = extension tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = no/, $log_location);
+	ok(1, 'unnamed tranche_id is within allocated shared memory and returns "extension"');
+
+	$next_index = $current_size;
+	$lookup_tranche_id = $next_index + $first_user_defined;
+	$node->safe_psql('postgres',
+		qq{select test_get_lwlock_identifier($next_index);
+		select test_get_lwlock_identifier($next_index)});
+	$node->wait_for_log(qr/LOG:  tranche_name = extension tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = no/, $log_location);
+	$log_location = $node->wait_for_log(qr/LOG:  tranche_name = extension tranche_id = $lookup_tranche_id tranche_index = $next_index synced_local = yes/, $log_location);
+	ok(1, 'unnamed tranche_id is outside of allocated shared memory and returns "extension"');
+}
+
+done_testing();
\ No newline at end of file
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
new file mode 100644
index 00000000000..7fdf90e3e23
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
@@ -0,0 +1,16 @@
+-- test_lwlock_tranches--1.0.sql
+
+CREATE FUNCTION test_lwlock_new_tranche_id(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_lwlock_new_tranche_id'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_get_lwlock_identifier(int)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_get_lwlock_identifier'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_lwlock_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_lwlock_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
new file mode 100644
index 00000000000..4433984b833
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
@@ -0,0 +1,138 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+extern PGDLLEXPORT void lwlock_tranches_injection_callback(const char *name,
+														   const void *private_data,
+														   void *arg);
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_lwlock_tranches_shmem_request(void);
+static void test_lwlock_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_lwlock_tranches_requested_named_tranches = 0;
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_lwlock_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_lwlock_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_lwlock_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_lwlock_tranches_requested_named_tranches,
+							2,
+							1,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_lwlock_tranches_shmem_startup(void)
+{
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+#ifdef USE_INJECTION_POINTS
+	InjectionPointAttach("lwlock-sync-tranche-names",
+						 "test_lwlock_tranches",
+						 "lwlock_tranches_injection_callback",
+						 NULL,
+						 0);
+#else
+	elog(ERROR, "injection points not supported");
+#endif
+}
+
+static void
+test_lwlock_tranches_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	for (int i = 0; i < test_lwlock_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_new_tranche_id);
+Datum
+test_lwlock_new_tranche_id(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+
+	for (int i = test_lwlock_tranches_requested_named_tranches; i < num; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	PG_RETURN_VOID();
+}
+
+void
+lwlock_tranches_injection_callback(const char *name, const void *private_data, void *arg)
+{
+#ifdef USE_INJECTION_POINTS
+	LWLockTranchesInjectionPoint *condition = (LWLockTranchesInjectionPoint *) arg;
+
+	if (condition->test_type == SYNC_LOCAL)
+		elog(LOG, "tranche_name = %s tranche_id = %d tranche_index = %d synced_local = %s",
+			 condition->tranche_name,
+			 condition->tranche_id,
+			 condition->tranche_index,
+			 condition->synced_local ? "yes" : "no");
+	else if (condition->test_type == SET_SHARED)
+		elog(LOG, "allocation_reason = %s allocation_size = %d",
+			 condition->initialized ? "initial" : "resize",
+			 condition->allocated);
+#endif
+}
+
+PG_FUNCTION_INFO_V1(test_get_lwlock_identifier);
+Datum
+test_get_lwlock_identifier(PG_FUNCTION_ARGS)
+{
+	int			id = PG_GETARG_INT32(0);
+	uint16		eventid = id + LWTRANCHE_FIRST_USER_DEFINED;
+
+	GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, eventid);
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_tranches_get_first_user_defined);
+Datum
+test_lwlock_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
new file mode 100644
index 00000000000..bf0ecf64376
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
@@ -0,0 +1,6 @@
+# test_lwlock_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_lwlock_tranches'
\ No newline at end of file
-- 
2.43.0

v7-0001-Implement-a-DSA-for-LWLock-tranche-names.patchapplication/octet-stream; name=v7-0001-Implement-a-DSA-for-LWLock-tranche-names.patchDownload
From 9d7157b07f125449141b85e6c0c93127e89e6899 Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Tue, 12 Aug 2025 15:01:55 +0000
Subject: [PATCH v7 1/2] Implement a DSA for LWLock tranche names

Previously, only LWLock tranches registered during postmaster startup were
available to all backends via a tranche name array inherited through fork.
Other backends that wanted to know about a tranche name had to register it
using LWLockRegisterTranche. While this design worked as intended, it was
not ideal because if a backend did not call LWLockRegisterTranche, it would
show a generic "extension" wait_event name in pg_stat_activity instead of
the actual tranche name. This caused inconsistencies with other backends
that registered the tranche name.

Commit fe07100e82b0 made dynamic creation of named DSA areas (and dshash)
much simpler for a backend, so it is anticipated that fewer tranche names
will be registered during postmaster startup in the future, with more
registered dynamically. This will make the above issue more common.

To address this, this change introduces a DSA to store LWLock tranche names
after postmaster startup. When a backend looks up a tranche name by tranche ID,
it first checks its local cache; if the name is not found locally, it fetches
it from the DSA and caches it for future lookups.

This also simplifies the API for registering tranche names: all registration
work is now done via LWLockNewTrancheId, which accepts a tranche name. The
LWLockRegisterTranche call is no longer required.

Additionally, while users should not pass arbitrary tranche IDs (that is,
IDs not created via LWLockNewTrancheId) to LWLockInitialize, nothing
technically prevents them from doing so. Therefore, we must continue to
handle such cases gracefully by returning a default "extension" tranche name.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   4 +-
 doc/src/sgml/xfunc.sgml                       |  20 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/ipc/ipci.c                |   2 +
 src/backend/storage/lmgr/lwlock.c             | 433 +++++++++++++++---
 .../utils/activity/wait_event_names.txt       |   2 +
 src/backend/utils/init/postinit.c             |   2 +
 src/include/storage/lwlock.h                  |  22 +-
 src/include/storage/lwlocklist.h              |   2 +
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   4 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 14 files changed, 418 insertions(+), 113 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..b623589b430 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,8 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
-
 	return found;
 }
 
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index 30219f432d9..32ea6c87be8 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3758,9 +3758,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3778,17 +3779,16 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
+      It is possible to use a <literal>tranche_id</literal> that was not retrieved
+      using <function>LWLockNewTrancheId</function>, but this is not recommended.
+      The ID may clash with an already registered tranche name, or the specified
+      name may not be found. In such cases, looking up the name will return a generic
+      "extension" tranche name.
      </para>
 
      <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..c8e4d55cf80 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -343,6 +343,8 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+
+	LWLockTrancheNamesShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index ec9c345ffdf..7bf455beda4 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -80,6 +80,7 @@
 #include "pg_trace.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
+#include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/proclist.h"
 #include "storage/procnumber.h"
@@ -125,9 +126,12 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. Tranche names are registered in LWLockTrancheNames in
+ * two ways: if registration happens during postmaster startup, the names are
+ * stored directly in the local array; if registration happens afterward, the
+ * names are first stored in shared memory and later cached into the local array
+ * during lookup.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -144,14 +148,6 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 				 LWTRANCHE_FIRST_USER_DEFINED,
 				 "missing entries in BuiltinTrancheNames[]");
 
-/*
- * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, 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 **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
-
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
  * the pointer by fork from the postmaster (except in the EXEC_BACKEND case,
@@ -187,6 +183,51 @@ typedef struct NamedLWLockTrancheRequest
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
 static int	NamedLWLockTrancheRequestsAllocated = 0;
 
+typedef struct LWLockTrancheNamesShmem
+{
+	/* pointer to the DSA shared memory, created in-place */
+	void	   *raw_dsa_area;
+	/* DSA Pointer to a list of DSA pointers, each storing a tranche name */
+	dsa_pointer list_ptr;
+
+	/*
+	 * The number of slots allocated for tranche names. The allocated size
+	 * will grow geometrically to reduce resizing of the list_ptr.
+	 */
+	int			allocated;
+
+	/* lock to protect access to the list */
+	LWLock		lock;
+}			LWLockTrancheNamesShmem;
+
+typedef struct LWLockTrancheNamesStruct
+{
+	/*
+	 * Tranche names allocated in shared memory after postmaster startup, when
+	 * dynamic allocation is allowed.
+	 */
+	LWLockTrancheNamesShmem *shmem;
+	dsa_area   *dsa;
+
+	/*
+	 * Tranche names stored in local backend memory, either during postmaster
+	 * startup or as a local cache afterward.
+	 */
+	const char **local;
+	int			allocated;		/* number of pre-allocated slots for tranche
+								 * names */
+	int			max_used_index; /* max index currently storing a tranche name.
+								 * This is used to know a starting point when
+								 * syncing from shared memory */
+}			LWLockTrancheNamesStruct;
+
+static LWLockTrancheNamesStruct LWLockTrancheNames =
+{
+	NULL, NULL, NULL, 0
+};
+
+#define LWLOCK_TRANCHE_NAMES_INIT_SIZE 16
+
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
  * and the length of the shared-memory NamedLWLockTrancheArray later on.
@@ -201,7 +242,11 @@ NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
+static void SetLocalTrancheName(int tranche_index, const char *tranche_name);
+static void SetSharedTrancheName(int tranche_index, const char *tranche_name);
 static const char *GetLWTrancheName(uint16 trancheId);
+static void LWLockTrancheNamesAttach(void);
+static void LWLockTrancheNamesDetach(void);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -448,11 +493,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -513,7 +553,7 @@ InitializeLWLocks(void)
 			name = trancheNames;
 			trancheNames += strlen(request->tranche_name) + 1;
 			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
+			tranche->trancheId = LWLockNewTrancheId(name);
 			tranche->trancheName = name;
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
@@ -569,61 +609,144 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID and register it with the given tranche
+ * name.  Tranche indexes created by the postmaster will not be used
+ * for a tranche created in shared memory, since the index is generated
+ * from the shared LWLockCounter.
+ *
+ * The tranche name will be user-visible as a wait event name, so use
+ * a name that matches the style of existing wait events.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
+	int			tranche_id;
+	int			tranche_index;
 	int		   *LWLockCounter;
 
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 	/* We use the ShmemLock spinlock to protect LWLockCounter */
 	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
+	tranche_id = (*LWLockCounter)++;
 	SpinLockRelease(ShmemLock);
 
-	return result;
+	tranche_index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
+
+	if (!IsUnderPostmaster)
+		SetLocalTrancheName(tranche_index, tranche_name);
+	else
+		SetSharedTrancheName(tranche_index, tranche_name);
+
+	return tranche_id;
 }
 
 /*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
+ * Adds a tranche name to shared memory. This routine should only be
+ * called from LWLockNewTrancheId.
  */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
+static void
+SetSharedTrancheName(int tranche_index, const char *tranche_name)
 {
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	dsa_pointer *name_ptrs;
+	dsa_pointer str_ptr;
+	char	   *str_addr;
+	int			len;
+	int			current_allocated;
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	current_allocated = LWLockTrancheNames.shmem->allocated;
+
+	/* Tranche array does not exist, initialize it */
+	if (!DsaPointerIsValid(LWLockTrancheNames.shmem->list_ptr))
+	{
+		int			init_alloc = LWLOCK_TRANCHE_NAMES_INIT_SIZE;
+
+		LWLockTrancheNames.shmem->allocated = init_alloc;
+		LWLockTrancheNames.shmem->list_ptr =
+			dsa_allocate0(LWLockTrancheNames.dsa, init_alloc * sizeof(dsa_pointer));
+
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+		memset(name_ptrs, InvalidDsaPointer, init_alloc);
+	}
+
+	/*
+	 * Need to enlarge the tranche array. Allocate a new list, copy the
+	 * current tranche name pointers into it, and update shared memory to
+	 * point to the new list.
+	 */
+	else if (tranche_index >= current_allocated)
 	{
-		int			newalloc;
+		int			new_alloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE,
+													 tranche_index + 1));
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+		dsa_pointer new_list = dsa_allocate(LWLockTrancheNames.dsa,
+											new_alloc * sizeof(dsa_pointer));
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
+		dsa_pointer *old_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+												LWLockTrancheNames.shmem->list_ptr);
+
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list);
+
+		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+		memcpy(name_ptrs, old_ptrs, sizeof(dsa_pointer) * current_allocated);
+
+		dsa_free(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+		LWLockTrancheNames.shmem->list_ptr = new_list;
+		LWLockTrancheNames.shmem->allocated = new_alloc;
+	}
+	/* Use the current list */
+	else
+	{
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+									LWLockTrancheNames.shmem->list_ptr);
+	}
+
+	/* Allocate and copy new string */
+	len = strlen(tranche_name) + 1;
+	str_ptr = dsa_allocate(LWLockTrancheNames.dsa, len);
+	str_addr = dsa_get_address(LWLockTrancheNames.dsa, str_ptr);
+	memcpy(str_addr, tranche_name, len);
+
+	name_ptrs[tranche_index] = str_ptr;
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
+/*
+ * Adds a tranche name to local memory. This routine should only be
+ * called from LWLockNewTrancheId and SyncLWLockTrancheNames.
+ */
+static void
+SetLocalTrancheName(int tranche_index, const char *tranche_name)
+{
+	int			newalloc;
+
+	Assert(tranche_name);
+
+	/* If necessary, create or enlarge array. */
+	if (tranche_index >= LWLockTrancheNames.allocated)
+	{
+		newalloc = pg_nextpower2_32(Max(8, tranche_index + 1));
+
+		if (LWLockTrancheNames.local == NULL)
+		{
+			LWLockTrancheNames.local = (const char **)
 				MemoryContextAllocZero(TopMemoryContext,
 									   newalloc * sizeof(char *));
+		}
 		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+		{
+			LWLockTrancheNames.local =
+				repalloc0_array(LWLockTrancheNames.local, const char *, LWLockTrancheNames.allocated, newalloc);
+		}
+
+		LWLockTrancheNames.allocated = newalloc;
 	}
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	LWLockTrancheNames.local[tranche_index] = tranche_name;
+	LWLockTrancheNames.max_used_index = tranche_index;
 }
 
 /*
@@ -708,28 +831,96 @@ LWLockReportWaitEnd(void)
 	pgstat_report_wait_end();
 }
 
+/*
+ * Synchronize local tranche names with those stored in shared memory.
+ *
+ * Starts syncing from the first unused local index up to the number of
+ * allocated tranche slots in shared memory. If the requested tranche_index
+ * is not found during syncing, sets its name to "extension" as a fallback.
+ *
+ * The tranche_index parameter is the one that triggered syncing. If it is
+ * not found in shared memory, set it to "extension" locally.
+ */
+static void
+SyncLWLockTrancheNames(uint16 tranche_index)
+{
+	bool		requested_id_set = false;
+
+	/* Acquire shared lock on tranche names shared memory */
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_SHARED);
+
+	/* Proceed only if tranche_index is within allocated range */
+	if (tranche_index < LWLockTrancheNames.shmem->allocated)
+	{
+		/* Get array of pointers to tranche names in shared memory */
+		dsa_pointer *pointers = dsa_get_address(LWLockTrancheNames.dsa,
+												LWLockTrancheNames.shmem->list_ptr);
+		int			next_index = LWLockTrancheNames.max_used_index + 1;
+		int			allocated = LWLockTrancheNames.shmem->allocated;
+
+		/* Iterate through tranche entries from next unused index */
+		for (int i = next_index; i < allocated; i++)
+		{
+			const char *tranche_name;
+
+			/*
+			 * Stop if pointer is invalid, indicating no more valid tranche
+			 * names after this point.
+			 */
+			if (!DsaPointerIsValid(pointers[i]))
+				break;
+
+			/* Get tranche name from shared memory */
+			tranche_name = (const char *) dsa_get_address(LWLockTrancheNames.dsa, pointers[i]);
+
+			/* Mark if requested tranche index was found */
+			if (tranche_index == i)
+				requested_id_set = true;
+
+			/* Update local tranche name */
+			SetLocalTrancheName(i, tranche_name);
+		}
+	}
+
+	/*
+	 * If requested tranche index was not found in shared memory, assign it a
+	 * default name of "extension".
+	 */
+	if (!requested_id_set)
+		SetLocalTrancheName(tranche_index, "extension");
+
+	/* Release shared lock */
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
 /*
  * Return the name of an LWLock tranche.
  */
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	const char *tranche_name = NULL;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
+	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * This is an extension tranche, so check LWLockTrancheNames[]. First,
+	 * synchronize the local list with shared memory if necessary, then
+	 * perform the lookup. If the tranche hasn't been registered by the
+	 * extension, return "extension".
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	if (trancheId >= LWLockTrancheNames.allocated ||
+		LWLockTrancheNames.local[trancheId] == NULL)
+		SyncLWLockTrancheNames(trancheId);
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (trancheId < LWLockTrancheNames.allocated)
+		tranche_name = LWLockTrancheNames.local[trancheId];
 
-	return LWLockTrancheNames[trancheId];
+	return tranche_name ? tranche_name : "extension";
 }
 
 /*
@@ -1995,3 +2186,135 @@ LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode)
 	}
 	return false;
 }
+
+/*
+ * Size of the shared memory to create in place the tranche DSA.
+ */
+static Size
+LWLockTrancheNamesInitSize()
+{
+	Size		sz;
+
+	/*
+	 * This value is used by other facilities, see pgstat_shmem.c, so it's
+	 * good enough.
+	 */
+	sz = 256 * 1024;
+
+	Assert(dsa_minimum_size() <= sz);
+
+	return MAXALIGN(sz);
+}
+
+Size
+LWLockTrancheNamesShmemSize(void)
+{
+	Size		sz;
+
+	sz = MAXALIGN(sizeof(LWLockTrancheNamesStruct));
+	sz = add_size(sz, LWLockTrancheNamesInitSize());
+
+	return sz;
+}
+
+/*
+ * Allocate the DSA to store tranche names in dynamic shared memory. This
+ * allocation happens during postmaster startup, so create_in_place is used.
+ * At this stage, the postmaster should only create the DSA and avoid any other
+ * operations, as allocating additional DSM segments will not yet be possible.
+ */
+void
+LWLockTrancheNamesShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize or attach to shared memory structure */
+	LWLockTrancheNames.shmem = (LWLockTrancheNamesShmem *)
+		ShmemInitStruct("Dynamic LWLock tranche names",
+						LWLockTrancheNamesShmemSize(),
+						&found);
+
+	if (!IsUnderPostmaster)
+	{
+		dsa_area   *dsa;
+		LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+		char	   *p = (char *) ctl;
+
+		/* Calculate layout within the shared memory region */
+		p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+		ctl->raw_dsa_area = p;
+		p += MAXALIGN(LWLockTrancheNamesInitSize());
+
+		LWLockInitialize(&ctl->lock, LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST);
+
+		/* Create a dynamic shared memory area in-place */
+		dsa = dsa_create_in_place(ctl->raw_dsa_area,
+								  LWLockTrancheNamesInitSize(),
+								  LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA,
+								  NULL);
+
+		/*
+		 * Postmaster will never access these again, thus free the local
+		 * dsa/dshash references.
+		 */
+		dsa_pin(dsa);
+		dsa_detach(dsa);
+
+		ctl->allocated = 0;
+		ctl->list_ptr = InvalidDsaPointer;
+	}
+	else
+	{
+		Assert(found);
+	}
+}
+
+static void
+LWLockTrancheNamesAttach(void)
+{
+	MemoryContext oldcontext;
+
+	Assert(LWLockTrancheNames.dsa == NULL);
+
+	/* stats shared memory persists for the backend lifetime */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	LWLockTrancheNames.dsa = dsa_attach_in_place(LWLockTrancheNames.shmem->raw_dsa_area,
+												 NULL);
+	dsa_pin_mapping(LWLockTrancheNames.dsa);
+
+	Assert(LWLockTrancheNames.dsa);
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+static void
+LWLockTrancheNamesDetach(void)
+{
+	Assert(LWLockTrancheNames.dsa);
+
+	dsa_detach(LWLockTrancheNames.dsa);
+
+	dsa_release_in_place(LWLockTrancheNames.shmem->raw_dsa_area);
+
+	LWLockTrancheNames.dsa = NULL;
+}
+
+/* Detach the DSA from a backend */
+static void
+LWLockTrancheNamesShutdownHook(int code, Datum arg)
+{
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	LWLockTrancheNamesDetach();
+}
+
+/* Attach the DSA to a backend */
+void
+LWLockTrancheNamesBEInit(void)
+{
+	LWLockTrancheNamesAttach();
+
+	/* Set up a process-exit hook to clean up */
+	before_shmem_exit(LWLockTrancheNamesShutdownHook, 0);
+}
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0be307d2ca0..f60ed927892 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -406,6 +406,8 @@ SubtransSLRU	"Waiting to access the sub-transaction SLRU cache."
 XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
+LWLockDynamicTranchesDSA	"Waiting to access the LWLock tranche names DSA area."
+LWLockDynamicTranchesList	"Waiting to access the list of LWLock tranche names in DSA."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 641e535a73c..1ef04c6b5ec 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -662,6 +662,8 @@ BaseInit(void)
 	 * drop ephemeral slots, which in turn triggers stats reporting.
 	 */
 	ReplicationSlotInitialize();
+
+	LWLockTrancheNamesBEInit();
 }
 
 
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..8a008fc67dd 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -156,22 +156,18 @@ extern void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
- * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * An alternative, more flexible method of obtaining lwlocks is available.
+ * First, call LWLockNewTrancheId once, providing an associated tranche name.
+ * This function allocates a tranche ID from a shared counter. Then, for each
+ * lwlock, call LWLockInitialize once, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
+extern void LWLockTrancheNamesShmemInit(void);
+extern Size LWLockTrancheNamesShmemSize(void);
+extern void LWLockTrancheNamesBEInit(void);
+
 /*
  * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
  * we reserve additional tranche IDs for builtin tranches not included in
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 208d2e3a8ed..5978648c09d 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -135,3 +135,5 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU)
 PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
 PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
 PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_DSA, LWLockDynamicTranchesDSA)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_LIST, LWLockDynamicTranchesList)
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..7f3e6849401 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,8 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
-
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
 
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.43.0

#43Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#42)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Tue, Aug 12, 2025 at 04:16:48PM -0500, Sami Imseih wrote:

I spoke offline with Bertrand and discussed the synchronization of
the local memory from shared memory. After that discussion it is clear
we don't need to track the highest used index in shared memory. Because
the tranche_id comes from a shared counter, it is not possible that
the same tranche_id can be used twice, so we will not have overlap.
That means, when we sync, we just need to know the highest used
index in the local memory ( since that could be populated during
postmaster startup and inherited via fork ) and start syncing local
memory from that point.

Thanks for the updated version!

I think that we can also get rid of max_used_index.

Indeed, I can see that it's updated when SetLocalTrancheName() is called.

But I don't think that's needed as that would be perfectly fine to restart always
from the "initial max_used_index" because that should be rare enough and the number
of entries that we scan is not large. That would mean: entry not found? resync
the entire local cache (starting again from the non updated max_used_index).

So, if we agree that we don't need to update max_used_index in
SetLocalTrancheName(), then during SyncLWLockTrancheNames() we could rely only on
DsaPointerIsValid(pointers[i]).

Indeed, those DSA pointers are only valid for dynamically registered tranches.

For example with max_used_index = 5, what you would find is DsaPointerIsValid(pointers[i])
invalid from 0 to 4 and starts to be valid at [5] (if a tranche has been dynamically
added). 5 is exactly:

"
int next_index = LWLockTrancheNames.max_used_index + 1;
"

So we could do something like:

int i = 0;
while (i < LWLockTrancheNames.shmem->allocated &&
!DsaPointerIsValid(shared_ptrs[i]))
{
i++;
}

Now this "i" acts as the "next_index" in v7 and now we can start iterating
from it and sync until it's invalid again.

That way, we get rid of max_used_index and I think that this part is simpler and
easier to understand.

Thoughts?

Regards,

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

#44Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#43)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

So we could do something like:

int i = 0;
while (i < LWLockTrancheNames.shmem->allocated &&
!DsaPointerIsValid(shared_ptrs[i]))
{
i++;
}

I found a different approach without tracking an additional counter.
Then sync stops when an invalid pointer is found after at least
one valid entry in shared memory, so we don't stop if there was a
lwlock registered in local memory during postmaster startup, and
this tranche will never actually be added to shared memory at
the same position.

Is there any reason to continue allowing this? For example, maybe we could
ERROR if LWLockInitialize()/GetLWTrancheName() are given a tranche_id
greater than the number allocated. I guess I'm not following why we should
gracefully handle these kinds of coding errors, especially when they result
in unhelpful behavior like an "extension" tranche.

Essentially, we can call GetLWTrancheName within LWLockInitialize, but
GetLWTrancheName can no longer have a fallback value such as "extension".
It should error out instead. I believe that is what you mean. correct?

I also added this, so now only LWLocks correctly registered can be looked
up.

I also reconsidered the use of INJECTION_POINT for this purpose.
I felt it added unnecessary code where we only need a DEBUG ( I used DEBUG3)
statement at a few key places.

See v7.

--
Sami

Attachments:

v7-0002-Add-tests-for-LWLock-tranche-names-DSA.patchapplication/octet-stream; name=v7-0002-Add-tests-for-LWLock-tranche-names-DSA.patchDownload
From 65b2b0d752006f08319b533d245ef494a5bd199b Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Fri, 15 Aug 2025 17:53:03 +0000
Subject: [PATCH v7 2/2] Add tests for LWLock tranche names DSA

---
 src/backend/storage/lmgr/lwlock.c             |  15 ++
 src/backend/utils/activity/wait_event.c       |   3 -
 src/include/utils/wait_classes.h              |   2 +
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 .../modules/test_lwlock_tranches/Makefile     |  25 +++
 .../modules/test_lwlock_tranches/meson.build  |  36 ++++
 .../t/001_test_lwlock_tranches.pl             | 181 ++++++++++++++++++
 .../test_lwlock_tranches--1.0.sql             |  16 ++
 .../test_lwlock_tranches.c                    | 102 ++++++++++
 .../test_lwlock_tranches.control              |   6 +
 11 files changed, 386 insertions(+), 4 deletions(-)
 create mode 100644 src/test/modules/test_lwlock_tranches/Makefile
 create mode 100644 src/test/modules/test_lwlock_tranches/meson.build
 create mode 100644 src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control

diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index ceaa4725d00..f1508d34869 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -650,6 +650,7 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 	int			len;
 	int			current_allocated;
 	int			new_alloc = 0;
+	bool		log = false;
 
 	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
 
@@ -666,6 +667,8 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 
 		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
 		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+
+		log = true;
 	}
 
 	/*
@@ -696,6 +699,8 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 
 		LWLockTrancheNames.shmem->list_ptr = new_list;
 		LWLockTrancheNames.shmem->allocated = new_alloc;
+
+		log = true;
 	}
 	/* Use the current list */
 	else
@@ -713,6 +718,9 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 	name_ptrs[tranche_index] = str_ptr;
 
 	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+
+	if (log)
+		elog(DEBUG3, "current_allocated: %d in tranche names shared memory", new_alloc);
 }
 
 /*
@@ -922,6 +930,13 @@ GetLWTrancheName(uint16 trancheId)
 	if (!tranche_name)
 		elog(ERROR, "tranche ID %d is not registered", tranche_id_saved);
 
+	elog(DEBUG3,
+		 "tranche_name %s with tranche_id %d found at index %d. needs_sync: %s",
+		 tranche_name,
+		 tranche_id_saved,
+		 trancheId,
+		 needs_sync ? "true" : "false");
+
 	return tranche_name;
 }
 
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d9b8f34a355..eba7d338c1f 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -39,9 +39,6 @@ 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_CLASS_MASK	0xFF000000
-#define WAIT_EVENT_ID_MASK		0x0000FFFF
-
 /*
  * Hash tables for storing custom wait event ids and their names in
  * shared memory.
diff --git a/src/include/utils/wait_classes.h b/src/include/utils/wait_classes.h
index 51ee68397d5..6ca0504cee1 100644
--- a/src/include/utils/wait_classes.h
+++ b/src/include/utils/wait_classes.h
@@ -10,6 +10,8 @@
 #ifndef WAIT_CLASSES_H
 #define WAIT_CLASSES_H
 
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+#define WAIT_EVENT_ID_MASK		0x0000FFFF
 
 /* ----------
  * Wait Classes
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..e72800cd2b7 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_lwlock_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..f04910d13d7 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_lwlock_tranches')
diff --git a/src/test/modules/test_lwlock_tranches/Makefile b/src/test/modules/test_lwlock_tranches/Makefile
new file mode 100644
index 00000000000..902867229c1
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/Makefile
@@ -0,0 +1,25 @@
+# src/test/modules/test_lwlock_tranches/Makefile
+
+MODULE_big = test_lwlock_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_lwlock_tranches.o
+PGFILEDESC = "test_lwlock_tranches - test code LWLock tranche management"
+
+EXTENSION = test_lwlock_tranches
+DATA = test_lwlock_tranches--1.0.sql
+
+TAP_TESTS = 1
+
+export enable_injection_points
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_lwlock_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_lwlock_tranches/meson.build b/src/test/modules/test_lwlock_tranches/meson.build
new file mode 100644
index 00000000000..f32417a4143
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/meson.build
@@ -0,0 +1,36 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_lwlock_tranches_sources = files(
+  'test_lwlock_tranches.c',
+)
+
+if host_system == 'windows'
+  test_lwlock_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_lwlock_tranches',
+    '--FILEDESC', 'test_lwlock_tranches - test code LWLock tranche management',])
+endif
+
+test_lwlock_tranches = shared_module('test_lwlock_tranches',
+  test_lwlock_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_lwlock_tranches
+
+test_install_data += files(
+  'test_lwlock_tranches.control',
+  'test_lwlock_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_lwlock_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
+    'tests': [
+      't/001_test_lwlock_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
new file mode 100644
index 00000000000..ba3d9b23c74
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
@@ -0,0 +1,181 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $ENABLE_LOG_WAIT = 1;
+
+my $isession;
+my $log_location;
+
+# Helper function: wait for one or more logs if $ENABLE_LOG_WAIT is true
+sub maybe_wait_for_log {
+    my ($node, $logs, $log_loc) = @_;
+    return $log_loc unless $ENABLE_LOG_WAIT;
+
+    # Normalize single regex to array
+    $logs = [$logs] unless ref($logs) eq 'ARRAY';
+
+    foreach my $regex (@$logs) {
+        $log_loc = $node->wait_for_log($regex, $log_loc);
+    }
+    return $log_loc;
+}
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+
+# The largest shared memory allocation we require for testing
+my $final_allocation = 64;
+
+# $unallocated_shared_memory will be overridden by the value
+# of $test_lwlock_tranches_requested_named_tranches when it's > 1
+my $unallocated_shared_memory = 2;
+my $requested_named_tranches = 2;
+
+# VALUES MATCHING INTERNALS
+# The initial array size. Should match LWLOCK_TRANCHE_NAMES_INIT_SIZE
+my $initial_size = 16;
+
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_lwlock_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_lwlock_tranches.requested_named_tranches=$requested_named_tranches));
+$node->append_conf('postgresql.conf',
+    qq(log_min_messages=DEBUG3));
+$node->start();
+
+$node->safe_psql('postgres', q(CREATE EXTENSION test_lwlock_tranches));
+
+my $first_user_defined = $node->safe_psql(
+    'postgres',
+    "SELECT test_lwlock_tranches_get_first_user_defined()"
+);
+
+my $next_index = 0;
+my $lookup_tranche_id = 0;
+my $current_size = $initial_size;
+
+$log_location = -s $node->logfile;
+
+if ($requested_named_tranches > 0) {
+    $unallocated_shared_memory += $requested_named_tranches;
+}
+
+if ($unallocated_shared_memory > 0) {
+    while ($current_size < $final_allocation) {
+        $current_size *= 2;
+    }
+
+    # Lookup before allocating shared memory
+    if ($requested_named_tranches > 0) {
+        for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+            $lookup_tranche_id = $next_index + $first_user_defined;
+            $node->safe_psql('postgres',
+                qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+            $log_location = maybe_wait_for_log(
+                $node,
+                qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: false/,
+                $log_location
+            );
+        }
+        ok(1, "requested_named_tranches looked up from local cache, before allocating shared memory");
+    }
+
+    # Create new tranches
+    $node->safe_psql('postgres',
+        qq{select test_lwlock_new_tranche_id($current_size - $unallocated_shared_memory)});
+    $log_location = maybe_wait_for_log(
+        $node,
+        qr/ DEBUG:  current_allocated: $final_allocation in tranche names shared memory/, $log_location
+    );
+
+    ok(1, "resize shared memory with allocation up to $current_size tranche names");
+
+    # Lookup after allocating shared memory
+    if ($requested_named_tranches > 0) {
+        for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+            $lookup_tranche_id = $next_index + $first_user_defined;
+            $node->safe_psql('postgres',
+                qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+            $log_location = maybe_wait_for_log(
+                $node,
+                qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: false/,
+                $log_location
+            );
+        }
+        ok(1, "requested_named_tranches looked up from local cache, after allocating shared memory");
+    }
+
+    $next_index = $requested_named_tranches;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    my $second_user_defined = $first_user_defined + 1;
+    $node->safe_psql('postgres',
+        qq{select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($first_user_defined);
+           select test_get_lwlock_identifier($second_user_defined)}
+    );
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: true/, $log_location);
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: false/, $log_location);
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: false/, $log_location);
+
+    for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+        $lookup_tranche_id = $first_user_defined + $next_index;
+        maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: false/, $log_location);
+    }
+    ok(1, "lookup with synchronization is successful");
+
+    $log_location = -s $node->logfile;
+
+    # Lookup unregistered tranches
+    $isession = $node->interactive_psql('postgres');
+
+    $next_index = $current_size - $unallocated_shared_memory;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    $isession->query(qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+    $log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $lookup_tranche_id is not registered/, $log_location);
+    ok(1, "test lookup within shared memory allocations");
+
+    $next_index = $current_size;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    $isession = $node->interactive_psql('postgres');
+    $isession->query(qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+    $log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $lookup_tranche_id is not registered/, $log_location);
+    ok(1, "test lookup outside shared memory allocations");
+
+    $isession->quit;
+}
+
+# Lookup when no tranche names are registered
+$node->append_conf('postgresql.conf',
+    qq(test_lwlock_tranches.requested_named_tranches=0));
+$node->stop();
+$node->start();
+
+my $last_possible = 65535 - $first_user_defined;
+my ($id1, $id2, $id3, $id4, $id5) = (0, $first_user_defined - 1, $first_user_defined, min($last_possible, $first_user_defined + 5000), $last_possible);
+
+$isession = $node->interactive_psql('postgres');
+
+$isession->query(qq{select test_get_lwlock_identifier($id1);});
+$isession->query(qq{select test_get_lwlock_identifier($id2);});
+ok(1, "check for no error when looking up built-in names");
+
+$isession->query(qq{select test_get_lwlock_identifier($id3);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id3 is not registered/, $log_location);
+
+$isession->query(qq{select test_get_lwlock_identifier($id4);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id4 is not registered/, $log_location);
+
+$isession->query(qq{select test_get_lwlock_identifier($id5);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id5 is not registered/, $log_location);
+
+$isession->quit;
+ok(1, "check for error looking up user-defined names");
+
+done_testing();
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
new file mode 100644
index 00000000000..7fdf90e3e23
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
@@ -0,0 +1,16 @@
+-- test_lwlock_tranches--1.0.sql
+
+CREATE FUNCTION test_lwlock_new_tranche_id(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_lwlock_new_tranche_id'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_get_lwlock_identifier(int)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_get_lwlock_identifier'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_lwlock_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_lwlock_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
new file mode 100644
index 00000000000..654ca9a5f44
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
@@ -0,0 +1,102 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_lwlock_tranches_shmem_request(void);
+static void test_lwlock_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_lwlock_tranches_requested_named_tranches = 0;
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_lwlock_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_lwlock_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_lwlock_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_lwlock_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_lwlock_tranches_shmem_startup(void)
+{
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+}
+
+static void
+test_lwlock_tranches_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	for (int i = 0; i < test_lwlock_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_new_tranche_id);
+Datum
+test_lwlock_new_tranche_id(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+
+	for (int i = test_lwlock_tranches_requested_named_tranches; i < num; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_get_lwlock_identifier);
+Datum
+test_get_lwlock_identifier(PG_FUNCTION_ARGS)
+{
+	GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_tranches_get_first_user_defined);
+Datum
+test_lwlock_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
new file mode 100644
index 00000000000..bf0ecf64376
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
@@ -0,0 +1,6 @@
+# test_lwlock_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_lwlock_tranches'
\ No newline at end of file
-- 
2.43.0

v7-0001-Implement-a-DSA-for-LWLock-tranche-names.patchapplication/octet-stream; name=v7-0001-Implement-a-DSA-for-LWLock-tranche-names.patchDownload
From 8c4d5344d1b2a0b4336ca9a76fc6278d43687f3e Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Fri, 15 Aug 2025 16:48:10 +0000
Subject: [PATCH v7 1/2] Implement a DSA for LWLock tranche names

Previously, only LWLock tranches registered during postmaster startup were
available to all backends via a tranche name array inherited through fork.
Other backends that wanted to know about a tranche name had to register it
using LWLockRegisterTranche. While this design worked as intended, it was
not ideal because if a backend did not call LWLockRegisterTranche, it would
show a generic "extension" wait_event name in pg_stat_activity instead of
the actual tranche name. This caused inconsistencies with other backends
that registered the tranche name.

Commit fe07100e82b0 made dynamic creation of named DSA areas (and dshash)
much simpler for a backend, so it is anticipated that fewer tranche names
will be registered during postmaster startup in the future, with more
registered dynamically. This will make the above issue more common.

To address this, this change introduces a DSA to store LWLock tranche names
after postmaster startup. When a backend looks up a tranche name by tranche ID,
it first checks its local cache; if the name is not found locally, it fetches
it from the DSA and caches it for future lookups.

This also simplifies the API for registering tranche names: all registration
work is now done via LWLockNewTrancheId, which accepts a tranche name. The
LWLockRegisterTranche call is no longer required.

Additionally, while users should not pass arbitrary tranche IDs (that is,
IDs not created via LWLockNewTrancheId) to LWLockInitialize, nothing
technically prevents them from doing so. Therefore, we must continue to
handle such cases gracefully by returning a default "extension" tranche name.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   4 +-
 doc/src/sgml/xfunc.sgml                       |  18 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/ipc/ipci.c                |   4 +
 src/backend/storage/lmgr/lwlock.c             | 441 +++++++++++++++---
 .../utils/activity/wait_event_names.txt       |   2 +
 src/backend/utils/init/postinit.c             |   2 +
 src/include/storage/lwlock.h                  |  22 +-
 src/include/storage/lwlocklist.h              |   2 +
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   4 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 14 files changed, 422 insertions(+), 117 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..b623589b430 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,8 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
-
 	return found;
 }
 
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..20b0767f204 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3757,9 +3757,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3778,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..2f0df67bf7a 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -184,6 +184,8 @@ AttachSharedMemoryStructs(void)
 
 	CreateOrAttachShmemStructs();
 
+	LWLockTrancheNamesBEInit();
+
 	/*
 	 * Now give loadable modules a chance to set up their shmem allocations
 	 */
@@ -343,6 +345,8 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+
+	LWLockTrancheNamesShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index ec9c345ffdf..ceaa4725d00 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -80,6 +80,7 @@
 #include "pg_trace.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
+#include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/proclist.h"
 #include "storage/procnumber.h"
@@ -125,9 +126,12 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. Tranche names are registered in LWLockTrancheNames in
+ * two ways: if registration happens during postmaster startup, the names are
+ * stored directly in the local array; if registration happens afterward, the
+ * names are first stored in shared memory and later cached into the local array
+ * during lookup.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -144,14 +148,6 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 				 LWTRANCHE_FIRST_USER_DEFINED,
 				 "missing entries in BuiltinTrancheNames[]");
 
-/*
- * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, 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 **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
-
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
  * the pointer by fork from the postmaster (except in the EXEC_BACKEND case,
@@ -187,6 +183,48 @@ typedef struct NamedLWLockTrancheRequest
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
 static int	NamedLWLockTrancheRequestsAllocated = 0;
 
+typedef struct LWLockTrancheNamesShmem
+{
+	/* pointer to the DSA shared memory, created in-place */
+	void	   *raw_dsa_area;
+	/* DSA Pointer to a list of DSA pointers, each storing a tranche name */
+	dsa_pointer list_ptr;
+
+	/*
+	 * The number of slots allocated for tranche names. The allocated size
+	 * will grow geometrically to reduce resizing of the list_ptr.
+	 */
+	int			allocated;
+
+	/* lock to protect access to the list */
+	LWLock		lock;
+}			LWLockTrancheNamesShmem;
+
+typedef struct LWLockTrancheNamesStruct
+{
+	/*
+	 * Tranche names allocated in shared memory after postmaster startup, when
+	 * dynamic allocation is allowed.
+	 */
+	LWLockTrancheNamesShmem *shmem;
+	dsa_area   *dsa;
+
+	/*
+	 * Tranche names stored in local backend memory, either during postmaster
+	 * startup or as a local cache afterward.
+	 */
+	const char **local;
+	int			allocated;		/* number of pre-allocated slots for tranche
+								 * names */
+}			LWLockTrancheNamesStruct;
+
+static LWLockTrancheNamesStruct LWLockTrancheNames =
+{
+	NULL, NULL, NULL, 0
+};
+
+#define LWLOCK_TRANCHE_NAMES_INIT_SIZE 16
+
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
  * and the length of the shared-memory NamedLWLockTrancheArray later on.
@@ -201,7 +239,11 @@ NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
+static void SetLocalTrancheName(int tranche_index, const char *tranche_name);
+static void SetSharedTrancheName(int tranche_index, const char *tranche_name);
 static const char *GetLWTrancheName(uint16 trancheId);
+static void LWLockTrancheNamesAttach(void);
+static void LWLockTrancheNamesDetach(void);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -448,11 +490,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -513,7 +550,7 @@ InitializeLWLocks(void)
 			name = trancheNames;
 			trancheNames += strlen(request->tranche_name) + 1;
 			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
+			tranche->trancheId = LWLockNewTrancheId(name);
 			tranche->trancheName = name;
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
@@ -569,61 +606,147 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID and register it with the given tranche
+ * name.  Tranche IDs created by the postmaster will not be used
+ * for a tranche created in shared memory, since the index is generated
+ * from the shared LWLockCounter.
+ *
+ * The tranche name will be user-visible as a wait event name, so use
+ * a name that matches the style of existing wait events.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
+	int			tranche_id;
+	int			tranche_index;
 	int		   *LWLockCounter;
 
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 	/* We use the ShmemLock spinlock to protect LWLockCounter */
 	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
+	tranche_id = (*LWLockCounter)++;
 	SpinLockRelease(ShmemLock);
 
-	return result;
+	tranche_index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
+
+	if (IsUnderPostmaster || !IsPostmasterEnvironment)
+		SetSharedTrancheName(tranche_index, tranche_name);
+	else
+		SetLocalTrancheName(tranche_index, tranche_name);
+
+	return tranche_id;
 }
 
 /*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
+ * Adds a tranche name to shared memory. This routine should only be
+ * called from LWLockNewTrancheId.
  */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
+static void
+SetSharedTrancheName(int tranche_index, const char *tranche_name)
 {
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	dsa_pointer *name_ptrs;
+	dsa_pointer str_ptr;
+	char	   *str_addr;
+	int			len;
+	int			current_allocated;
+	int			new_alloc = 0;
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	current_allocated = LWLockTrancheNames.shmem->allocated;
+
+	/* Tranche array does not exist, initialize it */
+	if (!DsaPointerIsValid(LWLockTrancheNames.shmem->list_ptr))
+	{
+		new_alloc = LWLOCK_TRANCHE_NAMES_INIT_SIZE;
+
+		LWLockTrancheNames.shmem->allocated = new_alloc;
+		LWLockTrancheNames.shmem->list_ptr =
+			dsa_allocate0(LWLockTrancheNames.dsa, new_alloc * sizeof(dsa_pointer));
+
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+	}
+
+	/*
+	 * Need to enlarge the tranche array. Allocate a new list, copy the
+	 * current tranche name pointers into it, and update shared memory to
+	 * point to the new list.
+	 */
+	else if (tranche_index >= current_allocated)
 	{
-		int			newalloc;
+		dsa_pointer new_list;
+		dsa_pointer *old_ptrs;
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+		new_alloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE,
+										 tranche_index + 1));
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
+		new_list = dsa_allocate(LWLockTrancheNames.dsa,
+								new_alloc * sizeof(dsa_pointer));
+
+		old_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+								   LWLockTrancheNames.shmem->list_ptr);
+
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list);
+
+		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+		memcpy(name_ptrs, old_ptrs, sizeof(dsa_pointer) * current_allocated);
+
+		dsa_free(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+		LWLockTrancheNames.shmem->list_ptr = new_list;
+		LWLockTrancheNames.shmem->allocated = new_alloc;
+	}
+	/* Use the current list */
+	else
+	{
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+									LWLockTrancheNames.shmem->list_ptr);
+	}
+
+	/* Allocate and copy new string */
+	len = strlen(tranche_name) + 1;
+	str_ptr = dsa_allocate(LWLockTrancheNames.dsa, len);
+	str_addr = dsa_get_address(LWLockTrancheNames.dsa, str_ptr);
+	memcpy(str_addr, tranche_name, len);
+
+	name_ptrs[tranche_index] = str_ptr;
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
+/*
+ * Adds a tranche name to local memory. This routine should only be
+ * called from LWLockNewTrancheId and SyncLWLockTrancheNames.
+ */
+static void
+SetLocalTrancheName(int tranche_index, const char *tranche_name)
+{
+	int			newalloc;
+
+	Assert(tranche_name);
+
+	/* If necessary, create or enlarge array. */
+	if (tranche_index >= LWLockTrancheNames.allocated)
+	{
+		newalloc = pg_nextpower2_32(Max(8, tranche_index + 1));
+
+		if (LWLockTrancheNames.local == NULL)
+		{
+			LWLockTrancheNames.local = (const char **)
 				MemoryContextAllocZero(TopMemoryContext,
 									   newalloc * sizeof(char *));
+		}
 		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+		{
+			LWLockTrancheNames.local =
+				repalloc0_array(LWLockTrancheNames.local, const char *, LWLockTrancheNames.allocated, newalloc);
+		}
+
+		LWLockTrancheNames.allocated = newalloc;
 	}
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	LWLockTrancheNames.local[tranche_index] = tranche_name;
 }
 
 /*
@@ -674,10 +797,15 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 
 /*
  * LWLockInitialize - initialize a new lwlock; it's initially unlocked
+ *
+ * We check if the tranche_id exists. GetLWTrancheName will raise an
+ * error if it does not.
  */
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -709,27 +837,92 @@ LWLockReportWaitEnd(void)
 }
 
 /*
- * Return the name of an LWLock tranche.
+ * Update the local cache of LWLock tranche names from the list in shared
+ * memory.
+ *
+ * Iteration stops when an invalid pointer is found after at least
+ * one valid entry, because tranche indexes at the start of shared memory
+ * may have been added to the local cache directly during postmaster
+ * initialization, before they were added to shared memory, so those indexes
+ * remain invalid in shared memory.
+ *
+ * Each valid name encountered is copied into the local cache.
+ */
+static void
+SyncLWLockTrancheNames(void)
+{
+	dsa_pointer *pointers = dsa_get_address(LWLockTrancheNames.dsa,
+											LWLockTrancheNames.shmem->list_ptr);
+	bool		found_valid = false;
+
+	/* Acquire shared lock on tranche names shared memory */
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_SHARED);
+
+	for (int i = 0; i < LWLockTrancheNames.shmem->allocated; i++)
+	{
+		const char *tranche_name;
+
+		/*
+		 * Stop iteration if the pointer is invalid, but only after at least
+		 * one valid entry has been processed.
+		 */
+		if (!DsaPointerIsValid(pointers[i]))
+		{
+			if (found_valid)
+				break;
+			continue;
+		}
+
+		/* Get tranche name from shared memory */
+		tranche_name = (const char *) dsa_get_address(LWLockTrancheNames.dsa, pointers[i]);
+
+		/* Update local tranche name */
+		SetLocalTrancheName(i, tranche_name);
+
+		found_valid = true;
+	}
+
+	/* Release shared lock */
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
+/*
+ * Return the name of an LWLock tranche; or raise an error
+ * if it not found.
  */
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	const char *tranche_name = NULL;
+	uint16		tranche_id_saved = trancheId;
+	bool		needs_sync = false;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
+	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * This is an extension tranche, so check LWLockTrancheNames[]. First,
+	 * sync the local list with shared memory (if not under postmaster or
+	 * single-user) if needed, then perform the lookup. If the tranche ID is
+	 * not found, error out.
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	needs_sync = (trancheId >= LWLockTrancheNames.allocated ||
+				  LWLockTrancheNames.local[trancheId] == NULL)
+		&& (IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	if (needs_sync)
+		SyncLWLockTrancheNames();
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (trancheId < LWLockTrancheNames.allocated)
+		tranche_name = LWLockTrancheNames.local[trancheId];
 
-	return LWLockTrancheNames[trancheId];
+	if (!tranche_name)
+		elog(ERROR, "tranche ID %d is not registered", tranche_id_saved);
+
+	return tranche_name;
 }
 
 /*
@@ -1995,3 +2188,139 @@ LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode)
 	}
 	return false;
 }
+
+/*
+ * Size of the shared memory to create in place the tranche DSA.
+ */
+static Size
+LWLockTrancheNamesInitSize()
+{
+	Size		sz;
+
+	/*
+	 * This value is used by other facilities, see pgstat_shmem.c, so it's
+	 * good enough.
+	 */
+	sz = 256 * 1024;
+
+	Assert(dsa_minimum_size() <= sz);
+
+	return MAXALIGN(sz);
+}
+
+Size
+LWLockTrancheNamesShmemSize(void)
+{
+	Size		sz;
+
+	sz = MAXALIGN(sizeof(LWLockTrancheNamesStruct));
+	sz = add_size(sz, LWLockTrancheNamesInitSize());
+
+	return sz;
+}
+
+/*
+ * Allocate the DSA to store tranche names in dynamic shared memory. This
+ * allocation happens during postmaster startup, so create_in_place is used.
+ * At this stage, the postmaster should only create the DSA and avoid any other
+ * operations, as allocating additional DSM segments will not yet be possible.
+ */
+void
+LWLockTrancheNamesShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize or attach to shared memory structure */
+	LWLockTrancheNames.shmem = (LWLockTrancheNamesShmem *)
+		ShmemInitStruct("Dynamic LWLock tranche names",
+						LWLockTrancheNamesShmemSize(),
+						&found);
+
+	if (!IsUnderPostmaster)
+	{
+		dsa_area   *dsa;
+		LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+		char	   *p = (char *) ctl;
+
+		/* Calculate layout within the shared memory region */
+		p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+		ctl->raw_dsa_area = p;
+		p += MAXALIGN(LWLockTrancheNamesInitSize());
+
+		LWLockInitialize(&ctl->lock, LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST);
+
+		/* Create a dynamic shared memory area in-place */
+		dsa = dsa_create_in_place(ctl->raw_dsa_area,
+								  LWLockTrancheNamesInitSize(),
+								  LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA,
+								  NULL);
+
+		/*
+		 * Postmaster will never access these again, thus free the local
+		 * dsa/dshash references.
+		 */
+		dsa_pin(dsa);
+		dsa_detach(dsa);
+
+		ctl->allocated = 0;
+		ctl->list_ptr = InvalidDsaPointer;
+	}
+	else
+	{
+		Assert(found);
+	}
+}
+
+static void
+LWLockTrancheNamesAttach(void)
+{
+	MemoryContext oldcontext;
+
+	Assert(LWLockTrancheNames.dsa == NULL);
+
+	/* stats shared memory persists for the backend lifetime */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	LWLockTrancheNames.dsa = dsa_attach_in_place(LWLockTrancheNames.shmem->raw_dsa_area,
+												 NULL);
+	dsa_pin_mapping(LWLockTrancheNames.dsa);
+
+	Assert(LWLockTrancheNames.dsa);
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+static void
+LWLockTrancheNamesDetach(void)
+{
+	Assert(LWLockTrancheNames.dsa);
+
+	dsa_detach(LWLockTrancheNames.dsa);
+
+	dsa_release_in_place(LWLockTrancheNames.shmem->raw_dsa_area);
+
+	LWLockTrancheNames.dsa = NULL;
+}
+
+/* Detach the DSA from a backend */
+static void
+LWLockTrancheNamesShutdownHook(int code, Datum arg)
+{
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	LWLockTrancheNamesDetach();
+}
+
+/* Attach the DSA to a backend */
+void
+LWLockTrancheNamesBEInit(void)
+{
+	/* already attached, nothing to do */
+	if (LWLockTrancheNames.dsa)
+		return;
+
+	LWLockTrancheNamesAttach();
+
+	/* Set up a process-exit hook to clean up */
+	before_shmem_exit(LWLockTrancheNamesShutdownHook, 0);
+}
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0be307d2ca0..f60ed927892 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -406,6 +406,8 @@ SubtransSLRU	"Waiting to access the sub-transaction SLRU cache."
 XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
+LWLockDynamicTranchesDSA	"Waiting to access the LWLock tranche names DSA area."
+LWLockDynamicTranchesList	"Waiting to access the list of LWLock tranche names in DSA."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 641e535a73c..1ef04c6b5ec 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -662,6 +662,8 @@ BaseInit(void)
 	 * drop ephemeral slots, which in turn triggers stats reporting.
 	 */
 	ReplicationSlotInitialize();
+
+	LWLockTrancheNamesBEInit();
 }
 
 
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..8a008fc67dd 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -156,22 +156,18 @@ extern void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
- * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * An alternative, more flexible method of obtaining lwlocks is available.
+ * First, call LWLockNewTrancheId once, providing an associated tranche name.
+ * This function allocates a tranche ID from a shared counter. Then, for each
+ * lwlock, call LWLockInitialize once, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
+extern void LWLockTrancheNamesShmemInit(void);
+extern Size LWLockTrancheNamesShmemSize(void);
+extern void LWLockTrancheNamesBEInit(void);
+
 /*
  * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
  * we reserve additional tranche IDs for builtin tranches not included in
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 208d2e3a8ed..5978648c09d 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -135,3 +135,5 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU)
 PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
 PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
 PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_DSA, LWLockDynamicTranchesDSA)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_LIST, LWLockDynamicTranchesList)
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..7f3e6849401 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,8 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
-
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
 
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.43.0

#45Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#44)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

See v7.

I fixed an earlier issue with Windows, which was due to not initializing the
shared memory inside

```
#ifdef EXEC_BACKEND
extern void AttachSharedMemoryStructs(void);
#endif
```

But then I found another one after. LWLockTrancheNames gets forked on Linux,
but of course that will not happen on Windows without extra work. So, the
tests failed because the requested tranches (via RequestNamedLWLockTranche)
were not being found when looked up.

But of course, we already have provisions to copy these tranches for
Windows ( see inside launch_backend.c ).

```
int NamedLWLockTrancheRequests;
NamedLWLockTranche *NamedLWLockTrancheArray;
LWLockPadded *MainLWLockArray;
```

So, this means that for the local memory sync, we can actually just copy the
requested tranches (via RequestNamedLWLockTranche) and then the shared memory
tranches. This is much better, as it syncs using both possible sources
for tranche names.

```
int i = 0;

while (i < NamedLWLockTrancheRequests)
{
NamedLWLockTranche *tranche;

tranche = &NamedLWLockTrancheArray[i];

SetLocalTrancheName(i, tranche->trancheName);

i++;
}

/* Acquire shared lock on tranche names shared memory */
LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_SHARED);

while (i < LWLockTrancheNames.shmem->allocated)
{
```

So, now these tests pass locally on Windows.

Attached is v8.

--

Sami

Attachments:

v8-0002-Add-tests-for-LWLock-tranche-names-DSA.patchapplication/octet-stream; name=v8-0002-Add-tests-for-LWLock-tranche-names-DSA.patchDownload
From d8c251401b31e61458828fa1447556f9f8f8e02a Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Fri, 15 Aug 2025 17:53:03 +0000
Subject: [PATCH v8 2/2] Add tests for LWLock tranche names DSA

---
 src/backend/storage/lmgr/lwlock.c             |  15 ++
 src/backend/utils/activity/wait_event.c       |   3 -
 src/include/utils/wait_classes.h              |   2 +
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 .../modules/test_lwlock_tranches/Makefile     |  23 +++
 .../expected/test_lwlock_tranches.out         |  20 ++
 .../modules/test_lwlock_tranches/meson.build  |  33 ++++
 .../t/001_test_lwlock_tranches.pl             | 173 ++++++++++++++++++
 .../test_lwlock_tranches--1.0.sql             |  16 ++
 .../test_lwlock_tranches.c                    | 102 +++++++++++
 .../test_lwlock_tranches.conf                 |   2 +
 .../test_lwlock_tranches.control              |   6 +
 13 files changed, 395 insertions(+), 4 deletions(-)
 create mode 100644 src/test/modules/test_lwlock_tranches/Makefile
 create mode 100644 src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out
 create mode 100644 src/test/modules/test_lwlock_tranches/meson.build
 create mode 100644 src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control

diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 93fddeed141..691ca302125 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -650,6 +650,7 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 	int			len;
 	int			current_allocated;
 	int			new_alloc = 0;
+	bool		log = false;
 
 	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
 
@@ -666,6 +667,8 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 
 		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
 		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+
+		log = true;
 	}
 
 	/*
@@ -696,6 +699,8 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 
 		LWLockTrancheNames.shmem->list_ptr = new_list;
 		LWLockTrancheNames.shmem->allocated = new_alloc;
+
+		log = true;
 	}
 	/* Use the current list */
 	else
@@ -713,6 +718,9 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 	name_ptrs[tranche_index] = str_ptr;
 
 	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+
+	if (log)
+		elog(DEBUG3, "current_allocated: %d in tranche names shared memory", new_alloc);
 }
 
 /*
@@ -921,6 +929,13 @@ GetLWTrancheName(uint16 trancheId)
 	if (!tranche_name)
 		elog(ERROR, "tranche ID %d is not registered", tranche_id_saved);
 
+	elog(DEBUG3,
+		 "tranche_name %s with tranche_id %d found at index %d. needs_sync: %s",
+		 tranche_name,
+		 tranche_id_saved,
+		 trancheId,
+		 needs_sync ? "true" : "false");
+
 	return tranche_name;
 }
 
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d9b8f34a355..eba7d338c1f 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -39,9 +39,6 @@ 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_CLASS_MASK	0xFF000000
-#define WAIT_EVENT_ID_MASK		0x0000FFFF
-
 /*
  * Hash tables for storing custom wait event ids and their names in
  * shared memory.
diff --git a/src/include/utils/wait_classes.h b/src/include/utils/wait_classes.h
index 51ee68397d5..6ca0504cee1 100644
--- a/src/include/utils/wait_classes.h
+++ b/src/include/utils/wait_classes.h
@@ -10,6 +10,8 @@
 #ifndef WAIT_CLASSES_H
 #define WAIT_CLASSES_H
 
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+#define WAIT_EVENT_ID_MASK		0x0000FFFF
 
 /* ----------
  * Wait Classes
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..e72800cd2b7 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_lwlock_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..f04910d13d7 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_lwlock_tranches')
diff --git a/src/test/modules/test_lwlock_tranches/Makefile b/src/test/modules/test_lwlock_tranches/Makefile
new file mode 100644
index 00000000000..9867c99cbfd
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_lwlock_tranches/Makefile
+
+MODULE_big = test_lwlock_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_lwlock_tranches.o
+PGFILEDESC = "test_lwlock_tranches - test code LWLock tranche management"
+
+EXTENSION = test_lwlock_tranches
+DATA = test_lwlock_tranches--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_lwlock_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out b/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out
new file mode 100644
index 00000000000..337a8438a59
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out
@@ -0,0 +1,20 @@
+CREATE EXTENSION test_lwlock_tranches;
+SELECT test_lwlock_tranches_get_first_user_defined() AS num_user_defined \gset
+SELECT :num_user_defined;
+ ?column? 
+----------
+       97
+(1 row)
+
+SELECT test_get_lwlock_identifier(:num_user_defined);
+ test_get_lwlock_identifier 
+----------------------------
+ 
+(1 row)
+
+SELECT test_get_lwlock_identifier(:num_user_defined + 1);
+ test_get_lwlock_identifier 
+----------------------------
+ 
+(1 row)
+
diff --git a/src/test/modules/test_lwlock_tranches/meson.build b/src/test/modules/test_lwlock_tranches/meson.build
new file mode 100644
index 00000000000..f8aa700b58f
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_lwlock_tranches_sources = files(
+  'test_lwlock_tranches.c',
+)
+
+if host_system == 'windows'
+  test_lwlock_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_lwlock_tranches',
+    '--FILEDESC', 'test_lwlock_tranches - test code LWLock tranche management',])
+endif
+
+test_lwlock_tranches = shared_module('test_lwlock_tranches',
+  test_lwlock_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_lwlock_tranches
+
+test_install_data += files(
+  'test_lwlock_tranches.control',
+  'test_lwlock_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_lwlock_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_test_lwlock_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
new file mode 100644
index 00000000000..f59925ea0b4
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
@@ -0,0 +1,173 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $ENABLE_LOG_WAIT = 1;
+
+my $isession;
+my $log_location;
+
+# Helper function: wait for one or more logs if $ENABLE_LOG_WAIT is true
+sub maybe_wait_for_log {
+    my ($node, $logs, $log_loc) = @_;
+    return $log_loc unless $ENABLE_LOG_WAIT;
+
+    # Normalize single regex to array
+    $logs = [$logs] unless ref($logs) eq 'ARRAY';
+
+    foreach my $regex (@$logs) {
+        $log_loc = $node->wait_for_log($regex, $log_loc);
+    }
+    return $log_loc;
+}
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+
+# The largest shared memory allocation we require for testing
+my $final_allocation = 64;
+
+# $unallocated_shared_memory will be overridden by the value
+# of $test_lwlock_tranches_requested_named_tranches when it's > 1
+my $unallocated_shared_memory = 2;
+my $requested_named_tranches = 2;
+
+# VALUES MATCHING INTERNALS
+# The initial array size. Should match LWLOCK_TRANCHE_NAMES_INIT_SIZE
+my $initial_size = 16;
+
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_lwlock_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_lwlock_tranches.requested_named_tranches=$requested_named_tranches));
+$node->append_conf('postgresql.conf',
+    qq(log_min_messages=DEBUG3));
+$node->start();
+
+$node->safe_psql('postgres', q(CREATE EXTENSION test_lwlock_tranches));
+
+my $first_user_defined = $node->safe_psql(
+    'postgres',
+    "SELECT test_lwlock_tranches_get_first_user_defined()"
+);
+
+my $next_index = 0;
+my $lookup_tranche_id = 0;
+my $current_size = $initial_size;
+
+$log_location = -s $node->logfile;
+
+if ($requested_named_tranches > 0) {
+    $unallocated_shared_memory += $requested_named_tranches;
+}
+
+if ($unallocated_shared_memory > 0) {
+    while ($current_size < $final_allocation) {
+        $current_size *= 2;
+    }
+
+    # Lookup before allocating shared memory
+    if ($requested_named_tranches > 0) {
+        for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+            $lookup_tranche_id = $next_index + $first_user_defined;
+            $node->safe_psql('postgres',
+                qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+            $log_location = maybe_wait_for_log(
+                $node,
+                qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index./,
+                $log_location
+            );
+        }
+        ok(1, "requested_named_tranches looked up from local cache, before allocating shared memory");
+    }
+
+    # Create new tranches
+    $node->safe_psql('postgres',
+        qq{select test_lwlock_new_tranche_id($current_size - $unallocated_shared_memory)});
+    $log_location = maybe_wait_for_log(
+        $node,
+        qr/ DEBUG:  current_allocated: $final_allocation in tranche names shared memory/, $log_location
+    );
+
+    ok(1, "resize shared memory with allocation up to $current_size tranche names");
+
+    # Lookup after allocating shared memory
+    if ($requested_named_tranches > 0) {
+        for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+            $lookup_tranche_id = $next_index + $first_user_defined;
+            $node->safe_psql('postgres',
+                qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+            $log_location = maybe_wait_for_log(
+                $node,
+                qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index./,
+                $log_location
+            );
+        }
+        ok(1, "requested_named_tranches looked up from local cache, after allocating shared memory");
+    }
+
+    $next_index = $requested_named_tranches;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    my $second_user_defined = $first_user_defined + 1;
+    $node->safe_psql('postgres',
+        qq{select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($first_user_defined);
+           select test_get_lwlock_identifier($second_user_defined)}
+    );
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: true/, $log_location);
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: false/, $log_location);
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: false/, $log_location);
+
+    for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+        $lookup_tranche_id = $first_user_defined + $next_index;
+        maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index./, $log_location);
+    }
+    ok(1, "lookup with synchronization is successful");
+
+    $log_location = -s $node->logfile;
+
+    # Lookup unregistered tranches
+    $next_index = $current_size - $unallocated_shared_memory;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    $node->psql('postgres', qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+    $log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $lookup_tranche_id is not registered/, $log_location);
+    ok(1, "test lookup within shared memory allocations");
+
+    $next_index = $current_size;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    $node->psql('postgres', qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+    $log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $lookup_tranche_id is not registered/, $log_location);
+    ok(1, "test lookup outside shared memory allocations");
+}
+
+# Lookup when no tranche names are registered
+$node->append_conf('postgresql.conf',
+    qq(test_lwlock_tranches.requested_named_tranches=0));
+$node->stop();
+$node->start();
+
+my $last_possible = 65535 - $first_user_defined;
+my ($id1, $id2, $id3, $id4, $id5) = (0, $first_user_defined - 1, $first_user_defined, min($last_possible, $first_user_defined + 5000), $last_possible);
+
+$node->safe_psql('postgres', qq{select test_get_lwlock_identifier($id1);});
+$node->safe_psql('postgres', qq{select test_get_lwlock_identifier($id2);});
+ok(1, "check for no error when looking up built-in names");
+
+$node->psql('postgres', qq{select test_get_lwlock_identifier($id3);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id3 is not registered/, $log_location);
+
+$node->psql('postgres', qq{select test_get_lwlock_identifier($id4);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id4 is not registered/, $log_location);
+
+$node->psql('postgres', qq{select test_get_lwlock_identifier($id5);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id5 is not registered/, $log_location);
+
+ok(1, "check for error looking up user-defined names");
+
+done_testing();
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
new file mode 100644
index 00000000000..7fdf90e3e23
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
@@ -0,0 +1,16 @@
+-- test_lwlock_tranches--1.0.sql
+
+CREATE FUNCTION test_lwlock_new_tranche_id(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_lwlock_new_tranche_id'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_get_lwlock_identifier(int)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_get_lwlock_identifier'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_lwlock_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_lwlock_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
new file mode 100644
index 00000000000..654ca9a5f44
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
@@ -0,0 +1,102 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_lwlock_tranches_shmem_request(void);
+static void test_lwlock_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_lwlock_tranches_requested_named_tranches = 0;
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_lwlock_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_lwlock_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_lwlock_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_lwlock_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_lwlock_tranches_shmem_startup(void)
+{
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+}
+
+static void
+test_lwlock_tranches_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	for (int i = 0; i < test_lwlock_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_new_tranche_id);
+Datum
+test_lwlock_new_tranche_id(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+
+	for (int i = test_lwlock_tranches_requested_named_tranches; i < num; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_get_lwlock_identifier);
+Datum
+test_get_lwlock_identifier(PG_FUNCTION_ARGS)
+{
+	GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_tranches_get_first_user_defined);
+Datum
+test_lwlock_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf
new file mode 100644
index 00000000000..cd8c352900d
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf
@@ -0,0 +1,2 @@
+shared_preload_libraries = 'test_lwlock_tranches'
+test_lwlock_tranches.requested_named_tranches = 2
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
new file mode 100644
index 00000000000..bf0ecf64376
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
@@ -0,0 +1,6 @@
+# test_lwlock_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_lwlock_tranches'
\ No newline at end of file
-- 
2.39.5 (Apple Git-154)

v8-0001-Implement-a-DSA-for-LWLock-tranche-names.patchapplication/octet-stream; name=v8-0001-Implement-a-DSA-for-LWLock-tranche-names.patchDownload
From ae4aeba973eecc443dca7bed72c8dededf62709f Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Fri, 15 Aug 2025 16:48:10 +0000
Subject: [PATCH v8 1/2] Implement a DSA for LWLock tranche names

Previously, only LWLock tranches registered during postmaster startup were
available to all backends via a tranche name array inherited through fork.
Other backends that wanted to know about a tranche name had to register it
using LWLockRegisterTranche. While this design worked as intended, it was
not ideal because if a backend did not call LWLockRegisterTranche, it would
show a generic "extension" wait_event name in pg_stat_activity instead of
the actual tranche name. This caused inconsistencies with other backends
that registered the tranche name.

Commit fe07100e82b0 made dynamic creation of named DSA areas (and dshash)
much simpler for a backend, so it is anticipated that fewer tranche names
will be registered during postmaster startup in the future, with more
registered dynamically. This will make the above issue more common.

To address this, this change introduces a DSA to store LWLock tranche names
after postmaster startup. When a backend looks up a tranche name by tranche ID,
it first checks its local cache; if the name is not found locally, it fetches
it from the DSA and caches it for future lookups.

This also simplifies the API for registering tranche names: all registration
work is now done via LWLockNewTrancheId, which accepts a tranche name. The
LWLockRegisterTranche call is no longer required.

Additionally, while users should not pass arbitrary tranche IDs (that is,
IDs not created via LWLockNewTrancheId) to LWLockInitialize, nothing
technically prevents them from doing so. Therefore, we must continue to
handle such cases gracefully by returning a default "extension" tranche name.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   4 +-
 doc/src/sgml/xfunc.sgml                       |  18 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/ipc/ipci.c                |   4 +
 src/backend/storage/lmgr/lwlock.c             | 440 +++++++++++++++---
 .../utils/activity/wait_event_names.txt       |   2 +
 src/backend/utils/init/postinit.c             |   2 +
 src/include/storage/lwlock.h                  |  22 +-
 src/include/storage/lwlocklist.h              |   2 +
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   4 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 14 files changed, 421 insertions(+), 117 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..b623589b430 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,8 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
-
 	return found;
 }
 
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..20b0767f204 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3757,9 +3757,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3778,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..2f0df67bf7a 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -184,6 +184,8 @@ AttachSharedMemoryStructs(void)
 
 	CreateOrAttachShmemStructs();
 
+	LWLockTrancheNamesBEInit();
+
 	/*
 	 * Now give loadable modules a chance to set up their shmem allocations
 	 */
@@ -343,6 +345,8 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+
+	LWLockTrancheNamesShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index ec9c345ffdf..93fddeed141 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -80,6 +80,7 @@
 #include "pg_trace.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
+#include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/proclist.h"
 #include "storage/procnumber.h"
@@ -125,9 +126,12 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. Tranche names are registered in LWLockTrancheNames in
+ * two ways: if registration happens during postmaster startup, the names are
+ * stored directly in the local array; if registration happens afterward, the
+ * names are first stored in shared memory and later cached into the local array
+ * during lookup.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -144,14 +148,6 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 				 LWTRANCHE_FIRST_USER_DEFINED,
 				 "missing entries in BuiltinTrancheNames[]");
 
-/*
- * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, 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 **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
-
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
  * the pointer by fork from the postmaster (except in the EXEC_BACKEND case,
@@ -187,6 +183,48 @@ typedef struct NamedLWLockTrancheRequest
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
 static int	NamedLWLockTrancheRequestsAllocated = 0;
 
+typedef struct LWLockTrancheNamesShmem
+{
+	/* pointer to the DSA shared memory, created in-place */
+	void	   *raw_dsa_area;
+	/* DSA Pointer to a list of DSA pointers, each storing a tranche name */
+	dsa_pointer list_ptr;
+
+	/*
+	 * The number of slots allocated for tranche names. The allocated size
+	 * will grow geometrically to reduce resizing of the list_ptr.
+	 */
+	int			allocated;
+
+	/* lock to protect access to the list */
+	LWLock		lock;
+}			LWLockTrancheNamesShmem;
+
+typedef struct LWLockTrancheNamesStruct
+{
+	/*
+	 * Tranche names allocated in shared memory after postmaster startup, when
+	 * dynamic allocation is allowed.
+	 */
+	LWLockTrancheNamesShmem *shmem;
+	dsa_area   *dsa;
+
+	/*
+	 * Tranche names stored in local backend memory, either during postmaster
+	 * startup or as a local cache afterward.
+	 */
+	const char **local;
+	int			allocated;		/* number of pre-allocated slots for tranche
+								 * names */
+}			LWLockTrancheNamesStruct;
+
+static LWLockTrancheNamesStruct LWLockTrancheNames =
+{
+	NULL, NULL, NULL, 0
+};
+
+#define LWLOCK_TRANCHE_NAMES_INIT_SIZE 16
+
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
  * and the length of the shared-memory NamedLWLockTrancheArray later on.
@@ -201,7 +239,11 @@ NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
+static void SetLocalTrancheName(int tranche_index, const char *tranche_name);
+static void SetSharedTrancheName(int tranche_index, const char *tranche_name);
 static const char *GetLWTrancheName(uint16 trancheId);
+static void LWLockTrancheNamesAttach(void);
+static void LWLockTrancheNamesDetach(void);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -448,11 +490,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -513,7 +550,7 @@ InitializeLWLocks(void)
 			name = trancheNames;
 			trancheNames += strlen(request->tranche_name) + 1;
 			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
+			tranche->trancheId = LWLockNewTrancheId(name);
 			tranche->trancheName = name;
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
@@ -569,61 +606,147 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID and register it with the given tranche
+ * name.  Tranche IDs created by the postmaster will not be used
+ * for a tranche created in shared memory, since the index is generated
+ * from the shared LWLockCounter.
+ *
+ * The tranche name will be user-visible as a wait event name, so use
+ * a name that matches the style of existing wait events.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
+	int			tranche_id;
+	int			tranche_index;
 	int		   *LWLockCounter;
 
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 	/* We use the ShmemLock spinlock to protect LWLockCounter */
 	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
+	tranche_id = (*LWLockCounter)++;
 	SpinLockRelease(ShmemLock);
 
-	return result;
+	tranche_index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
+
+	if (IsUnderPostmaster || !IsPostmasterEnvironment)
+		SetSharedTrancheName(tranche_index, tranche_name);
+	else
+		SetLocalTrancheName(tranche_index, tranche_name);
+
+	return tranche_id;
 }
 
 /*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
+ * Adds a tranche name to shared memory. This routine should only be
+ * called from LWLockNewTrancheId.
  */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
+static void
+SetSharedTrancheName(int tranche_index, const char *tranche_name)
 {
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	dsa_pointer *name_ptrs;
+	dsa_pointer str_ptr;
+	char	   *str_addr;
+	int			len;
+	int			current_allocated;
+	int			new_alloc = 0;
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	current_allocated = LWLockTrancheNames.shmem->allocated;
+
+	/* Tranche array does not exist, initialize it */
+	if (!DsaPointerIsValid(LWLockTrancheNames.shmem->list_ptr))
+	{
+		new_alloc = LWLOCK_TRANCHE_NAMES_INIT_SIZE;
+
+		LWLockTrancheNames.shmem->allocated = new_alloc;
+		LWLockTrancheNames.shmem->list_ptr =
+			dsa_allocate0(LWLockTrancheNames.dsa, new_alloc * sizeof(dsa_pointer));
+
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+	}
+
+	/*
+	 * Need to enlarge the tranche array. Allocate a new list, copy the
+	 * current tranche name pointers into it, and update shared memory to
+	 * point to the new list.
+	 */
+	else if (tranche_index >= current_allocated)
 	{
-		int			newalloc;
+		dsa_pointer new_list;
+		dsa_pointer *old_ptrs;
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+		new_alloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE,
+										 tranche_index + 1));
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
+		new_list = dsa_allocate(LWLockTrancheNames.dsa,
+								new_alloc * sizeof(dsa_pointer));
+
+		old_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+								   LWLockTrancheNames.shmem->list_ptr);
+
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list);
+
+		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+		memcpy(name_ptrs, old_ptrs, sizeof(dsa_pointer) * current_allocated);
+
+		dsa_free(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+		LWLockTrancheNames.shmem->list_ptr = new_list;
+		LWLockTrancheNames.shmem->allocated = new_alloc;
+	}
+	/* Use the current list */
+	else
+	{
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+									LWLockTrancheNames.shmem->list_ptr);
+	}
+
+	/* Allocate and copy new string */
+	len = strlen(tranche_name) + 1;
+	str_ptr = dsa_allocate(LWLockTrancheNames.dsa, len);
+	str_addr = dsa_get_address(LWLockTrancheNames.dsa, str_ptr);
+	memcpy(str_addr, tranche_name, len);
+
+	name_ptrs[tranche_index] = str_ptr;
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
+/*
+ * Adds a tranche name to local memory. This routine should only be
+ * called from LWLockNewTrancheId and SyncLWLockTrancheNames.
+ */
+static void
+SetLocalTrancheName(int tranche_index, const char *tranche_name)
+{
+	int			newalloc;
+
+	Assert(tranche_name);
+
+	/* If necessary, create or enlarge array. */
+	if (tranche_index >= LWLockTrancheNames.allocated)
+	{
+		newalloc = pg_nextpower2_32(Max(8, tranche_index + 1));
+
+		if (LWLockTrancheNames.local == NULL)
+		{
+			LWLockTrancheNames.local = (const char **)
 				MemoryContextAllocZero(TopMemoryContext,
 									   newalloc * sizeof(char *));
+		}
 		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+		{
+			LWLockTrancheNames.local =
+				repalloc0_array(LWLockTrancheNames.local, const char *, LWLockTrancheNames.allocated, newalloc);
+		}
+
+		LWLockTrancheNames.allocated = newalloc;
 	}
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	LWLockTrancheNames.local[tranche_index] = tranche_name;
 }
 
 /*
@@ -674,10 +797,15 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 
 /*
  * LWLockInitialize - initialize a new lwlock; it's initially unlocked
+ *
+ * We check if the tranche_id exists. GetLWTrancheName will raise an
+ * error if it does not.
  */
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -709,27 +837,91 @@ LWLockReportWaitEnd(void)
 }
 
 /*
- * Return the name of an LWLock tranche.
+ * Update the local cache of LWLock tranche names using the list from shared
+ * memory and the requested tranches. When syncing from shared memory, we stop
+ * updating the cache upon encountering an invalid DSA pointer, which indicates
+ * that no valid tranche names can exist beyond that point.
+ */
+static void
+SyncLWLockTrancheNames(void)
+{
+	dsa_pointer *pointers = dsa_get_address(LWLockTrancheNames.dsa,
+											LWLockTrancheNames.shmem->list_ptr);
+
+	int			i = 0;
+
+	while (i < NamedLWLockTrancheRequests)
+	{
+		NamedLWLockTranche *tranche;
+
+		tranche = &NamedLWLockTrancheArray[i];
+
+		SetLocalTrancheName(i, tranche->trancheName);
+
+		i++;
+	}
+
+	/* Acquire shared lock on tranche names shared memory */
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_SHARED);
+
+	while (i < LWLockTrancheNames.shmem->allocated)
+	{
+		const char *tranche_name;
+
+		/* Stop iteration if the pointer is invalid */
+		if (!DsaPointerIsValid(pointers[i]))
+			break;
+
+		/* Get tranche name from shared memory */
+		tranche_name = (const char *) dsa_get_address(LWLockTrancheNames.dsa, pointers[i]);
+
+		/* Update local tranche name */
+		SetLocalTrancheName(i, tranche_name);
+
+		i++;
+	}
+
+	/* Release shared lock */
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
+/*
+ * Return the name of an LWLock tranche; or raise an error
+ * if it not found.
  */
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	const char *tranche_name = NULL;
+	uint16		tranche_id_saved = trancheId;
+	bool		needs_sync = false;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
+	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * This is an extension tranche, so check LWLockTrancheNames[]. First,
+	 * sync the local list with shared memory (if not under postmaster or
+	 * single-user) if needed, then perform the lookup. If the tranche ID is
+	 * not found, error out.
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	needs_sync = (trancheId >= LWLockTrancheNames.allocated ||
+				  LWLockTrancheNames.local[trancheId] == NULL)
+		&& (IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	if (needs_sync)
+		SyncLWLockTrancheNames();
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (trancheId < LWLockTrancheNames.allocated)
+		tranche_name = LWLockTrancheNames.local[trancheId];
 
-	return LWLockTrancheNames[trancheId];
+	if (!tranche_name)
+		elog(ERROR, "tranche ID %d is not registered", tranche_id_saved);
+
+	return tranche_name;
 }
 
 /*
@@ -1995,3 +2187,139 @@ LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode)
 	}
 	return false;
 }
+
+/*
+ * Size of the shared memory to create in place the tranche DSA.
+ */
+static Size
+LWLockTrancheNamesInitSize()
+{
+	Size		sz;
+
+	/*
+	 * This value is used by other facilities, see pgstat_shmem.c, so it's
+	 * good enough.
+	 */
+	sz = 256 * 1024;
+
+	Assert(dsa_minimum_size() <= sz);
+
+	return MAXALIGN(sz);
+}
+
+Size
+LWLockTrancheNamesShmemSize(void)
+{
+	Size		sz;
+
+	sz = MAXALIGN(sizeof(LWLockTrancheNamesStruct));
+	sz = add_size(sz, LWLockTrancheNamesInitSize());
+
+	return sz;
+}
+
+/*
+ * Allocate the DSA to store tranche names in dynamic shared memory. This
+ * allocation happens during postmaster startup, so create_in_place is used.
+ * At this stage, the postmaster should only create the DSA and avoid any other
+ * operations, as allocating additional DSM segments will not yet be possible.
+ */
+void
+LWLockTrancheNamesShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize or attach to shared memory structure */
+	LWLockTrancheNames.shmem = (LWLockTrancheNamesShmem *)
+		ShmemInitStruct("Dynamic LWLock tranche names",
+						LWLockTrancheNamesShmemSize(),
+						&found);
+
+	if (!IsUnderPostmaster)
+	{
+		dsa_area   *dsa;
+		LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+		char	   *p = (char *) ctl;
+
+		/* Calculate layout within the shared memory region */
+		p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+		ctl->raw_dsa_area = p;
+		p += MAXALIGN(LWLockTrancheNamesInitSize());
+
+		LWLockInitialize(&ctl->lock, LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST);
+
+		/* Create a dynamic shared memory area in-place */
+		dsa = dsa_create_in_place(ctl->raw_dsa_area,
+								  LWLockTrancheNamesInitSize(),
+								  LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA,
+								  NULL);
+
+		/*
+		 * Postmaster will never access these again, thus free the local
+		 * dsa/dshash references.
+		 */
+		dsa_pin(dsa);
+		dsa_detach(dsa);
+
+		ctl->allocated = 0;
+		ctl->list_ptr = InvalidDsaPointer;
+	}
+	else
+	{
+		Assert(found);
+	}
+}
+
+static void
+LWLockTrancheNamesAttach(void)
+{
+	MemoryContext oldcontext;
+
+	Assert(LWLockTrancheNames.dsa == NULL);
+
+	/* stats shared memory persists for the backend lifetime */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	LWLockTrancheNames.dsa = dsa_attach_in_place(LWLockTrancheNames.shmem->raw_dsa_area,
+												 NULL);
+	dsa_pin_mapping(LWLockTrancheNames.dsa);
+
+	Assert(LWLockTrancheNames.dsa);
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+static void
+LWLockTrancheNamesDetach(void)
+{
+	Assert(LWLockTrancheNames.dsa);
+
+	dsa_detach(LWLockTrancheNames.dsa);
+
+	dsa_release_in_place(LWLockTrancheNames.shmem->raw_dsa_area);
+
+	LWLockTrancheNames.dsa = NULL;
+}
+
+/* Detach the DSA from a backend */
+static void
+LWLockTrancheNamesShutdownHook(int code, Datum arg)
+{
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	LWLockTrancheNamesDetach();
+}
+
+/* Attach the DSA to a backend */
+void
+LWLockTrancheNamesBEInit(void)
+{
+	/* already attached, nothing to do */
+	if (LWLockTrancheNames.dsa)
+		return;
+
+	LWLockTrancheNamesAttach();
+
+	/* Set up a process-exit hook to clean up */
+	before_shmem_exit(LWLockTrancheNamesShutdownHook, 0);
+}
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0be307d2ca0..f60ed927892 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -406,6 +406,8 @@ SubtransSLRU	"Waiting to access the sub-transaction SLRU cache."
 XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
+LWLockDynamicTranchesDSA	"Waiting to access the LWLock tranche names DSA area."
+LWLockDynamicTranchesList	"Waiting to access the list of LWLock tranche names in DSA."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 641e535a73c..1ef04c6b5ec 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -662,6 +662,8 @@ BaseInit(void)
 	 * drop ephemeral slots, which in turn triggers stats reporting.
 	 */
 	ReplicationSlotInitialize();
+
+	LWLockTrancheNamesBEInit();
 }
 
 
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..8a008fc67dd 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -156,22 +156,18 @@ extern void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
- * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * An alternative, more flexible method of obtaining lwlocks is available.
+ * First, call LWLockNewTrancheId once, providing an associated tranche name.
+ * This function allocates a tranche ID from a shared counter. Then, for each
+ * lwlock, call LWLockInitialize once, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
+extern void LWLockTrancheNamesShmemInit(void);
+extern Size LWLockTrancheNamesShmemSize(void);
+extern void LWLockTrancheNamesBEInit(void);
+
 /*
  * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
  * we reserve additional tranche IDs for builtin tranches not included in
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 208d2e3a8ed..5978648c09d 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -135,3 +135,5 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU)
 PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
 PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
 PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_DSA, LWLockDynamicTranchesDSA)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_LIST, LWLockDynamicTranchesList)
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..7f3e6849401 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,8 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
-
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
 
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

#46Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#45)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Sat, Aug 16, 2025 at 10:18:05PM -0500, Sami Imseih wrote:

Attached is v8.

Thanks for the new version!

A few random comments about 0001:

1 ===

+} LWLockTrancheNamesShmem;

+} LWLockTrancheNamesStruct;

Add LWLockTrancheNamesShmem and LWLockTrancheNamesStruct to
src/tools/pgindent/typedefs.list?

2 ===

Maybe a comment before the above structs definitions to explain what they are
used for?

3 ===

+static void
+SetSharedTrancheName(int tranche_index, const char *tranche_name)
 {
-       /* This should only be called for user-defined tranches. */
-       if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-               return;
+       dsa_pointer *name_ptrs;
+       dsa_pointer str_ptr;
+       char       *str_addr;
+       int                     len;
+       int                     current_allocated;
+       int                     new_alloc = 0;
-       /* Convert to array index. */
-       tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+       LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);

Add Assert(IsUnderPostmaster || !IsPostmasterEnvironment);?

4 ===

+static void
+SetLocalTrancheName(int tranche_index, const char *tranche_name)
+{
+       int                     newalloc;
+
+       Assert(tranche_name);

should we add some assert on IsUnderPostmaster and/or IsPostmasterEnvironment too?

5 ===

+       while (i < NamedLWLockTrancheRequests)
+       {
+               NamedLWLockTranche *tranche;
+
+               tranche = &NamedLWLockTrancheArray[i];
+
+               SetLocalTrancheName(i, tranche->trancheName);
+
+               i++;
+       }

Maybe add a comment that those are the ones allocated by the postmaster during
startup?

Also, as this will be done during each sync and those tranches don't change,
so one could think there is room for improvement. Maybe add a comment that's
probably not worth optimizing (due to the fact that NamedLWLockTrancheRequests
should be small enough and the sync rare)?

6 ===

There is this existing comment:

/*
* NamedLWLockTrancheRequests is both the valid length of the request array,
* and the length of the shared-memory NamedLWLockTrancheArray later on.
* This variable and NamedLWLockTrancheArray are non-static so that
* postmaster.c can copy them to child processes in EXEC_BACKEND builds.
*/
int NamedLWLockTrancheRequests = 0;

Maybe add something like? "Additional dynamic tranche names beyond this count
are stored in a DSA".

7 ===

+               old_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+                                                                  LWLockTrancheNames.shmem->list_ptr);
+
+               name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list);
+
+               memset(name_ptrs, InvalidDsaPointer, new_alloc);
+               memcpy(name_ptrs, old_ptrs, sizeof(dsa_pointer) * current_allocated);
+
+               dsa_free(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);

maybe use local variable for LWLockTrancheNames.shmem->list_ptr (and
LWLockTrancheNames.dsa)?

8 ===

+       needs_sync = (trancheId >= LWLockTrancheNames.allocated ||
+                                 LWLockTrancheNames.local[trancheId] == NULL)
+               && (IsUnderPostmaster || !IsPostmasterEnvironment);

formating does not look good.

9 ===

-       if (trancheId >= LWLockTrancheNamesAllocated ||
-               LWLockTrancheNames[trancheId] == NULL)
-               return "extension";
+       if (trancheId < LWLockTrancheNames.allocated)
+               tranche_name = LWLockTrancheNames.local[trancheId];
-       return LWLockTrancheNames[trancheId];
+       if (!tranche_name)
+               elog(ERROR, "tranche ID %d is not registered", tranche_id_saved);

We now error out instead of returning "extension". That looks ok given the
up-thread discussion but then the commit message needs some updates as it
states:

"
Additionally, while users should not pass arbitrary tranche IDs (that is,
IDs not created via LWLockNewTrancheId) to LWLockInitialize, nothing
technically prevents them from doing so. Therefore, we must continue to
handle such cases gracefully by returning a default "extension" tranche name.
"

10 ===

+LWLockTrancheNamesInitSize()
+{
+       Size            sz;
+
+       /*
+        * This value is used by other facilities, see pgstat_shmem.c, so it's
+        * good enough.
+        */
+       sz = 256 * 1024;

use DSA_MIN_SEGMENT_SIZE?

11 ===

+       if (!IsUnderPostmaster)
+       {
+               dsa_area   *dsa;
+               LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+               char       *p = (char *) ctl;
+
+               /* Calculate layout within the shared memory region */
+               p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+               ctl->raw_dsa_area = p;
+               p += MAXALIGN(LWLockTrancheNamesInitSize());

LWLockTrancheNamesInitSize() already does MAXALIGN() so the last one
above is not needed. But the last p advance seems not necessary as not used
after. I think the same is true in StatsShmemInit() (see [1]/messages/by-id/aKLsu2sdpnyeuSSc@ip-10-97-1-34.eu-west-3.compute.internal).

12 ===

+LWLockTrancheNamesBEInit(void)
+{
+       /* already attached, nothing to do */
+       if (LWLockTrancheNames.dsa)
+               return;
+
+       LWLockTrancheNamesAttach();
+
+       /* Set up a process-exit hook to clean up */

s/already/Already/?

For 0002, a quick review:

13 ===

+       if (log)
+               elog(DEBUG3, "current_allocated: %d in tranche names shared memory", new_alloc);

Instead of this, could we elog distinct messages where the patch currently sets
log = true?

14 ===

-                 xid_wraparound
+                 xid_wraparound \
+                 test_lwlock_tranches

breaks the ordering.

15 ===

subdir('worker_spi')
subdir('xid_wraparound')
+subdir('test_lwlock_tranches')

Same, breaks the ordering.

16 ===

+my $ENABLE_LOG_WAIT = 1;
+
+my $isession;
+my $log_location;
+
+# Helper function: wait for one or more logs if $ENABLE_LOG_WAIT is true
+sub maybe_wait_for_log {
+    my ($node, $logs, $log_loc) = @_;
+    return $log_loc unless $ENABLE_LOG_WAIT;

is ENABLE_LOG_WAIT needed as it does not change?

[1]: /messages/by-id/aKLsu2sdpnyeuSSc@ip-10-97-1-34.eu-west-3.compute.internal

Regards,

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

#47Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#46)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

1 ===

+} LWLockTrancheNamesShmem;

+} LWLockTrancheNamesStruct;

Add LWLockTrancheNamesShmem and LWLockTrancheNamesStruct to
src/tools/pgindent/typedefs.list?

done.

2 ===

Maybe a comment before the above structs definitions to explain what they are
used for?

done.

3 ===

+static void
+SetSharedTrancheName(int tranche_index, const char *tranche_name)
{
-       /* This should only be called for user-defined tranches. */
-       if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-               return;
+       dsa_pointer *name_ptrs;
+       dsa_pointer str_ptr;
+       char       *str_addr;
+       int                     len;
+       int                     current_allocated;
+       int                     new_alloc = 0;
-       /* Convert to array index. */
-       tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+       LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);

Add Assert(IsUnderPostmaster || !IsPostmasterEnvironment);?

done. This is required here since we should not be dealling with DSA
during postmaster or single-user.

4 ===

+static void
+SetLocalTrancheName(int tranche_index, const char *tranche_name)
+{
+       int                     newalloc;
+
+       Assert(tranche_name);

should we add some assert on IsUnderPostmaster and/or IsPostmasterEnvironment too?

No, It's not needed here. SetLocalTrancheName is called during startup
and by normal
backends.

5 ===

+       while (i < NamedLWLockTrancheRequests)
+       {
+               NamedLWLockTranche *tranche;
+
+               tranche = &NamedLWLockTrancheArray[i];
+
+               SetLocalTrancheName(i, tranche->trancheName);
+
+               i++;
+       }

Maybe add a comment that those are the ones allocated by the postmaster during
startup?

Also, as this will be done during each sync and those tranches don't change,
so one could think there is room for improvement. Maybe add a comment that's
probably not worth optimizing (due to the fact that NamedLWLockTrancheRequests
should be small enough and the sync rare)?

done.

6 ===

There is this existing comment:

/*
* NamedLWLockTrancheRequests is both the valid length of the request array,
* and the length of the shared-memory NamedLWLockTrancheArray later on.
* This variable and NamedLWLockTrancheArray are non-static so that
* postmaster.c can copy them to child processes in EXEC_BACKEND builds.
*/
int NamedLWLockTrancheRequests = 0;

Maybe add something like? "Additional dynamic tranche names beyond this count
are stored in a DSA".

No. I don't like talking about that here. This is already described in:
```
* 3. Extensions can create new tranches using either RequestNamedLWLockTranche
* or LWLockNewTrancheId. Tranche names are reg
```

7 ===

+               old_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+                                                                  LWLockTrancheNames.shmem->list_ptr);
+
+               name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list);
+
+               memset(name_ptrs, InvalidDsaPointer, new_alloc);
+               memcpy(name_ptrs, old_ptrs, sizeof(dsa_pointer) * current_allocated);
+
+               dsa_free(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);

maybe use local variable for LWLockTrancheNames.shmem->list_ptr (and
LWLockTrancheNames.dsa)?

hmm, I don't see the point to this.

8 ===

+       needs_sync = (trancheId >= LWLockTrancheNames.allocated ||
+                                 LWLockTrancheNames.local[trancheId] == NULL)
+               && (IsUnderPostmaster || !IsPostmasterEnvironment);

formating does not look good.

pgindent told me it's fine. But, I did add a parenthesis around the expression,
so pgindent now aligned the conditions in a better way.

9 ===

-       if (trancheId >= LWLockTrancheNamesAllocated ||
-               LWLockTrancheNames[trancheId] == NULL)
-               return "extension";
+       if (trancheId < LWLockTrancheNames.allocated)
+               tranche_name = LWLockTrancheNames.local[trancheId];
-       return LWLockTrancheNames[trancheId];
+       if (!tranche_name)
+               elog(ERROR, "tranche ID %d is not registered", tranche_id_saved);

We now error out instead of returning "extension". That looks ok given the
up-thread discussion but then the commit message needs some updates as it
states:
"
Additionally, while users should not pass arbitrary tranche IDs (that is,
IDs not created via LWLockNewTrancheId) to LWLockInitialize, nothing
technically prevents them from doing so. Therefore, we must continue to
handle such cases gracefully by returning a default "extension" tranche name.
"

done.

10 ===

+LWLockTrancheNamesInitSize()
+{
+       Size            sz;
+
+       /*
+        * This value is used by other facilities, see pgstat_shmem.c, so it's
+        * good enough.
+        */
+       sz = 256 * 1024;

use DSA_MIN_SEGMENT_SIZE?

done.

11 ===

+       if (!IsUnderPostmaster)
+       {
+               dsa_area   *dsa;
+               LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+               char       *p = (char *) ctl;
+
+               /* Calculate layout within the shared memory region */
+               p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+               ctl->raw_dsa_area = p;
+               p += MAXALIGN(LWLockTrancheNamesInitSize());

LWLockTrancheNamesInitSize() already does MAXALIGN() so the last one
above is not needed. But the last p advance seems not necessary as not used
after. I think the same is true in StatsShmemInit() (see [1]).

yeah, good point. done.

12 ===

+LWLockTrancheNamesBEInit(void)
+{
+       /* already attached, nothing to do */
+       if (LWLockTrancheNames.dsa)
+               return;
+
+       LWLockTrancheNamesAttach();
+
+       /* Set up a process-exit hook to clean up */

s/already/Already/?

done.

For 0002, a quick review:

13 ===

+       if (log)
+               elog(DEBUG3, "current_allocated: %d in tranche names shared memory", new_alloc);

Instead of this, could we elog distinct messages where the patch currently sets
log = true?

ok, that was my initial thought, but I did not like repeating the
messages. I went
back to distinct messages. I have no strong feelings either way.

14 ===

-                 xid_wraparound
+                 xid_wraparound \
+                 test_lwlock_tranches

breaks the ordering.

15 ===

subdir('worker_spi')
subdir('xid_wraparound')
+subdir('test_lwlock_tranches')

Same, breaks the ordering.

16 ===

done and done.

+my $ENABLE_LOG_WAIT = 1;
+
+my $isession;
+my $log_location;
+
+# Helper function: wait for one or more logs if $ENABLE_LOG_WAIT is true
+sub maybe_wait_for_log {
+    my ($node, $logs, $log_loc) = @_;
+    return $log_loc unless $ENABLE_LOG_WAIT;

is ENABLE_LOG_WAIT needed as it does not change?

That was a remnant of my testing. removed.

see v9

--

Sami

Attachments:

v9-0001-Implement-a-DSA-for-LWLock-tranche-names.patchapplication/octet-stream; name=v9-0001-Implement-a-DSA-for-LWLock-tranche-names.patchDownload
From e986769cbe00982461d1fc216afd4064203122fd Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Fri, 15 Aug 2025 16:48:10 +0000
Subject: [PATCH v9 1/2] Implement a DSA for LWLock tranche names

Previously, only LWLock tranches registered during postmaster startup were
available to all backends via a tranche name array inherited through fork.
Other backends that wanted to know about a tranche name had to register it
using LWLockRegisterTranche. While this design worked as intended, it was
not ideal because if a backend did not call LWLockRegisterTranche, it would
show a generic "extension" wait_event name in pg_stat_activity instead of
the actual tranche name. This caused inconsistencies with other backends
that registered the tranche name.

Commit fe07100e82b0 made dynamic creation of named DSA areas (and dshash)
much simpler for a backend, so it is anticipated that fewer tranche names
will be registered during postmaster startup in the future, with more
registered dynamically. This will make the above issue more common.

To address this, this change introduces a DSA to store LWLock tranche names
after postmaster startup. When a backend looks up a tranche name by tranche ID,
it first checks its local cache; if the name is not found locally, it fetches
it from the DSA and caches it for future lookups.

This also simplifies the API for registering tranche names: all registration
work is now done via LWLockNewTrancheId, which accepts a tranche name. The
LWLockRegisterTranche call is no longer required.

Previously users could pass arbitrary tranche IDs (that is, IDs not created
via LWLockNewTrancheId) to LWLockInitialize. This now results in an error
message.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   4 +-
 doc/src/sgml/xfunc.sgml                       |  18 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/ipc/ipci.c                |   4 +
 src/backend/storage/lmgr/lwlock.c             | 451 +++++++++++++++---
 .../utils/activity/wait_event_names.txt       |   2 +
 src/backend/utils/init/postinit.c             |   2 +
 src/include/storage/lwlock.h                  |  22 +-
 src/include/storage/lwlocklist.h              |   2 +
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   4 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 src/tools/pgindent/typedefs.list              |   2 +
 15 files changed, 434 insertions(+), 117 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..b623589b430 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,8 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
-
 	return found;
 }
 
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..20b0767f204 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3757,9 +3757,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3778,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..2f0df67bf7a 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -184,6 +184,8 @@ AttachSharedMemoryStructs(void)
 
 	CreateOrAttachShmemStructs();
 
+	LWLockTrancheNamesBEInit();
+
 	/*
 	 * Now give loadable modules a chance to set up their shmem allocations
 	 */
@@ -343,6 +345,8 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+
+	LWLockTrancheNamesShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index ec9c345ffdf..9437dd23c6e 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -80,6 +80,7 @@
 #include "pg_trace.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
+#include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/proclist.h"
 #include "storage/procnumber.h"
@@ -125,9 +126,11 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. Tranche names are registered in LWLockTrancheNames in
+ * two ways: if registration happens during postmaster startup, the names are
+ * stored directly in the local array; if registration happens afterward, the
+ * names are first stored in DSA and later cached into the local array during lookup.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -144,14 +147,6 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 				 LWTRANCHE_FIRST_USER_DEFINED,
 				 "missing entries in BuiltinTrancheNames[]");
 
-/*
- * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, 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 **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
-
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
  * the pointer by fork from the postmaster (except in the EXEC_BACKEND case,
@@ -187,6 +182,53 @@ typedef struct NamedLWLockTrancheRequest
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
 static int	NamedLWLockTrancheRequestsAllocated = 0;
 
+/* struct representing the LWLock tranche names stored in DSA */
+typedef struct LWLockTrancheNamesShmem
+{
+	/* pointer to the DSA shared memory, created in-place */
+	void	   *raw_dsa_area;
+	/* DSA Pointer to a list of DSA pointers, each storing a tranche name */
+	dsa_pointer list_ptr;
+
+	/*
+	 * The number of slots allocated for tranche names. The allocated size
+	 * will grow geometrically to reduce resizing of the list_ptr.
+	 */
+	int			allocated;
+
+	/* lock to protect access to the list */
+	LWLock		lock;
+} LWLockTrancheNamesShmem;
+
+/*
+ * Struct representing the names of LWLock tranches, containing pointers
+ * to both local and shared tranche names.
+ */
+typedef struct LWLockTrancheNamesStruct
+{
+	/*
+	 * Tranche names allocated in shared memory after postmaster startup, when
+	 * dynamic allocation is allowed.
+	 */
+	LWLockTrancheNamesShmem *shmem;
+	dsa_area   *dsa;
+
+	/*
+	 * Tranche names stored in local backend memory, either during postmaster
+	 * startup or as a local cache afterward.
+	 */
+	const char **local;
+	int			allocated;		/* number of pre-allocated slots for tranche
+								 * names */
+} LWLockTrancheNamesStruct;
+
+static LWLockTrancheNamesStruct LWLockTrancheNames =
+{
+	NULL, NULL, NULL, 0
+};
+
+#define LWLOCK_TRANCHE_NAMES_INIT_SIZE 16
+
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
  * and the length of the shared-memory NamedLWLockTrancheArray later on.
@@ -201,7 +243,11 @@ NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
+static void SetLocalTrancheName(int tranche_index, const char *tranche_name);
+static void SetSharedTrancheName(int tranche_index, const char *tranche_name);
 static const char *GetLWTrancheName(uint16 trancheId);
+static void LWLockTrancheNamesAttach(void);
+static void LWLockTrancheNamesDetach(void);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -448,11 +494,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -513,7 +554,7 @@ InitializeLWLocks(void)
 			name = trancheNames;
 			trancheNames += strlen(request->tranche_name) + 1;
 			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
+			tranche->trancheId = LWLockNewTrancheId(name);
 			tranche->trancheName = name;
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
@@ -569,61 +610,149 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID and register it with the given tranche
+ * name.  Tranche IDs created by the postmaster will not be used
+ * for a tranche created in shared memory, since the index is generated
+ * from the shared LWLockCounter.
+ *
+ * The tranche name will be user-visible as a wait event name, so use
+ * a name that matches the style of existing wait events.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
+	int			tranche_id;
+	int			tranche_index;
 	int		   *LWLockCounter;
 
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 	/* We use the ShmemLock spinlock to protect LWLockCounter */
 	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
+	tranche_id = (*LWLockCounter)++;
 	SpinLockRelease(ShmemLock);
 
-	return result;
+	tranche_index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
+
+	if (IsUnderPostmaster || !IsPostmasterEnvironment)
+		SetSharedTrancheName(tranche_index, tranche_name);
+	else
+		SetLocalTrancheName(tranche_index, tranche_name);
+
+	return tranche_id;
 }
 
 /*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
+ * Adds a tranche name to shared memory. This routine should only be
+ * called from LWLockNewTrancheId.
  */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
+static void
+SetSharedTrancheName(int tranche_index, const char *tranche_name)
 {
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	dsa_pointer *name_ptrs;
+	dsa_pointer str_ptr;
+	char	   *str_addr;
+	int			len;
+	int			current_allocated;
+	int			new_alloc = 0;
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
+
+	current_allocated = LWLockTrancheNames.shmem->allocated;
+
+	/* Tranche array does not exist, initialize it */
+	if (!DsaPointerIsValid(LWLockTrancheNames.shmem->list_ptr))
 	{
-		int			newalloc;
+		new_alloc = LWLOCK_TRANCHE_NAMES_INIT_SIZE;
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+		LWLockTrancheNames.shmem->allocated = new_alloc;
+		LWLockTrancheNames.shmem->list_ptr =
+			dsa_allocate0(LWLockTrancheNames.dsa, new_alloc * sizeof(dsa_pointer));
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+	}
+
+	/*
+	 * Need to enlarge the tranche array. Allocate a new list, copy the
+	 * current tranche name pointers into it, and update shared memory to
+	 * point to the new list.
+	 */
+	else if (tranche_index >= current_allocated)
+	{
+		dsa_pointer new_list;
+		dsa_pointer *old_ptrs;
+
+		new_alloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE,
+										 tranche_index + 1));
+
+		new_list = dsa_allocate(LWLockTrancheNames.dsa,
+								new_alloc * sizeof(dsa_pointer));
+
+		old_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+								   LWLockTrancheNames.shmem->list_ptr);
+
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list);
+
+		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+		memcpy(name_ptrs, old_ptrs, sizeof(dsa_pointer) * current_allocated);
+
+		dsa_free(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+		LWLockTrancheNames.shmem->list_ptr = new_list;
+		LWLockTrancheNames.shmem->allocated = new_alloc;
+	}
+	/* Use the current list */
+	else
+	{
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+									LWLockTrancheNames.shmem->list_ptr);
+	}
+
+	/* Allocate and copy new string */
+	len = strlen(tranche_name) + 1;
+	str_ptr = dsa_allocate(LWLockTrancheNames.dsa, len);
+	str_addr = dsa_get_address(LWLockTrancheNames.dsa, str_ptr);
+	memcpy(str_addr, tranche_name, len);
+
+	name_ptrs[tranche_index] = str_ptr;
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
+/*
+ * Adds a tranche name to local memory. This routine should only be
+ * called from LWLockNewTrancheId and SyncLWLockTrancheNames.
+ */
+static void
+SetLocalTrancheName(int tranche_index, const char *tranche_name)
+{
+	int			newalloc;
+
+	Assert(tranche_name);
+
+	/* If necessary, create or enlarge array. */
+	if (tranche_index >= LWLockTrancheNames.allocated)
+	{
+		newalloc = pg_nextpower2_32(Max(8, tranche_index + 1));
+
+		if (LWLockTrancheNames.local == NULL)
+		{
+			LWLockTrancheNames.local = (const char **)
 				MemoryContextAllocZero(TopMemoryContext,
 									   newalloc * sizeof(char *));
+		}
 		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+		{
+			LWLockTrancheNames.local =
+				repalloc0_array(LWLockTrancheNames.local, const char *, LWLockTrancheNames.allocated, newalloc);
+		}
+
+		LWLockTrancheNames.allocated = newalloc;
 	}
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	LWLockTrancheNames.local[tranche_index] = tranche_name;
 }
 
 /*
@@ -674,10 +803,15 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 
 /*
  * LWLockInitialize - initialize a new lwlock; it's initially unlocked
+ *
+ * We check if the tranche_id exists. GetLWTrancheName will raise an
+ * error if it does not.
  */
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -709,27 +843,100 @@ LWLockReportWaitEnd(void)
 }
 
 /*
- * Return the name of an LWLock tranche.
+ * Update the local cache of LWLock tranche names using the list from shared
+ * memory and the tranches requested suring postmaster startup. When syncing
+ * from shared memory, we stop updating the cache upon encountering an invalid
+ * DSA pointer, which indicates that no valid tranche names can exist beyond
+ * that point.
+ */
+static void
+SyncLWLockTrancheNames(void)
+{
+	dsa_pointer *pointers = dsa_get_address(LWLockTrancheNames.dsa,
+											LWLockTrancheNames.shmem->list_ptr);
+
+	int			i = 0;
+
+	/*
+	 * Sync tranches requested during postmaster startup (via
+	 * RequestNamedLWLockTranche). This list is static and syncing is only
+	 * needed the first time a backend looks up a tranche in the EXEC_BACKEND
+	 * case, since LWLockTrancheNames.local is not forked. The list is
+	 * typically small and syncs are infrequent, so further optimization is
+	 * unnecessary.
+	 */
+	while (i < NamedLWLockTrancheRequests)
+	{
+		NamedLWLockTranche *tranche;
+
+		tranche = &NamedLWLockTrancheArray[i];
+
+		SetLocalTrancheName(i, tranche->trancheName);
+
+		i++;
+	}
+
+	/* Acquire shared lock on tranche names shared memory */
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_SHARED);
+
+	while (i < LWLockTrancheNames.shmem->allocated)
+	{
+		const char *tranche_name;
+
+		/* Stop iteration if the pointer is invalid */
+		if (!DsaPointerIsValid(pointers[i]))
+			break;
+
+		/* Get tranche name from shared memory */
+		tranche_name = (const char *) dsa_get_address(LWLockTrancheNames.dsa, pointers[i]);
+
+		/* Update local tranche name */
+		SetLocalTrancheName(i, tranche_name);
+
+		i++;
+	}
+
+	/* Release shared lock */
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
+/*
+ * Return the name of an LWLock tranche; or raise an error
+ * if it not found.
  */
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	const char *tranche_name = NULL;
+	uint16		tranche_id_saved = trancheId;
+	bool		needs_sync = false;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
+	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * This is an extension tranche, so check LWLockTrancheNames[]. First,
+	 * sync the local list with shared memory (if not under postmaster or
+	 * single-user) if needed, then perform the lookup. If the tranche ID is
+	 * not found, error out.
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	needs_sync = ((trancheId >= LWLockTrancheNames.allocated ||
+				   LWLockTrancheNames.local[trancheId] == NULL)
+				  && (IsUnderPostmaster || !IsPostmasterEnvironment));
+
+	if (needs_sync)
+		SyncLWLockTrancheNames();
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (trancheId < LWLockTrancheNames.allocated)
+		tranche_name = LWLockTrancheNames.local[trancheId];
 
-	return LWLockTrancheNames[trancheId];
+	if (!tranche_name)
+		elog(ERROR, "tranche ID %d is not registered", tranche_id_saved);
+
+	return tranche_name;
 }
 
 /*
@@ -1995,3 +2202,135 @@ LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode)
 	}
 	return false;
 }
+
+/*
+ * Initial Size of the shared memory to create in place the tranche DSA.
+ * This should just be the minimum size of a DSA segment.
+ */
+static Size
+LWLockTrancheNamesInitSize()
+{
+	Size		sz;
+
+	sz = DSA_MIN_SEGMENT_SIZE;
+
+	Assert(dsa_minimum_size() <= sz);
+
+	return MAXALIGN(sz);
+}
+
+Size
+LWLockTrancheNamesShmemSize(void)
+{
+	Size		sz;
+
+	sz = MAXALIGN(sizeof(LWLockTrancheNamesStruct));
+	sz = add_size(sz, LWLockTrancheNamesInitSize());
+
+	return sz;
+}
+
+/*
+ * Allocate the DSA to store tranche names in dynamic shared memory. This
+ * allocation happens during postmaster startup, so create_in_place is used.
+ * At this stage, the postmaster should only create the DSA and avoid any other
+ * operations, as allocating additional DSM segments will not yet be possible.
+ */
+void
+LWLockTrancheNamesShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize or attach to shared memory structure */
+	LWLockTrancheNames.shmem = (LWLockTrancheNamesShmem *)
+		ShmemInitStruct("Dynamic LWLock tranche names",
+						LWLockTrancheNamesShmemSize(),
+						&found);
+
+	if (!IsUnderPostmaster)
+	{
+		dsa_area   *dsa;
+		LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+		char	   *p = (char *) ctl;
+
+		/* Calculate layout within the shared memory region */
+		p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+		ctl->raw_dsa_area = p;
+
+		LWLockInitialize(&ctl->lock, LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST);
+
+		/* Create a dynamic shared memory area in-place */
+		dsa = dsa_create_in_place(ctl->raw_dsa_area,
+								  LWLockTrancheNamesInitSize(),
+								  LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA,
+								  NULL);
+
+		/*
+		 * Postmaster will never access these again, thus free the local
+		 * dsa/dshash references.
+		 */
+		dsa_pin(dsa);
+		dsa_detach(dsa);
+
+		ctl->allocated = 0;
+		ctl->list_ptr = InvalidDsaPointer;
+	}
+	else
+	{
+		Assert(found);
+	}
+}
+
+static void
+LWLockTrancheNamesAttach(void)
+{
+	MemoryContext oldcontext;
+
+	Assert(LWLockTrancheNames.dsa == NULL);
+
+	/* stats shared memory persists for the backend lifetime */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	LWLockTrancheNames.dsa = dsa_attach_in_place(LWLockTrancheNames.shmem->raw_dsa_area,
+												 NULL);
+	dsa_pin_mapping(LWLockTrancheNames.dsa);
+
+	Assert(LWLockTrancheNames.dsa);
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+static void
+LWLockTrancheNamesDetach(void)
+{
+	Assert(LWLockTrancheNames.dsa);
+
+	dsa_detach(LWLockTrancheNames.dsa);
+
+	dsa_release_in_place(LWLockTrancheNames.shmem->raw_dsa_area);
+
+	LWLockTrancheNames.dsa = NULL;
+}
+
+/* Detach the DSA from a backend */
+static void
+LWLockTrancheNamesShutdownHook(int code, Datum arg)
+{
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	LWLockTrancheNamesDetach();
+}
+
+/* Attach the DSA to a backend */
+void
+LWLockTrancheNamesBEInit(void)
+{
+	/* Already attached, nothing to do */
+	if (LWLockTrancheNames.dsa)
+		return;
+
+	LWLockTrancheNamesAttach();
+
+	/* Set up a process-exit hook to clean up */
+	before_shmem_exit(LWLockTrancheNamesShutdownHook, 0);
+}
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0be307d2ca0..f60ed927892 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -406,6 +406,8 @@ SubtransSLRU	"Waiting to access the sub-transaction SLRU cache."
 XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
+LWLockDynamicTranchesDSA	"Waiting to access the LWLock tranche names DSA area."
+LWLockDynamicTranchesList	"Waiting to access the list of LWLock tranche names in DSA."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 641e535a73c..1ef04c6b5ec 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -662,6 +662,8 @@ BaseInit(void)
 	 * drop ephemeral slots, which in turn triggers stats reporting.
 	 */
 	ReplicationSlotInitialize();
+
+	LWLockTrancheNamesBEInit();
 }
 
 
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..8a008fc67dd 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -156,22 +156,18 @@ extern void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
- * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * An alternative, more flexible method of obtaining lwlocks is available.
+ * First, call LWLockNewTrancheId once, providing an associated tranche name.
+ * This function allocates a tranche ID from a shared counter. Then, for each
+ * lwlock, call LWLockInitialize once, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
+extern void LWLockTrancheNamesShmemInit(void);
+extern Size LWLockTrancheNamesShmemSize(void);
+extern void LWLockTrancheNamesBEInit(void);
+
 /*
  * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
  * we reserve additional tranche IDs for builtin tranches not included in
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 208d2e3a8ed..5978648c09d 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -135,3 +135,5 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU)
 PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
 PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
 PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_DSA, LWLockDynamicTranchesDSA)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_LIST, LWLockDynamicTranchesList)
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..7f3e6849401 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,8 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
-
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
 
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e6f2e93b2d6..80cdbfa6d0d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1531,6 +1531,8 @@ LWLock
 LWLockHandle
 LWLockMode
 LWLockPadded
+LWLockTrancheNamesShmem
+LWLockTrancheNamesStruct
 LZ4F_compressionContext_t
 LZ4F_decompressOptions_t
 LZ4F_decompressionContext_t
-- 
2.43.0

v9-0002-Add-tests-for-LWLock-tranche-names-DSA.patchapplication/octet-stream; name=v9-0002-Add-tests-for-LWLock-tranche-names-DSA.patchDownload
From 68f2f689d4db298913800c4b30b28162d35e3b45 Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Fri, 15 Aug 2025 17:53:03 +0000
Subject: [PATCH v9 2/2] Add tests for LWLock tranche names DSA

---
 src/backend/storage/lmgr/lwlock.c             |  11 ++
 src/backend/utils/activity/wait_event.c       |   3 -
 src/include/utils/wait_classes.h              |   2 +
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 .../modules/test_lwlock_tranches/Makefile     |  23 +++
 .../expected/test_lwlock_tranches.out         |  20 ++
 .../modules/test_lwlock_tranches/meson.build  |  33 ++++
 .../t/001_test_lwlock_tranches.pl             | 173 ++++++++++++++++++
 .../test_lwlock_tranches--1.0.sql             |  16 ++
 .../test_lwlock_tranches.c                    | 102 +++++++++++
 .../test_lwlock_tranches.conf                 |   2 +
 .../test_lwlock_tranches.control              |   6 +
 13 files changed, 391 insertions(+), 4 deletions(-)
 create mode 100644 src/test/modules/test_lwlock_tranches/Makefile
 create mode 100644 src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out
 create mode 100644 src/test/modules/test_lwlock_tranches/meson.build
 create mode 100644 src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control

diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 9437dd23c6e..bfe9692648a 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -672,6 +672,8 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 
 		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
 		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+
+		elog(DEBUG3, "current_allocated: %d in tranche names shared memory", new_alloc);
 	}
 
 	/*
@@ -702,6 +704,8 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 
 		LWLockTrancheNames.shmem->list_ptr = new_list;
 		LWLockTrancheNames.shmem->allocated = new_alloc;
+
+		elog(DEBUG3, "current_allocated: %d in tranche names shared memory", new_alloc);
 	}
 	/* Use the current list */
 	else
@@ -936,6 +940,13 @@ GetLWTrancheName(uint16 trancheId)
 	if (!tranche_name)
 		elog(ERROR, "tranche ID %d is not registered", tranche_id_saved);
 
+	elog(DEBUG3,
+		 "tranche_name %s with tranche_id %d found at index %d. needs_sync: %s",
+		 tranche_name,
+		 tranche_id_saved,
+		 trancheId,
+		 needs_sync ? "true" : "false");
+
 	return tranche_name;
 }
 
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d9b8f34a355..eba7d338c1f 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -39,9 +39,6 @@ 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_CLASS_MASK	0xFF000000
-#define WAIT_EVENT_ID_MASK		0x0000FFFF
-
 /*
  * Hash tables for storing custom wait event ids and their names in
  * shared memory.
diff --git a/src/include/utils/wait_classes.h b/src/include/utils/wait_classes.h
index 51ee68397d5..6ca0504cee1 100644
--- a/src/include/utils/wait_classes.h
+++ b/src/include/utils/wait_classes.h
@@ -10,6 +10,8 @@
 #ifndef WAIT_CLASSES_H
 #define WAIT_CLASSES_H
 
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+#define WAIT_EVENT_ID_MASK		0x0000FFFF
 
 /* ----------
  * Wait Classes
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..e72800cd2b7 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_lwlock_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..f04910d13d7 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_lwlock_tranches')
diff --git a/src/test/modules/test_lwlock_tranches/Makefile b/src/test/modules/test_lwlock_tranches/Makefile
new file mode 100644
index 00000000000..9867c99cbfd
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_lwlock_tranches/Makefile
+
+MODULE_big = test_lwlock_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_lwlock_tranches.o
+PGFILEDESC = "test_lwlock_tranches - test code LWLock tranche management"
+
+EXTENSION = test_lwlock_tranches
+DATA = test_lwlock_tranches--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_lwlock_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out b/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out
new file mode 100644
index 00000000000..337a8438a59
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out
@@ -0,0 +1,20 @@
+CREATE EXTENSION test_lwlock_tranches;
+SELECT test_lwlock_tranches_get_first_user_defined() AS num_user_defined \gset
+SELECT :num_user_defined;
+ ?column? 
+----------
+       97
+(1 row)
+
+SELECT test_get_lwlock_identifier(:num_user_defined);
+ test_get_lwlock_identifier 
+----------------------------
+ 
+(1 row)
+
+SELECT test_get_lwlock_identifier(:num_user_defined + 1);
+ test_get_lwlock_identifier 
+----------------------------
+ 
+(1 row)
+
diff --git a/src/test/modules/test_lwlock_tranches/meson.build b/src/test/modules/test_lwlock_tranches/meson.build
new file mode 100644
index 00000000000..f8aa700b58f
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_lwlock_tranches_sources = files(
+  'test_lwlock_tranches.c',
+)
+
+if host_system == 'windows'
+  test_lwlock_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_lwlock_tranches',
+    '--FILEDESC', 'test_lwlock_tranches - test code LWLock tranche management',])
+endif
+
+test_lwlock_tranches = shared_module('test_lwlock_tranches',
+  test_lwlock_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_lwlock_tranches
+
+test_install_data += files(
+  'test_lwlock_tranches.control',
+  'test_lwlock_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_lwlock_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_test_lwlock_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
new file mode 100644
index 00000000000..f59925ea0b4
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
@@ -0,0 +1,173 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $ENABLE_LOG_WAIT = 1;
+
+my $isession;
+my $log_location;
+
+# Helper function: wait for one or more logs if $ENABLE_LOG_WAIT is true
+sub maybe_wait_for_log {
+    my ($node, $logs, $log_loc) = @_;
+    return $log_loc unless $ENABLE_LOG_WAIT;
+
+    # Normalize single regex to array
+    $logs = [$logs] unless ref($logs) eq 'ARRAY';
+
+    foreach my $regex (@$logs) {
+        $log_loc = $node->wait_for_log($regex, $log_loc);
+    }
+    return $log_loc;
+}
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+
+# The largest shared memory allocation we require for testing
+my $final_allocation = 64;
+
+# $unallocated_shared_memory will be overridden by the value
+# of $test_lwlock_tranches_requested_named_tranches when it's > 1
+my $unallocated_shared_memory = 2;
+my $requested_named_tranches = 2;
+
+# VALUES MATCHING INTERNALS
+# The initial array size. Should match LWLOCK_TRANCHE_NAMES_INIT_SIZE
+my $initial_size = 16;
+
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_lwlock_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_lwlock_tranches.requested_named_tranches=$requested_named_tranches));
+$node->append_conf('postgresql.conf',
+    qq(log_min_messages=DEBUG3));
+$node->start();
+
+$node->safe_psql('postgres', q(CREATE EXTENSION test_lwlock_tranches));
+
+my $first_user_defined = $node->safe_psql(
+    'postgres',
+    "SELECT test_lwlock_tranches_get_first_user_defined()"
+);
+
+my $next_index = 0;
+my $lookup_tranche_id = 0;
+my $current_size = $initial_size;
+
+$log_location = -s $node->logfile;
+
+if ($requested_named_tranches > 0) {
+    $unallocated_shared_memory += $requested_named_tranches;
+}
+
+if ($unallocated_shared_memory > 0) {
+    while ($current_size < $final_allocation) {
+        $current_size *= 2;
+    }
+
+    # Lookup before allocating shared memory
+    if ($requested_named_tranches > 0) {
+        for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+            $lookup_tranche_id = $next_index + $first_user_defined;
+            $node->safe_psql('postgres',
+                qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+            $log_location = maybe_wait_for_log(
+                $node,
+                qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index./,
+                $log_location
+            );
+        }
+        ok(1, "requested_named_tranches looked up from local cache, before allocating shared memory");
+    }
+
+    # Create new tranches
+    $node->safe_psql('postgres',
+        qq{select test_lwlock_new_tranche_id($current_size - $unallocated_shared_memory)});
+    $log_location = maybe_wait_for_log(
+        $node,
+        qr/ DEBUG:  current_allocated: $final_allocation in tranche names shared memory/, $log_location
+    );
+
+    ok(1, "resize shared memory with allocation up to $current_size tranche names");
+
+    # Lookup after allocating shared memory
+    if ($requested_named_tranches > 0) {
+        for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+            $lookup_tranche_id = $next_index + $first_user_defined;
+            $node->safe_psql('postgres',
+                qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+            $log_location = maybe_wait_for_log(
+                $node,
+                qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index./,
+                $log_location
+            );
+        }
+        ok(1, "requested_named_tranches looked up from local cache, after allocating shared memory");
+    }
+
+    $next_index = $requested_named_tranches;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    my $second_user_defined = $first_user_defined + 1;
+    $node->safe_psql('postgres',
+        qq{select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($first_user_defined);
+           select test_get_lwlock_identifier($second_user_defined)}
+    );
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: true/, $log_location);
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: false/, $log_location);
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: false/, $log_location);
+
+    for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+        $lookup_tranche_id = $first_user_defined + $next_index;
+        maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index./, $log_location);
+    }
+    ok(1, "lookup with synchronization is successful");
+
+    $log_location = -s $node->logfile;
+
+    # Lookup unregistered tranches
+    $next_index = $current_size - $unallocated_shared_memory;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    $node->psql('postgres', qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+    $log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $lookup_tranche_id is not registered/, $log_location);
+    ok(1, "test lookup within shared memory allocations");
+
+    $next_index = $current_size;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    $node->psql('postgres', qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+    $log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $lookup_tranche_id is not registered/, $log_location);
+    ok(1, "test lookup outside shared memory allocations");
+}
+
+# Lookup when no tranche names are registered
+$node->append_conf('postgresql.conf',
+    qq(test_lwlock_tranches.requested_named_tranches=0));
+$node->stop();
+$node->start();
+
+my $last_possible = 65535 - $first_user_defined;
+my ($id1, $id2, $id3, $id4, $id5) = (0, $first_user_defined - 1, $first_user_defined, min($last_possible, $first_user_defined + 5000), $last_possible);
+
+$node->safe_psql('postgres', qq{select test_get_lwlock_identifier($id1);});
+$node->safe_psql('postgres', qq{select test_get_lwlock_identifier($id2);});
+ok(1, "check for no error when looking up built-in names");
+
+$node->psql('postgres', qq{select test_get_lwlock_identifier($id3);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id3 is not registered/, $log_location);
+
+$node->psql('postgres', qq{select test_get_lwlock_identifier($id4);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id4 is not registered/, $log_location);
+
+$node->psql('postgres', qq{select test_get_lwlock_identifier($id5);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id5 is not registered/, $log_location);
+
+ok(1, "check for error looking up user-defined names");
+
+done_testing();
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
new file mode 100644
index 00000000000..7fdf90e3e23
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
@@ -0,0 +1,16 @@
+-- test_lwlock_tranches--1.0.sql
+
+CREATE FUNCTION test_lwlock_new_tranche_id(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_lwlock_new_tranche_id'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_get_lwlock_identifier(int)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_get_lwlock_identifier'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_lwlock_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_lwlock_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
new file mode 100644
index 00000000000..654ca9a5f44
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
@@ -0,0 +1,102 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_lwlock_tranches_shmem_request(void);
+static void test_lwlock_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_lwlock_tranches_requested_named_tranches = 0;
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_lwlock_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_lwlock_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_lwlock_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_lwlock_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_lwlock_tranches_shmem_startup(void)
+{
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+}
+
+static void
+test_lwlock_tranches_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	for (int i = 0; i < test_lwlock_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_new_tranche_id);
+Datum
+test_lwlock_new_tranche_id(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+
+	for (int i = test_lwlock_tranches_requested_named_tranches; i < num; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_get_lwlock_identifier);
+Datum
+test_get_lwlock_identifier(PG_FUNCTION_ARGS)
+{
+	GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_tranches_get_first_user_defined);
+Datum
+test_lwlock_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf
new file mode 100644
index 00000000000..cd8c352900d
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf
@@ -0,0 +1,2 @@
+shared_preload_libraries = 'test_lwlock_tranches'
+test_lwlock_tranches.requested_named_tranches = 2
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
new file mode 100644
index 00000000000..bf0ecf64376
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
@@ -0,0 +1,6 @@
+# test_lwlock_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_lwlock_tranches'
\ No newline at end of file
-- 
2.43.0

#48Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#47)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

see v9

Sorry about the quick follow-up I was not happy with one of the
comment updates in
SyncLWLockTrancheNames. I updated it.

Attached is v10,

--

Sami

Attachments:

v10-0001-Implement-a-DSA-for-LWLock-tranche-names.patchapplication/octet-stream; name=v10-0001-Implement-a-DSA-for-LWLock-tranche-names.patchDownload
From c46a40fa773b996c1a292f91b1360ca4d28179ae Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Fri, 15 Aug 2025 16:48:10 +0000
Subject: [PATCH v10 1/2] Implement a DSA for LWLock tranche names

Previously, only LWLock tranches registered during postmaster startup were
available to all backends via a tranche name array inherited through fork.
Other backends that wanted to know about a tranche name had to register it
using LWLockRegisterTranche. While this design worked as intended, it was
not ideal because if a backend did not call LWLockRegisterTranche, it would
show a generic "extension" wait_event name in pg_stat_activity instead of
the actual tranche name. This caused inconsistencies with other backends
that registered the tranche name.

Commit fe07100e82b0 made dynamic creation of named DSA areas (and dshash)
much simpler for a backend, so it is anticipated that fewer tranche names
will be registered during postmaster startup in the future, with more
registered dynamically. This will make the above issue more common.

To address this, this change introduces a DSA to store LWLock tranche names
after postmaster startup. When a backend looks up a tranche name by tranche ID,
it first checks its local cache; if the name is not found locally, it fetches
it from the DSA and caches it for future lookups.

This also simplifies the API for registering tranche names: all registration
work is now done via LWLockNewTrancheId, which accepts a tranche name. The
LWLockRegisterTranche call is no longer required.

Previously users could pass arbitrary tranche IDs (that is, IDs not created
via LWLockNewTrancheId) to LWLockInitialize. This now results in an error
message.

Discussion: https://www.postgresql.org/message-id/flat/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   4 +-
 doc/src/sgml/xfunc.sgml                       |  18 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/ipc/ipci.c                |   4 +
 src/backend/storage/lmgr/lwlock.c             | 452 +++++++++++++++---
 .../utils/activity/wait_event_names.txt       |   2 +
 src/backend/utils/init/postinit.c             |   2 +
 src/include/storage/lwlock.h                  |  22 +-
 src/include/storage/lwlocklist.h              |   2 +
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   4 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 src/tools/pgindent/typedefs.list              |   2 +
 15 files changed, 435 insertions(+), 117 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..b623589b430 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,8 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
-
 	return found;
 }
 
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..20b0767f204 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3757,9 +3757,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3778,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 2fa045e6b0f..2f0df67bf7a 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -184,6 +184,8 @@ AttachSharedMemoryStructs(void)
 
 	CreateOrAttachShmemStructs();
 
+	LWLockTrancheNamesBEInit();
+
 	/*
 	 * Now give loadable modules a chance to set up their shmem allocations
 	 */
@@ -343,6 +345,8 @@ CreateOrAttachShmemStructs(void)
 	WaitEventCustomShmemInit();
 	InjectionPointShmemInit();
 	AioShmemInit();
+
+	LWLockTrancheNamesShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index ec9c345ffdf..941b1ce321b 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -80,6 +80,7 @@
 #include "pg_trace.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
+#include "storage/ipc.h"
 #include "storage/proc.h"
 #include "storage/proclist.h"
 #include "storage/procnumber.h"
@@ -125,9 +126,11 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. Tranche names are registered in LWLockTrancheNames in
+ * two ways: if registration happens during postmaster startup, the names are
+ * stored directly in the local array; if registration happens afterward, the
+ * names are first stored in DSA and later cached into the local array during lookup.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -144,14 +147,6 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 				 LWTRANCHE_FIRST_USER_DEFINED,
 				 "missing entries in BuiltinTrancheNames[]");
 
-/*
- * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, 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 **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
-
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
  * the pointer by fork from the postmaster (except in the EXEC_BACKEND case,
@@ -187,6 +182,53 @@ typedef struct NamedLWLockTrancheRequest
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
 static int	NamedLWLockTrancheRequestsAllocated = 0;
 
+/* struct representing the LWLock tranche names stored in DSA */
+typedef struct LWLockTrancheNamesShmem
+{
+	/* pointer to the DSA shared memory, created in-place */
+	void	   *raw_dsa_area;
+	/* DSA Pointer to a list of DSA pointers, each storing a tranche name */
+	dsa_pointer list_ptr;
+
+	/*
+	 * The number of slots allocated for tranche names. The allocated size
+	 * will grow geometrically to reduce resizing of the list_ptr.
+	 */
+	int			allocated;
+
+	/* lock to protect access to the list */
+	LWLock		lock;
+} LWLockTrancheNamesShmem;
+
+/*
+ * Struct representing the names of LWLock tranches, containing pointers
+ * to both local and shared tranche names.
+ */
+typedef struct LWLockTrancheNamesStruct
+{
+	/*
+	 * Tranche names allocated in shared memory after postmaster startup, when
+	 * dynamic allocation is allowed.
+	 */
+	LWLockTrancheNamesShmem *shmem;
+	dsa_area   *dsa;
+
+	/*
+	 * Tranche names stored in local backend memory, either during postmaster
+	 * startup or as a local cache afterward.
+	 */
+	const char **local;
+	int			allocated;		/* number of pre-allocated slots for tranche
+								 * names */
+} LWLockTrancheNamesStruct;
+
+static LWLockTrancheNamesStruct LWLockTrancheNames =
+{
+	NULL, NULL, NULL, 0
+};
+
+#define LWLOCK_TRANCHE_NAMES_INIT_SIZE 16
+
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
  * and the length of the shared-memory NamedLWLockTrancheArray later on.
@@ -201,7 +243,11 @@ NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
+static void SetLocalTrancheName(int tranche_index, const char *tranche_name);
+static void SetSharedTrancheName(int tranche_index, const char *tranche_name);
 static const char *GetLWTrancheName(uint16 trancheId);
+static void LWLockTrancheNamesAttach(void);
+static void LWLockTrancheNamesDetach(void);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -448,11 +494,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -513,7 +554,7 @@ InitializeLWLocks(void)
 			name = trancheNames;
 			trancheNames += strlen(request->tranche_name) + 1;
 			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
+			tranche->trancheId = LWLockNewTrancheId(name);
 			tranche->trancheName = name;
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
@@ -569,61 +610,149 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID and register it with the given tranche
+ * name.  Tranche IDs created by the postmaster will not be used
+ * for a tranche created in shared memory, since the index is generated
+ * from the shared LWLockCounter.
+ *
+ * The tranche name will be user-visible as a wait event name, so use
+ * a name that matches the style of existing wait events.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
+	int			tranche_id;
+	int			tranche_index;
 	int		   *LWLockCounter;
 
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 	/* We use the ShmemLock spinlock to protect LWLockCounter */
 	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
+	tranche_id = (*LWLockCounter)++;
 	SpinLockRelease(ShmemLock);
 
-	return result;
+	tranche_index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
+
+	if (IsUnderPostmaster || !IsPostmasterEnvironment)
+		SetSharedTrancheName(tranche_index, tranche_name);
+	else
+		SetLocalTrancheName(tranche_index, tranche_name);
+
+	return tranche_id;
 }
 
 /*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
+ * Adds a tranche name to shared memory. This routine should only be
+ * called from LWLockNewTrancheId.
  */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
+static void
+SetSharedTrancheName(int tranche_index, const char *tranche_name)
 {
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	dsa_pointer *name_ptrs;
+	dsa_pointer str_ptr;
+	char	   *str_addr;
+	int			len;
+	int			current_allocated;
+	int			new_alloc = 0;
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_EXCLUSIVE);
+
+	current_allocated = LWLockTrancheNames.shmem->allocated;
+
+	/* Tranche array does not exist, initialize it */
+	if (!DsaPointerIsValid(LWLockTrancheNames.shmem->list_ptr))
+	{
+		new_alloc = LWLOCK_TRANCHE_NAMES_INIT_SIZE;
+
+		LWLockTrancheNames.shmem->allocated = new_alloc;
+		LWLockTrancheNames.shmem->list_ptr =
+			dsa_allocate0(LWLockTrancheNames.dsa, new_alloc * sizeof(dsa_pointer));
+
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+	}
+
+	/*
+	 * Need to enlarge the tranche array. Allocate a new list, copy the
+	 * current tranche name pointers into it, and update shared memory to
+	 * point to the new list.
+	 */
+	else if (tranche_index >= current_allocated)
+	{
+		dsa_pointer new_list;
+		dsa_pointer *old_ptrs;
+
+		new_alloc = pg_nextpower2_32(Max(LWLOCK_TRANCHE_NAMES_INIT_SIZE,
+										 tranche_index + 1));
+
+		new_list = dsa_allocate(LWLockTrancheNames.dsa,
+								new_alloc * sizeof(dsa_pointer));
+
+		old_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+								   LWLockTrancheNames.shmem->list_ptr);
+
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, new_list);
+
+		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+		memcpy(name_ptrs, old_ptrs, sizeof(dsa_pointer) * current_allocated);
+
+		dsa_free(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
+
+		LWLockTrancheNames.shmem->list_ptr = new_list;
+		LWLockTrancheNames.shmem->allocated = new_alloc;
+	}
+	/* Use the current list */
+	else
 	{
-		int			newalloc;
+		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa,
+									LWLockTrancheNames.shmem->list_ptr);
+	}
+
+	/* Allocate and copy new string */
+	len = strlen(tranche_name) + 1;
+	str_ptr = dsa_allocate(LWLockTrancheNames.dsa, len);
+	str_addr = dsa_get_address(LWLockTrancheNames.dsa, str_ptr);
+	memcpy(str_addr, tranche_name, len);
+
+	name_ptrs[tranche_index] = str_ptr;
+
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
+/*
+ * Adds a tranche name to local memory. This routine should only be
+ * called from LWLockNewTrancheId and SyncLWLockTrancheNames.
+ */
+static void
+SetLocalTrancheName(int tranche_index, const char *tranche_name)
+{
+	int			newalloc;
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	Assert(tranche_name);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
+	/* If necessary, create or enlarge array. */
+	if (tranche_index >= LWLockTrancheNames.allocated)
+	{
+		newalloc = pg_nextpower2_32(Max(8, tranche_index + 1));
+
+		if (LWLockTrancheNames.local == NULL)
+		{
+			LWLockTrancheNames.local = (const char **)
 				MemoryContextAllocZero(TopMemoryContext,
 									   newalloc * sizeof(char *));
+		}
 		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+		{
+			LWLockTrancheNames.local =
+				repalloc0_array(LWLockTrancheNames.local, const char *, LWLockTrancheNames.allocated, newalloc);
+		}
+
+		LWLockTrancheNames.allocated = newalloc;
 	}
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	LWLockTrancheNames.local[tranche_index] = tranche_name;
 }
 
 /*
@@ -674,10 +803,15 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 
 /*
  * LWLockInitialize - initialize a new lwlock; it's initially unlocked
+ *
+ * We check if the tranche_id exists. GetLWTrancheName will raise an
+ * error if it does not.
  */
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -709,27 +843,101 @@ LWLockReportWaitEnd(void)
 }
 
 /*
- * Return the name of an LWLock tranche.
+ * Update the local cache of LWLock tranche names using the list from shared
+ * memory and the tranches requested suring postmaster startup. When syncing
+ * from shared memory, we stop updating the cache upon encountering an invalid
+ * DSA pointer, which indicates that no valid tranche names can exist beyond
+ * that point.
+ */
+static void
+SyncLWLockTrancheNames(void)
+{
+	dsa_pointer *pointers = dsa_get_address(LWLockTrancheNames.dsa,
+											LWLockTrancheNames.shmem->list_ptr);
+
+	int			i = 0;
+
+	/*
+	 * Sync tranches requested during postmaster startup (via
+	 * RequestNamedLWLockTranche). Although this list is static, it is not
+	 * worth doing something to avoid syncing each time, especially since
+	 * these syncs are infrequent and the list is likely very small.
+	 */
+	while (i < NamedLWLockTrancheRequests)
+	{
+		NamedLWLockTranche *tranche;
+
+		tranche = &NamedLWLockTrancheArray[i];
+
+		SetLocalTrancheName(i, tranche->trancheName);
+
+		i++;
+	}
+
+	/*
+	 * Acquire shared lock on tranche names shared memory, and sync the shared
+	 * tranches.
+	 */
+	LWLockAcquire(&LWLockTrancheNames.shmem->lock, LW_SHARED);
+
+	while (i < LWLockTrancheNames.shmem->allocated)
+	{
+		const char *tranche_name;
+
+		/* Stop iteration if the pointer is invalid */
+		if (!DsaPointerIsValid(pointers[i]))
+			break;
+
+		/* Get tranche name from shared memory */
+		tranche_name = (const char *) dsa_get_address(LWLockTrancheNames.dsa, pointers[i]);
+
+		/* Update local tranche name */
+		SetLocalTrancheName(i, tranche_name);
+
+		i++;
+	}
+
+	/* Release shared lock */
+	LWLockRelease(&LWLockTrancheNames.shmem->lock);
+}
+
+/*
+ * Return the name of an LWLock tranche; or raise an error
+ * if it not found.
  */
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	const char *tranche_name = NULL;
+	uint16		tranche_id_saved = trancheId;
+	bool		needs_sync = false;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
+	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * This is an extension tranche, so check LWLockTrancheNames[]. First,
+	 * sync the local list with shared memory (if not under postmaster or
+	 * single-user) if needed, then perform the lookup. If the tranche ID is
+	 * not found, error out.
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	needs_sync = ((trancheId >= LWLockTrancheNames.allocated ||
+				   LWLockTrancheNames.local[trancheId] == NULL)
+				  && (IsUnderPostmaster || !IsPostmasterEnvironment));
+
+	if (needs_sync)
+		SyncLWLockTrancheNames();
+
+	if (trancheId < LWLockTrancheNames.allocated)
+		tranche_name = LWLockTrancheNames.local[trancheId];
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	if (!tranche_name)
+		elog(ERROR, "tranche ID %d is not registered", tranche_id_saved);
 
-	return LWLockTrancheNames[trancheId];
+	return tranche_name;
 }
 
 /*
@@ -1995,3 +2203,135 @@ LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode)
 	}
 	return false;
 }
+
+/*
+ * Initial Size of the shared memory to create in place the tranche DSA.
+ * This should just be the minimum size of a DSA segment.
+ */
+static Size
+LWLockTrancheNamesInitSize()
+{
+	Size		sz;
+
+	sz = DSA_MIN_SEGMENT_SIZE;
+
+	Assert(dsa_minimum_size() <= sz);
+
+	return MAXALIGN(sz);
+}
+
+Size
+LWLockTrancheNamesShmemSize(void)
+{
+	Size		sz;
+
+	sz = MAXALIGN(sizeof(LWLockTrancheNamesStruct));
+	sz = add_size(sz, LWLockTrancheNamesInitSize());
+
+	return sz;
+}
+
+/*
+ * Allocate the DSA to store tranche names in dynamic shared memory. This
+ * allocation happens during postmaster startup, so create_in_place is used.
+ * At this stage, the postmaster should only create the DSA and avoid any other
+ * operations, as allocating additional DSM segments will not yet be possible.
+ */
+void
+LWLockTrancheNamesShmemInit(void)
+{
+	bool		found;
+
+	/* Initialize or attach to shared memory structure */
+	LWLockTrancheNames.shmem = (LWLockTrancheNamesShmem *)
+		ShmemInitStruct("Dynamic LWLock tranche names",
+						LWLockTrancheNamesShmemSize(),
+						&found);
+
+	if (!IsUnderPostmaster)
+	{
+		dsa_area   *dsa;
+		LWLockTrancheNamesShmem *ctl = LWLockTrancheNames.shmem;
+		char	   *p = (char *) ctl;
+
+		/* Calculate layout within the shared memory region */
+		p += MAXALIGN(sizeof(LWLockTrancheNamesShmem));
+		ctl->raw_dsa_area = p;
+
+		LWLockInitialize(&ctl->lock, LWTRANCHE_LWLOCK_TRANCHE_NAMES_LIST);
+
+		/* Create a dynamic shared memory area in-place */
+		dsa = dsa_create_in_place(ctl->raw_dsa_area,
+								  LWLockTrancheNamesInitSize(),
+								  LWTRANCHE_LWLOCK_TRANCHE_NAMES_DSA,
+								  NULL);
+
+		/*
+		 * Postmaster will never access these again, thus free the local
+		 * dsa/dshash references.
+		 */
+		dsa_pin(dsa);
+		dsa_detach(dsa);
+
+		ctl->allocated = 0;
+		ctl->list_ptr = InvalidDsaPointer;
+	}
+	else
+	{
+		Assert(found);
+	}
+}
+
+static void
+LWLockTrancheNamesAttach(void)
+{
+	MemoryContext oldcontext;
+
+	Assert(LWLockTrancheNames.dsa == NULL);
+
+	/* stats shared memory persists for the backend lifetime */
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+
+	LWLockTrancheNames.dsa = dsa_attach_in_place(LWLockTrancheNames.shmem->raw_dsa_area,
+												 NULL);
+	dsa_pin_mapping(LWLockTrancheNames.dsa);
+
+	Assert(LWLockTrancheNames.dsa);
+
+	MemoryContextSwitchTo(oldcontext);
+}
+
+static void
+LWLockTrancheNamesDetach(void)
+{
+	Assert(LWLockTrancheNames.dsa);
+
+	dsa_detach(LWLockTrancheNames.dsa);
+
+	dsa_release_in_place(LWLockTrancheNames.shmem->raw_dsa_area);
+
+	LWLockTrancheNames.dsa = NULL;
+}
+
+/* Detach the DSA from a backend */
+static void
+LWLockTrancheNamesShutdownHook(int code, Datum arg)
+{
+	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
+
+	LWLockTrancheNamesDetach();
+}
+
+/* Attach the DSA to a backend */
+void
+LWLockTrancheNamesBEInit(void)
+{
+	/* Already attached, nothing to do */
+	if (LWLockTrancheNames.dsa)
+		return;
+
+	LWLockTrancheNamesAttach();
+
+	/* Set up a process-exit hook to clean up */
+	before_shmem_exit(LWLockTrancheNamesShutdownHook, 0);
+}
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 0be307d2ca0..f60ed927892 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -406,6 +406,8 @@ SubtransSLRU	"Waiting to access the sub-transaction SLRU cache."
 XactSLRU	"Waiting to access the transaction status SLRU cache."
 ParallelVacuumDSA	"Waiting for parallel vacuum dynamic shared memory allocation."
 AioUringCompletion	"Waiting for another process to complete IO via io_uring."
+LWLockDynamicTranchesDSA	"Waiting to access the LWLock tranche names DSA area."
+LWLockDynamicTranchesList	"Waiting to access the list of LWLock tranche names in DSA."
 
 # No "ABI_compatibility" region here as WaitEventLWLock has its own C code.
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 641e535a73c..1ef04c6b5ec 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -662,6 +662,8 @@ BaseInit(void)
 	 * drop ephemeral slots, which in turn triggers stats reporting.
 	 */
 	ReplicationSlotInitialize();
+
+	LWLockTrancheNamesBEInit();
 }
 
 
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..8a008fc67dd 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -156,22 +156,18 @@ extern void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
- * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * An alternative, more flexible method of obtaining lwlocks is available.
+ * First, call LWLockNewTrancheId once, providing an associated tranche name.
+ * This function allocates a tranche ID from a shared counter. Then, for each
+ * lwlock, call LWLockInitialize once, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
+extern void LWLockTrancheNamesShmemInit(void);
+extern Size LWLockTrancheNamesShmemSize(void);
+extern void LWLockTrancheNamesBEInit(void);
+
 /*
  * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also,
  * we reserve additional tranche IDs for builtin tranches not included in
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 208d2e3a8ed..5978648c09d 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -135,3 +135,5 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU)
 PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU)
 PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA)
 PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_DSA, LWLockDynamicTranchesDSA)
+PG_LWLOCKTRANCHE(LWLOCK_TRANCHE_NAMES_LIST, LWLockDynamicTranchesList)
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..7f3e6849401 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,8 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
-
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
 
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e6f2e93b2d6..80cdbfa6d0d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1531,6 +1531,8 @@ LWLock
 LWLockHandle
 LWLockMode
 LWLockPadded
+LWLockTrancheNamesShmem
+LWLockTrancheNamesStruct
 LZ4F_compressionContext_t
 LZ4F_decompressOptions_t
 LZ4F_decompressionContext_t
-- 
2.43.0

v10-0002-Add-tests-for-LWLock-tranche-names-DSA.patchapplication/octet-stream; name=v10-0002-Add-tests-for-LWLock-tranche-names-DSA.patchDownload
From c4e47d76deb41e70b934faabdfb42ddd41bbef1c Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-38-230.ec2.internal>
Date: Fri, 15 Aug 2025 17:53:03 +0000
Subject: [PATCH v10 2/2] Add tests for LWLock tranche names DSA

---
 src/backend/storage/lmgr/lwlock.c             |  11 ++
 src/backend/utils/activity/wait_event.c       |   3 -
 src/include/utils/wait_classes.h              |   2 +
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 .../modules/test_lwlock_tranches/Makefile     |  23 +++
 .../expected/test_lwlock_tranches.out         |  20 ++
 .../modules/test_lwlock_tranches/meson.build  |  33 ++++
 .../t/001_test_lwlock_tranches.pl             | 173 ++++++++++++++++++
 .../test_lwlock_tranches--1.0.sql             |  16 ++
 .../test_lwlock_tranches.c                    | 102 +++++++++++
 .../test_lwlock_tranches.conf                 |   2 +
 .../test_lwlock_tranches.control              |   6 +
 13 files changed, 391 insertions(+), 4 deletions(-)
 create mode 100644 src/test/modules/test_lwlock_tranches/Makefile
 create mode 100644 src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out
 create mode 100644 src/test/modules/test_lwlock_tranches/meson.build
 create mode 100644 src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf
 create mode 100644 src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control

diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 941b1ce321b..773acd1008d 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -672,6 +672,8 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 
 		name_ptrs = dsa_get_address(LWLockTrancheNames.dsa, LWLockTrancheNames.shmem->list_ptr);
 		memset(name_ptrs, InvalidDsaPointer, new_alloc);
+
+		elog(DEBUG3, "current_allocated: %d in tranche names shared memory", new_alloc);
 	}
 
 	/*
@@ -702,6 +704,8 @@ SetSharedTrancheName(int tranche_index, const char *tranche_name)
 
 		LWLockTrancheNames.shmem->list_ptr = new_list;
 		LWLockTrancheNames.shmem->allocated = new_alloc;
+
+		elog(DEBUG3, "current_allocated: %d in tranche names shared memory", new_alloc);
 	}
 	/* Use the current list */
 	else
@@ -937,6 +941,13 @@ GetLWTrancheName(uint16 trancheId)
 	if (!tranche_name)
 		elog(ERROR, "tranche ID %d is not registered", tranche_id_saved);
 
+	elog(DEBUG3,
+		 "tranche_name %s with tranche_id %d found at index %d. needs_sync: %s",
+		 tranche_name,
+		 tranche_id_saved,
+		 trancheId,
+		 needs_sync ? "true" : "false");
+
 	return tranche_name;
 }
 
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index d9b8f34a355..eba7d338c1f 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -39,9 +39,6 @@ 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_CLASS_MASK	0xFF000000
-#define WAIT_EVENT_ID_MASK		0x0000FFFF
-
 /*
  * Hash tables for storing custom wait event ids and their names in
  * shared memory.
diff --git a/src/include/utils/wait_classes.h b/src/include/utils/wait_classes.h
index 51ee68397d5..6ca0504cee1 100644
--- a/src/include/utils/wait_classes.h
+++ b/src/include/utils/wait_classes.h
@@ -10,6 +10,8 @@
 #ifndef WAIT_CLASSES_H
 #define WAIT_CLASSES_H
 
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+#define WAIT_EVENT_ID_MASK		0x0000FFFF
 
 /* ----------
  * Wait Classes
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..e72800cd2b7 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_lwlock_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..f04910d13d7 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_lwlock_tranches')
diff --git a/src/test/modules/test_lwlock_tranches/Makefile b/src/test/modules/test_lwlock_tranches/Makefile
new file mode 100644
index 00000000000..9867c99cbfd
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_lwlock_tranches/Makefile
+
+MODULE_big = test_lwlock_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_lwlock_tranches.o
+PGFILEDESC = "test_lwlock_tranches - test code LWLock tranche management"
+
+EXTENSION = test_lwlock_tranches
+DATA = test_lwlock_tranches--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_lwlock_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out b/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out
new file mode 100644
index 00000000000..337a8438a59
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out
@@ -0,0 +1,20 @@
+CREATE EXTENSION test_lwlock_tranches;
+SELECT test_lwlock_tranches_get_first_user_defined() AS num_user_defined \gset
+SELECT :num_user_defined;
+ ?column? 
+----------
+       97
+(1 row)
+
+SELECT test_get_lwlock_identifier(:num_user_defined);
+ test_get_lwlock_identifier 
+----------------------------
+ 
+(1 row)
+
+SELECT test_get_lwlock_identifier(:num_user_defined + 1);
+ test_get_lwlock_identifier 
+----------------------------
+ 
+(1 row)
+
diff --git a/src/test/modules/test_lwlock_tranches/meson.build b/src/test/modules/test_lwlock_tranches/meson.build
new file mode 100644
index 00000000000..f8aa700b58f
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_lwlock_tranches_sources = files(
+  'test_lwlock_tranches.c',
+)
+
+if host_system == 'windows'
+  test_lwlock_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_lwlock_tranches',
+    '--FILEDESC', 'test_lwlock_tranches - test code LWLock tranche management',])
+endif
+
+test_lwlock_tranches = shared_module('test_lwlock_tranches',
+  test_lwlock_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_lwlock_tranches
+
+test_install_data += files(
+  'test_lwlock_tranches.control',
+  'test_lwlock_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_lwlock_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_test_lwlock_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
new file mode 100644
index 00000000000..f59925ea0b4
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/t/001_test_lwlock_tranches.pl
@@ -0,0 +1,173 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $ENABLE_LOG_WAIT = 1;
+
+my $isession;
+my $log_location;
+
+# Helper function: wait for one or more logs if $ENABLE_LOG_WAIT is true
+sub maybe_wait_for_log {
+    my ($node, $logs, $log_loc) = @_;
+    return $log_loc unless $ENABLE_LOG_WAIT;
+
+    # Normalize single regex to array
+    $logs = [$logs] unless ref($logs) eq 'ARRAY';
+
+    foreach my $regex (@$logs) {
+        $log_loc = $node->wait_for_log($regex, $log_loc);
+    }
+    return $log_loc;
+}
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+
+# The largest shared memory allocation we require for testing
+my $final_allocation = 64;
+
+# $unallocated_shared_memory will be overridden by the value
+# of $test_lwlock_tranches_requested_named_tranches when it's > 1
+my $unallocated_shared_memory = 2;
+my $requested_named_tranches = 2;
+
+# VALUES MATCHING INTERNALS
+# The initial array size. Should match LWLOCK_TRANCHE_NAMES_INIT_SIZE
+my $initial_size = 16;
+
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_lwlock_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_lwlock_tranches.requested_named_tranches=$requested_named_tranches));
+$node->append_conf('postgresql.conf',
+    qq(log_min_messages=DEBUG3));
+$node->start();
+
+$node->safe_psql('postgres', q(CREATE EXTENSION test_lwlock_tranches));
+
+my $first_user_defined = $node->safe_psql(
+    'postgres',
+    "SELECT test_lwlock_tranches_get_first_user_defined()"
+);
+
+my $next_index = 0;
+my $lookup_tranche_id = 0;
+my $current_size = $initial_size;
+
+$log_location = -s $node->logfile;
+
+if ($requested_named_tranches > 0) {
+    $unallocated_shared_memory += $requested_named_tranches;
+}
+
+if ($unallocated_shared_memory > 0) {
+    while ($current_size < $final_allocation) {
+        $current_size *= 2;
+    }
+
+    # Lookup before allocating shared memory
+    if ($requested_named_tranches > 0) {
+        for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+            $lookup_tranche_id = $next_index + $first_user_defined;
+            $node->safe_psql('postgres',
+                qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+            $log_location = maybe_wait_for_log(
+                $node,
+                qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index./,
+                $log_location
+            );
+        }
+        ok(1, "requested_named_tranches looked up from local cache, before allocating shared memory");
+    }
+
+    # Create new tranches
+    $node->safe_psql('postgres',
+        qq{select test_lwlock_new_tranche_id($current_size - $unallocated_shared_memory)});
+    $log_location = maybe_wait_for_log(
+        $node,
+        qr/ DEBUG:  current_allocated: $final_allocation in tranche names shared memory/, $log_location
+    );
+
+    ok(1, "resize shared memory with allocation up to $current_size tranche names");
+
+    # Lookup after allocating shared memory
+    if ($requested_named_tranches > 0) {
+        for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+            $lookup_tranche_id = $next_index + $first_user_defined;
+            $node->safe_psql('postgres',
+                qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+            $log_location = maybe_wait_for_log(
+                $node,
+                qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index./,
+                $log_location
+            );
+        }
+        ok(1, "requested_named_tranches looked up from local cache, after allocating shared memory");
+    }
+
+    $next_index = $requested_named_tranches;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    my $second_user_defined = $first_user_defined + 1;
+    $node->safe_psql('postgres',
+        qq{select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($lookup_tranche_id);
+           select test_get_lwlock_identifier($first_user_defined);
+           select test_get_lwlock_identifier($second_user_defined)}
+    );
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: true/, $log_location);
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: false/, $log_location);
+    maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock__$next_index with tranche_id $lookup_tranche_id found at index $next_index. needs_sync: false/, $log_location);
+
+    for ($next_index = 0; $next_index < $requested_named_tranches; $next_index++) {
+        $lookup_tranche_id = $first_user_defined + $next_index;
+        maybe_wait_for_log($node, qr/ DEBUG:  tranche_name test_lock_$next_index with tranche_id $lookup_tranche_id found at index $next_index./, $log_location);
+    }
+    ok(1, "lookup with synchronization is successful");
+
+    $log_location = -s $node->logfile;
+
+    # Lookup unregistered tranches
+    $next_index = $current_size - $unallocated_shared_memory;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    $node->psql('postgres', qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+    $log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $lookup_tranche_id is not registered/, $log_location);
+    ok(1, "test lookup within shared memory allocations");
+
+    $next_index = $current_size;
+    $lookup_tranche_id = $next_index + $first_user_defined;
+    $node->psql('postgres', qq{select test_get_lwlock_identifier($lookup_tranche_id)});
+    $log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $lookup_tranche_id is not registered/, $log_location);
+    ok(1, "test lookup outside shared memory allocations");
+}
+
+# Lookup when no tranche names are registered
+$node->append_conf('postgresql.conf',
+    qq(test_lwlock_tranches.requested_named_tranches=0));
+$node->stop();
+$node->start();
+
+my $last_possible = 65535 - $first_user_defined;
+my ($id1, $id2, $id3, $id4, $id5) = (0, $first_user_defined - 1, $first_user_defined, min($last_possible, $first_user_defined + 5000), $last_possible);
+
+$node->safe_psql('postgres', qq{select test_get_lwlock_identifier($id1);});
+$node->safe_psql('postgres', qq{select test_get_lwlock_identifier($id2);});
+ok(1, "check for no error when looking up built-in names");
+
+$node->psql('postgres', qq{select test_get_lwlock_identifier($id3);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id3 is not registered/, $log_location);
+
+$node->psql('postgres', qq{select test_get_lwlock_identifier($id4);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id4 is not registered/, $log_location);
+
+$node->psql('postgres', qq{select test_get_lwlock_identifier($id5);});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche ID $id5 is not registered/, $log_location);
+
+ok(1, "check for error looking up user-defined names");
+
+done_testing();
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
new file mode 100644
index 00000000000..7fdf90e3e23
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql
@@ -0,0 +1,16 @@
+-- test_lwlock_tranches--1.0.sql
+
+CREATE FUNCTION test_lwlock_new_tranche_id(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_lwlock_new_tranche_id'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_get_lwlock_identifier(int)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_get_lwlock_identifier'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_lwlock_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_lwlock_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
new file mode 100644
index 00000000000..654ca9a5f44
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c
@@ -0,0 +1,102 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_lwlock_tranches_shmem_request(void);
+static void test_lwlock_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_lwlock_tranches_requested_named_tranches = 0;
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_lwlock_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_lwlock_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_lwlock_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_lwlock_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_lwlock_tranches_shmem_startup(void)
+{
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+}
+
+static void
+test_lwlock_tranches_shmem_request(void)
+{
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	for (int i = 0; i < test_lwlock_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_new_tranche_id);
+Datum
+test_lwlock_new_tranche_id(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+
+	for (int i = test_lwlock_tranches_requested_named_tranches; i < num; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_get_lwlock_identifier);
+Datum
+test_get_lwlock_identifier(PG_FUNCTION_ARGS)
+{
+	GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_lwlock_tranches_get_first_user_defined);
+Datum
+test_lwlock_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf
new file mode 100644
index 00000000000..cd8c352900d
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf
@@ -0,0 +1,2 @@
+shared_preload_libraries = 'test_lwlock_tranches'
+test_lwlock_tranches.requested_named_tranches = 2
diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
new file mode 100644
index 00000000000..bf0ecf64376
--- /dev/null
+++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control
@@ -0,0 +1,6 @@
+# test_lwlock_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_lwlock_tranches'
\ No newline at end of file
-- 
2.43.0

#49Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#48)
Re: Improve LWLock tranche name visibility across backends

On Mon, Aug 18, 2025 at 01:06:42PM -0500, Sami Imseih wrote:

Attached is v10,

I've been staring at the latest patch for a bit, and I'm a bit concerned
at how much complexity it adds. I think it's a good idea to keep a local
array of tranche names indexed by tranche ID, but the code for managing the
list of DSA pointers scares me. I know we were trying to avoid using
dshash earlier, if for no other reason than it's perhaps not the best data
structure for the job, but ISTM we'd eliminate a lot of complexity if we
offloaded the shmem pieces to a dshash table (or some other shmem-based
data structure we have yet to introduce, like a dslist/dsarray). WDYT?

--
nathan

#50Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#49)
Re: Improve LWLock tranche name visibility across backends

I've been staring at the latest patch for a bit, and I'm a bit concerned
at how much complexity it adds.

The complexity is the fact we have to track a dsa_pointer that points
to an array of dsa_pointers that track the tranche names. We also
do have to copy the array of dsa_pointers into a new pointer when
enlarging. That adds some complexity that dshash would hide away,
but I don't think it's overly complex.

I know we were trying to avoid using
dshash earlier, if for no other reason than it's perhaps not the best data
structure for the job,

yeah, we really just need an append only structure, so dshash did not
seems unnecessary. But also, we moved away from dshash before it was
decided to have a local cache. Perhaps, it's ok now to go back to dshash
considering that it will only be used in rare scenarios: creations of
new tranches and
syncs to local memory. Most of the time the tranches will be satisfied
directly from local cache.

(or some other shmem-based
data structure we have yet to introduce, like a dslist/dsarray).

This will be an interesting API to invest time in, if there could be more
use-cases. I think it's a separate discussion at this point.

--
Sami

#51Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#50)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Mon, Aug 18, 2025 at 05:53:44PM -0500, Sami Imseih wrote:

I've been staring at the latest patch for a bit, and I'm a bit concerned
at how much complexity it adds.

The complexity is the fact we have to track a dsa_pointer that points
to an array of dsa_pointers that track the tranche names. We also
do have to copy the array of dsa_pointers into a new pointer when
enlarging. That adds some complexity that dshash would hide away,
but I don't think it's overly complex.

I also think that's not overly complex but OTOH why add extra complexity if
we have already a machinery at our disposal that takes care of what we need i.e
resizing and so on.

I know we were trying to avoid using
dshash earlier, if for no other reason than it's perhaps not the best data
structure for the job,

I can see that Nathan's concern about dshash was that it could be a bit slower
([1]/messages/by-id/aHV5BsKoSeOkIsNL@nathan). That's probably true but that does not worry me that much given that
adding tranches should be rare enough and the lookups should be rare (to sync the
local cache) and not be that sensible to performance.

yeah, we really just need an append only structure, so dshash did not
seems unnecessary. But also, we moved away from dshash before it was
decided to have a local cache. Perhaps, it's ok now to go back to dshash
considering that it will only be used in rare scenarios: creations of
new tranches and
syncs to local memory. Most of the time the tranches will be satisfied
directly from local cache.

Yes.

(or some other shmem-based
data structure we have yet to introduce, like a dslist/dsarray).

This will be an interesting API to invest time in, if there could be more
use-cases.

I did a quick check and I did not find current use cases: possible candidates
could be in ExecParallelHashTableAlloc(), PTIterationArray, PTEntryArray for
examples but I think they all know the final size upfront so there is no real
need for dsarray for those).

I think it's a separate discussion at this point.

OTOH, that would be a valid use case to introduce this new API but I'm not sure
it's worth it given that the dshash looks good enough for our case (even if not
ideal though).

[1]: /messages/by-id/aHV5BsKoSeOkIsNL@nathan

Regards,

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

#52Nathan Bossart
nathandbossart@gmail.com
In reply to: Bertrand Drouvot (#51)
Re: Improve LWLock tranche name visibility across backends

On Tue, Aug 19, 2025 at 08:09:53AM +0000, Bertrand Drouvot wrote:

On Mon, Aug 18, 2025 at 05:53:44PM -0500, Sami Imseih wrote:

(or some other shmem-based
data structure we have yet to introduce, like a dslist/dsarray).

This will be an interesting API to invest time in, if there could be more
use-cases.

I did a quick check and I did not find current use cases: possible candidates
could be in ExecParallelHashTableAlloc(), PTIterationArray, PTEntryArray for
examples but I think they all know the final size upfront so there is no real
need for dsarray for those).

I think it's a separate discussion at this point.

OTOH, that would be a valid use case to introduce this new API but I'm not sure
it's worth it given that the dshash looks good enough for our case (even if not
ideal though).

IMHO it'd be okay to proceed with dshash for now. It would be pretty easy
to switch to something like a "dslist" in the future.

--
nathan

#53Andres Freund
andres@anarazel.de
In reply to: Nathan Bossart (#52)
Re: Improve LWLock tranche name visibility across backends

Hi,

On 2025-08-19 12:29:14 -0500, Nathan Bossart wrote:

On Tue, Aug 19, 2025 at 08:09:53AM +0000, Bertrand Drouvot wrote:

On Mon, Aug 18, 2025 at 05:53:44PM -0500, Sami Imseih wrote:

(or some other shmem-based
data structure we have yet to introduce, like a dslist/dsarray).

This will be an interesting API to invest time in, if there could be more
use-cases.

I did a quick check and I did not find current use cases: possible candidates
could be in ExecParallelHashTableAlloc(), PTIterationArray, PTEntryArray for
examples but I think they all know the final size upfront so there is no real
need for dsarray for those).

I think it's a separate discussion at this point.

OTOH, that would be a valid use case to introduce this new API but I'm not sure
it's worth it given that the dshash looks good enough for our case (even if not
ideal though).

IMHO it'd be okay to proceed with dshash for now. It would be pretty easy
to switch to something like a "dslist" in the future.

Possibly stupid question - is it really worth having a dynamic structure here?
The number of tranches is strictly bound, it seems like it'd be simpler to
have an array of tranch nmes in shared memory.

Greetings,

Andres Freund

#54Nathan Bossart
nathandbossart@gmail.com
In reply to: Andres Freund (#53)
Re: Improve LWLock tranche name visibility across backends

On Tue, Aug 19, 2025 at 02:06:50PM -0400, Andres Freund wrote:

Possibly stupid question - is it really worth having a dynamic structure here?
The number of tranches is strictly bound, it seems like it'd be simpler to
have an array of tranch nmes in shared memory.

Tranches can be allocated post-startup with LWLockNewTrancheId() (e.g.,
autoprewarm).

--
nathan

#55Andres Freund
andres@anarazel.de
In reply to: Nathan Bossart (#54)
Re: Improve LWLock tranche name visibility across backends

Hi,

On 2025-08-19 13:31:35 -0500, Nathan Bossart wrote:

On Tue, Aug 19, 2025 at 02:06:50PM -0400, Andres Freund wrote:

Possibly stupid question - is it really worth having a dynamic structure here?
The number of tranches is strictly bound, it seems like it'd be simpler to
have an array of tranch nmes in shared memory.

Tranches can be allocated post-startup with LWLockNewTrancheId() (e.g.,
autoprewarm).

Sure, but we don't need to support a large number of tranches. Just make it,
idk, 128 entries long. Adding a dynamically allocated dsm to every server
seems like a waste - ever shared mapping makes fork / exit slower...

Greetings,

Andres Freund

#56Nathan Bossart
nathandbossart@gmail.com
In reply to: Andres Freund (#55)
Re: Improve LWLock tranche name visibility across backends

On Tue, Aug 19, 2025 at 02:37:19PM -0400, Andres Freund wrote:

On 2025-08-19 13:31:35 -0500, Nathan Bossart wrote:

On Tue, Aug 19, 2025 at 02:06:50PM -0400, Andres Freund wrote:

Possibly stupid question - is it really worth having a dynamic structure here?
The number of tranches is strictly bound, it seems like it'd be simpler to
have an array of tranch nmes in shared memory.

Tranches can be allocated post-startup with LWLockNewTrancheId() (e.g.,
autoprewarm).

Sure, but we don't need to support a large number of tranches. Just make it,
idk, 128 entries long. Adding a dynamically allocated dsm to every server
seems like a waste - ever shared mapping makes fork / exit slower...

The other issue is that there's presently no limit on the length of a
tranche name registered via LWLockRegisterTranche(). Life would become
much simpler if we're willing to put a limit on both that and the number of
tranches, but thus far we've been trying to avoid it.

--
nathan

#57Tom Lane
tgl@sss.pgh.pa.us
In reply to: Nathan Bossart (#56)
Re: Improve LWLock tranche name visibility across backends

Nathan Bossart <nathandbossart@gmail.com> writes:

On Tue, Aug 19, 2025 at 02:37:19PM -0400, Andres Freund wrote:

Sure, but we don't need to support a large number of tranches. Just make it,
idk, 128 entries long. Adding a dynamically allocated dsm to every server
seems like a waste - ever shared mapping makes fork / exit slower...

The other issue is that there's presently no limit on the length of a
tranche name registered via LWLockRegisterTranche(). Life would become
much simpler if we're willing to put a limit on both that and the number of
tranches, but thus far we've been trying to avoid it.

I can hardly imagine a reason why it wouldn't be okay to limit the
lengths of tranche names. But especially so if an unlimited length
causes practical problems.

regards, tom lane

#58Sami Imseih
samimseih@gmail.com
In reply to: Tom Lane (#57)
Re: Improve LWLock tranche name visibility across backends

Nathan Bossart <nathandbossart@gmail.com> writes:

On Tue, Aug 19, 2025 at 02:37:19PM -0400, Andres Freund wrote:

Sure, but we don't need to support a large number of tranches. Just make it,
idk, 128 entries long. Adding a dynamically allocated dsm to every server
seems like a waste - ever shared mapping makes fork / exit slower...

The other issue is that there's presently no limit on the length of a
tranche name registered via LWLockRegisterTranche(). Life would become
much simpler if we're willing to put a limit on both that and the number of
tranches, but thus far we've been trying to avoid it.

I can hardly imagine a reason why it wouldn't be okay to limit the
lengths of tranche names. But especially so if an unlimited length
causes practical problems.

If we limit the tranche name to NAMEDATALEN and also limit the
number of tranches an extension can register, we can put this
all in static shared memory (We would still need to have a backend local
cache to allow lookups to avoid going to shared memory).

However, I am not sure what the limit would be for the number of tranches,
and if we do impose something, will it break anything that is out there?

--
Sami

#59Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#58)
Re: Improve LWLock tranche name visibility across backends

On Tue, Aug 19, 2025 at 03:52:33PM -0500, Sami Imseih wrote:

If we limit the tranche name to NAMEDATALEN and also limit the
number of tranches an extension can register, we can put this
all in static shared memory (We would still need to have a backend local
cache to allow lookups to avoid going to shared memory).

I bet we could avoid the local cache by keeping a backend-local copy of
LWLockCounter that gets updated as needed.

However, I am not sure what the limit would be for the number of tranches,
and if we do impose something, will it break anything that is out there?

I can think of contrived scenarios where these limits would present
problems, but I'm not aware of anything that folks are actually doing that
would be affected. In general, the examples I've seen involve allocating a
small number of tranches for an extension, so my assumption is that the
most likely cause of breakage would be someone installing many, many
extensions.

--
nathan

#60Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#59)
Re: Improve LWLock tranche name visibility across backends

On Tue, Aug 19, 2025 at 03:52:33PM -0500, Sami Imseih wrote:

If we limit the tranche name to NAMEDATALEN and also limit the
number of tranches an extension can register, we can put this
all in static shared memory (We would still need to have a backend local
cache to allow lookups to avoid going to shared memory).

I bet we could avoid the local cache by keeping a backend-local copy of
LWLockCounter that gets updated as needed.

maybe. If we agree to impose limits ( both name length and # of tranches ),
that will allow us to do things a bit different.

If there is agreement on setting limits, may I propose
1024 tranches and NAMEDATALEN. Both seem reasonably sufficient.

--
Sami

#61Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#60)
Re: Improve LWLock tranche name visibility across backends

On Tue, Aug 19, 2025 at 04:16:26PM -0500, Sami Imseih wrote:

If there is agreement on setting limits, may I propose
1024 tranches and NAMEDATALEN. Both seem reasonably sufficient.

Let's proceed with that approach for now. We can worry about the exact
limits once this is closer to commit.

--
nathan

#62Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#61)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

If there is agreement on setting limits, may I propose
1024 tranches and NAMEDATALEN. Both seem reasonably sufficient.

Let's proceed with that approach for now. We can worry about the exact
limits once this is closer to commit.

v11 implements the fixed size shared memory as discussed. So, as previous
versions, we totally got rid of LWLockRegisterTranche and users can
register tranches with either RequestNamedLWLockTranche ( during startup )
or LWLockNewTrancheId ( after startup ).

The NamedLWLockTrancheArray, allocated at the end of MainLWLockArray
with a fixed size limited to the number of tranches requested via
RequestNamedLWLockTranche, has been changed in v11.
It can now grow, up to a maximum number of total tranches,
currently 1024, indexed by LWLockCounter. We don't need to keep track
of the number of tranches in a separate variable, and can rely on
LWLockCounter only.

I kept the local array to serve consecutive reads and to avoid having to
take a shared lock on shared memory every time
GetLWTrancheName is called. A new LWLock to protect this array is
required.

LWLockInitialize also errors if the tranche used has not been registered,
as was the case in the previous patch versions.

Several error message to enforce the fixed size limits have also been added.

The next patch set has 0001 which is the core change, and 0002 are the
tests ( not sure if we need them, but they help in the review ).

The tests in 0002 fail on EXEC_BACKEND due to a newly discovered bug [0]/messages/by-id/CAA5RZ0v1_15QPg5Sqd2Qz5rh_qcsyCeHHmRDY89xVHcy2yt5BQ@mail.gmail.com,
that repros on HEAD and likely other branches.

--
Sami

[0]: /messages/by-id/CAA5RZ0v1_15QPg5Sqd2Qz5rh_qcsyCeHHmRDY89xVHcy2yt5BQ@mail.gmail.com

Attachments:

v11-0001-Improve-LWLock-tranche-registration.patchapplication/octet-stream; name=v11-0001-Improve-LWLock-tranche-registration.patchDownload
From 14426e26138a58c827d0bf95ab42d39e0ab0da1e Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-31-110.ec2.internal>
Date: Thu, 21 Aug 2025 21:26:52 +0000
Subject: [PATCH v11 1/2] Improve LWLock tranche registration

Previously, tranche tracking was done in a backend local array. If a
backend did not explicitly register a tranche with
LWLockRegisterTranche, querying pg_stat_activity would show
"extension" instead of the tranche name.

This change moves the NamedLWLockTrancheArray into a fixed size
shared memory allocated from MainLWLockArray. Tranche creation is now
centralized, and a user only needs to call LWLockNewTrancheId with a
tranche name, and the tranche is stored in shared memory. Backends
look up tranche IDs locally, syncing their cache from shared memory
on a cache miss.

A new LWLock protects NamedLWLockTrancheArray during reads and
writes, ensuring consistency across backends.

Discussion: https://www.postgresql.org/message-id/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   3 +-
 doc/src/sgml/xfunc.sgml                       |  18 +-
 src/backend/postmaster/launch_backend.c       |   4 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/lmgr/lwlock.c             | 208 +++++++++---------
 .../utils/activity/wait_event_names.txt       |   1 +
 src/include/storage/lwlock.h                  |  18 +-
 src/include/storage/lwlocklist.h              |   1 +
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 13 files changed, 133 insertions(+), 163 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..880e897796a 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,7 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
 
 	return found;
 }
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..20b0767f204 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3757,9 +3757,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3778,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index bf6b55ee830..51832ed19fe 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,8 +101,8 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	NamedLWLockTranche *NamedLWLockTrancheArray;
 	LWLockPadded *MainLWLockArray;
+	const char *NamedLWLockTrancheArray;
 	slock_t    *ProcStructLock;
 	PROC_HDR   *ProcGlobal;
 	PGPROC	   *AuxiliaryProcs;
@@ -760,8 +760,8 @@ save_backend_variables(BackendParameters *param,
 #endif
 
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
-	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
 	param->MainLWLockArray = MainLWLockArray;
+	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
 	param->ProcStructLock = ProcStructLock;
 	param->ProcGlobal = ProcGlobal;
 	param->AuxiliaryProcs = AuxiliaryProcs;
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..8f9595e8730 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index c80b43f1f55..717583d7237 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -105,6 +105,9 @@
 #define LW_SHARED_MASK				MAX_BACKENDS
 #define LW_LOCK_MASK				(MAX_BACKENDS | LW_VAL_EXCLUSIVE)
 
+#define MAX_NAMED_TRANCHES			1024
+#define MAX_NAMED_TRANCHES_NAME_LEN			NAMEDATALEN
+
 
 StaticAssertDecl(((MAX_BACKENDS + 1) & MAX_BACKENDS) == 0,
 				 "MAX_BACKENDS + 1 needs to be a power of 2");
@@ -125,9 +128,10 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. The names of these tranches are first stored in the
+ * shared NamedLWLockTrancheArray, and then cached locally in LWLockTrancheNames[]
+ * to avoid accessing shared memory on every lookup.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -146,11 +150,18 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * stores the names of all dynamically created tranches in shared memory.
+ * Any unused entries in the array will contain NULL. This variable is
+ * non-static so that postmaster.c can copy it to child processes in
+ * EXEC_BACKEND builds.
+ */
+const char *NamedLWLockTrancheArray = NULL;
+
+/*
+ * This is a local copy of NamedLWLockTrancheArray to avoid repeated access
+ * to shared memory when lookig up a tranche with GetLWTrancheName.
  */
 static const char **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -179,12 +190,11 @@ static LWLockHandle held_lwlocks[MAX_SIMUL_LWLOCKS];
 /* struct representing the LWLock tranche request for named tranche */
 typedef struct NamedLWLockTrancheRequest
 {
-	char		tranche_name[NAMEDATALEN];
+	char		tranche_name[MAX_NAMED_TRANCHES_NAME_LEN];
 	int			num_lwlocks;
 } NamedLWLockTrancheRequest;
 
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
-static int	NamedLWLockTrancheRequestsAllocated = 0;
 
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
@@ -194,8 +204,6 @@ static int	NamedLWLockTrancheRequestsAllocated = 0;
  */
 int			NamedLWLockTrancheRequests = 0;
 
-/* points to data in shared memory: */
-NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
@@ -391,7 +399,6 @@ Size
 LWLockShmemSize(void)
 {
 	Size		size;
-	int			i;
 	int			numLocks = NUM_FIXED_LWLOCKS;
 
 	/* Calculate total number of locks needed in the main array. */
@@ -403,19 +410,18 @@ LWLockShmemSize(void)
 	/* Space for dynamic allocation counter, plus room for alignment. */
 	size = add_size(size, sizeof(int) + LWLOCK_PADDED_SIZE);
 
-	/* space for named tranches. */
+	/* Space for named tranches. */
 	size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche)));
 
-	/* space for name of each tranche. */
-	for (i = 0; i < NamedLWLockTrancheRequests; i++)
-		size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1);
+	/* Space for name of each tranche. */
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, MAX_NAMED_TRANCHES));
 
 	return size;
 }
 
 /*
  * Allocate shmem space for the main LWLock array and all tranches and
- * initialize it.  We also register extension LWLock tranches here.
+ * initialize it.
  */
 void
 CreateLWLocks(void)
@@ -447,11 +453,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -485,38 +486,27 @@ InitializeLWLocks(void)
 	for (id = 0; id < NUM_PREDICATELOCK_PARTITIONS; id++, lock++)
 		LWLockInitialize(&lock->lock, LWTRANCHE_PREDICATE_LOCK_MANAGER);
 
-	/*
-	 * Copy the info about any named tranches into shared memory (so that
-	 * other processes can see it), and initialize the requested LWLocks.
-	 */
+	NamedLWLockTrancheArray = (const char *)
+		&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
+
+	/* Register LWLocks requested with RequestNamedLWLockTranch */
 	if (NamedLWLockTrancheRequests > 0)
 	{
-		char	   *trancheNames;
-
-		NamedLWLockTrancheArray = (NamedLWLockTranche *)
-			&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
-
-		trancheNames = (char *) NamedLWLockTrancheArray +
-			(NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche));
 		lock = &MainLWLockArray[NUM_FIXED_LWLOCKS];
 
 		for (i = 0; i < NamedLWLockTrancheRequests; i++)
 		{
 			NamedLWLockTrancheRequest *request;
-			NamedLWLockTranche *tranche;
-			char	   *name;
+			int			tranche_id;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = &NamedLWLockTrancheArray[i];
 
-			name = trancheNames;
-			trancheNames += strlen(request->tranche_name) + 1;
-			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
-			tranche->trancheName = name;
+			/* Allocate a new tranche ID */
+			tranche_id = LWLockNewTrancheId(request->tranche_name);
 
+			/* Initialize the requested amount of LWLocks */
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
-				LWLockInitialize(&lock->lock, tranche->trancheId);
+				LWLockInitialize(&lock->lock, tranche_id);
 		}
 	}
 }
@@ -571,58 +561,44 @@ GetNamedLWLockTranche(const char *tranche_name)
  * Allocate a new tranche ID.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
+	int			tranche_id;
+	int			index;
+	Size		tranche_name_length = strlen(tranche_name) + 1;
 	int		   *LWLockCounter;
+	const char *slot;
+
+	/* The supplied tranche name is too long */
+	if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+		elog(ERROR, "tranche name too long");
 
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
-	/* We use the ShmemLock spinlock to protect LWLockCounter */
-	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
-	SpinLockRelease(ShmemLock);
 
-	return result;
-}
+	LWLockAcquire(NamedLWLockTrancheArrayLock, LW_EXCLUSIVE);
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-{
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	/* Get the next tranche ID while holding an Exclusive lock */
+	tranche_id = (*LWLockCounter)++;
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	/* Get the NamedLWLockTrancheArray index for the tranche ID */
+	index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	/* Extensions registered too many tranches */
+	if (index > MAX_NAMED_TRANCHES)
 	{
-		int			newalloc;
-
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+		LWLockRelease(NamedLWLockTrancheArrayLock);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+		elog(ERROR, "too many LWLock tranches registered");
 	}
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	/* Get the index location from the NamedLWLockTrancheArray array */
+	slot = NamedLWLockTrancheArray + (index * MAX_NAMED_TRANCHES_NAME_LEN);
+
+	strcpy((char *) slot, tranche_name);
+
+	LWLockRelease(NamedLWLockTrancheArrayLock);
+
+	return tranche_id;
 }
 
 /*
@@ -641,42 +617,41 @@ void
 RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 {
 	NamedLWLockTrancheRequest *request;
+	Size		tranche_name_length = strlen(tranche_name) + 1;
 
 	if (!process_shmem_requests_in_progress)
 		elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook");
 
+	if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+		elog(FATAL, "tranche name too long");
+
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
-		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
-							   NamedLWLockTrancheRequestsAllocated
+							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
-	{
-		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
-
-		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
-			repalloc(NamedLWLockTrancheRequestArray,
-					 i * sizeof(NamedLWLockTrancheRequest));
-		NamedLWLockTrancheRequestsAllocated = i;
-	}
+	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
+		elog(FATAL, "too many dynamic LWLock tranches registered");
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
-	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
-	strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);
+	strlcpy(request->tranche_name, tranche_name, tranche_name_length);
 	request->num_lwlocks = num_lwlocks;
 	NamedLWLockTrancheRequests++;
 }
 
 /*
  * LWLockInitialize - initialize a new lwlock; it's initially unlocked
+ * If the tranche was never registered, GetLWTrancheName will return
+ * and error.
  */
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -713,22 +688,51 @@ LWLockReportWaitEnd(void)
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
+	int			index = 0;
+
 	/* Built-in tranche or individual LWLock? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * This is an extension tranche. Look it up in LWLockTrancheNames[], and
+	 * synchronize from NamedLWLockTrancheArray[] if necessary. If the tranche
+	 * has never been registered by the extension, raise an error.
+	 */
+	index = trancheId - LWTRANCHE_FIRST_USER_DEFINED;
+
+	/*
+	 * Can't find the tranche in the local array, so we need to sync it from
+	 * the shared array.
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	if (LWLockTrancheNames == NULL || LWLockTrancheNames[index] == NULL)
+	{
+		int		   *LWLockCounter;
+
+		LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
+
+		/* first time, let's initialize the local array */
+		if (LWLockTrancheNames == NULL)
+			LWLockTrancheNames = (const char **)
+				MemoryContextAllocZero(TopMemoryContext,
+									   MAX_NAMED_TRANCHES * sizeof(char *));
+
+		/*
+		 * Sync the backend local array from the shared array. A shared lock
+		 * is held on the shared array while syncing.
+		 */
+		LWLockAcquire(NamedLWLockTrancheArrayLock, LW_SHARED);
+		for (int i = 0; i < *LWLockCounter - LWTRANCHE_FIRST_USER_DEFINED; i++)
+			LWLockTrancheNames[i] = NamedLWLockTrancheArray + (i * MAX_NAMED_TRANCHES_NAME_LEN);
+		LWLockRelease(NamedLWLockTrancheArrayLock);
+	}
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	/* Still can't find the tranche, raise an error */
+	if (!LWLockTrancheNames[index])
+		elog(ERROR, "LWLock tranche is not registered");
 
-	return LWLockTrancheNames[trancheId];
+	/* We have a tranche name, return it */
+	return LWLockTrancheNames[index];
 }
 
 /*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 5427da5bc1b..2eed0a0682d 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -352,6 +352,7 @@ DSMRegistry	"Waiting to read or update the dynamic shared memory registry."
 InjectionPoint	"Waiting to read or update information related to injection points."
 SerialControl	"Waiting to read or update shared <filename>pg_serial</filename> state."
 AioWorkerSubmissionQueue	"Waiting to access AIO worker submission queue."
+NamedLWLockTrancheArray	"Waiting to access the named LWLock tranches array."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..1ba77cb3658 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -80,7 +80,7 @@ typedef struct NamedLWLockTranche
 	char	   *trancheName;
 } NamedLWLockTranche;
 
-extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
+extern PGDLLIMPORT const char *NamedLWLockTrancheArray;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
 /*
@@ -157,19 +157,11 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
  * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * LWLockNewTrancheId with an associated tranche name just once to obtain a
+ * tranche ID; this allocates from a shared counter. Finally, LWLockInitialize
+ * should be called just once per lwlock, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
 /*
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 06a1ffd4b08..0b00c5a4b3f 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -85,6 +85,7 @@ PG_LWLOCK(50, DSMRegistry)
 PG_LWLOCK(51, InjectionPoint)
 PG_LWLOCK(52, SerialControl)
 PG_LWLOCK(53, AioWorkerSubmissionQueue)
+PG_LWLOCK(54, NamedLWLockTrancheArray)
 
 /*
  * There also exist several built-in LWLock tranches.  As with the predefined
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

v11-0002-Tests-for-LWLock-tranche-registration-improvemen.patchapplication/octet-stream; name=v11-0002-Tests-for-LWLock-tranche-registration-improvemen.patchDownload
From 3f7ad775a9fc82d2f183a370c2e0cfaf761b1393 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 22 Aug 2025 00:35:45 -0500
Subject: [PATCH v11 2/2] Tests for LWLock tranche registration improvements

---
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_tranches/Makefile       |  23 +++
 src/test/modules/test_tranches/meson.build    |  33 ++++
 .../test_tranches/t/001_test_tranches.pl      | 115 ++++++++++++
 .../test_tranches/test_tranches--1.0.sql      |  21 +++
 .../modules/test_tranches/test_tranches.c     | 165 ++++++++++++++++++
 .../test_tranches/test_tranches.control       |   6 +
 8 files changed, 366 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/test_tranches/Makefile
 create mode 100644 src/test/modules/test_tranches/meson.build
 create mode 100644 src/test/modules/test_tranches/t/001_test_tranches.pl
 create mode 100644 src/test/modules/test_tranches/test_tranches--1.0.sql
 create mode 100644 src/test/modules/test_tranches/test_tranches.c
 create mode 100644 src/test/modules/test_tranches/test_tranches.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..fd7aa4675c5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..52883dfb496 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_tranches')
diff --git a/src/test/modules/test_tranches/Makefile b/src/test/modules/test_tranches/Makefile
new file mode 100644
index 00000000000..5c9e78084da
--- /dev/null
+++ b/src/test/modules/test_tranches/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_tranches/Makefile
+
+MODULE_big = test_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_tranches.o
+PGFILEDESC = "test_tranches - test code LWLock tranche management"
+
+EXTENSION = test_tranches
+DATA = test_tranches--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_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_tranches/meson.build b/src/test/modules/test_tranches/meson.build
new file mode 100644
index 00000000000..976ce9f0a25
--- /dev/null
+++ b/src/test/modules/test_tranches/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_tranches_sources = files(
+  'test_tranches.c',
+)
+
+if host_system == 'windows'
+  test_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_tranches',
+    '--FILEDESC', 'test_tranches - test code LWLock tranche management',])
+endif
+
+test_tranches = shared_module('test_tranches',
+  test_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_tranches
+
+test_install_data += files(
+  'test_tranches.control',
+  'test_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_test_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/t/001_test_tranches.pl b/src/test/modules/test_tranches/t/001_test_tranches.pl
new file mode 100644
index 00000000000..9a56b25e204
--- /dev/null
+++ b/src/test/modules/test_tranches/t/001_test_tranches.pl
@@ -0,0 +1,115 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $UINT16_MAX = 65535;
+my $MAX_DYNAMIC_TRANCHES = 1024;
+
+# Helper function: wait for one or more logs
+sub maybe_wait_for_log {
+    my ($node, $logs, $log_loc) = @_;
+
+    # Normalize single regex to array
+    $logs = [$logs] unless ref($logs) eq 'ARRAY';
+
+    foreach my $regex (@$logs) {
+        $log_loc = $node->wait_for_log($regex, $log_loc);
+    }
+    return $log_loc;
+}
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+
+my $requested_named_tranches = 4;
+
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=$requested_named_tranches));
+$node->append_conf('postgresql.conf',
+    qq(log_min_messages=DEBUG3));
+$node->start();
+
+$node->safe_psql('postgres', q(CREATE EXTENSION test_tranches));
+
+my $next_index = 0;
+my $lookup_tranche_id = 0;
+
+my $log_location = -s $node->logfile;
+
+#
+# Lookup requested tranches
+#
+my $last_tranche_id = $node->safe_psql('postgres',
+    qq{select test_tranches_last('fake')});
+
+my $first_tranche_id = $last_tranche_id - $requested_named_tranches;
+my $second_tranche_id = $first_tranche_id + 1;
+
+my $requested_tranches = $node->safe_psql('postgres',
+    qq{select test_tranches_lookup($first_tranche_id);
+       select test_tranches_lookup($second_tranche_id);});
+
+ok ("test_lock_0\ntest_lock_1" eq $requested_tranches, "looked up requested tranches");
+
+#
+# Create tranches
+#
+$node->safe_psql('postgres', qq{select test_tranches_new(100)});
+my $next_tranche_id = $second_tranche_id + 1;
+$requested_tranches = $node->safe_psql('postgres',
+    qq{select test_tranches_lookup($next_tranche_id);});
+ok ("test_lock_2" eq $requested_tranches, "looked up first dynamic tranches");
+
+#
+# get LWLocks
+#
+$node->safe_psql('postgres',
+    qq{select test_tranches_get_named_lwlock('test_lock_3', 3);});
+
+#
+# restart node without requested tranches to test limits
+#
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=0));
+$node->restart;
+
+# long tranche names
+my $good_tranche_name = 'A' x 63;
+my $bad_tranche_name = 'B' x 64;
+$node->safe_psql('postgres', qq{select test_tranches_last('$good_tranche_name');});
+$node->psql('postgres', qq{select test_tranches_last('$bad_tranche_name');});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  tranche name too long/, $log_location);
+ok(1, "check for error on tranche name too long");
+
+#
+# Check for errors with not registered tranches
+#
+my $beyond_limit_id = $MAX_DYNAMIC_TRANCHES + 100;
+$node->psql('postgres',
+    qq{select test_tranches_lookup($beyond_limit_id)});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  LWLock tranche is not registered/, $log_location);
+
+my $within_limit_id = $MAX_DYNAMIC_TRANCHES - 100;
+$node->psql('postgres',
+    qq{select test_tranches_lookup($within_limit_id)});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  LWLock tranche is not registered/, $log_location);
+
+ok(1, "check for error with not registered tranches");
+
+
+# Check for errors with too many tranches registered
+my $dynamic_tranches_count = $MAX_DYNAMIC_TRANCHES;
+$node->safe_psql('postgres',
+    qq{select test_tranches_new($dynamic_tranches_count)});
+$node->psql('postgres',
+    qq{select test_tranches_new(1)});
+$log_location = maybe_wait_for_log($node, qr/ ERROR:  too many LWLock tranches registered/, $log_location);
+ok(1, "check for error with too many dynamic tranches");
+
+done_testing();
diff --git a/src/test/modules/test_tranches/test_tranches--1.0.sql b/src/test/modules/test_tranches/test_tranches--1.0.sql
new file mode 100644
index 00000000000..1de8972ab41
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches--1.0.sql
@@ -0,0 +1,21 @@
+-- test_tranches--1.0.sql
+
+CREATE FUNCTION test_tranches_new(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_new'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_last(text)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_last'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lookup(int)
+RETURNS text
+AS 'MODULE_PATHNAME', 'test_tranches_lookup'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_named_lwlock(text, int)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_named_lwlock'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_tranches/test_tranches.c b/src/test/modules/test_tranches/test_tranches.c
new file mode 100644
index 00000000000..a27455ce5d2
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.c
@@ -0,0 +1,165 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_tranches_shmem_request(void);
+static void test_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_tranches_requested_named_tranches = 0;
+
+typedef struct testTranchesSharedState
+{
+	int			next_index;
+}			testTranchesSharedState;
+
+static testTranchesSharedState * test_lwlock_ss = NULL;
+
+/*
+ * LWLock wait event masks. Copied from src/backend/utils/activity/wait_event.c
+ */
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_tranches_shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	test_lwlock_ss = NULL;
+
+	test_lwlock_ss = ShmemInitStruct("test_tranches",
+									 sizeof(testTranchesSharedState),
+									 &found);
+	if (!found)
+	{
+		test_lwlock_ss->next_index = test_tranches_requested_named_tranches;
+	}
+}
+
+static Size
+test_tranches_memsize(void)
+{
+	Size		size;
+
+	size = MAXALIGN(sizeof(testTranchesSharedState));
+
+	return size;
+}
+
+static void
+test_tranches_shmem_request(void)
+{
+	int			i = 0;
+
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(test_tranches_memsize());
+
+	for (i = 0; i < test_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new);
+Datum
+test_tranches_new(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+	int			i;
+
+	for (i = test_lwlock_ss->next_index; i < num + test_lwlock_ss->next_index; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	test_lwlock_ss->next_index = i;
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_last);
+Datum
+test_tranches_last(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT32(LWLockNewTrancheId(text_to_cstring(PG_GETARG_TEXT_PP(0))));
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lookup);
+Datum
+test_tranches_lookup(PG_FUNCTION_ARGS)
+{
+	const char *tranche_name = GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	PG_RETURN_TEXT_P(cstring_to_text(tranche_name));
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_named_lwlock);
+Datum
+test_tranches_get_named_lwlock(PG_FUNCTION_ARGS)
+{
+	int			i;
+	LWLockPadded *locks;
+
+	locks = GetNamedLWLockTranche(text_to_cstring(PG_GETARG_TEXT_PP(0)));
+
+	for (i = 0; i < PG_GETARG_INT32(1); i++)
+	{
+		LWLock	   *lock = &locks[i].lock;
+
+		LWLockAcquire(lock, LW_SHARED);
+		LWLockRelease(lock);
+	}
+
+	PG_RETURN_INT32(i);
+}
diff --git a/src/test/modules/test_tranches/test_tranches.control b/src/test/modules/test_tranches/test_tranches.control
new file mode 100644
index 00000000000..8e6254a7e43
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.control
@@ -0,0 +1,6 @@
+# test_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_tranches'
\ No newline at end of file
-- 
2.39.5 (Apple Git-154)

#63Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#62)
Re: Improve LWLock tranche name visibility across backends

On Fri, Aug 22, 2025 at 3:01 PM Sami Imseih <samimseih@gmail.com> wrote:

If there is agreement on setting limits, may I propose
1024 tranches and NAMEDATALEN. Both seem reasonably sufficient.

Let's proceed with that approach for now. We can worry about the exact
limits once this is closer to commit.

v11 implements the fixed size shared memory as discussed.

One point I did not make earlier is that the tranche name lengths will
need to be as long as we allow in dsm_registry.c.

```
#define DSMR_NAME_LEN 128

#define DSMR_DSA_TRANCHE_SUFFIX " DSA"
#define DSMR_DSA_TRANCHE_SUFFIX_LEN (sizeof(DSMR_DSA_TRANCHE_SUFFIX) - 1)
#define DSMR_DSA_TRANCHE_NAME_LEN (DSMR_NAME_LEN + DSMR_DSA_TRANCHE_SUFFIX_LEN)
```

We should make the limits external (in lwlock.h) and dsa_registery.c
can enforce
based on those values.

--
Sami

#64Tom Lane
tgl@sss.pgh.pa.us
In reply to: Sami Imseih (#63)
Re: Improve LWLock tranche name visibility across backends

Sami Imseih <samimseih@gmail.com> writes:

One point I did not make earlier is that the tranche name lengths will
need to be as long as we allow in dsm_registry.c.

#define DSMR_NAME_LEN 128

Huh. Why is that different from NAMEDATALEN in the first place?

regards, tom lane

#65Nathan Bossart
nathandbossart@gmail.com
In reply to: Tom Lane (#64)
Re: Improve LWLock tranche name visibility across backends

On Fri, Aug 22, 2025 at 05:33:55PM -0400, Tom Lane wrote:

Sami Imseih <samimseih@gmail.com> writes:

One point I did not make earlier is that the tranche name lengths will
need to be as long as we allow in dsm_registry.c.

#define DSMR_NAME_LEN 128

Huh. Why is that different from NAMEDATALEN in the first place?

One of the reviewers of commit fe07100 requested it [0]/messages/by-id/58DB9EB7-4646-4508-84BA-F0F067A7E8BA@gmail.com. At the time,
there was no reason to use NAMEDATALEN. I'm fine with lowering it if
needed for the shared tranche name work.

Presently, we also append a " DSA" suffix to the DSA tranche name in
GetNamedDSHash(). I was originally trying to use separate tranches for the
DSA and the dshash table for observability purposes, but since it seems to
be more trouble than it's worth, perhaps we should only allocate one
tranche.

[0]: /messages/by-id/58DB9EB7-4646-4508-84BA-F0F067A7E8BA@gmail.com

--
nathan

#66Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#62)
Re: Improve LWLock tranche name visibility across backends

On Fri, Aug 22, 2025 at 03:01:53PM -0500, Sami Imseih wrote:

I kept the local array to serve consecutive reads and to avoid having to
take a shared lock on shared memory every time GetLWTrancheName is
called. A new LWLock to protect this array is required.

I'm not seeing why we need this cache anymore. This is an append-only
list, so we could instead keep a backend-local copy of LWLockCounter that
gets updated as needed. As long as the ID is less than our backend-local
counter, we can go straight to the shared array. If it is greater, we'll
have to first update our counter, which should be rare and inexpensive.

--
nathan

#67Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#66)
Re: Improve LWLock tranche name visibility across backends

On Fri, Aug 22, 2025 at 03:01:53PM -0500, Sami Imseih wrote:

I kept the local array to serve consecutive reads and to avoid having to
take a shared lock on shared memory every time GetLWTrancheName is
called. A new LWLock to protect this array is required.

I'm not seeing why we need this cache anymore. This is an append-only
list, so we could instead keep a backend-local copy of LWLockCounter that
gets updated as needed. As long as the ID is less than our backend-local
counter, we can go straight to the shared array. If it is greater, we'll
have to first update our counter, which should be rare and inexpensive.

When we lookup from shared array only, we need to take a shared lock
every lookup. Acquiring that lock is what I am trying to avoid. You
are saying it's not worth optimizing that part, correct?

--
Sami

#68Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#67)
Re: Improve LWLock tranche name visibility across backends

On Mon, Aug 25, 2025 at 04:37:21PM -0500, Sami Imseih wrote:

When we lookup from shared array only, we need to take a shared lock
every lookup. Acquiring that lock is what I am trying to avoid. You
are saying it's not worth optimizing that part, correct?

Why do we need a shared lock here? IIUC there's no chance that existing
entries will change. We'll only ever add new ones to the end.

--
nathan

#69Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#68)
Re: Improve LWLock tranche name visibility across backends

On Mon, Aug 25, 2025 at 04:37:21PM -0500, Sami Imseih wrote:

When we lookup from shared array only, we need to take a shared lock
every lookup. Acquiring that lock is what I am trying to avoid. You
are saying it's not worth optimizing that part, correct?

Why do we need a shared lock here? IIUC there's no chance that existing
entries will change. We'll only ever add new ones to the end.

hmm, can we really avoid a shared lock when reading from shared memory?
considering access for both reads and writes can be concurrent to shared
memory. We are also taking an exclusive lock when writing a new tranche.

--
Sami

#70Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#69)
Re: Improve LWLock tranche name visibility across backends

On Mon, Aug 25, 2025 at 04:59:41PM -0500, Sami Imseih wrote:

hmm, can we really avoid a shared lock when reading from shared memory?
considering access for both reads and writes can be concurrent to shared
memory. We are also taking an exclusive lock when writing a new tranche.

We probably want to hold a lock while we 1) increment LWLockCounter and
copy a new tranche name to memory and 2) while we copy the current value of
LWLockCounter to our backend-local variable. Otherwise, AFAICT we don't
need one. We could probably use ShmemLock for this.

--
nathan

#71Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#70)
Re: Improve LWLock tranche name visibility across backends

On Mon, Aug 25, 2025 at 04:59:41PM -0500, Sami Imseih wrote:

hmm, can we really avoid a shared lock when reading from shared memory?
considering access for both reads and writes can be concurrent to shared
memory. We are also taking an exclusive lock when writing a new tranche.

We probably want to hold a lock while we 1) increment LWLockCounter and
copy a new tranche name to memory and

In the last rev, I removed the spinlock acquired on ShmemLock in-lieu of
a LWLock. This is because I wanted a single LWLock acquisition while
both incrementing LWLockCounter and writing to shared memory, and
doing this much work, particularly writing to shared memory,
with a spinlock seemed inappropriate. With that said, this is not high
concurrency of performance sensitive activity at all, so perhaps I was
being overly paranoid.

--
Sami

#72Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#71)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

On Mon, Aug 25, 2025 at 04:59:41PM -0500, Sami Imseih wrote:

hmm, can we really avoid a shared lock when reading from shared memory?
considering access for both reads and writes can be concurrent to shared
memory. We are also taking an exclusive lock when writing a new tranche.

We probably want to hold a lock while we 1) increment LWLockCounter and
copy a new tranche name to memory and

In the last rev, I removed the spinlock acquired on ShmemLock in-lieu of
a LWLock. This is because I wanted a single LWLock acquisition while
both incrementing LWLockCounter and writing to shared memory, and
doing this much work, particularly writing to shared memory,
with a spinlock seemed inappropriate. With that said, this is not high
concurrency of performance sensitive activity at all, so perhaps I was
being overly paranoid.

Here is v12 that replaces the LWLock to access the shared memory with a
ShmemLock and implements a local counter.

--
Sami

Attachments:

v12-0002-Tests-for-LWLock-tranche-registration-improvemen.patchapplication/octet-stream; name=v12-0002-Tests-for-LWLock-tranche-registration-improvemen.patchDownload
From 45f4762338cdcb76515884e7f05fca787fa4513f Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 22 Aug 2025 00:35:45 -0500
Subject: [PATCH v12 2/2] Tests for LWLock tranche registration improvements

---
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_tranches/Makefile       |  23 +++
 src/test/modules/test_tranches/meson.build    |  33 ++++
 .../test_tranches/t/001_test_tranches.pl      |  60 +++++++
 .../test_tranches/test_tranches--1.0.sql      |  21 +++
 .../modules/test_tranches/test_tranches.c     | 165 ++++++++++++++++++
 .../test_tranches/test_tranches.control       |   6 +
 8 files changed, 311 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/test_tranches/Makefile
 create mode 100644 src/test/modules/test_tranches/meson.build
 create mode 100644 src/test/modules/test_tranches/t/001_test_tranches.pl
 create mode 100644 src/test/modules/test_tranches/test_tranches--1.0.sql
 create mode 100644 src/test/modules/test_tranches/test_tranches.c
 create mode 100644 src/test/modules/test_tranches/test_tranches.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..fd7aa4675c5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..52883dfb496 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_tranches')
diff --git a/src/test/modules/test_tranches/Makefile b/src/test/modules/test_tranches/Makefile
new file mode 100644
index 00000000000..5c9e78084da
--- /dev/null
+++ b/src/test/modules/test_tranches/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_tranches/Makefile
+
+MODULE_big = test_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_tranches.o
+PGFILEDESC = "test_tranches - test code LWLock tranche management"
+
+EXTENSION = test_tranches
+DATA = test_tranches--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_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_tranches/meson.build b/src/test/modules/test_tranches/meson.build
new file mode 100644
index 00000000000..976ce9f0a25
--- /dev/null
+++ b/src/test/modules/test_tranches/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_tranches_sources = files(
+  'test_tranches.c',
+)
+
+if host_system == 'windows'
+  test_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_tranches',
+    '--FILEDESC', 'test_tranches - test code LWLock tranche management',])
+endif
+
+test_tranches = shared_module('test_tranches',
+  test_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_tranches
+
+test_install_data += files(
+  'test_tranches.control',
+  'test_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_test_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/t/001_test_tranches.pl b/src/test/modules/test_tranches/t/001_test_tranches.pl
new file mode 100644
index 00000000000..f74fb9a54a1
--- /dev/null
+++ b/src/test/modules/test_tranches/t/001_test_tranches.pl
@@ -0,0 +1,60 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=0));
+$node->start();
+
+$node->safe_psql('postgres', q(CREATE EXTENSION test_tranches));
+
+my $log_location = -s $node->logfile;
+
+my ($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_new(5);
+        select test_tranches_lookup(96);
+        select test_tranches_lookup(97);
+        select test_tranches_lookup(98);
+        select test_tranches_lookup(99);
+        select test_tranches_lookup(100);
+        select test_tranches_lookup(101);
+        select test_tranches_new(5);
+        select test_tranches_lookup(101);
+        select test_tranches_lookup(102);
+        select test_tranches_lookup(103);
+        select test_tranches_lookup(104);
+        select test_tranches_lookup(105);
+        select test_tranches_lookup(106);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  LWLock tranche is not registered\n.*ERROR:  LWLock tranche is not registered/, "unregistered tranche error");
+like("$stdout", qr/test_lock__0.*test_lock__1.*test_lock__2.*test_lock__3.*test_lock__4/s, "matches first 5 tranche names");
+
+my $good_tranche_name = 'A' x 63;
+my $bad_tranche_name = 'B' x 64;
+$node->safe_psql('postgres', qq{select test_tranches_last('$good_tranche_name');});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_last('$bad_tranche_name');
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche name too long/, "tranche name too long");
+
+$node->restart();
+
+$node->safe_psql('postgres', qq{select test_tranches_new(1023)});
+$node->safe_psql('postgres', qq{select test_tranches_new(1)});
+($result, $stdout, $stderr) = $node->psql('postgres', qq{select test_tranches_new(1);}, on_error_stop => 0);
+like("$stderr", qr/ERROR:  too many LWLock tranches registered/, "too many tranches registered");
+
+done_testing();
diff --git a/src/test/modules/test_tranches/test_tranches--1.0.sql b/src/test/modules/test_tranches/test_tranches--1.0.sql
new file mode 100644
index 00000000000..1de8972ab41
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches--1.0.sql
@@ -0,0 +1,21 @@
+-- test_tranches--1.0.sql
+
+CREATE FUNCTION test_tranches_new(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_new'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_last(text)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_last'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lookup(int)
+RETURNS text
+AS 'MODULE_PATHNAME', 'test_tranches_lookup'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_named_lwlock(text, int)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_named_lwlock'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_tranches/test_tranches.c b/src/test/modules/test_tranches/test_tranches.c
new file mode 100644
index 00000000000..a27455ce5d2
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.c
@@ -0,0 +1,165 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_tranches_shmem_request(void);
+static void test_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_tranches_requested_named_tranches = 0;
+
+typedef struct testTranchesSharedState
+{
+	int			next_index;
+}			testTranchesSharedState;
+
+static testTranchesSharedState * test_lwlock_ss = NULL;
+
+/*
+ * LWLock wait event masks. Copied from src/backend/utils/activity/wait_event.c
+ */
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_tranches_shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	test_lwlock_ss = NULL;
+
+	test_lwlock_ss = ShmemInitStruct("test_tranches",
+									 sizeof(testTranchesSharedState),
+									 &found);
+	if (!found)
+	{
+		test_lwlock_ss->next_index = test_tranches_requested_named_tranches;
+	}
+}
+
+static Size
+test_tranches_memsize(void)
+{
+	Size		size;
+
+	size = MAXALIGN(sizeof(testTranchesSharedState));
+
+	return size;
+}
+
+static void
+test_tranches_shmem_request(void)
+{
+	int			i = 0;
+
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(test_tranches_memsize());
+
+	for (i = 0; i < test_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new);
+Datum
+test_tranches_new(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+	int			i;
+
+	for (i = test_lwlock_ss->next_index; i < num + test_lwlock_ss->next_index; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	test_lwlock_ss->next_index = i;
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_last);
+Datum
+test_tranches_last(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT32(LWLockNewTrancheId(text_to_cstring(PG_GETARG_TEXT_PP(0))));
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lookup);
+Datum
+test_tranches_lookup(PG_FUNCTION_ARGS)
+{
+	const char *tranche_name = GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	PG_RETURN_TEXT_P(cstring_to_text(tranche_name));
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_named_lwlock);
+Datum
+test_tranches_get_named_lwlock(PG_FUNCTION_ARGS)
+{
+	int			i;
+	LWLockPadded *locks;
+
+	locks = GetNamedLWLockTranche(text_to_cstring(PG_GETARG_TEXT_PP(0)));
+
+	for (i = 0; i < PG_GETARG_INT32(1); i++)
+	{
+		LWLock	   *lock = &locks[i].lock;
+
+		LWLockAcquire(lock, LW_SHARED);
+		LWLockRelease(lock);
+	}
+
+	PG_RETURN_INT32(i);
+}
diff --git a/src/test/modules/test_tranches/test_tranches.control b/src/test/modules/test_tranches/test_tranches.control
new file mode 100644
index 00000000000..8e6254a7e43
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.control
@@ -0,0 +1,6 @@
+# test_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_tranches'
\ No newline at end of file
-- 
2.39.5 (Apple Git-154)

v12-0001-Improve-LWLock-tranche-registration.patchapplication/octet-stream; name=v12-0001-Improve-LWLock-tranche-registration.patchDownload
From ba84c4f086e70a24415d692603ecb9c699773b38 Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-31-110.ec2.internal>
Date: Thu, 21 Aug 2025 21:26:52 +0000
Subject: [PATCH v12 1/2] Improve LWLock tranche registration

Previously, tranche tracking was done in a backend local array. If a backend
did not explicitly register a tranche with LWLockRegisterTranche, querying
pg_stat_activity would show "extension" instead of the tranche name.

This change reserves more space in NamedLWLockTrancheArray, up to 1024 tranches
with each name of length NAMEDATALEN, in shared memory. Tranche creation is
now centralized. A user only needs to call LWLockNewTrancheId with a tranche
name, and the tranche is stored in shared memory. Access to shared memory is
protected by ShmemLock.

With LWLockNewTrancheId taking full responsibility for registering a tranche,
the LWLockRegisterTranche API is no longer required.

Discussion: https://www.postgresql.org/message-id/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   3 +-
 doc/src/sgml/xfunc.sgml                       |  18 +-
 src/backend/postmaster/launch_backend.c       |   4 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/lmgr/lwlock.c             | 197 ++++++++----------
 .../utils/activity/wait_event_names.txt       |   1 +
 src/include/storage/lwlock.h                  |  18 +-
 src/include/storage/lwlocklist.h              |   1 +
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 13 files changed, 118 insertions(+), 167 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..880e897796a 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,7 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
 
 	return found;
 }
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..20b0767f204 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3757,9 +3757,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3778,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index bf6b55ee830..51832ed19fe 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,8 +101,8 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	NamedLWLockTranche *NamedLWLockTrancheArray;
 	LWLockPadded *MainLWLockArray;
+	const char *NamedLWLockTrancheArray;
 	slock_t    *ProcStructLock;
 	PROC_HDR   *ProcGlobal;
 	PGPROC	   *AuxiliaryProcs;
@@ -760,8 +760,8 @@ save_backend_variables(BackendParameters *param,
 #endif
 
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
-	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
 	param->MainLWLockArray = MainLWLockArray;
+	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
 	param->ProcStructLock = ProcStructLock;
 	param->ProcGlobal = ProcGlobal;
 	param->AuxiliaryProcs = AuxiliaryProcs;
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..8f9595e8730 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index c80b43f1f55..e6ab1070bda 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -105,6 +105,9 @@
 #define LW_SHARED_MASK				MAX_BACKENDS
 #define LW_LOCK_MASK				(MAX_BACKENDS | LW_VAL_EXCLUSIVE)
 
+#define MAX_NAMED_TRANCHES			1024
+#define MAX_NAMED_TRANCHES_NAME_LEN			NAMEDATALEN
+
 
 StaticAssertDecl(((MAX_BACKENDS + 1) & MAX_BACKENDS) == 0,
 				 "MAX_BACKENDS + 1 needs to be a power of 2");
@@ -125,9 +128,9 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. The names of these tranches are stored in the shared
+ * NamedLWLockTrancheArray.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -146,11 +149,12 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * stores the names of all dynamically created tranches in shared memory.
+ * Any unused entries in the array will contain NULL. This variable is
+ * non-static so that postmaster.c can copy it to child processes in
+ * EXEC_BACKEND builds.
  */
-static const char **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
+const char *NamedLWLockTrancheArray = NULL;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -179,12 +183,11 @@ static LWLockHandle held_lwlocks[MAX_SIMUL_LWLOCKS];
 /* struct representing the LWLock tranche request for named tranche */
 typedef struct NamedLWLockTrancheRequest
 {
-	char		tranche_name[NAMEDATALEN];
+	char		tranche_name[MAX_NAMED_TRANCHES_NAME_LEN];
 	int			num_lwlocks;
 } NamedLWLockTrancheRequest;
 
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
-static int	NamedLWLockTrancheRequestsAllocated = 0;
 
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
@@ -194,8 +197,13 @@ static int	NamedLWLockTrancheRequestsAllocated = 0;
  */
 int			NamedLWLockTrancheRequests = 0;
 
-/* points to data in shared memory: */
-NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
+/*
+ * Backend-local counter for the number of registered tranches.
+ * This is used to ensure the backend is aware of the latest tranche
+ * before accessing shared memory.
+ */
+static int	NamedLWLocksCount = 0;
+
 
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
@@ -391,7 +399,6 @@ Size
 LWLockShmemSize(void)
 {
 	Size		size;
-	int			i;
 	int			numLocks = NUM_FIXED_LWLOCKS;
 
 	/* Calculate total number of locks needed in the main array. */
@@ -403,19 +410,18 @@ LWLockShmemSize(void)
 	/* Space for dynamic allocation counter, plus room for alignment. */
 	size = add_size(size, sizeof(int) + LWLOCK_PADDED_SIZE);
 
-	/* space for named tranches. */
+	/* Space for named tranches. */
 	size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche)));
 
-	/* space for name of each tranche. */
-	for (i = 0; i < NamedLWLockTrancheRequests; i++)
-		size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1);
+	/* Space for name of each tranche. */
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, MAX_NAMED_TRANCHES));
 
 	return size;
 }
 
 /*
  * Allocate shmem space for the main LWLock array and all tranches and
- * initialize it.  We also register extension LWLock tranches here.
+ * initialize it.
  */
 void
 CreateLWLocks(void)
@@ -447,11 +453,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -485,38 +486,27 @@ InitializeLWLocks(void)
 	for (id = 0; id < NUM_PREDICATELOCK_PARTITIONS; id++, lock++)
 		LWLockInitialize(&lock->lock, LWTRANCHE_PREDICATE_LOCK_MANAGER);
 
-	/*
-	 * Copy the info about any named tranches into shared memory (so that
-	 * other processes can see it), and initialize the requested LWLocks.
-	 */
+	NamedLWLockTrancheArray = (const char *)
+		&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
+
+	/* Register LWLocks requested with RequestNamedLWLockTranch */
 	if (NamedLWLockTrancheRequests > 0)
 	{
-		char	   *trancheNames;
-
-		NamedLWLockTrancheArray = (NamedLWLockTranche *)
-			&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
-
-		trancheNames = (char *) NamedLWLockTrancheArray +
-			(NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche));
 		lock = &MainLWLockArray[NUM_FIXED_LWLOCKS];
 
 		for (i = 0; i < NamedLWLockTrancheRequests; i++)
 		{
 			NamedLWLockTrancheRequest *request;
-			NamedLWLockTranche *tranche;
-			char	   *name;
+			int			tranche_id;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = &NamedLWLockTrancheArray[i];
 
-			name = trancheNames;
-			trancheNames += strlen(request->tranche_name) + 1;
-			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
-			tranche->trancheName = name;
+			/* Allocate a new tranche ID */
+			tranche_id = LWLockNewTrancheId(request->tranche_name);
 
+			/* Initialize the requested amount of LWLocks */
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
-				LWLockInitialize(&lock->lock, tranche->trancheId);
+				LWLockInitialize(&lock->lock, tranche_id);
 		}
 	}
 }
@@ -571,58 +561,36 @@ GetNamedLWLockTranche(const char *tranche_name)
  * Allocate a new tranche ID.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
-	int		   *LWLockCounter;
+	int			tranche_id,
+				index,
+			   *LWLockCounter;
+	Size		tranche_name_length = strlen(tranche_name) + 1;
 
-	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
-	/* We use the ShmemLock spinlock to protect LWLockCounter */
-	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
-	SpinLockRelease(ShmemLock);
+	if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+		elog(ERROR, "tranche name too long");
 
-	return result;
-}
+	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-{
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	SpinLockAcquire(ShmemLock);
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	tranche_id = (*LWLockCounter)++;
+	index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (index >= MAX_NAMED_TRANCHES)
 	{
-		int			newalloc;
+		SpinLockRelease(ShmemLock);
+		elog(ERROR, "too many LWLock tranches registered");
+	}
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	/* Directly copy into the NamedLWLockTrancheArray */
+	strcpy((char *) NamedLWLockTrancheArray + (index * MAX_NAMED_TRANCHES_NAME_LEN),
+		   tranche_name);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
-	}
+	SpinLockRelease(ShmemLock);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	return tranche_id;
 }
 
 /*
@@ -641,42 +609,41 @@ void
 RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 {
 	NamedLWLockTrancheRequest *request;
+	Size		tranche_name_length = strlen(tranche_name) + 1;
 
 	if (!process_shmem_requests_in_progress)
 		elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook");
 
+	if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+		elog(FATAL, "tranche name too long");
+
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
-		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
-							   NamedLWLockTrancheRequestsAllocated
+							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
-	{
-		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
-
-		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
-			repalloc(NamedLWLockTrancheRequestArray,
-					 i * sizeof(NamedLWLockTrancheRequest));
-		NamedLWLockTrancheRequestsAllocated = i;
-	}
+	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
+		elog(FATAL, "too many LWLock tranches registered");
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
-	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
-	strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);
+	strlcpy(request->tranche_name, tranche_name, tranche_name_length);
 	request->num_lwlocks = num_lwlocks;
 	NamedLWLockTrancheRequests++;
 }
 
 /*
  * LWLockInitialize - initialize a new lwlock; it's initially unlocked
+ * If the tranche was never registered, GetLWTrancheName will return
+ * and error.
  */
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -713,22 +680,40 @@ LWLockReportWaitEnd(void)
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
-	/* Built-in tranche or individual LWLock? */
+	int			index;
+
+	/* Built-in tranche? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in NamedLWLockTrancheArray[].
+	 * Compute the index to lookup NamedLWLockTrancheArray[].
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	index = trancheId - LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	/*
+	 * If the requested index exceeds the count of known tranches, update
+	 * NamedLWLocksCount to reflect the current state in shared memory. This
+	 * ensures the backend is aware of all registered tranches.
+	 */
+	if (index >= NamedLWLocksCount)
+	{
+		int		   *LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 
-	return LWLockTrancheNames[trancheId];
+		SpinLockAcquire(ShmemLock);
+		NamedLWLocksCount = *LWLockCounter - LWTRANCHE_FIRST_USER_DEFINED;
+		SpinLockRelease(ShmemLock);
+	}
+
+	/*
+	 * Return the tranche name if the index is valid. Otherwise, raise an
+	 * error.
+	 */
+	if (index < NamedLWLocksCount)
+		return NamedLWLockTrancheArray + (index * MAX_NAMED_TRANCHES_NAME_LEN);
+	else
+		elog(ERROR, "LWLock tranche is not registered");
 }
 
 /*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 5427da5bc1b..2eed0a0682d 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -352,6 +352,7 @@ DSMRegistry	"Waiting to read or update the dynamic shared memory registry."
 InjectionPoint	"Waiting to read or update information related to injection points."
 SerialControl	"Waiting to read or update shared <filename>pg_serial</filename> state."
 AioWorkerSubmissionQueue	"Waiting to access AIO worker submission queue."
+NamedLWLockTrancheArray	"Waiting to access the named LWLock tranches array."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..1ba77cb3658 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -80,7 +80,7 @@ typedef struct NamedLWLockTranche
 	char	   *trancheName;
 } NamedLWLockTranche;
 
-extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
+extern PGDLLIMPORT const char *NamedLWLockTrancheArray;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
 /*
@@ -157,19 +157,11 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
  * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * LWLockNewTrancheId with an associated tranche name just once to obtain a
+ * tranche ID; this allocates from a shared counter. Finally, LWLockInitialize
+ * should be called just once per lwlock, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
 /*
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index 06a1ffd4b08..0b00c5a4b3f 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -85,6 +85,7 @@ PG_LWLOCK(50, DSMRegistry)
 PG_LWLOCK(51, InjectionPoint)
 PG_LWLOCK(52, SerialControl)
 PG_LWLOCK(53, AioWorkerSubmissionQueue)
+PG_LWLOCK(54, NamedLWLockTrancheArray)
 
 /*
  * There also exist several built-in LWLock tranches.  As with the predefined
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

#73Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#72)
Re: Improve LWLock tranche name visibility across backends

On Tue, Aug 26, 2025 at 02:56:22PM -0500, Sami Imseih wrote:

Here is v12 that replaces the LWLock to access the shared memory with a
ShmemLock and implements a local counter.

This looks much closer to what I was imagining.

 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);

We probably need to do the sprintf/strcpy before LWLockNewTrancheId().
Also, I'm thinking we should just use the same tranche for both the DSA and
the dshash table [0]/messages/by-id/aKzIg1JryN1qhNuy@nathan to evade the DSMR_DSA_TRANCHE_SUFFIX problem, i.e.,
those tranche names potentially require more space.

+	/* Space for name of each tranche. */
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, MAX_NAMED_TRANCHES));

Presumably one of the mul_size() arguments should be
MAX_NAMED_TRANCHES_NAME_LEN.

+NamedLWLockTrancheArray "Waiting to access the named LWLock tranches array."

+PG_LWLOCK(54, NamedLWLockTrancheArray)

These can be removed now, right?

[0]: /messages/by-id/aKzIg1JryN1qhNuy@nathan

--
nathan

#74Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#73)
3 attachment(s)
Re: Improve LWLock tranche name visibility across backends

fixed the issues mentioned above in v13.

We probably need to do the sprintf/strcpy before LWLockNewTrancheId().
Also, I'm thinking we should just use the same tranche for both the DSA and
the dshash table [0] to evade the DSMR_DSA_TRANCHE_SUFFIX problem, i.e.,
those tranche names potentially require more space.

v13 also includes a separate patch for this. It removes the
DSMR_DSA_TRANCHE_SUFFIX, and dsh and dsa now use the same
user defined tranche. The tranche name is also limited to
the max length of a named tranche, MAX_NAMED_TRANCHES_NAME_LEN,
coming from lwlock.h

--
Sami

Attachments:

v13-0002-Tests-for-LWLock-tranche-registration-improvemen.patchapplication/octet-stream; name=v13-0002-Tests-for-LWLock-tranche-registration-improvemen.patchDownload
From 4f91d69167764c6fbd6af0ab970033c8b4a1115e Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 22 Aug 2025 00:35:45 -0500
Subject: [PATCH v13 2/3] Tests for LWLock tranche registration improvements

---
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_tranches/Makefile       |  23 +++
 src/test/modules/test_tranches/meson.build    |  33 ++++
 .../test_tranches/t/001_test_tranches.pl      |  98 ++++++++++
 .../test_tranches/test_tranches--1.0.sql      |  26 +++
 .../modules/test_tranches/test_tranches.c     | 172 ++++++++++++++++++
 .../test_tranches/test_tranches.control       |   6 +
 8 files changed, 361 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/test_tranches/Makefile
 create mode 100644 src/test/modules/test_tranches/meson.build
 create mode 100644 src/test/modules/test_tranches/t/001_test_tranches.pl
 create mode 100644 src/test/modules/test_tranches/test_tranches--1.0.sql
 create mode 100644 src/test/modules/test_tranches/test_tranches.c
 create mode 100644 src/test/modules/test_tranches/test_tranches.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..fd7aa4675c5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..52883dfb496 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_tranches')
diff --git a/src/test/modules/test_tranches/Makefile b/src/test/modules/test_tranches/Makefile
new file mode 100644
index 00000000000..5c9e78084da
--- /dev/null
+++ b/src/test/modules/test_tranches/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_tranches/Makefile
+
+MODULE_big = test_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_tranches.o
+PGFILEDESC = "test_tranches - test code LWLock tranche management"
+
+EXTENSION = test_tranches
+DATA = test_tranches--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_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_tranches/meson.build b/src/test/modules/test_tranches/meson.build
new file mode 100644
index 00000000000..976ce9f0a25
--- /dev/null
+++ b/src/test/modules/test_tranches/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_tranches_sources = files(
+  'test_tranches.c',
+)
+
+if host_system == 'windows'
+  test_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_tranches',
+    '--FILEDESC', 'test_tranches - test code LWLock tranche management',])
+endif
+
+test_tranches = shared_module('test_tranches',
+  test_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_tranches
+
+test_install_data += files(
+  'test_tranches.control',
+  'test_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_test_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/t/001_test_tranches.pl b/src/test/modules/test_tranches/t/001_test_tranches.pl
new file mode 100644
index 00000000000..86951eb5485
--- /dev/null
+++ b/src/test/modules/test_tranches/t/001_test_tranches.pl
@@ -0,0 +1,98 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+#
+# Create the cluster without any requested tranches
+#
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=0));
+$node->start();
+$node->safe_psql('postgres', q(CREATE EXTENSION test_tranches));
+
+my $first_user_defined = $node->safe_psql('postgres', q(select test_tranches_get_first_user_defined()));
+my @user_defined = ($first_user_defined .. $first_user_defined + 5);
+my ($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+#
+# Run lookup tests to ensure the correct tranche names are returned
+# and an error is raised for unregistered tranches
+#
+my ($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_new(5);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  LWLock tranche is not registered/, "unregistered tranche error");
+like("$stdout", qr/test_lock__0.*test_lock__1.*test_lock__2.*test_lock__3.*test_lock__4/s, "matches first 5 tranche names");
+
+#
+# Test the error for long tranche names
+#
+my $good_tranche_name = 'A' x 63;
+my $bad_tranche_name = 'B' x 64; # MAX_NAMED_TRANCHES_NAME_LEN
+$node->safe_psql('postgres', qq{select test_tranches_last('$good_tranche_name');});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_last('$bad_tranche_name');
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche name too long/, "tranche name too long");
+
+$node->restart();
+
+#
+# Test the error for too many registered tranches
+#
+$node->safe_psql('postgres', qq{select test_tranches_new(1023)});
+$node->safe_psql('postgres', qq{select test_tranches_new(1)});
+($result, $stdout, $stderr) = $node->psql('postgres', qq{select test_tranches_new(1);}, on_error_stop => 0);
+like("$stderr", qr/ERROR:  too many LWLock tranches registered/, "too many tranches registered");
+
+#
+# Repeat the lookup test with 2 requested tranches
+#
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=2));
+$node->restart();
+
+$first_user_defined = $first_user_defined;
+@user_defined = ($first_user_defined .. $first_user_defined + 5);
+($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_new(3);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  LWLock tranche is not registered/, "unregistered tranche error");
+like("$stdout", qr/test_lock_0.*test_lock_1.*test_lock__2.*test_lock__3.*test_lock__4/s, "matches first 5 tranche names");
+
+done_testing();
diff --git a/src/test/modules/test_tranches/test_tranches--1.0.sql b/src/test/modules/test_tranches/test_tranches--1.0.sql
new file mode 100644
index 00000000000..c7c99bc2b8c
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches--1.0.sql
@@ -0,0 +1,26 @@
+-- test_tranches--1.0.sql
+
+CREATE FUNCTION test_tranches_new(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_new'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_last(text)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_last'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lookup(int)
+RETURNS text
+AS 'MODULE_PATHNAME', 'test_tranches_lookup'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_named_lwlock(text, int)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_named_lwlock'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/test_tranches.c b/src/test/modules/test_tranches/test_tranches.c
new file mode 100644
index 00000000000..54c1528e5c7
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.c
@@ -0,0 +1,172 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_tranches_shmem_request(void);
+static void test_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_tranches_requested_named_tranches = 0;
+
+typedef struct testTranchesSharedState
+{
+	int			next_index;
+}			testTranchesSharedState;
+
+static testTranchesSharedState * test_lwlock_ss = NULL;
+
+/*
+ * LWLock wait event masks. Copied from src/backend/utils/activity/wait_event.c
+ */
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_tranches_shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	test_lwlock_ss = NULL;
+
+	test_lwlock_ss = ShmemInitStruct("test_tranches",
+									 sizeof(testTranchesSharedState),
+									 &found);
+	if (!found)
+	{
+		test_lwlock_ss->next_index = test_tranches_requested_named_tranches;
+	}
+}
+
+static Size
+test_tranches_memsize(void)
+{
+	Size		size;
+
+	size = MAXALIGN(sizeof(testTranchesSharedState));
+
+	return size;
+}
+
+static void
+test_tranches_shmem_request(void)
+{
+	int			i = 0;
+
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(test_tranches_memsize());
+
+	for (i = 0; i < test_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new);
+Datum
+test_tranches_new(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+	int			i;
+
+	for (i = test_lwlock_ss->next_index; i < num + test_lwlock_ss->next_index; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	test_lwlock_ss->next_index = i;
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_last);
+Datum
+test_tranches_last(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT32(LWLockNewTrancheId(text_to_cstring(PG_GETARG_TEXT_PP(0))));
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lookup);
+Datum
+test_tranches_lookup(PG_FUNCTION_ARGS)
+{
+	const char *tranche_name = GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	PG_RETURN_TEXT_P(cstring_to_text(tranche_name));
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_named_lwlock);
+Datum
+test_tranches_get_named_lwlock(PG_FUNCTION_ARGS)
+{
+	int			i;
+	LWLockPadded *locks;
+
+	locks = GetNamedLWLockTranche(text_to_cstring(PG_GETARG_TEXT_PP(0)));
+
+	for (i = 0; i < PG_GETARG_INT32(1); i++)
+	{
+		LWLock	   *lock = &locks[i].lock;
+
+		LWLockAcquire(lock, LW_SHARED);
+		LWLockRelease(lock);
+	}
+
+	PG_RETURN_INT32(i);
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_first_user_defined);
+Datum
+test_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
diff --git a/src/test/modules/test_tranches/test_tranches.control b/src/test/modules/test_tranches/test_tranches.control
new file mode 100644
index 00000000000..8e6254a7e43
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.control
@@ -0,0 +1,6 @@
+# test_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_tranches'
\ No newline at end of file
-- 
2.39.5 (Apple Git-154)

v13-0001-Improve-LWLock-tranche-registration.patchapplication/octet-stream; name=v13-0001-Improve-LWLock-tranche-registration.patchDownload
From 983bf9b05dae1749d891fe50c8233c3c739abb23 Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-31-110.ec2.internal>
Date: Thu, 21 Aug 2025 21:26:52 +0000
Subject: [PATCH v13 1/3] Improve LWLock tranche registration

Previously, tranche tracking was done in a backend local array. If a backend
did not explicitly register a tranche with LWLockRegisterTranche, querying
pg_stat_activity would show "extension" instead of the tranche name.

This change reserves more space in NamedLWLockTrancheArray, up to 1024 tranches
with each name of length NAMEDATALEN, in shared memory. Tranche creation is
now centralized. A user only needs to call LWLockNewTrancheId with a tranche
name, and the tranche is stored in shared memory. Access to shared memory is
protected by ShmemLock.

With LWLockNewTrancheId taking full responsibility for registering a tranche,
the LWLockRegisterTranche API is no longer required.

Discussion: https://www.postgresql.org/message-id/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   3 +-
 doc/src/sgml/xfunc.sgml                       |  18 +-
 src/backend/postmaster/launch_backend.c       |   4 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/lmgr/lwlock.c             | 197 ++++++++----------
 src/include/storage/lwlock.h                  |  18 +-
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 11 files changed, 116 insertions(+), 167 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..880e897796a 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,7 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
 
 	return found;
 }
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..20b0767f204 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3757,9 +3757,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3778,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index bf6b55ee830..51832ed19fe 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,8 +101,8 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	NamedLWLockTranche *NamedLWLockTrancheArray;
 	LWLockPadded *MainLWLockArray;
+	const char *NamedLWLockTrancheArray;
 	slock_t    *ProcStructLock;
 	PROC_HDR   *ProcGlobal;
 	PGPROC	   *AuxiliaryProcs;
@@ -760,8 +760,8 @@ save_backend_variables(BackendParameters *param,
 #endif
 
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
-	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
 	param->MainLWLockArray = MainLWLockArray;
+	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
 	param->ProcStructLock = ProcStructLock;
 	param->ProcGlobal = ProcGlobal;
 	param->AuxiliaryProcs = AuxiliaryProcs;
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index c80b43f1f55..81f9914bf08 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -105,6 +105,9 @@
 #define LW_SHARED_MASK				MAX_BACKENDS
 #define LW_LOCK_MASK				(MAX_BACKENDS | LW_VAL_EXCLUSIVE)
 
+#define MAX_NAMED_TRANCHES			1024
+#define MAX_NAMED_TRANCHES_NAME_LEN			NAMEDATALEN
+
 
 StaticAssertDecl(((MAX_BACKENDS + 1) & MAX_BACKENDS) == 0,
 				 "MAX_BACKENDS + 1 needs to be a power of 2");
@@ -125,9 +128,9 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. The names of these tranches are stored in the shared
+ * NamedLWLockTrancheArray.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -146,11 +149,12 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * stores the names of all dynamically created tranches in shared memory.
+ * Any unused entries in the array will contain NULL. This variable is
+ * non-static so that postmaster.c can copy it to child processes in
+ * EXEC_BACKEND builds.
  */
-static const char **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
+const char *NamedLWLockTrancheArray = NULL;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -179,12 +183,11 @@ static LWLockHandle held_lwlocks[MAX_SIMUL_LWLOCKS];
 /* struct representing the LWLock tranche request for named tranche */
 typedef struct NamedLWLockTrancheRequest
 {
-	char		tranche_name[NAMEDATALEN];
+	char		tranche_name[MAX_NAMED_TRANCHES_NAME_LEN];
 	int			num_lwlocks;
 } NamedLWLockTrancheRequest;
 
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
-static int	NamedLWLockTrancheRequestsAllocated = 0;
 
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
@@ -194,8 +197,13 @@ static int	NamedLWLockTrancheRequestsAllocated = 0;
  */
 int			NamedLWLockTrancheRequests = 0;
 
-/* points to data in shared memory: */
-NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
+/*
+ * Backend-local counter for the number of registered tranches.
+ * This is used to ensure the backend is aware of the latest tranche
+ * before accessing shared memory.
+ */
+static int	NamedLWLocksCount = 0;
+
 
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
@@ -391,7 +399,6 @@ Size
 LWLockShmemSize(void)
 {
 	Size		size;
-	int			i;
 	int			numLocks = NUM_FIXED_LWLOCKS;
 
 	/* Calculate total number of locks needed in the main array. */
@@ -403,19 +410,18 @@ LWLockShmemSize(void)
 	/* Space for dynamic allocation counter, plus room for alignment. */
 	size = add_size(size, sizeof(int) + LWLOCK_PADDED_SIZE);
 
-	/* space for named tranches. */
+	/* Space for named tranches. */
 	size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche)));
 
-	/* space for name of each tranche. */
-	for (i = 0; i < NamedLWLockTrancheRequests; i++)
-		size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1);
+	/* Space for name of each tranche. */
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES_NAME_LEN, MAX_NAMED_TRANCHES));
 
 	return size;
 }
 
 /*
  * Allocate shmem space for the main LWLock array and all tranches and
- * initialize it.  We also register extension LWLock tranches here.
+ * initialize it.
  */
 void
 CreateLWLocks(void)
@@ -447,11 +453,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -485,38 +486,27 @@ InitializeLWLocks(void)
 	for (id = 0; id < NUM_PREDICATELOCK_PARTITIONS; id++, lock++)
 		LWLockInitialize(&lock->lock, LWTRANCHE_PREDICATE_LOCK_MANAGER);
 
-	/*
-	 * Copy the info about any named tranches into shared memory (so that
-	 * other processes can see it), and initialize the requested LWLocks.
-	 */
+	NamedLWLockTrancheArray = (const char *)
+		&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
+
+	/* Register LWLocks requested with RequestNamedLWLockTranch */
 	if (NamedLWLockTrancheRequests > 0)
 	{
-		char	   *trancheNames;
-
-		NamedLWLockTrancheArray = (NamedLWLockTranche *)
-			&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
-
-		trancheNames = (char *) NamedLWLockTrancheArray +
-			(NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche));
 		lock = &MainLWLockArray[NUM_FIXED_LWLOCKS];
 
 		for (i = 0; i < NamedLWLockTrancheRequests; i++)
 		{
 			NamedLWLockTrancheRequest *request;
-			NamedLWLockTranche *tranche;
-			char	   *name;
+			int			tranche_id;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = &NamedLWLockTrancheArray[i];
 
-			name = trancheNames;
-			trancheNames += strlen(request->tranche_name) + 1;
-			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
-			tranche->trancheName = name;
+			/* Allocate a new tranche ID */
+			tranche_id = LWLockNewTrancheId(request->tranche_name);
 
+			/* Initialize the requested amount of LWLocks */
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
-				LWLockInitialize(&lock->lock, tranche->trancheId);
+				LWLockInitialize(&lock->lock, tranche_id);
 		}
 	}
 }
@@ -571,58 +561,36 @@ GetNamedLWLockTranche(const char *tranche_name)
  * Allocate a new tranche ID.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
-	int		   *LWLockCounter;
+	int			tranche_id,
+				index,
+			   *LWLockCounter;
+	Size		tranche_name_length = strlen(tranche_name) + 1;
 
-	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
-	/* We use the ShmemLock spinlock to protect LWLockCounter */
-	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
-	SpinLockRelease(ShmemLock);
+	if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+		elog(ERROR, "tranche name too long");
 
-	return result;
-}
+	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-{
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	SpinLockAcquire(ShmemLock);
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	tranche_id = (*LWLockCounter)++;
+	index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (index >= MAX_NAMED_TRANCHES)
 	{
-		int			newalloc;
+		SpinLockRelease(ShmemLock);
+		elog(ERROR, "too many LWLock tranches registered");
+	}
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	/* Directly copy into the NamedLWLockTrancheArray */
+	strcpy((char *) NamedLWLockTrancheArray + (index * MAX_NAMED_TRANCHES_NAME_LEN),
+		   tranche_name);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
-	}
+	SpinLockRelease(ShmemLock);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	return tranche_id;
 }
 
 /*
@@ -641,42 +609,41 @@ void
 RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 {
 	NamedLWLockTrancheRequest *request;
+	Size		tranche_name_length = strlen(tranche_name) + 1;
 
 	if (!process_shmem_requests_in_progress)
 		elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook");
 
+	if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+		elog(FATAL, "tranche name too long");
+
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
-		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
-							   NamedLWLockTrancheRequestsAllocated
+							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
-	{
-		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
-
-		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
-			repalloc(NamedLWLockTrancheRequestArray,
-					 i * sizeof(NamedLWLockTrancheRequest));
-		NamedLWLockTrancheRequestsAllocated = i;
-	}
+	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
+		elog(FATAL, "too many LWLock tranches registered");
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
-	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
-	strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);
+	strlcpy(request->tranche_name, tranche_name, tranche_name_length);
 	request->num_lwlocks = num_lwlocks;
 	NamedLWLockTrancheRequests++;
 }
 
 /*
  * LWLockInitialize - initialize a new lwlock; it's initially unlocked
+ * If the tranche was never registered, GetLWTrancheName will return
+ * and error.
  */
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -713,22 +680,40 @@ LWLockReportWaitEnd(void)
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
-	/* Built-in tranche or individual LWLock? */
+	int			index;
+
+	/* Built-in tranche? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in NamedLWLockTrancheArray[].
+	 * Compute the index to lookup NamedLWLockTrancheArray[].
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	index = trancheId - LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	/*
+	 * If the requested index exceeds the count of known tranches, update
+	 * NamedLWLocksCount to reflect the current state in shared memory. This
+	 * ensures the backend is aware of all registered tranches.
+	 */
+	if (index >= NamedLWLocksCount)
+	{
+		int		   *LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 
-	return LWLockTrancheNames[trancheId];
+		SpinLockAcquire(ShmemLock);
+		NamedLWLocksCount = *LWLockCounter - LWTRANCHE_FIRST_USER_DEFINED;
+		SpinLockRelease(ShmemLock);
+	}
+
+	/*
+	 * Return the tranche name if the index is valid. Otherwise, raise an
+	 * error.
+	 */
+	if (index < NamedLWLocksCount)
+		return NamedLWLockTrancheArray + (index * MAX_NAMED_TRANCHES_NAME_LEN);
+	else
+		elog(ERROR, "LWLock tranche is not registered");
 }
 
 /*
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..1ba77cb3658 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -80,7 +80,7 @@ typedef struct NamedLWLockTranche
 	char	   *trancheName;
 } NamedLWLockTranche;
 
-extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
+extern PGDLLIMPORT const char *NamedLWLockTrancheArray;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
 /*
@@ -157,19 +157,11 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
  * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * LWLockNewTrancheId with an associated tranche name just once to obtain a
+ * tranche ID; this allocates from a shared counter. Finally, LWLockInitialize
+ * should be called just once per lwlock, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
 /*
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

v13-0003-Remove-the-DSA-suffix-for-tranches-created-with-.patchapplication/octet-stream; name=v13-0003-Remove-the-DSA-suffix-for-tranches-created-with-.patchDownload
From e55750f146db201494002545fda1b9bfec2a98db Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 26 Aug 2025 16:58:29 -0500
Subject: [PATCH v13 3/3] Remove the DSA suffix for tranches created with
 GetNamedDSHash

The previous commit fe07100 increased the tranche name length to 128 bytes,
but there is no reason to exceed the tranche name length defined in lwlock.h.
This change restores the limit to NAMEDATALEN for tranches created
with GetNamedDSHash. This also removes the separate DSA tranche name.
DSA and DSH will now share the same tranche name, reducing complexity.

Discussion: https://www.postgresql.org/message-id/aKzIg1JryN1qhNuy@nathan
---
 src/backend/storage/ipc/dsm_registry.c | 16 ++++++----------
 src/backend/storage/lmgr/lwlock.c      |  2 --
 src/include/storage/lwlock.h           |  3 +++
 3 files changed, 9 insertions(+), 12 deletions(-)

diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index cc0adc19971..9b50e212270 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -48,11 +48,7 @@
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 
-#define DSMR_NAME_LEN				128
-
-#define DSMR_DSA_TRANCHE_SUFFIX		" DSA"
-#define DSMR_DSA_TRANCHE_SUFFIX_LEN (sizeof(DSMR_DSA_TRANCHE_SUFFIX) - 1)
-#define DSMR_DSA_TRANCHE_NAME_LEN	(DSMR_NAME_LEN + DSMR_DSA_TRANCHE_SUFFIX_LEN)
+#define DSMR_NAME_LEN	MAX_NAMED_TRANCHES_NAME_LEN
 
 typedef struct DSMRegistryCtxStruct
 {
@@ -72,7 +68,7 @@ typedef struct NamedDSAState
 {
 	dsa_handle	handle;
 	int			tranche;
-	char		tranche_name[DSMR_DSA_TRANCHE_NAME_LEN];
+	char		tranche_name[DSMR_NAME_LEN];
 } NamedDSAState;
 
 typedef struct NamedDSHState
@@ -384,14 +380,14 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
-		/* Initialize the LWLock tranche for the DSA. */
-		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
-
 		/* Initialize the LWLock tranche for the dshash table. */
 		strcpy(dsh_state->tranche_name, name);
 		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
+		/* DSA uses the same tranche as DSH */
+		strcpy(dsa_state->tranche_name, dsh_state->tranche_name);
+		dsa_state->tranche = dsh_state->tranche;
+
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
 		dsa_pin(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 81f9914bf08..041aebf101e 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -106,8 +106,6 @@
 #define LW_LOCK_MASK				(MAX_BACKENDS | LW_VAL_EXCLUSIVE)
 
 #define MAX_NAMED_TRANCHES			1024
-#define MAX_NAMED_TRANCHES_NAME_LEN			NAMEDATALEN
-
 
 StaticAssertDecl(((MAX_BACKENDS + 1) & MAX_BACKENDS) == 0,
 				 "MAX_BACKENDS + 1 needs to be a power of 2");
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 1ba77cb3658..a514960fe29 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -83,6 +83,9 @@ typedef struct NamedLWLockTranche
 extern PGDLLIMPORT const char *NamedLWLockTrancheArray;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
+/* Maximum length of a named tranche */
+#define MAX_NAMED_TRANCHES_NAME_LEN			NAMEDATALEN
+
 /*
  * It's a bit odd to declare NUM_BUFFER_PARTITIONS and NUM_LOCK_PARTITIONS
  * here, but we need them to figure out offsets within MainLWLockArray, and
-- 
2.39.5 (Apple Git-154)

#75Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#74)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Tue, Aug 26, 2025 at 05:50:34PM -0500, Sami Imseih wrote:

fixed the issues mentioned above in v13.

We probably need to do the sprintf/strcpy before LWLockNewTrancheId().
Also, I'm thinking we should just use the same tranche for both the DSA and
the dshash table [0] to evade the DSMR_DSA_TRANCHE_SUFFIX problem, i.e.,
those tranche names potentially require more space.

v13 also includes a separate patch for this. It removes the
DSMR_DSA_TRANCHE_SUFFIX, and dsh and dsa now use the same
user defined tranche. The tranche name is also limited to
the max length of a named tranche, MAX_NAMED_TRANCHES_NAME_LEN,
coming from lwlock.h

Thanks for the new version.

=== 1

+LWLockNewTrancheId(const char *tranche_name)
 {
-       int                     result;
-       int                *LWLockCounter;
+       int                     tranche_id,
+                               index,
+                          *LWLockCounter;
+       Size            tranche_name_length = strlen(tranche_name) + 1;

We need to check if tranche_name is NULL and report an error if that's the case.
If not, strlen() would segfault.

=== 2

+       if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+               elog(ERROR, "tranche name too long");

I think that we should mention in the doc that the tranche name is limited to
63 bytes.

=== 3

-       /* Convert to array index. */
-       tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+       tranche_id = (*LWLockCounter)++;
+       index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
-       /* If necessary, create or enlarge array. */
-       if (tranche_id >= LWLockTrancheNamesAllocated)
+       if (index >= MAX_NAMED_TRANCHES)
        {
-               int                     newalloc;
+               SpinLockRelease(ShmemLock);
+               elog(ERROR, "too many LWLock tranches registered");
+       }
-               newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+       /* Directly copy into the NamedLWLockTrancheArray */
+       strcpy((char *) NamedLWLockTrancheArray + (index * MAX_NAMED_TRANCHES_NAME_LEN),
+                  tranche_name);

I was skeptical about using strcpy() while we hold a spinlock. I do see some
examples with strlcpy() though (walreceiver.c for example), so that looks OK-ish.

Using strcpy() might be OK too, as we already have validated the length, but maybe
it would be safer to switch to strlcpy(), instead?

Regards,

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

#76Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#75)
3 attachment(s)
Re: Improve LWLock tranche name visibility across backends

Thanks for reviewing!

=== 1

We need to check if tranche_name is NULL and report an error if that's the case.
If not, strlen() would segfault.

Added an error. Good call. The error message follows previously used
convention.

```
+       if (!tranche_name)
+               elog(ERROR, "tranche name cannot be null");
```

=== 2

+       if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+               elog(ERROR, "tranche name too long");

I think that we should mention in the doc that the tranche name is limited to
63 bytes.

Done. I just mentioned NAMEDATALEN -1 in the docs.

=== 3

I was skeptical about using strcpy() while we hold a spinlock. I do see some
examples with strlcpy() though (walreceiver.c for example), so that looks OK-ish.

Using strcpy() might be OK too, as we already have validated the length, but maybe
it would be safer to switch to strlcpy(), instead?

OK, since that is the pattern used, I changed to strlcpy. But since we are doing
checks in advance, I think it will be safe either way.

--
Sami

Attachments:

v14-0001-Improve-LWLock-tranche-registration.patchapplication/octet-stream; name=v14-0001-Improve-LWLock-tranche-registration.patchDownload
From 539668c5e460de64d49952927c75f30521e054bd Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-31-110.ec2.internal>
Date: Thu, 21 Aug 2025 21:26:52 +0000
Subject: [PATCH v14 1/3] Improve LWLock tranche registration

Previously, tranche tracking was done in a backend local array. If a backend
did not explicitly register a tranche with LWLockRegisterTranche, querying
pg_stat_activity would show "extension" instead of the tranche name.

This change reserves more space in NamedLWLockTrancheArray, up to 1024 tranches
with each name of length NAMEDATALEN, in shared memory. Tranche creation is
now centralized. A user only needs to call LWLockNewTrancheId with a tranche
name, and the tranche is stored in shared memory. Access to shared memory is
protected by ShmemLock.

With LWLockNewTrancheId taking full responsibility for registering a tranche,
the LWLockRegisterTranche API is no longer required.

Discussion: https://www.postgresql.org/message-id/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   3 +-
 doc/src/sgml/xfunc.sgml                       |  18 +-
 src/backend/postmaster/launch_backend.c       |   4 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/lmgr/lwlock.c             | 200 ++++++++----------
 src/include/storage/lwlock.h                  |  18 +-
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 11 files changed, 119 insertions(+), 167 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..880e897796a 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,7 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
 
 	return found;
 }
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..f7c28a46b8a 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3757,9 +3757,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      (up to <symbol>NAMEDATALEN</symbol>-1 bytes) by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3778,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index bf6b55ee830..51832ed19fe 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,8 +101,8 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	NamedLWLockTranche *NamedLWLockTrancheArray;
 	LWLockPadded *MainLWLockArray;
+	const char *NamedLWLockTrancheArray;
 	slock_t    *ProcStructLock;
 	PROC_HDR   *ProcGlobal;
 	PGPROC	   *AuxiliaryProcs;
@@ -760,8 +760,8 @@ save_backend_variables(BackendParameters *param,
 #endif
 
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
-	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
 	param->MainLWLockArray = MainLWLockArray;
+	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
 	param->ProcStructLock = ProcStructLock;
 	param->ProcGlobal = ProcGlobal;
 	param->AuxiliaryProcs = AuxiliaryProcs;
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index c80b43f1f55..5de2b6a29cc 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -105,6 +105,9 @@
 #define LW_SHARED_MASK				MAX_BACKENDS
 #define LW_LOCK_MASK				(MAX_BACKENDS | LW_VAL_EXCLUSIVE)
 
+#define MAX_NAMED_TRANCHES			1024
+#define MAX_NAMED_TRANCHES_NAME_LEN			NAMEDATALEN
+
 
 StaticAssertDecl(((MAX_BACKENDS + 1) & MAX_BACKENDS) == 0,
 				 "MAX_BACKENDS + 1 needs to be a power of 2");
@@ -125,9 +128,9 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. The names of these tranches are stored in the shared
+ * NamedLWLockTrancheArray.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -146,11 +149,12 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * stores the names of all dynamically created tranches in shared memory.
+ * Any unused entries in the array will contain NULL. This variable is
+ * non-static so that postmaster.c can copy it to child processes in
+ * EXEC_BACKEND builds.
  */
-static const char **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
+const char *NamedLWLockTrancheArray = NULL;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -179,12 +183,11 @@ static LWLockHandle held_lwlocks[MAX_SIMUL_LWLOCKS];
 /* struct representing the LWLock tranche request for named tranche */
 typedef struct NamedLWLockTrancheRequest
 {
-	char		tranche_name[NAMEDATALEN];
+	char		tranche_name[MAX_NAMED_TRANCHES_NAME_LEN];
 	int			num_lwlocks;
 } NamedLWLockTrancheRequest;
 
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
-static int	NamedLWLockTrancheRequestsAllocated = 0;
 
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
@@ -194,8 +197,13 @@ static int	NamedLWLockTrancheRequestsAllocated = 0;
  */
 int			NamedLWLockTrancheRequests = 0;
 
-/* points to data in shared memory: */
-NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
+/*
+ * Backend-local counter for the number of registered tranches.
+ * This is used to ensure the backend is aware of the latest tranche
+ * before accessing shared memory.
+ */
+static int	NamedLWLocksCount = 0;
+
 
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
@@ -391,7 +399,6 @@ Size
 LWLockShmemSize(void)
 {
 	Size		size;
-	int			i;
 	int			numLocks = NUM_FIXED_LWLOCKS;
 
 	/* Calculate total number of locks needed in the main array. */
@@ -403,19 +410,18 @@ LWLockShmemSize(void)
 	/* Space for dynamic allocation counter, plus room for alignment. */
 	size = add_size(size, sizeof(int) + LWLOCK_PADDED_SIZE);
 
-	/* space for named tranches. */
+	/* Space for named tranches. */
 	size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche)));
 
-	/* space for name of each tranche. */
-	for (i = 0; i < NamedLWLockTrancheRequests; i++)
-		size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1);
+	/* Space for name of each tranche. */
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES_NAME_LEN, MAX_NAMED_TRANCHES));
 
 	return size;
 }
 
 /*
  * Allocate shmem space for the main LWLock array and all tranches and
- * initialize it.  We also register extension LWLock tranches here.
+ * initialize it.
  */
 void
 CreateLWLocks(void)
@@ -447,11 +453,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -485,38 +486,27 @@ InitializeLWLocks(void)
 	for (id = 0; id < NUM_PREDICATELOCK_PARTITIONS; id++, lock++)
 		LWLockInitialize(&lock->lock, LWTRANCHE_PREDICATE_LOCK_MANAGER);
 
-	/*
-	 * Copy the info about any named tranches into shared memory (so that
-	 * other processes can see it), and initialize the requested LWLocks.
-	 */
+	NamedLWLockTrancheArray = (const char *)
+		&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
+
+	/* Register LWLocks requested with RequestNamedLWLockTranch */
 	if (NamedLWLockTrancheRequests > 0)
 	{
-		char	   *trancheNames;
-
-		NamedLWLockTrancheArray = (NamedLWLockTranche *)
-			&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
-
-		trancheNames = (char *) NamedLWLockTrancheArray +
-			(NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche));
 		lock = &MainLWLockArray[NUM_FIXED_LWLOCKS];
 
 		for (i = 0; i < NamedLWLockTrancheRequests; i++)
 		{
 			NamedLWLockTrancheRequest *request;
-			NamedLWLockTranche *tranche;
-			char	   *name;
+			int			tranche_id;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = &NamedLWLockTrancheArray[i];
 
-			name = trancheNames;
-			trancheNames += strlen(request->tranche_name) + 1;
-			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
-			tranche->trancheName = name;
+			/* Allocate a new tranche ID */
+			tranche_id = LWLockNewTrancheId(request->tranche_name);
 
+			/* Initialize the requested amount of LWLocks */
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
-				LWLockInitialize(&lock->lock, tranche->trancheId);
+				LWLockInitialize(&lock->lock, tranche_id);
 		}
 	}
 }
@@ -571,58 +561,39 @@ GetNamedLWLockTranche(const char *tranche_name)
  * Allocate a new tranche ID.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
-	int		   *LWLockCounter;
+	int			tranche_id,
+				index,
+			   *LWLockCounter;
+	Size		tranche_name_length = strlen(tranche_name) + 1;
 
-	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
-	/* We use the ShmemLock spinlock to protect LWLockCounter */
-	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
-	SpinLockRelease(ShmemLock);
+	if (!tranche_name)
+		elog(ERROR, "tranche name cannot be null");
 
-	return result;
-}
+	if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+		elog(ERROR, "tranche name too long");
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-{
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
+
+	SpinLockAcquire(ShmemLock);
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	tranche_id = (*LWLockCounter)++;
+	index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (index >= MAX_NAMED_TRANCHES)
 	{
-		int			newalloc;
+		SpinLockRelease(ShmemLock);
+		elog(ERROR, "too many LWLock tranches registered");
+	}
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	/* Directly copy into the NamedLWLockTrancheArray */
+	strlcpy((char *) NamedLWLockTrancheArray + (index * MAX_NAMED_TRANCHES_NAME_LEN),
+			tranche_name, MAX_NAMED_TRANCHES_NAME_LEN);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
-	}
+	SpinLockRelease(ShmemLock);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	return tranche_id;
 }
 
 /*
@@ -641,42 +612,41 @@ void
 RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 {
 	NamedLWLockTrancheRequest *request;
+	Size		tranche_name_length = strlen(tranche_name) + 1;
 
 	if (!process_shmem_requests_in_progress)
 		elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook");
 
+	if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+		elog(FATAL, "tranche name too long");
+
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
-		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
-							   NamedLWLockTrancheRequestsAllocated
+							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
-	{
-		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
-
-		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
-			repalloc(NamedLWLockTrancheRequestArray,
-					 i * sizeof(NamedLWLockTrancheRequest));
-		NamedLWLockTrancheRequestsAllocated = i;
-	}
+	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
+		elog(FATAL, "too many LWLock tranches registered");
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
-	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
-	strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);
+	strlcpy(request->tranche_name, tranche_name, tranche_name_length);
 	request->num_lwlocks = num_lwlocks;
 	NamedLWLockTrancheRequests++;
 }
 
 /*
  * LWLockInitialize - initialize a new lwlock; it's initially unlocked
+ * If the tranche was never registered, GetLWTrancheName will return
+ * and error.
  */
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -713,22 +683,40 @@ LWLockReportWaitEnd(void)
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
-	/* Built-in tranche or individual LWLock? */
+	int			index;
+
+	/* Built-in tranche? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in NamedLWLockTrancheArray[].
+	 * Compute the index to lookup NamedLWLockTrancheArray[].
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	index = trancheId - LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	/*
+	 * If the requested index exceeds the count of known tranches, update
+	 * NamedLWLocksCount to reflect the current state in shared memory. This
+	 * ensures the backend is aware of all registered tranches.
+	 */
+	if (index >= NamedLWLocksCount)
+	{
+		int		   *LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 
-	return LWLockTrancheNames[trancheId];
+		SpinLockAcquire(ShmemLock);
+		NamedLWLocksCount = *LWLockCounter - LWTRANCHE_FIRST_USER_DEFINED;
+		SpinLockRelease(ShmemLock);
+	}
+
+	/*
+	 * Return the tranche name if the index is valid. Otherwise, raise an
+	 * error.
+	 */
+	if (index < NamedLWLocksCount)
+		return NamedLWLockTrancheArray + (index * MAX_NAMED_TRANCHES_NAME_LEN);
+	else
+		elog(ERROR, "LWLock tranche is not registered");
 }
 
 /*
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..1ba77cb3658 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -80,7 +80,7 @@ typedef struct NamedLWLockTranche
 	char	   *trancheName;
 } NamedLWLockTranche;
 
-extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
+extern PGDLLIMPORT const char *NamedLWLockTrancheArray;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
 /*
@@ -157,19 +157,11 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
  * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * LWLockNewTrancheId with an associated tranche name just once to obtain a
+ * tranche ID; this allocates from a shared counter. Finally, LWLockInitialize
+ * should be called just once per lwlock, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
 /*
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.43.0

v14-0003-Remove-the-DSA-suffix-for-tranches-created-with-.patchapplication/octet-stream; name=v14-0003-Remove-the-DSA-suffix-for-tranches-created-with-.patchDownload
From a31e199934f258f018da3420fee83c1bbc21c2e2 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Tue, 26 Aug 2025 16:58:29 -0500
Subject: [PATCH v14 3/3] Remove the DSA suffix for tranches created with
 GetNamedDSHash

The previous commit fe07100 increased the tranche name length to 128 bytes,
but there is no reason to exceed the tranche name length defined in lwlock.h.
This change restores the limit to NAMEDATALEN for tranches created
with GetNamedDSHash. This also removes the separate DSA tranche name.
DSA and DSH will now share the same tranche name, reducing complexity.

Discussion: https://www.postgresql.org/message-id/aKzIg1JryN1qhNuy@nathan
---
 src/backend/storage/ipc/dsm_registry.c | 16 ++++++----------
 src/backend/storage/lmgr/lwlock.c      |  2 --
 src/include/storage/lwlock.h           |  3 +++
 3 files changed, 9 insertions(+), 12 deletions(-)

diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index cc0adc19971..9b50e212270 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -48,11 +48,7 @@
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 
-#define DSMR_NAME_LEN				128
-
-#define DSMR_DSA_TRANCHE_SUFFIX		" DSA"
-#define DSMR_DSA_TRANCHE_SUFFIX_LEN (sizeof(DSMR_DSA_TRANCHE_SUFFIX) - 1)
-#define DSMR_DSA_TRANCHE_NAME_LEN	(DSMR_NAME_LEN + DSMR_DSA_TRANCHE_SUFFIX_LEN)
+#define DSMR_NAME_LEN	MAX_NAMED_TRANCHES_NAME_LEN
 
 typedef struct DSMRegistryCtxStruct
 {
@@ -72,7 +68,7 @@ typedef struct NamedDSAState
 {
 	dsa_handle	handle;
 	int			tranche;
-	char		tranche_name[DSMR_DSA_TRANCHE_NAME_LEN];
+	char		tranche_name[DSMR_NAME_LEN];
 } NamedDSAState;
 
 typedef struct NamedDSHState
@@ -384,14 +380,14 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
-		/* Initialize the LWLock tranche for the DSA. */
-		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
-
 		/* Initialize the LWLock tranche for the dshash table. */
 		strcpy(dsh_state->tranche_name, name);
 		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
+		/* DSA uses the same tranche as DSH */
+		strcpy(dsa_state->tranche_name, dsh_state->tranche_name);
+		dsa_state->tranche = dsh_state->tranche;
+
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
 		dsa_pin(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 5de2b6a29cc..99caed6fcb3 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -106,8 +106,6 @@
 #define LW_LOCK_MASK				(MAX_BACKENDS | LW_VAL_EXCLUSIVE)
 
 #define MAX_NAMED_TRANCHES			1024
-#define MAX_NAMED_TRANCHES_NAME_LEN			NAMEDATALEN
-
 
 StaticAssertDecl(((MAX_BACKENDS + 1) & MAX_BACKENDS) == 0,
 				 "MAX_BACKENDS + 1 needs to be a power of 2");
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 1ba77cb3658..a514960fe29 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -83,6 +83,9 @@ typedef struct NamedLWLockTranche
 extern PGDLLIMPORT const char *NamedLWLockTrancheArray;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
+/* Maximum length of a named tranche */
+#define MAX_NAMED_TRANCHES_NAME_LEN			NAMEDATALEN
+
 /*
  * It's a bit odd to declare NUM_BUFFER_PARTITIONS and NUM_LOCK_PARTITIONS
  * here, but we need them to figure out offsets within MainLWLockArray, and
-- 
2.43.0

v14-0002-Tests-for-LWLock-tranche-registration-improvemen.patchapplication/octet-stream; name=v14-0002-Tests-for-LWLock-tranche-registration-improvemen.patchDownload
From e43a9a0ded70fb2a85d241fe0f947d1829fcaeb9 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 22 Aug 2025 00:35:45 -0500
Subject: [PATCH v14 2/3] Tests for LWLock tranche registration improvements

---
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_tranches/Makefile       |  23 +++
 src/test/modules/test_tranches/meson.build    |  33 ++++
 .../test_tranches/t/001_test_tranches.pl      | 112 +++++++++++
 .../test_tranches/test_tranches--1.0.sql      |  31 +++
 .../modules/test_tranches/test_tranches.c     | 186 ++++++++++++++++++
 .../test_tranches/test_tranches.control       |   6 +
 8 files changed, 394 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/test_tranches/Makefile
 create mode 100644 src/test/modules/test_tranches/meson.build
 create mode 100644 src/test/modules/test_tranches/t/001_test_tranches.pl
 create mode 100644 src/test/modules/test_tranches/test_tranches--1.0.sql
 create mode 100644 src/test/modules/test_tranches/test_tranches.c
 create mode 100644 src/test/modules/test_tranches/test_tranches.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..fd7aa4675c5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..52883dfb496 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_tranches')
diff --git a/src/test/modules/test_tranches/Makefile b/src/test/modules/test_tranches/Makefile
new file mode 100644
index 00000000000..5c9e78084da
--- /dev/null
+++ b/src/test/modules/test_tranches/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_tranches/Makefile
+
+MODULE_big = test_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_tranches.o
+PGFILEDESC = "test_tranches - test code LWLock tranche management"
+
+EXTENSION = test_tranches
+DATA = test_tranches--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_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_tranches/meson.build b/src/test/modules/test_tranches/meson.build
new file mode 100644
index 00000000000..976ce9f0a25
--- /dev/null
+++ b/src/test/modules/test_tranches/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_tranches_sources = files(
+  'test_tranches.c',
+)
+
+if host_system == 'windows'
+  test_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_tranches',
+    '--FILEDESC', 'test_tranches - test code LWLock tranche management',])
+endif
+
+test_tranches = shared_module('test_tranches',
+  test_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_tranches
+
+test_install_data += files(
+  'test_tranches.control',
+  'test_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_test_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/t/001_test_tranches.pl b/src/test/modules/test_tranches/t/001_test_tranches.pl
new file mode 100644
index 00000000000..08868a87428
--- /dev/null
+++ b/src/test/modules/test_tranches/t/001_test_tranches.pl
@@ -0,0 +1,112 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+#
+# Create the cluster without any requested tranches
+#
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=0));
+$node->start();
+$node->safe_psql('postgres', q(CREATE EXTENSION test_tranches));
+
+my $first_user_defined = $node->safe_psql('postgres', q(select test_tranches_get_first_user_defined()));
+my @user_defined = ($first_user_defined .. $first_user_defined + 5);
+my ($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+#
+# Run lookup tests to ensure the correct tranche names are returned
+# and an error is raised for unregistered tranches
+#
+my ($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_new(5);
+        select test_tranches_lookup(1);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  LWLock tranche is not registered/, "unregistered tranche error");
+like("$stdout",
+     qr/ShmemIndex.*test_lock__0.*test_lock__1.*test_lock__2.*test_lock__3.*test_lock__4/s,
+     "match tranche names without requested tranches");
+
+#
+# Test the error for long tranche names
+#
+my $good_tranche_name = 'A' x 63;
+my $bad_tranche_name = 'B' x 64; # MAX_NAMED_TRANCHES_NAME_LEN
+$node->safe_psql('postgres', qq{select test_tranches_last('$good_tranche_name');});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_last('$bad_tranche_name');
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche name too long/, "tranche name too long");
+
+$node->restart();
+
+#
+# Test the error for too many registered tranches
+#
+$node->safe_psql('postgres', qq{select test_tranches_new(1023)});
+$node->safe_psql('postgres', qq{select test_tranches_new(1)});
+($result, $stdout, $stderr) = $node->psql('postgres', qq{select test_tranches_new(1);}, on_error_stop => 0);
+like("$stderr", qr/ERROR:  too many LWLock tranches registered/, "too many tranches registered");
+
+#
+# Repeat the lookup test with 2 requested tranches
+#
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=2));
+$node->restart();
+
+$first_user_defined = $first_user_defined;
+@user_defined = ($first_user_defined .. $first_user_defined + 5);
+($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_new(3);
+        select test_tranches_lookup(1);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  LWLock tranche is not registered/, "unregistered tranche error");
+like("$stdout", qr/ShmemIndex.*test_lock_0.*test_lock_1.*test_lock__2.*test_lock__3.*test_lock__4/s,
+     "match tranche names with requested tranches");
+
+#
+# Test lwlock initialize
+#
+$node->safe_psql('postgres', qq{select test_tranches_lwlock_initialize($first_user_defined)});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{select test_tranches_lwlock_initialize($sixth_user_defined)},
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  LWLock tranche is not registered/, "LWLock intialization error on invalid tranche name");
+
+done_testing();
diff --git a/src/test/modules/test_tranches/test_tranches--1.0.sql b/src/test/modules/test_tranches/test_tranches--1.0.sql
new file mode 100644
index 00000000000..808e68af4b8
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches--1.0.sql
@@ -0,0 +1,31 @@
+-- test_tranches--1.0.sql
+
+CREATE FUNCTION test_tranches_new(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_new'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_last(text)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_last'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lookup(int)
+RETURNS text
+AS 'MODULE_PATHNAME', 'test_tranches_lookup'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_named_lwlock(text, int)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_named_lwlock'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lwlock_initialize(int)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_lwlock_initialize'
+LANGUAGE C STRICT;
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/test_tranches.c b/src/test/modules/test_tranches/test_tranches.c
new file mode 100644
index 00000000000..f64927a3114
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.c
@@ -0,0 +1,186 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_tranches_shmem_request(void);
+static void test_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_tranches_requested_named_tranches = 0;
+
+typedef struct testTranchesSharedState
+{
+	int			next_index;
+}			testTranchesSharedState;
+
+static testTranchesSharedState * test_lwlock_ss = NULL;
+
+/*
+ * LWLock wait event masks. Copied from src/backend/utils/activity/wait_event.c
+ */
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_tranches_shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	test_lwlock_ss = NULL;
+
+	test_lwlock_ss = ShmemInitStruct("test_tranches",
+									 sizeof(testTranchesSharedState),
+									 &found);
+	if (!found)
+	{
+		test_lwlock_ss->next_index = test_tranches_requested_named_tranches;
+	}
+}
+
+static Size
+test_tranches_memsize(void)
+{
+	Size		size;
+
+	size = MAXALIGN(sizeof(testTranchesSharedState));
+
+	return size;
+}
+
+static void
+test_tranches_shmem_request(void)
+{
+	int			i = 0;
+
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(test_tranches_memsize());
+
+	for (i = 0; i < test_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new);
+Datum
+test_tranches_new(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+	int			i;
+
+	for (i = test_lwlock_ss->next_index; i < num + test_lwlock_ss->next_index; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	test_lwlock_ss->next_index = i;
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_last);
+Datum
+test_tranches_last(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT32(LWLockNewTrancheId(text_to_cstring(PG_GETARG_TEXT_PP(0))));
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lookup);
+Datum
+test_tranches_lookup(PG_FUNCTION_ARGS)
+{
+	const char *tranche_name = GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	if (tranche_name)
+		PG_RETURN_TEXT_P(cstring_to_text(tranche_name));
+	else
+		PG_RETURN_NULL();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_named_lwlock);
+Datum
+test_tranches_get_named_lwlock(PG_FUNCTION_ARGS)
+{
+	int			i;
+	LWLockPadded *locks;
+
+	locks = GetNamedLWLockTranche(text_to_cstring(PG_GETARG_TEXT_PP(0)));
+
+	for (i = 0; i < PG_GETARG_INT32(1); i++)
+	{
+		LWLock	   *lock = &locks[i].lock;
+
+		LWLockAcquire(lock, LW_SHARED);
+		LWLockRelease(lock);
+	}
+
+	PG_RETURN_INT32(i);
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_first_user_defined);
+Datum
+test_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lwlock_initialize);
+Datum
+test_tranches_lwlock_initialize(PG_FUNCTION_ARGS)
+{
+	LWLock		lock;
+
+	LWLockInitialize(&lock, PG_GETARG_INT32(0));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_tranches/test_tranches.control b/src/test/modules/test_tranches/test_tranches.control
new file mode 100644
index 00000000000..8e6254a7e43
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.control
@@ -0,0 +1,6 @@
+# test_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_tranches'
\ No newline at end of file
-- 
2.43.0

#77Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#76)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Wed, Aug 27, 2025 at 02:13:39PM -0500, Sami Imseih wrote:

Thanks for reviewing!

=== 1

We need to check if tranche_name is NULL and report an error if that's the case.
If not, strlen() would segfault.

Added an error. Good call. The error message follows previously used
convention.

```
+       if (!tranche_name)
+               elog(ERROR, "tranche name cannot be null");
```
+LWLockNewTrancheId(const char *tranche_name)
 {
-       int                     result;
-       int                *LWLockCounter;
+       int                     tranche_id,
+                               index,
+                          *LWLockCounter;
+       Size            tranche_name_length = strlen(tranche_name) + 1;
-       LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
-       /* We use the ShmemLock spinlock to protect LWLockCounter */
-       SpinLockAcquire(ShmemLock);
-       result = (*LWLockCounter)++;
-       SpinLockRelease(ShmemLock);
+       if (!tranche_name)
+               elog(ERROR, "tranche name cannot be null");

The check has to be done before the strlen() call, if not it segfault:

Breakpoint 1, LWLockNewTrancheId (tranche_name=0x0) at lwlock.c:569
569 Size tranche_name_length = strlen(tranche_name) + 1;
(gdb) n

Program received signal SIGSEGV, Segmentation fault.

=== 2

+       if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+               elog(ERROR, "tranche name too long");

I think that we should mention in the doc that the tranche name is limited to
63 bytes.

Done. I just mentioned NAMEDATALEN -1 in the docs.

Most of the places where NAMEDATALEN is mentioned in sgml files also mention
it's 64 bytes. Should we do the same here?

=== 3

I was skeptical about using strcpy() while we hold a spinlock. I do see some
examples with strlcpy() though (walreceiver.c for example), so that looks OK-ish.

Using strcpy() might be OK too, as we already have validated the length, but maybe
it would be safer to switch to strlcpy(), instead?

OK, since that is the pattern used, I changed to strlcpy. But since we are doing
checks in advance, I think it will be safe either way.

Agree.

- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * stores the names of all dynamically created tranches in shared memory.
+ * Any unused entries in the array will contain NULL. This variable is
+ * non-static so that postmaster.c can copy it to child processes in
+ * EXEC_BACKEND builds.

"Any unused entries in the array will contain NULL": this is not true anymore.
It now contains empty strings:

(gdb) p *(char(*)[64])NamedLWLockTrancheArray@4
$5 = {"tutu", '\000' <repeats 59 times>, '\000' <repeats 63 times>, '\000' <repeats 63 times>, '\000' <repeats 63 times>}

Regards,

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

#78Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#77)
3 attachment(s)
Re: Improve LWLock tranche name visibility across backends

The check has to be done before the strlen() call, if not it segfault:

I don't know what I was thinking there :( silly mistake.
It was also missed in RequestNamedLWLockTranche.

Most of the places where NAMEDATALEN is mentioned in sgml files also mention
it's 64 bytes. Should we do the same here?

I don't have an issue with that, although I did not like how the
various docs are not
consistent. Some claim 64 characters, others claim 63 bytes. They are
not the same
since multibyte characters are allowed. For example [0]https://www.postgresql.org/docs/18/runtime-config-logging.html#GUC-APPLICATION-NAME is definitely wrong,
and it should say "bytes".
I think it would be nice if all references to NAMEDATALEN point to a single doc
(a follow-up topic, perhaps)

"Any unused entries in the array will contain NULL": this is not true anymore.
It now contains empty strings:

Yes. Fixed the comment.

Also, it has not yet been discussed that the max number of named
tranches should be.
Thus far I have been using a likely extremely high value of 1024. In
v15, I set it to 128
as that was a possibly more realistic number and one suggested earlier [1]/messages/by-id/5to6tftuml6nkas4jaaljfzecasvslxq3mumeslh74wsol4mzw@rgxpxxlqqwtf.

There maybe some odd cases out there in which this value may not be enough;
many extensions that init lwlocks or maybe some extension out there that uses
partitioned locks and assign a unique tranche for every lock. I have
not seen anything
like this. Maybe it's not be something to worry about and we can say
128 is reasonable sufficiently
large. I would hate to reserve too much shared memory for this unnecessarily.

Thoughts?

[0]: https://www.postgresql.org/docs/18/runtime-config-logging.html#GUC-APPLICATION-NAME
[1]: /messages/by-id/5to6tftuml6nkas4jaaljfzecasvslxq3mumeslh74wsol4mzw@rgxpxxlqqwtf

--
Sami

Attachments:

v15-0001-Improve-LWLock-tranche-registration.patchapplication/octet-stream; name=v15-0001-Improve-LWLock-tranche-registration.patchDownload
From 7b615227735364b9a0021009cefa8b191f9261a8 Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-31-110.ec2.internal>
Date: Thu, 21 Aug 2025 21:26:52 +0000
Subject: [PATCH v15 1/3] Improve LWLock tranche registration
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Previously, tranche tracking was done in a backend local array. If a backend
did not explicitly register a tranche with LWLockRegisterTranche, querying
pg_stat_activity would show "extension" instead of the tranche name.

This change reserves more space in NamedLWLockTrancheArray, up to 128 tranches
with each name occupying a length NAMEDATALEN, in shared memory. Tranche creation
is now centralized. A user only needs to call LWLockNewTrancheId with a tranche
name, and the tranche is stored in shared memory. Access to shared memory is
protected by ShmemLock.

With LWLockNewTrancheId taking full responsibility for registering a tranche,
the LWLockRegisterTranche API is no longer required.

This also changes the behavior of LWLockInitialize. Previously, it was allowed
to provide an unregistered tranche_id. Now, doing so will fail, because looking
up a LWLock with an unregistered tranche will fail later, rather than returning
an “extension” tranche name as it did before.

Discussion: https://www.postgresql.org/message-id/CAA5RZ0vvED3naph8My8Szv6DL4AxOVK3eTPS0qXsaKi%3DbVdW2A%40mail.gmail.com
---
 contrib/pg_prewarm/autoprewarm.c              |   3 +-
 doc/src/sgml/xfunc.sgml                       |  18 +-
 src/backend/postmaster/launch_backend.c       |   4 +-
 src/backend/storage/ipc/dsm_registry.c        |  16 +-
 src/backend/storage/lmgr/lwlock.c             | 209 +++++++++---------
 src/include/storage/lwlock.h                  |  18 +-
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 11 files changed, 128 insertions(+), 167 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..880e897796a 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,7 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
 
 	return found;
 }
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..16052fe690b 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3757,9 +3757,10 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       There is another, more flexible method of obtaining LWLocks that can be
       done after server startup and outside a
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
-      <literal>tranche_id</literal> by calling:
+      <literal>tranche_id</literal> with an associated <literal>tranche_name</literal>
+      (up to <symbol>NAMEDATALEN</symbol>-1, which is 63 bytes by default) by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *tranche_name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3778,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index bf6b55ee830..51832ed19fe 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,8 +101,8 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	NamedLWLockTranche *NamedLWLockTrancheArray;
 	LWLockPadded *MainLWLockArray;
+	const char *NamedLWLockTrancheArray;
 	slock_t    *ProcStructLock;
 	PROC_HDR   *ProcGlobal;
 	PGPROC	   *AuxiliaryProcs;
@@ -760,8 +760,8 @@ save_backend_variables(BackendParameters *param,
 #endif
 
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
-	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
 	param->MainLWLockArray = MainLWLockArray;
+	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
 	param->ProcStructLock = ProcStructLock;
 	param->ProcGlobal = ProcGlobal;
 	param->AuxiliaryProcs = AuxiliaryProcs;
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..cc0adc19971 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -307,9 +307,8 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
 		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		state->tranche = LWLockNewTrancheId(state->tranche_name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -330,9 +329,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -389,14 +385,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
 		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
+		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
 
 		/* Initialize the LWLock tranche for the dshash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
 		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
@@ -427,10 +421,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsa_state->handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index c80b43f1f55..6633c5ec37d 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -105,6 +105,11 @@
 #define LW_SHARED_MASK				MAX_BACKENDS
 #define LW_LOCK_MASK				(MAX_BACKENDS | LW_VAL_EXCLUSIVE)
 
+/* 128 named tranches limit: arbitrary but sufficient */
+#define MAX_NAMED_TRANCHES			128
+/* Maximum length of a named tranche */
+#define MAX_NAMED_TRANCHES_NAME_LEN			NAMEDATALEN
+
 
 StaticAssertDecl(((MAX_BACKENDS + 1) & MAX_BACKENDS) == 0,
 				 "MAX_BACKENDS + 1 needs to be a power of 2");
@@ -125,9 +130,9 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * 2. There are some predefined tranches for built-in groups of locks defined
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
- * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * 3. Extensions can create new tranches using either RequestNamedLWLockTranche
+ * or LWLockNewTrancheId. The names of these tranches are stored in the shared
+ * NamedLWLockTrancheArray.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -146,11 +151,12 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * stores the names of all dynamically created tranches in shared memory.
+ * Any unused entries in the array will contain an empty string. This variable
+ * is non-static so that postmaster.c can copy it to child processes in
+ * EXEC_BACKEND builds.
  */
-static const char **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
+const char *NamedLWLockTrancheArray = NULL;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -179,12 +185,11 @@ static LWLockHandle held_lwlocks[MAX_SIMUL_LWLOCKS];
 /* struct representing the LWLock tranche request for named tranche */
 typedef struct NamedLWLockTrancheRequest
 {
-	char		tranche_name[NAMEDATALEN];
+	char		tranche_name[MAX_NAMED_TRANCHES_NAME_LEN];
 	int			num_lwlocks;
 } NamedLWLockTrancheRequest;
 
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
-static int	NamedLWLockTrancheRequestsAllocated = 0;
 
 /*
  * NamedLWLockTrancheRequests is both the valid length of the request array,
@@ -194,8 +199,13 @@ static int	NamedLWLockTrancheRequestsAllocated = 0;
  */
 int			NamedLWLockTrancheRequests = 0;
 
-/* points to data in shared memory: */
-NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
+/*
+ * Backend-local counter for the number of registered tranches.
+ * This is used to ensure the backend is aware of the latest tranche
+ * before accessing shared memory.
+ */
+static int	NamedLWLocksCount = 0;
+
 
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
@@ -391,7 +401,6 @@ Size
 LWLockShmemSize(void)
 {
 	Size		size;
-	int			i;
 	int			numLocks = NUM_FIXED_LWLOCKS;
 
 	/* Calculate total number of locks needed in the main array. */
@@ -403,19 +412,18 @@ LWLockShmemSize(void)
 	/* Space for dynamic allocation counter, plus room for alignment. */
 	size = add_size(size, sizeof(int) + LWLOCK_PADDED_SIZE);
 
-	/* space for named tranches. */
+	/* Space for named tranches. */
 	size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche)));
 
-	/* space for name of each tranche. */
-	for (i = 0; i < NamedLWLockTrancheRequests; i++)
-		size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1);
+	/* Space for name of each tranche. */
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES_NAME_LEN, MAX_NAMED_TRANCHES));
 
 	return size;
 }
 
 /*
  * Allocate shmem space for the main LWLock array and all tranches and
- * initialize it.  We also register extension LWLock tranches here.
+ * initialize it.
  */
 void
 CreateLWLocks(void)
@@ -447,11 +455,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -485,38 +488,27 @@ InitializeLWLocks(void)
 	for (id = 0; id < NUM_PREDICATELOCK_PARTITIONS; id++, lock++)
 		LWLockInitialize(&lock->lock, LWTRANCHE_PREDICATE_LOCK_MANAGER);
 
-	/*
-	 * Copy the info about any named tranches into shared memory (so that
-	 * other processes can see it), and initialize the requested LWLocks.
-	 */
+	NamedLWLockTrancheArray = (const char *)
+		&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
+
+	/* Register LWLocks requested with RequestNamedLWLockTranch */
 	if (NamedLWLockTrancheRequests > 0)
 	{
-		char	   *trancheNames;
-
-		NamedLWLockTrancheArray = (NamedLWLockTranche *)
-			&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
-
-		trancheNames = (char *) NamedLWLockTrancheArray +
-			(NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche));
 		lock = &MainLWLockArray[NUM_FIXED_LWLOCKS];
 
 		for (i = 0; i < NamedLWLockTrancheRequests; i++)
 		{
 			NamedLWLockTrancheRequest *request;
-			NamedLWLockTranche *tranche;
-			char	   *name;
+			int			tranche_id;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = &NamedLWLockTrancheArray[i];
 
-			name = trancheNames;
-			trancheNames += strlen(request->tranche_name) + 1;
-			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
-			tranche->trancheName = name;
+			/* Allocate a new tranche ID */
+			tranche_id = LWLockNewTrancheId(request->tranche_name);
 
+			/* Initialize the requested amount of LWLocks */
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
-				LWLockInitialize(&lock->lock, tranche->trancheId);
+				LWLockInitialize(&lock->lock, tranche_id);
 		}
 	}
 }
@@ -571,58 +563,41 @@ GetNamedLWLockTranche(const char *tranche_name)
  * Allocate a new tranche ID.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
-	int			result;
-	int		   *LWLockCounter;
+	int			tranche_id,
+				index,
+			   *LWLockCounter;
+	Size		tranche_name_length;
 
-	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
-	/* We use the ShmemLock spinlock to protect LWLockCounter */
-	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
-	SpinLockRelease(ShmemLock);
+	if (!tranche_name)
+		elog(ERROR, "tranche name cannot be null");
 
-	return result;
-}
+	tranche_name_length = strlen(tranche_name) + 1;
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-{
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+		elog(ERROR, "tranche name too long");
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
-	{
-		int			newalloc;
+	SpinLockAcquire(ShmemLock);
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	tranche_id = (*LWLockCounter)++;
+	index = tranche_id - LWTRANCHE_FIRST_USER_DEFINED;
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
+	if (index >= MAX_NAMED_TRANCHES)
+	{
+		SpinLockRelease(ShmemLock);
+		elog(ERROR, "too many LWLock tranches registered");
 	}
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	/* Directly copy into the NamedLWLockTrancheArray */
+	strlcpy((char *) NamedLWLockTrancheArray + (index * MAX_NAMED_TRANCHES_NAME_LEN),
+			tranche_name, MAX_NAMED_TRANCHES_NAME_LEN);
+
+	SpinLockRelease(ShmemLock);
+
+	return tranche_id;
 }
 
 /*
@@ -641,42 +616,46 @@ void
 RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 {
 	NamedLWLockTrancheRequest *request;
+	Size		tranche_name_length;
 
 	if (!process_shmem_requests_in_progress)
 		elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook");
 
+	if (!tranche_name)
+		elog(FATAL, "tranche name cannot be null");
+
+	tranche_name_length = strlen(tranche_name) + 1;
+
+	if (tranche_name_length > MAX_NAMED_TRANCHES_NAME_LEN)
+		elog(FATAL, "tranche name too long");
+
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
-		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
-							   NamedLWLockTrancheRequestsAllocated
+							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
-	{
-		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
-
-		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
-			repalloc(NamedLWLockTrancheRequestArray,
-					 i * sizeof(NamedLWLockTrancheRequest));
-		NamedLWLockTrancheRequestsAllocated = i;
-	}
+	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
+		elog(FATAL, "too many LWLock tranches registered");
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
-	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
-	strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);
+	strlcpy(request->tranche_name, tranche_name, tranche_name_length);
 	request->num_lwlocks = num_lwlocks;
 	NamedLWLockTrancheRequests++;
 }
 
 /*
  * LWLockInitialize - initialize a new lwlock; it's initially unlocked
+ * If the tranche was never registered, GetLWTrancheName will return
+ * and error.
  */
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -713,22 +692,40 @@ LWLockReportWaitEnd(void)
 static const char *
 GetLWTrancheName(uint16 trancheId)
 {
-	/* Built-in tranche or individual LWLock? */
+	int			index;
+
+	/* Built-in tranche? */
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in NamedLWLockTrancheArray[].
+	 * Compute the index to lookup NamedLWLockTrancheArray[].
+	 */
+	index = trancheId - LWTRANCHE_FIRST_USER_DEFINED;
+
+	/*
+	 * If the requested index exceeds the count of known tranches, update
+	 * NamedLWLocksCount to reflect the current state in shared memory. This
+	 * ensures the backend is aware of all registered tranches.
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	if (index >= NamedLWLocksCount)
+	{
+		int		   *LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+		SpinLockAcquire(ShmemLock);
+		NamedLWLocksCount = *LWLockCounter - LWTRANCHE_FIRST_USER_DEFINED;
+		SpinLockRelease(ShmemLock);
+	}
 
-	return LWLockTrancheNames[trancheId];
+	/*
+	 * Return the tranche name if the index is valid. Otherwise, raise an
+	 * error.
+	 */
+	if (index < NamedLWLocksCount)
+		return NamedLWLockTrancheArray + (index * MAX_NAMED_TRANCHES_NAME_LEN);
+	else
+		elog(ERROR, "LWLock tranche is not registered");
 }
 
 /*
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..1ba77cb3658 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -80,7 +80,7 @@ typedef struct NamedLWLockTranche
 	char	   *trancheName;
 } NamedLWLockTranche;
 
-extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
+extern PGDLLIMPORT const char *NamedLWLockTrancheArray;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
 /*
@@ -157,19 +157,11 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
  * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * LWLockNewTrancheId with an associated tranche name just once to obtain a
+ * tranche ID; this allocates from a shared counter. Finally, LWLockInitialize
+ * should be called just once per lwlock, passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
 /*
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.43.0

v15-0003-Remove-the-DSA-suffix-for-tranches-created-with-.patchapplication/octet-stream; name=v15-0003-Remove-the-DSA-suffix-for-tranches-created-with-.patchDownload
From 87de1f9bf75c470382abffb88e930f367b9b4c83 Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-83-116.ec2.internal>
Date: Thu, 28 Aug 2025 14:43:35 +0000
Subject: [PATCH v15 3/3] Remove the DSA suffix for tranches created with
 GetNamedDSHash

The previous commit fe07100 increased the tranche name length to 128 bytes,
but there is no reason to exceed the tranche name length defined in lwlock.h.
This change restores the limit to NAMEDATALEN for tranches created
with GetNamedDSHash. This also removes the separate DSA tranche name.
DSA and DSH will now share the same tranche name, reducing complexity.

Discussion: https://www.postgresql.org/message-id/aKzIg1JryN1qhNuy@nathan
---
 src/backend/storage/ipc/dsm_registry.c | 16 ++++++----------
 src/backend/storage/lmgr/lwlock.c      |  3 ---
 src/include/storage/lwlock.h           |  3 +++
 3 files changed, 9 insertions(+), 13 deletions(-)

diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index cc0adc19971..9b50e212270 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -48,11 +48,7 @@
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 
-#define DSMR_NAME_LEN				128
-
-#define DSMR_DSA_TRANCHE_SUFFIX		" DSA"
-#define DSMR_DSA_TRANCHE_SUFFIX_LEN (sizeof(DSMR_DSA_TRANCHE_SUFFIX) - 1)
-#define DSMR_DSA_TRANCHE_NAME_LEN	(DSMR_NAME_LEN + DSMR_DSA_TRANCHE_SUFFIX_LEN)
+#define DSMR_NAME_LEN	MAX_NAMED_TRANCHES_NAME_LEN
 
 typedef struct DSMRegistryCtxStruct
 {
@@ -72,7 +68,7 @@ typedef struct NamedDSAState
 {
 	dsa_handle	handle;
 	int			tranche;
-	char		tranche_name[DSMR_DSA_TRANCHE_NAME_LEN];
+	char		tranche_name[DSMR_NAME_LEN];
 } NamedDSAState;
 
 typedef struct NamedDSHState
@@ -384,14 +380,14 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
-		/* Initialize the LWLock tranche for the DSA. */
-		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		dsa_state->tranche = LWLockNewTrancheId(dsa_state->tranche_name);
-
 		/* Initialize the LWLock tranche for the dshash table. */
 		strcpy(dsh_state->tranche_name, name);
 		dsh_state->tranche = LWLockNewTrancheId(dsh_state->tranche_name);
 
+		/* DSA uses the same tranche as DSH */
+		strcpy(dsa_state->tranche_name, dsh_state->tranche_name);
+		dsa_state->tranche = dsh_state->tranche;
+
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsa_state->tranche);
 		dsa_pin(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 6633c5ec37d..a9a61ffac38 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -107,9 +107,6 @@
 
 /* 128 named tranches limit: arbitrary but sufficient */
 #define MAX_NAMED_TRANCHES			128
-/* Maximum length of a named tranche */
-#define MAX_NAMED_TRANCHES_NAME_LEN			NAMEDATALEN
-
 
 StaticAssertDecl(((MAX_BACKENDS + 1) & MAX_BACKENDS) == 0,
 				 "MAX_BACKENDS + 1 needs to be a power of 2");
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 1ba77cb3658..a514960fe29 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -83,6 +83,9 @@ typedef struct NamedLWLockTranche
 extern PGDLLIMPORT const char *NamedLWLockTrancheArray;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
+/* Maximum length of a named tranche */
+#define MAX_NAMED_TRANCHES_NAME_LEN			NAMEDATALEN
+
 /*
  * It's a bit odd to declare NUM_BUFFER_PARTITIONS and NUM_LOCK_PARTITIONS
  * here, but we need them to figure out offsets within MainLWLockArray, and
-- 
2.43.0

v15-0002-Tests-for-LWLock-tranche-registration-improvemen.patchapplication/octet-stream; name=v15-0002-Tests-for-LWLock-tranche-registration-improvemen.patchDownload
From e4e81e8dfe45e78f27e2a9d6909383a09e5783a3 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 22 Aug 2025 00:35:45 -0500
Subject: [PATCH v15 2/3] Tests for LWLock tranche registration improvements

---
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_tranches/Makefile       |  23 +++
 src/test/modules/test_tranches/meson.build    |  33 +++
 .../test_tranches/t/001_test_tranches.pl      | 120 +++++++++++
 .../test_tranches/test_tranches--1.0.sql      |  35 ++++
 .../modules/test_tranches/test_tranches.c     | 191 ++++++++++++++++++
 .../test_tranches/test_tranches.control       |   6 +
 8 files changed, 411 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/test_tranches/Makefile
 create mode 100644 src/test/modules/test_tranches/meson.build
 create mode 100644 src/test/modules/test_tranches/t/001_test_tranches.pl
 create mode 100644 src/test/modules/test_tranches/test_tranches--1.0.sql
 create mode 100644 src/test/modules/test_tranches/test_tranches.c
 create mode 100644 src/test/modules/test_tranches/test_tranches.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..fd7aa4675c5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..52883dfb496 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_tranches')
diff --git a/src/test/modules/test_tranches/Makefile b/src/test/modules/test_tranches/Makefile
new file mode 100644
index 00000000000..5c9e78084da
--- /dev/null
+++ b/src/test/modules/test_tranches/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_tranches/Makefile
+
+MODULE_big = test_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_tranches.o
+PGFILEDESC = "test_tranches - test code LWLock tranche management"
+
+EXTENSION = test_tranches
+DATA = test_tranches--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_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_tranches/meson.build b/src/test/modules/test_tranches/meson.build
new file mode 100644
index 00000000000..976ce9f0a25
--- /dev/null
+++ b/src/test/modules/test_tranches/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_tranches_sources = files(
+  'test_tranches.c',
+)
+
+if host_system == 'windows'
+  test_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_tranches',
+    '--FILEDESC', 'test_tranches - test code LWLock tranche management',])
+endif
+
+test_tranches = shared_module('test_tranches',
+  test_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_tranches
+
+test_install_data += files(
+  'test_tranches.control',
+  'test_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_test_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/t/001_test_tranches.pl b/src/test/modules/test_tranches/t/001_test_tranches.pl
new file mode 100644
index 00000000000..dd500e49e24
--- /dev/null
+++ b/src/test/modules/test_tranches/t/001_test_tranches.pl
@@ -0,0 +1,120 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+#
+# Create the cluster without any requested tranches
+#
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=0));
+$node->start();
+$node->safe_psql('postgres', q(CREATE EXTENSION test_tranches));
+
+my $first_user_defined = $node->safe_psql('postgres', q(select test_tranches_get_first_user_defined()));
+my @user_defined = ($first_user_defined .. $first_user_defined + 5);
+my ($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+#
+# Run lookup tests to ensure the correct tranche names are returned
+# and an error is raised for unregistered tranches
+#
+my ($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_new(5);
+        select test_tranches_lookup(1);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  LWLock tranche is not registered/, "unregistered tranche error");
+like("$stdout",
+     qr/ShmemIndex.*test_lock__0.*test_lock__1.*test_lock__2.*test_lock__3.*test_lock__4/s,
+     "match tranche names without requested tranches");
+
+#
+# Test the error for long tranche names
+#
+my $good_tranche_name = 'A' x 63;
+my $bad_tranche_name = 'B' x 64; # MAX_NAMED_TRANCHES_NAME_LEN
+$node->safe_psql('postgres', qq{select test_tranches_new_tranche('$good_tranche_name');});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_new_tranche('$bad_tranche_name');
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche name too long/, "tranche name too long");
+
+$node->restart();
+
+#
+# Test the error when > MAX_NAMED_TRANCHES named tranches registered.
+#
+$node->safe_psql('postgres', qq{select test_tranches_new(127)});
+$node->safe_psql('postgres', qq{select test_tranches_new(1)});
+($result, $stdout, $stderr) = $node->psql('postgres', qq{select test_tranches_new(1);}, on_error_stop => 0);
+like("$stderr", qr/ERROR:  too many LWLock tranches registered/, "too many tranches registered");
+
+#
+# Repeat the lookup test with 2 requested tranches
+#
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=2));
+$node->restart();
+
+$first_user_defined = $first_user_defined;
+@user_defined = ($first_user_defined .. $first_user_defined + 5);
+($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_new(3);
+        select test_tranches_lookup(1);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  LWLock tranche is not registered/, "unregistered tranche error");
+like("$stdout", qr/ShmemIndex.*test_lock_0.*test_lock_1.*test_lock__2.*test_lock__3.*test_lock__4/s,
+     "match tranche names with requested tranches");
+
+#
+# Test lwlock initialize
+#
+$node->safe_psql('postgres', qq{select test_tranches_lwlock_initialize($first_user_defined)});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{select test_tranches_lwlock_initialize($sixth_user_defined)},
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  LWLock tranche is not registered/, "LWLock intialization error on invalid tranche name");
+
+#
+# Test error for NULL tranche name
+#
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{select test_tranches_new_tranche(NULL)},
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche name cannot be null/, "NULL tranche name");
+
+done_testing();
diff --git a/src/test/modules/test_tranches/test_tranches--1.0.sql b/src/test/modules/test_tranches/test_tranches--1.0.sql
new file mode 100644
index 00000000000..f504098e04a
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches--1.0.sql
@@ -0,0 +1,35 @@
+-- test_tranches--1.0.sql
+
+CREATE FUNCTION test_tranches_new(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_new'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lookup(int)
+RETURNS text
+AS 'MODULE_PATHNAME', 'test_tranches_lookup'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_named_lwlock(text, int)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_named_lwlock'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lwlock_initialize(int)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_lwlock_initialize'
+LANGUAGE C STRICT;
+
+/*
+ * Function is CALLED ON NULL INPUT to allow NULL input
+ * for testing
+ */
+CREATE FUNCTION test_tranches_new_tranche(text)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_new_tranche'
+LANGUAGE C;
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/test_tranches.c b/src/test/modules/test_tranches/test_tranches.c
new file mode 100644
index 00000000000..7d3031ace9f
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.c
@@ -0,0 +1,191 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_tranches_shmem_request(void);
+static void test_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_tranches_requested_named_tranches = 0;
+
+typedef struct testTranchesSharedState
+{
+	int			next_index;
+}			testTranchesSharedState;
+
+static testTranchesSharedState * test_lwlock_ss = NULL;
+
+/*
+ * LWLock wait event masks. Copied from src/backend/utils/activity/wait_event.c
+ */
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_tranches_shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	test_lwlock_ss = NULL;
+
+	test_lwlock_ss = ShmemInitStruct("test_tranches",
+									 sizeof(testTranchesSharedState),
+									 &found);
+	if (!found)
+	{
+		test_lwlock_ss->next_index = test_tranches_requested_named_tranches;
+	}
+}
+
+static Size
+test_tranches_memsize(void)
+{
+	Size		size;
+
+	size = MAXALIGN(sizeof(testTranchesSharedState));
+
+	return size;
+}
+
+static void
+test_tranches_shmem_request(void)
+{
+	int			i = 0;
+
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(test_tranches_memsize());
+
+	for (i = 0; i < test_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new);
+Datum
+test_tranches_new(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+	int			i;
+
+	for (i = test_lwlock_ss->next_index; i < num + test_lwlock_ss->next_index; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	test_lwlock_ss->next_index = i;
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new_tranche);
+Datum
+test_tranches_new_tranche(PG_FUNCTION_ARGS)
+{
+	char	   *tranche_name = NULL;
+
+	if (!PG_ARGISNULL(0))
+		tranche_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+
+	PG_RETURN_INT32(LWLockNewTrancheId(tranche_name));
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lookup);
+Datum
+test_tranches_lookup(PG_FUNCTION_ARGS)
+{
+	const char *tranche_name = GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	if (tranche_name)
+		PG_RETURN_TEXT_P(cstring_to_text(tranche_name));
+	else
+		PG_RETURN_NULL();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_named_lwlock);
+Datum
+test_tranches_get_named_lwlock(PG_FUNCTION_ARGS)
+{
+	int			i;
+	LWLockPadded *locks;
+
+	locks = GetNamedLWLockTranche(text_to_cstring(PG_GETARG_TEXT_PP(0)));
+
+	for (i = 0; i < PG_GETARG_INT32(1); i++)
+	{
+		LWLock	   *lock = &locks[i].lock;
+
+		LWLockAcquire(lock, LW_SHARED);
+		LWLockRelease(lock);
+	}
+
+	PG_RETURN_INT32(i);
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_first_user_defined);
+Datum
+test_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lwlock_initialize);
+Datum
+test_tranches_lwlock_initialize(PG_FUNCTION_ARGS)
+{
+	LWLock		lock;
+
+	LWLockInitialize(&lock, PG_GETARG_INT32(0));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_tranches/test_tranches.control b/src/test/modules/test_tranches/test_tranches.control
new file mode 100644
index 00000000000..8e6254a7e43
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.control
@@ -0,0 +1,6 @@
+# test_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_tranches'
\ No newline at end of file
-- 
2.43.0

#79Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#78)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

I've spent some time getting this prepared for commit, so apologies if I am
steamrolling over some of the latest discussion points. The majority of
what I've changed amounts to what I'd characterize as relatively minor
editorialization, but I'd imagine reasonable people could disagree. The
bigger things I've changed include:

* I've moved the DSM registry adjustments to be a prerequisite patch. I
noticed that we can also stop tracking the tranche names there since we
always use the name of the DSM registry entry.

* I modified the array of tranche names to be a char ** to simplify
lookups. I also changed how it is first initialized in CreateLWLocks() a
bit.

* I've left out the tests for now. Those are great for the development
phase, but I'm not completely sold on committing them. In any case, I'd
probably treat that as a follow-up effort once this stuff is committed.

I think this patch set will require reworking the "GetNamedLWLockTranche
crashes on Windows in normal backend" patch [0]/messages/by-id/CAA5RZ0v1_15QPg5Sqd2Qz5rh_qcsyCeHHmRDY89xVHcy2yt5BQ@mail.gmail.com, but AFAICT we can easily
adjust it to scan through NamedLWLockTrancheNames instead.

Overall, I'm pretty happy about these patches. They simplify the API and
the code while also fixing the problem with tranche name visibility.

[0]: /messages/by-id/CAA5RZ0v1_15QPg5Sqd2Qz5rh_qcsyCeHHmRDY89xVHcy2yt5BQ@mail.gmail.com

--
nathan

Attachments:

v16-0001-dsm_registry-Use-one-LWLock-tranche-for-dshash-t.patchtext/plain; charset=us-asciiDownload
From 484ebb5cc6ae4300aea654b0f148889d63367256 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 27 Aug 2025 10:28:26 -0500
Subject: [PATCH v16 1/2] dsm_registry: Use one LWLock tranche for dshash table
 entries.

XXX: Also use NAMEDATALEN for entry names.
---
 src/backend/storage/ipc/dsm_registry.c | 56 +++++++++-----------------
 1 file changed, 18 insertions(+), 38 deletions(-)

diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..332796465ff 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -48,12 +48,6 @@
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 
-#define DSMR_NAME_LEN				128
-
-#define DSMR_DSA_TRANCHE_SUFFIX		" DSA"
-#define DSMR_DSA_TRANCHE_SUFFIX_LEN (sizeof(DSMR_DSA_TRANCHE_SUFFIX) - 1)
-#define DSMR_DSA_TRANCHE_NAME_LEN	(DSMR_NAME_LEN + DSMR_DSA_TRANCHE_SUFFIX_LEN)
-
 typedef struct DSMRegistryCtxStruct
 {
 	dsa_handle	dsah;
@@ -72,15 +66,13 @@ typedef struct NamedDSAState
 {
 	dsa_handle	handle;
 	int			tranche;
-	char		tranche_name[DSMR_DSA_TRANCHE_NAME_LEN];
 } NamedDSAState;
 
 typedef struct NamedDSHState
 {
-	NamedDSAState dsa;
-	dshash_table_handle handle;
+	dsa_handle dsa_handle;
+	dshash_table_handle dsh_handle;
 	int			tranche;
-	char		tranche_name[DSMR_NAME_LEN];
 } NamedDSHState;
 
 typedef enum DSMREntryType
@@ -99,7 +91,7 @@ static const char *const DSMREntryTypeNames[] =
 
 typedef struct DSMRegistryEntry
 {
-	char		name[DSMR_NAME_LEN];
+	char		name[NAMEDATALEN];
 	DSMREntryType type;
 	union
 	{
@@ -308,8 +300,7 @@ GetNamedDSA(const char *name, bool *found)
 
 		/* Initialize the LWLock tranche for the DSA. */
 		state->tranche = LWLockNewTrancheId();
-		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		LWLockRegisterTranche(state->tranche, name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -331,7 +322,7 @@ GetNamedDSA(const char *name, bool *found)
 					(errmsg("requested DSA already attached to current process")));
 
 		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		LWLockRegisterTranche(state->tranche, name);
 
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
@@ -348,11 +339,9 @@ GetNamedDSA(const char *name, bool *found)
  * Initialize or attach a named dshash table.
  *
  * This routine returns the address of the table.  The tranche_id member of
- * params is ignored; new tranche IDs will be generated if needed.  Note that
- * the DSA lock tranche will be registered with the provided name with " DSA"
- * appended.  The dshash lock tranche will be registered with the provided
- * name.  Also note that this should be called at most once for a given table
- * in each backend.
+ * params is ignored; a new tranche ID will be generated if needed.  Note that
+ * the lock tranche will be registered with the provided name.  Also note that
+ * this should be called at most once for a given table in each backend.
  */
 dshash_table *
 GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
@@ -381,25 +370,18 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 	entry = dshash_find_or_insert(dsm_registry_table, name, found);
 	if (!(*found))
 	{
-		NamedDSAState *dsa_state = &entry->data.dsh.dsa;
 		NamedDSHState *dsh_state = &entry->data.dsh;
 		dshash_parameters params_copy;
 		dsa_area   *dsa;
 
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
-		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
-		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-
-		/* Initialize the LWLock tranche for the dshash table. */
+		/* Initialize the LWLock tranche for the hash table. */
 		dsh_state->tranche = LWLockNewTrancheId();
-		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		LWLockRegisterTranche(dsh_state->tranche, name);
 
 		/* Initialize the DSA for the hash table. */
-		dsa = dsa_create(dsa_state->tranche);
+		dsa = dsa_create(dsh_state->tranche);
 		dsa_pin(dsa);
 		dsa_pin_mapping(dsa);
 
@@ -409,34 +391,32 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		ret = dshash_create(dsa, &params_copy, NULL);
 
 		/* Store handles for other backends to use. */
-		dsa_state->handle = dsa_get_handle(dsa);
-		dsh_state->handle = dshash_get_hash_table_handle(ret);
+		dsh_state->dsa_handle = dsa_get_handle(dsa);
+		dsh_state->dsh_handle = dshash_get_hash_table_handle(ret);
 	}
 	else if (entry->type != DSMR_ENTRY_TYPE_DSH)
 		ereport(ERROR,
 				(errmsg("requested DSHash does not match type of existing entry")));
 	else
 	{
-		NamedDSAState *dsa_state = &entry->data.dsh.dsa;
 		NamedDSHState *dsh_state = &entry->data.dsh;
 		dsa_area   *dsa;
 
 		/* XXX: Should we verify params matches what table was created with? */
 
-		if (dsa_is_attached(dsa_state->handle))
+		if (dsa_is_attached(dsh_state->dsa_handle))
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		/* Initialize existing LWLock tranche for the hash table. */
+		LWLockRegisterTranche(dsh_state->tranche, name);
 
 		/* Attach to existing DSA for the hash table. */
-		dsa = dsa_attach(dsa_state->handle);
+		dsa = dsa_attach(dsh_state->dsa_handle);
 		dsa_pin_mapping(dsa);
 
 		/* Attach to existing dshash table. */
-		ret = dshash_attach(dsa, params, dsh_state->handle, NULL);
+		ret = dshash_attach(dsa, params, dsh_state->dsh_handle, NULL);
 	}
 
 	dshash_release_lock(dsm_registry_table, entry);
-- 
2.39.5 (Apple Git-154)

v16-0002-Move-dynamically-allocated-tranche-names-to-shar.patchtext/plain; charset=us-asciiDownload
From 6ad1dac7633e4701bdfecc6885b3553ee33a40ab Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 27 Aug 2025 15:13:41 -0500
Subject: [PATCH v16 2/2] Move dynamically-allocated tranche names to shared
 memory.

---
 contrib/pg_prewarm/autoprewarm.c              |   3 +-
 doc/src/sgml/xfunc.sgml                       |  15 +-
 src/backend/postmaster/launch_backend.c       |   6 +-
 src/backend/storage/ipc/dsm_registry.c        |  12 +-
 src/backend/storage/lmgr/lwlock.c             | 179 ++++++++----------
 src/include/storage/lwlock.h                  |  24 +--
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 11 files changed, 99 insertions(+), 167 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..880e897796a 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,7 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
 
 	return found;
 }
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..da21ef56891 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3759,7 +3759,7 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
       <literal>tranche_id</literal> by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3777,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index bf6b55ee830..cbb44344b5a 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,7 +101,7 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	NamedLWLockTranche *NamedLWLockTrancheArray;
+	char	  **NamedLWLockTrancheNames;
 	LWLockPadded *MainLWLockArray;
 	slock_t    *ProcStructLock;
 	PROC_HDR   *ProcGlobal;
@@ -760,7 +760,7 @@ save_backend_variables(BackendParameters *param,
 #endif
 
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
-	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
+	param->NamedLWLockTrancheNames = NamedLWLockTrancheNames;
 	param->MainLWLockArray = MainLWLockArray;
 	param->ProcStructLock = ProcStructLock;
 	param->ProcGlobal = ProcGlobal;
@@ -1020,7 +1020,7 @@ restore_backend_variables(BackendParameters *param)
 #endif
 
 	NamedLWLockTrancheRequests = param->NamedLWLockTrancheRequests;
-	NamedLWLockTrancheArray = param->NamedLWLockTrancheArray;
+	NamedLWLockTrancheNames = param->NamedLWLockTrancheNames;
 	MainLWLockArray = param->MainLWLockArray;
 	ProcStructLock = param->ProcStructLock;
 	ProcGlobal = param->ProcGlobal;
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 332796465ff..8999dee6761 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -299,8 +299,7 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(state->tranche, name);
+		state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -321,9 +320,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -377,8 +373,7 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the hash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(dsh_state->tranche, name);
+		dsh_state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsh_state->tranche);
@@ -408,9 +403,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the hash table. */
-		LWLockRegisterTranche(dsh_state->tranche, name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsh_state->dsa_handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index c80b43f1f55..39f17c387f3 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -126,8 +126,8 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
  * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * or LWLockNewTrancheId.  These names are stored in shared memory and can be
+ * accessed via NamedLWLockTrancheNames.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -146,11 +146,12 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * points to the shared memory locations of the names of all
+ * dynamically-created tranches.  Backends inherit the pointer by fork from the
+ * postmaster (except in the EXEC_BACKEND case, where we have special measures
+ * to pass it down).
  */
-static const char **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
+char	  **NamedLWLockTrancheNames = NULL;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -184,18 +185,18 @@ typedef struct NamedLWLockTrancheRequest
 } NamedLWLockTrancheRequest;
 
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
-static int	NamedLWLockTrancheRequestsAllocated = 0;
 
 /*
- * NamedLWLockTrancheRequests is both the valid length of the request array,
- * and the length of the shared-memory NamedLWLockTrancheArray later on.
- * This variable and NamedLWLockTrancheArray are non-static so that
- * postmaster.c can copy them to child processes in EXEC_BACKEND builds.
+ * NamedLWLockTrancheRequests is the valid length of the request array.  This
+ * variable is non-static so that postmaster.c can copy them to child processes
+ * in EXEC_BACKEND builds.
  */
 int			NamedLWLockTrancheRequests = 0;
 
-/* points to data in shared memory: */
-NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
+/* backend-local counter of registered tranches */
+static int	LocalLWLockCounter;
+
+#define MAX_NAMED_TRANCHES 256
 
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
@@ -391,7 +392,6 @@ Size
 LWLockShmemSize(void)
 {
 	Size		size;
-	int			i;
 	int			numLocks = NUM_FIXED_LWLOCKS;
 
 	/* Calculate total number of locks needed in the main array. */
@@ -404,18 +404,15 @@ LWLockShmemSize(void)
 	size = add_size(size, sizeof(int) + LWLOCK_PADDED_SIZE);
 
 	/* space for named tranches. */
-	size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche)));
-
-	/* space for name of each tranche. */
-	for (i = 0; i < NamedLWLockTrancheRequests; i++)
-		size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1);
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, sizeof(char *)));
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, NAMEDATALEN));
 
 	return size;
 }
 
 /*
  * Allocate shmem space for the main LWLock array and all tranches and
- * initialize it.  We also register extension LWLock tranches here.
+ * initialize it.
  */
 void
 CreateLWLocks(void)
@@ -429,6 +426,15 @@ CreateLWLocks(void)
 		/* Allocate space */
 		ptr = (char *) ShmemAlloc(spaceLocks);
 
+		/* Initialize tranche names */
+		NamedLWLockTrancheNames = (char **) ptr;
+		ptr += MAX_NAMED_TRANCHES * sizeof(char *);
+		for (int i = 0; i < MAX_NAMED_TRANCHES; i++)
+		{
+			NamedLWLockTrancheNames[i] = ptr;
+			ptr += NAMEDATALEN;
+		}
+
 		/* Leave room for dynamic allocation of tranches */
 		ptr += sizeof(int);
 
@@ -447,11 +453,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -460,7 +461,6 @@ CreateLWLocks(void)
 static void
 InitializeLWLocks(void)
 {
-	int			numNamedLocks = NumLWLocksForNamedTranches();
 	int			id;
 	int			i;
 	int			j;
@@ -491,32 +491,18 @@ InitializeLWLocks(void)
 	 */
 	if (NamedLWLockTrancheRequests > 0)
 	{
-		char	   *trancheNames;
-
-		NamedLWLockTrancheArray = (NamedLWLockTranche *)
-			&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
-
-		trancheNames = (char *) NamedLWLockTrancheArray +
-			(NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche));
 		lock = &MainLWLockArray[NUM_FIXED_LWLOCKS];
 
 		for (i = 0; i < NamedLWLockTrancheRequests; i++)
 		{
 			NamedLWLockTrancheRequest *request;
-			NamedLWLockTranche *tranche;
-			char	   *name;
+			int			tranche;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = &NamedLWLockTrancheArray[i];
-
-			name = trancheNames;
-			trancheNames += strlen(request->tranche_name) + 1;
-			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
-			tranche->trancheName = name;
+			tranche = LWLockNewTrancheId(request->tranche_name);
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
-				LWLockInitialize(&lock->lock, tranche->trancheId);
+				LWLockInitialize(&lock->lock, tranche);
 		}
 	}
 }
@@ -568,61 +554,41 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID with the provided name.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *name)
 {
 	int			result;
 	int		   *LWLockCounter;
 
+	if (strlen(name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("tranche name too long"),
+				 errdetail("LWLock tranche name must be less than %d bytes.",
+						   NAMEDATALEN)));
+
 	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
+
 	/* We use the ShmemLock spinlock to protect LWLockCounter */
 	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
-	SpinLockRelease(ShmemLock);
-
-	return result;
-}
-
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-{
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
-
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (*LWLockCounter - LWTRANCHE_FIRST_USER_DEFINED >= MAX_NAMED_TRANCHES)
 	{
-		int			newalloc;
+		SpinLockRelease(ShmemLock);
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("At most %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
+	}
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	result = LocalLWLockCounter = (*LWLockCounter)++;
+	strcpy(NamedLWLockTrancheNames[result - LWTRANCHE_FIRST_USER_DEFINED], name);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
-	}
+	SpinLockRelease(ShmemLock);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	return result;
 }
 
 /*
@@ -647,22 +613,17 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
-		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
-							   NamedLWLockTrancheRequestsAllocated
+							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
-	{
-		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
-
-		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
-			repalloc(NamedLWLockTrancheRequestArray,
-					 i * sizeof(NamedLWLockTrancheRequest));
-		NamedLWLockTrancheRequestsAllocated = i;
-	}
+	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("At most %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
 	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
@@ -677,6 +638,9 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	/* verify the tranche_id is valid */
+	(void) GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -717,18 +681,27 @@ GetLWTrancheName(uint16 trancheId)
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
+	/* verify the trancheId is valid */
+	if (trancheId >= LocalLWLockCounter)
+	{
+		int		   *LWLockCounter;
+
+		LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
+
+		SpinLockAcquire(ShmemLock);
+		LocalLWLockCounter = *LWLockCounter;
+		SpinLockRelease(ShmemLock);
+
+		if (trancheId >= LocalLWLockCounter)
+			elog(ERROR, "tranche %d is not registered", trancheId);
+	}
+
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in NamedLWLockTrancheNames.
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
-
-	return LWLockTrancheNames[trancheId];
+	return NamedLWLockTrancheNames[trancheId];
 }
 
 /*
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..9f9c4c7b5ca 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -73,14 +73,7 @@ typedef union LWLockPadded
 
 extern PGDLLIMPORT LWLockPadded *MainLWLockArray;
 
-/* struct for storing named tranche information */
-typedef struct NamedLWLockTranche
-{
-	int			trancheId;
-	char	   *trancheName;
-} NamedLWLockTranche;
-
-extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
+extern PGDLLIMPORT char **NamedLWLockTrancheNames;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
 /*
@@ -157,18 +150,11 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
  * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * LWLockNewTrancheId to obtain a tranche ID; this allocates from a shared
+ * counter.  Second, LWLockInitialize should be called just once per lwlock,
+ * passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
+extern int	LWLockNewTrancheId(const char *name);
 extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

#80Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#79)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

* I modified the array of tranche names to be a char ** to simplify
lookups. I also changed how it is first initialized in CreateLWLocks() a
bit.

That works also.

* I've left out the tests for now. Those are great for the development
phase, but I'm not completely sold on committing them. In any case, I'd
probably treat that as a follow-up effort once this stuff is committed.

I was mainly using them to verify the functionality since we lack tests in
this area. I think long-term, it will be good to have test coverage. We don't
need to decide now.

I think this patch set will require reworking the "GetNamedLWLockTranche
crashes on Windows in normal backend" patch [0], but AFAICT we can easily
adjust it to scan through NamedLWLockTrancheNames instead.

Except, we will need to turn the char**NamedLWLockTranche into a struct
that will hold the num_lwlocks as well. Something like. But that is doable.

```
typedef struct NamedLWLockTranche
{
char trancheName[NAMEDATALEN];
int num_lwlocks;
} NamedLWLockTranche;
```
If there is no interest to backpatch [0]/messages/by-id/CAA5RZ0v1_15QPg5Sqd2Qz5rh_qcsyCeHHmRDY89xVHcy2yt5BQ@mail.gmail.com, maybe we should just make this
change as part of this patch set. What do you think? I can make this change
in v18.

Overall, I'm pretty happy about these patches. They simplify the API and
the code while also fixing the problem with tranche name visibility.

Just a few things that were discussed earlier, that I incorporated now.

1/ We should be checking that tranche_name is NOT NULL when
LWLockNewTrancheId or RequestNamedLWLockTranche is called.
Also check for the length of the tranche name inside
RequestNamedLWLockTranche, instead of relying on an Assert to check
the length. I think that is much safer.

2/ Bertrand suggested to use strlcpy instead of strcpy when copying while
holding a spinlock [1]/messages/by-id/CAA5RZ0ve-fHuNYW-ruMwg1y1v7-aCqMm_MiNq1KOdg2Y2-pKDw@mail.gmail.com

3/ There was a doc update to mention the 63 byte tranche length. We can
skip that for now, since the ERROR message in v16 is clear about the
limit.

[0]: /messages/by-id/CAA5RZ0v1_15QPg5Sqd2Qz5rh_qcsyCeHHmRDY89xVHcy2yt5BQ@mail.gmail.com
[1]: /messages/by-id/CAA5RZ0ve-fHuNYW-ruMwg1y1v7-aCqMm_MiNq1KOdg2Y2-pKDw@mail.gmail.com

--
Sami

Attachments:

v17-0001-dsm_registry-Use-one-LWLock-tranche-for-dshash-t.patchapplication/octet-stream; name=v17-0001-dsm_registry-Use-one-LWLock-tranche-for-dshash-t.patchDownload
From 97a6395a541faebf6f41ffc7c7749275e99204f0 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 27 Aug 2025 10:28:26 -0500
Subject: [PATCH v17 1/4] dsm_registry: Use one LWLock tranche for dshash table
 entries.

XXX: Also use NAMEDATALEN for entry names.
---
 src/backend/storage/ipc/dsm_registry.c | 56 +++++++++-----------------
 1 file changed, 18 insertions(+), 38 deletions(-)

diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..332796465ff 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -48,12 +48,6 @@
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 
-#define DSMR_NAME_LEN				128
-
-#define DSMR_DSA_TRANCHE_SUFFIX		" DSA"
-#define DSMR_DSA_TRANCHE_SUFFIX_LEN (sizeof(DSMR_DSA_TRANCHE_SUFFIX) - 1)
-#define DSMR_DSA_TRANCHE_NAME_LEN	(DSMR_NAME_LEN + DSMR_DSA_TRANCHE_SUFFIX_LEN)
-
 typedef struct DSMRegistryCtxStruct
 {
 	dsa_handle	dsah;
@@ -72,15 +66,13 @@ typedef struct NamedDSAState
 {
 	dsa_handle	handle;
 	int			tranche;
-	char		tranche_name[DSMR_DSA_TRANCHE_NAME_LEN];
 } NamedDSAState;
 
 typedef struct NamedDSHState
 {
-	NamedDSAState dsa;
-	dshash_table_handle handle;
+	dsa_handle dsa_handle;
+	dshash_table_handle dsh_handle;
 	int			tranche;
-	char		tranche_name[DSMR_NAME_LEN];
 } NamedDSHState;
 
 typedef enum DSMREntryType
@@ -99,7 +91,7 @@ static const char *const DSMREntryTypeNames[] =
 
 typedef struct DSMRegistryEntry
 {
-	char		name[DSMR_NAME_LEN];
+	char		name[NAMEDATALEN];
 	DSMREntryType type;
 	union
 	{
@@ -308,8 +300,7 @@ GetNamedDSA(const char *name, bool *found)
 
 		/* Initialize the LWLock tranche for the DSA. */
 		state->tranche = LWLockNewTrancheId();
-		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		LWLockRegisterTranche(state->tranche, name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -331,7 +322,7 @@ GetNamedDSA(const char *name, bool *found)
 					(errmsg("requested DSA already attached to current process")));
 
 		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		LWLockRegisterTranche(state->tranche, name);
 
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
@@ -348,11 +339,9 @@ GetNamedDSA(const char *name, bool *found)
  * Initialize or attach a named dshash table.
  *
  * This routine returns the address of the table.  The tranche_id member of
- * params is ignored; new tranche IDs will be generated if needed.  Note that
- * the DSA lock tranche will be registered with the provided name with " DSA"
- * appended.  The dshash lock tranche will be registered with the provided
- * name.  Also note that this should be called at most once for a given table
- * in each backend.
+ * params is ignored; a new tranche ID will be generated if needed.  Note that
+ * the lock tranche will be registered with the provided name.  Also note that
+ * this should be called at most once for a given table in each backend.
  */
 dshash_table *
 GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
@@ -381,25 +370,18 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 	entry = dshash_find_or_insert(dsm_registry_table, name, found);
 	if (!(*found))
 	{
-		NamedDSAState *dsa_state = &entry->data.dsh.dsa;
 		NamedDSHState *dsh_state = &entry->data.dsh;
 		dshash_parameters params_copy;
 		dsa_area   *dsa;
 
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
-		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
-		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-
-		/* Initialize the LWLock tranche for the dshash table. */
+		/* Initialize the LWLock tranche for the hash table. */
 		dsh_state->tranche = LWLockNewTrancheId();
-		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		LWLockRegisterTranche(dsh_state->tranche, name);
 
 		/* Initialize the DSA for the hash table. */
-		dsa = dsa_create(dsa_state->tranche);
+		dsa = dsa_create(dsh_state->tranche);
 		dsa_pin(dsa);
 		dsa_pin_mapping(dsa);
 
@@ -409,34 +391,32 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		ret = dshash_create(dsa, &params_copy, NULL);
 
 		/* Store handles for other backends to use. */
-		dsa_state->handle = dsa_get_handle(dsa);
-		dsh_state->handle = dshash_get_hash_table_handle(ret);
+		dsh_state->dsa_handle = dsa_get_handle(dsa);
+		dsh_state->dsh_handle = dshash_get_hash_table_handle(ret);
 	}
 	else if (entry->type != DSMR_ENTRY_TYPE_DSH)
 		ereport(ERROR,
 				(errmsg("requested DSHash does not match type of existing entry")));
 	else
 	{
-		NamedDSAState *dsa_state = &entry->data.dsh.dsa;
 		NamedDSHState *dsh_state = &entry->data.dsh;
 		dsa_area   *dsa;
 
 		/* XXX: Should we verify params matches what table was created with? */
 
-		if (dsa_is_attached(dsa_state->handle))
+		if (dsa_is_attached(dsh_state->dsa_handle))
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		/* Initialize existing LWLock tranche for the hash table. */
+		LWLockRegisterTranche(dsh_state->tranche, name);
 
 		/* Attach to existing DSA for the hash table. */
-		dsa = dsa_attach(dsa_state->handle);
+		dsa = dsa_attach(dsh_state->dsa_handle);
 		dsa_pin_mapping(dsa);
 
 		/* Attach to existing dshash table. */
-		ret = dshash_attach(dsa, params, dsh_state->handle, NULL);
+		ret = dshash_attach(dsa, params, dsh_state->dsh_handle, NULL);
 	}
 
 	dshash_release_lock(dsm_registry_table, entry);
-- 
2.39.5 (Apple Git-154)

v17-0002-Move-dynamically-allocated-tranche-names-to-shar.patchapplication/octet-stream; name=v17-0002-Move-dynamically-allocated-tranche-names-to-shar.patchDownload
From 4846b6489da75a171fb4f6f1677f7a0d8dcb756b Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 27 Aug 2025 15:13:41 -0500
Subject: [PATCH v17 2/4] Move dynamically-allocated tranche names to shared
 memory.

---
 contrib/pg_prewarm/autoprewarm.c              |   3 +-
 doc/src/sgml/xfunc.sgml                       |  15 +-
 src/backend/postmaster/launch_backend.c       |   6 +-
 src/backend/storage/ipc/dsm_registry.c        |  12 +-
 src/backend/storage/lmgr/lwlock.c             | 196 +++++++++---------
 src/include/storage/lwlock.h                  |  24 +--
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 11 files changed, 116 insertions(+), 167 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..880e897796a 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,7 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
 
 	return found;
 }
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..da21ef56891 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3759,7 +3759,7 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
       <literal>tranche_id</literal> by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3777,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index bf6b55ee830..cbb44344b5a 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,7 +101,7 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	NamedLWLockTranche *NamedLWLockTrancheArray;
+	char	  **NamedLWLockTrancheNames;
 	LWLockPadded *MainLWLockArray;
 	slock_t    *ProcStructLock;
 	PROC_HDR   *ProcGlobal;
@@ -760,7 +760,7 @@ save_backend_variables(BackendParameters *param,
 #endif
 
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
-	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
+	param->NamedLWLockTrancheNames = NamedLWLockTrancheNames;
 	param->MainLWLockArray = MainLWLockArray;
 	param->ProcStructLock = ProcStructLock;
 	param->ProcGlobal = ProcGlobal;
@@ -1020,7 +1020,7 @@ restore_backend_variables(BackendParameters *param)
 #endif
 
 	NamedLWLockTrancheRequests = param->NamedLWLockTrancheRequests;
-	NamedLWLockTrancheArray = param->NamedLWLockTrancheArray;
+	NamedLWLockTrancheNames = param->NamedLWLockTrancheNames;
 	MainLWLockArray = param->MainLWLockArray;
 	ProcStructLock = param->ProcStructLock;
 	ProcGlobal = param->ProcGlobal;
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 332796465ff..8999dee6761 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -299,8 +299,7 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(state->tranche, name);
+		state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -321,9 +320,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -377,8 +373,7 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the hash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(dsh_state->tranche, name);
+		dsh_state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsh_state->tranche);
@@ -408,9 +403,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the hash table. */
-		LWLockRegisterTranche(dsh_state->tranche, name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsh_state->dsa_handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index c80b43f1f55..31a4133a1df 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -126,8 +126,8 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
  * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * or LWLockNewTrancheId.  These names are stored in shared memory and can be
+ * accessed via NamedLWLockTrancheNames.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -146,11 +146,12 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * points to the shared memory locations of the names of all
+ * dynamically-created tranches.  Backends inherit the pointer by fork from the
+ * postmaster (except in the EXEC_BACKEND case, where we have special measures
+ * to pass it down).
  */
-static const char **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
+char	  **NamedLWLockTrancheNames = NULL;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -184,18 +185,18 @@ typedef struct NamedLWLockTrancheRequest
 } NamedLWLockTrancheRequest;
 
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
-static int	NamedLWLockTrancheRequestsAllocated = 0;
 
 /*
- * NamedLWLockTrancheRequests is both the valid length of the request array,
- * and the length of the shared-memory NamedLWLockTrancheArray later on.
- * This variable and NamedLWLockTrancheArray are non-static so that
- * postmaster.c can copy them to child processes in EXEC_BACKEND builds.
+ * NamedLWLockTrancheRequests is the valid length of the request array.  This
+ * variable is non-static so that postmaster.c can copy them to child processes
+ * in EXEC_BACKEND builds.
  */
 int			NamedLWLockTrancheRequests = 0;
 
-/* points to data in shared memory: */
-NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
+/* backend-local counter of registered tranches */
+static int	LocalLWLockCounter;
+
+#define MAX_NAMED_TRANCHES 256
 
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
@@ -391,7 +392,6 @@ Size
 LWLockShmemSize(void)
 {
 	Size		size;
-	int			i;
 	int			numLocks = NUM_FIXED_LWLOCKS;
 
 	/* Calculate total number of locks needed in the main array. */
@@ -404,18 +404,15 @@ LWLockShmemSize(void)
 	size = add_size(size, sizeof(int) + LWLOCK_PADDED_SIZE);
 
 	/* space for named tranches. */
-	size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche)));
-
-	/* space for name of each tranche. */
-	for (i = 0; i < NamedLWLockTrancheRequests; i++)
-		size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1);
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, sizeof(char *)));
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, NAMEDATALEN));
 
 	return size;
 }
 
 /*
  * Allocate shmem space for the main LWLock array and all tranches and
- * initialize it.  We also register extension LWLock tranches here.
+ * initialize it.
  */
 void
 CreateLWLocks(void)
@@ -429,6 +426,15 @@ CreateLWLocks(void)
 		/* Allocate space */
 		ptr = (char *) ShmemAlloc(spaceLocks);
 
+		/* Initialize tranche names */
+		NamedLWLockTrancheNames = (char **) ptr;
+		ptr += MAX_NAMED_TRANCHES * sizeof(char *);
+		for (int i = 0; i < MAX_NAMED_TRANCHES; i++)
+		{
+			NamedLWLockTrancheNames[i] = ptr;
+			ptr += NAMEDATALEN;
+		}
+
 		/* Leave room for dynamic allocation of tranches */
 		ptr += sizeof(int);
 
@@ -447,11 +453,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -460,7 +461,6 @@ CreateLWLocks(void)
 static void
 InitializeLWLocks(void)
 {
-	int			numNamedLocks = NumLWLocksForNamedTranches();
 	int			id;
 	int			i;
 	int			j;
@@ -491,32 +491,18 @@ InitializeLWLocks(void)
 	 */
 	if (NamedLWLockTrancheRequests > 0)
 	{
-		char	   *trancheNames;
-
-		NamedLWLockTrancheArray = (NamedLWLockTranche *)
-			&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
-
-		trancheNames = (char *) NamedLWLockTrancheArray +
-			(NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche));
 		lock = &MainLWLockArray[NUM_FIXED_LWLOCKS];
 
 		for (i = 0; i < NamedLWLockTrancheRequests; i++)
 		{
 			NamedLWLockTrancheRequest *request;
-			NamedLWLockTranche *tranche;
-			char	   *name;
+			int			tranche;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = &NamedLWLockTrancheArray[i];
-
-			name = trancheNames;
-			trancheNames += strlen(request->tranche_name) + 1;
-			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
-			tranche->trancheName = name;
+			tranche = LWLockNewTrancheId(request->tranche_name);
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
-				LWLockInitialize(&lock->lock, tranche->trancheId);
+				LWLockInitialize(&lock->lock, tranche);
 		}
 	}
 }
@@ -568,61 +554,46 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID with the provided name.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
 	int			result;
 	int		   *LWLockCounter;
 
-	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
-	/* We use the ShmemLock spinlock to protect LWLockCounter */
-	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
-	SpinLockRelease(ShmemLock);
+	if (!tranche_name)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("tranche name cannot be null")));
 
-	return result;
-}
+	if (strlen(tranche_name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("tranche name too long"),
+				 errdetail("LWLock tranche name must be less than %d bytes.",
+						   NAMEDATALEN)));
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-{
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	/* We use the ShmemLock spinlock to protect LWLockCounter */
+	SpinLockAcquire(ShmemLock);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (*LWLockCounter - LWTRANCHE_FIRST_USER_DEFINED >= MAX_NAMED_TRANCHES)
 	{
-		int			newalloc;
+		SpinLockRelease(ShmemLock);
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("At most %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
+	}
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	result = LocalLWLockCounter = (*LWLockCounter)++;
+	strlcpy(NamedLWLockTrancheNames[result - LWTRANCHE_FIRST_USER_DEFINED], tranche_name, NAMEDATALEN);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
-	}
+	SpinLockRelease(ShmemLock);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	return result;
 }
 
 /*
@@ -645,24 +616,31 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 	if (!process_shmem_requests_in_progress)
 		elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook");
 
+	if (!tranche_name)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("tranche name cannot be null")));
+
+	if (strlen(tranche_name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("tranche name too long"),
+				 errdetail("LWLock tranche name must be less than %d bytes.",
+						   NAMEDATALEN)));
+
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
-		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
-							   NamedLWLockTrancheRequestsAllocated
+							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
-	{
-		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
-
-		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
-			repalloc(NamedLWLockTrancheRequestArray,
-					 i * sizeof(NamedLWLockTrancheRequest));
-		NamedLWLockTrancheRequestsAllocated = i;
-	}
+	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("At most %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
 	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
@@ -677,6 +655,9 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	/* verify the tranche_id is valid */
+	(void) GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -717,18 +698,27 @@ GetLWTrancheName(uint16 trancheId)
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
+	/* verify the trancheId is valid */
+	if (trancheId >= LocalLWLockCounter)
+	{
+		int		   *LWLockCounter;
+
+		LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
+
+		SpinLockAcquire(ShmemLock);
+		LocalLWLockCounter = *LWLockCounter;
+		SpinLockRelease(ShmemLock);
+
+		if (trancheId >= LocalLWLockCounter)
+			elog(ERROR, "tranche %d is not registered", trancheId);
+	}
+
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in NamedLWLockTrancheNames.
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
-
-	return LWLockTrancheNames[trancheId];
+	return NamedLWLockTrancheNames[trancheId];
 }
 
 /*
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..9f9c4c7b5ca 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -73,14 +73,7 @@ typedef union LWLockPadded
 
 extern PGDLLIMPORT LWLockPadded *MainLWLockArray;
 
-/* struct for storing named tranche information */
-typedef struct NamedLWLockTranche
-{
-	int			trancheId;
-	char	   *trancheName;
-} NamedLWLockTranche;
-
-extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
+extern PGDLLIMPORT char **NamedLWLockTrancheNames;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
 /*
@@ -157,18 +150,11 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
  * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * LWLockNewTrancheId to obtain a tranche ID; this allocates from a shared
+ * counter.  Second, LWLockInitialize should be called just once per lwlock,
+ * passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
+extern int	LWLockNewTrancheId(const char *name);
 extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

#81Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Sami Imseih (#80)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Thu, Aug 28, 2025 at 05:53:23PM -0500, Sami Imseih wrote:

Just a few things that were discussed earlier, that I incorporated now.

1/ We should be checking that tranche_name is NOT NULL when
LWLockNewTrancheId or RequestNamedLWLockTranche is called.

Right, if not strlen() does segfault.

In addition to checking for NULL, should we also check for empty string? Currently,
the patch does accept strlen(tranche_name) == 0.

Also check for the length of the tranche name inside
RequestNamedLWLockTranche, instead of relying on an Assert to check
the length. I think that is much safer.

Same remark as above but in RequestNamedLWLockTranche().

Regards,

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

#82Sami Imseih
samimseih@gmail.com
In reply to: Bertrand Drouvot (#81)
3 attachment(s)
Re: Improve LWLock tranche name visibility across backends

On Thu, Aug 28, 2025 at 05:53:23PM -0500, Sami Imseih wrote:

Just a few things that were discussed earlier, that I incorporated now.

1/ We should be checking that tranche_name is NOT NULL when
LWLockNewTrancheId or RequestNamedLWLockTranche is called.

Right, if not strlen() does segfault.

In addition to checking for NULL, should we also check for empty string? Currently,
the patch does accept strlen(tranche_name) == 0.

I am not inclined to prevent an empty string. It's currently allowed and rather
not change that.

```
typedef struct NamedLWLockTranche
{
char trancheName[NAMEDATALEN];
int num_lwlocks;
} NamedLWLockTranche;
```
if there is no interest to backpatch [0], maybe we should just make this
change as part of this patch set. What do you think? I can make this change
in v18.

Here is v18. It includes a third patch to fix the issue identified in
[0]: , which can be applied to HEAD as part of this thread. If we want to backpatch the stable branches, the version in [0] is suitable.
be applied to HEAD as part of this thread. If we want to backpatch the stable
branches, the version in [0], which can be applied to HEAD as part of this thread. If we want to backpatch the stable branches, the version in [0] is suitable. is suitable.

Note that I created a LWLockNewTrancheIdInternal which takes a tranch
name and number of lwlocks. The Internal version is used during startup when
requested lwlocks are appended to shared memory, and the existing
LWLockNewTrancheId calls the internal version with 0 lwlocks.
This keeps all the logic to appending a new tranche ( while holding
the spinlock )
in the same routine.

--
Sami

Attachments:

v18-0002-Move-dynamically-allocated-tranche-names-to-shar.patchapplication/octet-stream; name=v18-0002-Move-dynamically-allocated-tranche-names-to-shar.patchDownload
From baaca1b894e143b1dabdd3a50deb2eae14beeb6b Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 27 Aug 2025 15:13:41 -0500
Subject: [PATCH v18 2/4] Move dynamically-allocated tranche names to shared
 memory.

---
 contrib/pg_prewarm/autoprewarm.c              |   3 +-
 doc/src/sgml/xfunc.sgml                       |  15 +-
 src/backend/postmaster/launch_backend.c       |   6 +-
 src/backend/storage/ipc/dsm_registry.c        |  12 +-
 src/backend/storage/lmgr/lwlock.c             | 196 +++++++++---------
 src/include/storage/lwlock.h                  |  24 +--
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 11 files changed, 116 insertions(+), 167 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..880e897796a 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,7 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
 
 	return found;
 }
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..da21ef56891 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3759,7 +3759,7 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
       <literal>tranche_id</literal> by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3777,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index bf6b55ee830..cbb44344b5a 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,7 +101,7 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	NamedLWLockTranche *NamedLWLockTrancheArray;
+	char	  **NamedLWLockTrancheNames;
 	LWLockPadded *MainLWLockArray;
 	slock_t    *ProcStructLock;
 	PROC_HDR   *ProcGlobal;
@@ -760,7 +760,7 @@ save_backend_variables(BackendParameters *param,
 #endif
 
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
-	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
+	param->NamedLWLockTrancheNames = NamedLWLockTrancheNames;
 	param->MainLWLockArray = MainLWLockArray;
 	param->ProcStructLock = ProcStructLock;
 	param->ProcGlobal = ProcGlobal;
@@ -1020,7 +1020,7 @@ restore_backend_variables(BackendParameters *param)
 #endif
 
 	NamedLWLockTrancheRequests = param->NamedLWLockTrancheRequests;
-	NamedLWLockTrancheArray = param->NamedLWLockTrancheArray;
+	NamedLWLockTrancheNames = param->NamedLWLockTrancheNames;
 	MainLWLockArray = param->MainLWLockArray;
 	ProcStructLock = param->ProcStructLock;
 	ProcGlobal = param->ProcGlobal;
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 332796465ff..8999dee6761 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -299,8 +299,7 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(state->tranche, name);
+		state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -321,9 +320,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -377,8 +373,7 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the hash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(dsh_state->tranche, name);
+		dsh_state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsh_state->tranche);
@@ -408,9 +403,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the hash table. */
-		LWLockRegisterTranche(dsh_state->tranche, name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsh_state->dsa_handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index c80b43f1f55..31a4133a1df 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -126,8 +126,8 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
  * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * or LWLockNewTrancheId.  These names are stored in shared memory and can be
+ * accessed via NamedLWLockTrancheNames.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -146,11 +146,12 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * points to the shared memory locations of the names of all
+ * dynamically-created tranches.  Backends inherit the pointer by fork from the
+ * postmaster (except in the EXEC_BACKEND case, where we have special measures
+ * to pass it down).
  */
-static const char **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
+char	  **NamedLWLockTrancheNames = NULL;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -184,18 +185,18 @@ typedef struct NamedLWLockTrancheRequest
 } NamedLWLockTrancheRequest;
 
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
-static int	NamedLWLockTrancheRequestsAllocated = 0;
 
 /*
- * NamedLWLockTrancheRequests is both the valid length of the request array,
- * and the length of the shared-memory NamedLWLockTrancheArray later on.
- * This variable and NamedLWLockTrancheArray are non-static so that
- * postmaster.c can copy them to child processes in EXEC_BACKEND builds.
+ * NamedLWLockTrancheRequests is the valid length of the request array.  This
+ * variable is non-static so that postmaster.c can copy them to child processes
+ * in EXEC_BACKEND builds.
  */
 int			NamedLWLockTrancheRequests = 0;
 
-/* points to data in shared memory: */
-NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
+/* backend-local counter of registered tranches */
+static int	LocalLWLockCounter;
+
+#define MAX_NAMED_TRANCHES 256
 
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
@@ -391,7 +392,6 @@ Size
 LWLockShmemSize(void)
 {
 	Size		size;
-	int			i;
 	int			numLocks = NUM_FIXED_LWLOCKS;
 
 	/* Calculate total number of locks needed in the main array. */
@@ -404,18 +404,15 @@ LWLockShmemSize(void)
 	size = add_size(size, sizeof(int) + LWLOCK_PADDED_SIZE);
 
 	/* space for named tranches. */
-	size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche)));
-
-	/* space for name of each tranche. */
-	for (i = 0; i < NamedLWLockTrancheRequests; i++)
-		size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1);
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, sizeof(char *)));
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, NAMEDATALEN));
 
 	return size;
 }
 
 /*
  * Allocate shmem space for the main LWLock array and all tranches and
- * initialize it.  We also register extension LWLock tranches here.
+ * initialize it.
  */
 void
 CreateLWLocks(void)
@@ -429,6 +426,15 @@ CreateLWLocks(void)
 		/* Allocate space */
 		ptr = (char *) ShmemAlloc(spaceLocks);
 
+		/* Initialize tranche names */
+		NamedLWLockTrancheNames = (char **) ptr;
+		ptr += MAX_NAMED_TRANCHES * sizeof(char *);
+		for (int i = 0; i < MAX_NAMED_TRANCHES; i++)
+		{
+			NamedLWLockTrancheNames[i] = ptr;
+			ptr += NAMEDATALEN;
+		}
+
 		/* Leave room for dynamic allocation of tranches */
 		ptr += sizeof(int);
 
@@ -447,11 +453,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -460,7 +461,6 @@ CreateLWLocks(void)
 static void
 InitializeLWLocks(void)
 {
-	int			numNamedLocks = NumLWLocksForNamedTranches();
 	int			id;
 	int			i;
 	int			j;
@@ -491,32 +491,18 @@ InitializeLWLocks(void)
 	 */
 	if (NamedLWLockTrancheRequests > 0)
 	{
-		char	   *trancheNames;
-
-		NamedLWLockTrancheArray = (NamedLWLockTranche *)
-			&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
-
-		trancheNames = (char *) NamedLWLockTrancheArray +
-			(NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche));
 		lock = &MainLWLockArray[NUM_FIXED_LWLOCKS];
 
 		for (i = 0; i < NamedLWLockTrancheRequests; i++)
 		{
 			NamedLWLockTrancheRequest *request;
-			NamedLWLockTranche *tranche;
-			char	   *name;
+			int			tranche;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = &NamedLWLockTrancheArray[i];
-
-			name = trancheNames;
-			trancheNames += strlen(request->tranche_name) + 1;
-			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
-			tranche->trancheName = name;
+			tranche = LWLockNewTrancheId(request->tranche_name);
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
-				LWLockInitialize(&lock->lock, tranche->trancheId);
+				LWLockInitialize(&lock->lock, tranche);
 		}
 	}
 }
@@ -568,61 +554,46 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID with the provided name.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *tranche_name)
 {
 	int			result;
 	int		   *LWLockCounter;
 
-	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
-	/* We use the ShmemLock spinlock to protect LWLockCounter */
-	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
-	SpinLockRelease(ShmemLock);
+	if (!tranche_name)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("tranche name cannot be null")));
 
-	return result;
-}
+	if (strlen(tranche_name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("tranche name too long"),
+				 errdetail("LWLock tranche name must be less than %d bytes.",
+						   NAMEDATALEN)));
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-{
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
+	LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
 
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	/* We use the ShmemLock spinlock to protect LWLockCounter */
+	SpinLockAcquire(ShmemLock);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (*LWLockCounter - LWTRANCHE_FIRST_USER_DEFINED >= MAX_NAMED_TRANCHES)
 	{
-		int			newalloc;
+		SpinLockRelease(ShmemLock);
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("At most %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
+	}
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	result = LocalLWLockCounter = (*LWLockCounter)++;
+	strlcpy(NamedLWLockTrancheNames[result - LWTRANCHE_FIRST_USER_DEFINED], tranche_name, NAMEDATALEN);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
-	}
+	SpinLockRelease(ShmemLock);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	return result;
 }
 
 /*
@@ -645,24 +616,31 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 	if (!process_shmem_requests_in_progress)
 		elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook");
 
+	if (!tranche_name)
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("tranche name cannot be null")));
+
+	if (strlen(tranche_name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("tranche name too long"),
+				 errdetail("LWLock tranche name must be less than %d bytes.",
+						   NAMEDATALEN)));
+
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
-		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
-							   NamedLWLockTrancheRequestsAllocated
+							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
-	{
-		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
-
-		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
-			repalloc(NamedLWLockTrancheRequestArray,
-					 i * sizeof(NamedLWLockTrancheRequest));
-		NamedLWLockTrancheRequestsAllocated = i;
-	}
+	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("At most %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
 	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
@@ -677,6 +655,9 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	/* verify the tranche_id is valid */
+	(void) GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -717,18 +698,27 @@ GetLWTrancheName(uint16 trancheId)
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
+	/* verify the trancheId is valid */
+	if (trancheId >= LocalLWLockCounter)
+	{
+		int		   *LWLockCounter;
+
+		LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int));
+
+		SpinLockAcquire(ShmemLock);
+		LocalLWLockCounter = *LWLockCounter;
+		SpinLockRelease(ShmemLock);
+
+		if (trancheId >= LocalLWLockCounter)
+			elog(ERROR, "tranche %d is not registered", trancheId);
+	}
+
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in NamedLWLockTrancheNames.
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
-
-	return LWLockTrancheNames[trancheId];
+	return NamedLWLockTrancheNames[trancheId];
 }
 
 /*
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 5e717765764..9f9c4c7b5ca 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -73,14 +73,7 @@ typedef union LWLockPadded
 
 extern PGDLLIMPORT LWLockPadded *MainLWLockArray;
 
-/* struct for storing named tranche information */
-typedef struct NamedLWLockTranche
-{
-	int			trancheId;
-	char	   *trancheName;
-} NamedLWLockTranche;
-
-extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
+extern PGDLLIMPORT char **NamedLWLockTrancheNames;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
 /*
@@ -157,18 +150,11 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
  * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * LWLockNewTrancheId to obtain a tranche ID; this allocates from a shared
+ * counter.  Second, LWLockInitialize should be called just once per lwlock,
+ * passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
+extern int	LWLockNewTrancheId(const char *name);
 extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

v18-0003-Fix-EXEC_BACKEND-segfault-on-NamedLWLockTrancheR.patchapplication/octet-stream; name=v18-0003-Fix-EXEC_BACKEND-segfault-on-NamedLWLockTrancheR.patchDownload
From 52395326c677e09284365f831d7b4d9838c3a76a Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 29 Aug 2025 08:13:42 -0500
Subject: [PATCH v18 3/4] Fix EXEC_BACKEND segfault on
 NamedLWLockTrancheRequestArray access

Unlike fork(), EXEC_BACKEND did not pass NamedLWLockTrancheRequestArray
down to backends. This caused crashes if backends accessed the array,
such as in GetNamedLWLockTranche.

This patch fixes the issue by adding a num_lwlocks field to the shared
memory NamedLWLockTrancheArray, with the postmaster copying the value
from NamedLWLockTrancheRequestArray. GetNamedLWLockTranche now looks up
the LWLock from NamedLWLockTrancheArray instead.

NamedLWLockTrancheArray already points to shared memory and is passed
down to backends in launch_backend.c. NamedLWLockTrancheRequests still
acts as the counter for both arrays and is already handled in
EXEC_BACKEND, so no further changes are needed there.

This issue was discovered during testing of nearby code. No reports have
been seen in the field, so backpatching is not required.
---
 src/backend/postmaster/launch_backend.c |  2 +-
 src/backend/storage/lmgr/lwlock.c       | 55 ++++++++++++++-----------
 src/include/storage/lwlock.h            |  8 +++-
 3 files changed, 40 insertions(+), 25 deletions(-)

diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index cbb44344b5a..5a495217c5b 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,7 +101,7 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	char	  **NamedLWLockTrancheNames;
+	NamedLWLockTranche *NamedLWLockTrancheNames;
 	LWLockPadded *MainLWLockArray;
 	slock_t    *ProcStructLock;
 	PROC_HDR   *ProcGlobal;
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 31a4133a1df..5cae49ea817 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -146,12 +146,12 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * points to the shared memory locations of the names of all
+ * points to the shared memory locations of the names and number of locks of all
  * dynamically-created tranches.  Backends inherit the pointer by fork from the
  * postmaster (except in the EXEC_BACKEND case, where we have special measures
  * to pass it down).
  */
-char	  **NamedLWLockTrancheNames = NULL;
+NamedLWLockTranche *NamedLWLockTrancheNames = NULL;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -202,6 +202,7 @@ static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
 static const char *GetLWTrancheName(uint16 trancheId);
+static int	LWLockNewTrancheIdInternal(const char *tranche_name, int num_lwlocks);
 
 #define T_NAME(lock) \
 	GetLWTrancheName((lock)->tranche)
@@ -404,8 +405,7 @@ LWLockShmemSize(void)
 	size = add_size(size, sizeof(int) + LWLOCK_PADDED_SIZE);
 
 	/* space for named tranches. */
-	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, sizeof(char *)));
-	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, NAMEDATALEN));
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, sizeof(NamedLWLockTranche)));
 
 	return size;
 }
@@ -426,15 +426,6 @@ CreateLWLocks(void)
 		/* Allocate space */
 		ptr = (char *) ShmemAlloc(spaceLocks);
 
-		/* Initialize tranche names */
-		NamedLWLockTrancheNames = (char **) ptr;
-		ptr += MAX_NAMED_TRANCHES * sizeof(char *);
-		for (int i = 0; i < MAX_NAMED_TRANCHES; i++)
-		{
-			NamedLWLockTrancheNames[i] = ptr;
-			ptr += NAMEDATALEN;
-		}
-
 		/* Leave room for dynamic allocation of tranches */
 		ptr += sizeof(int);
 
@@ -461,6 +452,7 @@ CreateLWLocks(void)
 static void
 InitializeLWLocks(void)
 {
+	int			numNamedLocks = NumLWLocksForNamedTranches();
 	int			id;
 	int			i;
 	int			j;
@@ -485,9 +477,12 @@ InitializeLWLocks(void)
 	for (id = 0; id < NUM_PREDICATELOCK_PARTITIONS; id++, lock++)
 		LWLockInitialize(&lock->lock, LWTRANCHE_PREDICATE_LOCK_MANAGER);
 
+	NamedLWLockTrancheNames = (NamedLWLockTranche *)
+		&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
+
 	/*
-	 * Copy the info about any named tranches into shared memory (so that
-	 * other processes can see it), and initialize the requested LWLocks.
+	 * Allocate a tranche ID in shared memory, and initialize the requested
+	 * LWLocks.
 	 */
 	if (NamedLWLockTrancheRequests > 0)
 	{
@@ -499,7 +494,7 @@ InitializeLWLocks(void)
 			int			tranche;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = LWLockNewTrancheId(request->tranche_name);
+			tranche = LWLockNewTrancheIdInternal(request->tranche_name, request->num_lwlocks);
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
 				LWLockInitialize(&lock->lock, tranche);
@@ -540,11 +535,11 @@ GetNamedLWLockTranche(const char *tranche_name)
 	lock_pos = NUM_FIXED_LWLOCKS;
 	for (i = 0; i < NamedLWLockTrancheRequests; i++)
 	{
-		if (strcmp(NamedLWLockTrancheRequestArray[i].tranche_name,
+		if (strcmp(NamedLWLockTrancheNames[i].trancheName,
 				   tranche_name) == 0)
 			return &MainLWLockArray[lock_pos];
 
-		lock_pos += NamedLWLockTrancheRequestArray[i].num_lwlocks;
+		lock_pos += NamedLWLockTrancheNames[i].num_lwlocks;
 	}
 
 	elog(ERROR, "requested tranche is not registered");
@@ -554,13 +549,15 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID with the provided name.
+ * Workhorse for allocating a new tranche with the specified
+ * number of lwlocks.
  */
-int
-LWLockNewTrancheId(const char *tranche_name)
+static int
+LWLockNewTrancheIdInternal(const char *tranche_name, int num_lwlocks)
 {
 	int			result;
 	int		   *LWLockCounter;
+	int			index;
 
 	if (!tranche_name)
 		ereport(ERROR,
@@ -589,13 +586,25 @@ LWLockNewTrancheId(const char *tranche_name)
 	}
 
 	result = LocalLWLockCounter = (*LWLockCounter)++;
-	strlcpy(NamedLWLockTrancheNames[result - LWTRANCHE_FIRST_USER_DEFINED], tranche_name, NAMEDATALEN);
+	index = result - LWTRANCHE_FIRST_USER_DEFINED;
+
+	strlcpy(NamedLWLockTrancheNames[index].trancheName, tranche_name, NAMEDATALEN);
+	NamedLWLockTrancheNames[index].num_lwlocks = num_lwlocks;
 
 	SpinLockRelease(ShmemLock);
 
 	return result;
 }
 
+/*
+ * Allocate a new tranche ID with the provided name.
+ */
+int
+LWLockNewTrancheId(const char *tranche_name)
+{
+	return LWLockNewTrancheIdInternal(tranche_name, 0);
+}
+
 /*
  * RequestNamedLWLockTranche
  *		Request that extra LWLocks be allocated during postmaster
@@ -718,7 +727,7 @@ GetLWTrancheName(uint16 trancheId)
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	return NamedLWLockTrancheNames[trancheId];
+	return NamedLWLockTrancheNames[trancheId].trancheName;
 }
 
 /*
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index 9f9c4c7b5ca..eb5b5ca65ed 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -71,9 +71,15 @@ typedef union LWLockPadded
 	char		pad[LWLOCK_PADDED_SIZE];
 } LWLockPadded;
 
+typedef struct NamedLWLockTranche
+{
+	char		trancheName[NAMEDATALEN];
+	int			num_lwlocks;
+} NamedLWLockTranche;
+
 extern PGDLLIMPORT LWLockPadded *MainLWLockArray;
 
-extern PGDLLIMPORT char **NamedLWLockTrancheNames;
+extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheNames;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 
 /*
-- 
2.39.5 (Apple Git-154)

v18-0001-dsm_registry-Use-one-LWLock-tranche-for-dshash-t.patchapplication/octet-stream; name=v18-0001-dsm_registry-Use-one-LWLock-tranche-for-dshash-t.patchDownload
From aea3527b5ca12417871238ba9440f0a968951bcd Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 27 Aug 2025 10:28:26 -0500
Subject: [PATCH v18 1/4] dsm_registry: Use one LWLock tranche for dshash table
 entries.

XXX: Also use NAMEDATALEN for entry names.
---
 src/backend/storage/ipc/dsm_registry.c | 56 +++++++++-----------------
 1 file changed, 18 insertions(+), 38 deletions(-)

diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index 1682cc6d34c..332796465ff 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -48,12 +48,6 @@
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 
-#define DSMR_NAME_LEN				128
-
-#define DSMR_DSA_TRANCHE_SUFFIX		" DSA"
-#define DSMR_DSA_TRANCHE_SUFFIX_LEN (sizeof(DSMR_DSA_TRANCHE_SUFFIX) - 1)
-#define DSMR_DSA_TRANCHE_NAME_LEN	(DSMR_NAME_LEN + DSMR_DSA_TRANCHE_SUFFIX_LEN)
-
 typedef struct DSMRegistryCtxStruct
 {
 	dsa_handle	dsah;
@@ -72,15 +66,13 @@ typedef struct NamedDSAState
 {
 	dsa_handle	handle;
 	int			tranche;
-	char		tranche_name[DSMR_DSA_TRANCHE_NAME_LEN];
 } NamedDSAState;
 
 typedef struct NamedDSHState
 {
-	NamedDSAState dsa;
-	dshash_table_handle handle;
+	dsa_handle dsa_handle;
+	dshash_table_handle dsh_handle;
 	int			tranche;
-	char		tranche_name[DSMR_NAME_LEN];
 } NamedDSHState;
 
 typedef enum DSMREntryType
@@ -99,7 +91,7 @@ static const char *const DSMREntryTypeNames[] =
 
 typedef struct DSMRegistryEntry
 {
-	char		name[DSMR_NAME_LEN];
+	char		name[NAMEDATALEN];
 	DSMREntryType type;
 	union
 	{
@@ -308,8 +300,7 @@ GetNamedDSA(const char *name, bool *found)
 
 		/* Initialize the LWLock tranche for the DSA. */
 		state->tranche = LWLockNewTrancheId();
-		strcpy(state->tranche_name, name);
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		LWLockRegisterTranche(state->tranche, name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -331,7 +322,7 @@ GetNamedDSA(const char *name, bool *found)
 					(errmsg("requested DSA already attached to current process")));
 
 		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, state->tranche_name);
+		LWLockRegisterTranche(state->tranche, name);
 
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
@@ -348,11 +339,9 @@ GetNamedDSA(const char *name, bool *found)
  * Initialize or attach a named dshash table.
  *
  * This routine returns the address of the table.  The tranche_id member of
- * params is ignored; new tranche IDs will be generated if needed.  Note that
- * the DSA lock tranche will be registered with the provided name with " DSA"
- * appended.  The dshash lock tranche will be registered with the provided
- * name.  Also note that this should be called at most once for a given table
- * in each backend.
+ * params is ignored; a new tranche ID will be generated if needed.  Note that
+ * the lock tranche will be registered with the provided name.  Also note that
+ * this should be called at most once for a given table in each backend.
  */
 dshash_table *
 GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
@@ -381,25 +370,18 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 	entry = dshash_find_or_insert(dsm_registry_table, name, found);
 	if (!(*found))
 	{
-		NamedDSAState *dsa_state = &entry->data.dsh.dsa;
 		NamedDSHState *dsh_state = &entry->data.dsh;
 		dshash_parameters params_copy;
 		dsa_area   *dsa;
 
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
-		/* Initialize the LWLock tranche for the DSA. */
-		dsa_state->tranche = LWLockNewTrancheId();
-		sprintf(dsa_state->tranche_name, "%s%s", name, DSMR_DSA_TRANCHE_SUFFIX);
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-
-		/* Initialize the LWLock tranche for the dshash table. */
+		/* Initialize the LWLock tranche for the hash table. */
 		dsh_state->tranche = LWLockNewTrancheId();
-		strcpy(dsh_state->tranche_name, name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		LWLockRegisterTranche(dsh_state->tranche, name);
 
 		/* Initialize the DSA for the hash table. */
-		dsa = dsa_create(dsa_state->tranche);
+		dsa = dsa_create(dsh_state->tranche);
 		dsa_pin(dsa);
 		dsa_pin_mapping(dsa);
 
@@ -409,34 +391,32 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		ret = dshash_create(dsa, &params_copy, NULL);
 
 		/* Store handles for other backends to use. */
-		dsa_state->handle = dsa_get_handle(dsa);
-		dsh_state->handle = dshash_get_hash_table_handle(ret);
+		dsh_state->dsa_handle = dsa_get_handle(dsa);
+		dsh_state->dsh_handle = dshash_get_hash_table_handle(ret);
 	}
 	else if (entry->type != DSMR_ENTRY_TYPE_DSH)
 		ereport(ERROR,
 				(errmsg("requested DSHash does not match type of existing entry")));
 	else
 	{
-		NamedDSAState *dsa_state = &entry->data.dsh.dsa;
 		NamedDSHState *dsh_state = &entry->data.dsh;
 		dsa_area   *dsa;
 
 		/* XXX: Should we verify params matches what table was created with? */
 
-		if (dsa_is_attached(dsa_state->handle))
+		if (dsa_is_attached(dsh_state->dsa_handle))
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranches for the DSA and dshash table. */
-		LWLockRegisterTranche(dsa_state->tranche, dsa_state->tranche_name);
-		LWLockRegisterTranche(dsh_state->tranche, dsh_state->tranche_name);
+		/* Initialize existing LWLock tranche for the hash table. */
+		LWLockRegisterTranche(dsh_state->tranche, name);
 
 		/* Attach to existing DSA for the hash table. */
-		dsa = dsa_attach(dsa_state->handle);
+		dsa = dsa_attach(dsh_state->dsa_handle);
 		dsa_pin_mapping(dsa);
 
 		/* Attach to existing dshash table. */
-		ret = dshash_attach(dsa, params, dsh_state->handle, NULL);
+		ret = dshash_attach(dsa, params, dsh_state->dsh_handle, NULL);
 	}
 
 	dshash_release_lock(dsm_registry_table, entry);
-- 
2.39.5 (Apple Git-154)

#83Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#80)
Re: Improve LWLock tranche name visibility across backends

On Thu, Aug 28, 2025 at 05:53:23PM -0500, Sami Imseih wrote:

I think this patch set will require reworking the "GetNamedLWLockTranche
crashes on Windows in normal backend" patch [0], but AFAICT we can easily
adjust it to scan through NamedLWLockTrancheNames instead.

Except, we will need to turn the char**NamedLWLockTranche into a struct
that will hold the num_lwlocks as well. Something like. But that is doable.

Ah, right.

If there is no interest to backpatch [0], maybe we should just make this
change as part of this patch set. What do you think? I can make this change
in v18.

I think we should keep that separate. I don't see any benefit to combining
it, and it'd be good to keep the discussion in a single thread in the
archives for future reference.

--
nathan

#84Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#83)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

I've committed the DSM registry changes. I apologize for the lackluster
commit message. My attempts at including all the details ended up being
super wordy and hard to read, so I decided to keep it terse and leave out
some of the context.

I've also attached a rebased patch that addresses all the latest feedback.
A reworked verison of the test patch is also included, but that's mostly
intended for CI purposes and is still not intended for commit (yet).

--
nathan

Attachments:

v19-0001-Move-dynamically-allocated-tranche-names-to-shar.patchtext/plain; charset=us-asciiDownload
From cf8eee964066152f6aa7dce045f3b338d0959599 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 27 Aug 2025 15:13:41 -0500
Subject: [PATCH v19 1/2] Move dynamically-allocated tranche names to shared
 memory.

---
 contrib/pg_prewarm/autoprewarm.c              |   3 +-
 doc/src/sgml/xfunc.sgml                       |  15 +-
 src/backend/postmaster/launch_backend.c       |   6 +-
 src/backend/storage/ipc/dsm_registry.c        |  12 +-
 src/backend/storage/lmgr/lwlock.c             | 193 ++++++++----------
 src/include/storage/lwlock.h                  |  24 +--
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 11 files changed, 113 insertions(+), 167 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..880e897796a 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,7 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
 
 	return found;
 }
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..da21ef56891 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3759,7 +3759,7 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
       <literal>tranche_id</literal> by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3777,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index cd9547b03a3..de1b06df10d 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,7 +101,7 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	NamedLWLockTranche *NamedLWLockTrancheArray;
+	char	  **NamedLWLockTrancheNames;
 	int		   *LWLockCounter;
 	LWLockPadded *MainLWLockArray;
 	slock_t    *ProcStructLock;
@@ -761,7 +761,7 @@ save_backend_variables(BackendParameters *param,
 #endif
 
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
-	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
+	param->NamedLWLockTrancheNames = NamedLWLockTrancheNames;
 	param->LWLockCounter = LWLockCounter;
 	param->MainLWLockArray = MainLWLockArray;
 	param->ProcStructLock = ProcStructLock;
@@ -1022,7 +1022,7 @@ restore_backend_variables(BackendParameters *param)
 #endif
 
 	NamedLWLockTrancheRequests = param->NamedLWLockTrancheRequests;
-	NamedLWLockTrancheArray = param->NamedLWLockTrancheArray;
+	NamedLWLockTrancheNames = param->NamedLWLockTrancheNames;
 	LWLockCounter = param->LWLockCounter;
 	MainLWLockArray = param->MainLWLockArray;
 	ProcStructLock = param->ProcStructLock;
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index ca12815f4a8..97130925106 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -299,8 +299,7 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(state->tranche, name);
+		state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -321,9 +320,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -378,8 +374,7 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the hash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(dsh_state->tranche, name);
+		dsh_state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsh_state->tranche);
@@ -409,9 +404,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the hash table. */
-		LWLockRegisterTranche(dsh_state->tranche, name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsh_state->dsa_handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index a4aecd1fbc3..0e30dc1ba03 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -126,8 +126,8 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
  * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * or LWLockNewTrancheId.  These names are stored in shared memory and can be
+ * accessed via NamedLWLockTrancheNames.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -146,11 +146,12 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * points to the shared memory locations of the names of all
+ * dynamically-created tranches.  Backends inherit the pointer by fork from the
+ * postmaster (except in the EXEC_BACKEND case, where we have special measures
+ * to pass it down).
  */
-static const char **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
+char	  **NamedLWLockTrancheNames = NULL;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -184,20 +185,22 @@ typedef struct NamedLWLockTrancheRequest
 } NamedLWLockTrancheRequest;
 
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
-static int	NamedLWLockTrancheRequestsAllocated = 0;
 
 /*
- * NamedLWLockTrancheRequests is both the valid length of the request array,
- * and the length of the shared-memory NamedLWLockTrancheArray later on.
- * This variable and NamedLWLockTrancheArray are non-static so that
- * postmaster.c can copy them to child processes in EXEC_BACKEND builds.
+ * NamedLWLockTrancheRequests is the valid length of the request array.  This
+ * variable is non-static so that postmaster.c can copy them to child processes
+ * in EXEC_BACKEND builds.
  */
 int			NamedLWLockTrancheRequests = 0;
 
 /* points to data in shared memory: */
-NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 int		   *LWLockCounter = NULL;
 
+/* backend-local counter of registered tranches */
+static int	LocalLWLockCounter;
+
+#define MAX_NAMED_TRANCHES 256
+
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
@@ -392,7 +395,6 @@ Size
 LWLockShmemSize(void)
 {
 	Size		size;
-	int			i;
 	int			numLocks = NUM_FIXED_LWLOCKS;
 
 	/* Calculate total number of locks needed in the main array. */
@@ -405,18 +407,15 @@ LWLockShmemSize(void)
 	size = add_size(size, mul_size(numLocks, sizeof(LWLockPadded)));
 
 	/* space for named tranches. */
-	size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche)));
-
-	/* space for name of each tranche. */
-	for (i = 0; i < NamedLWLockTrancheRequests; i++)
-		size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1);
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, sizeof(char *)));
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, NAMEDATALEN));
 
 	return size;
 }
 
 /*
  * Allocate shmem space for the main LWLock array and all tranches and
- * initialize it.  We also register extension LWLock tranches here.
+ * initialize it.
  */
 void
 CreateLWLocks(void)
@@ -434,6 +433,15 @@ CreateLWLocks(void)
 		*LWLockCounter = LWTRANCHE_FIRST_USER_DEFINED;
 		ptr += sizeof(int);
 
+		/* Initialize tranche names */
+		NamedLWLockTrancheNames = (char **) ptr;
+		ptr += MAX_NAMED_TRANCHES * sizeof(char *);
+		for (int i = 0; i < MAX_NAMED_TRANCHES; i++)
+		{
+			NamedLWLockTrancheNames[i] = ptr;
+			ptr += NAMEDATALEN;
+		}
+
 		/* Ensure desired alignment of LWLock array */
 		ptr += LWLOCK_PADDED_SIZE - ((uintptr_t) ptr) % LWLOCK_PADDED_SIZE;
 		MainLWLockArray = (LWLockPadded *) ptr;
@@ -441,11 +449,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -454,7 +457,6 @@ CreateLWLocks(void)
 static void
 InitializeLWLocks(void)
 {
-	int			numNamedLocks = NumLWLocksForNamedTranches();
 	int			id;
 	int			i;
 	int			j;
@@ -485,32 +487,18 @@ InitializeLWLocks(void)
 	 */
 	if (NamedLWLockTrancheRequests > 0)
 	{
-		char	   *trancheNames;
-
-		NamedLWLockTrancheArray = (NamedLWLockTranche *)
-			&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
-
-		trancheNames = (char *) NamedLWLockTrancheArray +
-			(NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche));
 		lock = &MainLWLockArray[NUM_FIXED_LWLOCKS];
 
 		for (i = 0; i < NamedLWLockTrancheRequests; i++)
 		{
 			NamedLWLockTrancheRequest *request;
-			NamedLWLockTranche *tranche;
-			char	   *name;
+			int			tranche;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = &NamedLWLockTrancheArray[i];
-
-			name = trancheNames;
-			trancheNames += strlen(request->tranche_name) + 1;
-			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
-			tranche->trancheName = name;
+			tranche = LWLockNewTrancheId(request->tranche_name);
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
-				LWLockInitialize(&lock->lock, tranche->trancheId);
+				LWLockInitialize(&lock->lock, tranche);
 		}
 	}
 }
@@ -562,59 +550,44 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID with the provided name.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *name)
 {
 	int			result;
 
-	/* We use the ShmemLock spinlock to protect LWLockCounter */
-	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
-	SpinLockRelease(ShmemLock);
+	if (!name)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_NAME),
+				 errmsg("tranche name cannot be NULL")));
 
-	return result;
-}
+	if (strlen(name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("tranche name too long"),
+				 errdetail("LWLock tranche names must be no longer than %d bytes.",
+						   NAMEDATALEN - 1)));
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-{
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
-
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	/* We use the ShmemLock spinlock to protect LWLockCounter */
+	SpinLockAcquire(ShmemLock);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (*LWLockCounter - LWTRANCHE_FIRST_USER_DEFINED >= MAX_NAMED_TRANCHES)
 	{
-		int			newalloc;
+		SpinLockRelease(ShmemLock);
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("No more than %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
+	}
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	result = (*LWLockCounter)++;
+	LocalLWLockCounter = *LWLockCounter;
+	strlcpy(NamedLWLockTrancheNames[result - LWTRANCHE_FIRST_USER_DEFINED], name, NAMEDATALEN);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
-	}
+	SpinLockRelease(ShmemLock);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	return result;
 }
 
 /*
@@ -637,27 +610,33 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 	if (!process_shmem_requests_in_progress)
 		elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook");
 
+	if (!tranche_name)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_NAME),
+				 errmsg("tranche name cannot be NULL")));
+
+	if (strlen(tranche_name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("tranche name too long"),
+				 errdetail("LWLock tranche names must be no longer than %d bytes.",
+						   NAMEDATALEN - 1)));
+
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
-		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
-							   NamedLWLockTrancheRequestsAllocated
+							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
-	{
-		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
-
-		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
-			repalloc(NamedLWLockTrancheRequestArray,
-					 i * sizeof(NamedLWLockTrancheRequest));
-		NamedLWLockTrancheRequestsAllocated = i;
-	}
+	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("No more than %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
-	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
 	strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);
 	request->num_lwlocks = num_lwlocks;
 	NamedLWLockTrancheRequests++;
@@ -669,6 +648,9 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	/* verify the tranche_id is valid */
+	(void) GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -709,18 +691,23 @@ GetLWTrancheName(uint16 trancheId)
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
+	/* verify the trancheId is valid */
+	if (trancheId >= LocalLWLockCounter)
+	{
+		SpinLockAcquire(ShmemLock);
+		LocalLWLockCounter = *LWLockCounter;
+		SpinLockRelease(ShmemLock);
+
+		if (trancheId >= LocalLWLockCounter)
+			elog(ERROR, "tranche %d is not registered", trancheId);
+	}
+
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in NamedLWLockTrancheNames.
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
-
-	return LWLockTrancheNames[trancheId];
+	return NamedLWLockTrancheNames[trancheId];
 }
 
 /*
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index f9cf57f8d26..3877aaa7f03 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -73,14 +73,7 @@ typedef union LWLockPadded
 
 extern PGDLLIMPORT LWLockPadded *MainLWLockArray;
 
-/* struct for storing named tranche information */
-typedef struct NamedLWLockTranche
-{
-	int			trancheId;
-	char	   *trancheName;
-} NamedLWLockTranche;
-
-extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
+extern PGDLLIMPORT char **NamedLWLockTrancheNames;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 extern PGDLLIMPORT int *LWLockCounter;
 
@@ -158,18 +151,11 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
  * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * LWLockNewTrancheId to obtain a tranche ID; this allocates from a shared
+ * counter.  Second, LWLockInitialize should be called just once per lwlock,
+ * passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
+extern int	LWLockNewTrancheId(const char *name);
 extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

v19-0002-Tests-for-LWLock-tranche-registration-improvemen.patchtext/plain; charset=us-asciiDownload
From 5b0b16b75c4d0e0bbc00c8f91aab488f896b804f Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 22 Aug 2025 00:35:45 -0500
Subject: [PATCH v19 2/2] Tests for LWLock tranche registration improvements

---
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_tranches/.gitignore     |   4 +
 src/test/modules/test_tranches/Makefile       |  23 +++
 src/test/modules/test_tranches/meson.build    |  33 +++
 .../test_tranches/t/001_test_tranches.pl      | 120 +++++++++++
 .../test_tranches/test_tranches--1.0.sql      |  35 ++++
 .../modules/test_tranches/test_tranches.c     | 191 ++++++++++++++++++
 .../test_tranches/test_tranches.control       |   6 +
 9 files changed, 415 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/test_tranches/.gitignore
 create mode 100644 src/test/modules/test_tranches/Makefile
 create mode 100644 src/test/modules/test_tranches/meson.build
 create mode 100644 src/test/modules/test_tranches/t/001_test_tranches.pl
 create mode 100644 src/test/modules/test_tranches/test_tranches--1.0.sql
 create mode 100644 src/test/modules/test_tranches/test_tranches.c
 create mode 100644 src/test/modules/test_tranches/test_tranches.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..fd7aa4675c5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..52883dfb496 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_tranches')
diff --git a/src/test/modules/test_tranches/.gitignore b/src/test/modules/test_tranches/.gitignore
new file mode 100644
index 00000000000..5dcb3ff9723
--- /dev/null
+++ b/src/test/modules/test_tranches/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_tranches/Makefile b/src/test/modules/test_tranches/Makefile
new file mode 100644
index 00000000000..5c9e78084da
--- /dev/null
+++ b/src/test/modules/test_tranches/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_tranches/Makefile
+
+MODULE_big = test_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_tranches.o
+PGFILEDESC = "test_tranches - test code LWLock tranche management"
+
+EXTENSION = test_tranches
+DATA = test_tranches--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_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_tranches/meson.build b/src/test/modules/test_tranches/meson.build
new file mode 100644
index 00000000000..976ce9f0a25
--- /dev/null
+++ b/src/test/modules/test_tranches/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_tranches_sources = files(
+  'test_tranches.c',
+)
+
+if host_system == 'windows'
+  test_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_tranches',
+    '--FILEDESC', 'test_tranches - test code LWLock tranche management',])
+endif
+
+test_tranches = shared_module('test_tranches',
+  test_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_tranches
+
+test_install_data += files(
+  'test_tranches.control',
+  'test_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_test_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/t/001_test_tranches.pl b/src/test/modules/test_tranches/t/001_test_tranches.pl
new file mode 100644
index 00000000000..477035bdb99
--- /dev/null
+++ b/src/test/modules/test_tranches/t/001_test_tranches.pl
@@ -0,0 +1,120 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+#
+# Create the cluster without any requested tranches
+#
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=0));
+$node->start();
+$node->safe_psql('postgres', q(CREATE EXTENSION test_tranches));
+
+my $first_user_defined = $node->safe_psql('postgres', q(select test_tranches_get_first_user_defined()));
+my @user_defined = ($first_user_defined .. $first_user_defined + 5);
+my ($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+#
+# Run lookup tests to ensure the correct tranche names are returned
+# and an error is raised for unregistered tranches
+#
+$node->safe_psql('postgres', "select test_tranches_new(5);");
+my ($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_lookup(1);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche 100 is not registered/, "unregistered tranche error");
+like("$stdout",
+     qr/ShmemIndex.*test_lock__0.*test_lock__1.*test_lock__2.*test_lock__3.*test_lock__4/s,
+     "match tranche names without requested tranches");
+
+#
+# Test the error for long tranche names
+#
+my $good_tranche_name = 'A' x 63;
+my $bad_tranche_name = 'B' x 64; # MAX_NAMED_TRANCHES_NAME_LEN
+$node->safe_psql('postgres', qq{select test_tranches_new_tranche('$good_tranche_name');});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_new_tranche('$bad_tranche_name');
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche name too long/, "tranche name too long");
+
+$node->restart();
+
+#
+# Test the error when > MAX_NAMED_TRANCHES named tranches registered.
+#
+$node->safe_psql('postgres', qq{select test_tranches_new(255)});
+$node->safe_psql('postgres', qq{select test_tranches_new(1)});
+($result, $stdout, $stderr) = $node->psql('postgres', qq{select test_tranches_new(1);}, on_error_stop => 0);
+like("$stderr", qr/ERROR:  maximum number of tranches already registered/, "too many tranches registered");
+
+#
+# Repeat the lookup test with 2 requested tranches
+#
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=2));
+$node->restart();
+
+$first_user_defined = $first_user_defined;
+@user_defined = ($first_user_defined .. $first_user_defined + 5);
+($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+
+$node->safe_psql('postgres', "select test_tranches_new(3);");
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_lookup(1);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche 100 is not registered/, "unregistered tranche error");
+like("$stdout", qr/ShmemIndex.*test_lock_0.*test_lock_1.*test_lock__2.*test_lock__3.*test_lock__4/s,
+     "match tranche names with requested tranches");
+
+#
+# Test lwlock initialize
+#
+$node->safe_psql('postgres', qq{select test_tranches_lwlock_initialize($first_user_defined)});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{select test_tranches_lwlock_initialize($sixth_user_defined)},
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche 100 is not registered/, "LWLock intialization error on invalid tranche name");
+
+#
+# Test error for NULL tranche name
+#
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{select test_tranches_new_tranche(NULL)},
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche name cannot be NULL/, "NULL tranche name");
+
+done_testing();
diff --git a/src/test/modules/test_tranches/test_tranches--1.0.sql b/src/test/modules/test_tranches/test_tranches--1.0.sql
new file mode 100644
index 00000000000..f504098e04a
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches--1.0.sql
@@ -0,0 +1,35 @@
+-- test_tranches--1.0.sql
+
+CREATE FUNCTION test_tranches_new(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_new'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lookup(int)
+RETURNS text
+AS 'MODULE_PATHNAME', 'test_tranches_lookup'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_named_lwlock(text, int)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_named_lwlock'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lwlock_initialize(int)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_lwlock_initialize'
+LANGUAGE C STRICT;
+
+/*
+ * Function is CALLED ON NULL INPUT to allow NULL input
+ * for testing
+ */
+CREATE FUNCTION test_tranches_new_tranche(text)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_new_tranche'
+LANGUAGE C;
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/test_tranches.c b/src/test/modules/test_tranches/test_tranches.c
new file mode 100644
index 00000000000..7d3031ace9f
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.c
@@ -0,0 +1,191 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_tranches_shmem_request(void);
+static void test_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_tranches_requested_named_tranches = 0;
+
+typedef struct testTranchesSharedState
+{
+	int			next_index;
+}			testTranchesSharedState;
+
+static testTranchesSharedState * test_lwlock_ss = NULL;
+
+/*
+ * LWLock wait event masks. Copied from src/backend/utils/activity/wait_event.c
+ */
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_tranches_shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	test_lwlock_ss = NULL;
+
+	test_lwlock_ss = ShmemInitStruct("test_tranches",
+									 sizeof(testTranchesSharedState),
+									 &found);
+	if (!found)
+	{
+		test_lwlock_ss->next_index = test_tranches_requested_named_tranches;
+	}
+}
+
+static Size
+test_tranches_memsize(void)
+{
+	Size		size;
+
+	size = MAXALIGN(sizeof(testTranchesSharedState));
+
+	return size;
+}
+
+static void
+test_tranches_shmem_request(void)
+{
+	int			i = 0;
+
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(test_tranches_memsize());
+
+	for (i = 0; i < test_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new);
+Datum
+test_tranches_new(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+	int			i;
+
+	for (i = test_lwlock_ss->next_index; i < num + test_lwlock_ss->next_index; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	test_lwlock_ss->next_index = i;
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new_tranche);
+Datum
+test_tranches_new_tranche(PG_FUNCTION_ARGS)
+{
+	char	   *tranche_name = NULL;
+
+	if (!PG_ARGISNULL(0))
+		tranche_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+
+	PG_RETURN_INT32(LWLockNewTrancheId(tranche_name));
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lookup);
+Datum
+test_tranches_lookup(PG_FUNCTION_ARGS)
+{
+	const char *tranche_name = GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	if (tranche_name)
+		PG_RETURN_TEXT_P(cstring_to_text(tranche_name));
+	else
+		PG_RETURN_NULL();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_named_lwlock);
+Datum
+test_tranches_get_named_lwlock(PG_FUNCTION_ARGS)
+{
+	int			i;
+	LWLockPadded *locks;
+
+	locks = GetNamedLWLockTranche(text_to_cstring(PG_GETARG_TEXT_PP(0)));
+
+	for (i = 0; i < PG_GETARG_INT32(1); i++)
+	{
+		LWLock	   *lock = &locks[i].lock;
+
+		LWLockAcquire(lock, LW_SHARED);
+		LWLockRelease(lock);
+	}
+
+	PG_RETURN_INT32(i);
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_first_user_defined);
+Datum
+test_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lwlock_initialize);
+Datum
+test_tranches_lwlock_initialize(PG_FUNCTION_ARGS)
+{
+	LWLock		lock;
+
+	LWLockInitialize(&lock, PG_GETARG_INT32(0));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_tranches/test_tranches.control b/src/test/modules/test_tranches/test_tranches.control
new file mode 100644
index 00000000000..8e6254a7e43
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.control
@@ -0,0 +1,6 @@
+# test_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_tranches'
\ No newline at end of file
-- 
2.39.5 (Apple Git-154)

#85Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#84)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

On Fri, Aug 29, 2025 at 09:51:38PM -0500, Nathan Bossart wrote:

I've also attached a rebased patch that addresses all the latest feedback.
A reworked verison of the test patch is also included, but that's mostly
intended for CI purposes and is still not intended for commit (yet).

And here's an attempt at fixing the alignment problems revealed by cfbot.

--
nathan

Attachments:

v20-0001-Move-dynamically-allocated-tranche-names-to-shar.patchtext/plain; charset=us-asciiDownload
From 72260a5f2fd93c77927a50aaf521c9f294d58ac7 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 27 Aug 2025 15:13:41 -0500
Subject: [PATCH v20 1/2] Move dynamically-allocated tranche names to shared
 memory.

---
 contrib/pg_prewarm/autoprewarm.c              |   3 +-
 doc/src/sgml/xfunc.sgml                       |  15 +-
 src/backend/postmaster/launch_backend.c       |   6 +-
 src/backend/storage/ipc/dsm_registry.c        |  12 +-
 src/backend/storage/lmgr/lwlock.c             | 206 +++++++++---------
 src/include/storage/lwlock.h                  |  24 +-
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 11 files changed, 120 insertions(+), 173 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..880e897796a 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,7 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
 
 	return found;
 }
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..da21ef56891 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3759,7 +3759,7 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
       <literal>tranche_id</literal> by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3777,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index cd9547b03a3..de1b06df10d 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,7 +101,7 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	NamedLWLockTranche *NamedLWLockTrancheArray;
+	char	  **NamedLWLockTrancheNames;
 	int		   *LWLockCounter;
 	LWLockPadded *MainLWLockArray;
 	slock_t    *ProcStructLock;
@@ -761,7 +761,7 @@ save_backend_variables(BackendParameters *param,
 #endif
 
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
-	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
+	param->NamedLWLockTrancheNames = NamedLWLockTrancheNames;
 	param->LWLockCounter = LWLockCounter;
 	param->MainLWLockArray = MainLWLockArray;
 	param->ProcStructLock = ProcStructLock;
@@ -1022,7 +1022,7 @@ restore_backend_variables(BackendParameters *param)
 #endif
 
 	NamedLWLockTrancheRequests = param->NamedLWLockTrancheRequests;
-	NamedLWLockTrancheArray = param->NamedLWLockTrancheArray;
+	NamedLWLockTrancheNames = param->NamedLWLockTrancheNames;
 	LWLockCounter = param->LWLockCounter;
 	MainLWLockArray = param->MainLWLockArray;
 	ProcStructLock = param->ProcStructLock;
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index ca12815f4a8..97130925106 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -299,8 +299,7 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(state->tranche, name);
+		state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -321,9 +320,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -378,8 +374,7 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the hash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(dsh_state->tranche, name);
+		dsh_state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsh_state->tranche);
@@ -409,9 +404,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the hash table. */
-		LWLockRegisterTranche(dsh_state->tranche, name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsh_state->dsa_handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index a4aecd1fbc3..b4636d86ca6 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -126,8 +126,8 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
  * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * or LWLockNewTrancheId.  These names are stored in shared memory and can be
+ * accessed via NamedLWLockTrancheNames.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -146,11 +146,12 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * points to the shared memory locations of the names of all
+ * dynamically-created tranches.  Backends inherit the pointer by fork from the
+ * postmaster (except in the EXEC_BACKEND case, where we have special measures
+ * to pass it down).
  */
-static const char **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
+char	  **NamedLWLockTrancheNames = NULL;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -184,20 +185,22 @@ typedef struct NamedLWLockTrancheRequest
 } NamedLWLockTrancheRequest;
 
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
-static int	NamedLWLockTrancheRequestsAllocated = 0;
 
 /*
- * NamedLWLockTrancheRequests is both the valid length of the request array,
- * and the length of the shared-memory NamedLWLockTrancheArray later on.
- * This variable and NamedLWLockTrancheArray are non-static so that
- * postmaster.c can copy them to child processes in EXEC_BACKEND builds.
+ * NamedLWLockTrancheRequests is the valid length of the request array.  This
+ * variable is non-static so that postmaster.c can copy them to child processes
+ * in EXEC_BACKEND builds.
  */
 int			NamedLWLockTrancheRequests = 0;
 
 /* points to data in shared memory: */
-NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
 int		   *LWLockCounter = NULL;
 
+/* backend-local counter of registered tranches */
+static int	LocalLWLockCounter;
+
+#define MAX_NAMED_TRANCHES 256
+
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
@@ -392,31 +395,28 @@ Size
 LWLockShmemSize(void)
 {
 	Size		size;
-	int			i;
 	int			numLocks = NUM_FIXED_LWLOCKS;
 
 	/* Calculate total number of locks needed in the main array. */
 	numLocks += NumLWLocksForNamedTranches();
 
-	/* Space for dynamic allocation counter, plus room for alignment. */
-	size = sizeof(int) + LWLOCK_PADDED_SIZE;
+	/* Space for dynamic allocation counter. */
+	size = MAXALIGN(sizeof(int));
 
-	/* Space for the LWLock array. */
-	size = add_size(size, mul_size(numLocks, sizeof(LWLockPadded)));
+	/* Space for named tranches. */
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, sizeof(char *)));
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, NAMEDATALEN));
 
-	/* space for named tranches. */
-	size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche)));
-
-	/* space for name of each tranche. */
-	for (i = 0; i < NamedLWLockTrancheRequests; i++)
-		size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1);
+	/* Space for the LWLock array, plus room for cache line alignment. */
+	size = add_size(size, LWLOCK_PADDED_SIZE);
+	size = add_size(size, mul_size(numLocks, sizeof(LWLockPadded)));
 
 	return size;
 }
 
 /*
  * Allocate shmem space for the main LWLock array and all tranches and
- * initialize it.  We also register extension LWLock tranches here.
+ * initialize it.
  */
 void
 CreateLWLocks(void)
@@ -432,7 +432,16 @@ CreateLWLocks(void)
 		/* Initialize the dynamic-allocation counter for tranches */
 		LWLockCounter = (int *) ptr;
 		*LWLockCounter = LWTRANCHE_FIRST_USER_DEFINED;
-		ptr += sizeof(int);
+		ptr += MAXALIGN(sizeof(int));
+
+		/* Initialize tranche names */
+		NamedLWLockTrancheNames = (char **) ptr;
+		ptr += MAX_NAMED_TRANCHES * sizeof(char *);
+		for (int i = 0; i < MAX_NAMED_TRANCHES; i++)
+		{
+			NamedLWLockTrancheNames[i] = ptr;
+			ptr += NAMEDATALEN;
+		}
 
 		/* Ensure desired alignment of LWLock array */
 		ptr += LWLOCK_PADDED_SIZE - ((uintptr_t) ptr) % LWLOCK_PADDED_SIZE;
@@ -441,11 +450,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -454,7 +458,6 @@ CreateLWLocks(void)
 static void
 InitializeLWLocks(void)
 {
-	int			numNamedLocks = NumLWLocksForNamedTranches();
 	int			id;
 	int			i;
 	int			j;
@@ -485,32 +488,18 @@ InitializeLWLocks(void)
 	 */
 	if (NamedLWLockTrancheRequests > 0)
 	{
-		char	   *trancheNames;
-
-		NamedLWLockTrancheArray = (NamedLWLockTranche *)
-			&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
-
-		trancheNames = (char *) NamedLWLockTrancheArray +
-			(NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche));
 		lock = &MainLWLockArray[NUM_FIXED_LWLOCKS];
 
 		for (i = 0; i < NamedLWLockTrancheRequests; i++)
 		{
 			NamedLWLockTrancheRequest *request;
-			NamedLWLockTranche *tranche;
-			char	   *name;
+			int			tranche;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = &NamedLWLockTrancheArray[i];
-
-			name = trancheNames;
-			trancheNames += strlen(request->tranche_name) + 1;
-			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
-			tranche->trancheName = name;
+			tranche = LWLockNewTrancheId(request->tranche_name);
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
-				LWLockInitialize(&lock->lock, tranche->trancheId);
+				LWLockInitialize(&lock->lock, tranche);
 		}
 	}
 }
@@ -562,59 +551,44 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID with the provided name.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *name)
 {
 	int			result;
 
-	/* We use the ShmemLock spinlock to protect LWLockCounter */
-	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
-	SpinLockRelease(ShmemLock);
+	if (!name)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_NAME),
+				 errmsg("tranche name cannot be NULL")));
 
-	return result;
-}
+	if (strlen(name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("tranche name too long"),
+				 errdetail("LWLock tranche names must be no longer than %d bytes.",
+						   NAMEDATALEN - 1)));
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-{
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
-
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	/* We use the ShmemLock spinlock to protect LWLockCounter */
+	SpinLockAcquire(ShmemLock);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (*LWLockCounter - LWTRANCHE_FIRST_USER_DEFINED >= MAX_NAMED_TRANCHES)
 	{
-		int			newalloc;
+		SpinLockRelease(ShmemLock);
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("No more than %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
+	}
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	result = (*LWLockCounter)++;
+	LocalLWLockCounter = *LWLockCounter;
+	strlcpy(NamedLWLockTrancheNames[result - LWTRANCHE_FIRST_USER_DEFINED], name, NAMEDATALEN);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
-	}
+	SpinLockRelease(ShmemLock);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	return result;
 }
 
 /*
@@ -637,27 +611,33 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 	if (!process_shmem_requests_in_progress)
 		elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook");
 
+	if (!tranche_name)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_NAME),
+				 errmsg("tranche name cannot be NULL")));
+
+	if (strlen(tranche_name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("tranche name too long"),
+				 errdetail("LWLock tranche names must be no longer than %d bytes.",
+						   NAMEDATALEN - 1)));
+
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
-		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
-							   NamedLWLockTrancheRequestsAllocated
+							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
-	{
-		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
-
-		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
-			repalloc(NamedLWLockTrancheRequestArray,
-					 i * sizeof(NamedLWLockTrancheRequest));
-		NamedLWLockTrancheRequestsAllocated = i;
-	}
+	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("No more than %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
-	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
 	strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);
 	request->num_lwlocks = num_lwlocks;
 	NamedLWLockTrancheRequests++;
@@ -669,6 +649,9 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	/* verify the tranche_id is valid */
+	(void) GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -709,18 +692,23 @@ GetLWTrancheName(uint16 trancheId)
 	if (trancheId < LWTRANCHE_FIRST_USER_DEFINED)
 		return BuiltinTrancheNames[trancheId];
 
+	/* verify the trancheId is valid */
+	if (trancheId >= LocalLWLockCounter)
+	{
+		SpinLockAcquire(ShmemLock);
+		LocalLWLockCounter = *LWLockCounter;
+		SpinLockRelease(ShmemLock);
+
+		if (trancheId >= LocalLWLockCounter)
+			elog(ERROR, "tranche %d is not registered", trancheId);
+	}
+
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * It's an extension tranche, so look in NamedLWLockTrancheNames.
 	 */
 	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
-
-	return LWLockTrancheNames[trancheId];
+	return NamedLWLockTrancheNames[trancheId];
 }
 
 /*
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index f9cf57f8d26..3877aaa7f03 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -73,14 +73,7 @@ typedef union LWLockPadded
 
 extern PGDLLIMPORT LWLockPadded *MainLWLockArray;
 
-/* struct for storing named tranche information */
-typedef struct NamedLWLockTranche
-{
-	int			trancheId;
-	char	   *trancheName;
-} NamedLWLockTranche;
-
-extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
+extern PGDLLIMPORT char **NamedLWLockTrancheNames;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 extern PGDLLIMPORT int *LWLockCounter;
 
@@ -158,18 +151,11 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
  * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * LWLockNewTrancheId to obtain a tranche ID; this allocates from a shared
+ * counter.  Second, LWLockInitialize should be called just once per lwlock,
+ * passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
+extern int	LWLockNewTrancheId(const char *name);
 extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

v20-0002-Tests-for-LWLock-tranche-registration-improvemen.patchtext/plain; charset=us-asciiDownload
From 65c80071e74c36328873dfa9cf6ce8f537d83850 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 22 Aug 2025 00:35:45 -0500
Subject: [PATCH v20 2/2] Tests for LWLock tranche registration improvements

---
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_tranches/.gitignore     |   4 +
 src/test/modules/test_tranches/Makefile       |  23 +++
 src/test/modules/test_tranches/meson.build    |  33 +++
 .../test_tranches/t/001_test_tranches.pl      | 120 +++++++++++
 .../test_tranches/test_tranches--1.0.sql      |  35 ++++
 .../modules/test_tranches/test_tranches.c     | 191 ++++++++++++++++++
 .../test_tranches/test_tranches.control       |   6 +
 9 files changed, 415 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/test_tranches/.gitignore
 create mode 100644 src/test/modules/test_tranches/Makefile
 create mode 100644 src/test/modules/test_tranches/meson.build
 create mode 100644 src/test/modules/test_tranches/t/001_test_tranches.pl
 create mode 100644 src/test/modules/test_tranches/test_tranches--1.0.sql
 create mode 100644 src/test/modules/test_tranches/test_tranches.c
 create mode 100644 src/test/modules/test_tranches/test_tranches.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..fd7aa4675c5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..52883dfb496 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_tranches')
diff --git a/src/test/modules/test_tranches/.gitignore b/src/test/modules/test_tranches/.gitignore
new file mode 100644
index 00000000000..5dcb3ff9723
--- /dev/null
+++ b/src/test/modules/test_tranches/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_tranches/Makefile b/src/test/modules/test_tranches/Makefile
new file mode 100644
index 00000000000..5c9e78084da
--- /dev/null
+++ b/src/test/modules/test_tranches/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_tranches/Makefile
+
+MODULE_big = test_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_tranches.o
+PGFILEDESC = "test_tranches - test code LWLock tranche management"
+
+EXTENSION = test_tranches
+DATA = test_tranches--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_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_tranches/meson.build b/src/test/modules/test_tranches/meson.build
new file mode 100644
index 00000000000..976ce9f0a25
--- /dev/null
+++ b/src/test/modules/test_tranches/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_tranches_sources = files(
+  'test_tranches.c',
+)
+
+if host_system == 'windows'
+  test_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_tranches',
+    '--FILEDESC', 'test_tranches - test code LWLock tranche management',])
+endif
+
+test_tranches = shared_module('test_tranches',
+  test_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_tranches
+
+test_install_data += files(
+  'test_tranches.control',
+  'test_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_test_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/t/001_test_tranches.pl b/src/test/modules/test_tranches/t/001_test_tranches.pl
new file mode 100644
index 00000000000..477035bdb99
--- /dev/null
+++ b/src/test/modules/test_tranches/t/001_test_tranches.pl
@@ -0,0 +1,120 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+#
+# Create the cluster without any requested tranches
+#
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=0));
+$node->start();
+$node->safe_psql('postgres', q(CREATE EXTENSION test_tranches));
+
+my $first_user_defined = $node->safe_psql('postgres', q(select test_tranches_get_first_user_defined()));
+my @user_defined = ($first_user_defined .. $first_user_defined + 5);
+my ($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+#
+# Run lookup tests to ensure the correct tranche names are returned
+# and an error is raised for unregistered tranches
+#
+$node->safe_psql('postgres', "select test_tranches_new(5);");
+my ($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_lookup(1);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche 100 is not registered/, "unregistered tranche error");
+like("$stdout",
+     qr/ShmemIndex.*test_lock__0.*test_lock__1.*test_lock__2.*test_lock__3.*test_lock__4/s,
+     "match tranche names without requested tranches");
+
+#
+# Test the error for long tranche names
+#
+my $good_tranche_name = 'A' x 63;
+my $bad_tranche_name = 'B' x 64; # MAX_NAMED_TRANCHES_NAME_LEN
+$node->safe_psql('postgres', qq{select test_tranches_new_tranche('$good_tranche_name');});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_new_tranche('$bad_tranche_name');
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche name too long/, "tranche name too long");
+
+$node->restart();
+
+#
+# Test the error when > MAX_NAMED_TRANCHES named tranches registered.
+#
+$node->safe_psql('postgres', qq{select test_tranches_new(255)});
+$node->safe_psql('postgres', qq{select test_tranches_new(1)});
+($result, $stdout, $stderr) = $node->psql('postgres', qq{select test_tranches_new(1);}, on_error_stop => 0);
+like("$stderr", qr/ERROR:  maximum number of tranches already registered/, "too many tranches registered");
+
+#
+# Repeat the lookup test with 2 requested tranches
+#
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=2));
+$node->restart();
+
+$first_user_defined = $first_user_defined;
+@user_defined = ($first_user_defined .. $first_user_defined + 5);
+($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+
+$node->safe_psql('postgres', "select test_tranches_new(3);");
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_lookup(1);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche 100 is not registered/, "unregistered tranche error");
+like("$stdout", qr/ShmemIndex.*test_lock_0.*test_lock_1.*test_lock__2.*test_lock__3.*test_lock__4/s,
+     "match tranche names with requested tranches");
+
+#
+# Test lwlock initialize
+#
+$node->safe_psql('postgres', qq{select test_tranches_lwlock_initialize($first_user_defined)});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{select test_tranches_lwlock_initialize($sixth_user_defined)},
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche 100 is not registered/, "LWLock intialization error on invalid tranche name");
+
+#
+# Test error for NULL tranche name
+#
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{select test_tranches_new_tranche(NULL)},
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche name cannot be NULL/, "NULL tranche name");
+
+done_testing();
diff --git a/src/test/modules/test_tranches/test_tranches--1.0.sql b/src/test/modules/test_tranches/test_tranches--1.0.sql
new file mode 100644
index 00000000000..f504098e04a
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches--1.0.sql
@@ -0,0 +1,35 @@
+-- test_tranches--1.0.sql
+
+CREATE FUNCTION test_tranches_new(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_new'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lookup(int)
+RETURNS text
+AS 'MODULE_PATHNAME', 'test_tranches_lookup'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_named_lwlock(text, int)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_named_lwlock'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lwlock_initialize(int)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_lwlock_initialize'
+LANGUAGE C STRICT;
+
+/*
+ * Function is CALLED ON NULL INPUT to allow NULL input
+ * for testing
+ */
+CREATE FUNCTION test_tranches_new_tranche(text)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_new_tranche'
+LANGUAGE C;
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/test_tranches.c b/src/test/modules/test_tranches/test_tranches.c
new file mode 100644
index 00000000000..7d3031ace9f
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.c
@@ -0,0 +1,191 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_tranches_shmem_request(void);
+static void test_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_tranches_requested_named_tranches = 0;
+
+typedef struct testTranchesSharedState
+{
+	int			next_index;
+}			testTranchesSharedState;
+
+static testTranchesSharedState * test_lwlock_ss = NULL;
+
+/*
+ * LWLock wait event masks. Copied from src/backend/utils/activity/wait_event.c
+ */
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_tranches_shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	test_lwlock_ss = NULL;
+
+	test_lwlock_ss = ShmemInitStruct("test_tranches",
+									 sizeof(testTranchesSharedState),
+									 &found);
+	if (!found)
+	{
+		test_lwlock_ss->next_index = test_tranches_requested_named_tranches;
+	}
+}
+
+static Size
+test_tranches_memsize(void)
+{
+	Size		size;
+
+	size = MAXALIGN(sizeof(testTranchesSharedState));
+
+	return size;
+}
+
+static void
+test_tranches_shmem_request(void)
+{
+	int			i = 0;
+
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(test_tranches_memsize());
+
+	for (i = 0; i < test_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new);
+Datum
+test_tranches_new(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+	int			i;
+
+	for (i = test_lwlock_ss->next_index; i < num + test_lwlock_ss->next_index; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	test_lwlock_ss->next_index = i;
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new_tranche);
+Datum
+test_tranches_new_tranche(PG_FUNCTION_ARGS)
+{
+	char	   *tranche_name = NULL;
+
+	if (!PG_ARGISNULL(0))
+		tranche_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+
+	PG_RETURN_INT32(LWLockNewTrancheId(tranche_name));
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lookup);
+Datum
+test_tranches_lookup(PG_FUNCTION_ARGS)
+{
+	const char *tranche_name = GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	if (tranche_name)
+		PG_RETURN_TEXT_P(cstring_to_text(tranche_name));
+	else
+		PG_RETURN_NULL();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_named_lwlock);
+Datum
+test_tranches_get_named_lwlock(PG_FUNCTION_ARGS)
+{
+	int			i;
+	LWLockPadded *locks;
+
+	locks = GetNamedLWLockTranche(text_to_cstring(PG_GETARG_TEXT_PP(0)));
+
+	for (i = 0; i < PG_GETARG_INT32(1); i++)
+	{
+		LWLock	   *lock = &locks[i].lock;
+
+		LWLockAcquire(lock, LW_SHARED);
+		LWLockRelease(lock);
+	}
+
+	PG_RETURN_INT32(i);
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_first_user_defined);
+Datum
+test_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lwlock_initialize);
+Datum
+test_tranches_lwlock_initialize(PG_FUNCTION_ARGS)
+{
+	LWLock		lock;
+
+	LWLockInitialize(&lock, PG_GETARG_INT32(0));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_tranches/test_tranches.control b/src/test/modules/test_tranches/test_tranches.control
new file mode 100644
index 00000000000..8e6254a7e43
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.control
@@ -0,0 +1,6 @@
+# test_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_tranches'
\ No newline at end of file
-- 
2.39.5 (Apple Git-154)

#86Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#85)
Re: Improve LWLock tranche name visibility across backends

I've also attached a rebased patch that addresses all the latest feedback.
A reworked verison of the test patch is also included, but that's mostly
intended for CI purposes and is still not intended for commit (yet).

And here's an attempt at fixing the alignment problems revealed by cfbot.

This LGTM. Once we get this committed, will follow-up on [0]/messages/by-id/CAA5RZ0v1_15QPg5Sqd2Qz5rh_qcsyCeHHmRDY89xVHcy2yt5BQ@mail.gmail.com

[0]: /messages/by-id/CAA5RZ0v1_15QPg5Sqd2Qz5rh_qcsyCeHHmRDY89xVHcy2yt5BQ@mail.gmail.com

--
Sami

#87Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Nathan Bossart (#85)
1 attachment(s)
Re: Improve LWLock tranche name visibility across backends

Hi,

On Sat, Aug 30, 2025 at 09:14:46AM -0500, Nathan Bossart wrote:

On Fri, Aug 29, 2025 at 09:51:38PM -0500, Nathan Bossart wrote:

I've also attached a rebased patch that addresses all the latest feedback.
A reworked verison of the test patch is also included, but that's mostly
intended for CI purposes and is still not intended for commit (yet).

And here's an attempt at fixing the alignment problems revealed by cfbot.

Indeed:

==24409==Using libbacktrace symbolizer.
../src/backend/storage/lmgr/lwlock.c:441:31: runtime error: store to misaligned address 0x7f7644188904 for type 'char *', which requires 8 byte alignment

Changes look good.

Not directly related, but I think that we can get rid of:

size = add_size(size, LWLOCK_PADDED_SIZE);

in LWLockShmemSize() and of:

ptr += LWLOCK_PADDED_SIZE - ((uintptr_t) ptr) % LWLOCK_PADDED_SIZE;

in CreateLWLocks(), and just make use of CACHELINEALIGN().

Attached, a patch doing so. It applies on top of your v20.

Regards,

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

Attachments:

v20-0003-Make-use-of-CACHELINEALIGN-in-LWLockShmemSize-an.txttext/plain; charset=us-asciiDownload
From 4720ffe5c804c6ad2f4ebddcc9e5e35ee7890360 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Mon, 1 Sep 2025 07:30:19 +0000
Subject: [PATCH v20 3/3] Make use of CACHELINEALIGN in LWLockShmemSize() and
 CreateLWLocks()

Let's get rid of the LWLOCK_PADDED_SIZE usage in those functions and use
CACHELINEALIGN() that perfectly fits with our needs. The current approach was
wasting some bytes.
---
 src/backend/storage/lmgr/lwlock.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
 100.0% src/backend/storage/lmgr/

diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index b4636d86ca6..2443dfd3d59 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -408,7 +408,7 @@ LWLockShmemSize(void)
 	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, NAMEDATALEN));
 
 	/* Space for the LWLock array, plus room for cache line alignment. */
-	size = add_size(size, LWLOCK_PADDED_SIZE);
+	size = CACHELINEALIGN(size);
 	size = add_size(size, mul_size(numLocks, sizeof(LWLockPadded)));
 
 	return size;
@@ -444,7 +444,7 @@ CreateLWLocks(void)
 		}
 
 		/* Ensure desired alignment of LWLock array */
-		ptr += LWLOCK_PADDED_SIZE - ((uintptr_t) ptr) % LWLOCK_PADDED_SIZE;
+		ptr = (char *) CACHELINEALIGN(ptr);
 		MainLWLockArray = (LWLockPadded *) ptr;
 
 		/* Initialize all LWLocks */
-- 
2.34.1

#88Rahila Syed
rahilasyed90@gmail.com
In reply to: Nathan Bossart (#84)
Re: Improve LWLock tranche name visibility across backends

Hi,

I've also attached a rebased patch that addresses all the latest feedback.
A reworked verison of the test patch is also included, but that's mostly
intended for CI purposes and is still not intended for commit (yet).

Please see below for some comments regarding v20 patch.

1. Following unused declaration is present in the lwlock.h file.

extern void LWLockRegisterTranche(int tranche_id, const char
*tranche_name);

2. I am struggling to understand the use of LocalLWLockCounter in the patch.

If the intention is to delay acquiring the spin lock when trancheId is less
than LocalLWLockCounter,
it seems possible that we might read NamedLWLockTrancheNames in the
GetLWTrancheName function
after LocalLWLockCounter has been updated, but before
NamedLWLockTrancheNames has been updated
in the LWLockNewTrancheId function.
To prevent reading an outdated tranche name, we should acquire the spin
lock unconditionally in the
GetLWTrancheName function

3.
+               ereport(ERROR,
+                               (errmsg("maximum number of tranches already
registered"),
+                                errdetail("No more than %d tranches may be
registered.",
+                                                  MAX_NAMED_TRANCHES)));
+       }

Since this patch sets a maximum limit on the number of LW lock tranches
that can be registered,
would it make sense to offer a configurable option rather than using a
hard-coded MAX_NUM_TRANCHES?
This will allow users who have reached the maximum limit to register their
LW Locks.

Thank you,
Rahila Syed

#89Nathan Bossart
nathandbossart@gmail.com
In reply to: Bertrand Drouvot (#87)
Re: Improve LWLock tranche name visibility across backends

On Mon, Sep 01, 2025 at 10:18:46AM +0000, Bertrand Drouvot wrote:

Changes look good.

Thanks for looking.

Not directly related, but I think that we can get rid of:

size = add_size(size, LWLOCK_PADDED_SIZE);

in LWLockShmemSize() and of:

ptr += LWLOCK_PADDED_SIZE - ((uintptr_t) ptr) % LWLOCK_PADDED_SIZE;

in CreateLWLocks(), and just make use of CACHELINEALIGN().

Let's take care of the tranche name stuff first.

--
nathan

#90Nathan Bossart
nathandbossart@gmail.com
In reply to: Rahila Syed (#88)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

On Mon, Sep 01, 2025 at 06:35:55PM +0530, Rahila Syed wrote:

Please see below for some comments regarding v20 patch.

Thanks for looking.

1. Following unused declaration is present in the lwlock.h file.

extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);

Whoops, good catch.

If the intention is to delay acquiring the spin lock when trancheId is
less than LocalLWLockCounter, it seems possible that we might read
NamedLWLockTrancheNames in the GetLWTrancheName function after
LocalLWLockCounter has been updated, but before NamedLWLockTrancheNames
has been updated in the LWLockNewTrancheId function. To prevent reading
an outdated tranche name, we should acquire the spin lock unconditionally
in the GetLWTrancheName function

We only ever add new tranche names to the end of the list, so we should be
able to avoid the lock for the vast majority of tranche name lookups. We
hold the lock while we increment LWLockCounter and add a name, and also
whenever we update LocalLWLockCounter. This should prevent any unsafe
accesses to NamedLWLockTrancheNames.

I've added some additional commentary about this in v21.

+               ereport(ERROR,
+                               (errmsg("maximum number of tranches already registered"),
+                                errdetail("No more than %d tranches may be registered.",
+                                                  MAX_NAMED_TRANCHES)));
+       }

Since this patch sets a maximum limit on the number of LW lock tranches
that can be registered, would it make sense to offer a configurable
option rather than using a hard-coded MAX_NUM_TRANCHES? This will allow
users who have reached the maximum limit to register their LW Locks.

Hm. I'm not sure. I think it would be good to offer a way to accommodate
more tranche names that didn't involve recompiling, but I don't know
whether that situation is likely enough in practice to add yet another GUC.
Thus far, we've been operating under the assumption that we'll be able to
choose a number far beyond any realistic usage. If others have opinions
about this, please share...

--
nathan

Attachments:

v21-0001-Move-dynamically-allocated-tranche-names-to-shar.patchtext/plain; charset=us-asciiDownload
From a7d8c29dc6a9edf83e3607c84cf5d15dc464da9f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 27 Aug 2025 15:13:41 -0500
Subject: [PATCH v21 1/2] Move dynamically-allocated tranche names to shared
 memory.

---
 contrib/pg_prewarm/autoprewarm.c              |   3 +-
 doc/src/sgml/xfunc.sgml                       |  15 +-
 src/backend/postmaster/launch_backend.c       |   6 +-
 src/backend/storage/ipc/dsm_registry.c        |  12 +-
 src/backend/storage/lmgr/lwlock.c             | 218 +++++++++---------
 src/include/storage/lwlock.h                  |  25 +-
 src/test/modules/test_dsa/test_dsa.c          |   6 +-
 .../test_dsm_registry/test_dsm_registry.c     |   3 +-
 .../modules/test_radixtree/test_radixtree.c   |   9 +-
 src/test/modules/test_slru/test_slru.c        |   6 +-
 .../modules/test_tidstore/test_tidstore.c     |   3 +-
 11 files changed, 131 insertions(+), 175 deletions(-)

diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index c01b9c7e6a4..880e897796a 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -864,7 +864,7 @@ apw_init_state(void *ptr)
 {
 	AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr;
 
-	LWLockInitialize(&state->lock, LWLockNewTrancheId());
+	LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm"));
 	state->bgworker_pid = InvalidPid;
 	state->pid_using_dumpfile = InvalidPid;
 }
@@ -883,7 +883,6 @@ apw_init_shmem(void)
 								   sizeof(AutoPrewarmSharedState),
 								   apw_init_state,
 								   &found);
-	LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm");
 
 	return found;
 }
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
index f116d0648e5..da21ef56891 100644
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -3759,7 +3759,7 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name)
       <literal>shmem_request_hook</literal>.  To do so, first allocate a
       <literal>tranche_id</literal> by calling:
 <programlisting>
-int LWLockNewTrancheId(void)
+int LWLockNewTrancheId(const char *name)
 </programlisting>
       Next, initialize each LWLock, passing the new
       <literal>tranche_id</literal> as an argument:
@@ -3777,17 +3777,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id)
      </para>
 
      <para>
-      Finally, each backend using the <literal>tranche_id</literal> should
-      associate it with a <literal>tranche_name</literal> by calling:
-<programlisting>
-void LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-</programlisting>
-     </para>
-
-     <para>
-      A complete usage example of <function>LWLockNewTrancheId</function>,
-      <function>LWLockInitialize</function>, and
-      <function>LWLockRegisterTranche</function> can be found in
+      A complete usage example of <function>LWLockNewTrancheId</function> and
+      <function>LWLockInitialize</function> can be found in
       <filename>contrib/pg_prewarm/autoprewarm.c</filename> in the
       <productname>PostgreSQL</productname> source tree.
      </para>
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index cd9547b03a3..de1b06df10d 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -101,7 +101,7 @@ typedef struct
 	struct InjectionPointsCtl *ActiveInjectionPoints;
 #endif
 	int			NamedLWLockTrancheRequests;
-	NamedLWLockTranche *NamedLWLockTrancheArray;
+	char	  **NamedLWLockTrancheNames;
 	int		   *LWLockCounter;
 	LWLockPadded *MainLWLockArray;
 	slock_t    *ProcStructLock;
@@ -761,7 +761,7 @@ save_backend_variables(BackendParameters *param,
 #endif
 
 	param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests;
-	param->NamedLWLockTrancheArray = NamedLWLockTrancheArray;
+	param->NamedLWLockTrancheNames = NamedLWLockTrancheNames;
 	param->LWLockCounter = LWLockCounter;
 	param->MainLWLockArray = MainLWLockArray;
 	param->ProcStructLock = ProcStructLock;
@@ -1022,7 +1022,7 @@ restore_backend_variables(BackendParameters *param)
 #endif
 
 	NamedLWLockTrancheRequests = param->NamedLWLockTrancheRequests;
-	NamedLWLockTrancheArray = param->NamedLWLockTrancheArray;
+	NamedLWLockTrancheNames = param->NamedLWLockTrancheNames;
 	LWLockCounter = param->LWLockCounter;
 	MainLWLockArray = param->MainLWLockArray;
 	ProcStructLock = param->ProcStructLock;
diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index ca12815f4a8..97130925106 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -299,8 +299,7 @@ GetNamedDSA(const char *name, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSA;
 
 		/* Initialize the LWLock tranche for the DSA. */
-		state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(state->tranche, name);
+		state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA. */
 		ret = dsa_create(state->tranche);
@@ -321,9 +320,6 @@ GetNamedDSA(const char *name, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSA already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the DSA. */
-		LWLockRegisterTranche(state->tranche, name);
-
 		/* Attach to existing DSA. */
 		ret = dsa_attach(state->handle);
 		dsa_pin_mapping(ret);
@@ -378,8 +374,7 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		entry->type = DSMR_ENTRY_TYPE_DSH;
 
 		/* Initialize the LWLock tranche for the hash table. */
-		dsh_state->tranche = LWLockNewTrancheId();
-		LWLockRegisterTranche(dsh_state->tranche, name);
+		dsh_state->tranche = LWLockNewTrancheId(name);
 
 		/* Initialize the DSA for the hash table. */
 		dsa = dsa_create(dsh_state->tranche);
@@ -409,9 +404,6 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 			ereport(ERROR,
 					(errmsg("requested DSHash already attached to current process")));
 
-		/* Initialize existing LWLock tranche for the hash table. */
-		LWLockRegisterTranche(dsh_state->tranche, name);
-
 		/* Attach to existing DSA for the hash table. */
 		dsa = dsa_attach(dsh_state->dsa_handle);
 		dsa_pin_mapping(dsa);
diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index a4aecd1fbc3..7bd9aaf8377 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -126,8 +126,8 @@ StaticAssertDecl((LW_VAL_EXCLUSIVE & LW_FLAG_MASK) == 0,
  * in lwlocklist.h.  We absorb the names of these tranches, too.
  *
  * 3. Extensions can create new tranches, via either RequestNamedLWLockTranche
- * or LWLockRegisterTranche.  The names of these that are known in the current
- * process appear in LWLockTrancheNames[].
+ * or LWLockNewTrancheId.  These names are stored in shared memory and can be
+ * accessed via NamedLWLockTrancheNames.
  *
  * All these names are user-visible as wait event names, so choose with care
  * ... and do not forget to update the documentation's list of wait events.
@@ -146,11 +146,12 @@ StaticAssertDecl(lengthof(BuiltinTrancheNames) ==
 
 /*
  * This is indexed by tranche ID minus LWTRANCHE_FIRST_USER_DEFINED, and
- * stores the names of all dynamically-created tranches known to the current
- * process.  Any unused entries in the array will contain NULL.
+ * points to the shared memory locations of the names of all
+ * dynamically-created tranches.  Backends inherit the pointer by fork from the
+ * postmaster (except in the EXEC_BACKEND case, where we have special measures
+ * to pass it down).
  */
-static const char **LWLockTrancheNames = NULL;
-static int	LWLockTrancheNamesAllocated = 0;
+char	  **NamedLWLockTrancheNames = NULL;
 
 /*
  * This points to the main array of LWLocks in shared memory.  Backends inherit
@@ -184,20 +185,22 @@ typedef struct NamedLWLockTrancheRequest
 } NamedLWLockTrancheRequest;
 
 static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL;
-static int	NamedLWLockTrancheRequestsAllocated = 0;
 
 /*
- * NamedLWLockTrancheRequests is both the valid length of the request array,
- * and the length of the shared-memory NamedLWLockTrancheArray later on.
- * This variable and NamedLWLockTrancheArray are non-static so that
- * postmaster.c can copy them to child processes in EXEC_BACKEND builds.
+ * NamedLWLockTrancheRequests is the valid length of the request array.  This
+ * variable is non-static so that postmaster.c can copy them to child processes
+ * in EXEC_BACKEND builds.
  */
 int			NamedLWLockTrancheRequests = 0;
 
-/* points to data in shared memory: */
-NamedLWLockTranche *NamedLWLockTrancheArray = NULL;
+/* shared memory counter of registered tranches */
 int		   *LWLockCounter = NULL;
 
+/* backend-local counter of registered tranches */
+static int	LocalLWLockCounter;
+
+#define MAX_NAMED_TRANCHES 256
+
 static void InitializeLWLocks(void);
 static inline void LWLockReportWaitStart(LWLock *lock);
 static inline void LWLockReportWaitEnd(void);
@@ -392,31 +395,28 @@ Size
 LWLockShmemSize(void)
 {
 	Size		size;
-	int			i;
 	int			numLocks = NUM_FIXED_LWLOCKS;
 
 	/* Calculate total number of locks needed in the main array. */
 	numLocks += NumLWLocksForNamedTranches();
 
-	/* Space for dynamic allocation counter, plus room for alignment. */
-	size = sizeof(int) + LWLOCK_PADDED_SIZE;
+	/* Space for dynamic allocation counter. */
+	size = MAXALIGN(sizeof(int));
 
-	/* Space for the LWLock array. */
-	size = add_size(size, mul_size(numLocks, sizeof(LWLockPadded)));
+	/* Space for named tranches. */
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, sizeof(char *)));
+	size = add_size(size, mul_size(MAX_NAMED_TRANCHES, NAMEDATALEN));
 
-	/* space for named tranches. */
-	size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche)));
-
-	/* space for name of each tranche. */
-	for (i = 0; i < NamedLWLockTrancheRequests; i++)
-		size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1);
+	/* Space for the LWLock array, plus room for cache line alignment. */
+	size = add_size(size, LWLOCK_PADDED_SIZE);
+	size = add_size(size, mul_size(numLocks, sizeof(LWLockPadded)));
 
 	return size;
 }
 
 /*
  * Allocate shmem space for the main LWLock array and all tranches and
- * initialize it.  We also register extension LWLock tranches here.
+ * initialize it.
  */
 void
 CreateLWLocks(void)
@@ -432,7 +432,16 @@ CreateLWLocks(void)
 		/* Initialize the dynamic-allocation counter for tranches */
 		LWLockCounter = (int *) ptr;
 		*LWLockCounter = LWTRANCHE_FIRST_USER_DEFINED;
-		ptr += sizeof(int);
+		ptr += MAXALIGN(sizeof(int));
+
+		/* Initialize tranche names */
+		NamedLWLockTrancheNames = (char **) ptr;
+		ptr += MAX_NAMED_TRANCHES * sizeof(char *);
+		for (int i = 0; i < MAX_NAMED_TRANCHES; i++)
+		{
+			NamedLWLockTrancheNames[i] = ptr;
+			ptr += NAMEDATALEN;
+		}
 
 		/* Ensure desired alignment of LWLock array */
 		ptr += LWLOCK_PADDED_SIZE - ((uintptr_t) ptr) % LWLOCK_PADDED_SIZE;
@@ -441,11 +450,6 @@ CreateLWLocks(void)
 		/* Initialize all LWLocks */
 		InitializeLWLocks();
 	}
-
-	/* Register named extension LWLock tranches in the current process. */
-	for (int i = 0; i < NamedLWLockTrancheRequests; i++)
-		LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
-							  NamedLWLockTrancheArray[i].trancheName);
 }
 
 /*
@@ -454,7 +458,6 @@ CreateLWLocks(void)
 static void
 InitializeLWLocks(void)
 {
-	int			numNamedLocks = NumLWLocksForNamedTranches();
 	int			id;
 	int			i;
 	int			j;
@@ -485,32 +488,18 @@ InitializeLWLocks(void)
 	 */
 	if (NamedLWLockTrancheRequests > 0)
 	{
-		char	   *trancheNames;
-
-		NamedLWLockTrancheArray = (NamedLWLockTranche *)
-			&MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks];
-
-		trancheNames = (char *) NamedLWLockTrancheArray +
-			(NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche));
 		lock = &MainLWLockArray[NUM_FIXED_LWLOCKS];
 
 		for (i = 0; i < NamedLWLockTrancheRequests; i++)
 		{
 			NamedLWLockTrancheRequest *request;
-			NamedLWLockTranche *tranche;
-			char	   *name;
+			int			tranche;
 
 			request = &NamedLWLockTrancheRequestArray[i];
-			tranche = &NamedLWLockTrancheArray[i];
-
-			name = trancheNames;
-			trancheNames += strlen(request->tranche_name) + 1;
-			strcpy(name, request->tranche_name);
-			tranche->trancheId = LWLockNewTrancheId();
-			tranche->trancheName = name;
+			tranche = LWLockNewTrancheId(request->tranche_name);
 
 			for (j = 0; j < request->num_lwlocks; j++, lock++)
-				LWLockInitialize(&lock->lock, tranche->trancheId);
+				LWLockInitialize(&lock->lock, tranche);
 		}
 	}
 }
@@ -562,59 +551,47 @@ GetNamedLWLockTranche(const char *tranche_name)
 }
 
 /*
- * Allocate a new tranche ID.
+ * Allocate a new tranche ID with the provided name.
  */
 int
-LWLockNewTrancheId(void)
+LWLockNewTrancheId(const char *name)
 {
 	int			result;
 
-	/* We use the ShmemLock spinlock to protect LWLockCounter */
-	SpinLockAcquire(ShmemLock);
-	result = (*LWLockCounter)++;
-	SpinLockRelease(ShmemLock);
+	if (!name)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_NAME),
+				 errmsg("tranche name cannot be NULL")));
 
-	return result;
-}
+	if (strlen(name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("tranche name too long"),
+				 errdetail("LWLock tranche names must be no longer than %d bytes.",
+						   NAMEDATALEN - 1)));
 
-/*
- * Register a dynamic tranche name in the lookup table of the current process.
- *
- * This routine will save a pointer to the 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 tranche name will be user-visible as a wait event name, so try to
- * use a name that fits the style for those.
- */
-void
-LWLockRegisterTranche(int tranche_id, const char *tranche_name)
-{
-	/* This should only be called for user-defined tranches. */
-	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
-		return;
-
-	/* Convert to array index. */
-	tranche_id -= LWTRANCHE_FIRST_USER_DEFINED;
+	/*
+	 * We use the ShmemLock spinlock to protect LWLockCounter and
+	 * NamedLWLockTrancheNames.
+	 */
+	SpinLockAcquire(ShmemLock);
 
-	/* If necessary, create or enlarge array. */
-	if (tranche_id >= LWLockTrancheNamesAllocated)
+	if (*LWLockCounter - LWTRANCHE_FIRST_USER_DEFINED >= MAX_NAMED_TRANCHES)
 	{
-		int			newalloc;
+		SpinLockRelease(ShmemLock);
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("No more than %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
+	}
 
-		newalloc = pg_nextpower2_32(Max(8, tranche_id + 1));
+	result = (*LWLockCounter)++;
+	LocalLWLockCounter = *LWLockCounter;
+	strlcpy(NamedLWLockTrancheNames[result - LWTRANCHE_FIRST_USER_DEFINED], name, NAMEDATALEN);
 
-		if (LWLockTrancheNames == NULL)
-			LWLockTrancheNames = (const char **)
-				MemoryContextAllocZero(TopMemoryContext,
-									   newalloc * sizeof(char *));
-		else
-			LWLockTrancheNames =
-				repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc);
-		LWLockTrancheNamesAllocated = newalloc;
-	}
+	SpinLockRelease(ShmemLock);
 
-	LWLockTrancheNames[tranche_id] = tranche_name;
+	return result;
 }
 
 /*
@@ -637,27 +614,33 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 	if (!process_shmem_requests_in_progress)
 		elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook");
 
+	if (!tranche_name)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_NAME),
+				 errmsg("tranche name cannot be NULL")));
+
+	if (strlen(tranche_name) >= NAMEDATALEN)
+		ereport(ERROR,
+				(errcode(ERRCODE_NAME_TOO_LONG),
+				 errmsg("tranche name too long"),
+				 errdetail("LWLock tranche names must be no longer than %d bytes.",
+						   NAMEDATALEN - 1)));
+
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
-		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
-							   NamedLWLockTrancheRequestsAllocated
+							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
-	{
-		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
-
-		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
-			repalloc(NamedLWLockTrancheRequestArray,
-					 i * sizeof(NamedLWLockTrancheRequest));
-		NamedLWLockTrancheRequestsAllocated = i;
-	}
+	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
+		ereport(ERROR,
+				(errmsg("maximum number of tranches already registered"),
+				 errdetail("No more than %d tranches may be registered.",
+						   MAX_NAMED_TRANCHES)));
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
-	Assert(strlen(tranche_name) + 1 <= NAMEDATALEN);
 	strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);
 	request->num_lwlocks = num_lwlocks;
 	NamedLWLockTrancheRequests++;
@@ -669,6 +652,9 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 void
 LWLockInitialize(LWLock *lock, int tranche_id)
 {
+	/* verify the tranche_id is valid */
+	(void) GetLWTrancheName(tranche_id);
+
 	pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK);
 #ifdef LOCK_DEBUG
 	pg_atomic_init_u32(&lock->nwaiters, 0);
@@ -710,17 +696,29 @@ GetLWTrancheName(uint16 trancheId)
 		return BuiltinTrancheNames[trancheId];
 
 	/*
-	 * It's an extension tranche, so look in LWLockTrancheNames[].  However,
-	 * it's possible that the tranche has never been registered in the current
-	 * process, in which case give up and return "extension".
+	 * We only ever add new entries to NamedLWLockTrancheNames, so most
+	 * lookups can avoid taking the spinlock as long as the backend-local
+	 * counter (LocalLWLockCounter) is greater than the requested tranche ID.
+	 * Else, we need to first update the backend-local counter with ShmemLock
+	 * held before attempting the lookup again.  In practice, the latter case
+	 * is probably rare.
 	 */
-	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
+	if (trancheId >= LocalLWLockCounter)
+	{
+		SpinLockAcquire(ShmemLock);
+		LocalLWLockCounter = *LWLockCounter;
+		SpinLockRelease(ShmemLock);
+
+		if (trancheId >= LocalLWLockCounter)
+			elog(ERROR, "tranche %d is not registered", trancheId);
+	}
 
-	if (trancheId >= LWLockTrancheNamesAllocated ||
-		LWLockTrancheNames[trancheId] == NULL)
-		return "extension";
+	/*
+	 * It's an extension tranche, so look in NamedLWLockTrancheNames.
+	 */
+	trancheId -= LWTRANCHE_FIRST_USER_DEFINED;
 
-	return LWLockTrancheNames[trancheId];
+	return NamedLWLockTrancheNames[trancheId];
 }
 
 /*
diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h
index f9cf57f8d26..16728521b9a 100644
--- a/src/include/storage/lwlock.h
+++ b/src/include/storage/lwlock.h
@@ -73,14 +73,7 @@ typedef union LWLockPadded
 
 extern PGDLLIMPORT LWLockPadded *MainLWLockArray;
 
-/* struct for storing named tranche information */
-typedef struct NamedLWLockTranche
-{
-	int			trancheId;
-	char	   *trancheName;
-} NamedLWLockTranche;
-
-extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray;
+extern PGDLLIMPORT char **NamedLWLockTrancheNames;
 extern PGDLLIMPORT int NamedLWLockTrancheRequests;
 extern PGDLLIMPORT int *LWLockCounter;
 
@@ -158,19 +151,11 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name);
 
 /*
  * There is another, more flexible method of obtaining lwlocks. First, call
- * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from
- * a shared counter.  Next, each individual process using the tranche should
- * call LWLockRegisterTranche() to associate that tranche ID with a name.
- * Finally, LWLockInitialize should be called just once per lwlock, passing
- * the tranche ID as an argument.
- *
- * 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.
+ * LWLockNewTrancheId to obtain a tranche ID; this allocates from a shared
+ * counter.  Second, LWLockInitialize should be called just once per lwlock,
+ * passing the tranche ID as an argument.
  */
-extern int	LWLockNewTrancheId(void);
-extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name);
+extern int	LWLockNewTrancheId(const char *name);
 extern void LWLockInitialize(LWLock *lock, int tranche_id);
 
 /*
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index cd24d0f4873..01d5c6fa67f 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -29,8 +29,7 @@ test_dsa_basic(PG_FUNCTION_ARGS)
 	dsa_pointer p[100];
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	a = dsa_create(tranche_id);
 	for (int i = 0; i < 100; i++)
@@ -70,8 +69,7 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 	ResourceOwner childowner;
 
 	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(tranche_id, "test_dsa");
+	tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c
index 141c8ed1b34..4cc2ccdac3f 100644
--- a/src/test/modules/test_dsm_registry/test_dsm_registry.c
+++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c
@@ -48,7 +48,7 @@ init_tdr_dsm(void *ptr)
 {
 	TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr;
 
-	LWLockInitialize(&dsm->lck, LWLockNewTrancheId());
+	LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry"));
 	dsm->val = 0;
 }
 
@@ -61,7 +61,6 @@ tdr_attach_shmem(void)
 								 sizeof(TestDSMRegistryStruct),
 								 init_tdr_dsm,
 								 &found);
-	LWLockRegisterTranche(tdr_dsm->lck.tranche, "test_dsm_registry");
 
 	if (tdr_dsa == NULL)
 		tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found);
diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c
index 80ad0296164..787162c8793 100644
--- a/src/test/modules/test_radixtree/test_radixtree.c
+++ b/src/test/modules/test_radixtree/test_radixtree.c
@@ -124,10 +124,9 @@ test_empty(void)
 	rt_iter    *iter;
 	uint64		key;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc)
 	uint64	   *keys;
 	int			children = test_info->nkeys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
@@ -304,10 +302,9 @@ test_random(void)
 	int			num_keys = 100000;
 	uint64	   *keys;
 #ifdef TEST_SHARED_RT
-	int			tranche_id = LWLockNewTrancheId();
+	int			tranche_id = LWLockNewTrancheId("test_radix_tree");
 	dsa_area   *dsa;
 
-	LWLockRegisterTranche(tranche_id, "test_radix_tree");
 	dsa = dsa_create(tranche_id);
 	radixtree = rt_create(dsa, tranche_id);
 #else
diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c
index 32750930e43..8c0367eeee4 100644
--- a/src/test/modules/test_slru/test_slru.c
+++ b/src/test/modules/test_slru/test_slru.c
@@ -232,11 +232,9 @@ test_slru_shmem_startup(void)
 	(void) MakePGDirectory(slru_dir_name);
 
 	/* initialize the SLRU facility */
-	test_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_slru_tranche");
+	test_tranche_id = LWLockNewTrancheId("test_slru_tranche");
 
-	test_buffer_tranche_id = LWLockNewTrancheId();
-	LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche");
+	test_buffer_tranche_id = LWLockNewTrancheId("test_buffer_tranche");
 
 	TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically;
 	SimpleLruInit(TestSlruCtl, "TestSLRU",
diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c
index eb16e0fbfa6..0c8f43867e5 100644
--- a/src/test/modules/test_tidstore/test_tidstore.c
+++ b/src/test/modules/test_tidstore/test_tidstore.c
@@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS)
 	{
 		int			tranche_id;
 
-		tranche_id = LWLockNewTrancheId();
-		LWLockRegisterTranche(tranche_id, "test_tidstore");
+		tranche_id = LWLockNewTrancheId("test_tidstore");
 
 		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);
 
-- 
2.39.5 (Apple Git-154)

v21-0002-Tests-for-LWLock-tranche-registration-improvemen.patchtext/plain; charset=us-asciiDownload
From d4c065cf7685a5e2ca1cb00abe8cc96a3dde84a8 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Fri, 22 Aug 2025 00:35:45 -0500
Subject: [PATCH v21 2/2] Tests for LWLock tranche registration improvements

---
 src/test/modules/Makefile                     |   3 +-
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_tranches/.gitignore     |   4 +
 src/test/modules/test_tranches/Makefile       |  23 +++
 src/test/modules/test_tranches/meson.build    |  33 +++
 .../test_tranches/t/001_test_tranches.pl      | 120 +++++++++++
 .../test_tranches/test_tranches--1.0.sql      |  35 ++++
 .../modules/test_tranches/test_tranches.c     | 191 ++++++++++++++++++
 .../test_tranches/test_tranches.control       |   6 +
 9 files changed, 415 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/test_tranches/.gitignore
 create mode 100644 src/test/modules/test_tranches/Makefile
 create mode 100644 src/test/modules/test_tranches/meson.build
 create mode 100644 src/test/modules/test_tranches/t/001_test_tranches.pl
 create mode 100644 src/test/modules/test_tranches/test_tranches--1.0.sql
 create mode 100644 src/test/modules/test_tranches/test_tranches.c
 create mode 100644 src/test/modules/test_tranches/test_tranches.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 903a8ac151a..fd7aa4675c5 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -44,7 +44,8 @@ SUBDIRS = \
 		  test_tidstore \
 		  unsafe_tests \
 		  worker_spi \
-		  xid_wraparound
+		  xid_wraparound \
+		  test_tranches
 
 
 ifeq ($(enable_injection_points),yes)
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 93be0f57289..52883dfb496 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -45,3 +45,4 @@ subdir('typcache')
 subdir('unsafe_tests')
 subdir('worker_spi')
 subdir('xid_wraparound')
+subdir('test_tranches')
diff --git a/src/test/modules/test_tranches/.gitignore b/src/test/modules/test_tranches/.gitignore
new file mode 100644
index 00000000000..5dcb3ff9723
--- /dev/null
+++ b/src/test/modules/test_tranches/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_tranches/Makefile b/src/test/modules/test_tranches/Makefile
new file mode 100644
index 00000000000..5c9e78084da
--- /dev/null
+++ b/src/test/modules/test_tranches/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_tranches/Makefile
+
+MODULE_big = test_tranches
+OBJS = \
+	$(WIN32RES) \
+	test_tranches.o
+PGFILEDESC = "test_tranches - test code LWLock tranche management"
+
+EXTENSION = test_tranches
+DATA = test_tranches--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_tranches
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_tranches/meson.build b/src/test/modules/test_tranches/meson.build
new file mode 100644
index 00000000000..976ce9f0a25
--- /dev/null
+++ b/src/test/modules/test_tranches/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2025, PostgreSQL Global Development Group
+
+test_tranches_sources = files(
+  'test_tranches.c',
+)
+
+if host_system == 'windows'
+  test_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_tranches',
+    '--FILEDESC', 'test_tranches - test code LWLock tranche management',])
+endif
+
+test_tranches = shared_module('test_tranches',
+  test_tranches_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_tranches
+
+test_install_data += files(
+  'test_tranches.control',
+  'test_tranches--1.0.sql',
+)
+
+tests += {
+  'name': 'test_tranches',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_test_tranches.pl',
+    ],
+  },
+}
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/t/001_test_tranches.pl b/src/test/modules/test_tranches/t/001_test_tranches.pl
new file mode 100644
index 00000000000..477035bdb99
--- /dev/null
+++ b/src/test/modules/test_tranches/t/001_test_tranches.pl
@@ -0,0 +1,120 @@
+use strict;
+use warnings FATAL => 'all';
+
+use List::Util qw(min);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+#
+# Create the cluster without any requested tranches
+#
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+$node->append_conf('postgresql.conf',
+    qq(shared_preload_libraries='test_tranches'));
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=0));
+$node->start();
+$node->safe_psql('postgres', q(CREATE EXTENSION test_tranches));
+
+my $first_user_defined = $node->safe_psql('postgres', q(select test_tranches_get_first_user_defined()));
+my @user_defined = ($first_user_defined .. $first_user_defined + 5);
+my ($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+#
+# Run lookup tests to ensure the correct tranche names are returned
+# and an error is raised for unregistered tranches
+#
+$node->safe_psql('postgres', "select test_tranches_new(5);");
+my ($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_lookup(1);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche 100 is not registered/, "unregistered tranche error");
+like("$stdout",
+     qr/ShmemIndex.*test_lock__0.*test_lock__1.*test_lock__2.*test_lock__3.*test_lock__4/s,
+     "match tranche names without requested tranches");
+
+#
+# Test the error for long tranche names
+#
+my $good_tranche_name = 'A' x 63;
+my $bad_tranche_name = 'B' x 64; # MAX_NAMED_TRANCHES_NAME_LEN
+$node->safe_psql('postgres', qq{select test_tranches_new_tranche('$good_tranche_name');});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_new_tranche('$bad_tranche_name');
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche name too long/, "tranche name too long");
+
+$node->restart();
+
+#
+# Test the error when > MAX_NAMED_TRANCHES named tranches registered.
+#
+$node->safe_psql('postgres', qq{select test_tranches_new(255)});
+$node->safe_psql('postgres', qq{select test_tranches_new(1)});
+($result, $stdout, $stderr) = $node->psql('postgres', qq{select test_tranches_new(1);}, on_error_stop => 0);
+like("$stderr", qr/ERROR:  maximum number of tranches already registered/, "too many tranches registered");
+
+#
+# Repeat the lookup test with 2 requested tranches
+#
+$node->append_conf('postgresql.conf',
+    qq(test_tranches.requested_named_tranches=2));
+$node->restart();
+
+$first_user_defined = $first_user_defined;
+@user_defined = ($first_user_defined .. $first_user_defined + 5);
+($second_user_defined,
+    $third_user_defined,
+    $fourth_user_defined,
+    $fifth_user_defined,
+    $sixth_user_defined) = @user_defined[1..5];
+
+$node->safe_psql('postgres', "select test_tranches_new(3);");
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{
+        select test_tranches_lookup(1);
+        select test_tranches_lookup($first_user_defined);
+        select test_tranches_lookup($second_user_defined);
+        select test_tranches_lookup($third_user_defined);
+        select test_tranches_lookup($fourth_user_defined);
+        select test_tranches_lookup($fifth_user_defined);
+        select test_tranches_lookup($sixth_user_defined);
+        },
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche 100 is not registered/, "unregistered tranche error");
+like("$stdout", qr/ShmemIndex.*test_lock_0.*test_lock_1.*test_lock__2.*test_lock__3.*test_lock__4/s,
+     "match tranche names with requested tranches");
+
+#
+# Test lwlock initialize
+#
+$node->safe_psql('postgres', qq{select test_tranches_lwlock_initialize($first_user_defined)});
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{select test_tranches_lwlock_initialize($sixth_user_defined)},
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche 100 is not registered/, "LWLock intialization error on invalid tranche name");
+
+#
+# Test error for NULL tranche name
+#
+($result, $stdout, $stderr) = $node->psql('postgres',
+    qq{select test_tranches_new_tranche(NULL)},
+    on_error_stop => 0);
+like("$stderr", qr/ERROR:  tranche name cannot be NULL/, "NULL tranche name");
+
+done_testing();
diff --git a/src/test/modules/test_tranches/test_tranches--1.0.sql b/src/test/modules/test_tranches/test_tranches--1.0.sql
new file mode 100644
index 00000000000..f504098e04a
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches--1.0.sql
@@ -0,0 +1,35 @@
+-- test_tranches--1.0.sql
+
+CREATE FUNCTION test_tranches_new(bigint)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_new'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lookup(int)
+RETURNS text
+AS 'MODULE_PATHNAME', 'test_tranches_lookup'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_named_lwlock(text, int)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_named_lwlock'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_get_first_user_defined()
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_get_first_user_defined'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_tranches_lwlock_initialize(int)
+RETURNS void
+AS 'MODULE_PATHNAME', 'test_tranches_lwlock_initialize'
+LANGUAGE C STRICT;
+
+/*
+ * Function is CALLED ON NULL INPUT to allow NULL input
+ * for testing
+ */
+CREATE FUNCTION test_tranches_new_tranche(text)
+RETURNS int
+AS 'MODULE_PATHNAME', 'test_tranches_new_tranche'
+LANGUAGE C;
\ No newline at end of file
diff --git a/src/test/modules/test_tranches/test_tranches.c b/src/test/modules/test_tranches/test_tranches.c
new file mode 100644
index 00000000000..7d3031ace9f
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.c
@@ -0,0 +1,191 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+#include "storage/dsm_registry.h"
+#include "storage/ipc.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/injection_point.h"
+#include "utils/wait_classes.h"
+
+PG_MODULE_MAGIC;
+
+/* hooks */
+static shmem_request_hook_type prev_shmem_request_hook = NULL;
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static void test_tranches_shmem_request(void);
+static void test_tranches_shmem_startup(void);
+
+/* GUC */
+static int	test_tranches_requested_named_tranches = 0;
+
+typedef struct testTranchesSharedState
+{
+	int			next_index;
+}			testTranchesSharedState;
+
+static testTranchesSharedState * test_lwlock_ss = NULL;
+
+/*
+ * LWLock wait event masks. Copied from src/backend/utils/activity/wait_event.c
+ */
+#define WAIT_EVENT_CLASS_MASK	0xFF000000
+
+/*
+ * Module load callback
+ */
+void
+_PG_init(void)
+{
+	prev_shmem_request_hook = shmem_request_hook;
+	shmem_request_hook = test_tranches_shmem_request;
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_tranches_shmem_startup;
+
+	DefineCustomIntVariable("test_tranches.requested_named_tranches",
+							"Sets the number of locks created during shmem request",
+							NULL,
+							&test_tranches_requested_named_tranches,
+							2,
+							0,
+							UINT16_MAX,
+							PGC_POSTMASTER,
+							0,
+							NULL,
+							NULL,
+							NULL);
+}
+
+static void
+test_tranches_shmem_startup(void)
+{
+	bool		found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	test_lwlock_ss = NULL;
+
+	test_lwlock_ss = ShmemInitStruct("test_tranches",
+									 sizeof(testTranchesSharedState),
+									 &found);
+	if (!found)
+	{
+		test_lwlock_ss->next_index = test_tranches_requested_named_tranches;
+	}
+}
+
+static Size
+test_tranches_memsize(void)
+{
+	Size		size;
+
+	size = MAXALIGN(sizeof(testTranchesSharedState));
+
+	return size;
+}
+
+static void
+test_tranches_shmem_request(void)
+{
+	int			i = 0;
+
+	if (prev_shmem_request_hook)
+		prev_shmem_request_hook();
+
+	RequestAddinShmemSpace(test_tranches_memsize());
+
+	for (i = 0; i < test_tranches_requested_named_tranches; i++)
+	{
+		char		name[15];
+
+		snprintf(name, sizeof(name), "test_lock_%d", i);
+		RequestNamedLWLockTranche(name, i);
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new);
+Datum
+test_tranches_new(PG_FUNCTION_ARGS)
+{
+	int64		num = PG_GETARG_INT64(0);
+	int			i;
+
+	for (i = test_lwlock_ss->next_index; i < num + test_lwlock_ss->next_index; i++)
+	{
+		char		name[50];
+
+		snprintf(name, 50, "test_lock__%d", i);
+
+		LWLockNewTrancheId(name);
+	}
+
+	test_lwlock_ss->next_index = i;
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_new_tranche);
+Datum
+test_tranches_new_tranche(PG_FUNCTION_ARGS)
+{
+	char	   *tranche_name = NULL;
+
+	if (!PG_ARGISNULL(0))
+		tranche_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+
+	PG_RETURN_INT32(LWLockNewTrancheId(tranche_name));
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lookup);
+Datum
+test_tranches_lookup(PG_FUNCTION_ARGS)
+{
+	const char *tranche_name = GetLWLockIdentifier(PG_WAIT_LWLOCK & WAIT_EVENT_CLASS_MASK, PG_GETARG_INT32(0));
+
+	if (tranche_name)
+		PG_RETURN_TEXT_P(cstring_to_text(tranche_name));
+	else
+		PG_RETURN_NULL();
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_named_lwlock);
+Datum
+test_tranches_get_named_lwlock(PG_FUNCTION_ARGS)
+{
+	int			i;
+	LWLockPadded *locks;
+
+	locks = GetNamedLWLockTranche(text_to_cstring(PG_GETARG_TEXT_PP(0)));
+
+	for (i = 0; i < PG_GETARG_INT32(1); i++)
+	{
+		LWLock	   *lock = &locks[i].lock;
+
+		LWLockAcquire(lock, LW_SHARED);
+		LWLockRelease(lock);
+	}
+
+	PG_RETURN_INT32(i);
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_get_first_user_defined);
+Datum
+test_tranches_get_first_user_defined(PG_FUNCTION_ARGS)
+{
+	return LWTRANCHE_FIRST_USER_DEFINED;
+}
+
+PG_FUNCTION_INFO_V1(test_tranches_lwlock_initialize);
+Datum
+test_tranches_lwlock_initialize(PG_FUNCTION_ARGS)
+{
+	LWLock		lock;
+
+	LWLockInitialize(&lock, PG_GETARG_INT32(0));
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_tranches/test_tranches.control b/src/test/modules/test_tranches/test_tranches.control
new file mode 100644
index 00000000000..8e6254a7e43
--- /dev/null
+++ b/src/test/modules/test_tranches/test_tranches.control
@@ -0,0 +1,6 @@
+# test_tranches.control
+
+comment = 'Test LWLock tranch names tracking'
+default_version = '1.0'
+relocatable = false
+module_pathname = '$libdir/test_tranches'
\ No newline at end of file
-- 
2.39.5 (Apple Git-154)

#91Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#90)
Re: Improve LWLock tranche name visibility across backends

Committed.

--
nathan

#92Rahila Syed
rahilasyed90@gmail.com
In reply to: Nathan Bossart (#90)
Re: Improve LWLock tranche name visibility across backends

Hi Nathan,

We only ever add new tranche names to the end of the list, so we should be
able to avoid the lock for the vast majority of tranche name lookups. We
hold the lock while we increment LWLockCounter and add a name, and also
whenever we update LocalLWLockCounter. This should prevent any unsafe
accesses to NamedLWLockTrancheNames.

I've added some additional commentary about this in v21.

Thanks for adding the comments.
I think there may be a brief period in the current code where unsafe access
to
LWLockTrancheNames is possible.

Since updates to LocalLWLockCounter and LWLockTrancheNames are performed
while
holding the lock, but reading LocalLWLockCounter and then
LWLockTrancheNames in
GetLWTrancheName can occur without acquiring the same lock in case
trancheID < LocalLWLockCounter,
There is a small window between updating LocalLWLockCounter and adding the
name
to LWLockTrancheNames. During this window, if GetLWTrancheNames is called,
it might attempt
to access a name in LWLockTrancheNames that hasn't been added yet.

I’ll see if I can demonstrate the situation with a test.

+               ereport(ERROR,
+                               (errmsg("maximum number of tranches

already registered"),

+ errdetail("No more than %d tranches may

be registered.",

+                                                  MAX_NAMED_TRANCHES)));
+       }

Since this patch sets a maximum limit on the number of LW lock tranches
that can be registered, would it make sense to offer a configurable
option rather than using a hard-coded MAX_NUM_TRANCHES? This will allow
users who have reached the maximum limit to register their LW Locks.

Hm. I'm not sure. I think it would be good to offer a way to accommodate
more tranche names that didn't involve recompiling, but I don't know
whether that situation is likely enough in practice to add yet another GUC.
Thus far, we've been operating under the assumption that we'll be able to
choose a number far beyond any realistic usage. If others have opinions
about this, please share...

Thank you for clarifying the rationale behind the decision.

Thank you,
Rahila Syed

#93Nathan Bossart
nathandbossart@gmail.com
In reply to: Rahila Syed (#92)
Re: Improve LWLock tranche name visibility across backends

On Thu, Sep 04, 2025 at 03:29:52PM +0530, Rahila Syed wrote:

Since updates to LocalLWLockCounter and LWLockTrancheNames are performed
while holding the lock, but reading LocalLWLockCounter and then
LWLockTrancheNames in GetLWTrancheName can occur without acquiring the
same lock in case trancheID < LocalLWLockCounter, There is a small window
between updating LocalLWLockCounter and adding the name to
LWLockTrancheNames. During this window, if GetLWTrancheNames is called,
it might attempt to access a name in LWLockTrancheNames that hasn't been
added yet.

We hold the lock when reading/writing the shared-memory LWLockCounter.
There's no need to take a lock when reading the backend-local
LocalLWLockCounter, as it can't be modified by any other process.
Additionally, when adding a new tranche name, we hold the lock the entire
time while we bump the shared-memory counter and copy the name, so there's
no chance another backend will see the counter update but not the name or
attempt to use the same LWLockTrancheNames slot.

--
nathan

#94Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#91)
1 attachment(s)
Re: Improve LWLock tranche name visibility across backends

On Wed, Sep 03, 2025 at 02:01:14PM -0500, Nathan Bossart wrote:

Committed.

I'm having some regrets about the changes to RequestNamedLWLockTranche().
Specifically, when it is first called, it immediately allocates an array
big enough to hold 256 requests (~17 KB), whereas it used to only allocate
space for 16 requests (~1 KB) and resize as needed. Furthermore, the
MAX_NAMED_TRANCHES check isn't actually needed because InitializeLWLocks()
will do the same check via its calls to LWLockNewTrancheId() for all the
named tranche requests.

--
nathan

Attachments:

v1-0001-Revert-some-recent-changes-to-RequestNamedLWLockT.patchtext/plain; charset=us-asciiDownload
From 6fe6590f86427c55ac831c5b48784d3d259fe1c8 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 4 Sep 2025 10:36:06 -0500
Subject: [PATCH v1 1/1] Revert some recent changes to
 RequestNamedLWLockTranche().

---
 src/backend/storage/lmgr/lwlock.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c
index 258cdebd0f5..a5f6b81806d 100644
--- a/src/backend/storage/lmgr/lwlock.c
+++ b/src/backend/storage/lmgr/lwlock.c
@@ -610,6 +610,7 @@ void
 RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 {
 	NamedLWLockTrancheRequest *request;
+	static int	NamedLWLockTrancheRequestsAllocated;
 
 	if (!process_shmem_requests_in_progress)
 		elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook");
@@ -628,17 +629,22 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
 
 	if (NamedLWLockTrancheRequestArray == NULL)
 	{
+		NamedLWLockTrancheRequestsAllocated = 16;
 		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
 			MemoryContextAlloc(TopMemoryContext,
 							   MAX_NAMED_TRANCHES
 							   * sizeof(NamedLWLockTrancheRequest));
 	}
 
-	if (NamedLWLockTrancheRequests >= MAX_NAMED_TRANCHES)
-		ereport(ERROR,
-				(errmsg("maximum number of tranches already registered"),
-				 errdetail("No more than %d tranches may be registered.",
-						   MAX_NAMED_TRANCHES)));
+	if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated)
+	{
+		int			i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1);
+
+		NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
+			repalloc(NamedLWLockTrancheRequestArray,
+					 i * sizeof(NamedLWLockTrancheRequest));
+		NamedLWLockTrancheRequestsAllocated = i;
+	}
 
 	request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests];
 	strlcpy(request->tranche_name, tranche_name, NAMEDATALEN);
-- 
2.39.5 (Apple Git-154)

#95Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#94)
Re: Improve LWLock tranche name visibility across backends

I'm having some regrets about the changes to RequestNamedLWLockTranche().
Specifically, when it is first called, it immediately allocates an array
big enough to hold 256 requests (~17 KB), whereas it used to only allocate
space for 16 requests (~1 KB) and resize as needed.

I liked removing the repalloc calls inside this routine and did not think
it was worth optimizing. I am OK with reverting it back. Although v1
is incorrect since it's still initializing
NamedLWLockTrancheRequestArray to MAX_NAMED_TRANCHES
```

if (NamedLWLockTrancheRequestArray == NULL)
{
+ NamedLWLockTrancheRequestsAllocated = 16;
NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *)
MemoryContextAlloc(TopMemoryContext,
MAX_NAMED_TRANCHES
* sizeof(NamedLWLockTrancheRequest));
}

```

instead of MAX_NAMED_TRANCHES, it should be
NamedLWLockTrancheRequestsAllocated .

Also, Previously NamedLWLockTrancheRequestsAllocated was global, but I don't
think it should ever be used outside of this function, so it's OK to declare it
as you have.

Furthermore, the
MAX_NAMED_TRANCHES check isn't actually needed because InitializeLWLocks()
will do the same check via its calls to LWLockNewTrancheId() for all the
named tranche requests.

I thought about that one and decided to add the error message there, since
requesting a tranche happens way before LWLockNewTrancheId is called
during CreateLWLocks, so it was more about erroring out slightly earlier.
But it may be ok to also just remove it.

--
Sami Imseih
Amazon Web Services (AWS)

#96Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#95)
Re: Improve LWLock tranche name visibility across backends

On Thu, Sep 04, 2025 at 12:30:27PM -0500, Sami Imseih wrote:

I liked removing the repalloc calls inside this routine and did not think
it was worth optimizing. I am OK with reverting it back. Although v1
is incorrect since it's still initializing
NamedLWLockTrancheRequestArray to MAX_NAMED_TRANCHES

Committed with that fix.

Furthermore, the
MAX_NAMED_TRANCHES check isn't actually needed because InitializeLWLocks()
will do the same check via its calls to LWLockNewTrancheId() for all the
named tranche requests.

I thought about that one and decided to add the error message there, since
requesting a tranche happens way before LWLockNewTrancheId is called
during CreateLWLocks, so it was more about erroring out slightly earlier.
But it may be ok to also just remove it.

We needed it before because the array could only ever hold
MAX_NAMED_TRANCHES requests.

--
nathan

#97Alexander Lakhin
exclusion@gmail.com
In reply to: Nathan Bossart (#96)
Re: Improve LWLock tranche name visibility across backends

Hello Nathan,

04.09.2025 23:37, Nathan Bossart wrote:

On Thu, Sep 04, 2025 at 12:30:27PM -0500, Sami Imseih wrote:

I liked removing the repalloc calls inside this routine and did not think
it was worth optimizing. I am OK with reverting it back. Although v1
is incorrect since it's still initializing
NamedLWLockTrancheRequestArray to MAX_NAMED_TRANCHES

Committed with that fix.

Furthermore, the
MAX_NAMED_TRANCHES check isn't actually needed because InitializeLWLocks()
will do the same check via its calls to LWLockNewTrancheId() for all the
named tranche requests.

I thought about that one and decided to add the error message there, since
requesting a tranche happens way before LWLockNewTrancheId is called
during CreateLWLocks, so it was more about erroring out slightly earlier.
But it may be ok to also just remove it.

We needed it before because the array could only ever hold
MAX_NAMED_TRANCHES requests.

I've discovered (with SQLsmith) that the new possible error makes
pg_prewarm/autoprewarm fail when the error triggered during it's
initialization and then another instance tries to acquire apw_state->lock.

Please try the following:
psql -c "CREATE EXTENSION test_dsa; CREATE EXTENSION pg_prewarm;"

psql -c "SELECT test_dsa_resowners() FROM generate_series(1, 256)" >/dev/null

psql -c "SELECT pg_sleep(0.5); SELECT autoprewarm_dump_now()" &

psql -c "SELECT pg_sleep(1); SELECT autoprewarm_dump_now()"

with
debug_parallel_query = 'on'
min_dynamic_shared_memory = '1GB'
in postgresql.conf

it causes PANIC for me (on roughly 5 out or 10 runs) as below:
CREATE EXTENSION
CREATE EXTENSION
 pg_sleep
----------

(1 row)

ERROR:  maximum number of tranches already registered
DETAIL:  No more than 256 tranches may be registered.
 pg_sleep
----------

(1 row)

server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.

PANIC:  stuck spinlock detected at LWLockWaitListLock, lwlock.c:882
...
#5  0x000057831e809d1d in errfinish (filename=0x57831ea53edd "s_lock.c", lineno=89, funcname=0x57831ea53ee8 <__func__.0>
"s_lock_stuck") at elog.c:609
#6  0x000057831e5f2185 in s_lock_stuck (file=0x57831ea5184c "lwlock.c", line=882, func=0x57831ea51c30 <__func__.5>
"LWLockWaitListLock") at s_lock.c:89
#7  0x000057831e5f2291 in perform_spin_delay (status=0x7ffdf9fabbe0) at s_lock.c:135
#8  0x000057831e5e4015 in LWLockWaitListLock (lock=0x736a16ad6500) at lwlock.c:886
#9  0x000057831e5e4475 in LWLockQueueSelf (lock=0x736a16ad6500, mode=LW_EXCLUSIVE) at lwlock.c:1055
#10 0x000057831e5e4728 in LWLockAcquire (lock=0x736a16ad6500, mode=LW_EXCLUSIVE) at lwlock.c:1259
#11 0x0000736a63dbcc82 in apw_dump_now (is_bgworker=false, dump_unlogged=true) at autoprewarm.c:676
#12 0x0000736a63dbd5c9 in autoprewarm_dump_now (fcinfo=0x57835c770fe8) at autoprewarm.c:854
...

Best regards,
Alexander

#98Sami Imseih
samimseih@gmail.com
In reply to: Alexander Lakhin (#97)
Re: Improve LWLock tranche name visibility across backends

I've discovered (with SQLsmith) that the new possible error makes
pg_prewarm/autoprewarm fail when the error triggered during it's
initialization and then another instance tries to acquire apw_state->lock.

From just a quick look at this, it does not seem to be the fault
of the error message itself, but because we no longer can
tolerate leaks in new tranche IDs. Currently the function shows:

/* XXX: this tranche is leaked */
tranche_id = LWLockNewTrancheId("test_dsa");

the test_dsa_resowners seems to not be handling
such scenarios and releasing locks, etc somewhere. If you modify
the function test_dsa_resowners and only keep the
LWLockNewTrancheId call, like this:

"
PG_FUNCTION_INFO_V1(test_dsa_resowners);
Datum
test_dsa_resowners(PG_FUNCTION_ARGS)
{
LWLockNewTrancheId("test_dsa");

PG_RETURN_VOID();
}
"
it does not repro.

Not really sure why this required "debug_parallel_query"
to be ON either, but that option itself should also be
used with caution.

I am not sure we need to do anything about this.

--
Sami Imseih
Amazon Web Services (AWS)

#99Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#98)
Re: Improve LWLock tranche name visibility across backends

I am not sure we need to do anything about this.

Or maybe we just avoid the tranche_id from leaking
in test_dsa_resowners() by making it a static variable
and checking if we have a valid tranche id before calling
LWLockNewTrancheId()? That is the proper pattern.

--
Sami

#100Sami Imseih
samimseih@gmail.com
In reply to: Sami Imseih (#99)
1 attachment(s)
Re: Improve LWLock tranche name visibility across backends

I am not sure we need to do anything about this.

Or maybe we just avoid the tranche_id from leaking
in test_dsa_resowners() by making it a static variable
and checking if we have a valid tranche id before calling
LWLockNewTrancheId()? That is the proper pattern.

Like the attached.

--
Sami

Attachments:

v1-0001-Prevent-tranche_id-leak-in-test_dsa_resowners.patchapplication/octet-stream; name=v1-0001-Prevent-tranche_id-leak-in-test_dsa_resowners.patchDownload
From dd892f0d8bea26e8b4b7b7ff224d7a16e021232e Mon Sep 17 00:00:00 2001
From: Ubuntu <ubuntu@ip-172-31-46-230.ec2.internal>
Date: Mon, 3 Nov 2025 17:36:19 +0000
Subject: [PATCH v1 1/1] Prevent tranche_id leak in test_dsa_resowners

tranche_id was previously allocated on every call and intentionally
leaked, which was acceptable for this test. However, 38b602b02 no
longer tolerates such leaks, and this can cause test_dsa_resowners to
fail to release resources properly.

tranche_id is now static and allocated only once by checking that its
value is below LWTRANCHE_FIRST_USER_DEFINED. This is sufficient for
this test. In general, tranche IDs should be tracked in shared memory.

Discussion: https://www.postgresql.org/message-id/dd36d384-55df-4fc2-825c-5bc56c950fa9%40gmail.com
---
 src/test/modules/test_dsa/test_dsa.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index 01d5c6fa67f..09670964ee9 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -62,14 +62,18 @@ PG_FUNCTION_INFO_V1(test_dsa_resowners);
 Datum
 test_dsa_resowners(PG_FUNCTION_ARGS)
 {
-	int			tranche_id;
+	static int	tranche_id;
 	dsa_area   *a;
 	dsa_pointer p[10000];
 	ResourceOwner oldowner;
 	ResourceOwner childowner;
 
-	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId("test_dsa");
+	/*
+	 * Allocate a tranche ID once.  A static variable is fine for this test;
+	 * it otherwise should be tracked in shared memory.
+	 */
+	if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED)
+		tranche_id = LWLockNewTrancheId("test_dsa");
 
 	/* Create DSA in parent resource owner */
 	a = dsa_create(tranche_id);
-- 
2.43.0

#101Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#100)
Re: Improve LWLock tranche name visibility across backends

On Mon, Nov 03, 2025 at 11:50:48AM -0600, Sami Imseih wrote:

I am not sure we need to do anything about this.

Or maybe we just avoid the tranche_id from leaking
in test_dsa_resowners() by making it a static variable
and checking if we have a valid tranche id before calling
LWLockNewTrancheId()? That is the proper pattern.

Like the attached.

It's probably a good idea to avoid tranche leaks, but IMHO there's room for
improvement in the DSM registry, too. IIUC the problem is that the DSM
segment is still being added to the registry and found by other backends
despite the initialization callback failing. My first instinct is that we
should keep track of whether the DSM segments/DSAs/dshash tables in the
registry have been fully initialized and to just ERROR in other backends
when attaching if they aren't. That shouldn't really happen in practice,
but it'd be good to avoid the strange errors, anyway.

--
nathan

#102Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#101)
Re: Improve LWLock tranche name visibility across backends

On Mon, Nov 10, 2025 at 10:26:17AM -0600, Nathan Bossart wrote:

On Mon, Nov 03, 2025 at 11:50:48AM -0600, Sami Imseih wrote:

I am not sure we need to do anything about this.

Or maybe we just avoid the tranche_id from leaking
in test_dsa_resowners() by making it a static variable
and checking if we have a valid tranche id before calling
LWLockNewTrancheId()? That is the proper pattern.

Like the attached.

It's probably a good idea to avoid tranche leaks, but IMHO there's room for
improvement in the DSM registry, too. IIUC the problem is that the DSM
segment is still being added to the registry and found by other backends
despite the initialization callback failing. My first instinct is that we
should keep track of whether the DSM segments/DSAs/dshash tables in the
registry have been fully initialized and to just ERROR in other backends
when attaching if they aren't. That shouldn't really happen in practice,
but it'd be good to avoid the strange errors, anyway.

BTW if we really wanted to avoid leaking tranches in test_dsa, we'd need to
store the ID in shared memory. Your patch helps in the case where a single
backend is repeatedly calling test_dsa_resowners(), but other backends
would still allocate their own tranche. I don't see a strong need to fix
this on back-branches, given these functions run exactly once as part of a
test, but fixing it on master seems worthwhile so that extension authors
don't copy/paste this broken code.

--
nathan

#103Nathan Bossart
nathandbossart@gmail.com
In reply to: Nathan Bossart (#102)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

On Mon, Nov 10, 2025 at 10:49:58AM -0600, Nathan Bossart wrote:

On Mon, Nov 10, 2025 at 10:26:17AM -0600, Nathan Bossart wrote:

It's probably a good idea to avoid tranche leaks, but IMHO there's room for
improvement in the DSM registry, too. IIUC the problem is that the DSM
segment is still being added to the registry and found by other backends
despite the initialization callback failing. My first instinct is that we
should keep track of whether the DSM segments/DSAs/dshash tables in the
registry have been fully initialized and to just ERROR in other backends
when attaching if they aren't. That shouldn't really happen in practice,
but it'd be good to avoid the strange errors, anyway.

0001 does this. When applied, Alexander's reproduction steps fail with

ERROR: requested DSM segment failed initialization

This one should probably get back-patched to v17, where the DSM registry
was introduced.

BTW if we really wanted to avoid leaking tranches in test_dsa, we'd need to
store the ID in shared memory. Your patch helps in the case where a single
backend is repeatedly calling test_dsa_resowners(), but other backends
would still allocate their own tranche. I don't see a strong need to fix
this on back-branches, given these functions run exactly once as part of a
test, but fixing it on master seems worthwhile so that extension authors
don't copy/paste this broken code.

0002 does this.

--
nathan

Attachments:

v2-0001-DSM-registry-ERROR-if-entry-was-not-initialized.patchtext/plain; charset=us-asciiDownload
From 0b4ccd2fd0393ed43edf39f018b2dabd9ca2b001 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Nov 2025 11:43:51 -0600
Subject: [PATCH v2 1/2] DSM registry: ERROR if entry was not initialized.

---
 src/backend/storage/ipc/dsm_registry.c | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index a926b9c3f32..ec8b48ad9a0 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -93,6 +93,7 @@ typedef struct DSMRegistryEntry
 {
 	char		name[NAMEDATALEN];
 	DSMREntryType type;
+	bool		initialized;
 	union
 	{
 		NamedDSMState dsm;
@@ -216,6 +217,7 @@ GetNamedDSMSegment(const char *name, size_t size,
 		dsm_segment *seg;
 
 		entry->type = DSMR_ENTRY_TYPE_DSM;
+		entry->initialized = false;
 
 		/* Initialize the segment. */
 		seg = dsm_create(size, 0);
@@ -228,7 +230,12 @@ GetNamedDSMSegment(const char *name, size_t size,
 
 		if (init_callback)
 			(*init_callback) (ret);
+
+		entry->initialized = true;
 	}
+	else if (!entry->initialized)
+		ereport(ERROR,
+				(errmsg("requested DSM segment failed initialization")));
 	else if (entry->type != DSMR_ENTRY_TYPE_DSM)
 		ereport(ERROR,
 				(errmsg("requested DSM segment does not match type of existing entry")));
@@ -297,6 +304,7 @@ GetNamedDSA(const char *name, bool *found)
 		NamedDSAState *state = &entry->dsa;
 
 		entry->type = DSMR_ENTRY_TYPE_DSA;
+		entry->initialized = false;
 
 		/* Initialize the LWLock tranche for the DSA. */
 		state->tranche = LWLockNewTrancheId(name);
@@ -308,7 +316,12 @@ GetNamedDSA(const char *name, bool *found)
 
 		/* Store handle for other backends to use. */
 		state->handle = dsa_get_handle(ret);
+
+		entry->initialized = true;
 	}
+	else if (!entry->initialized)
+		ereport(ERROR,
+				(errmsg("requested DSA failed initialization")));
 	else if (entry->type != DSMR_ENTRY_TYPE_DSA)
 		ereport(ERROR,
 				(errmsg("requested DSA does not match type of existing entry")));
@@ -372,6 +385,7 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		dsa_area   *dsa;
 
 		entry->type = DSMR_ENTRY_TYPE_DSH;
+		entry->initialized = false;
 
 		/* Initialize the LWLock tranche for the hash table. */
 		dsh_state->tranche = LWLockNewTrancheId(name);
@@ -389,7 +403,12 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		/* Store handles for other backends to use. */
 		dsh_state->dsa_handle = dsa_get_handle(dsa);
 		dsh_state->dsh_handle = dshash_get_hash_table_handle(ret);
+
+		entry->initialized = true;
 	}
+	else if (!entry->initialized)
+		ereport(ERROR,
+				(errmsg("requested DSHash failed initialization")));
 	else if (entry->type != DSMR_ENTRY_TYPE_DSH)
 		ereport(ERROR,
 				(errmsg("requested DSHash does not match type of existing entry")));
-- 
2.39.5 (Apple Git-154)

v2-0002-test_dsa-Avoid-leaking-LWLock-tranches.patchtext/plain; charset=us-asciiDownload
From 37ef7e1fd21cc1aa93cdc1af4fd84a26516faaa3 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Nov 2025 12:03:47 -0600
Subject: [PATCH v2 2/2] test_dsa: Avoid leaking LWLock tranches.

---
 src/test/modules/test_dsa/test_dsa.c | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index 01d5c6fa67f..21e4ce6a745 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -13,25 +13,35 @@
 #include "postgres.h"
 
 #include "fmgr.h"
+#include "storage/dsm_registry.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
 #include "utils/resowner.h"
 
 PG_MODULE_MAGIC;
 
+static void
+init_tranche(void *ptr)
+{
+	int		   *tranche_id = (int *) ptr;
+
+	*tranche_id = LWLockNewTrancheId("test_dsa");
+}
+
 /* Test basic DSA functionality */
 PG_FUNCTION_INFO_V1(test_dsa_basic);
 Datum
 test_dsa_basic(PG_FUNCTION_ARGS)
 {
-	int			tranche_id;
+	int		   *tranche_id;
+	bool		found;
 	dsa_area   *a;
 	dsa_pointer p[100];
 
-	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId("test_dsa");
+	tranche_id = GetNamedDSMSegment("test_dsa", sizeof(int),
+									init_tranche, &found);
 
-	a = dsa_create(tranche_id);
+	a = dsa_create(*tranche_id);
 	for (int i = 0; i < 100; i++)
 	{
 		p[i] = dsa_allocate(a, 1000);
@@ -62,17 +72,18 @@ PG_FUNCTION_INFO_V1(test_dsa_resowners);
 Datum
 test_dsa_resowners(PG_FUNCTION_ARGS)
 {
-	int			tranche_id;
+	int		   *tranche_id;
+	bool		found;
 	dsa_area   *a;
 	dsa_pointer p[10000];
 	ResourceOwner oldowner;
 	ResourceOwner childowner;
 
-	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId("test_dsa");
+	tranche_id = GetNamedDSMSegment("test_dsa", sizeof(int),
+									init_tranche, &found);
 
 	/* Create DSA in parent resource owner */
-	a = dsa_create(tranche_id);
+	a = dsa_create(*tranche_id);
 
 	/*
 	 * Switch to child resource owner, and do a bunch of allocations in the
-- 
2.39.5 (Apple Git-154)

#104Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#103)
Re: Improve LWLock tranche name visibility across backends

On Mon, Nov 10, 2025 at 12:05 PM Nathan Bossart
<nathandbossart@gmail.com> wrote:

On Mon, Nov 10, 2025 at 10:49:58AM -0600, Nathan Bossart wrote:

On Mon, Nov 10, 2025 at 10:26:17AM -0600, Nathan Bossart wrote:

It's probably a good idea to avoid tranche leaks, but IMHO there's room for
improvement in the DSM registry, too. IIUC the problem is that the DSM
segment is still being added to the registry and found by other backends
despite the initialization callback failing. My first instinct is that we
should keep track of whether the DSM segments/DSAs/dshash tables in the
registry have been fully initialized and to just ERROR in other backends
when attaching if they aren't. That shouldn't really happen in practice,
but it'd be good to avoid the strange errors, anyway.

0001 does this. When applied, Alexander's reproduction steps fail with

ERROR: requested DSM segment failed initialization

This one should probably get back-patched to v17, where the DSM registry
was introduced.

I tested this and it seems like a good idea. The first time
GetNamedDSMSegment is called, the entry is created but it's
left in an incomplete state due to an error. You will see the
error. The next time it's called, the caller encounters
"ERROR: requested DSM segment failed initialization"

psql -c "SELECT test_dsa_resowners() FROM generate_series(1, 256)" >/dev/null

psql -c "SELECT autoprewarm_dump_now()"
ERROR: maximum number of tranches already registered
DETAIL: No more than 256 tranches may be registered.

psql -c "SELECT autoprewarm_dump_now()"
ERROR: requested DSM segment failed initialization

There is no likely way to actually delete the boken
entry at that point, and even so, I am not sure how
useful that will be.

Maybe we can improve the error message by showing
the "name", like this, In case, there are nested calls.

```
else if (!entry->initialized)
{
ereport(ERROR,
(errmsg("requested DSM segment \"%s\" failed initialization", name)));
}
```

```
psql -c "SELECT autoprewarm_dump_now()"
ERROR: requested DSM segment "autoprewarm" failed initialization
```

BTW if we really wanted to avoid leaking tranches in test_dsa, we'd need to
store the ID in shared memory. Your patch helps in the case where a single
backend is repeatedly calling test_dsa_resowners(), but other backends
would still allocate their own tranche.

yes, I agree it should be in shared memory, and I did mention that
point in the v1 commit message, but was not sure if it was worth
doing so.

I don't see a strong need to fix

this on back-branches, given these functions run exactly once as part of a
test, but fixing it on master seems worthwhile so that extension authors
don't copy/paste this broken code.

0002 does this.

v2 LGTM.

--
Sami Imseih
Amazon Web Services (AWS)

#105Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#104)
2 attachment(s)
Re: Improve LWLock tranche name visibility across backends

On Mon, Nov 10, 2025 at 05:17:47PM -0600, Sami Imseih wrote:

Maybe we can improve the error message by showing
the "name", like this, In case, there are nested calls.

```
else if (!entry->initialized)
{
ereport(ERROR,
(errmsg("requested DSM segment \"%s\" failed initialization", name)));
}
```

Done.

--
nathan

Attachments:

v3-0001-DSM-registry-ERROR-if-entry-was-not-initialized.patchtext/plain; charset=us-asciiDownload
From 0b12db92e17dd315cc08b5a918196b685e3f6f61 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Nov 2025 11:43:51 -0600
Subject: [PATCH v3 1/2] DSM registry: ERROR if entry was not initialized.

---
 src/backend/storage/ipc/dsm_registry.c | 34 +++++++++++++++++++++++---
 1 file changed, 30 insertions(+), 4 deletions(-)

diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c
index a926b9c3f32..7eba8a8cffb 100644
--- a/src/backend/storage/ipc/dsm_registry.c
+++ b/src/backend/storage/ipc/dsm_registry.c
@@ -93,6 +93,7 @@ typedef struct DSMRegistryEntry
 {
 	char		name[NAMEDATALEN];
 	DSMREntryType type;
+	bool		initialized;
 	union
 	{
 		NamedDSMState dsm;
@@ -216,6 +217,7 @@ GetNamedDSMSegment(const char *name, size_t size,
 		dsm_segment *seg;
 
 		entry->type = DSMR_ENTRY_TYPE_DSM;
+		entry->initialized = false;
 
 		/* Initialize the segment. */
 		seg = dsm_create(size, 0);
@@ -228,13 +230,21 @@ GetNamedDSMSegment(const char *name, size_t size,
 
 		if (init_callback)
 			(*init_callback) (ret);
+
+		entry->initialized = true;
 	}
 	else if (entry->type != DSMR_ENTRY_TYPE_DSM)
 		ereport(ERROR,
-				(errmsg("requested DSM segment does not match type of existing entry")));
+				(errmsg("requested DSM segment \"%s\" does not match type of existing entry",
+						name)));
+	else if (!entry->initialized)
+		ereport(ERROR,
+				(errmsg("requested DSM segment \"%s\" failed initialization",
+						name)));
 	else if (entry->dsm.size != size)
 		ereport(ERROR,
-				(errmsg("requested DSM segment size does not match size of existing segment")));
+				(errmsg("requested DSM segment \"%s\" does not match size of existing entry",
+						name)));
 	else
 	{
 		NamedDSMState *state = &entry->dsm;
@@ -297,6 +307,7 @@ GetNamedDSA(const char *name, bool *found)
 		NamedDSAState *state = &entry->dsa;
 
 		entry->type = DSMR_ENTRY_TYPE_DSA;
+		entry->initialized = false;
 
 		/* Initialize the LWLock tranche for the DSA. */
 		state->tranche = LWLockNewTrancheId(name);
@@ -308,10 +319,17 @@ GetNamedDSA(const char *name, bool *found)
 
 		/* Store handle for other backends to use. */
 		state->handle = dsa_get_handle(ret);
+
+		entry->initialized = true;
 	}
 	else if (entry->type != DSMR_ENTRY_TYPE_DSA)
 		ereport(ERROR,
-				(errmsg("requested DSA does not match type of existing entry")));
+				(errmsg("requested DSA \"%s\" does not match type of existing entry",
+						name)));
+	else if (!entry->initialized)
+		ereport(ERROR,
+				(errmsg("requested DSA \"%s\" failed initialization",
+						name)));
 	else
 	{
 		NamedDSAState *state = &entry->dsa;
@@ -372,6 +390,7 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		dsa_area   *dsa;
 
 		entry->type = DSMR_ENTRY_TYPE_DSH;
+		entry->initialized = false;
 
 		/* Initialize the LWLock tranche for the hash table. */
 		dsh_state->tranche = LWLockNewTrancheId(name);
@@ -389,10 +408,17 @@ GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found)
 		/* Store handles for other backends to use. */
 		dsh_state->dsa_handle = dsa_get_handle(dsa);
 		dsh_state->dsh_handle = dshash_get_hash_table_handle(ret);
+
+		entry->initialized = true;
 	}
 	else if (entry->type != DSMR_ENTRY_TYPE_DSH)
 		ereport(ERROR,
-				(errmsg("requested DSHash does not match type of existing entry")));
+				(errmsg("requested DSHash \"%s\" does not match type of existing entry",
+						name)));
+	else if (!entry->initialized)
+		ereport(ERROR,
+				(errmsg("requested DSHash \"%s\" failed initialization",
+						name)));
 	else
 	{
 		NamedDSHState *dsh_state = &entry->dsh;
-- 
2.39.5 (Apple Git-154)

v3-0002-test_dsa-Avoid-leaking-LWLock-tranches.patchtext/plain; charset=us-asciiDownload
From bc876604668340706ead2dc64db2b692f0658618 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Nov 2025 12:03:47 -0600
Subject: [PATCH v3 2/2] test_dsa: Avoid leaking LWLock tranches.

---
 src/test/modules/test_dsa/test_dsa.c | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index 01d5c6fa67f..21e4ce6a745 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -13,25 +13,35 @@
 #include "postgres.h"
 
 #include "fmgr.h"
+#include "storage/dsm_registry.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
 #include "utils/resowner.h"
 
 PG_MODULE_MAGIC;
 
+static void
+init_tranche(void *ptr)
+{
+	int		   *tranche_id = (int *) ptr;
+
+	*tranche_id = LWLockNewTrancheId("test_dsa");
+}
+
 /* Test basic DSA functionality */
 PG_FUNCTION_INFO_V1(test_dsa_basic);
 Datum
 test_dsa_basic(PG_FUNCTION_ARGS)
 {
-	int			tranche_id;
+	int		   *tranche_id;
+	bool		found;
 	dsa_area   *a;
 	dsa_pointer p[100];
 
-	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId("test_dsa");
+	tranche_id = GetNamedDSMSegment("test_dsa", sizeof(int),
+									init_tranche, &found);
 
-	a = dsa_create(tranche_id);
+	a = dsa_create(*tranche_id);
 	for (int i = 0; i < 100; i++)
 	{
 		p[i] = dsa_allocate(a, 1000);
@@ -62,17 +72,18 @@ PG_FUNCTION_INFO_V1(test_dsa_resowners);
 Datum
 test_dsa_resowners(PG_FUNCTION_ARGS)
 {
-	int			tranche_id;
+	int		   *tranche_id;
+	bool		found;
 	dsa_area   *a;
 	dsa_pointer p[10000];
 	ResourceOwner oldowner;
 	ResourceOwner childowner;
 
-	/* XXX: this tranche is leaked */
-	tranche_id = LWLockNewTrancheId("test_dsa");
+	tranche_id = GetNamedDSMSegment("test_dsa", sizeof(int),
+									init_tranche, &found);
 
 	/* Create DSA in parent resource owner */
-	a = dsa_create(tranche_id);
+	a = dsa_create(*tranche_id);
 
 	/*
 	 * Switch to child resource owner, and do a bunch of allocations in the
-- 
2.39.5 (Apple Git-154)

#106Sami Imseih
samimseih@gmail.com
In reply to: Nathan Bossart (#105)
Re: Improve LWLock tranche name visibility across backends

Done.

LGTM.

--
Sami Imseih
Amazon Web Services (AWS)

#107Nathan Bossart
nathandbossart@gmail.com
In reply to: Sami Imseih (#106)
Re: Improve LWLock tranche name visibility across backends

On Tue, Nov 11, 2025 at 01:50:20PM -0600, Sami Imseih wrote:

LGTM.

Committed, thanks for reviewing.

--
nathan