From 2cbbe8ccde03b46971b81e2f3f0affa99bded0d1 Mon Sep 17 00:00:00 2001
From: Daniil Davidov <d.davydov@postgrespro.ru>
Date: Sat, 23 May 2026 20:27:30 +0700
Subject: [PATCH] Reproduce error with twophase commit

---
 src/backend/access/transam/twophase.c         |  2 +
 src/test/recovery/meson.build                 |  1 +
 .../recovery/t/053_twophase_commit_error.pl   | 76 +++++++++++++++++++
 3 files changed, 79 insertions(+)
 create mode 100644 src/test/recovery/t/053_twophase_commit_error.pl

diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 1035e8b3fc7..07a628e2a78 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -1652,6 +1652,8 @@ FinishPreparedTransaction(const char *gid, bool isCommit)
 	 */
 	LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE);
 
+	INJECTION_POINT("twophase-callbacks-execution", NULL);
+
 	/* And now do the callbacks */
 	if (isCommit)
 		ProcessRecords(bufptr, fxid, twophase_postcommit_callbacks);
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 36d789720a3..6305ce88b67 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -61,6 +61,7 @@ tests += {
       't/050_redo_segment_missing.pl',
       't/051_effective_wal_level.pl',
       't/052_checkpoint_segment_missing.pl',
+      't/053_twophase_commit_error.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/053_twophase_commit_error.pl b/src/test/recovery/t/053_twophase_commit_error.pl
new file mode 100644
index 00000000000..96006c6dd2d
--- /dev/null
+++ b/src/test/recovery/t/053_twophase_commit_error.pl
@@ -0,0 +1,76 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test failure when COMMIT PREPARED gets an ERROR after its commit WAL record
+# has been emitted, but before two-phase cleanup callbacks have released locks.
+
+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('node');
+$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', 'CREATE EXTENSION injection_points');
+$node->safe_psql(
+	'postgres', q{
+	SELECT injection_points_attach('twophase-callbacks-execution', 'error')
+});
+
+$node->safe_psql(
+	'postgres', q{
+	CREATE TABLE twophase_lock_target(id int);
+	CREATE TABLE twophase_commit_marker(id int);
+
+	BEGIN;
+	LOCK TABLE twophase_lock_target IN ACCESS EXCLUSIVE MODE;
+	INSERT INTO twophase_commit_marker VALUES (1);
+	PREPARE TRANSACTION 'prepared';
+});
+
+my $table_oid = $node->safe_psql(
+	'postgres', q{
+	SELECT oid FROM pg_class WHERE relname = 'twophase_lock_target';
+});
+
+
+my ($ret, $stdout, $stderr) =
+  $node->psql('postgres', q{COMMIT PREPARED 'prepared'});
+
+isnt($ret, 0, 'COMMIT PREPARED failed at injectcion point');
+
+$ret = $node->safe_psql(
+	'postgres', q{
+	SELECT count(*) FROM pg_prepared_xacts;
+});
+is($ret, 0, 'we cannot see transaction in the catalog');
+
+$ret = $node->safe_psql(
+	'postgres', q{
+	SELECT * FROM twophase_commit_marker;
+});
+is($ret, 1, 'transaction has been commited');
+
+
+$ret = $node->safe_psql(
+	'postgres', qq{
+	SELECT COUNT(*) FROM pg_locks WHERE locktype = 'relation' AND relation = $table_oid;
+});
+is($ret, 1, 'orphaned lock detected');
+
+$node->stop;
+done_testing();
-- 
2.43.0

