From 5cba2022027a0bb70af1579b2bbe73b26804509a Mon Sep 17 00:00:00 2001 From: Masahiko Sawada Date: Thu, 26 Mar 2020 21:41:29 +0500 Subject: [PATCH v32 11/11] Add regression tests for foreign twophase commit. Co-authored-by: Masahiko Sawada, Ashutosh Bapat --- 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 | 200 +++++++ src/test/modules/test_fdwxact/fdwxact.conf | 7 + .../modules/test_fdwxact/sql/test_fdwxact.sql | 185 +++++++ src/test/modules/test_fdwxact/t/001_basic.pl | 110 ++++ .../test_fdwxact/test_fdwxact--1.0.sql | 44 ++ src/test/modules/test_fdwxact/test_fdwxact.c | 524 ++++++++++++++++++ .../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 +- src/tools/msvc/Mkvcbuild.pm | 3 +- 14 files changed, 1294 insertions(+), 6 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 59921b46cf..45ddcdcb0a 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -15,6 +15,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..ca8a90f3e5 --- /dev/null +++ b/src/test/modules/test_fdwxact/expected/test_fdwxact.out @@ -0,0 +1,200 @@ +-- +-- Test for foreign transaction management. +-- +CREATE EXTENSION test_fdwxact; +-- setup one server that don't support transaction management API +CREATE SERVER srv_1 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_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_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; +-- function to wait for counters to advance +CREATE PROCEDURE wait_for_resolution(expected int) AS $$ +DECLARE + start_time timestamptz := clock_timestamp(); + resolved bool; +BEGIN + -- we don't want to wait forever; loop will exit after 30 seconds + FOR i IN 1 .. 300 LOOP + + -- check to see if all updates have been reset/updated + SELECT count(*) = expected INTO resolved FROM pg_foreign_xacts; + + exit WHEN resolved; + + -- wait a little + perform pg_sleep_for('100 milliseconds'); + + END LOOP; + + -- report time waited in postmaster log (where it won't change test output) + RAISE LOG 'wait_for_resolution delayed % seconds', + extract(epoch from clock_timestamp() - start_time); +END +$$ LANGUAGE plpgsql; +-- Test 'disabled' mode. +-- 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_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' mode. +-- 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_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; +-- Ok. only ft_2pc_1 is committed in one-phase. +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: cannot process a distributed transaction that has operated on a foreign server that does not support two-phase commit protocol +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 that does not support two-phase commit protocol +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 that does not support two-phase commit protocol +DETAIL: foreign_twophase_commit is 'required' but the transaction has some foreign servers which are not capable of two-phase commit +-- Tests for PREPARE. +-- Prepare two transactions: local and foreign. +BEGIN; +INSERT INTO ft_2pc_1 VALUES(1); +INSERT INTO t VALUES(3); +PREPARE TRANSACTION 'global_x1'; +SELECT count(*) FROM pg_foreign_xacts; + count +------- + 1 +(1 row) + +COMMIT PREPARED 'global_x1'; +CALL wait_for_resolution(0); +-- Even if the transaction modified only one foreign server, +-- we prepare foreign transaction. +BEGIN; +INSERT INTO ft_2pc_1 VALUES(1); +PREPARE TRANSACTION 'global_x1'; +SELECT count(*) FROM pg_foreign_xacts; + count +------- + 1 +(1 row) + +ROLLBACK PREPARED 'global_x1'; +CALL wait_for_resolution(0); +-- Error. PREPARE needs all involved foreign servers to +-- support two-phsae commit. +BEGIN; +INSERT INTO ft_no2pc_1 VALUES (1); +PREPARE TRANSACTION 'global_x1'; +ERROR: cannot PREPARE a distributed transaction which has operated on a foreign server not supporting two-phase commit protocol 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..40b774e5d0 --- /dev/null +++ b/src/test/modules/test_fdwxact/sql/test_fdwxact.sql @@ -0,0 +1,185 @@ +-- +-- Test for foreign transaction management. +-- + +CREATE EXTENSION test_fdwxact; + +-- setup one server that don't support transaction management API +CREATE SERVER srv_1 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_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_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; + +-- function to wait for counters to advance +CREATE PROCEDURE wait_for_resolution(expected int) AS $$ +DECLARE + start_time timestamptz := clock_timestamp(); + resolved bool; +BEGIN + -- we don't want to wait forever; loop will exit after 30 seconds + FOR i IN 1 .. 300 LOOP + + -- check to see if all updates have been reset/updated + SELECT count(*) = expected INTO resolved FROM pg_foreign_xacts; + + exit WHEN resolved; + + -- wait a little + perform pg_sleep_for('100 milliseconds'); + + END LOOP; + + -- report time waited in postmaster log (where it won't change test output) + RAISE LOG 'wait_for_resolution delayed % seconds', + extract(epoch from clock_timestamp() - start_time); +END +$$ LANGUAGE plpgsql; + +-- Test 'disabled' mode. +-- 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_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' mode. +-- 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_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; + +-- Ok. only ft_2pc_1 is committed in one-phase. +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_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; + + +-- Tests for PREPARE. +-- Prepare two transactions: local and foreign. +BEGIN; +INSERT INTO ft_2pc_1 VALUES(1); +INSERT INTO t VALUES(3); +PREPARE TRANSACTION 'global_x1'; +SELECT count(*) FROM pg_foreign_xacts; +COMMIT PREPARED 'global_x1'; +CALL wait_for_resolution(0); + +-- Even if the transaction modified only one foreign server, +-- we prepare foreign transaction. +BEGIN; +INSERT INTO ft_2pc_1 VALUES(1); +PREPARE TRANSACTION 'global_x1'; +SELECT count(*) FROM pg_foreign_xacts; +ROLLBACK PREPARED 'global_x1'; +CALL wait_for_resolution(0); + +-- Error. PREPARE needs all involved foreign servers to +-- support two-phsae commit. +BEGIN; +INSERT INTO ft_no2pc_1 VALUES (1); +PREPARE TRANSACTION 'global_x1'; 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..52e4971aed --- /dev/null +++ b/src/test/modules/test_fdwxact/t/001_basic.pl @@ -0,0 +1,110 @@ +use strict; +use warnings; +use PostgresNode; +use TestLib; +use Test::More tests => 7; + +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, $endsql, $wait_until) = @_; + + $endsql = 'COMMIT' unless defined $endsql; + $wait_until = 0 unless defined $wait_until; + + 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 + $endsql; + "); + $node->poll_query_until('postgres', + "SELECT count(*) FROM pg_foreign_xacts", + $wait_until); + + 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"); + +# Test the failure case of PREPARE TRANSACTION. We prepare the distributed +# transaction with the same identifer. The second attempt will fail when preparing +# the local transaction, which is performed after preparing the foreign transaction +# on srv_2pc_1. Therefore the transaction should rollback the prepared foreign +# transaction. +($log, $xid) = run_transaction($node, "", + "INSERT INTO t VALUES(1); + INSERT INTO ft_2pc_1 VALUES(1);", + "PREPARE TRANSACTION 'tx1'", 1); +($log, $xid) = run_transaction($node, "", + "INSERT INTO t VALUES(1); + INSERT INTO ft_2pc_1 VALUES(1);", + "PREPARE TRANSACTION 'tx1'", 1); +like($log, qr/rollback prepared tx_$xid on srv_2pc_1/, "failure after prepare transaction"); +$node->safe_psql('postgres', "COMMIT PREPARED 'tx1'"); + +# 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"); 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..8e2a57b052 --- /dev/null +++ b/src/test/modules/test_fdwxact/test_fdwxact.c @@ -0,0 +1,524 @@ +/*------------------------------------------------------------------------- + * + * 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 "access/xact.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" +#include "utils/guc.h" +#include "utils/lsyscache.h" +#include "utils/rel.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 void testBeginForeignModifyWithRegistration(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 testBeginForeignInsertWithRegistration(ModifyTableState *mtstate, + ResultRelInfo *resultRelInfo); +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 = testBeginForeignModifyWithRegistration; + routine->ExecForeignInsert = testExecForeignInsert; + routine->EndForeignModify = testEndForeignModify; + routine->BeginForeignInsert = testBeginForeignInsertWithRegistration; + 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 = testBeginForeignModifyWithRegistration; + routine->ExecForeignInsert = testExecForeignInsert; + routine->EndForeignModify = testEndForeignModify; + routine->BeginForeignInsert = testBeginForeignInsertWithRegistration; + 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; +} + +/* Register the foreign transaction */ +static void +testRegisterFdwXact(ModifyTableState *mtstate, + ResultRelInfo *resultRelInfo, + bool modified) +{ + Relation rel = resultRelInfo->ri_RelationDesc; + RangeTblEntry *rte; + ForeignTable *table; + Oid userid; + + rte = exec_rt_fetch(resultRelInfo->ri_RangeTableIndex, + mtstate->ps.state); + userid = rte->checkAsUser ? rte->checkAsUser : GetUserId(); + table = GetForeignTable(RelationGetRelid(rel)); + FdwXactRegisterXact(table->serverid, userid, modified); +} + + +static void +testBeginForeignModify(ModifyTableState *mtstate, + ResultRelInfo *resultRelInfo, + List *fdw_private, + int subplan_index, + int eflags) +{ + return; +} + +static void +testBeginForeignModifyWithRegistration(ModifyTableState *mtstate, + ResultRelInfo *resultRelInfo, + List *fdw_private, + int subplan_index, + int eflags) +{ + testRegisterFdwXact(mtstate, resultRelInfo, + (eflags & EXEC_FLAG_EXPLAIN_ONLY) == 0); + 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 +testBeginForeignInsertWithRegistration(ModifyTableState *mtstate, + ResultRelInfo *resultRelInfo) +{ + testRegisterFdwXact(mtstate, resultRelInfo, true); + 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; + TransactionId xid = GetTopTransactionIdIfAny(); + + 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", + xid, state->server->servername))); + else + ereport(LOG, (errmsg("commit prepared %s on %s", + state->fdwxact_id, + state->server->servername))); + } +} + +static void +testRollbackForeignTransaction(FdwXactRslvState *state) +{ + TransactionId xid = GetTopTransactionIdIfAny(); + + if (log_api_calls) + { + if (state->flags && FDWXACT_FLAG_ONEPHASE) + ereport(LOG, (errmsg("rollback %u on %s", + 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 (pg_strcasecmp(fxss->server, servername) != 0 || + pg_strcasecmp(fxss->phase, phase) != 0) + { + LWLockRelease(fxss->lock); + return false; + } + + /* Currently support only error and panic */ + if (pg_strcasecmp(fxss->elevel, "error") == 0) + *elevel = ERROR; + if (pg_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 96442ceb4e..0e5e05e41a 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 b284cc88c4..5ceba8972a 100644 --- a/src/test/regress/pg_regress.c +++ b/src/test/regress/pg_regress.c @@ -2350,9 +2350,12 @@ regression_main(int argc, char *argv[], * 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"); @@ -2367,7 +2370,9 @@ regression_main(int argc, char *argv[], 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) { diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm index 5634b2d40c..3c48fbb2d9 100644 --- a/src/tools/msvc/Mkvcbuild.pm +++ b/src/tools/msvc/Mkvcbuild.pm @@ -50,7 +50,8 @@ my @contrib_excludes = ( 'pgcrypto', 'sepgsql', 'brin', 'test_extensions', 'test_misc', 'test_pg_dump', - 'snapshot_too_old', 'unsafe_tests'); + 'snapshot_too_old', 'unsafe_tests', + 'test_fdwxact'); # Set of variables for frontend modules my $frontend_defines = { 'initdb' => 'FRONTEND' }; -- 2.27.0