From 5517189c28bf447e46471f4c5be48d3eead2c525 Mon Sep 17 00:00:00 2001 From: Andrey Borodin Date: Fri, 13 Nov 2026 11:37:49 +0500 Subject: [PATCH v1 3/4] Preserve transaction timeout across recoverable errors PostgresMain disabled all timeouts on ERROR, including the timer of a transaction that can continue after ROLLBACK TO SAVEPOINT. With no GUC change to invoke the assign hook, that transaction could then run without its timeout. Save the active timer's deadline before disabling the timeouts and rearm it at that same deadline. AbortTransaction still disables the timer when the whole transaction aborts. Keeping the deadline avoids granting a new full timeout after every recoverable error. Back-patch to v17, where transaction_timeout was introduced. Reported-by: Nicke van Oorschot Discussion: https://postgr.es/m/19896-555a806fe6352359@postgresql.org --- src/backend/tcop/postgres.c | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index e3bceac8379..20327194532 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -4571,6 +4571,9 @@ PostgresMain(const char *dbname, const char *username) if (sigsetjmp(local_sigjmp_buf, 1) != 0) { + bool transaction_timeout_active; + TimestampTz transaction_timeout_finish = 0; + /* * NOTE: if you are tempted to add more code in this if-block, * consider the high probability that it should be in @@ -4587,16 +4590,23 @@ PostgresMain(const char *dbname, const char *username) /* * Forget any pending QueryCancel request, since we're returning to - * the idle loop anyway, and cancel any active timeout requests. (In - * future we might want to allow some timeout requests to survive, but - * at minimum it'd be necessary to do reschedule_timeouts(), in case - * we got here because of a query cancel interrupting the SIGALRM - * interrupt handler.) Note in particular that we must clear the - * statement and lock timeout indicators, to prevent any future plain - * query cancels from being misreported as timeouts in case we're - * forgetting a timeout cancel. + * the idle loop anyway, and cancel active timeout requests except for + * the transaction timeout. A transaction can survive this error + * through ROLLBACK TO SAVEPOINT, so preserve its existing deadline. + * AbortTransaction will disable the timer if the whole transaction is + * being aborted. Rearming it also repairs a possibly lost SIGALRM + * interrupt, if query cancel interrupted the SIGALRM handler. + * + * Clear the statement and lock timeout indicators, to prevent future + * plain query cancels from being misreported as timeouts in case + * we're forgetting a timeout cancel. */ + transaction_timeout_active = get_timeout_active(TRANSACTION_TIMEOUT); + if (transaction_timeout_active) + transaction_timeout_finish = get_timeout_finish_time(TRANSACTION_TIMEOUT); disable_all_timeouts(false); /* do first to avoid race condition */ + if (transaction_timeout_active) + enable_timeout_at(TRANSACTION_TIMEOUT, transaction_timeout_finish); QueryCancelPending = false; idle_in_transaction_timeout_enabled = false; idle_session_timeout_enabled = false; -- 2.50.1 (Apple Git-155)