public inbox for [email protected]  
help / color / mirror / Atom feed
From: warda Bibi <[email protected]>
To: Paul A Jungwirth <[email protected]>
Cc: Jim Jones <[email protected]>
Cc: Roman Khapov <[email protected]>
Cc: Andrey Borodin <[email protected]>
Cc: Kirill Reshke <[email protected]>
Cc: Daniel Gustafsson <[email protected]>
Cc: [email protected]
Subject: Re: Additional message in pg_terminate_backend
Date: Mon, 29 Jun 2026 15:21:23 +0500
Message-ID: <CAJqHjGriG=FiwWQ4M2fOVzUwwG0Rg87fhSQgf68heNhVh=SBgA@mail.gmail.com> (raw)
In-Reply-To: <CA+renyUsB3T07L14naz7P8gZz1zz8x=3MJcQnn5BWmwjx=FqSw@mail.gmail.com>
References: <[email protected]>
	<[email protected]>
	<[email protected]>
	<CALdSSPhU526xXqjsb=BPO689+qFJQDeimWrhOv=ehzveQsZJgw@mail.gmail.com>
	<[email protected]>
	<[email protected]>
	<[email protected]>
	<[email protected]>
	<[email protected]>
	<[email protected]>
	<CAJqHjGogwJd9+ESibKU1WYqhMK+7UaoTDF1oh99z9Q3wiEn0jw@mail.gmail.com>
	<CA+renyUsB3T07L14naz7P8gZz1zz8x=3MJcQnn5BWmwjx=FqSw@mail.gmail.com>

Hi Paul, Roman, and all,

Thank you for the thorough review, Paul. I have put together v7 addressing
all of the feedback below.

On Mon, Jun 23, 2026, Paul A Jungwirth wrote:

> Are these entries still needed? After 759b03b24c we can set defaults with
just pg_proc.dat.

Correct. v7 removes the system_functions.sql entries entirely; the DEFAULT
values are now specified only in pg_proc.dat, which is sufficient after
759b03b24c.

> What keeps the backend from being killed and the slot being re-used? v5
was checking the pid while holding the spinlock; perhaps we should still do
that.

BackendMsgSet() now acquires the slot spinlock before checking slot->pid ==
pid, ensuring that a slot reused by a new backend after the target exited
cannot be written to. The O(1) procno indexing introduced in v6 is retained.

> I don't think there is any reason to keep the _internal helper functions.

pg_cancel_backend_internal() and pg_terminate_backend_internal() have been
inlined into their respective callers, since each had only a single call
site.

> What if we emit msg as an errdetail?

Done. Both paths now use errdetail("%s", msg) instead of appending the
message to errmsg().

> Also, parallel workers receive the generic message as they hit the

> IsBackgroundWorker branch in ProcessInterrupts(), which bypasses

> BackendMsgGet(). This is fine since parallel workers are ephemeral,

> but should it be documented in the docs?

The documentation now notes that parallel workers always receive the
generic termination message.

> The v6 patch looks like an incremental patch on top of a previous

> patch (v5 I think). But v5, v4, and v3 no longer apply. Would you mind

> rebasing and putting all the changes into one patch?

Done. v7 is a single self-contained patch rebased on the current master
(b574fec00f2).

Testing:

All 7 subtests in src/test/modules/test_misc/t/014_backend_msg.pl pass on
Linux (Ubuntu 24.04, arm64):

t/014_backend_msg.pl .. ok

Result: PASS

Patch attached.

Best Regards,

Warda Bibi




On Tue, Jun 23, 2026 at 11:17 PM Paul A Jungwirth <
[email protected]> wrote:

> We talked about this patch for the Patch Review Workshop, and I
> thought I would share a few thoughts.
>
> On Sun, Apr 5, 2026 at 12:27 PM warda Bibi <[email protected]> wrote:
> >
> > We agree with Jim's point of view that the v3 approach is better. v5
> > introduced separate _msg overloads (OID 8223 for
> > pg_cancel_backend_msg, OID 8222 for pg_terminate_backend_msg), which
> > adds catalog bloat and forces callers to use a different function
> > name:
> >
> > > +{ oid => '8223', descr => 'cancel a server process\' current query',
> > >   proname => 'pg_cancel_backend', provolatile => 'v', prorettype =>
> 'bool',
> > >   proargtypes => 'int4 text', prosrc => 'pg_cancel_backend_msg' },
> > > +{ oid => '8222', descr => 'terminate a server process',
> > >   proname => 'pg_terminate_backend', provolatile => 'v', prorettype =>
> 'bool',
> > >   proargtypes => 'int4 int8 text', proargnames =>
> '{pid,timeout,message}',
> > >   prosrc => 'pg_terminate_backend_msg' },
> >
> > V3 keeps one OID per function and stays backward-compatible. v6
> > restores the v3 approach.
>
> I agree about adding defaults to the existing functions instead of
> introducing new ones. This doesn't need to be an extension, so there
> is no need to have separate functions.
>
> > --- a/src/backend/catalog/system_functions.sql
> > +++ b/src/backend/catalog/system_functions.sql
> > @@ -378,6 +378,16 @@ BEGIN ATOMIC
> >  END;
> >
> > +CREATE OR REPLACE FUNCTION
> > +  pg_cancel_backend(pid integer, message text DEFAULT '')
> > +  RETURNS boolean STRICT VOLATILE LANGUAGE INTERNAL AS
> 'pg_cancel_backend'
> > +  PARALLEL SAFE;
> > +
> > +CREATE OR REPLACE FUNCTION
> > +  pg_terminate_backend(pid integer, timeout int8 DEFAULT 0, message
> > text DEFAULT '')
> > +  RETURNS boolean STRICT VOLATILE LANGUAGE INTERNAL AS
> 'pg_terminate_backend'
> > +  PARALLEL SAFE;
> > +
>
> Are these entries still needed? After 759b03b24c we can set defaults
> with just pg_proc.dat.
>
> > The current approach scans all MaxBackends slots by PID twice. Since
> > pg_signal_backend() already calls BackendPidGetProc(pid) and has the
> > PGPROC* in hand, the PGPROC index into allProcs[ ] is the ProcNumber,
> > which is also the direct index into BackendMsgSlots[]. So v6 changes
> > the signature to BackendMsgSet(ProcNumber procno, ...) and the call in
> > pg_signal_backend() passes GetNumberFromPGProc(proc), making the
> > lookup O(1).
>
> This seems like a nice improvement. But is there a race condition?
> What keeps the backend from being killed (or just finishing the
> connection) and the slot being re-used? v5 was checking the pid while
> holding the spinlock; perhaps we should still do that.
>
> > The _internal helper functions (pg_cancel_backend_internal,
> > pg_terminate_backend_internal) each have only one call site. What is
> > the intent behind keeping them separate, or can they be inlined
> > directly into pg_cancel_backend() and pg_terminate_backend()?
>
> Since we aren't implementing this as an extension, I don't think there
> is any reason to keep the _internal helper functions.
>
> > Andrey noted that:
> >
> > > We have a race condition if many backends cancel same backend.
> > > Won't they mess each other's reason?
> >
> > The spinlock in BackendMsgSet protects the write, but there is no
> > atomicity between writing the slot and delivering the signal. Another
> > backend can overwrite the slot between one writer's write and the
> > target reading it. Would it be valuable to define the behavior
> > explicitly, for example last-writer-wins, or document the limitation?
>
> I think last-writer-wins is okay for canceling the same backend twice,
> but we should make sure the slot is still owned by the right pid.
>
> > Andrey also raised this about translation:
> >
> > > Keep in mind that Postgres literals are translated into many languages.
> > > So text ought to be clear enough for translators to build a sentence
> > > that precedes termination reason.
> >
> > The current pattern:
> >
> > >   errmsg("terminating connection due to administrator command: %s",
> msg)
> >
> > The colon-append pattern breaks in languages where the reason clause
> > is structured differently. Any suggestions on how to approach this, or
> > how similar cases are handled elsewhere in the codebase?
>
> What if we emit msg as an errdetail? That seems to resolve the
> translation difficulties, and it makes the logs more structured.
>
> > Also, parallel workers receive the generic message as they hit the
> > IsBackgroundWorker branch in ProcessInterrupts(), which bypasses
> > BackendMsgGet(). This is fine since parallel workers are ephemeral,
> > but should it be documented in the docs?
>
> That seems okay. Agreed about documenting it.
>
> The v6 patch looks like an incremental patch on top of a previous
> patch (v5 I think). But v5, v4, and v3 no longer apply. Would you mind
> rebasing and putting all the changes into one patch? I think your
> suggestions are extensive enough that a single file would be easier to
> review.
>
> Yours,
>
> --
> Paul              ~{:-)
> [email protected]
>


Attachments:

  [application/octet-stream] v7-0001-message-in-pg_terminate_backend-and-pg_cancel_backend.patch (20.5K, ../CAJqHjGriG=FiwWQ4M2fOVzUwwG0Rg87fhSQgf68heNhVh=SBgA@mail.gmail.com/3-v7-0001-message-in-pg_terminate_backend-and-pg_cancel_backend.patch)
  download | inline diff:
From 5a754e4408e4c14053dd26bf07ac33a35365c0fc Mon Sep 17 00:00:00 2001
From: Warda Bibi <[email protected]>
Date: Sun, 28 Jun 2026 22:42:02 +0500
Subject: [PATCH] message in pg_terminate_backend and pg_cancel_backend

Sometimes it is useful to terminate or cancel a backend with an
additional message explaining the reason, so the client sees something
more informative than the generic error text.

This patch adds an optional message argument to pg_terminate_backend()
and pg_cancel_backend(), folded into the existing functions via DEFAULT
parameters so no new catalog entries are needed:

  pg_cancel_backend(pid integer, message text DEFAULT '')
  pg_terminate_backend(pid integer, timeout int8 DEFAULT 0, message text DEFAULT '')

The message is stored in a dedicated shared memory region
(BackendMsgSlots, one slot per MaxBackends indexed by ProcNumber) and
read by the target backend in ProcessInterrupts() before the
ereport(FATAL/ERROR). The message is emitted as errdetail for clean
i18n. Unicode-safe truncation is applied via pg_mbcliplen() if the
message exceeds BACKEND_MSG_MAX_LEN bytes, with a NOTICE emitted to
the caller.

The slot is indexed directly by ProcNumber (O(1)) but the target pid
is verified under the spinlock to guard against slot reuse if the
backend exits between signal dispatch and slot write.

Author: Daniel Gustafsson <[email protected]>
Author: Roman Khapov <[email protected]>
Author: Warda Bibi <[email protected]>
Reviewed-by: Kirill Reshke <[email protected]>
Reviewed-by: Jim Jones <[email protected]>
Reviewed-by: Andrey Borodin <[email protected]>
Reviewed-by: Paul Jungwirth <[email protected]>
---
 doc/src/sgml/func/func-admin.sgml             |  15 +-
 src/backend/storage/ipc/ipci.c                |   3 +
 src/backend/storage/ipc/signalfuncs.c         |  31 ++-
 src/backend/tcop/postgres.c                   |  37 +++-
 src/backend/utils/init/postinit.c             |   3 +
 src/backend/utils/misc/Makefile               |   1 +
 src/backend/utils/misc/backend_msg.c          | 191 ++++++++++++++++++
 src/backend/utils/misc/meson.build            |   1 +
 src/include/catalog/pg_proc.dat               |   8 +-
 src/include/utils/backend_msg.h               |  29 +++
 src/test/modules/test_misc/meson.build        |   1 +
 .../modules/test_misc/t/011_backend_msg.pl    |  62 ++++++
 12 files changed, 363 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/utils/misc/backend_msg.c
 create mode 100644 src/include/utils/backend_msg.h
 create mode 100644 src/test/modules/test_misc/t/011_backend_msg.pl

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 3ac81905d1f..de5b1314408 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -147,7 +147,7 @@
         <indexterm>
          <primary>pg_cancel_backend</primary>
         </indexterm>
-        <function>pg_cancel_backend</function> ( <parameter>pid</parameter> <type>integer</type> )
+        <function>pg_cancel_backend</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>message</parameter> <type>text</type> <literal>DEFAULT</literal> <literal>''</literal> )
         <returnvalue>boolean</returnvalue>
        </para>
        <para>
@@ -160,6 +160,12 @@
         <literal>pg_signal_autovacuum_worker</literal> are permitted to
         cancel autovacuum worker processes, which are otherwise considered
         superuser backends.
+       </para>
+       <para>
+        If <parameter>message</parameter> is specified and non-empty, it is
+        included as detail in the error reported to the canceled session.
+        If multiple backends simultaneously cancel the same target, only one
+        message will be delivered.
        </para></entry>
       </row>
 
@@ -225,7 +231,7 @@
         <indexterm>
          <primary>pg_terminate_backend</primary>
         </indexterm>
-        <function>pg_terminate_backend</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>timeout</parameter> <type>bigint</type> <literal>DEFAULT</literal> <literal>0</literal> )
+        <function>pg_terminate_backend</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>timeout</parameter> <type>bigint</type> <literal>DEFAULT</literal> <literal>0</literal>, <parameter>message</parameter> <type>text</type> <literal>DEFAULT</literal> <literal>''</literal> )
         <returnvalue>boolean</returnvalue>
        </para>
        <para>
@@ -249,6 +255,11 @@
         the process is terminated, the function
         returns <literal>true</literal>.  On timeout, a warning is emitted and
         <literal>false</literal> is returned.
+        If <parameter>message</parameter> is specified and non-empty, it is
+        included as detail in the fatal error reported to the terminated
+        session.  If multiple backends simultaneously terminate the same
+        target, only one message will be delivered.  Parallel workers always
+        receive the generic termination message regardless of this parameter.
        </para></entry>
       </row>
      </tbody>
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 1f7e933d500..e71bdec588c 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -51,6 +51,7 @@
 #include "storage/procsignal.h"
 #include "storage/sinvaladt.h"
 #include "utils/guc.h"
+#include "utils/backend_msg.h"
 #include "utils/injection_point.h"
 
 /* GUCs */
@@ -140,6 +141,7 @@ CalculateShmemSize(void)
 	size = add_size(size, SlotSyncShmemSize());
 	size = add_size(size, AioShmemSize());
 	size = add_size(size, WaitLSNShmemSize());
+	size = add_size(size, BackendMsgShmemSize());
 	size = add_size(size, LogicalDecodingCtlShmemSize());
 
 	/* include additional requested shmem from preload libraries */
@@ -327,6 +329,7 @@ CreateOrAttachShmemStructs(void)
 	InjectionPointShmemInit();
 	AioShmemInit();
 	WaitLSNShmemInit();
+	BackendMsgShmemInit();
 	LogicalDecodingCtlShmemInit();
 }
 
diff --git a/src/backend/storage/ipc/signalfuncs.c b/src/backend/storage/ipc/signalfuncs.c
index d48b4fe3799..c6b871d1edb 100644
--- a/src/backend/storage/ipc/signalfuncs.c
+++ b/src/backend/storage/ipc/signalfuncs.c
@@ -24,6 +24,8 @@
 #include "storage/proc.h"
 #include "storage/procarray.h"
 #include "utils/acl.h"
+#include "utils/backend_msg.h"
+#include "utils/builtins.h"
 #include "utils/fmgrprotos.h"
 
 
@@ -48,7 +50,7 @@
 #define SIGNAL_BACKEND_NOSUPERUSER 3
 #define SIGNAL_BACKEND_NOAUTOVAC 4
 static int
-pg_signal_backend(int pid, int sig)
+pg_signal_backend(int pid, int sig, const char *msg)
 {
 	PGPROC	   *proc = BackendPidGetProc(pid);
 
@@ -108,6 +110,15 @@ pg_signal_backend(int pid, int sig)
 	 * too unlikely to worry about.
 	 */
 
+	if (msg != NULL && msg[0] != '\0')
+	{
+		int			r = BackendMsgSet(GetNumberFromPGProc(proc), proc->pid, msg);
+
+		if (r != -1 && r != (int) strlen(msg))
+			ereport(NOTICE,
+					(errmsg("message is too long, truncated to %d bytes", r)));
+	}
+
 	/* If we have setsid(), signal the backend's whole process group */
 #ifdef HAVE_SETSID
 	if (kill(-pid, sig))
@@ -132,7 +143,11 @@ pg_signal_backend(int pid, int sig)
 Datum
 pg_cancel_backend(PG_FUNCTION_ARGS)
 {
-	int			r = pg_signal_backend(PG_GETARG_INT32(0), SIGINT);
+	int			pid = PG_GETARG_INT32(0);
+	char	   *msg = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	int			r = pg_signal_backend(pid, SIGINT, msg);
+
+	pfree(msg);
 
 	if (r == SIGNAL_BACKEND_NOSUPERUSER)
 		ereport(ERROR,
@@ -233,19 +248,19 @@ pg_wait_until_termination(int pid, int64 timeout)
 Datum
 pg_terminate_backend(PG_FUNCTION_ARGS)
 {
-	int			pid;
+	int			pid = PG_GETARG_INT32(0);
+	int			timeout = PG_GETARG_INT64(1); /* milliseconds */
+	char	   *msg = text_to_cstring(PG_GETARG_TEXT_PP(2));
 	int			r;
-	int			timeout;		/* milliseconds */
-
-	pid = PG_GETARG_INT32(0);
-	timeout = PG_GETARG_INT64(1);
 
 	if (timeout < 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
 				 errmsg("\"timeout\" must not be negative")));
 
-	r = pg_signal_backend(pid, SIGTERM);
+	r = pg_signal_backend(pid, SIGTERM, msg);
+
+	pfree(msg);
 
 	if (r == SIGNAL_BACKEND_NOSUPERUSER)
 		ereport(ERROR,
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index d01a09dd0c4..e3e23733479 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -81,6 +81,7 @@
 #include "utils/snapmgr.h"
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
+#include "utils/backend_msg.h"
 #include "utils/varlena.h"
 
 /* ----------------
@@ -3390,9 +3391,22 @@ ProcessInterrupts(void)
 			proc_exit(0);
 		}
 		else
-			ereport(FATAL,
-					(errcode(ERRCODE_ADMIN_SHUTDOWN),
-					 errmsg("terminating connection due to administrator command")));
+		{
+			if (BackendMsgIsSet())
+			{
+				char		msg[BACKEND_MSG_MAX_LEN];
+
+				BackendMsgGet(msg, sizeof(msg));
+				ereport(FATAL,
+						(errcode(ERRCODE_ADMIN_SHUTDOWN),
+						 errmsg("terminating connection due to administrator command"),
+						 errdetail("%s", msg)));
+			}
+			else
+				ereport(FATAL,
+						(errcode(ERRCODE_ADMIN_SHUTDOWN),
+						 errmsg("terminating connection due to administrator command")));
+		}
 	}
 
 	if (CheckClientConnectionPending)
@@ -3500,9 +3514,20 @@ ProcessInterrupts(void)
 		if (!DoingCommandRead)
 		{
 			LockErrorCleanup();
-			ereport(ERROR,
-					(errcode(ERRCODE_QUERY_CANCELED),
-					 errmsg("canceling statement due to user request")));
+			if (BackendMsgIsSet())
+			{
+				char		msg[BACKEND_MSG_MAX_LEN];
+
+				BackendMsgGet(msg, sizeof(msg));
+				ereport(ERROR,
+						(errcode(ERRCODE_QUERY_CANCELED),
+						 errmsg("canceling statement due to user request"),
+						 errdetail("%s", msg)));
+			}
+			else
+				ereport(ERROR,
+						(errcode(ERRCODE_QUERY_CANCELED),
+						 errmsg("canceling statement due to user request")));
 		}
 	}
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index b59e08605cc..9b14f51dcfd 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -68,6 +68,7 @@
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
+#include "utils/backend_msg.h"
 #include "utils/timeout.h"
 
 /* has this backend called EmitConnectionWarnings()? */
@@ -912,6 +913,8 @@ InitPostgres(const char *in_dbname, Oid dboid,
 		am_superuser = superuser();
 	}
 
+	BackendMsgInit(MyProcNumber);
+
 	/* Report any SSL/GSS details for the session. */
 	if (MyProcPort != NULL)
 	{
diff --git a/src/backend/utils/misc/Makefile b/src/backend/utils/misc/Makefile
index f142d17178b..60c4685541c 100644
--- a/src/backend/utils/misc/Makefile
+++ b/src/backend/utils/misc/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
 override CPPFLAGS := -I. -I$(srcdir) $(CPPFLAGS)
 
 OBJS = \
+	backend_msg.o \
 	conffiles.o \
 	guc.o \
 	guc-file.o \
diff --git a/src/backend/utils/misc/backend_msg.c b/src/backend/utils/misc/backend_msg.c
new file mode 100644
index 00000000000..940b9fd7c33
--- /dev/null
+++ b/src/backend/utils/misc/backend_msg.c
@@ -0,0 +1,191 @@
+/*-------------------------------------------------------------------------
+ *
+ * backend_msg.c
+ *	  Shared memory region for passing messages to backend processes.
+ *
+ * When pg_terminate_backend() or pg_cancel_backend() is called with a
+ * non-empty message, the signaling backend writes the message into the
+ * target's BackendMsgSlot before delivering the signal.  The target reads
+ * it in ProcessInterrupts() and includes it as errdetail in the FATAL/ERROR.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/misc/backend_msg.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "miscadmin.h"
+#include "mb/pg_wchar.h"
+#include "storage/ipc.h"
+#include "storage/shmem.h"
+#include "storage/spin.h"
+#include "utils/backend_msg.h"
+
+typedef struct
+{
+	pid_t		pid;
+	slock_t		lock;
+	char		msg[BACKEND_MSG_MAX_LEN];
+} BackendMsgSlot;
+
+
+static BackendMsgSlot *BackendMsgSlots;
+static BackendMsgSlot *MyBackendMsgSlot;
+
+static void
+backend_msg_slot_clean(int code, Datum arg)
+{
+	Assert(MyBackendMsgSlot != NULL);
+
+	SpinLockAcquire(&MyBackendMsgSlot->lock);
+
+	MyBackendMsgSlot->msg[0] = '\0';
+	MyBackendMsgSlot->pid = 0;
+
+	SpinLockRelease(&MyBackendMsgSlot->lock);
+
+	MyBackendMsgSlot = NULL;
+}
+
+/*
+ * BackendMsgShmemInit
+ *		Allocate and initialize the BackendMsgSlots shared memory array.
+ *		Called once by the postmaster at startup.
+ */
+void
+BackendMsgShmemInit(void)
+{
+	Size		size;
+	bool		found;
+
+	size = BackendMsgShmemSize();
+	BackendMsgSlots = ShmemInitStruct("BackendMsgSlots", size, &found);
+
+	if (found)
+		return;
+
+	memset(BackendMsgSlots, 0, size);
+
+	for (int i = 0; i < MaxBackends; ++i)
+		SpinLockInit(&BackendMsgSlots[i].lock);
+}
+
+/*
+ * BackendMsgShmemSize
+ *		Compute the shared memory size required for BackendMsgSlots.
+ */
+Size
+BackendMsgShmemSize(void)
+{
+	return mul_size(MaxBackends, sizeof(BackendMsgSlot));
+}
+
+/*
+ * BackendMsgInit
+ *		Initialize the slot for the current backend.  Must be called once
+ *		per backend after MyProcPid and MyProcNumber are set.
+ */
+void
+BackendMsgInit(int id)
+{
+	BackendMsgSlot *slot;
+
+	slot = &BackendMsgSlots[id];
+
+	slot->msg[0] = '\0';
+	slot->pid = MyProcPid;
+
+	MyBackendMsgSlot = slot;
+
+	on_shmem_exit(backend_msg_slot_clean, Int32GetDatum(0) /* not used */ );
+}
+
+/*
+ * Write msg into the slot for the backend identified by (procno, pid).
+ *
+ * We index directly by procno (O(1)) but verify slot->pid under the spinlock
+ * to guard against the slot being reused by a new backend after the target
+ * exited.
+ *
+ * Returns the number of bytes written, 0 if msg is empty, or -1 if the slot
+ * is no longer owned by the expected pid.
+ */
+int
+BackendMsgSet(ProcNumber procno, pid_t pid, const char *msg)
+{
+	BackendMsgSlot *slot;
+	int			len;
+
+	if (msg == NULL || msg[0] == '\0')
+		return 0;
+
+	slot = &BackendMsgSlots[procno];
+
+	SpinLockAcquire(&slot->lock);
+
+	if (slot->pid != pid)
+	{
+		SpinLockRelease(&slot->lock);
+		ereport(DEBUG1,
+				(errmsg("could not set message for backend with PID %d: no longer exists",
+						pid)));
+		return -1;
+	}
+
+	len = pg_mbcliplen(msg, strlen(msg), sizeof(slot->msg) - 1);
+	memcpy(slot->msg, msg, len);
+	slot->msg[len] = '\0';
+
+	SpinLockRelease(&slot->lock);
+
+	return len;
+}
+
+/*
+ * BackendMsgGet
+ *		Copy the pending message (if any) into buf and clear the slot.
+ *		Returns the number of bytes copied.  Called by the target backend
+ *		in ProcessInterrupts() before issuing the FATAL/ERROR.
+ */
+int
+BackendMsgGet(char *buf, int max_len)
+{
+	int			len;
+
+	if (MyBackendMsgSlot == NULL)
+		return 0;
+
+	SpinLockAcquire(&MyBackendMsgSlot->lock);
+
+	len = strlcpy(buf, MyBackendMsgSlot->msg, max_len);
+	memset(MyBackendMsgSlot->msg, '\0', sizeof(MyBackendMsgSlot->msg));
+
+	SpinLockRelease(&MyBackendMsgSlot->lock);
+
+	return len;
+}
+
+/*
+ * BackendMsgIsSet
+ *		Return true if a non-empty message is waiting in the current
+ *		backend's slot.
+ */
+bool
+BackendMsgIsSet(void)
+{
+	bool		result = false;
+
+	if (MyBackendMsgSlot == NULL)
+		return false;
+
+	SpinLockAcquire(&MyBackendMsgSlot->lock);
+	result = MyBackendMsgSlot->msg[0] != '\0';
+	SpinLockRelease(&MyBackendMsgSlot->lock);
+
+	return result;
+}
diff --git a/src/backend/utils/misc/meson.build b/src/backend/utils/misc/meson.build
index 232e74d0af9..831bf6c6bab 100644
--- a/src/backend/utils/misc/meson.build
+++ b/src/backend/utils/misc/meson.build
@@ -1,6 +1,7 @@
 # Copyright (c) 2022-2026, PostgreSQL Global Development Group
 
 backend_sources += files(
+  'backend_msg.c',
   'conffiles.c',
   'guc.c',
   'guc_funcs.c',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dac40992cbc..3833fc4ba2c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6727,11 +6727,13 @@
 
 { oid => '2171', descr => 'cancel a server process\' current query',
   proname => 'pg_cancel_backend', provolatile => 'v', prorettype => 'bool',
-  proargtypes => 'int4', prosrc => 'pg_cancel_backend' },
+  proargtypes => 'int4 text', proargnames => '{pid,message}',
+  proargdefaults => '{""}',
+  prosrc => 'pg_cancel_backend' },
 { oid => '2096', descr => 'terminate a server process',
   proname => 'pg_terminate_backend', provolatile => 'v', prorettype => 'bool',
-  proargtypes => 'int4 int8', proargnames => '{pid,timeout}',
-  proargdefaults => '{0}',
+  proargtypes => 'int4 int8 text', proargnames => '{pid,timeout,message}',
+  proargdefaults => '{0,""}',
   prosrc => 'pg_terminate_backend' },
 { oid => '2172', descr => 'prepare for taking an online backup',
   proname => 'pg_backup_start', provolatile => 'v', proparallel => 'r',
diff --git a/src/include/utils/backend_msg.h b/src/include/utils/backend_msg.h
new file mode 100644
index 00000000000..735c56a9c61
--- /dev/null
+++ b/src/include/utils/backend_msg.h
@@ -0,0 +1,29 @@
+/*--------------------------------------------------------------------
+ * backend_msg.h
+ *
+ * Utility to pass additional message to backend processes.
+ * Ex: cancel or terminate messages
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/backend_msg.h
+ *
+ *--------------------------------------------------------------------
+ */
+
+#ifndef BACKEND_MSG_H
+#define BACKEND_MSG_H
+
+#include "storage/proc.h"
+
+#define BACKEND_MSG_MAX_LEN 128
+
+extern void BackendMsgShmemInit(void);
+extern Size BackendMsgShmemSize(void);
+extern void BackendMsgInit(int id);
+extern int	BackendMsgSet(ProcNumber procno, pid_t pid, const char *msg);
+extern int	BackendMsgGet(char *buf, int max_len);
+extern bool BackendMsgIsSet(void);
+
+#endif							/* BACKEND_MSG_H */
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 6e8db1621a7..674675b7ce1 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -19,6 +19,7 @@ tests += {
       't/008_replslot_single_user.pl',
       't/009_log_temp_files.pl',
       't/010_index_concurrently_upsert.pl',
+      't/011_backend_msg.pl',
     ],
     # The injection points are cluster-wide, so disable installcheck
     'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/011_backend_msg.pl b/src/test/modules/test_misc/t/011_backend_msg.pl
new file mode 100644
index 00000000000..38de87d0033
--- /dev/null
+++ b/src/test/modules/test_misc/t/011_backend_msg.pl
@@ -0,0 +1,62 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Check that messages are passed to backends by
+# pg_terminate_backend, pg_cancel_backend
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+$node->start;
+
+my ($stdout, $stderr);
+
+# pg_terminate_backend with message
+$node->psql('postgres',
+	q[SELECT pg_terminate_backend(pg_backend_pid(), 0, 'Have you seen my coffee cup?');],
+	stdout => \$stdout, stderr => \$stderr);
+like($stderr, qr/Have you seen my coffee cup\?/,
+	"terminate message appears in DETAIL of FATAL");
+
+# pg_terminate_backend with empty message uses generic text
+$stdout = '';
+$stderr = '';
+$node->psql('postgres',
+	q[SELECT pg_terminate_backend(pg_backend_pid(), 0, '');],
+	stdout => \$stdout, stderr => \$stderr);
+like($stderr, qr/terminating connection due to administrator command/,
+	"terminate without message uses generic text");
+unlike($stderr, qr/DETAIL/,
+	"no DETAIL line when message is empty");
+
+# pg_cancel_backend with message
+$stdout = '';
+$stderr = '';
+$node->psql('postgres',
+	q[SELECT pg_cancel_backend(pg_backend_pid(), 'You have to wear some ridiculous tie');],
+	stdout => \$stdout, stderr => \$stderr);
+like($stderr, qr/You have to wear some ridiculous tie/,
+	"cancel message appears in DETAIL of ERROR");
+
+# Long message is truncated with a NOTICE
+$stdout = '';
+$stderr = '';
+my $longstr = "a" x 200;
+my $truncated = "a" x 127;
+$node->psql('postgres',
+	qq[SELECT pg_terminate_backend(pg_backend_pid(), 0, '$longstr');],
+	stdout => \$stdout, stderr => \$stderr);
+like($stderr, qr/NOTICE:  message is too long, truncated to 127 bytes/,
+	"NOTICE emitted for truncated message");
+like($stderr, qr/\Q$truncated\E/,
+	"truncated message (127 bytes) appears in DETAIL");
+unlike($stderr, qr/\Q$longstr\E/,
+	"full message is not passed through");
+
+$node->stop;
+
+done_testing();
-- 
2.51.0



view thread (18+ messages)

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]
  Subject: Re: Additional message in pg_terminate_backend
  In-Reply-To: <CAJqHjGriG=FiwWQ4M2fOVzUwwG0Rg87fhSQgf68heNhVh=SBgA@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox