From 2875c4c56f24ee7e8f20b024376ba627ebcab534 Mon Sep 17 00:00:00 2001 From: Andrey Borodin Date: Fri, 13 Nov 2026 11:35:37 +0500 Subject: [PATCH v1 1/4] Add coverage for transaction timeout cleanup Inspect active deadlines to check GUC restoration and timer ownership without relying on short timeouts. Exercise recoverable errors twice to verify that error cleanup preserves the deadline rather than restarting the timeout. Also cover an idle timeout in an aborted block with a shorter, inactive transaction timeout. Reload the settings after aborting, so the test does not depend on beating the transaction timer. --- .../injection_points--1.0.sql | 5 + .../injection_points/regress_injection.c | 17 +++ src/test/modules/test_misc/t/005_timeouts.pl | 140 ++++++++++++++++++ 3 files changed, 162 insertions(+) diff --git a/src/test/modules/injection_points/injection_points--1.0.sql b/src/test/modules/injection_points/injection_points--1.0.sql index 2efb307f5bf..542c00c989f 100644 --- a/src/test/modules/injection_points/injection_points--1.0.sql +++ b/src/test/modules/injection_points/injection_points--1.0.sql @@ -105,6 +105,11 @@ LANGUAGE C STRICT VOLATILE PARALLEL RESTRICTED; -- -- regress_injection.c functions -- +CREATE FUNCTION transaction_timeout_deadline() +RETURNS timestamptz +AS 'MODULE_PATHNAME' +LANGUAGE C; + CREATE FUNCTION removable_cutoff(rel regclass) RETURNS xid8 AS 'MODULE_PATHNAME' diff --git a/src/test/modules/injection_points/regress_injection.c b/src/test/modules/injection_points/regress_injection.c index ad9c792a551..76d20c9516d 100644 --- a/src/test/modules/injection_points/regress_injection.c +++ b/src/test/modules/injection_points/regress_injection.c @@ -21,12 +21,29 @@ #include "replication/logicalproto.h" #include "storage/procarray.h" #include "utils/rel.h" +#include "utils/timeout.h" +#include "utils/timestamp.h" #include "utils/xid8.h" extern PGDLLEXPORT void logicalrep_relation_redirect(const char *name, const void *private_data, void *arg); +/* + * Inspect transaction timeout state without waiting for a timer to fire. + * Using a long timeout lets tests exercise transaction and GUC cleanup + * without depending on how quickly those operations finish. + */ +PG_FUNCTION_INFO_V1(transaction_timeout_deadline); +Datum +transaction_timeout_deadline(PG_FUNCTION_ARGS) +{ + if (!get_timeout_active(TRANSACTION_TIMEOUT)) + PG_RETURN_NULL(); + + PG_RETURN_TIMESTAMPTZ(get_timeout_finish_time(TRANSACTION_TIMEOUT)); +} + /* * Pretend that the publisher asked for a different target relation than it * really did, so that relations which must never be replication targets can diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl index c16b7dbf5e6..b3a89905308 100644 --- a/src/test/modules/test_misc/t/005_timeouts.pl +++ b/src/test/modules/test_misc/t/005_timeouts.pl @@ -35,6 +35,76 @@ if (!$node->check_extension('injection_points')) $node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); +# Check timer ownership and GUC restoration with long timeouts, so that slow +# machines cannot expire a timer before reaching the cleanup being tested. +my $state_session = $node->background_psql('postgres', on_error_stop => 0); + +is( $state_session->query_safe(q{ + BEGIN; + SAVEPOINT s; + SET LOCAL transaction_timeout = '1d'; + ROLLBACK TO SAVEPOINT s; + SELECT transaction_timeout_deadline() IS NULL; +}), 't', 'savepoint rollback disables a timeout restored to zero'); + +is( $state_session->query_safe(q{ + COMMIT; + SELECT transaction_timeout_deadline() IS NULL; +}), 't', 'transaction timeout does not survive commit'); + +is( $state_session->query_safe(q{ + BEGIN; + SET LOCAL transaction_timeout = '2d'; + SELECT transaction_timeout_deadline() > + clock_timestamp() + interval '47 hours'; + COMMIT; +}), 't', 'next transaction uses its own timeout'); + +is( $state_session->query_safe(q{ + BEGIN; + SET LOCAL transaction_timeout = '1d'; + SAVEPOINT s; + SET LOCAL transaction_timeout = 0; + ROLLBACK TO SAVEPOINT s; + SELECT transaction_timeout_deadline() IS NOT NULL; + COMMIT; +}), 't', 'savepoint rollback re-enables a restored positive timeout'); + +is( $state_session->query_safe(q{ + DO $$ + BEGIN + PERFORM set_config('transaction_timeout', '1d', true); + RAISE EXCEPTION 'rollback'; + EXCEPTION WHEN raise_exception THEN + NULL; + END + $$; + SELECT transaction_timeout_deadline() IS NULL; +}), 't', 'caught PL/pgSQL exception restores the timeout'); + +$state_session->query_safe(q{ + BEGIN; + SET LOCAL transaction_timeout = '1d'; + SELECT transaction_timeout_deadline() AS deadline \gset + SAVEPOINT s; +}); + +for my $attempt (1 .. 2) +{ + my $result = $state_session->query(q{ + SELECT 1 / 0; + ROLLBACK TO SAVEPOINT s; + SELECT transaction_timeout_deadline() = :'deadline'::timestamptz; + }); + like($state_session->{stderr}, qr/division by zero/, + "recoverable error $attempt occurred"); + $state_session->{stderr} = ''; + is($result, 't', "recoverable error $attempt preserves the deadline"); +} + +$state_session->query_safe('COMMIT'); +ok($state_session->quit); + # # 1. Test of the transaction timeout # @@ -139,4 +209,74 @@ $node->wait_for_log('terminating connection due to idle-session timeout', ok($psql_session->quit); +# A reload can set transaction_timeout while a block is already aborted, +# without arming its timer. This lets us check the idle timeout independently +# of how quickly a transaction can be made to fail. +$node->safe_psql('postgres', q{ + SELECT injection_points_detach('transaction-timeout'); +}); + +# Keep a control connection with a session-level setting that overrides the +# shorter transaction timeout we will load from the configuration file. +my $control_session = $node->background_psql('postgres', + timeout => 2 * $PostgreSQL::Test::Utils::timeout_default); +$control_session->query_safe('SET transaction_timeout = 0'); + +$psql_session = $node->background_psql('postgres', + on_error_stop => 0, + timeout => 2 * $PostgreSQL::Test::Utils::timeout_default); +my $pid = $psql_session->query_safe('SELECT pg_backend_pid()'); +# Attach the callback's shared memory before releasing transaction resources. +$psql_session->query_safe(q{ + SELECT injection_points_load('idle-in-transaction-session-timeout'); +}); +$psql_session->query('BEGIN; SELECT 1 / 0;'); +like($psql_session->{stderr}, qr/division by zero/, + 'transaction is aborted before enabling the timeouts'); +$psql_session->{stderr} = ''; + +$log_offset = -s $node->logfile; +$node->append_conf('postgresql.conf', q{ +transaction_timeout = '1ms' +idle_in_transaction_session_timeout = '10ms' +}); +$node->reload; + +# Send commands until the backend processes the reload and reaches the idle +# timeout injection point. Do not wait for a response after sending a command: +# the timer could fire before the client receives it. The injection point +# prevents FATAL from racing with the next command. +my $idle_timeout_fired = 0; +for my $attempt (1 .. $PostgreSQL::Test::Utils::timeout_default * 10) +{ + if ($control_session->query_safe(qq{ + SELECT EXISTS (SELECT FROM pg_stat_activity WHERE pid = $pid + AND wait_event = 'idle-in-transaction-session-timeout'); + }) eq 't') + { + $idle_timeout_fired = 1; + last; + } + $psql_session->{stdin} .= "SELECT 1;\n"; + $psql_session->{run}->pump_nb; + usleep(100_000); +} +ok($idle_timeout_fired, 'inactive transaction timer does not suppress idle timeout'); + +if ($idle_timeout_fired) +{ + $control_session->query_safe(q{ + SELECT injection_points_wakeup('idle-in-transaction-session-timeout'); + }); + $node->wait_for_log( + qr/\[$pid\].*terminating connection due to idle-in-transaction timeout/, + $log_offset); +} + +# A command may still be queued behind the injection point, so psql can exit +# with a connection-lost error. The server log above checks the FATAL reason. +$psql_session->{stdin} .= "\\q\n"; +$psql_session->{run}->finish; +ok($control_session->quit); + done_testing(); base-commit: 87524d0dd8470819ce8aa3708a9bf3fa3f1d34fd -- 2.50.1 (Apple Git-155)