From 61c3c79b45c0f6744cfac69eda94774ee178471d Mon Sep 17 00:00:00 2001 From: Jingtang Zhang 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