From c335e0c6b19982c9ea45b1979be4eeaa4ab3ece3 Mon Sep 17 00:00:00 2001
From: roman khapov <r.khapov@ya.ru>
Date: Tue, 3 Feb 2026 07:37:15 +0000
Subject: [PATCH v4] message in pg_terminate_backend and pg_cancel_backend

Sometimes it is useful to terminate some backend
process with additional message from admin.

This patch introduces a new argument, message to
pg_terminate_backend and pg_cancel_backend.
The message, that will be passed into
FATAL/ERROR packet when terminating/canceling backend.

To do that, the patch introduces new module: BackendMsg - shared memory
region that holds pairs of (message, pid) which are checked in ProcessInterrupts()

Ex. of usage:
postgres=# select pg_terminate_backend(pg_backend_pid(), 0, 'Some message');
FATAL:  terminating connection due to administrator command: Some message

Author: Daniel Gustafsson <daniel@yesql.se>
Author: Roman Khapov <r.khapov@ya.ru>
Reviewed-by: Kirill Reshke <reshkekirill@gmail.com>
Reviewed-by: Jim Jones <jim.jones@uni-muenster.de>
---
 doc/src/sgml/func/func-admin.sgml             |  10 +-
 src/backend/catalog/system_functions.sql      |   7 +-
 src/backend/storage/ipc/ipci.c                |   3 +
 src/backend/storage/ipc/signalfuncs.c         |  54 ++++--
 src/backend/tcop/postgres.c                   |  32 +++-
 src/backend/utils/init/postinit.c             |   2 +
 src/backend/utils/misc/Makefile               |   3 +-
 src/backend/utils/misc/backend_msg.c          | 156 ++++++++++++++++++
 src/backend/utils/misc/meson.build            |   1 +
 src/include/utils/backend_msg.h               |  28 ++++
 src/test/modules/test_misc/meson.build        |   1 +
 .../modules/test_misc/t/011_backend_msg.pl    |  42 +++++
 12 files changed, 320 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..40bc0947f75 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>test</type> <literal>DEFAULT</literal> <literal>''</literal> )
         <returnvalue>boolean</returnvalue>
        </para>
        <para>
@@ -160,6 +160,9 @@
         <literal>pg_signal_autovacuum_worker</literal> are permitted to
         cancel autovacuum worker processes, which are otherwise considered
         superuser backends.
+        If <parameter>message</parameter> is specified and non-empty, this
+        string will be passed as additional message in ERROR text for
+        canceled backend.
        </para></entry>
       </row>
 
@@ -225,7 +228,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>test</type> <literal>DEFAULT</literal> <literal>''</literal>)
         <returnvalue>boolean</returnvalue>
        </para>
        <para>
@@ -249,6 +252,9 @@
         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, this
+        string will be passed as additional message in FATAL text for
+        terminated backend.
        </para></entry>
       </row>
      </tbody>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index eb9e31ae1bf..5e2c1386193 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -400,7 +400,12 @@ CREATE OR REPLACE FUNCTION
   PARALLEL SAFE;
 
 CREATE OR REPLACE FUNCTION
-  pg_terminate_backend(pid integer, timeout int8 DEFAULT 0)
+  pg_cancel_backend(pid integer, msg 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, msg text DEFAULT '')
   RETURNS boolean STRICT VOLATILE LANGUAGE INTERNAL AS 'pg_terminate_backend'
   PARALLEL SAFE;
 
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 1f7e933d500..9f50adf030c 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -52,6 +52,7 @@
 #include "storage/sinvaladt.h"
 #include "utils/guc.h"
 #include "utils/injection_point.h"
+#include "utils/backend_msg.h"
 
 /* GUCs */
 int			shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE;
@@ -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, BackendStatusShmemSize());
 	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 6f7759cd720..b66aadc7c0e 100644
--- a/src/backend/storage/ipc/signalfuncs.c
+++ b/src/backend/storage/ipc/signalfuncs.c
@@ -25,6 +25,8 @@
 #include "storage/procarray.h"
 #include "utils/acl.h"
 #include "utils/fmgrprotos.h"
+#include "utils/builtins.h"
+#include "utils/backend_msg.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);
 
@@ -111,6 +113,15 @@ pg_signal_backend(int pid, int sig)
 	 * too unlikely to worry about.
 	 */
 
+	if (msg != NULL && msg[0] != '\0')
+	{
+		int		r = BackendMsgSet(pid, msg);
+
+		if (r != -1 && r != strlen(msg))
+			ereport(NOTICE,
+					(errmsg("message is too long, truncated to %d", r)));
+	}
+
 	/* If we have setsid(), signal the backend's whole process group */
 #ifdef HAVE_SETSID
 	if (kill(-pid, sig))
@@ -132,10 +143,10 @@ pg_signal_backend(int pid, int sig)
  *
  * Note that only superusers can signal superuser-owned processes.
  */
-Datum
-pg_cancel_backend(PG_FUNCTION_ARGS)
+static Datum
+pg_cancel_backend_internal(pid_t pid, const char *msg)
 {
-	int			r = pg_signal_backend(PG_GETARG_INT32(0), SIGINT);
+	int			r = pg_signal_backend(pid, SIGINT, msg);
 
 	if (r == SIGNAL_BACKEND_NOSUPERUSER)
 		ereport(ERROR,
@@ -161,6 +172,17 @@ pg_cancel_backend(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(r == SIGNAL_BACKEND_SUCCESS);
 }
 
+Datum pg_cancel_backend(PG_FUNCTION_ARGS)
+{
+	int			pid;
+	char	   *msg;
+
+	pid = PG_GETARG_INT32(0);
+	msg = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	return pg_cancel_backend_internal(pid, msg);
+}
+
 /*
  * Wait until there is no backend process with the given PID and return true.
  * On timeout, a warning is emitted and false is returned.
@@ -233,22 +255,17 @@ pg_wait_until_termination(int pid, int64 timeout)
  *
  * Note that only superusers can signal superuser-owned processes.
  */
-Datum
-pg_terminate_backend(PG_FUNCTION_ARGS)
+static Datum
+pg_terminate_backend_internal(int pid, int timeout, const char *msg)
 {
-	int			pid;
 	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);
 
 	if (r == SIGNAL_BACKEND_NOSUPERUSER)
 		ereport(ERROR,
@@ -278,6 +295,19 @@ pg_terminate_backend(PG_FUNCTION_ARGS)
 		PG_RETURN_BOOL(r == SIGNAL_BACKEND_SUCCESS);
 }
 
+Datum pg_terminate_backend(PG_FUNCTION_ARGS)
+{
+	int pid;
+	int timeout; /* milliseconds */
+	char *msg;
+
+	pid = PG_GETARG_INT32(0);
+	timeout = PG_GETARG_INT64(1);
+	msg = text_to_cstring(PG_GETARG_TEXT_PP(2));
+
+	return pg_terminate_backend_internal(pid, timeout, msg);
+}
+
 /*
  * Signal to reload the database configuration
  *
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index b4a8d2f3a1c..3c6d285a4dc 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -81,6 +81,7 @@
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
 #include "utils/varlena.h"
+#include "utils/backend_msg.h"
 
 /* ----------------
  *		global variables
@@ -3356,9 +3357,22 @@ ProcessInterrupts(void)
 			proc_exit(0);
 		}
 		else
+		{
+			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: %s", msg)));
+			}
+
 			ereport(FATAL,
 					(errcode(ERRCODE_ADMIN_SHUTDOWN),
 					 errmsg("terminating connection due to administrator command")));
+		}
 	}
 
 	if (CheckClientConnectionPending)
@@ -3466,9 +3480,21 @@ 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: %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 3f401faf3de..debf6da4a7b 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -69,6 +69,7 @@
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 #include "utils/timeout.h"
+#include "utils/backend_msg.h"
 
 static HeapTuple GetDatabaseTuple(const char *dbname);
 static HeapTuple GetDatabaseTupleByOid(Oid dboid);
@@ -902,6 +903,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 			InitializeSystemUser(MyClientConnectionInfo.authn_id,
 								 hba_authname(MyClientConnectionInfo.auth_method));
 		am_superuser = superuser();
+		BackendMsgInit(MyProcNumber);
 	}
 
 	/* Report any SSL/GSS details for the session. */
diff --git a/src/backend/utils/misc/Makefile b/src/backend/utils/misc/Makefile
index f142d17178b..5494994669f 100644
--- a/src/backend/utils/misc/Makefile
+++ b/src/backend/utils/misc/Makefile
@@ -32,7 +32,8 @@ OBJS = \
 	stack_depth.o \
 	superuser.o \
 	timeout.o \
-	tzparser.o
+	tzparser.o \
+	backend_msg.o
 
 # This location might depend on the installation directories. Therefore
 # we can't substitute it into pg_config.h.
diff --git a/src/backend/utils/misc/backend_msg.c b/src/backend/utils/misc/backend_msg.c
new file mode 100644
index 00000000000..b7c7d30e049
--- /dev/null
+++ b/src/backend/utils/misc/backend_msg.c
@@ -0,0 +1,156 @@
+/*--------------------------------------------------------------------
+ * backend_msg.h
+ *
+ * Utility to pass additional message to backend processes.
+ * Ex: cancel or terminate messages
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/misc/backend_msg.c
+ *
+ *--------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "miscadmin.h"
+#include "storage/shmem.h"
+#include "storage/spin.h"
+#include "storage/ipc.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)
+{
+	(void) code;
+	(void) arg;
+
+	Assert(MyBackendMsgSlot != NULL);
+
+	SpinLockAcquire(&MyBackendMsgSlot->lock);
+
+	MyBackendMsgSlot->msg[0] = '\0';
+	MyBackendMsgSlot->pid = 0;
+
+	SpinLockRelease(&MyBackendMsgSlot->lock);
+
+	MyBackendMsgSlot = NULL;
+}
+
+
+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);
+}
+
+Size
+BackendMsgShmemSize(void)
+{
+	return mul_size(MaxBackends, sizeof(BackendMsgSlot));
+}
+
+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 */);
+}
+
+int BackendMsgSet(pid_t pid, const char *msg)
+{
+	BackendMsgSlot		*slot;
+	int					len;
+
+	if (msg == NULL || msg[0] == '\0')
+		return 0;
+
+	for (int i = 0; i < MaxBackends; ++i)
+	{
+		slot = &BackendMsgSlots[i];
+
+		if (slot->pid == 0 || slot->pid != pid)
+			continue;
+
+		SpinLockAcquire(&slot->lock);
+
+		if (slot->pid != pid)
+		{
+			SpinLockRelease(&slot->lock);
+			break;
+		}
+
+		len = stpncpy(slot->msg, msg, sizeof(slot->msg) - 1) - slot->msg;
+		slot->msg[len] = '\0';
+
+		SpinLockRelease(&slot->lock);
+
+		return len;
+	}
+
+	ereport(LOG,
+			(errmsg("Can't set message for missing backend %ld, requested by %ld",
+				(long) pid, (long) MyProcPid)));
+
+	return -1;
+}
+
+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;
+}
+
+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/utils/backend_msg.h b/src/include/utils/backend_msg.h
new file mode 100644
index 00000000000..825bf7100e2
--- /dev/null
+++ b/src/include/utils/backend_msg.h
@@ -0,0 +1,28 @@
+/*--------------------------------------------------------------------
+ * backend_msg.h
+ *
+ * Utility to pass additional message to backend processes.
+ * Ex: cancel or terminate messages
+ *
+ * Portions Copyright (c) 1996-2025, 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
+
+#define BACKEND_MSG_MAX_LEN 128
+
+extern void BackendMsgShmemInit(void);
+extern Size BackendMsgShmemSize(void);
+extern void BackendMsgInit(int id);
+extern int BackendMsgSet(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..795f3d55cfb
--- /dev/null
+++ b/src/test/modules/test_misc/t/011_backend_msg.pl
@@ -0,0 +1,42 @@
+# Copyright (c) 2025, 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);
+$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\?/, "expected message to be passed");
+
+$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/, "expected message to be passed");
+
+$stdout = '';
+$stderr = '';
+my $longstr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+my $truncated = substr($longstr, 0, 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/, "NOTICE message should be created");
+like($stderr, qr/\Q$truncated\E/, "expected truncated message (127 chars) to be passed");
+unlike($stderr, qr/\Q$longstr\E/, "full message must not be passed");
+
+$node->stop;
+
+done_testing();
-- 
2.43.0

