From e85dae5f0ebfce8612c615aa8c9505672e08bed2 Mon Sep 17 00:00:00 2001
From: roman khapov <r.khapov@ya.ru>
Date: Sat, 20 Dec 2025 08:03:01 +0000
Subject: [PATCH v2] pg_terminate_backend_msg and pg_cancel_backend_msg

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

This patch introduces two new functions:
 - pg_terminate_backend_msg(pid, timeout, msg)
 - pg_cancel_backend_msg(pid, msg)

The functions are similar with pg_terminate_backend/pg_cancel_backend,
but adds additional argument: 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_msg(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:
Discussion:
---
 src/backend/catalog/system_functions.sql      |   5 +
 src/backend/storage/ipc/ipci.c                |   3 +
 src/backend/storage/ipc/signalfuncs.c         |  78 +++++++--
 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          | 155 ++++++++++++++++++
 src/include/catalog/pg_proc.dat               |   7 +
 src/include/utils/backend_msg.h               |  30 ++++
 .../modules/test_misc/t/010_backend_msg.pl    |  31 ++++
 10 files changed, 330 insertions(+), 16 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/010_backend_msg.pl

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..5209ee3f1ab 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -404,6 +404,11 @@ CREATE OR REPLACE FUNCTION
   RETURNS boolean STRICT VOLATILE LANGUAGE INTERNAL AS 'pg_terminate_backend'
   PARALLEL SAFE;
 
+CREATE OR REPLACE FUNCTION
+  pg_terminate_backend_msg(pid integer, timeout int8 DEFAULT 0, msg text DEFAULT '')
+  RETURNS boolean STRICT VOLATILE LANGUAGE INTERNAL AS 'pg_terminate_backend_msg'
+  PARALLEL SAFE;
+
 -- legacy definition for compatibility with 9.3
 CREATE OR REPLACE FUNCTION
   json_populate_record(base anyelement, from_json json, use_json_as_text boolean DEFAULT false)
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index b23d0c19360..94a89ff5e57 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());
 
 	/* include additional requested shmem from preload libraries */
 	size = add_size(size, total_addin_request);
@@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void)
 	InjectionPointShmemInit();
 	AioShmemInit();
 	WaitLSNShmemInit();
+	BackendMsgShmemInit();
 }
 
 /*
diff --git a/src/backend/storage/ipc/signalfuncs.c b/src/backend/storage/ipc/signalfuncs.c
index a3a670ba247..d7ca2d77186 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)
+	{
+		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,28 @@ pg_cancel_backend(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(r == SIGNAL_BACKEND_SUCCESS);
 }
 
+Datum
+pg_cancel_backend(PG_FUNCTION_ARGS)
+{
+	int		pid;
+	
+	pid = PG_GETARG_INT32(0);
+
+	return pg_cancel_backend_internal(pid, NULL);
+}
+
+Datum
+pg_cancel_backend_msg(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 +266,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 +306,32 @@ 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 */
+
+	pid = PG_GETARG_INT32(0);
+	timeout = PG_GETARG_INT64(1);
+
+	return pg_terminate_backend_internal(pid, timeout, NULL);
+}
+
+Datum
+pg_terminate_backend_msg(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 7dd75a490aa..94c1636c7e7 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 4ed69ac7ba2..c653bcc12c5 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);
@@ -899,6 +900,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..7f638c864bc
--- /dev/null
+++ b/src/backend/utils/misc/backend_msg.c
@@ -0,0 +1,155 @@
+/*--------------------------------------------------------------------
+ * 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)) - slot->msg;
+
+		SpinLockRelease(&slot->lock);
+
+		return len;
+	}
+
+	ereport(LOG,
+			(errmsg("Can't set message for missing backend %d, requested by %d",
+				pid, 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/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fd9448ec7b9..66b4a397284 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6713,6 +6713,13 @@
   proname => 'pg_terminate_backend', provolatile => 'v', prorettype => 'bool',
   proargtypes => 'int4 int8', proargnames => '{pid,timeout}',
   prosrc => 'pg_terminate_backend' },
+{ oid => '8328', descr => 'cancel a server process\' current query with message',
+  proname => 'pg_cancel_backend_msg', provolatile => 'v', prorettype => 'bool',
+  proargtypes => 'int4 text', prosrc => 'pg_cancel_backend_msg' },
+{ oid => '8329', descr => 'terminate a server process with message',
+  proname => 'pg_terminate_backend_msg', provolatile => 'v', prorettype => 'bool',
+  proargtypes => 'int4 int8 text', proargnames => '{pid,timeout,msg}',
+  prosrc => 'pg_terminate_backend_msg' },
 { oid => '2172', descr => 'prepare for taking an online backup',
   proname => 'pg_backup_start', provolatile => 'v', proparallel => 'r',
   prorettype => 'pg_lsn', proargtypes => 'text bool',
diff --git a/src/include/utils/backend_msg.h b/src/include/utils/backend_msg.h
new file mode 100644
index 00000000000..db69efbe915
--- /dev/null
+++ b/src/include/utils/backend_msg.h
@@ -0,0 +1,30 @@
+/*--------------------------------------------------------------------
+ * 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
+
+#include <sys/types.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/t/010_backend_msg.pl b/src/test/modules/test_misc/t/010_backend_msg.pl
new file mode 100644
index 00000000000..d0d68b453f2
--- /dev/null
+++ b/src/test/modules/test_misc/t/010_backend_msg.pl
@@ -0,0 +1,31 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+# Check that messages are passed to backends by
+# pg_terminate_backend_msg, pg_cancel_backend_msg
+
+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_msg(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_msg(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");
+
+$node->stop;
+
+done_testing();
-- 
2.43.0

