From 5a754e4408e4c14053dd26bf07ac33a35365c0fc Mon Sep 17 00:00:00 2001
From: Warda Bibi <wardabibi221@gmail.com>
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 <daniel@yesql.se>
Author: Roman Khapov <r.khapov@ya.ru>
Author: Warda Bibi <wardabibi221@gmail.com>
Reviewed-by: Kirill Reshke <reshkekirill@gmail.com>
Reviewed-by: Jim Jones <jim.jones@uni-muenster.de>
Reviewed-by: Andrey Borodin <x4mmm@yandex-team.ru>
Reviewed-by: Paul Jungwirth <pj@illuminatedcomputing.com>
---
 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 @@
         <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>text</type> <literal>DEFAULT</literal> <literal>''</literal> )
         <returnvalue>boolean</returnvalue>
        </para>
        <para>
@@ -160,6 +160,12 @@
         <literal>pg_signal_autovacuum_worker</literal> are permitted to
         cancel autovacuum worker processes, which are otherwise considered
         superuser backends.
+       </para>
+       <para>
+        If <parameter>message</parameter> 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.
        </para></entry>
       </row>
 
@@ -225,7 +231,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>text</type> <literal>DEFAULT</literal> <literal>''</literal> )
         <returnvalue>boolean</returnvalue>
        </para>
        <para>
@@ -249,6 +255,11 @@
         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, 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.
        </para></entry>
       </row>
      </tbody>
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

