On Thu Oct 23, 2025 at 3:04 PM CEST, Jelte Fennema-Nio wrote:
This change 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.

After pushback on another threadabout introducing additional minor
protocol versions[1], I've decided to change this patch to use a
protocol extension instead of a minor version bump.

I personally don't think this patch is any better now, but that's fine.
If this means it has a chance of going into PG19, that's totally worth
it to me. (also I'd like to stop spending time on discussions where
clearly neither side will agree with eachother).

The automated test requires the not yet committed pytest changes[2]. I
don't think the automated test is required for a merge, so I don't think
this is blocked on pytest support getting in. It's here mainly as an
example and as a regression test during development, to know I did not
break the goaway functionality while changing the implementation.

[1]: 
https://www.postgresql.org/message-id/flat/CADK3HHKe1PA1U6aB5-7tWBQ0yZGgNvY7H=ecdd9955pas_z...@mail.gmail.com
[2]: https://commitfest.postgresql.org/patch/6045/
From a8feab784eb6e78d33e8038fd82862dde1570868 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Thu, 23 Oct 2025 14:31:52 +0200
Subject: [PATCH v3 2/2] 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/interfaces/libpq/meson.build        |  1 +
 src/interfaces/libpq/pyt/test_goaway.py | 59 +++++++++++++++++++++++++
 src/test/pytest/libpq/_core.py          | 18 ++++++++
 src/test/pytest/pypg/fixtures.py        | 24 ++++++++++
 4 files changed, 102 insertions(+)
 create mode 100644 src/interfaces/libpq/pyt/test_goaway.py

diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 56790dd92a9..983af1d5bea 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -163,6 +163,7 @@ tests += {
   'pytest': {
     'tests': [
       'pyt/test_load_balance.py',
+      'pyt/test_goaway.py',
     ],
   },
 }
diff --git a/src/interfaces/libpq/pyt/test_goaway.py b/src/interfaces/libpq/pyt/test_goaway.py
new file mode 100644
index 00000000000..399cb4d10ac
--- /dev/null
+++ b/src/interfaces/libpq/pyt/test_goaway.py
@@ -0,0 +1,59 @@
+# Copyright (c) 2025, PostgreSQL Global Development Group
+
+"""
+Tests for the GoAway protocol message during smart shutdown.
+
+The GoAway message is sent by the server during smart shutdown to politely
+request that clients disconnect when convenient. The connection remains
+functional after receiving the message.
+"""
+
+import signal
+
+
+def test_goaway_smart_shutdown(pg, wait_until):
+    """
+    Test that GoAway message is sent during smart shutdown.
+
+    This test:
+    1. Connects to a running PostgreSQL server via Unix socket
+    2. Verifies GoAway is not received initially
+    3. Initiates a smart shutdown
+    4. Verifies that GoAway is received
+    5. Verifies that queries still work after GoAway
+    6. Disconnects and verifies the server shuts down
+    """
+
+    # Connect to the server via Unix socket, libpq will request the
+    # _pq_.goaway protocol extension
+    conn = pg.connect(max_protocol_version="latest")
+
+    # Initially, GoAway should not be received
+    assert not conn.goaway_received(), "GoAway should not be received initially"
+
+    # Execute a simple query to ensure connection is working
+    conn.sql("SELECT 1")
+
+    # Initiate smart shutdown by sending SIGTERM to the postmaster
+    import os
+
+    os.kill(pg.pid, signal.SIGTERM)
+
+    for _ in wait_until("Did not receive GoAway after smart shutdown"):
+        # Consume any data the backend may have sent (like GoAway)
+        assert conn.consume_input()
+        if conn.goaway_received():
+            break
+
+    # Execute a query - this will trigger the backend to send GoAway before
+    # processing the query, and the client will parse it
+    conn.sql("SELECT 2")
+
+    # Check that GoAway was received
+    assert conn.goaway_received(), "GoAway should be received after smart shutdown"
+
+    # Connection should still be functional - try one more query
+    conn.sql("SELECT 3")
+
+    # Verify GoAway is still flagged
+    assert conn.goaway_received(), "GoAway flag should remain set"
diff --git a/src/test/pytest/libpq/_core.py b/src/test/pytest/libpq/_core.py
index 0d77996d572..f2783630fd3 100644
--- a/src/test/pytest/libpq/_core.py
+++ b/src/test/pytest/libpq/_core.py
@@ -147,6 +147,12 @@ def load_libpq_handle(libdir, bindir):
     lib.PQresultErrorField.restype = ctypes.c_char_p
     lib.PQresultErrorField.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
 
 
@@ -420,6 +426,18 @@ class PGconn(contextlib.AbstractContextManager):
         else:
             res.raise_error()
 
+    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/pypg/fixtures.py b/src/test/pytest/pypg/fixtures.py
index 8c0cb60daa5..4aa6c73349d 100644
--- a/src/test/pytest/pypg/fixtures.py
+++ b/src/test/pytest/pypg/fixtures.py
@@ -57,6 +57,30 @@ def remaining_timeout_module():
     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, bindir):
     """
-- 
2.52.0

From 08c57d000c06f92f65bc6b48e8b546e5e1ba79eb Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email protected]>
Date: Sun, 19 Oct 2025 00:30:48 +0200
Subject: [PATCH v3 1/2] 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                       |  51 +++++++++
 doc/src/sgml/protocol.sgml                    | 103 +++++++++++++++++-
 src/backend/postmaster/postmaster.c           |  24 ++++
 src/backend/storage/ipc/procsignal.c          |   3 +
 src/backend/tcop/backend_startup.c            |  21 +++-
 src/backend/tcop/postgres.c                   |  36 ++++++
 src/include/libpq/libpq-be.h                  |   5 +
 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           |  39 ++++++-
 src/interfaces/libpq/libpq-fe.h               |   5 +
 src/interfaces/libpq/libpq-int.h              |   1 +
 .../modules/libpq_pipeline/libpq_pipeline.c   |  22 +++-
 17 files changed, 329 insertions(+), 13 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 7d05938feda..be34e862dc7 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2231,6 +2231,13 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
         equivalent to the latest protocol version supported by the libpq
         version being used, which is currently <literal>3.2</literal>.
        </para>
+
+       <para>
+        When using protocol version <literal>3.2</literal> or higher,
+        <application>libpq</application> requests all supported protocol
+        extensions from the server, such as <literal>goaway</literal>
+        which enables <xref linkend="libpq-PQgoAwayReceived"/>.
+       </para>
       </listitem>
      </varlistentry>
 
@@ -2992,6 +2999,50 @@ 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 that request the
+       <literal>goaway</literal> protocol extension in the startup message,
+       which <application>libpq</application> does when using protocol version
+       3.2 or higher (see <xref linkend="libpq-connect-max-protocol-version"/>).
+       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 41c5954a424..223d9d00d81 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -270,6 +270,47 @@
      </tbody>
     </tgroup>
    </table>
+
+   <para>
+    In addition to protocol version negotiation, the client can request
+    optional protocol extensions by including parameters in the startup message.
+    Protocol extension parameter names must be prefixed with
+    <literal>_pq_.</literal> to distinguish them from server parameters. For
+    example, the <literal>goaway</literal> extension is requested by including
+    <literal>_pq_.goaway</literal> in the startup message. If the server does
+    not recognize an extension, it will report it in the
+    NegotiateProtocolVersion message. <xref linkend="protocol-extensions-table"/>
+    shows the currently supported protocol extensions.
+   </para>
+
+   <table id="protocol-extensions-table">
+    <title>Protocol Extensions</title>
+
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>Extension</entry>
+       <entry>Supported Values</entry>
+       <entry>Introduced In</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry><literal>goaway</literal></entry>
+       <entry><literal>1</literal></entry>
+       <entry>PostgreSQL 19</entry>
+       <entry>
+        Enables the server to send
+        <link linkend="protocol-message-formats-GoAway">GoAway</link>
+        messages to request the client to disconnect when convenient.
+        See <xref linkend="protocol-async"/> for more details.
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
   </sect2>
  </sect1>
 
@@ -590,6 +631,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>
 
@@ -1338,7 +1390,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
@@ -1449,6 +1501,27 @@ 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>). The server only sends the GoAway
+    message to clients that request the <literal>goaway</literal>
+    protocol extension in the startup message.
+   </para>
+
    <para>
     If a frontend issues a <command>LISTEN</command> command, then the
     backend will send a NotificationResponse message (not to be
@@ -5240,6 +5313,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>
@@ -6152,7 +6250,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
 
          In addition to the above, other parameters may be listed.
          Parameter names beginning with <literal>_pq_.</literal> are
-         reserved for use as protocol extensions, while others are
+         reserved for use as protocol extensions (see
+         <xref linkend="protocol-extensions-table"/>), while others are
          treated as run-time parameters to be set at backend start
          time.  Such settings will be applied during backend start
          (after parsing the command-line arguments if any) and will
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 921d73226d6..017f29018fb 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2134,7 +2134,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 8e56922dcea..5c36eff9b53 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -694,6 +694,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/backend_startup.c b/src/backend/tcop/backend_startup.c
index 94a7b839563..9acbd9f03aa 100644
--- a/src/backend/tcop/backend_startup.c
+++ b/src/backend/tcop/backend_startup.c
@@ -779,11 +779,24 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 			{
 				/*
 				 * Any option beginning with _pq_. is reserved for use as a
-				 * protocol-level option, but at present no such options are
-				 * defined.
+				 * protocol-level option.
 				 */
-				unrecognized_protocol_options =
-					lappend(unrecognized_protocol_options, pstrdup(nameptr));
+				if (strcmp(nameptr, "_pq_.goaway") == 0)
+				{
+					/* Client wants to receive GoAway messages. */
+					if (strcmp(valptr, "1") != 0)
+						ereport(FATAL,
+								(errcode(ERRCODE_PROTOCOL_VIOLATION),
+								 errmsg("invalid value for protocol option \"%s\": \"%s\"",
+										nameptr, valptr),
+								 errhint("Valid values are: \"1\".")));
+					port->goaway_negotiated = true;
+				}
+				else
+				{
+					unrecognized_protocol_options =
+						lappend(unrecognized_protocol_options, pstrdup(nameptr));
+				}
 			}
 			else
 			{
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 015c67bbeba..32bfc0d583a 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->goaway_negotiated)
+		{
+			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/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..b8769412c72 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -202,6 +202,11 @@ typedef struct Port
 	void	   *gss;
 #endif
 
+	/*
+	 * Protocol extensions.
+	 */
+	bool		goaway_negotiated;	/* client supports GoAway message */
+
 	/*
 	 * SSL structures.
 	 */
diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h
index eae8f0e7238..fd3c195daa2 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 e52b8eb7697..f5fdf80da9f 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 54ddee875ed..2c029efe778 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 a0d2f749811..c7580874b5a 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -700,6 +700,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 203d388bdbf..0c33eaa3fb0 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -2702,6 +2702,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 34518bbe6ea..f47b3ecf4bb 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -134,8 +134,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
@@ -159,6 +159,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 ... */
@@ -305,6 +309,9 @@ pqParseInput3(PGconn *conn)
 					if (getParameterStatus(conn))
 						return;
 					break;
+				case PqMsg_GoAway:
+					conn->goaway_received = true;
+					break;
 				case PqMsg_BackendKeyData:
 
 					/*
@@ -1499,8 +1506,7 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 	conn->pversion = their_version;
 
 	/*
-	 * We don't currently request any protocol extensions, so we don't expect
-	 * the server to reply with any either.
+	 * Process the list of unsupported protocol extensions.
 	 */
 	for (int i = 0; i < num; i++)
 	{
@@ -1513,6 +1519,17 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported unsupported parameter name without a \"%s\" prefix (\"%s\")", "_pq_.", conn->workBuffer.data);
 			goto failure;
 		}
+
+		/*
+		 * Handle known protocol extensions. Unknown extensions that were not
+		 * requested by us are an error.
+		 */
+		if (strcmp(conn->workBuffer.data, "_pq_.goaway") == 0)
+		{
+			/* Server doesn't support GoAway, that's fine */
+			continue;
+		}
+
 		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data);
 		goto failure;
 	}
@@ -1856,6 +1873,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:
@@ -2349,6 +2369,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);
@@ -2474,6 +2497,14 @@ build_startup_packet(const PGconn *conn, char *packet,
 		}
 	}
 
+	/*
+	 * Request protocol extensions. Only do this for protocol version 3.2 and
+	 * later, to avoid confusing old proxies that don't understand _pq_.*
+	 * options.
+	 */
+	if (conn->pversion >= PG_PROTOCOL(3, 2))
+		ADD_STARTUP_OPTION("_pq_.goaway", "1");
+
 	/* Add trailing terminator */
 	if (packet)
 		packet[packet_len] = '\0';
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 905f2f33ab8..96efdcc523d 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 fb6a7cbf15d..d2026a6a849 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? */
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 03371721460..cc10d98e3be 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);
 

base-commit: 0547aeae0fd6f6d03dd7499c84145ad9e3aa51b9
prerequisite-patch-id: 622792f2fa36e00183a5e04efa957cf30cdff0e1
prerequisite-patch-id: e4c4123bcdbf066b4b5d69500cb0c196374192c0
prerequisite-patch-id: 7c330eca229517757cbd7b2709d16fd69a781519
prerequisite-patch-id: d1a66b051d3ad79bee110acc25338445ddf6a5fd
prerequisite-patch-id: 581583212dd8e68350ffa30abd3f39683a3e1fb9
prerequisite-patch-id: 03e6c36b2eb504339a4547fca1dfb465c3925ce3
prerequisite-patch-id: d5bd9d7bbef967905058cd78007e4766b29e7833
prerequisite-patch-id: 6a38bd4f9f9974394648bbe1542dec9578d4e15a
-- 
2.52.0

Reply via email to