From 61c3c79b45c0f6744cfac69eda94774ee178471d Mon Sep 17 00:00:00 2001
From: Jingtang Zhang <mrdrivingduck@gmail.com>
Date: Fri, 13 Nov 2026 16:41:51 +0800
Subject: [PATCH v1] Honor transaction timeout during blocked client writes

A client that stops reading can leave its backend blocked in a write
after the transaction timeout expires. Handle transaction timeout in the
blocked write path alongside explicit session termination so the backend
can exit while the client is still stalled.

Give explicit session termination first priority, followed by
transaction timeout, so a pending query cancellation cannot interrupt
the write with a recoverable error when the session is due to terminate.
---
 src/backend/tcop/postgres.c                  | 31 ++++++++++----------
 src/test/modules/test_misc/t/005_timeouts.pl | 31 ++++++++++++++++++++
 2 files changed, 47 insertions(+), 15 deletions(-)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index d6761aeea9f..01bce9e782b 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -562,7 +562,8 @@ ProcessClientWriteInterrupt(bool blocked)
 {
 	int			save_errno = errno;
 
-	if (ProcDiePending)
+	if (ProcDiePending ||
+		(TransactionTimeoutPending && TransactionTimeout > 0))
 	{
 		/*
 		 * We're dying.  If it's not possible to write, then we should handle
@@ -577,7 +578,7 @@ ProcessClientWriteInterrupt(bool blocked)
 		{
 			/*
 			 * Don't mess with whereToSendOutput if ProcessInterrupts wouldn't
-			 * service ProcDiePending.
+			 * service the pending termination.
 			 */
 			if (InterruptHoldoffCount == 0 && CritSectionCount == 0)
 			{
@@ -3525,6 +3526,19 @@ ProcessInterrupts(void)
 					 ERRDETAIL_SIGNAL_SENDER(sender_pid, sender_uid)));
 	}
 
+	if (TransactionTimeoutPending)
+	{
+		/* Ignore the signal if the GUC has been reset to zero. */
+		TransactionTimeoutPending = false;
+		if (TransactionTimeout > 0)
+		{
+			INJECTION_POINT("transaction-timeout", NULL);
+			ereport(FATAL,
+					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
+					 errmsg("terminating connection due to transaction timeout")));
+		}
+	}
+
 	if (CheckClientConnectionPending)
 	{
 		CheckClientConnectionPending = false;
@@ -3657,19 +3671,6 @@ ProcessInterrupts(void)
 		}
 	}
 
-	if (TransactionTimeoutPending)
-	{
-		/* As above, ignore the signal if the GUC has been reset to zero. */
-		TransactionTimeoutPending = false;
-		if (TransactionTimeout > 0)
-		{
-			INJECTION_POINT("transaction-timeout", NULL);
-			ereport(FATAL,
-					(errcode(ERRCODE_TRANSACTION_TIMEOUT),
-					 errmsg("terminating connection due to transaction timeout")));
-		}
-	}
-
 	if (IdleSessionTimeoutPending)
 	{
 		/* As above, ignore the signal if the GUC has been reset to zero. */
diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl
index c16b7dbf5e6..22b55ce2a59 100644
--- a/src/test/modules/test_misc/t/005_timeouts.pl
+++ b/src/test/modules/test_misc/t/005_timeouts.pl
@@ -139,4 +139,35 @@ $node->wait_for_log('terminating connection due to idle-session timeout',
 
 ok($psql_session->quit);
 
+#
+# 4. Test transaction timeout while blocked writing to the client.
+#
+$node->safe_psql('postgres',
+	"SELECT injection_points_detach('transaction-timeout');");
+
+$psql_session = $node->background_psql('postgres');
+my $pid = $psql_session->query_safe('SELECT pg_backend_pid()');
+$log_offset = -s $node->logfile;
+
+# Stop draining psql's stdout at the marker. COPY streams its output, so psql
+# will stop reading from the server once its stdout pipe fills up.
+$psql_session->query_until(
+	qr/copy_starting/, q(
+   \echo copy_starting
+   BEGIN;
+   SET LOCAL transaction_timeout = '5s';
+   COPY (SELECT repeat('x', 8192) FROM generate_series(1, 100000)) TO STDOUT;
+));
+$node->wait_for_event('client backend', 'ClientWrite');
+
+$node->wait_for_log(
+	qr/\[$pid\].*FATAL:\s+terminating connection due to transaction timeout/,
+	$log_offset);
+ok( $node->poll_query_until(
+		'postgres', qq[
+		SELECT NOT EXISTS (SELECT FROM pg_stat_activity WHERE pid = $pid);
+	]),
+	'transaction timeout terminates the backend during ClientWrite');
+$psql_session->{run}->finish;
+
 done_testing();

base-commit: 13eb56409f5b6ed15e3ab5fa2b1fa98f66932c10
-- 
2.43.7

