public inbox for [email protected]
help / color / mirror / Atom feedFrom: Roman Khapov <[email protected]>
To: Jim Jones <[email protected]>
Cc: Kirill Reshke <[email protected]>
Cc: Daniel Gustafsson <[email protected]>
Cc: [email protected]
Subject: Re: Additional message in pg_terminate_backend
Date: Tue, 3 Feb 2026 12:52:57 +0500
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>
References: <[email protected]>
<[email protected]>
<[email protected]>
<CALdSSPhU526xXqjsb=BPO689+qFJQDeimWrhOv=ehzveQsZJgw@mail.gmail.com>
<[email protected]>
<[email protected]>
Hi Jim!
Thanks for your review and rebase!
> Since the message's size is limited to BACKEND_MSG_MAX_LEN, shouldn't
> you use it to limit msg at pg_terminate_backend[_msg]()? Something like:
>
The message is truncated inside BackendMsgSet function, so I see a little
point in truncating it at pg_terminate_backend..
Also, changing the stpncpy() to strlcpy() breaks the logic
of returning result length of the message and NOTICE message about it,
so I reverted this change. But this note make me think about adding test
to truncation logic, so I added it in v4.
--
Best regards,
Roman Khapov
Attachments:
[application/octet-stream] v4-0001-message-in-pg_terminate_backend-and-pg_cancel_bac.patch (18.3K, 2-v4-0001-message-in-pg_terminate_backend-and-pg_cancel_bac.patch)
download | inline diff:
From c335e0c6b19982c9ea45b1979be4eeaa4ab3ece3 Mon Sep 17 00:00:00 2001
From: roman khapov <[email protected]>
Date: Tue, 3 Feb 2026 07:37:15 +0000
Subject: [PATCH v4] message in pg_terminate_backend and pg_cancel_backend
Sometimes it is useful to terminate some backend
process with additional message from admin.
This patch introduces a new argument, message to
pg_terminate_backend and pg_cancel_backend.
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(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: Kirill Reshke <[email protected]>
Reviewed-by: Jim Jones <[email protected]>
---
doc/src/sgml/func/func-admin.sgml | 10 +-
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 | 156 ++++++++++++++++++
src/backend/utils/misc/meson.build | 1 +
src/include/utils/backend_msg.h | 28 ++++
src/test/modules/test_misc/meson.build | 1 +
.../modules/test_misc/t/011_backend_msg.pl | 42 +++++
12 files changed, 320 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..40bc0947f75 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>test</type> <literal>DEFAULT</literal> <literal>''</literal> )
<returnvalue>boolean</returnvalue>
</para>
<para>
@@ -160,6 +160,9 @@
<literal>pg_signal_autovacuum_worker</literal> are permitted to
cancel autovacuum worker processes, which are otherwise considered
superuser backends.
+ If <parameter>message</parameter> is specified and non-empty, this
+ string will be passed as additional message in ERROR text for
+ canceled backend.
</para></entry>
</row>
@@ -225,7 +228,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>test</type> <literal>DEFAULT</literal> <literal>''</literal>)
<returnvalue>boolean</returnvalue>
</para>
<para>
@@ -249,6 +252,9 @@
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, this
+ string will be passed as additional message in FATAL text for
+ terminated backend.
</para></entry>
</row>
</tbody>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index eb9e31ae1bf..5e2c1386193 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 1f7e933d500..9f50adf030c 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 6f7759cd720..b66aadc7c0e 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 && msg[0] != '\0')
+ {
+ 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 b4a8d2f3a1c..3c6d285a4dc 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 3f401faf3de..debf6da4a7b 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 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..b7c7d30e049
--- /dev/null
+++ b/src/backend/utils/misc/backend_msg.c
@@ -0,0 +1,156 @@
+/*--------------------------------------------------------------------
+ * 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) - 1) - slot->msg;
+ slot->msg[len] = '\0';
+
+ 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 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/utils/backend_msg.h b/src/include/utils/backend_msg.h
new file mode 100644
index 00000000000..825bf7100e2
--- /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/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..795f3d55cfb
--- /dev/null
+++ b/src/test/modules/test_misc/t/011_backend_msg.pl
@@ -0,0 +1,42 @@
+# 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");
+
+$stdout = '';
+$stderr = '';
+my $longstr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+my $truncated = substr($longstr, 0, 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/, "NOTICE message should be created");
+like($stderr, qr/\Q$truncated\E/, "expected truncated message (127 chars) to be passed");
+unlike($stderr, qr/\Q$longstr\E/, "full message must not be passed");
+
+$node->stop;
+
+done_testing();
--
2.43.0
view thread (10+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected], [email protected], [email protected]
Subject: Re: Additional message in pg_terminate_backend
In-Reply-To: <[email protected]>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox