On Fri Oct 24, 2025 at 7:04 AM CEST, Kirill Reshke wrote:
On Thu, 23 Oct 2025 at 18:05, Jelte Fennema-Nio <[email protected]> wrote:
Im +1 on this idea. This is something I wanted back in 2020, when
implementing the 'online restart' feature for odyssey[0], but never
bothered to create a thread.
Yeah, to be clear: A big goal of this is definitely to be used by
poolers/proxies/middleware. Those systems will often be more frequently
restarted than the actual database servers, so being able to do that
quickly without disrupting active connections is much more important
there than with plain PostgreSQL servers.
About patches:
Thanks for the review. Attached is a new patchset. I think I addressed
all of your comments (I almost fully rewrote the docs). I also fixed
two other issues that I found:
- updating docs for 3.3 in more places
- handling the GoAway message in more code paths on the client side
From 5e037b9cf644e23fd9e9806a0b72690ddb867f75 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Thu, 23 Oct 2025 14:31:52 +0200
Subject: [PATCH v2 3/3] Add pytest based tests for GoAway message
I used this patchset as a trial for the new pytest suite that Jacob is
trying to introduce. Feel free to look at it, but I'd say don't review
this test in detail until we have the pytest changes merged or at least
in a more agreed upon state. This patch is built on top of that v3
patchset. This test is not applied by cfbot.
Testing this any other way is actually quite difficult with the
infrastructure we currently have (of course I could change that, but I'd
much rather spend that energy/time on making the pytest test suite a
thing):
- pgregress and perl tests don't work because we need to call a new
libpq function that is not exposed in psql (I guess I could expose it
with some \goawayreceived command, but it doesn't seem very useful).
- libpq_pipeline cannot test this because it would need to restart
the Postgres server and all it has
---
src/test/pytest/libpq.py | 18 ++++++++++++
src/test/pytest/meson.build | 1 +
src/test/pytest/pypg/fixtures.py | 33 +++++++++++++++++++++
src/test/pytest/pypg/server.py | 50 ++++++++++++++++++++++++++++++++
4 files changed, 102 insertions(+)
diff --git a/src/test/pytest/libpq.py b/src/test/pytest/libpq.py
index b851a117b66..5536b605c16 100644
--- a/src/test/pytest/libpq.py
+++ b/src/test/pytest/libpq.py
@@ -133,6 +133,12 @@ def load_libpq_handle(libdir):
lib.PQftype.restype = ctypes.c_uint
lib.PQftype.argtypes = [_PGresult_p, ctypes.c_int]
+ lib.PQgoAwayReceived.restype = ctypes.c_int
+ lib.PQgoAwayReceived.argtypes = [_PGconn_p]
+
+ lib.PQconsumeInput.restype = ctypes.c_int
+ lib.PQconsumeInput.argtypes = [_PGconn_p]
+
return lib
@@ -340,6 +346,18 @@ class PGconn(contextlib.AbstractContextManager):
error_msg = res.error_message() or f"Unexpected status: {status}"
raise LibpqError(f"Query failed: {error_msg}\nQuery: {query}")
+ def consume_input(self) -> bool:
+ """
+ Consumes any available input from the server. Returns True on success.
+ """
+ return bool(self._lib.PQconsumeInput(self._handle))
+
+ def goaway_received(self) -> bool:
+ """
+ Returns True if a GoAway message was received from the server.
+ """
+ return bool(self._lib.PQgoAwayReceived(self._handle))
+
def connstr(opts: Dict[str, Any]) -> str:
"""
diff --git a/src/test/pytest/meson.build b/src/test/pytest/meson.build
index f53193e8686..3c8518243d9 100644
--- a/src/test/pytest/meson.build
+++ b/src/test/pytest/meson.build
@@ -12,6 +12,7 @@ tests += {
'tests': [
'pyt/test_something.py',
'pyt/test_libpq.py',
+ 'pyt/test_goaway.py',
],
},
}
diff --git a/src/test/pytest/pypg/fixtures.py b/src/test/pytest/pypg/fixtures.py
index cf22c8ec436..ba46f048beb 100644
--- a/src/test/pytest/pypg/fixtures.py
+++ b/src/test/pytest/pypg/fixtures.py
@@ -30,6 +30,30 @@ def remaining_timeout():
return lambda: max(deadline - time.monotonic(), 0)
[email protected]
+def wait_until(remaining_timeout):
+ def wait_until(error_message="Did not complete in time", timeout=None, interval=1):
+ """
+ Loop until the timeout is reached. If the timeout is reached, raise an
+ exception with the given error message.
+ """
+ if timeout is None:
+ timeout = remaining_timeout()
+
+ end = time.time() + timeout
+ print_progress = timeout / 10 > 4
+ last_printed_progress = 0
+ while time.time() < end:
+ if print_progress and time.time() - last_printed_progress > 4:
+ last_printed_progress = time.time()
+ print(f"{error_message} - will retry")
+ yield
+ time.sleep(interval)
+ raise TimeoutError(error_message)
+
+ return wait_until
+
+
@pytest.fixture(scope="session")
def libpq_handle(libdir):
"""
@@ -149,6 +173,15 @@ def pg_server_module(pg_server_global):
yield s
[email protected](autouse=True, scope="function")
+def ensure_server_running(pg_server_global):
+ """
+ Autouse fixture that ensures the server is running before each test.
+ If a test shuts down the server, this will restart it for the next test.
+ """
+ pg_server_global.ensure_running()
+
+
@pytest.fixture
def pg(pg_server_module, remaining_timeout):
"""
diff --git a/src/test/pytest/pypg/server.py b/src/test/pytest/pypg/server.py
index d6675cde93d..f09651c089e 100644
--- a/src/test/pytest/pypg/server.py
+++ b/src/test/pytest/pypg/server.py
@@ -332,6 +332,56 @@ class PostgresServer:
# Server may have already been stopped
pass
+ def ensure_running(self):
+ """
+ Ensure that the PostgreSQL server is running and accepting connections.
+
+ If the server is not running, it will be restarted. This method waits
+ for any in-progress shutdown to complete before attempting to restart.
+ """
+ pid_file = os.path.join(self.datadir, "postmaster.pid")
+
+ # Wait for any in-progress shutdown to complete
+ socket_pattern = os.path.join(self.sockdir, f".s.PGSQL.{self.port}*")
+ for _ in range(100): # Wait up to 10 seconds
+ # Server is fully down when both PID file and sockets are gone
+ if not os.path.exists(pid_file) and len(glob.glob(socket_pattern)) == 0:
+ break
+ # Server is running if PID exists and we can connect
+ if os.path.exists(pid_file):
+ # Use pg_isready to check if server is accepting connections
+ try:
+ pg_isready = os.path.join(self._bindir, "pg_isready")
+ run(
+ pg_isready,
+ "-h",
+ self.sockdir,
+ "-p",
+ self.port,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ timeout=1,
+ )
+ # Server is up and ready
+ break
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
+ # Server is not ready yet, keep waiting
+ pass
+ time.sleep(0.1)
+
+ # Now check if server needs to be started
+ if not os.path.exists(pid_file):
+ # Restart the server and wait for it to be ready
+ run(
+ self._pg_ctl,
+ "-D",
+ self.datadir,
+ "-l",
+ self._log,
+ "-w",
+ "start",
+ )
+
def cleanup(self):
"""Run all registered cleanup callbacks."""
self._cleanup_stack.close()
--
2.51.1
From 587df6b1a81833b14cf8c2d659b21cd54a3c55f6 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Sun, 19 Oct 2025 00:30:48 +0200
Subject: [PATCH v2 1/3] Bump protocol version to 3.3
This commit increments the PostgreSQL frontend/backend protocol version
from 3.2 to 3.3 and does the required boilerplate changes. The next
commit will introduce an actual protocol change.
---
doc/src/sgml/libpq.sgml | 12 ++++++----
doc/src/sgml/protocol.sgml | 22 ++++++++++++-------
src/include/libpq/pqcomm.h | 2 +-
src/interfaces/libpq/fe-connect.c | 6 +++++
.../modules/libpq_pipeline/libpq_pipeline.c | 22 ++++++++++++++++---
5 files changed, 48 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 5bf59a19855..0f43ee6b039 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2200,10 +2200,12 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
<para>
The current supported values are
- <literal>3.0</literal>, <literal>3.2</literal>,
+ <literal>3.0</literal>,
+ <literal>3.2</literal>,
+ <literal>3.3</literal>
and <literal>latest</literal>. The <literal>latest</literal> value is
equivalent to the latest protocol version supported by the libpq
- version being used, which is currently <literal>3.2</literal>.
+ version being used, which is currently <literal>3.3</literal>.
</para>
</listitem>
</varlistentry>
@@ -2226,10 +2228,12 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
<para>
The current supported values are
- <literal>3.0</literal>, <literal>3.2</literal>,
+ <literal>3.0</literal>,
+ <literal>3.2</literal>,
+ <literal>3.3</literal>
and <literal>latest</literal>. The <literal>latest</literal> value is
equivalent to the latest protocol version supported by the libpq
- version being used, which is currently <literal>3.2</literal>.
+ version being used, which is currently <literal>3.3</literal>.
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 9d755232873..7665bd7dcb8 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -18,8 +18,8 @@
</para>
<para>
- This document describes version 3.2 of the protocol, introduced in
- <productname>PostgreSQL</productname> version 18. The server and the libpq
+ This document describes version 3.3 of the protocol, introduced in
+ <productname>PostgreSQL</productname> version 19. The server and the libpq
client library are backwards compatible with protocol version 3.0,
implemented in <productname>PostgreSQL</productname> 7.4 and later.
</para>
@@ -192,7 +192,7 @@
<title>Protocol Versions</title>
<para>
- The current, latest version of the protocol is version 3.2. However, for
+ The current, latest version of the protocol is version 3.3. However, for
backwards compatibility with old server versions and middleware that don't
support the version negotiation yet, libpq still uses protocol version 3.0
by default.
@@ -206,7 +206,7 @@
this would occur if the client requested protocol version 4.0, which does
not exist as of this writing). If the minor version requested by the
client is not supported by the server (e.g., the client requests version
- 3.2, but the server supports only 3.0), the server may either reject the
+ 3.3, but the server supports only 3.0), the server may either reject the
connection or may respond with a NegotiateProtocolVersion message
containing the highest minor protocol version which it supports. The
client may then choose either to continue with the connection using the
@@ -238,10 +238,16 @@
</thead>
<tbody>
+ <row>
+ <entry>3.3</entry>
+ <entry>PostgreSQL 19 and later</entry>
+ <entry>Current latest version.
+ </entry>
+ </row>
<row>
<entry>3.2</entry>
<entry>PostgreSQL 18 and later</entry>
- <entry>Current latest version. The secret key used in query
+ <entry>The secret key used in query
cancellation was enlarged from 4 bytes to a variable length field. The
BackendKeyData message was changed to accommodate that, and the CancelRequest
message was redefined to have a variable length payload.
@@ -6076,9 +6082,9 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
<para>
The protocol version number. The most significant 16 bits are
the major version number. The least significant 16 bits are the minor
- version number. As an example protocol version 3.2 is represented as
- <literal>196610</literal> in decimal or more clearly as
- <literal>0x00030002</literal> in hexadecimal.
+ version number. As an example protocol version 3.3 is represented as
+ <literal>196611</literal> in decimal or more clearly as
+ <literal>0x00030003</literal> in hexadecimal.
</para>
</listitem>
</varlistentry>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index f04ca135653..2423394b348 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -94,7 +94,7 @@ is_unixsock_path(const char *path)
*/
#define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0)
-#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2)
+#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3)
typedef uint32 ProtocolVersion; /* FE/BE protocol version number */
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a3d12931fff..62d35e0c789 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -8301,6 +8301,12 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn,
return true;
}
+ if (strcmp(value, "3.3") == 0)
+ {
+ *result = PG_PROTOCOL(3, 3);
+ return true;
+ }
+
libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
context, value);
return false;
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index b3af70fa09b..0b113b271ea 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1405,7 +1405,23 @@ test_protocol_version(PGconn *conn)
PQfinish(conn);
/*
- * Test max_protocol_version=latest. 'latest' currently means '3.2'.
+ * Test max_protocol_version=3.3
+ */
+ vals[max_protocol_version_index] = "3.3";
+ conn = PQconnectdbParams(keywords, vals, false);
+
+ if (PQstatus(conn) != CONNECTION_OK)
+ pg_fatal("Connection to database failed: %s",
+ PQerrorMessage(conn));
+
+ protocol_version = PQfullProtocolVersion(conn);
+ if (protocol_version != 30003)
+ pg_fatal("expected 30003, got %d", protocol_version);
+
+ PQfinish(conn);
+
+ /*
+ * Test max_protocol_version=latest. 'latest' currently means '3.3'.
*/
vals[max_protocol_version_index] = "latest";
conn = PQconnectdbParams(keywords, vals, false);
@@ -1415,8 +1431,8 @@ test_protocol_version(PGconn *conn)
PQerrorMessage(conn));
protocol_version = PQfullProtocolVersion(conn);
- if (protocol_version != 30002)
- pg_fatal("expected 30002, got %d", protocol_version);
+ if (protocol_version != 30003)
+ pg_fatal("expected 30003, got %d", protocol_version);
PQfinish(conn);
--
2.51.1
From fd486d8a83112504114dbaeb4da40106af3361b9 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Sun, 19 Oct 2025 00:36:30 +0200
Subject: [PATCH v2 2/3] Add GoAway protocol message for graceful but fast
server shutdown/switchover
This commit introduces a new GoAway backend-to-frontend protocol
message (byte 'g') that the server can send to the client to politely
request that client to disconnect/reconnect when convenient. This message is
advisory only - the connection remains fully functional and clients may
continue executing queries and starting new transactions. "When
convenient" is obviously not very well defined, but the primary target
clients are clients that maintain a connection pool. Such clients should
disconnect/reconnect a connection in the pool when there's no user of
that connection. This is similar to how such clients often currently
remove a connection from the pool after the connection hits a maximum
lifetime of e.g. 1 hour.
This new message is used by Postgres during the already existing "smart"
shutdown procedure (i.e. when postmaster receives SIGTERM). When
Postgres is in "smart" shutdown mode existing clients can continue to
run queries as usual but new connection attempts are rejected. This mode
is primarily useful when triggering a switchover of a read replica. A
load balancer can route new connections only to the new read replica,
while the old load balancer keeps serving the existing connections until
they disconnect. The problem is that this draining of connections could
often take a long time. Even when clients only run very short
queries/transactions because the session can be kept open much longer
(many connection pools use 1 hour max lifetime of a connection by default).
With the introduction of the GoAway message Postgres now sends this
message to all connected clients when it enters smart shutdown mode.
If these clients respond to the message by reconnecting/disconnecting
earlier than their maximum connection lifetime the draining can complete
much quicker. Similar benefits to switchover duration can be achieved
for other applications or proxies implementing the Postgres protocol,
like when switching over a cluster of PgBouncer machines to a newer
version.
Applications/clients that use libpq can periodically check the result of
PQgoAwayReceived() at an inactive time to see whether they are asked to
reconnect.
---
doc/src/sgml/libpq.sgml | 41 ++++++++++++++++++++
doc/src/sgml/protocol.sgml | 58 +++++++++++++++++++++++++++-
src/backend/postmaster/postmaster.c | 24 ++++++++++++
src/backend/storage/ipc/procsignal.c | 3 ++
src/backend/tcop/postgres.c | 36 +++++++++++++++++
src/include/libpq/protocol.h | 1 +
src/include/storage/procsignal.h | 1 +
src/include/tcop/tcopprot.h | 1 +
src/interfaces/libpq/exports.txt | 1 +
src/interfaces/libpq/fe-connect.c | 1 +
src/interfaces/libpq/fe-exec.c | 27 +++++++++++++
src/interfaces/libpq/fe-protocol3.c | 17 +++++++-
src/interfaces/libpq/libpq-fe.h | 5 +++
src/interfaces/libpq/libpq-int.h | 1 +
14 files changed, 214 insertions(+), 3 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 0f43ee6b039..563e25d9f27 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2996,6 +2996,47 @@ int PQserverVersion(const PGconn *conn);
</listitem>
</varlistentry>
+ <varlistentry id="libpq-PQgoAwayReceived">
+ <term><function>PQgoAwayReceived</function><indexterm><primary>PQgoAwayReceived</primary></indexterm></term>
+ <listitem>
+ <para>
+ Returns true if the server has sent a <literal>GoAway</literal> message,
+ requesting the client to disconnect when convenient.
+
+<synopsis>
+int PQgoAwayReceived(const PGconn *conn);
+</synopsis>
+ </para>
+
+ <para>
+ The <literal>GoAway</literal> message is sent by the server during a
+ smart shutdown to politely request that clients disconnect. This is
+ advisory only - the connection remains fully functional and queries
+ can continue to be executed. Applications can choose to honor the rquest
+ by calling this function periodically and disconnect gracefully when
+ possible, such as after completing the current transaction.
+ </para>
+
+ <para>
+ This message is only sent to clients using protocol version 3.3 or
+ later. The function returns 1 if the <literal>GoAway</literal> message
+ was received, 0 otherwise.
+ </para>
+
+ <para>
+ <function>PQgoAwayReceived</function> does not actually read data from the
+ server; it just returns messages previously absorbed by another
+ <application>libpq</application> function. So normally you would first
+ call <xref linkend="libpq-PQconsumeInput"/>, then check
+ <function>PQgoAwayReceived</function>. You can use
+ <function>select()</function> to wait for data to arrive from the
+ server, thereby using no <acronym>CPU</acronym> power unless there is
+ something to do. (See <xref linkend="libpq-PQsocket"/> to obtain the file
+ descriptor number to use with <function>select()</function>.)
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-PQerrorMessage">
<term>
<function>PQerrorMessage</function><indexterm><primary>PQerrorMessage</primary></indexterm>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 7665bd7dcb8..484e2970e98 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -242,6 +242,7 @@
<entry>3.3</entry>
<entry>PostgreSQL 19 and later</entry>
<entry>Current latest version.
+ The GoAway message was introduced.
</entry>
</row>
<row>
@@ -596,6 +597,17 @@
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term>GoAway</term>
+ <listitem>
+ <para>
+ The server requests the server politely to close the connection at its
+ earliest convenient moment. See <xref linkend="protocol-async"/> for
+ more details.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</para>
@@ -1344,7 +1356,7 @@ SELCT 1/0;<!-- this typo is intentional -->
</para>
<para>
- It is possible for NoticeResponse and ParameterStatus messages to be
+ It is possible for NoticeResponse, ParameterStatus and GoAway messages to be
interspersed between CopyData messages; frontends must handle these cases,
and should be prepared for other asynchronous message types as well (see
<xref linkend="protocol-async"/>). Otherwise, any message type other than
@@ -1455,6 +1467,25 @@ SELCT 1/0;<!-- this typo is intentional -->
parameters that it does not understand or care about.
</para>
+ <para>
+ A GoAway message is sent by the server to politely request the client to
+ disconnect when convenient and reconnect when the client needs a connection
+ again. This is advisory only - the connection remains fully functional and
+ queries can continue to be executed. "When convenient" is very vaguely
+ defined on purpose because it depends on the client and application whether
+ such a moment even exists. So clients are allowed to completely ignore this
+ message and disconnect whenever they otherwise would have. An important
+ type of client that can actually honor the request to disconnect early is a
+ client that maintains a connection pool. Such a client can honor the
+ request by disconnecting a connection that has received a GoAway message
+ when it's not in use by a user of the pool. It is allowed for a server to
+ send multiple GoAway messages on the same connection, but any subsequent
+ GoAway messages after the first GoAway have no effect on the client's
+ behavior. The GoAway message is currently sent by Postgres during the
+ "smart" shutdown procedure (i.e. when postmaster receives
+ <systemitem>SIGTERM</systemitem>).
+ </para>
+
<para>
If a frontend issues a <command>LISTEN</command> command, then the
backend will send a NotificationResponse message (not to be
@@ -5246,6 +5277,31 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
</listitem>
</varlistentry>
+ <varlistentry id="protocol-message-formats-GoAway">
+ <term>GoAway (B)</term>
+ <listitem>
+ <variablelist>
+ <varlistentry>
+ <term>Byte1('g')</term>
+ <listitem>
+ <para>
+ Identifies the message as a polite request for the client to disconnect.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>Int32(4)</term>
+ <listitem>
+ <para>
+ Length of message contents in bytes, including self.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="protocol-message-formats-GSSENCRequest">
<term>GSSENCRequest (F)</term>
<listitem>
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index e1d643b013d..79f116a5a5e 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2126,7 +2126,31 @@ process_pm_shutdown_request(void)
* later state, do not change it.
*/
if (pmState == PM_RUN || pmState == PM_HOT_STANDBY)
+ {
+ dlist_iter iter;
+
connsAllowed = false;
+
+ /*
+ * Signal all backends to send a GoAway message to their
+ * clients, to politely request that they disconnect.
+ */
+ dlist_foreach(iter, &ActiveChildList)
+ {
+ PMChild *bp = dlist_container(PMChild, elem, iter.cur);
+
+ /*
+ * Only signal regular backends and walsenders. Skip
+ * auxiliary processes and dead-end backends.
+ */
+ if (bp->bkend_type == B_BACKEND ||
+ bp->bkend_type == B_WAL_SENDER)
+ {
+ SendProcSignal(bp->pid, PROCSIG_SMART_SHUTDOWN,
+ INVALID_PROC_NUMBER);
+ }
+ }
+ }
else if (pmState == PM_STARTUP || pmState == PM_RECOVERY)
{
/* There should be no clients, so proceed to stop children */
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 087821311cc..6011c30d520 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT))
HandleLogMemoryContextInterrupt();
+ if (CheckProcSignal(PROCSIG_SMART_SHUTDOWN))
+ HandleSmartShutdownInterrupt();
+
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7dd75a490aa..7a59e65f2d3 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -42,8 +42,10 @@
#include "common/pg_prng.h"
#include "jit/jit.h"
#include "libpq/libpq.h"
+#include "libpq/pqcomm.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
+#include "libpq/protocol.h"
#include "mb/pg_wchar.h"
#include "mb/stringinfo_mb.h"
#include "miscadmin.h"
@@ -91,6 +93,14 @@ const char *debug_query_string; /* client-supplied query string */
/* Note: whereToSendOutput is initialized for the bootstrap/standalone case */
CommandDest whereToSendOutput = DestDebug;
+/*
+ * Track whether we've been notified of smart shutdown and sent GoAway.
+ * SmartShutdownPending is set by the PROCSIG_SMART_SHUTDOWN signal handler.
+ * GoAwaySent tracks whether we've already sent the GoAway message.
+ */
+static volatile sig_atomic_t SmartShutdownPending = false;
+static bool GoAwaySent = false;
+
/* flag for logging end of session */
bool Log_disconnections = false;
@@ -508,6 +518,20 @@ ProcessClientReadInterrupt(bool blocked)
/* Check for general interrupts that arrived before/while reading */
CHECK_FOR_INTERRUPTS();
+ /* Send GoAway message if smart shutdown is pending */
+ if (SmartShutdownPending && !GoAwaySent &&
+ whereToSendOutput == DestRemote &&
+ MyProcPort && MyProcPort->proto >= PG_PROTOCOL(3, 3))
+ {
+ StringInfoData buf;
+
+ pq_beginmessage(&buf, PqMsg_GoAway);
+ pq_endmessage(&buf);
+ pq_flush();
+
+ GoAwaySent = true;
+ }
+
/* Process sinval catchup interrupts, if any */
if (catchupInterruptPending)
ProcessCatchupInterrupt();
@@ -3087,6 +3111,18 @@ FloatExceptionHandler(SIGNAL_ARGS)
"invalid operation, such as division by zero.")));
}
+/*
+ * Tell the next CHECK_FOR_INTERRUPTS() or main loop iteration to send a
+ * GoAway message to the client. Runs in a SIGUSR1 handler.
+ */
+void
+HandleSmartShutdownInterrupt(void)
+{
+ SmartShutdownPending = true;
+ InterruptPending = true;
+ /* latch will be set by procsignal_sigusr1_handler */
+}
+
/*
* Tell the next CHECK_FOR_INTERRUPTS() to check for a particular type of
* recovery conflict. Runs in a SIGUSR1 handler.
diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h
index 7bf90053bcb..24fbc9f2613 100644
--- a/src/include/libpq/protocol.h
+++ b/src/include/libpq/protocol.h
@@ -53,6 +53,7 @@
#define PqMsg_FunctionCallResponse 'V'
#define PqMsg_CopyBothResponse 'W'
#define PqMsg_ReadyForQuery 'Z'
+#define PqMsg_GoAway 'g'
#define PqMsg_NoData 'n'
#define PqMsg_PortalSuspended 's'
#define PqMsg_ParameterDescription 't'
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index afeeb1ca019..b629341a4af 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_SMART_SHUTDOWN, /* notify backend of smart shutdown */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index c1bcfdec673..b7fd22c43bb 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -74,6 +74,7 @@ extern void die(SIGNAL_ARGS);
pg_noreturn extern void quickdie(SIGNAL_ARGS);
extern void StatementCancelHandler(SIGNAL_ARGS);
pg_noreturn extern void FloatExceptionHandler(SIGNAL_ARGS);
+extern void HandleSmartShutdownInterrupt(void);
extern void HandleRecoveryConflictInterrupt(ProcSignalReason reason);
extern void ProcessClientReadInterrupt(bool blocked);
extern void ProcessClientWriteInterrupt(bool blocked);
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index dbbae642d76..3385e65c389 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -210,3 +210,4 @@ PQgetAuthDataHook 207
PQdefaultAuthDataHook 208
PQfullProtocolVersion 209
appendPQExpBufferVA 210
+PQgoAwayReceived 211
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 62d35e0c789..f9ee52f0c0a 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -698,6 +698,7 @@ pqDropServerData(PGconn *conn)
conn->password_needed = false;
conn->gssapi_used = false;
conn->write_failed = false;
+ conn->goaway_received = false;
free(conn->write_err_msg);
conn->write_err_msg = NULL;
conn->oauth_want_retry = false;
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 0b1e37ec30b..16bf10280e5 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -2696,6 +2696,33 @@ PQnotifies(PGconn *conn)
return event;
}
+/*
+ * PQgoAwayReceived
+ * returns 1 if a GoAway message has been received from the server
+ * returns 0 if not
+ *
+ * Note that this function does not read any new data from the socket;
+ * caller should call PQconsumeInput() first if they want to ensure
+ * all available data has been read.
+ */
+int
+PQgoAwayReceived(PGconn *conn)
+{
+ if (!conn)
+ return 0;
+
+ if (conn->goaway_received)
+ return 1;
+
+ /*
+ * Parse any available data to see if a GoAway message has arrived.
+ */
+ pqParseInput3(conn);
+
+ return conn->goaway_received ? 1 : 0;
+}
+
+
/*
* PQputCopyData - send some data to the backend during COPY IN or COPY BOTH
*
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index da7a8db68c8..f60fe188806 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -132,8 +132,8 @@ pqParseInput3(PGconn *conn)
}
/*
- * NOTIFY and NOTICE messages can happen in any state; always process
- * them right away.
+ * NOTIFY and NOTICE and GoAway messages can happen in any state;
+ * always process them right away.
*
* Most other messages should only be processed while in BUSY state.
* (In particular, in READY state we hold off further parsing until
@@ -157,6 +157,10 @@ pqParseInput3(PGconn *conn)
if (pqGetErrorNotice3(conn, false))
return;
}
+ else if (id == PqMsg_GoAway)
+ {
+ conn->goaway_received = true;
+ }
else if (conn->asyncStatus != PGASYNC_BUSY)
{
/* If not IDLE state, just wait ... */
@@ -303,6 +307,9 @@ pqParseInput3(PGconn *conn)
if (getParameterStatus(conn))
return;
break;
+ case PqMsg_GoAway:
+ conn->goaway_received = true;
+ break;
case PqMsg_BackendKeyData:
/*
@@ -1841,6 +1848,9 @@ getCopyDataMessage(PGconn *conn)
if (getParameterStatus(conn))
return 0;
break;
+ case PqMsg_GoAway:
+ conn->goaway_received = true;
+ break;
case PqMsg_CopyData:
return msgLength;
case PqMsg_CopyDone:
@@ -2334,6 +2344,9 @@ pqFunctionCall3(PGconn *conn, Oid fnid,
if (getParameterStatus(conn))
continue;
break;
+ case PqMsg_GoAway:
+ conn->goaway_received = true;
+ break;
default:
/* The backend violates the protocol. */
libpq_append_conn_error(conn, "protocol error: id=0x%x", id);
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 0852584edae..1f3bd671a4b 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -63,6 +63,10 @@ extern "C"
/* Indicates presence of the PQAUTHDATA_PROMPT_OAUTH_DEVICE authdata hook */
#define LIBPQ_HAS_PROMPT_OAUTH_DEVICE 1
+/* Features added in PostgreSQL v19: */
+/* Indicates presence of PQgoAwayReceived */
+#define LIBPQ_HAS_GOAWAY 1
+
/*
* Option flags for PQcopyResult
*/
@@ -411,6 +415,7 @@ extern const char *PQparameterStatus(const PGconn *conn,
extern int PQprotocolVersion(const PGconn *conn);
extern int PQfullProtocolVersion(const PGconn *conn);
extern int PQserverVersion(const PGconn *conn);
+extern int PQgoAwayReceived(PGconn *conn);
extern char *PQerrorMessage(const PGconn *conn);
extern int PQsocket(const PGconn *conn);
extern int PQbackendPID(const PGconn *conn);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 02c114f1405..7dc6e858a16 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -511,6 +511,7 @@ struct pg_conn
bool sigpipe_flag; /* can we mask SIGPIPE via MSG_NOSIGNAL? */
bool write_failed; /* have we had a write failure on sock? */
char *write_err_msg; /* write error message, or NULL if OOM */
+ bool goaway_received; /* true if server sent GoAway message */
bool auth_required; /* require an authentication challenge from
* the server? */
--
2.51.1