From 7be6fa2edeaf0da8c9c6f167fcbef4af56c56bed Mon Sep 17 00:00:00 2001 From: Maxim Orlov Date: Wed, 21 Oct 2026 16:25:03 +0300 Subject: [PATCH v1 1/3] Add 2pc race test between CheckPointTwoPhase() and FinishPreparedTransaction() --- src/backend/access/transam/twophase.c | 5 +- src/test/recovery/t/061_2pc_lsn_race.pl | 93 +++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/test/recovery/t/061_2pc_lsn_race.pl diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c index 48e478a4ecb..2129c75336a 100644 --- a/src/backend/access/transam/twophase.c +++ b/src/backend/access/transam/twophase.c @@ -1536,7 +1536,9 @@ FinishPreparedTransaction(const char *gid, bool isCommit) * in WAL files if the LSN is after the last checkpoint record, or moved * to disk if for some reason they have lived for a long time. */ - if (gxact->ondisk) + ondisk = gxact->ondisk; + INJECTION_POINT("twophase-finish-after-ondisk-read", NULL); + if (ondisk) buf = ReadTwoPhaseFile(fxid, false); else XlogReadTwoPhaseData(gxact->prepare_start_lsn, &buf, NULL); @@ -1872,6 +1874,7 @@ CheckPointTwoPhase(XLogRecPtr redo_horizon) XlogReadTwoPhaseData(gxact->prepare_start_lsn, &buf, &len); RecreateTwoPhaseFile(gxact->fxid, buf, len); gxact->ondisk = true; + INJECTION_POINT("twophase-checkpoint-after-ondisk-set", NULL); gxact->prepare_start_lsn = InvalidXLogRecPtr; gxact->prepare_end_lsn = InvalidXLogRecPtr; pfree(buf); diff --git a/src/test/recovery/t/061_2pc_lsn_race.pl b/src/test/recovery/t/061_2pc_lsn_race.pl new file mode 100644 index 00000000000..615fbbde8e5 --- /dev/null +++ b/src/test/recovery/t/061_2pc_lsn_race.pl @@ -0,0 +1,93 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group +# +# Reproduce a race between CheckPointTwoPhase() changing ondisk/LSNs and +# FinishPreparedTransaction() 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 $node1 = PostgreSQL::Test::Cluster->new('twophase_lsn_race'); +$node1->init; +$node1->append_conf('postgresql.conf', q{ + max_prepared_transactions = 10 + log_checkpoints = on +}); +$node1->start; + +if (!$node1->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +$node1->safe_psql('postgres', q{CREATE EXTENSION injection_points}); +$node1->safe_psql( + 'postgres', + q{SELECT injection_points_attach('twophase-finish-after-ondisk-read', + 'wait')}); +$node1->safe_psql( + 'postgres', + q{SELECT injection_points_attach('twophase-checkpoint-after-ondisk-set', + 'wait')}); + +$node1->safe_psql('postgres', q{ + BEGIN; + CREATE TABLE twophase_lsn_race_table (id integer); + INSERT INTO twophase_lsn_race_table VALUES (1); + PREPARE TRANSACTION 'twophase_lsn_race_gid'; +}); + +# Pause COMMIT PREPARED after it has copied ondisk=false to its local variable. +my $commit = $node1->background_psql('postgres'); +$commit->query_until( + qr/commit_started/, q{ + \echo commit_started + COMMIT PREPARED 'twophase_lsn_race_gid'; + \echo commit_finished +}); +$node1->wait_for_event('client backend', 'twophase-finish-after-ondisk-read'); + +# The checkpoint now sets ondisk=true and stops before clearing the LSNs. +my $checkpoint = $node1->background_psql('postgres'); +$checkpoint->query_until( + qr/checkpoint_started/, q{ + \echo checkpoint_started + CHECKPOINT; + \echo checkpoint_finished +}); +$node1->wait_for_event('checkpointer', 'twophase-checkpoint-after-ondisk-set'); + +# Finish the checkpoint first, so prepare_start_lsn is invalidated. Then +# resume COMMIT PREPARED, which still has the earlier ondisk=false value. +$node1->safe_psql('postgres', + q{SELECT injection_points_wakeup('twophase-checkpoint-after-ondisk-set')}); +$checkpoint->query_until(qr/checkpoint_finished/, ''); + +$node1->safe_psql('postgres', + q{SELECT injection_points_wakeup('twophase-finish-after-ondisk-read')}); + +$checkpoint->quit; +$commit->quit; + +ok($node1->is_alive(), "server is still running"); + +$node1->stop; + +my $logfile = slurp_file($node1->logfile()); +ok($logfile !~ + qr/TRAP: failed Assert*/, + "no failed assertion"); + +# Reproduce a race between CheckPointTwoPhase() changing ondisk/LSNs and +# LookupGXact() reading those fields without syncing. +# TODO: add here... + +done_testing(); -- 2.55.0