From c64479392f1d47d128d6c034697d251f6f37b527 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <masahiko.sawada@2ndquadrant.com>
Date: Thu, 26 Mar 2020 21:41:29 +0500
Subject: [PATCH v20 5/5] Add regression tests for atomic commit.

---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/test_fdwxact/.gitignore      |   4 +
 src/test/modules/test_fdwxact/Makefile        |  28 ++
 .../test_fdwxact/expected/test_fdwxact.out    | 188 +++++++
 src/test/modules/test_fdwxact/fdwxact.conf    |   7 +
 .../modules/test_fdwxact/sql/test_fdwxact.sql | 178 +++++++
 src/test/modules/test_fdwxact/t/001_basic.pl  | 119 +++++
 .../test_fdwxact/test_fdwxact--1.0.sql        |  44 ++
 src/test/modules/test_fdwxact/test_fdwxact.c  | 471 ++++++++++++++++++
 .../modules/test_fdwxact/test_fdwxact.control |   4 +
 src/test/recovery/Makefile                    |   2 +-
 src/test/recovery/t/021_fdwxact.pl            | 175 +++++++
 src/test/regress/pg_regress.c                 |  13 +-
 13 files changed, 1229 insertions(+), 5 deletions(-)
 create mode 100644 src/test/modules/test_fdwxact/.gitignore
 create mode 100644 src/test/modules/test_fdwxact/Makefile
 create mode 100644 src/test/modules/test_fdwxact/expected/test_fdwxact.out
 create mode 100644 src/test/modules/test_fdwxact/fdwxact.conf
 create mode 100644 src/test/modules/test_fdwxact/sql/test_fdwxact.sql
 create mode 100644 src/test/modules/test_fdwxact/t/001_basic.pl
 create mode 100644 src/test/modules/test_fdwxact/test_fdwxact--1.0.sql
 create mode 100644 src/test/modules/test_fdwxact/test_fdwxact.c
 create mode 100644 src/test/modules/test_fdwxact/test_fdwxact.control
 create mode 100644 src/test/recovery/t/021_fdwxact.pl

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 29de73c060..8a48e6ba19 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -13,6 +13,7 @@ SUBDIRS = \
 		  test_bloomfilter \
 		  test_ddl_deparse \
 		  test_extensions \
+		  test_fdwxact \
 		  test_ginpostinglist \
 		  test_integerset \
 		  test_misc \
diff --git a/src/test/modules/test_fdwxact/.gitignore b/src/test/modules/test_fdwxact/.gitignore
new file mode 100644
index 0000000000..5dcb3ff972
--- /dev/null
+++ b/src/test/modules/test_fdwxact/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_fdwxact/Makefile b/src/test/modules/test_fdwxact/Makefile
new file mode 100644
index 0000000000..b3fc99aee3
--- /dev/null
+++ b/src/test/modules/test_fdwxact/Makefile
@@ -0,0 +1,28 @@
+# src/test/modules/test_fdwxact/Makefile
+
+MODULE_big = test_fdwxact
+OBJS = \
+	$(WIN32RES) \
+	test_fdwxact.o
+PGFILEDESC = "test_fdwxact - test code for src/backend/access/fdwxact"
+
+EXTENSION = test_fdwxact
+DATA = test_fdwxact--1.0.sql
+
+REGRESS_OPTS = --temp-config=$(top_srcdir)/src/test/modules/test_fdwxact/fdwxact.conf
+REGRESS = test_fdwxact
+
+NO_INSTALLCHECK = 1
+
+TAP_TESTS =1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_fdwxact
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_fdwxact/expected/test_fdwxact.out b/src/test/modules/test_fdwxact/expected/test_fdwxact.out
new file mode 100644
index 0000000000..a5c8b89655
--- /dev/null
+++ b/src/test/modules/test_fdwxact/expected/test_fdwxact.out
@@ -0,0 +1,188 @@
+--
+-- Test for foreign transaction management.
+--
+CREATE EXTENSION test_fdwxact;
+-- setup two servers that don't support transaction management API
+CREATE SERVER srv_1 FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER srv_2 FOREIGN DATA WRAPPER test_fdw;
+-- setup two servers that support only commit and rollback API
+CREATE SERVER srv_no2pc_1 FOREIGN DATA WRAPPER test_no2pc_fdw;
+CREATE SERVER srv_no2pc_2 FOREIGN DATA WRAPPER test_no2pc_fdw;
+-- setup two servers that support commit, rollback and prepare API.
+-- That is, those two server support two-phase commit protocol.
+CREATE SERVER srv_2pc_1 FOREIGN DATA WRAPPER test_2pc_fdw;
+CREATE SERVER srv_2pc_2 FOREIGN DATA WRAPPER test_2pc_fdw;
+CREATE TABLE t (i int);
+CREATE FOREIGN TABLE ft_1 (i int) SERVER srv_1;
+CREATE FOREIGN TABLE ft_2 (i int) SERVER srv_2;
+CREATE FOREIGN TABLE ft_no2pc_1 (i int) SERVER srv_no2pc_1;
+CREATE FOREIGN TABLE ft_no2pc_2 (i int) SERVER srv_no2pc_2;
+CREATE FOREIGN TABLE ft_2pc_1 (i int) SERVER srv_2pc_1;
+CREATE FOREIGN TABLE ft_2pc_2 (i int) SERVER srv_2pc_2;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_1;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_2;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_no2pc_1;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_no2pc_2;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_2pc_1;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_2pc_2;
+-- Test 'disabled' case.
+-- Modifies one or two servers but since we don't require two-phase
+-- commit, all case should not raise an error.
+SET foreign_twophase_commit TO disabled;
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+ROLLBACK;
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+ROLLBACK;
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+ROLLBACK;
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+INSERT INTO ft_no2pc_2 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+INSERT INTO ft_2pc_2 VALUES (1);
+COMMIT;
+-- Test 'required' case.
+-- In this case, when two-phase commit is required, all servers
+-- which are involved in the and modified need to support two-phase
+-- commit protocol. Otherwise transaction will rollback.
+SET foreign_twophase_commit TO 'required';
+-- Ok. Writing only one server doesn't require two-phase commit.
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+-- Ok. Writing two servers, we require two-phase commit and success.
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+INSERT INTO ft_2pc_2 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO t VALUES (1);
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+-- Ok. Only reading servers doesn't require two-phase commit.
+BEGIN;
+SELECT * FROM ft_2pc_1;
+ i 
+---
+(0 rows)
+
+SELECT * FROM ft_2pc_2;
+ i 
+---
+(0 rows)
+
+COMMIT;
+BEGIN;
+SELECT * FROM ft_1;
+ i 
+---
+(0 rows)
+
+SELECT * FROM ft_no2pc_1;
+ i 
+---
+(0 rows)
+
+COMMIT;
+-- Ok. Read one server and write one server.
+BEGIN;
+SELECT * FROM ft_1;
+ i 
+---
+(0 rows)
+
+INSERT INTO ft_no2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+SELECT * FROM ft_no2pc_1;
+ i 
+---
+(0 rows)
+
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+-- Error. ft_1 doesn't support two-phase commit.
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+ERROR:  cannot process a distributed transaction that has operated on a foreign server
+DETAIL:  foreign_twophase_commit is 'required' but the transaction has some foreign servers which are not capable of two-phase commit
+-- Error. ft_no2pc_1 doesn't support two-phase commit.
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+ERROR:  cannot process a distributed transaction that has operated on a foreign server
+DETAIL:  foreign_twophase_commit is 'required' but the transaction has some foreign servers which are not capable of two-phase commit
+-- Error. Both ft_1 and ft_2 don't support two-phase commit.
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+INSERT INTO ft_2 VALUES (1);
+COMMIT;
+ERROR:  cannot process a distributed transaction that has operated on a foreign server
+DETAIL:  foreign_twophase_commit is 'required' but the transaction has some foreign servers which are not capable of two-phase commit
+-- Error. Both ft_no2pc_1 and ft_no2pc_2 don't support two-phase
+-- commit.
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+INSERT INTO ft_no2pc_2 VALUES (1);
+COMMIT;
+ERROR:  cannot process a distributed transaction that has operated on a foreign server
+DETAIL:  foreign_twophase_commit is 'required' but the transaction has some foreign servers which are not capable of two-phase commit
+-- Error. Two-phase commit is required because of writes on two
+-- servers: local node and ft_no2pc_1. But ft_no2pc_1 doesn't support
+-- two-phase commit.
+BEGIN;
+INSERT INTO t VALUES (1);
+INSERT INTO ft_no2pc_1 VALUES (1);
+COMMIT;
+ERROR:  cannot process a distributed transaction that has operated on a foreign server
+DETAIL:  foreign_twophase_commit is 'required' but the transaction has some foreign servers which are not capable of two-phase commit
+-- Test 'prefer' case.
+-- The cases where failed in 'required' case shoul pass in 'prefer'.
+-- We simply commit/rollback a transaction in one-phase on a server
+-- that doesn't support two-phase commit, instead of error.
+SET foreign_twophase_commit TO 'prefer';
+-- We modify at least one server that doesn't support two-phase commit.
+-- These servers are committed in one-phase.
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+INSERT INTO ft_2 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+INSERT INTO ft_no2pc_2 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO t VALUES (1);
+INSERT INTO ft_no2pc_1 VALUES (1);
+COMMIT;
diff --git a/src/test/modules/test_fdwxact/fdwxact.conf b/src/test/modules/test_fdwxact/fdwxact.conf
new file mode 100644
index 0000000000..20e4a671df
--- /dev/null
+++ b/src/test/modules/test_fdwxact/fdwxact.conf
@@ -0,0 +1,7 @@
+shared_preload_libraries = 'test_fdwxact'
+max_prepared_transactions = 10
+max_prepared_foreign_transactions = 10
+max_foreign_transaction_resolvers = 1
+foreign_transaction_resolver_timeout = 0
+foreign_transaction_resolution_retry_interval = 5s
+foreign_twophase_commit = disabled
diff --git a/src/test/modules/test_fdwxact/sql/test_fdwxact.sql b/src/test/modules/test_fdwxact/sql/test_fdwxact.sql
new file mode 100644
index 0000000000..554312542f
--- /dev/null
+++ b/src/test/modules/test_fdwxact/sql/test_fdwxact.sql
@@ -0,0 +1,178 @@
+--
+-- Test for foreign transaction management.
+--
+
+CREATE EXTENSION test_fdwxact;
+
+-- setup two servers that don't support transaction management API
+CREATE SERVER srv_1 FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER srv_2 FOREIGN DATA WRAPPER test_fdw;
+
+-- setup two servers that support only commit and rollback API
+CREATE SERVER srv_no2pc_1 FOREIGN DATA WRAPPER test_no2pc_fdw;
+CREATE SERVER srv_no2pc_2 FOREIGN DATA WRAPPER test_no2pc_fdw;
+
+-- setup two servers that support commit, rollback and prepare API.
+-- That is, those two server support two-phase commit protocol.
+CREATE SERVER srv_2pc_1 FOREIGN DATA WRAPPER test_2pc_fdw;
+CREATE SERVER srv_2pc_2 FOREIGN DATA WRAPPER test_2pc_fdw;
+
+CREATE TABLE t (i int);
+CREATE FOREIGN TABLE ft_1 (i int) SERVER srv_1;
+CREATE FOREIGN TABLE ft_2 (i int) SERVER srv_2;
+CREATE FOREIGN TABLE ft_no2pc_1 (i int) SERVER srv_no2pc_1;
+CREATE FOREIGN TABLE ft_no2pc_2 (i int) SERVER srv_no2pc_2;
+CREATE FOREIGN TABLE ft_2pc_1 (i int) SERVER srv_2pc_1;
+CREATE FOREIGN TABLE ft_2pc_2 (i int) SERVER srv_2pc_2;
+
+CREATE USER MAPPING FOR PUBLIC SERVER srv_1;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_2;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_no2pc_1;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_no2pc_2;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_2pc_1;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_2pc_2;
+
+-- Test 'disabled' case.
+-- Modifies one or two servers but since we don't require two-phase
+-- commit, all case should not raise an error.
+SET foreign_twophase_commit TO disabled;
+
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+ROLLBACK;
+
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+ROLLBACK;
+
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+ROLLBACK;
+
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+INSERT INTO ft_no2pc_2 VALUES (1);
+COMMIT;
+
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+INSERT INTO ft_2pc_2 VALUES (1);
+COMMIT;
+
+-- Test 'required' case.
+-- In this case, when two-phase commit is required, all servers
+-- which are involved in the and modified need to support two-phase
+-- commit protocol. Otherwise transaction will rollback.
+SET foreign_twophase_commit TO 'required';
+
+-- Ok. Writing only one server doesn't require two-phase commit.
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+
+-- Ok. Writing two servers, we require two-phase commit and success.
+BEGIN;
+INSERT INTO ft_2pc_1 VALUES (1);
+INSERT INTO ft_2pc_2 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO t VALUES (1);
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+
+-- Ok. Only reading servers doesn't require two-phase commit.
+BEGIN;
+SELECT * FROM ft_2pc_1;
+SELECT * FROM ft_2pc_2;
+COMMIT;
+BEGIN;
+SELECT * FROM ft_1;
+SELECT * FROM ft_no2pc_1;
+COMMIT;
+
+-- Ok. Read one server and write one server.
+BEGIN;
+SELECT * FROM ft_1;
+INSERT INTO ft_no2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+SELECT * FROM ft_no2pc_1;
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+
+-- Error. ft_1 doesn't support two-phase commit.
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+
+-- Error. ft_no2pc_1 doesn't support two-phase commit.
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+
+-- Error. Both ft_1 and ft_2 don't support two-phase commit.
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+INSERT INTO ft_2 VALUES (1);
+COMMIT;
+
+-- Error. Both ft_no2pc_1 and ft_no2pc_2 don't support two-phase
+-- commit.
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+INSERT INTO ft_no2pc_2 VALUES (1);
+COMMIT;
+
+-- Error. Two-phase commit is required because of writes on two
+-- servers: local node and ft_no2pc_1. But ft_no2pc_1 doesn't support
+-- two-phase commit.
+BEGIN;
+INSERT INTO t VALUES (1);
+INSERT INTO ft_no2pc_1 VALUES (1);
+COMMIT;
+
+-- Test 'prefer' case.
+-- The cases where failed in 'required' case shoul pass in 'prefer'.
+-- We simply commit/rollback a transaction in one-phase on a server
+-- that doesn't support two-phase commit, instead of error.
+SET foreign_twophase_commit TO 'prefer';
+
+-- We modify at least one server that doesn't support two-phase commit.
+-- These servers are committed in one-phase.
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+INSERT INTO ft_2pc_1 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_1 VALUES (1);
+INSERT INTO ft_2 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO ft_no2pc_1 VALUES (1);
+INSERT INTO ft_no2pc_2 VALUES (1);
+COMMIT;
+BEGIN;
+INSERT INTO t VALUES (1);
+INSERT INTO ft_no2pc_1 VALUES (1);
+COMMIT;
diff --git a/src/test/modules/test_fdwxact/t/001_basic.pl b/src/test/modules/test_fdwxact/t/001_basic.pl
new file mode 100644
index 0000000000..c712fd1cf3
--- /dev/null
+++ b/src/test/modules/test_fdwxact/t/001_basic.pl
@@ -0,0 +1,119 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 10;
+
+my $node = get_new_node('main');
+$node->init;
+$node->append_conf('postgresql.conf', qq(
+shared_preload_libraries = 'test_fdwxact'
+max_prepared_transactions = 10
+max_prepared_foreign_transactions = 10
+max_foreign_transaction_resolvers = 2
+foreign_transaction_resolver_timeout = 0
+foreign_transaction_resolution_retry_interval = 5s
+foreign_twophase_commit = required
+test_fdwxact.log_api_calls = true
+				   ));
+$node->start;
+
+$node->psql(
+	'postgres', "
+CREATE EXTENSION test_fdwxact;
+CREATE SERVER srv FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER srv_no2pc FOREIGN DATA WRAPPER test_no2pc_fdw;
+CREATE SERVER srv_2pc_1 FOREIGN DATA WRAPPER test_2pc_fdw;
+CREATE SERVER srv_2pc_2 FOREIGN DATA WRAPPER test_2pc_fdw;
+
+CREATE TABLE t (i int);
+CREATE FOREIGN TABLE ft (i int) SERVER srv;
+CREATE FOREIGN TABLE ft_no2pc (i int) SERVER srv_no2pc;
+CREATE FOREIGN TABLE ft_2pc_1 (i int) SERVER srv_2pc_1;
+CREATE FOREIGN TABLE ft_2pc_2 (i int) SERVER srv_2pc_2;
+
+CREATE USER MAPPING FOR PUBLIC SERVER srv;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_no2pc;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_2pc_1;
+CREATE USER MAPPING FOR PUBLIC SERVER srv_2pc_2;
+	");
+
+sub run_transaction
+{
+	my ($node, $prepsql, $sql) = @_;
+
+	local $ENV{PGHOST} = $node->host;
+	local $ENV{PGPORT} = $node->port;
+
+	truncate $node->logfile, 0;
+
+	$node->safe_psql('postgres', $prepsql);
+	my ($cmdret, $stdout, $stderr) = $node->psql('postgres',
+												 "BEGIN;
+												 SELECT txid_current() as xid;
+												 $sql
+												 COMMIT;");
+	my $log = TestLib::slurp_file($node->logfile);
+
+	return $log, $stdout;
+}
+
+my ($log, $xid);
+
+# The transaction is committed using two-phase commit.
+($log, $xid) = run_transaction($node, "",
+							   "INSERT INTO ft_2pc_1 VALUES(1);
+							   INSERT INTO ft_2pc_2 VALUES(1);");
+like($log, qr/commit prepared tx_$xid on srv_2pc_1/, "commit prepared transaction-1");
+like($log, qr/commit prepared tx_$xid on srv_2pc_2/, "commit prepared transaction-2");
+
+# Similary, two-phase commit is used.
+($log, $xid) = run_transaction($node, "",
+					  "INSERT INTO t VALUES(1);
+					  INSERT INTO ft_2pc_1 VALUES(1);");
+like($log, qr/commit prepared tx_$xid on srv_2pc_1/, "commit prepared transaction-3");
+
+# Inject an error into prepare phase on srv_2pc_1. The transaction fails during
+# preparing the foreign transaction on srv_2pc_1. Then, we try to both 'rollback' and
+# 'rollback prepared' the foreign transaction, and rollback another foreign
+# transaction.
+($log, $xid) = run_transaction($node,
+							   "SELECT test_inject_error('error', 'prepare', 'srv_2pc_1');",
+							   "INSERT INTO ft_2pc_1 VALUES(1);
+							   INSERT INTO ft_2pc_2 VALUES(1);");
+like($log, qr/rollback $xid on srv_2pc_1/, "rollback on failed server");
+like($log, qr/rollback prepared tx_$xid on srv_2pc_1/, "rollback prepared on failed server");
+like($log, qr/rollback $xid on srv_2pc_2/, "rollback on another server");
+
+# Inject an panic into prepare phase on srv_2pc_2. The server crashes after preparing both
+# foreign transaction. After the restart, those transactions are recovered as in-doubt
+# transactions. We check if the resolver process rollbacks those transaction after recovery.
+($log, $xid) = run_transaction($node,
+							   "SELECT test_inject_error('panic', 'prepare', 'srv_2pc_2');",
+							   "INSERT INTO ft_2pc_1 VALUES(1);
+							   INSERT INTO ft_2pc_2 VALUES(1);");
+$node->restart();
+$node->poll_query_until('postgres',
+						"SELECT count(*) = 0 FROM pg_foreign_xacts")
+  or die "Timeout while waiting for resolver process to resolve in-doubt transactions";
+$log = TestLib::slurp_file($node->logfile);
+like($log, qr/rollback prepared tx_[0-9]+ on srv_2pc_1/, "resolver rolled back in-doubt transaction");
+like($log, qr/rollback prepared tx_[0-9]+ on srv_2pc_2/, "resolver rolled back in-doubt transaction");
+truncate $node->logfile, 0;
+
+# Inject an panic into commit phase on srv_2pc_1. The server crashes due to the panic
+# error raised by resolver process during commit prepared foreign transaction on srv_2pc_1.
+# After the restart, those transactions are recovered as in-doubt transactions. We check if
+# the resolver process commits those transaction after recovery.
+($log, $xid) = run_transaction($node,
+							   "SELECT test_inject_error('panic', 'commit', 'srv_2pc_1');",
+							   "INSERT INTO ft_2pc_1 VALUES(1);
+							   INSERT INTO ft_2pc_2 VALUES(1);");
+$node->restart();
+$node->poll_query_until('postgres',
+						"SELECT count(*) = 0 FROM pg_foreign_xacts")
+  or die "Timeout while waiting for resolver process to resolve in-doubt transactions";
+$log = TestLib::slurp_file($node->logfile);
+like($log, qr/commit prepared tx_[0-9]+ on srv_2pc_1/, "resolver rolled back in-doubt transaction");
+like($log, qr/commit prepared tx_[0-9]+ on srv_2pc_2/, "resolver rolled back in-doubt transaction");
+truncate $node->logfile, 0;
diff --git a/src/test/modules/test_fdwxact/test_fdwxact--1.0.sql b/src/test/modules/test_fdwxact/test_fdwxact--1.0.sql
new file mode 100644
index 0000000000..f676dfe04b
--- /dev/null
+++ b/src/test/modules/test_fdwxact/test_fdwxact--1.0.sql
@@ -0,0 +1,44 @@
+/* src/test/modules/test_atomic_commit/test_atomic_commit--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_atomic_commit" to load this file. \quit
+
+-- test_fdw doesn't use transaction API
+CREATE FUNCTION test_fdw_handler()
+RETURNS fdw_handler
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FOREIGN DATA WRAPPER test_fdw
+  HANDLER test_fdw_handler;
+
+-- test_no2pc_fdw uses only COMMIT and ROLLBACK API
+CREATE FUNCTION test_no2pc_fdw_handler()
+RETURNS fdw_handler
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FOREIGN DATA WRAPPER test_no2pc_fdw
+  HANDLER test_no2pc_fdw_handler;
+
+-- test_2pc uses PREPARE API as well
+CREATE FUNCTION test_2pc_fdw_handler()
+RETURNS fdw_handler
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FOREIGN DATA WRAPPER test_2pc_fdw
+  HANDLER test_2pc_fdw_handler;
+
+CREATE FUNCTION test_inject_error(
+elevel text,
+phase text,
+server text)
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION test_reset_error()
+RETURNS bool
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
diff --git a/src/test/modules/test_fdwxact/test_fdwxact.c b/src/test/modules/test_fdwxact/test_fdwxact.c
new file mode 100644
index 0000000000..d8bfe48d96
--- /dev/null
+++ b/src/test/modules/test_fdwxact/test_fdwxact.c
@@ -0,0 +1,471 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_fdwxact.c
+ *		  Test modules for foreign transaction management
+ *
+ * This module implements three types of foreign data wrapper: the first
+ * doesn't support any transaction FDW APIs, the second supports only
+ * commit and rollback API and the third supports all transaction API including
+ * prepare.
+ *
+ * Also, this module has an ability to inject an error at prepare callback or
+ * commit callback using test_inject_error() SQL function. The information of
+ * injected error is stored in the shared memory so that backend processes and
+ * resolver processes can see it.
+ *
+ * Portions Copyright (c) 2020, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_fdwxact/test_fdwxact.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/fdwxact.h"
+#include "commands/defrem.h"
+#include "access/reloptions.h"
+#include "foreign/fdwapi.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "optimizer/clauses.h"
+#include "optimizer/cost.h"
+#include "optimizer/optimizer.h"
+#include "optimizer/pathnode.h"
+#include "optimizer/paths.h"
+#include "optimizer/planmain.h"
+#include "optimizer/restrictinfo.h"
+#include "storage/ipc.h"
+#include "storage/spin.h"
+#include "utils/builtins.h"
+
+PG_MODULE_MAGIC;
+
+#define TEST_FDWXCT_MAX_NAME_LEN 32
+
+typedef struct testFdwXactSharedState
+{
+	char	elevel[TEST_FDWXCT_MAX_NAME_LEN];
+	char	phase[TEST_FDWXCT_MAX_NAME_LEN];
+	char	server[TEST_FDWXCT_MAX_NAME_LEN];
+	LWLock	*lock;
+} testFdwXactSharedState;
+testFdwXactSharedState *fxss = NULL;
+
+static shmem_startup_hook_type prev_shmem_startup_hook = NULL;
+static bool log_api_calls = false;
+
+void _PG_init(void);
+void _PG_fini(void);
+PG_FUNCTION_INFO_V1(test_fdw_handler);
+PG_FUNCTION_INFO_V1(test_no2pc_fdw_handler);
+PG_FUNCTION_INFO_V1(test_2pc_fdw_handler);
+PG_FUNCTION_INFO_V1(test_inject_error);
+PG_FUNCTION_INFO_V1(test_reset_error);
+
+static void test_fdwxact_shmem_startup(void);
+static bool check_event(char *servername, char *phase, int *elevel);
+static void testGetForeignRelSize(PlannerInfo *root,
+								  RelOptInfo *baserel,
+								  Oid foreigntableid);
+static void testGetForeignPaths(PlannerInfo *root,
+								RelOptInfo *baserel,
+								Oid foreigntableid);
+static ForeignScan *testGetForeignPlan(PlannerInfo *root,
+									   RelOptInfo *foreignrel,
+									   Oid foreigntableid,
+									   ForeignPath *best_path,
+									   List *tlist,
+									   List *scan_clauses,
+									   Plan *outer_plan);
+static void testBeginForeignScan(ForeignScanState *node, int eflags);
+static TupleTableSlot *testIterateForeignScan(ForeignScanState *node);
+static void testReScanForeignScan(ForeignScanState *node);
+static void testEndForeignScan(ForeignScanState *node);
+static void testBeginForeignModify(ModifyTableState *mtstate,
+								   ResultRelInfo *resultRelInfo,
+								   List *fdw_private,
+								   int subplan_index,
+								   int eflags);
+static TupleTableSlot *testExecForeignInsert(EState *estate,
+											 ResultRelInfo *resultRelInfo,
+											 TupleTableSlot *slot,
+											 TupleTableSlot *planSlot);
+static void testEndForeignModify(EState *estate,
+								 ResultRelInfo *resultRelInfo);
+static void testBeginForeignInsert(ModifyTableState *mtstate,
+								   ResultRelInfo *resultRelInfo);
+static void testEndForeignInsert(EState *estate,
+								 ResultRelInfo *resultRelInfo);
+static int	testIsForeignRelUpdatable(Relation rel);
+static void testPrepareForeignTransaction(FdwXactRslvState *state);
+static void testCommitForeignTransaction(FdwXactRslvState *state);
+static void testRollbackForeignTransaction(FdwXactRslvState *state);
+static char *testGetPrepareId(TransactionId xid, Oid serverid,
+							  Oid userid, int *prep_id_len);
+
+void
+_PG_init(void)
+{
+	DefineCustomBoolVariable("test_fdwxact.log_api_calls",
+							 "Report transaction API calls to logs.",
+							 NULL,
+							 &log_api_calls,
+							 false,
+							 PGC_USERSET,
+							 0,
+							 NULL, NULL, NULL);
+
+	RequestAddinShmemSpace(MAXALIGN(sizeof(testFdwXactSharedState)));
+	RequestNamedLWLockTranche("test_fdwxact", 1);
+
+	prev_shmem_startup_hook = shmem_startup_hook;
+	shmem_startup_hook = test_fdwxact_shmem_startup;
+}
+
+void
+_PG_fini(void)
+{
+	/* Uninstall hooks. */
+	shmem_startup_hook = prev_shmem_startup_hook;
+}
+
+static void
+test_fdwxact_shmem_startup(void)
+{
+	bool found;
+
+	if (prev_shmem_startup_hook)
+		prev_shmem_startup_hook();
+
+	fxss = ShmemInitStruct("test_fdwxact",
+						   sizeof(testFdwXactSharedState),
+						   &found);
+	if (!found)
+	{
+		memset(fxss->elevel, 0, TEST_FDWXCT_MAX_NAME_LEN);
+		memset(fxss->phase, 0, TEST_FDWXCT_MAX_NAME_LEN);
+		memset(fxss->server, 0, TEST_FDWXCT_MAX_NAME_LEN);
+		fxss->lock = &(GetNamedLWLockTranche("test_fdwxact"))->lock;
+	}
+}
+
+Datum
+test_fdw_handler(PG_FUNCTION_ARGS)
+{
+	FdwRoutine *routine = makeNode(FdwRoutine);
+
+	/* Functions for scanning foreign tables */
+	routine->GetForeignRelSize = testGetForeignRelSize;
+	routine->GetForeignPaths = testGetForeignPaths;
+	routine->GetForeignPlan = testGetForeignPlan;
+	routine->BeginForeignScan = testBeginForeignScan;
+	routine->IterateForeignScan = testIterateForeignScan;
+	routine->ReScanForeignScan = testReScanForeignScan;
+	routine->EndForeignScan = testEndForeignScan;
+
+	/* Functions for updating foreign tables */
+	routine->AddForeignUpdateTargets = NULL;
+	routine->PlanForeignModify = NULL;
+	routine->BeginForeignModify = testBeginForeignModify;
+	routine->ExecForeignInsert = testExecForeignInsert;
+	routine->EndForeignModify = testEndForeignModify;
+	routine->BeginForeignInsert = testBeginForeignInsert;
+	routine->EndForeignInsert = testEndForeignInsert;
+	routine->IsForeignRelUpdatable = testIsForeignRelUpdatable;
+
+	PG_RETURN_POINTER(routine);
+}
+
+Datum
+test_no2pc_fdw_handler(PG_FUNCTION_ARGS)
+{
+	FdwRoutine *routine = makeNode(FdwRoutine);
+
+	/* Functions for scanning foreign tables */
+	routine->GetForeignRelSize = testGetForeignRelSize;
+	routine->GetForeignPaths = testGetForeignPaths;
+	routine->GetForeignPlan = testGetForeignPlan;
+	routine->BeginForeignScan = testBeginForeignScan;
+	routine->IterateForeignScan = testIterateForeignScan;
+	routine->ReScanForeignScan = testReScanForeignScan;
+	routine->EndForeignScan = testEndForeignScan;
+
+	/* Functions for updating foreign tables */
+	routine->AddForeignUpdateTargets = NULL;
+	routine->PlanForeignModify = NULL;
+	routine->BeginForeignModify = testBeginForeignModify;
+	routine->ExecForeignInsert = testExecForeignInsert;
+	routine->EndForeignModify = testEndForeignModify;
+	routine->BeginForeignInsert = testBeginForeignInsert;
+	routine->EndForeignInsert = testEndForeignInsert;
+	routine->IsForeignRelUpdatable = testIsForeignRelUpdatable;
+
+	/* Support only COMMIT and ROLLBACK */
+	routine->CommitForeignTransaction = testCommitForeignTransaction;
+	routine->RollbackForeignTransaction = testRollbackForeignTransaction;
+
+	PG_RETURN_POINTER(routine);
+}
+
+Datum
+test_2pc_fdw_handler(PG_FUNCTION_ARGS)
+{
+	FdwRoutine *routine = makeNode(FdwRoutine);
+
+	/* Functions for scanning foreign tables */
+	routine->GetForeignRelSize = testGetForeignRelSize;
+	routine->GetForeignPaths = testGetForeignPaths;
+	routine->GetForeignPlan = testGetForeignPlan;
+	routine->BeginForeignScan = testBeginForeignScan;
+	routine->IterateForeignScan = testIterateForeignScan;
+	routine->ReScanForeignScan = testReScanForeignScan;
+	routine->EndForeignScan = testEndForeignScan;
+
+	/* Functions for updating foreign tables */
+	routine->AddForeignUpdateTargets = NULL;
+	routine->PlanForeignModify = NULL;
+	routine->BeginForeignModify = testBeginForeignModify;
+	routine->ExecForeignInsert = testExecForeignInsert;
+	routine->EndForeignModify = testEndForeignModify;
+	routine->BeginForeignInsert = testBeginForeignInsert;
+	routine->EndForeignInsert = testEndForeignInsert;
+	routine->IsForeignRelUpdatable = testIsForeignRelUpdatable;
+
+	/* Support all functions for foreign transactions */
+	routine->GetPrepareId = testGetPrepareId;
+	routine->PrepareForeignTransaction = testPrepareForeignTransaction;
+	routine->CommitForeignTransaction = testCommitForeignTransaction;
+	routine->RollbackForeignTransaction = testRollbackForeignTransaction;
+
+	PG_RETURN_POINTER(routine);
+}
+
+static void
+testGetForeignRelSize(PlannerInfo *root,
+					  RelOptInfo *baserel,
+					  Oid foreigntableid)
+{
+	baserel->pages = 10;
+	baserel->tuples = 100;
+}
+
+static void
+testGetForeignPaths(PlannerInfo *root,
+					RelOptInfo *baserel,
+					Oid foreigntableid)
+{
+	add_path(baserel, (Path *) create_foreignscan_path(root, baserel,
+													   NULL,
+													   10, 10, 10,
+													   NIL,
+													   baserel->lateral_relids,
+													   NULL, NIL));
+}
+
+static ForeignScan *
+testGetForeignPlan(PlannerInfo *root,
+				   RelOptInfo *foreignrel,
+				   Oid foreigntableid,
+				   ForeignPath *best_path,
+				   List *tlist,
+				   List *scan_clauses,
+				   Plan *outer_plan)
+{
+	return make_foreignscan(tlist,
+							NIL,
+							foreignrel->relid,
+							NIL,
+							NULL,
+							NIL,
+							NIL,
+							outer_plan);
+}
+
+static void
+testBeginForeignScan(ForeignScanState *node, int eflags)
+{
+	return;
+}
+
+static TupleTableSlot *
+testIterateForeignScan(ForeignScanState *node)
+{
+	return ExecClearTuple(node->ss.ss_ScanTupleSlot);
+}
+
+static void
+testReScanForeignScan(ForeignScanState *node)
+{
+	return;
+}
+
+static void
+testEndForeignScan(ForeignScanState *node)
+{
+	return;
+}
+
+static void
+testBeginForeignModify(ModifyTableState *mtstate,
+					   ResultRelInfo *resultRelInfo,
+					   List *fdw_private,
+					   int subplan_index,
+					   int eflags)
+{
+	return;
+}
+
+static TupleTableSlot *
+testExecForeignInsert(EState *estate,
+					  ResultRelInfo *resultRelInfo,
+					  TupleTableSlot *slot,
+					  TupleTableSlot *planSlot)
+{
+	return slot;
+}
+
+static void
+testEndForeignModify(EState *estate,
+					 ResultRelInfo *resultRelInfo)
+{
+	return;
+}
+
+static void
+testBeginForeignInsert(ModifyTableState *mtstate,
+					   ResultRelInfo *resultRelInfo)
+{
+	return;
+}
+
+static void
+testEndForeignInsert(EState *estate,
+					 ResultRelInfo *resultRelInfo)
+{
+	return;
+}
+
+static int
+testIsForeignRelUpdatable(Relation rel)
+{
+	/* allow only inserts */
+	return (1 << CMD_INSERT);
+}
+
+static char *
+testGetPrepareId(TransactionId xid, Oid serverid,
+				 Oid userid, int *prep_id_len)
+{
+	static char buf[32] = {0};
+
+	*prep_id_len = snprintf(buf, 32, "tx_%u", xid);
+
+	return buf;
+}
+
+static void
+testPrepareForeignTransaction(FdwXactRslvState *state)
+{
+	int elevel;
+
+	if (check_event(state->server->servername, "prepare", &elevel))
+		elog(elevel, "injected error at prepare");
+
+	if (log_api_calls)
+		ereport(LOG, (errmsg("prepare %s on %s",
+							 state->fdwxact_id,
+							 state->server->servername)));
+}
+
+static void
+testCommitForeignTransaction(FdwXactRslvState *state)
+{
+	int elevel;
+
+	if (check_event(state->server->servername, "commit", &elevel))
+		elog(elevel, "injected error at commit");
+
+	if (log_api_calls)
+	{
+		if (state->flags && FDWXACT_FLAG_ONEPHASE)
+			ereport(LOG, (errmsg("commit %u on %s",
+								 state->xid, state->server->servername)));
+		else
+			ereport(LOG, (errmsg("commit prepared %s on %s",
+								 state->fdwxact_id,
+								 state->server->servername)));
+	}
+}
+
+static void
+testRollbackForeignTransaction(FdwXactRslvState *state)
+{
+	if (log_api_calls)
+	{
+		if (state->flags && FDWXACT_FLAG_ONEPHASE)
+			ereport(LOG, (errmsg("rollback %u on %s",
+								 state->xid, state->server->servername)));
+		else
+			ereport(LOG, (errmsg("rollback prepared %s on %s",
+								 state->fdwxact_id,
+								 state->server->servername)));
+	}
+}
+
+/*
+ * Check if an event is set at the phase on the server. If there is, set
+ * elevel and return true.
+ */
+static bool
+check_event(char *servername, char *phase, int *elevel)
+{
+	LWLockAcquire(fxss->lock, LW_SHARED);
+
+	if (strcasecmp(fxss->server, servername) != 0 ||
+		strcasecmp(fxss->phase, phase) != 0)
+	{
+		LWLockRelease(fxss->lock);
+		return false;
+	}
+
+	/* Currently support only error and panic */
+	if (strcasecmp(fxss->elevel, "error") == 0)
+		*elevel = ERROR;
+	if (strcasecmp(fxss->elevel, "panic") == 0)
+		*elevel = PANIC;
+
+	LWLockRelease(fxss->lock);
+
+	return true;
+}
+
+/* SQL function to inject an error */
+Datum
+test_inject_error(PG_FUNCTION_ARGS)
+{
+	char *elevel = text_to_cstring(PG_GETARG_TEXT_P(0));
+	char *phase = text_to_cstring(PG_GETARG_TEXT_P(1));
+	char *server = text_to_cstring(PG_GETARG_TEXT_P(2));
+
+	LWLockAcquire(fxss->lock, LW_EXCLUSIVE);
+	strncpy(fxss->elevel, elevel, TEST_FDWXCT_MAX_NAME_LEN);
+	strncpy(fxss->phase, phase, TEST_FDWXCT_MAX_NAME_LEN);
+	strncpy(fxss->server, server, TEST_FDWXCT_MAX_NAME_LEN);
+	LWLockRelease(fxss->lock);
+
+	PG_RETURN_NULL();
+}
+
+/* SQL function to reset an error */
+Datum
+test_reset_error(PG_FUNCTION_ARGS)
+{
+	LWLockAcquire(fxss->lock, LW_EXCLUSIVE);
+	memset(fxss->elevel, 0, TEST_FDWXCT_MAX_NAME_LEN);
+	memset(fxss->phase, 0, TEST_FDWXCT_MAX_NAME_LEN);
+	memset(fxss->server, 0, TEST_FDWXCT_MAX_NAME_LEN);
+	LWLockRelease(fxss->lock);
+
+	PG_RETURN_NULL();
+}
diff --git a/src/test/modules/test_fdwxact/test_fdwxact.control b/src/test/modules/test_fdwxact/test_fdwxact.control
new file mode 100644
index 0000000000..ac9945ba03
--- /dev/null
+++ b/src/test/modules/test_fdwxact/test_fdwxact.control
@@ -0,0 +1,4 @@
+comment = 'Test code for fdwxact'
+default_version = '1.0'
+module_pathname = '$libdir/test_fdwxact'
+relocatable = true
diff --git a/src/test/recovery/Makefile b/src/test/recovery/Makefile
index fa8e031526..d47d96975b 100644
--- a/src/test/recovery/Makefile
+++ b/src/test/recovery/Makefile
@@ -9,7 +9,7 @@
 #
 #-------------------------------------------------------------------------
 
-EXTRA_INSTALL=contrib/test_decoding
+EXTRA_INSTALL=contrib/test_decoding contrib/pageinspect contrib/postgres_fdw
 
 subdir = src/test/recovery
 top_builddir = ../../..
diff --git a/src/test/recovery/t/021_fdwxact.pl b/src/test/recovery/t/021_fdwxact.pl
new file mode 100644
index 0000000000..9af9bb81dc
--- /dev/null
+++ b/src/test/recovery/t/021_fdwxact.pl
@@ -0,0 +1,175 @@
+# Tests for transaction involving foreign servers
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 7;
+
+# Setup master node
+my $node_master = get_new_node("master");
+my $node_standby = get_new_node("standby");
+
+$node_master->init(allows_streaming => 1);
+$node_master->append_conf('postgresql.conf', qq(
+max_prepared_transactions = 10
+max_prepared_foreign_transactions = 10
+max_foreign_transaction_resolvers = 2
+foreign_transaction_resolver_timeout = 0
+foreign_transaction_resolution_retry_interval = 5s
+foreign_twophase_commit = on
+));
+$node_master->start;
+
+# Take backup from master node
+my $backup_name = 'master_backup';
+$node_master->backup($backup_name);
+
+# Set up standby node
+$node_standby->init_from_backup($node_master, $backup_name,
+							   has_streaming => 1);
+$node_standby->start;
+
+# Set up foreign nodes
+my $node_fs1 = get_new_node("fs1");
+my $node_fs2 = get_new_node("fs2");
+my $fs1_port = $node_fs1->port;
+my $fs2_port = $node_fs2->port;
+$node_fs1->init;
+$node_fs2->init;
+$node_fs1->append_conf('postgresql.conf', qq(max_prepared_transactions = 10));
+$node_fs2->append_conf('postgresql.conf', qq(max_prepared_transactions = 10));
+$node_fs1->start;
+$node_fs2->start;
+
+# Create foreign servers on the master node
+$node_master->safe_psql('postgres', qq(
+CREATE EXTENSION postgres_fdw
+));
+$node_master->safe_psql('postgres', qq(
+CREATE SERVER fs1 FOREIGN DATA WRAPPER postgres_fdw
+OPTIONS (dbname 'postgres', port '$fs1_port');
+));
+$node_master->safe_psql('postgres', qq(
+CREATE SERVER fs2 FOREIGN DATA WRAPPER postgres_fdw
+OPTIONS (dbname 'postgres', port '$fs2_port');
+));
+
+# Create user mapping on the master node
+$node_master->safe_psql('postgres', qq(
+CREATE USER MAPPING FOR CURRENT_USER SERVER fs1;
+CREATE USER MAPPING FOR CURRENT_USER SERVER fs2;
+));
+
+# Create tables on foreign nodes and import them to the master node
+$node_fs1->safe_psql('postgres', qq(
+CREATE SCHEMA fs;
+CREATE TABLE fs.t1 (c int);
+));
+$node_fs2->safe_psql('postgres', qq(
+CREATE SCHEMA fs;
+CREATE TABLE fs.t2 (c int);
+));
+$node_master->safe_psql('postgres', qq(
+IMPORT FOREIGN SCHEMA fs FROM SERVER fs1 INTO public;
+IMPORT FOREIGN SCHEMA fs FROM SERVER fs2 INTO public;
+CREATE TABLE l_table (c int);
+));
+
+# Switch to synchronous replication
+$node_master->safe_psql('postgres', qq(
+ALTER SYSTEM SET synchronous_standby_names ='*';
+));
+$node_master->reload;
+
+my $result;
+
+# Prepare two transactions involving multiple foreign servers and shutdown
+# the master node. Check if we can commit and rollback the foreign transactions
+# after the normal recovery.
+$node_master->safe_psql('postgres', qq(
+BEGIN;
+INSERT INTO t1 VALUES (1);
+INSERT INTO t2 VALUES (1);
+PREPARE TRANSACTION 'gxid1';
+BEGIN;
+INSERT INTO t1 VALUES (2);
+INSERT INTO t2 VALUES (2);
+PREPARE TRANSACTION 'gxid2';
+));
+
+$node_master->stop;
+$node_master->start;
+
+# Commit and rollback foreign transactions after the recovery.
+$result = $node_master->psql('postgres', qq(COMMIT PREPARED 'gxid1'));
+is($result, 0, 'Commit foreign transactions after recovery');
+$result = $node_master->psql('postgres', qq(ROLLBACK PREPARED 'gxid2'));
+is($result, 0, 'Rollback foreign transactions after recovery');
+
+#
+# Prepare two transactions involving multiple foreign servers and shutdown
+# the master node immediately. Check if we can commit and rollback the foreign
+# transactions after the crash recovery.
+#
+$node_master->safe_psql('postgres', qq(
+BEGIN;
+INSERT INTO t1 VALUES (3);
+INSERT INTO t2 VALUES (3);
+PREPARE TRANSACTION 'gxid1';
+BEGIN;
+INSERT INTO t1 VALUES (4);
+INSERT INTO t2 VALUES (4);
+PREPARE TRANSACTION 'gxid2';
+));
+
+$node_master->teardown_node;
+$node_master->start;
+
+# Commit and rollback foreign transactions after the crash recovery.
+$result = $node_master->psql('postgres', qq(COMMIT PREPARED 'gxid1'));
+is($result, 0, 'Commit foreign transactions after crash recovery');
+$result = $node_master->psql('postgres', qq(ROLLBACK PREPARED 'gxid2'));
+is($result, 0, 'Rollback foreign transactions after crash recovery');
+
+#
+# Commit transaction involving foreign servers and shutdown the master node
+# immediately before checkpoint. Check that WAL replay cleans up
+# its shared memory state release locks while replaying transaction commit.
+#
+$node_master->safe_psql('postgres', qq(
+BEGIN;
+INSERT INTO t1 VALUES (5);
+INSERT INTO t2 VALUES (5);
+COMMIT;
+));
+
+$node_master->teardown_node;
+$node_master->start;
+
+$result = $node_master->safe_psql('postgres', qq(
+SELECT count(*) FROM pg_foreign_xacts;
+));
+is($result, 0, "Cleanup of shared memory state for foreign transactions");
+
+#
+# Check if the standby node can process prepared foreign transaction
+# after promotion.
+#
+$node_master->safe_psql('postgres', qq(
+BEGIN;
+INSERT INTO t1 VALUES (6);
+INSERT INTO t2 VALUES (6);
+PREPARE TRANSACTION 'gxid1';
+BEGIN;
+INSERT INTO t1 VALUES (7);
+INSERT INTO t2 VALUES (7);
+PREPARE TRANSACTION 'gxid2';
+));
+
+$node_master->teardown_node;
+$node_standby->promote;
+
+$result = $node_standby->psql('postgres', qq(COMMIT PREPARED 'gxid1';));
+is($result, 0, 'Commit foreign transaction after promotion');
+$result = $node_standby->psql('postgres', qq(ROLLBACK PREPARED 'gxid2';));
+is($result, 0, 'Rollback foreign transaction after promotion');
diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c
index 38b2b1e8e1..f30fe6b492 100644
--- a/src/test/regress/pg_regress.c
+++ b/src/test/regress/pg_regress.c
@@ -2335,9 +2335,12 @@ regression_main(int argc, char *argv[], init_function ifunc, test_function tfunc
 		 * Adjust the default postgresql.conf for regression testing. The user
 		 * can specify a file to be appended; in any case we expand logging
 		 * and set max_prepared_transactions to enable testing of prepared
-		 * xacts.  (Note: to reduce the probability of unexpected shmmax
-		 * failures, don't set max_prepared_transactions any higher than
-		 * actually needed by the prepared_xacts regression test.)
+		 * xacts.  We also set max_prepared_foreign_transactions and
+		 * max_foreign_transaction_resolvers to enable testing of transaction
+		 * involving multiple foreign servers. (Note: to reduce the probability
+		 * of unexpected shmmax failures, don't set max_prepared_transactions
+		 * any higher than actually needed by the prepared_xacts regression
+		 * test.)
 		 */
 		snprintf(buf, sizeof(buf), "%s/data/postgresql.conf", temp_instance);
 		pg_conf = fopen(buf, "a");
@@ -2352,7 +2355,9 @@ regression_main(int argc, char *argv[], init_function ifunc, test_function tfunc
 		fputs("log_line_prefix = '%m %b[%p] %q%a '\n", pg_conf);
 		fputs("log_lock_waits = on\n", pg_conf);
 		fputs("log_temp_files = 128kB\n", pg_conf);
-		fputs("max_prepared_transactions = 2\n", pg_conf);
+		fputs("max_prepared_transactions = 3\n", pg_conf);
+		fputs("max_prepared_foreign_transactions = 2\n", pg_conf);
+		fputs("max_foreign_transaction_resolvers = 2\n", pg_conf);
 
 		for (sl = temp_configs; sl != NULL; sl = sl->next)
 		{
-- 
2.23.0

