From 5a754e4408e4c14053dd26bf07ac33a35365c0fc Mon Sep 17 00:00:00 2001 From: Warda Bibi 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 Author: Roman Khapov Author: Warda Bibi Reviewed-by: Kirill Reshke Reviewed-by: Jim Jones Reviewed-by: Andrey Borodin Reviewed-by: Paul Jungwirth --- 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 @@ pg_cancel_backend - pg_cancel_backend ( pid integer ) + pg_cancel_backend ( pid integer, message text DEFAULT '' ) boolean @@ -160,6 +160,12 @@ pg_signal_autovacuum_worker are permitted to cancel autovacuum worker processes, which are otherwise considered superuser backends. + + + If message 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. @@ -225,7 +231,7 @@ pg_terminate_backend - pg_terminate_backend ( pid integer, timeout bigint DEFAULT 0 ) + pg_terminate_backend ( pid integer, timeout bigint DEFAULT 0, message text DEFAULT '' ) boolean @@ -249,6 +255,11 @@ the process is terminated, the function returns true. On timeout, a warning is emitted and false is returned. + If message 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. 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