From b296e8db18264114049db95c634dbaef18a3c467 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Wed, 28 Oct 2026 23:14:07 -0400 Subject: [PATCH v1 2/2] Add test for checkpoint during ROLLBACK PREPARED Add an injection point between the abort record flush and the pg_xact update in RecordTransactionAbortPrepared(), and a TAP test that runs a checkpoint and crashes there. Backpatch-through: 18 --- src/backend/access/transam/twophase.c | 5 + src/test/recovery/meson.build | 1 + .../t/061_abort_prepared_checkpoint.pl | 93 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 src/test/recovery/t/061_abort_prepared_checkpoint.pl diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c index 77c8dae0fdc..28820348fb3 100644 --- a/src/backend/access/transam/twophase.c +++ b/src/backend/access/transam/twophase.c @@ -2492,6 +2492,9 @@ RecordTransactionAbortPrepared(TransactionId xid, elog(PANIC, "cannot abort transaction %u, it was already committed", xid); + /* Load the injection point before entering the critical section */ + INJECTION_POINT_LOAD("abort-prepared-before-clog"); + START_CRIT_SECTION(); /* @@ -2524,6 +2527,8 @@ RecordTransactionAbortPrepared(TransactionId xid, /* Always flush, since we're about to remove the 2PC state file */ XLogFlush(recptr); + INJECTION_POINT_CACHED("abort-prepared-before-clog", NULL); + /* * Mark the transaction aborted in clog. Recovery relies on this to * discard a state file that a checkpoint wrote before we removed it. diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build index b6ac9dd07a7..865169d2698 100644 --- a/src/test/recovery/meson.build +++ b/src/test/recovery/meson.build @@ -69,6 +69,7 @@ tests += { 't/058_shutdown_crash_restart.pl', 't/059_remote_apply_status_interval.pl', 't/060_multixact_crashed_updater.pl', + 't/061_abort_prepared_checkpoint.pl', ], }, } diff --git a/src/test/recovery/t/061_abort_prepared_checkpoint.pl b/src/test/recovery/t/061_abort_prepared_checkpoint.pl new file mode 100644 index 00000000000..41bd726e8c3 --- /dev/null +++ b/src/test/recovery/t/061_abort_prepared_checkpoint.pl @@ -0,0 +1,93 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test that a checkpoint starting while ROLLBACK PREPARED has written its +# abort record, but has not yet updated pg_xact, waits for the abort to +# finish. Otherwise, the checkpoint could set its redo pointer after the +# abort record and still write out the two-phase state file without the +# abort in pg_xact. After a crash, the abort record would not be replayed +# and the transaction would come back as prepared. + +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 $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf('postgresql.conf', 'max_prepared_transactions = 5'); +$node->start; + +if (!$node->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +$node->safe_psql( + 'postgres', q( + CREATE EXTENSION injection_points; + CREATE TABLE abort_prep (id int); + BEGIN; + INSERT INTO abort_prep VALUES (1); + PREPARE TRANSACTION 'abort_prep_gxact'; +)); + +# Stop ROLLBACK PREPARED after its abort record has been flushed, before +# pg_xact is updated. +my $rollback = $node->background_psql('postgres', on_error_stop => 0); +$rollback->query_safe( + q(SELECT injection_points_set_local(); + SELECT injection_points_attach('abort-prepared-before-clog', 'wait');)); +$rollback->query_until( + qr/starting_rollback/, q( + \echo starting_rollback + ROLLBACK PREPARED 'abort_prep_gxact'; +)); +$node->wait_for_event('client backend', 'abort-prepared-before-clog'); + +my $redo_before = + $node->safe_psql('postgres', 'SELECT redo_lsn FROM pg_control_checkpoint()'); + +my $checkpoint = $node->background_psql('postgres', on_error_stop => 0); +$checkpoint->query_until( + qr/starting_checkpoint/, q( + \echo starting_checkpoint + CHECKPOINT; +)); + +# Wait until the checkpoint either waits for the rollback, or completes. +$node->poll_query_until( + 'postgres', qq( + SELECT EXISTS (SELECT 1 FROM pg_stat_activity + WHERE backend_type = 'checkpointer' AND + wait_event = 'CheckpointDelayStart') OR + (SELECT redo_lsn FROM pg_control_checkpoint()) > '$redo_before' +)) + or die "timed out waiting for the checkpoint"; + +is( $node->safe_psql( + 'postgres', + 'SELECT redo_lsn FROM pg_control_checkpoint()'), + $redo_before, + 'checkpoint does not complete while ROLLBACK PREPARED is in progress'); + +# Crash while the rollback is still waiting. Its abort record is flushed, +# so the transaction must not be prepared after recovery. +$node->stop('immediate'); +$rollback->{run}->finish; +$checkpoint->{run}->finish; +$node->start; + +is( $node->safe_psql('postgres', 'SELECT count(*) FROM pg_prepared_xacts'), + '0', 'rolled back transaction is not prepared after crash recovery'); +is($node->safe_psql('postgres', 'SELECT count(*) FROM abort_prep'), + '0', 'row of rolled back transaction is not visible'); + +$node->stop; +done_testing(); -- 2.37.1 (Apple Git-137.1)