Hi Roman,
As pointed out by Kirill, there is no reason to create
pg_terminate_backend_msg or pg_cancel_backend_msg. You can simply use
the existing functions and expand them to use one extra parameter, e.g.
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;
Here I don't think we need to check PG_NARGS, since the function calls
will always have a default value. Something like this perhaps:
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);
}
stpncpy() -> strlcpy()
Documentation is missing -- assuming the feature design is already solid.
Add backend_msg.c to meson.build.
I rebased the patch (it was failing for quite some time) with some
suggestions. Feel free to remove them and revert to your v2 if you disagree.
I'll review the rest of code in the next days.
Best, JimFrom 37672f6496436ea8ec68cb19b150df32c7b0217d Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Sun, 1 Feb 2026 00:26:21 +0100
Subject: [PATCH v3] 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 <[email protected]>
Author: Roman Khapov <[email protected]>
Reviewed-by:
Discussion:
---
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 | 155 ++++++++++++++++++
src/backend/utils/misc/meson.build | 1 +
src/include/catalog/pg_proc.dat | 5 +-
src/include/utils/backend_msg.h | 28 ++++
.../modules/test_misc/t/010_backend_msg.pl | 31 ++++
11 files changed, 302 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/010_backend_msg.pl
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index eb9e31ae1b..5e2c138619 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 1f7e933d50..9f50adf030 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 6f7759cd72..f242cebf46 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,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 b4a8d2f3a1..3c6d285a4d 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 3f401faf3d..debf6da4a7 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 f142d17178..5494994669 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 0000000000..e36a6bc9ce
--- /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 = strlcpy(slot->msg, msg, sizeof(slot->msg));
+
+ 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 232e74d0af..831bf6c6ba 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 5e5e33f64f..47aa30d716 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6724,10 +6724,11 @@
{ 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,msg}',
+ 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}',
+ proargtypes => 'int4 int8 text', proargnames => '{pid,timeout,msg}',
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 0000000000..825bf7100e
--- /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/t/010_backend_msg.pl b/src/test/modules/test_misc/t/010_backend_msg.pl
new file mode 100644
index 0000000000..69aef23de5
--- /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, 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");
+
+$node->stop;
+
+done_testing();
--
2.43.0