From ec3ed422c4e3fd70d46ef5a0df8391c6a32848c8 Mon Sep 17 00:00:00 2001 From: Maxim Orlov Date: Mon, 26 Oct 2026 15:49:41 +0300 Subject: [PATCH v1 2/3] Add 2pc race test between CheckPointTwoPhase() and EndPrepare() --- src/backend/access/transam/twophase.c | 8 ++- src/test/recovery/t/062_2pc_lsn_race.pl | 81 +++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 src/test/recovery/t/062_2pc_lsn_race.pl diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c index 2129c75336a..e6e029050a9 100644 --- a/src/backend/access/transam/twophase.c +++ b/src/backend/access/transam/twophase.c @@ -1153,6 +1153,7 @@ EndPrepare(GlobalTransaction gxact) TwoPhaseFileHeader *hdr; StateFileChunk *record; bool replorigin; + XLogRecPtr prepare_end_lsn; /* Add the end sentinel to the list of 2PC records */ RegisterTwoPhaseRecord(TWOPHASE_RM_END_ID, 0, @@ -1212,7 +1213,8 @@ EndPrepare(GlobalTransaction gxact) XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN); - gxact->prepare_end_lsn = XLogInsert(RM_XACT_ID, XLOG_XACT_PREPARE); + prepare_end_lsn = XLogInsert(RM_XACT_ID, XLOG_XACT_PREPARE); + gxact->prepare_end_lsn = prepare_end_lsn; if (replorigin) { @@ -1258,13 +1260,15 @@ EndPrepare(GlobalTransaction gxact) END_CRIT_SECTION(); + INJECTION_POINT("twophase-prepare-before-syncrep", NULL); + /* * Wait for synchronous replication, if required. * * Note that at this stage we have marked the prepare, but still show as * running in the procarray (twice!) and continue to hold locks. */ - SyncRepWaitForLSN(gxact->prepare_end_lsn, false); + SyncRepWaitForLSN(prepare_end_lsn, false); records.tail = records.head = NULL; records.num_chunks = 0; diff --git a/src/test/recovery/t/062_2pc_lsn_race.pl b/src/test/recovery/t/062_2pc_lsn_race.pl new file mode 100644 index 00000000000..6eb52f6d7b4 --- /dev/null +++ b/src/test/recovery/t/062_2pc_lsn_race.pl @@ -0,0 +1,81 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group +# +# Reproduce a race between CheckPointTwoPhase() changing ondisk/LSNs and +# EndPrepare() reading those fields without syncing. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +my $node2 = PostgreSQL::Test::Cluster->new('twophase_endprepare_lsn_race'); +$node2->init(allows_streaming => 1); +$node2->append_conf('postgresql.conf', q{ + max_prepared_transactions = 10 + log_checkpoints = on + synchronous_standby_names = 'nonexistent_standby' +}); +$node2->start; + +if (!$node2->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +# The standby above never connects, so only the session that exercises +# EndPrepare() may wait for it. Setup commits must not. +$node2->safe_psql('postgres', q{ + SET synchronous_commit = local; + CREATE EXTENSION injection_points; +}); +$node2->safe_psql( + 'postgres', + q{SELECT injection_points_attach('twophase-prepare-before-syncrep', + 'wait')}); + +# Pause PREPARE TRANSACTION in EndPrepare() right before it waits for the +# standby, when the gxact is already visible to the checkpointer. +my $prepare = $node2->background_psql('postgres'); +my $pid = $prepare->query_safe('SELECT pg_backend_pid()'); +chomp($pid); +$prepare->query_until( + qr/prepare_started/, q{ + \echo prepare_started + BEGIN; + CREATE TABLE twophase_endprepare_race_table (id integer); + PREPARE TRANSACTION 'twophase_endprepare_race_gid'; + \echo prepare_finished +}); +$node2->wait_for_event('client backend', 'twophase-prepare-before-syncrep'); + +# The checkpoint writes the state file and clears prepare_end_lsn. +$node2->safe_psql('postgres', 'CHECKPOINT'); + +$node2->safe_psql('postgres', + q{SELECT injection_points_wakeup('twophase-prepare-before-syncrep')}); + +# With a valid LSN PREPARE TRANSACTION waits for the standby forever. With an +# already cleared LSN it returns at once and the backend goes idle. Wait for +# either, then check which one it was. +$node2->poll_query_until('postgres', qq{ + SELECT wait_event = 'SyncRep' OR state = 'idle' + FROM pg_stat_activity WHERE pid = $pid}) + or die "backend neither waits for the standby nor finished"; +is($node2->safe_psql('postgres', + qq{SELECT wait_event FROM pg_stat_activity WHERE pid = $pid}), + 'SyncRep', + "PREPARE TRANSACTION waits for the synchronous standby"); + +# Release the backend from the sync rep wait. +$node2->safe_psql('postgres', qq{SELECT pg_cancel_backend($pid)}); +$prepare->quit; +$node2->stop; + +done_testing(); -- 2.55.0