From: Andrey Rachitskiy Date: Sat, 8 Aug 2026 12:40:00 +0500 Subject: [PATCH] Fix temp cleanup hang under SERIALIZABLE READ ONLY DEFERRABLE (BUG #19441) RemoveTempRelationsCallback() pushes an active snapshot so toast fetches during backend-exit temp drops succeed. Using GetTransactionSnapshot() honors SERIALIZABLE READ ONLY DEFERRABLE session defaults and waits in GetSafeSnapshot() for concurrent read/write serializable transactions. A still-prepared serializable xact never finishes that wait, and during proc_exit interrupts are held off, so the backend hangs until the prepared transaction is resolved. Switch to GetCatalogSnapshot(), which is sufficient for this catalog cleanup and never enters the deferrable safe-snapshot wait. Bug: #19441 Author: Andrey Rachitskiy Reported-by: Alexander Lakhin Discussion: https://postgr.es/m/19441-ec29f3b1363b4a68@postgresql.org --- diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 56b87d878e8..165a87e6397 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -4700,7 +4700,15 @@ RemoveTempRelationsCallback(int code, Datum arg) /* Need to ensure we have a usable transaction. */ AbortOutOfAnyTransaction(); StartTransactionCommand(); - PushActiveSnapshot(GetTransactionSnapshot()); + + /* + * Need an active snapshot for toast fetches during deletion. Do not + * use GetTransactionSnapshot(): under SERIALIZABLE READ ONLY + * DEFERRABLE it may wait in GetSafeSnapshot(), and proc_exit holds + * off interrupts so that wait cannot be cancelled. A catalog + * snapshot is enough and avoids that path. + */ + PushActiveSnapshot(GetCatalogSnapshot(RelationRelationId)); RemoveTempRelations(myTempNamespace); diff --git a/src/test/modules/test_misc/t/015_temp_schema_exit_deferrable.pl b/src/test/modules/test_misc/t/015_temp_schema_exit_deferrable.pl new file mode 100644 index 00000000000..07de36630a6 --- /dev/null +++ b/src/test/modules/test_misc/t/015_temp_schema_exit_deferrable.pl @@ -0,0 +1,58 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Backend exit must finish temp-schema cleanup even when the session default +# is SERIALIZABLE READ ONLY DEFERRABLE and a prepared serializable transaction +# is still around. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('deferrable_temp_exit'); +$node->init; +$node->append_conf('postgresql.conf', 'max_prepared_transactions = 1'); +$node->start; + +my $psql1 = $node->background_psql('postgres'); + +$psql1->query_safe( + q{ +CREATE TEMPORARY TABLE tt (i int); +SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE; +}); + +my $pid = $psql1->query_safe(q{SELECT pg_backend_pid();}); +chomp $pid; +like($pid, qr/^\d+$/, "backend pid $pid"); + +# Overlapping read/write serializable xact that outlives session 1. +$node->safe_psql( + 'postgres', + q{ +CREATE TABLE t (i int); +BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; +INSERT INTO t VALUES (1); +PREPARE TRANSACTION 'pt'; +}); + +# Disconnect: RemoveTempRelationsCallback runs during backend exit. +$psql1->quit; + +ok( $node->poll_query_until( + 'postgres', + "SELECT count(*) = 0 FROM pg_stat_activity WHERE pid = $pid"), + 'backend exited despite prepared serializable xact'); + +is( $node->safe_psql( + 'postgres', + q{SELECT count(*) FROM pg_class WHERE relname = 'tt' AND relpersistence = 't'} + ), + '0', + 'temporary table cleaned up on exit'); + +$node->safe_psql('postgres', q{ROLLBACK PREPARED 'pt';}); +$node->safe_psql('postgres', q{DROP TABLE t;}); + +done_testing();