From e85dae5f0ebfce8612c615aa8c9505672e08bed2 Mon Sep 17 00:00:00 2001 From: roman khapov 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 Author: Roman Khapov 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 + +#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