Subject: Additional message in pg_terminate_backend

From: Roman Khapov <[email-masked]>
Date: Sat, 13 Dec 2025 12:44:46 +0500

Hi hackers!

Recently I started working on patch for adding additional message from admin 
in pg_terminate_backend for one of our greenplum fork. The main idea is that there must be 
done little investigation every time you see 'FATAL:  terminating connection due to administrator command’ to understand the reason, especially in cases where connection was terminated by another
user. So it was decided to create some new functions, that allows to terminate connection with
additional message.

I did POC patches with the next main ideas:
 - lets add termReasonStr field in every PGPROC, that field can be used in ProcessInterrupts() 
 - implementation of pg_terminate_backend/pg_cancel_backend should be accessible from extensions, so lets move it in pg_terminate_backend_impl/pg_cancel_backend_impl and add definitions for it somewhere
 - write simple extensions, which defines functions like pg_terminate_backend_msg, that sets termReasonStr and calls pg_terminate_backend_impl

After patch and added extension, it is possible to do smth like:
postgres=# select pg_terminate_backend_msg(pg_backend_pid(), 0, ’the message');
FATAL:  terminating connection due to administrator command: the message

The general question I want to ask: can this patches be useful for vanilla PostgreSQL?

If so, there are some questions about patch improvements:
 - maybe the message can be delivered to backend by some other way than the field in struct PGPROC?
termReasonStr field consumes some shared memory and are used in rare cases
 - names of all new funcs/fields/etc should be changed to some better names
 - new file signalfuncs.h seems like too complicated solutions to define *_impl functions, it can be done in
some other files? 

-----
Best regards,
Roman Khapov

---

From: Andrey Borodin <[email-masked]>
Date: Wed, 17 Dec 2025 16:34:09 +0500

> On 13 Dec 2025, at 12:44, Roman Khapov <[email-masked]> wrote:
> 

Overall idea seems good to me.
Keep in mind that Postgres literals are translated into many languages. So text ought to be clear enough for translators to build a sentence that precedes termination reason.

> - write simple extensions, which defines functions like pg_terminate_backend_msg, that sets termReasonStr and calls pg_terminate_backend_impl

First thing that we need to do is to agree on API of the new feature. We do not need core-extension separation for this.
My vote would be for having pg_cancel_backend(reason text)\pg_terminate_backend(reason text) along with parameterless versions.
32 bytes per PGPROC seems reasonable for a "reason". The patch doesn't seem to take care of cleaning "termReasonStr". Is it done elsewhere?
We have a race condition if many backends cancel same backend. Won't they mess each other's reason?

Thanks!

Best regards, Andrey Borodin.

---

From: Daniel Gustafsson <[email-masked]>
Date: Wed, 17 Dec 2025 17:02:29 +0100

> On 13 Dec 2025, at 08:44, Roman Khapov <[email-masked]> wrote:

> in pg_terminate_backend for one of our greenplum fork.

Greenplum already has support for passing a message in the terminate command
doesnt it?  Or at least it used to have but perhaps it was ripped out, my
memory is getting a bit fuzzy.

https://github.com/greenplum-db/gpdb-archive/commit/fa6c2d43d675aa05e2d9f797e3008f6fe075ee2c

--
Daniel Gustafsson

---

From: Roman Khapov <[email-masked]>
Date: Sat, 20 Dec 2025 14:26:40 +0500

> On 17 Dec 2025, at 21:02, Daniel Gustafsson <[email-masked]> wrote:
> 

Well, seems like I missed that patch..

Anyway, now I need same functionality in PostgreSQL, and your patch seems interesting,
especially in part where you keep messages in separated shmem region.

So I adopted your patch, maybe in that form it can be useful for PostgreSQL?

---

From: Kirill Reshke <[email-masked]>
Date: Sat, 20 Dec 2025 15:32:34 +0500

>  +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;

I don't think we need to create a function with a name other than
`pg_terminate_backend`.  I also do not think we need
pg_terminate_backend_msg as a wrapper to another function - all of
this can be a single function, accepting different number of params,
exampli gratia "bt_index_check"

```
Datum
bt_index_check(PG_FUNCTION_ARGS)
{
Oid indrelid = PG_GETARG_OID(0);

...

if (PG_NARGS() >= 2)
args.heapallindexed = PG_GETARG_BOOL(1);
if (PG_NARGS() >= 3)
args.checkunique = PG_GETARG_BOOL(2);
....

PG_RETURN_VOID();
}

```

-- 
Best regards,
Kirill Reshke

---

From: warda Bibi <[email-masked]>
Date: Mon, 6 Apr 2026 00:27:33 +0500

Hi all,

Thank you for the continued work on this patch.

Prafulla Ranadive and I reviewed this patch through the patch review
workshop. Overall, we find that the proposed feature is useful. We
have reviewed all five versions and have the following observations.

We agree with Jim's point of view that the v3 approach is better. v5
introduced separate _msg overloads (OID 8223 for
pg_cancel_backend_msg, OID 8222 for pg_terminate_backend_msg), which
adds catalog bloat and forces callers to use a different function
name:

> +{ oid => '8223', descr => 'cancel a server process\' current query',
>   proname => 'pg_cancel_backend', provolatile => 'v', prorettype => 'bool',
>   proargtypes => 'int4 text', prosrc => 'pg_cancel_backend_msg' },
> +{ oid => '8222', descr => 'terminate a server process',
>   proname => 'pg_terminate_backend', provolatile => 'v', prorettype => 'bool',
>   proargtypes => 'int4 int8 text', proargnames => '{pid,timeout,message}',
>   prosrc => 'pg_terminate_backend_msg' },

V3 keeps one OID per function and stays backward-compatible. v6
restores the v3 approach.

--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6727,19 +6727,14 @@

 { oid => '2171', descr => 'cancel a server process\' current query',
   proname => 'pg_cancel_backend', provolatile => 'v', prorettype => 'bool',
-  proargtypes => 'int4', prosrc => 'pg_cancel_backend' },
-{ oid => '8223', descr => 'cancel a server process\' current query',
-  proname => 'pg_cancel_backend', provolatile => 'v', prorettype => 'bool',
-  proargtypes => 'int4 text', prosrc => 'pg_cancel_backend_msg' },
+  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}',
-  prosrc => 'pg_terminate_backend' },
-{ oid => '8222', descr => 'terminate a server process',
   proname => 'pg_terminate_backend', provolatile => 'v', prorettype => 'bool',
   proargtypes => 'int4 int8 text', proargnames => '{pid,timeout,message}',
-  prosrc => 'pg_terminate_backend_msg' },
+  proargdefaults => '{0,""}',
+  prosrc => 'pg_terminate_backend' },

--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -378,6 +378,16 @@ BEGIN ATOMIC
 END;

+CREATE OR REPLACE FUNCTION
+  pg_cancel_backend(pid integer, message 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, message
text DEFAULT '')
+  RETURNS boolean STRICT VOLATILE LANGUAGE INTERNAL AS 'pg_terminate_backend'
+  PARALLEL SAFE;
+

The current approach scans all MaxBackends slots by PID twice. Since
pg_signal_backend() already calls BackendPidGetProc(pid) and has the
PGPROC* in hand, the PGPROC index into allProcs[ ] is the ProcNumber,
which is also the direct index into BackendMsgSlots[]. So v6 changes
the signature to BackendMsgSet(ProcNumber procno, ...) and the call in
pg_signal_backend() passes GetNumberFromPGProc(proc), making the
lookup O(1).

-int BackendMsgSet(pid_t pid, const char *msg)
+int BackendMsgSet(ProcNumber procno, const char *msg)
 {
    BackendMsgSlot      *slot;
    int                 len;
@@ -94,35 +94,26 @@ int BackendMsgSet(pid_t pid, const char *msg)
    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;
-       }
+   slot = &BackendMsgSlots[procno];

Also, fixed a stale file-header path in backend_msg.c:

--- a/src/backend/utils/misc/backend_msg.c
+++ b/src/backend/utils/misc/backend_msg.c
@@ -7,7 +7,7 @@
  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
- * src/include/utils/misc/backend_msg.c
+ * src/backend/utils/misc/backend_msg.c

Removed dead NULL checks on msg in pg_cancel_backend_msg() and
pg_terminate_backend_msg() because text_to_cstring() never returns
NULL.

    pid = PG_GETARG_INT32(0);
    msg = text_to_cstring(PG_GETARG_TEXT_PP(1));
-   if (msg == NULL) {
-       PG_RETURN_BOOL(false);
-   }

Furthermore, the else before the fallback ereport(FATAL) in the
terminate path is missing in v5. Without the else, the fallback
ereport is unreachable when BackendMsgIsSet() is true because
ereport(FATAL) never returns, but it misleads readers into thinking
the generic message is always emitted. Adding an else is also
consistent with the pg_cancel approach. v6 adds else to make the
intent explicit.

-           ereport(FATAL,
-                   (errcode(ERRCODE_ADMIN_SHUTDOWN),
-                    errmsg("terminating connection due to
administrator command")));
+           else
+               ereport(FATAL,
+                       (errcode(ERRCODE_ADMIN_SHUTDOWN),
+                        errmsg("terminating connection due to
administrator command")));
        }
    }

The _internal helper functions (pg_cancel_backend_internal,
pg_terminate_backend_internal) each have only one call site. What is
the intent behind keeping them separate, or can they be inlined
directly into pg_cancel_backend() and pg_terminate_backend()?

> Won't they mess each other's reason?

The spinlock in BackendMsgSet protects the write, but there is no
atomicity between writing the slot and delivering the signal. Another
backend can overwrite the slot between one writer's write and the
target reading it. Would it be valuable to define the behavior
explicitly, for example last-writer-wins, or document the limitation?

Andrey also raised this about translation:

> Keep in mind that Postgres literals are translated into many languages.
> So text ought to be clear enough for translators to build a sentence
> that precedes termination reason.

The current pattern:

>   errmsg("terminating connection due to administrator command: %s", msg)

The colon-append pattern breaks in languages where the reason clause
is structured differently. Any suggestions on how to approach this, or
how similar cases are handled elsewhere in the codebase?

Also, parallel workers receive the generic message as they hit the
IsBackgroundWorker branch in ProcessInterrupts(), which bypasses
BackendMsgGet(). This is fine since parallel workers are ephemeral,
but should it be documented in the docs?

Patch attached.

--

Best Wishes,
Warda Bibi

---

From: Jim Jones <[email-masked]>
Date: Thu, 9 Apr 2026 13:07:12 +0200

Hi Warda and Prafulla

On 05/04/2026 21:27, warda Bibi wrote:
> workshop. Overall, we find that the proposed feature is useful.

Thanks for the nice review!

Roman, are you still planning to work on this? The patch has been
failing for several weeks.

https://cirrus-ci.com/github/postgresql-cfbot/postgresql/cf%2F6331

Best, Jim

---

From: Roman Khapov <[email-masked]>
Date: Fri, 10 Apr 2026 11:05:08 +0500

Hi!

Thanks Warda for review!

Yes, I am planning to continue work on this patch, will send new version soon.

--
Best regards,
Roman Khapov

> On 9 Apr 2026, at 16:07, Jim Jones <[email-masked]> wrote:
> 
> Hi Warda and Prafulla
>