mirror of https://github.com/postgres/postgres
Previosly we simply set the LSN for the new key to the first write location. This is however not correct, as there are many corner cases around this: * recovery / replication might write old LSNs * we can't handle multiple keys with the same TLI/LSN, which can happen with quick restarts without writes To support this in this commit we modify the following: * We only activate new keys outside crash recovery, or immediately if encryption is turned off * We also take the already existing last key into account (if exists), and only activate a new key if we progressed past its start location The remaining changes are just support infrastructure for this: * Since we might rewrite old records, we use the already existing keys for those writes, not the active last keys * We prefetch existing keys during initialization, so it doesn't accidentally happen in the critical section during a write There is a remaining bug with stopping wal encryption, also mentioned in a TODO message in the code. This will be addressed in a later PR as this fix already took too long.pull/238/head
parent
c7e7dc52a7
commit
9dfed22f84
@ -0,0 +1,637 @@ |
|||||||
|
|
||||||
|
# Copyright (c) 2021-2024, PostgreSQL Global Development Group |
||||||
|
|
||||||
|
# Tests dedicated to two-phase commit in recovery |
||||||
|
use strict; |
||||||
|
use warnings FATAL => 'all'; |
||||||
|
|
||||||
|
use PostgreSQL::Test::Cluster; |
||||||
|
use PostgreSQL::Test::Utils; |
||||||
|
use Test::More; |
||||||
|
|
||||||
|
my $psql_out = ''; |
||||||
|
my $psql_rc = ''; |
||||||
|
|
||||||
|
sub configure_and_reload |
||||||
|
{ |
||||||
|
local $Test::Builder::Level = $Test::Builder::Level + 1; |
||||||
|
|
||||||
|
my ($node, $parameter) = @_; |
||||||
|
my $name = $node->name; |
||||||
|
|
||||||
|
$node->append_conf( |
||||||
|
'postgresql.conf', qq( |
||||||
|
$parameter |
||||||
|
)); |
||||||
|
$node->psql('postgres', "SELECT pg_reload_conf()", stdout => \$psql_out); |
||||||
|
is($psql_out, 't', "reload node $name with $parameter"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
# Set up two nodes, which will alternately be primary and replication standby. |
||||||
|
|
||||||
|
# Setup london node |
||||||
|
my $node_london = PostgreSQL::Test::Cluster->new("london"); |
||||||
|
$node_london->init(allows_streaming => 1); |
||||||
|
$node_london->append_conf( |
||||||
|
'postgresql.conf', qq( |
||||||
|
max_prepared_transactions = 10 |
||||||
|
log_checkpoints = true |
||||||
|
)); |
||||||
|
$node_london->append_conf('postgresql.conf', |
||||||
|
"shared_preload_libraries = 'pg_tde'"); |
||||||
|
$node_london->append_conf('postgresql.conf', |
||||||
|
"default_table_access_method = 'tde_heap'"); |
||||||
|
$node_london->start; |
||||||
|
|
||||||
|
# Create and enable tde extension |
||||||
|
$node_london->safe_psql('postgres', 'CREATE EXTENSION IF NOT EXISTS pg_tde;'); |
||||||
|
$node_london->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_add_global_key_provider_file('global_key_provider', '/tmp/pg_global_keyring.file');" |
||||||
|
); |
||||||
|
$node_london->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_create_key_using_global_key_provider('global_test_key', 'global_key_provider');" |
||||||
|
); |
||||||
|
$node_london->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_set_server_key_using_global_key_provider('global_test_key', 'global_key_provider');" |
||||||
|
); |
||||||
|
$node_london->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_add_database_key_provider_file('local_key_provider', '/tmp/pg_local_keyring.file');" |
||||||
|
); |
||||||
|
$node_london->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_create_key_using_database_key_provider('local_test_key', 'local_key_provider');" |
||||||
|
); |
||||||
|
$node_london->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_set_key_using_database_key_provider('local_test_key', 'local_key_provider');" |
||||||
|
); |
||||||
|
|
||||||
|
$node_london->append_conf( |
||||||
|
'postgresql.conf', qq( |
||||||
|
pg_tde.wal_encrypt = on |
||||||
|
log_min_messages = DEBUG2 |
||||||
|
)); |
||||||
|
|
||||||
|
$node_london->restart; |
||||||
|
|
||||||
|
# if ($WAL_ENCRYPTION eq 'on'){ |
||||||
|
# enable_wal_encryption($node_london); |
||||||
|
# } |
||||||
|
|
||||||
|
# $node_london->safe_psql('postgres', 'ALTER SYSTEM SET pg_tde.wal_encrypt = on;'); |
||||||
|
$node_london->restart; |
||||||
|
|
||||||
|
my $backup_dir = $node_london->backup_dir . '/london_backup'; |
||||||
|
mkdir $backup_dir or die "mkdir($backup_dir) failed: $!"; |
||||||
|
PostgreSQL::Test::RecursiveCopy::copypath($node_london->data_dir . '/pg_tde', |
||||||
|
$backup_dir . '/pg_tde'); |
||||||
|
|
||||||
|
$node_london->backup('london_backup'); |
||||||
|
|
||||||
|
# Setup paris node |
||||||
|
my $node_paris = PostgreSQL::Test::Cluster->new('paris'); |
||||||
|
$node_paris->init_from_backup($node_london, 'london_backup', |
||||||
|
has_streaming => 1); |
||||||
|
$node_paris->append_conf( |
||||||
|
'postgresql.conf', qq( |
||||||
|
subtransaction_buffers = 32 |
||||||
|
pg_tde.wal_encrypt = on |
||||||
|
)); |
||||||
|
$node_paris->start; |
||||||
|
|
||||||
|
# Switch to synchronous replication in both directions |
||||||
|
configure_and_reload($node_london, "synchronous_standby_names = 'paris'"); |
||||||
|
configure_and_reload($node_paris, "synchronous_standby_names = 'london'"); |
||||||
|
|
||||||
|
# Set up nonce names for current primary and standby nodes |
||||||
|
note "Initially, london is primary and paris is standby"; |
||||||
|
my ($cur_primary, $cur_standby) = ($node_london, $node_paris); |
||||||
|
my $cur_primary_name = $cur_primary->name; |
||||||
|
|
||||||
|
# Create table we'll use in the test transactions |
||||||
|
$cur_primary->psql('postgres', "CREATE TABLE t_009_tbl (id int, msg text)"); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check that we can commit and abort transaction after soft restart. |
||||||
|
# Here checkpoint happens before shutdown and no WAL replay will occur at next |
||||||
|
# startup. In this case postgres re-creates shared-memory state from twophase |
||||||
|
# files. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (1, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (2, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_1'; |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (3, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (4, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_2';"); |
||||||
|
$cur_primary->stop; |
||||||
|
$cur_primary->start; |
||||||
|
|
||||||
|
$psql_rc = $cur_primary->psql('postgres', "COMMIT PREPARED 'xact_009_1'"); |
||||||
|
is($psql_rc, '0', 'Commit prepared transaction after restart'); |
||||||
|
|
||||||
|
$psql_rc = $cur_primary->psql('postgres', "ROLLBACK PREPARED 'xact_009_2'"); |
||||||
|
is($psql_rc, '0', 'Rollback prepared transaction after restart'); |
||||||
|
|
||||||
|
is( $cur_primary->safe_psql( |
||||||
|
'postgres', "SELECT pg_tde_is_encrypted('t_009_tbl');"), |
||||||
|
't', |
||||||
|
"Table t_009_tbl is encrypted on primary"); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check that we can commit and abort after a hard restart. |
||||||
|
# At next startup, WAL replay will re-create shared memory state for prepared |
||||||
|
# transaction using dedicated WAL records. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
CHECKPOINT; |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (5, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (6, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_3'; |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (7, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (8, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_4';"); |
||||||
|
$cur_primary->teardown_node; |
||||||
|
$cur_primary->start; |
||||||
|
|
||||||
|
$psql_rc = $cur_primary->psql('postgres', "COMMIT PREPARED 'xact_009_3'"); |
||||||
|
is($psql_rc, '0', 'Commit prepared transaction after teardown'); |
||||||
|
|
||||||
|
$psql_rc = $cur_primary->psql('postgres', "ROLLBACK PREPARED 'xact_009_4'"); |
||||||
|
is($psql_rc, '0', 'Rollback prepared transaction after teardown'); |
||||||
|
|
||||||
|
is( $cur_primary->safe_psql( |
||||||
|
'postgres', "SELECT pg_tde_is_encrypted('t_009_tbl');"), |
||||||
|
't', |
||||||
|
"Table t_009_tbl is encrypted on primary"); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check that WAL replay can handle several transactions with same GID name. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
CHECKPOINT; |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (9, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (10, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_5'; |
||||||
|
COMMIT PREPARED 'xact_009_5'; |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (11, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (12, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_5';"); |
||||||
|
$cur_primary->teardown_node; |
||||||
|
$cur_primary->start; |
||||||
|
|
||||||
|
$psql_rc = $cur_primary->psql('postgres', "COMMIT PREPARED 'xact_009_5'"); |
||||||
|
is($psql_rc, '0', 'Replay several transactions with same GID'); |
||||||
|
|
||||||
|
is( $cur_primary->safe_psql( |
||||||
|
'postgres', "SELECT pg_tde_is_encrypted('t_009_tbl');"), |
||||||
|
't', |
||||||
|
"Table t_009_tbl is encrypted on primary"); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check that WAL replay cleans up its shared memory state and releases locks |
||||||
|
# while replaying transaction commits. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (13, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (14, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_6'; |
||||||
|
COMMIT PREPARED 'xact_009_6';"); |
||||||
|
$cur_primary->teardown_node; |
||||||
|
$cur_primary->start; |
||||||
|
$psql_rc = $cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (15, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (16, 'issued to ${cur_primary_name}'); |
||||||
|
-- This prepare can fail due to conflicting GID or locks conflicts if |
||||||
|
-- replay did not fully cleanup its state on previous commit. |
||||||
|
PREPARE TRANSACTION 'xact_009_7';"); |
||||||
|
is($psql_rc, '0', "Cleanup of shared memory state for 2PC commit"); |
||||||
|
|
||||||
|
$cur_primary->psql('postgres', "COMMIT PREPARED 'xact_009_7'"); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check that WAL replay will cleanup its shared memory state on running standby. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (17, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (18, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_8'; |
||||||
|
COMMIT PREPARED 'xact_009_8';"); |
||||||
|
$cur_standby->psql( |
||||||
|
'postgres', |
||||||
|
"SELECT count(*) FROM pg_prepared_xacts", |
||||||
|
stdout => \$psql_out); |
||||||
|
is($psql_out, '0', |
||||||
|
"Cleanup of shared memory state on running standby without checkpoint"); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Same as in previous case, but let's force checkpoint on standby between |
||||||
|
# prepare and commit to use on-disk twophase files. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (19, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (20, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_9';"); |
||||||
|
$cur_standby->psql('postgres', "CHECKPOINT"); |
||||||
|
$cur_primary->psql('postgres', "COMMIT PREPARED 'xact_009_9'"); |
||||||
|
$cur_standby->psql( |
||||||
|
'postgres', |
||||||
|
"SELECT count(*) FROM pg_prepared_xacts", |
||||||
|
stdout => \$psql_out); |
||||||
|
is($psql_out, '0', |
||||||
|
"Cleanup of shared memory state on running standby after checkpoint"); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check that prepared transactions can be committed on promoted standby. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (21, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (22, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_10';"); |
||||||
|
$cur_primary->teardown_node; |
||||||
|
$cur_standby->promote; |
||||||
|
|
||||||
|
# change roles |
||||||
|
note "Now paris is primary and london is standby"; |
||||||
|
($cur_primary, $cur_standby) = ($node_paris, $node_london); |
||||||
|
$cur_primary_name = $cur_primary->name; |
||||||
|
|
||||||
|
# because london is not running at this point, we can't use syncrep commit |
||||||
|
# on this command |
||||||
|
$psql_rc = $cur_primary->psql('postgres', |
||||||
|
"SET synchronous_commit = off; COMMIT PREPARED 'xact_009_10'"); |
||||||
|
is($psql_rc, '0', "Restore of prepared transaction on promoted standby"); |
||||||
|
|
||||||
|
# restart old primary as new standby |
||||||
|
$cur_standby->enable_streaming($cur_primary); |
||||||
|
$cur_standby->start; |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check that prepared transactions are replayed after soft restart of standby |
||||||
|
# while primary is down. Since standby knows that primary is down it uses a |
||||||
|
# different code path on startup to ensure that the status of transactions is |
||||||
|
# consistent. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (23, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (24, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_11';"); |
||||||
|
$cur_primary->stop; |
||||||
|
$cur_standby->restart; |
||||||
|
$cur_standby->promote; |
||||||
|
|
||||||
|
# change roles |
||||||
|
note "Now london is primary and paris is standby"; |
||||||
|
($cur_primary, $cur_standby) = ($node_london, $node_paris); |
||||||
|
$cur_primary_name = $cur_primary->name; |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', |
||||||
|
"SELECT count(*) FROM pg_prepared_xacts", |
||||||
|
stdout => \$psql_out); |
||||||
|
is($psql_out, '1', |
||||||
|
"Restore prepared transactions from files with primary down"); |
||||||
|
|
||||||
|
# restart old primary as new standby |
||||||
|
$cur_standby->enable_streaming($cur_primary); |
||||||
|
$cur_standby->start; |
||||||
|
|
||||||
|
$cur_primary->psql('postgres', "COMMIT PREPARED 'xact_009_11'"); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check that prepared transactions are correctly replayed after standby hard |
||||||
|
# restart while primary is down. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl VALUES (25, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl VALUES (26, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_12'; |
||||||
|
"); |
||||||
|
$cur_primary->stop; |
||||||
|
$cur_standby->teardown_node; |
||||||
|
$cur_standby->start; |
||||||
|
$cur_standby->promote; |
||||||
|
|
||||||
|
# change roles |
||||||
|
note "Now paris is primary and london is standby"; |
||||||
|
($cur_primary, $cur_standby) = ($node_paris, $node_london); |
||||||
|
$cur_primary_name = $cur_primary->name; |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', |
||||||
|
"SELECT count(*) FROM pg_prepared_xacts", |
||||||
|
stdout => \$psql_out); |
||||||
|
is($psql_out, '1', |
||||||
|
"Restore prepared transactions from records with primary down"); |
||||||
|
|
||||||
|
# restart old primary as new standby |
||||||
|
$cur_standby->enable_streaming($cur_primary); |
||||||
|
$cur_standby->start; |
||||||
|
|
||||||
|
$cur_primary->psql('postgres', "COMMIT PREPARED 'xact_009_12'"); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check visibility of prepared transactions in standby after a restart while |
||||||
|
# primary is down. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
SET synchronous_commit='remote_apply'; -- To ensure the standby is caught up |
||||||
|
CREATE TABLE t_009_tbl_standby_mvcc (id int, msg text); |
||||||
|
BEGIN; |
||||||
|
INSERT INTO t_009_tbl_standby_mvcc VALUES (1, 'issued to ${cur_primary_name}'); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl_standby_mvcc VALUES (2, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_standby_mvcc'; |
||||||
|
"); |
||||||
|
$cur_primary->stop; |
||||||
|
$cur_standby->restart; |
||||||
|
|
||||||
|
# Acquire a snapshot in standby, before we commit the prepared transaction |
||||||
|
my $standby_session = |
||||||
|
$cur_standby->background_psql('postgres', on_error_die => 1); |
||||||
|
$standby_session->query_safe("BEGIN ISOLATION LEVEL REPEATABLE READ"); |
||||||
|
$psql_out = |
||||||
|
$standby_session->query_safe("SELECT count(*) FROM t_009_tbl_standby_mvcc"); |
||||||
|
is($psql_out, '0', |
||||||
|
"Prepared transaction not visible in standby before commit"); |
||||||
|
|
||||||
|
# Commit the transaction in primary |
||||||
|
$cur_primary->start; |
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
SET synchronous_commit='remote_apply'; -- To ensure the standby is caught up |
||||||
|
COMMIT PREPARED 'xact_009_standby_mvcc'; |
||||||
|
"); |
||||||
|
|
||||||
|
# Still not visible to the old snapshot |
||||||
|
$psql_out = |
||||||
|
$standby_session->query_safe("SELECT count(*) FROM t_009_tbl_standby_mvcc"); |
||||||
|
is($psql_out, '0', |
||||||
|
"Committed prepared transaction not visible to old snapshot in standby"); |
||||||
|
|
||||||
|
# Is visible to a new snapshot |
||||||
|
$standby_session->query_safe("COMMIT"); |
||||||
|
$psql_out = |
||||||
|
$standby_session->query_safe("SELECT count(*) FROM t_009_tbl_standby_mvcc"); |
||||||
|
is($psql_out, '2', |
||||||
|
"Committed prepared transaction is visible to new snapshot in standby"); |
||||||
|
$standby_session->quit; |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check for a lock conflict between prepared transaction with DDL inside and |
||||||
|
# replay of XLOG_STANDBY_LOCK wal record. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
BEGIN; |
||||||
|
CREATE TABLE t_009_tbl2 (id int, msg text); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl2 VALUES (27, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_13'; |
||||||
|
-- checkpoint will issue XLOG_STANDBY_LOCK that can conflict with lock |
||||||
|
-- held by 'create table' statement |
||||||
|
CHECKPOINT; |
||||||
|
COMMIT PREPARED 'xact_009_13';"); |
||||||
|
|
||||||
|
# Ensure that last transaction is replayed on standby. |
||||||
|
my $cur_primary_lsn = |
||||||
|
$cur_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); |
||||||
|
my $caughtup_query = |
||||||
|
"SELECT '$cur_primary_lsn'::pg_lsn <= pg_last_wal_replay_lsn()"; |
||||||
|
$cur_standby->poll_query_until('postgres', $caughtup_query) |
||||||
|
or die "Timed out while waiting for standby to catch up"; |
||||||
|
|
||||||
|
$cur_standby->psql( |
||||||
|
'postgres', |
||||||
|
"SELECT count(*) FROM t_009_tbl2", |
||||||
|
stdout => \$psql_out); |
||||||
|
is($psql_out, '1', "Replay prepared transaction with DDL"); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check recovery of prepared transaction with DDL inside after a hard restart |
||||||
|
# of the primary. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
BEGIN; |
||||||
|
CREATE TABLE t_009_tbl3 (id int, msg text); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl3 VALUES (28, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_14'; |
||||||
|
BEGIN; |
||||||
|
CREATE TABLE t_009_tbl4 (id int, msg text); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl4 VALUES (29, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_15';"); |
||||||
|
|
||||||
|
$cur_primary->teardown_node; |
||||||
|
$cur_primary->start; |
||||||
|
|
||||||
|
$psql_rc = $cur_primary->psql('postgres', "COMMIT PREPARED 'xact_009_14'"); |
||||||
|
is($psql_rc, '0', 'Commit prepared transaction after teardown'); |
||||||
|
|
||||||
|
$psql_rc = $cur_primary->psql('postgres', "ROLLBACK PREPARED 'xact_009_15'"); |
||||||
|
is($psql_rc, '0', 'Rollback prepared transaction after teardown'); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Check recovery of prepared transaction with DDL inside after a soft restart |
||||||
|
# of the primary. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', " |
||||||
|
BEGIN; |
||||||
|
CREATE TABLE t_009_tbl5 (id int, msg text); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl5 VALUES (30, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_16'; |
||||||
|
BEGIN; |
||||||
|
CREATE TABLE t_009_tbl6 (id int, msg text); |
||||||
|
SAVEPOINT s1; |
||||||
|
INSERT INTO t_009_tbl6 VALUES (31, 'issued to ${cur_primary_name}'); |
||||||
|
PREPARE TRANSACTION 'xact_009_17';"); |
||||||
|
|
||||||
|
$cur_primary->stop; |
||||||
|
$cur_primary->start; |
||||||
|
|
||||||
|
$psql_rc = $cur_primary->psql('postgres', "COMMIT PREPARED 'xact_009_16'"); |
||||||
|
is($psql_rc, '0', 'Commit prepared transaction after restart'); |
||||||
|
|
||||||
|
$psql_rc = $cur_primary->psql('postgres', "ROLLBACK PREPARED 'xact_009_17'"); |
||||||
|
is($psql_rc, '0', 'Rollback prepared transaction after restart'); |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Verify expected data appears on both servers. |
||||||
|
############################################################################### |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', |
||||||
|
"SELECT count(*) FROM pg_prepared_xacts", |
||||||
|
stdout => \$psql_out); |
||||||
|
is($psql_out, '0', "No uncommitted prepared transactions on primary"); |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', |
||||||
|
"SELECT * FROM t_009_tbl ORDER BY id", |
||||||
|
stdout => \$psql_out); |
||||||
|
is( $psql_out, qq{1|issued to london |
||||||
|
2|issued to london |
||||||
|
5|issued to london |
||||||
|
6|issued to london |
||||||
|
9|issued to london |
||||||
|
10|issued to london |
||||||
|
11|issued to london |
||||||
|
12|issued to london |
||||||
|
13|issued to london |
||||||
|
14|issued to london |
||||||
|
15|issued to london |
||||||
|
16|issued to london |
||||||
|
17|issued to london |
||||||
|
18|issued to london |
||||||
|
19|issued to london |
||||||
|
20|issued to london |
||||||
|
21|issued to london |
||||||
|
22|issued to london |
||||||
|
23|issued to paris |
||||||
|
24|issued to paris |
||||||
|
25|issued to london |
||||||
|
26|issued to london}, |
||||||
|
"Check expected t_009_tbl data on primary"); |
||||||
|
|
||||||
|
$cur_primary->psql( |
||||||
|
'postgres', |
||||||
|
"SELECT * FROM t_009_tbl2", |
||||||
|
stdout => \$psql_out); |
||||||
|
is( $psql_out, |
||||||
|
qq{27|issued to paris}, |
||||||
|
"Check expected t_009_tbl2 data on primary"); |
||||||
|
|
||||||
|
$cur_standby->psql( |
||||||
|
'postgres', |
||||||
|
"SELECT count(*) FROM pg_prepared_xacts", |
||||||
|
stdout => \$psql_out); |
||||||
|
is($psql_out, '0', "No uncommitted prepared transactions on standby"); |
||||||
|
|
||||||
|
$cur_standby->psql( |
||||||
|
'postgres', |
||||||
|
"SELECT * FROM t_009_tbl ORDER BY id", |
||||||
|
stdout => \$psql_out); |
||||||
|
is( $psql_out, qq{1|issued to london |
||||||
|
2|issued to london |
||||||
|
5|issued to london |
||||||
|
6|issued to london |
||||||
|
9|issued to london |
||||||
|
10|issued to london |
||||||
|
11|issued to london |
||||||
|
12|issued to london |
||||||
|
13|issued to london |
||||||
|
14|issued to london |
||||||
|
15|issued to london |
||||||
|
16|issued to london |
||||||
|
17|issued to london |
||||||
|
18|issued to london |
||||||
|
19|issued to london |
||||||
|
20|issued to london |
||||||
|
21|issued to london |
||||||
|
22|issued to london |
||||||
|
23|issued to paris |
||||||
|
24|issued to paris |
||||||
|
25|issued to london |
||||||
|
26|issued to london}, |
||||||
|
"Check expected t_009_tbl data on standby"); |
||||||
|
|
||||||
|
$cur_standby->psql( |
||||||
|
'postgres', |
||||||
|
"SELECT * FROM t_009_tbl2", |
||||||
|
stdout => \$psql_out); |
||||||
|
is( $psql_out, |
||||||
|
qq{27|issued to paris}, |
||||||
|
"Check expected t_009_tbl2 data on standby"); |
||||||
|
|
||||||
|
|
||||||
|
# Exercise the 2PC recovery code in StartupSUBTRANS, which is concerned with |
||||||
|
# ensuring that enough pg_subtrans pages exist on disk to cover the range of |
||||||
|
# prepared transactions at server start time. There's not much we can verify |
||||||
|
# directly, but let's at least get the code to run. |
||||||
|
$cur_standby->stop(); |
||||||
|
configure_and_reload($cur_primary, "synchronous_standby_names = ''"); |
||||||
|
|
||||||
|
$cur_primary->safe_psql('postgres', "CHECKPOINT"); |
||||||
|
|
||||||
|
my $start_lsn = |
||||||
|
$cur_primary->safe_psql('postgres', 'select pg_current_wal_insert_lsn()'); |
||||||
|
$cur_primary->safe_psql('postgres', |
||||||
|
"CREATE TABLE test(); BEGIN; CREATE TABLE test1(); PREPARE TRANSACTION 'foo';" |
||||||
|
); |
||||||
|
my $osubtrans = $cur_primary->safe_psql('postgres', |
||||||
|
"select 'pg_subtrans/'||f, s.size from pg_ls_dir('pg_subtrans') f, pg_stat_file('pg_subtrans/'||f) s" |
||||||
|
); |
||||||
|
$cur_primary->pgbench( |
||||||
|
'--no-vacuum --client=5 --transactions=1000', |
||||||
|
0, |
||||||
|
[], |
||||||
|
[], |
||||||
|
'pgbench run to cause pg_subtrans traffic', |
||||||
|
{ |
||||||
|
'009_twophase.pgb' => 'insert into test default values' |
||||||
|
}); |
||||||
|
# StartupSUBTRANS is exercised with a wide range of visible XIDs in this |
||||||
|
# stop/start sequence, because we left a prepared transaction open above. |
||||||
|
# Also, setting subtransaction_buffers to 32 above causes to switch SLRU |
||||||
|
# bank, for additional code coverage. |
||||||
|
$cur_primary->stop; |
||||||
|
$cur_primary->start; |
||||||
|
my $nsubtrans = $cur_primary->safe_psql('postgres', |
||||||
|
"select 'pg_subtrans/'||f, s.size from pg_ls_dir('pg_subtrans') f, pg_stat_file('pg_subtrans/'||f) s" |
||||||
|
); |
||||||
|
isnt($osubtrans, $nsubtrans, "contents of pg_subtrans/ have changed"); |
||||||
|
|
||||||
|
done_testing(); |
@ -0,0 +1,659 @@ |
|||||||
|
# Copyright (c) 2021-2024, PostgreSQL Global Development Group |
||||||
|
|
||||||
|
# Minimal test testing streaming replication |
||||||
|
use strict; |
||||||
|
use warnings FATAL => 'all'; |
||||||
|
use PostgreSQL::Test::Cluster; |
||||||
|
use PostgreSQL::Test::Utils; |
||||||
|
use Test::More; |
||||||
|
use lib 't'; |
||||||
|
use pgtde; |
||||||
|
|
||||||
|
# Initialize primary node |
||||||
|
my $node_primary = PostgreSQL::Test::Cluster->new('primary'); |
||||||
|
# A specific role is created to perform some tests related to replication, |
||||||
|
# and it needs proper authentication configuration. |
||||||
|
$node_primary->init( |
||||||
|
allows_streaming => 1, |
||||||
|
auth_extra => [ '--create-role', 'repl_role' ]); |
||||||
|
$node_primary->append_conf('postgresql.conf', |
||||||
|
"shared_preload_libraries = 'pg_tde'"); |
||||||
|
$node_primary->append_conf('postgresql.conf', |
||||||
|
"default_table_access_method = 'tde_heap'"); |
||||||
|
$node_primary->start; |
||||||
|
my $backup_name = 'my_backup'; |
||||||
|
|
||||||
|
unlink('/tmp/global_keyring.file'); |
||||||
|
unlink('/tmp/local_keyring.file'); |
||||||
|
# Create and enable tde extension |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
'CREATE EXTENSION IF NOT EXISTS pg_tde;'); |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_add_global_key_provider_file('global_key_provider', '/tmp/global_keyring.file');" |
||||||
|
); |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_create_key_using_global_key_provider('global_test_key_stream', 'global_key_provider');" |
||||||
|
); |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_set_server_key_using_global_key_provider('global_test_key_stream', 'global_key_provider');" |
||||||
|
); |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_add_database_key_provider_file('local_key_provider', '/tmp/local_keyring.file');" |
||||||
|
); |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_create_key_using_database_key_provider('local_test_key_stream', 'local_key_provider');" |
||||||
|
); |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
"SELECT pg_tde_set_key_using_database_key_provider('local_test_key_stream', 'local_key_provider');" |
||||||
|
); |
||||||
|
|
||||||
|
my $WAL_ENCRYPTION = $ENV{WAL_ENCRYPTION} // 'off'; |
||||||
|
|
||||||
|
if ($WAL_ENCRYPTION eq 'on') |
||||||
|
{ |
||||||
|
$node_primary->append_conf( |
||||||
|
'postgresql.conf', qq( |
||||||
|
pg_tde.wal_encrypt = on |
||||||
|
)); |
||||||
|
} |
||||||
|
|
||||||
|
$node_primary->restart; |
||||||
|
|
||||||
|
# Take backup |
||||||
|
PGTDE::backup($node_primary, $backup_name); |
||||||
|
|
||||||
|
# Create streaming standby linking to primary |
||||||
|
my $node_standby_1 = PostgreSQL::Test::Cluster->new('standby_1'); |
||||||
|
$node_standby_1->init_from_backup($node_primary, $backup_name, |
||||||
|
has_streaming => 1); |
||||||
|
$node_standby_1->start; |
||||||
|
|
||||||
|
# Take backup of standby 1 (not mandatory, but useful to check if |
||||||
|
# pg_basebackup works on a standby). |
||||||
|
PGTDE::backup($node_standby_1, $backup_name); |
||||||
|
|
||||||
|
# Take a second backup of the standby while the primary is offline. |
||||||
|
$node_primary->stop; |
||||||
|
PGTDE::backup($node_standby_1, 'my_backup_2'); |
||||||
|
$node_primary->start; |
||||||
|
|
||||||
|
# Create second standby node linking to standby 1 |
||||||
|
my $node_standby_2 = PostgreSQL::Test::Cluster->new('standby_2'); |
||||||
|
$node_standby_2->init_from_backup($node_standby_1, $backup_name, |
||||||
|
has_streaming => 1); |
||||||
|
$node_standby_2->start; |
||||||
|
|
||||||
|
# Create some content on primary and check its presence in standby nodes |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
"CREATE TABLE tab_int AS SELECT generate_series(1,1002) AS a"); |
||||||
|
|
||||||
|
$node_primary->safe_psql( |
||||||
|
'postgres', q{ |
||||||
|
CREATE TABLE user_logins(id serial, who text); |
||||||
|
|
||||||
|
CREATE FUNCTION on_login_proc() RETURNS EVENT_TRIGGER AS $$ |
||||||
|
BEGIN |
||||||
|
IF NOT pg_is_in_recovery() THEN |
||||||
|
INSERT INTO user_logins (who) VALUES (session_user); |
||||||
|
END IF; |
||||||
|
IF session_user = 'regress_hacker' THEN |
||||||
|
RAISE EXCEPTION 'You are not welcome!'; |
||||||
|
END IF; |
||||||
|
END; |
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER; |
||||||
|
|
||||||
|
CREATE EVENT TRIGGER on_login_trigger ON login EXECUTE FUNCTION on_login_proc(); |
||||||
|
ALTER EVENT TRIGGER on_login_trigger ENABLE ALWAYS; |
||||||
|
}); |
||||||
|
|
||||||
|
# Wait for standbys to catch up |
||||||
|
$node_primary->wait_for_replay_catchup($node_standby_1); |
||||||
|
$node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary); |
||||||
|
|
||||||
|
my $result = |
||||||
|
$node_standby_1->safe_psql('postgres', "SELECT count(*) FROM tab_int"); |
||||||
|
print "standby 1: $result\n"; |
||||||
|
is($result, qq(1002), 'check streamed content on standby 1'); |
||||||
|
|
||||||
|
$result = |
||||||
|
$node_standby_2->safe_psql('postgres', "SELECT count(*) FROM tab_int"); |
||||||
|
print "standby 2: $result\n"; |
||||||
|
is($result, qq(1002), 'check streamed content on standby 2'); |
||||||
|
|
||||||
|
# Likewise, but for a sequence |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
"CREATE SEQUENCE seq1; SELECT nextval('seq1')"); |
||||||
|
|
||||||
|
# Wait for standbys to catch up |
||||||
|
$node_primary->wait_for_replay_catchup($node_standby_1); |
||||||
|
$node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary); |
||||||
|
|
||||||
|
$result = $node_standby_1->safe_psql('postgres', "SELECT * FROM seq1"); |
||||||
|
print "standby 1: $result\n"; |
||||||
|
is($result, qq(33|0|t), 'check streamed sequence content on standby 1'); |
||||||
|
|
||||||
|
$result = $node_standby_2->safe_psql('postgres', "SELECT * FROM seq1"); |
||||||
|
print "standby 2: $result\n"; |
||||||
|
is($result, qq(33|0|t), 'check streamed sequence content on standby 2'); |
||||||
|
|
||||||
|
# Check pg_sequence_last_value() returns NULL for unlogged sequence on standby |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
"CREATE UNLOGGED SEQUENCE ulseq; SELECT nextval('ulseq')"); |
||||||
|
$node_primary->wait_for_replay_catchup($node_standby_1); |
||||||
|
is( $node_standby_1->safe_psql( |
||||||
|
'postgres', |
||||||
|
"SELECT pg_sequence_last_value('ulseq'::regclass) IS NULL"), |
||||||
|
't', |
||||||
|
'pg_sequence_last_value() on unlogged sequence on standby 1'); |
||||||
|
|
||||||
|
# Check that only READ-only queries can run on standbys |
||||||
|
is($node_standby_1->psql('postgres', 'INSERT INTO tab_int VALUES (1)'), |
||||||
|
3, 'read-only queries on standby 1'); |
||||||
|
is($node_standby_2->psql('postgres', 'INSERT INTO tab_int VALUES (1)'), |
||||||
|
3, 'read-only queries on standby 2'); |
||||||
|
|
||||||
|
# Tests for connection parameter target_session_attrs |
||||||
|
note "testing connection parameter \"target_session_attrs\""; |
||||||
|
|
||||||
|
# Attempt to connect to $node1, then $node2, using target_session_attrs=$mode. |
||||||
|
# Expect to connect to $target_node (undef for failure) with given $status. |
||||||
|
sub test_target_session_attrs |
||||||
|
{ |
||||||
|
local $Test::Builder::Level = $Test::Builder::Level + 1; |
||||||
|
|
||||||
|
my $node1 = shift; |
||||||
|
my $node2 = shift; |
||||||
|
my $target_node = shift; |
||||||
|
my $mode = shift; |
||||||
|
my $status = shift; |
||||||
|
|
||||||
|
my $node1_host = $node1->host; |
||||||
|
my $node1_port = $node1->port; |
||||||
|
my $node1_name = $node1->name; |
||||||
|
my $node2_host = $node2->host; |
||||||
|
my $node2_port = $node2->port; |
||||||
|
my $node2_name = $node2->name; |
||||||
|
my $target_port = undef; |
||||||
|
$target_port = $target_node->port if (defined $target_node); |
||||||
|
my $target_name = undef; |
||||||
|
$target_name = $target_node->name if (defined $target_node); |
||||||
|
|
||||||
|
# Build connection string for connection attempt. |
||||||
|
my $connstr = "host=$node1_host,$node2_host "; |
||||||
|
$connstr .= "port=$node1_port,$node2_port "; |
||||||
|
$connstr .= "target_session_attrs=$mode"; |
||||||
|
|
||||||
|
# Attempt to connect, and if successful, get the server port number |
||||||
|
# we connected to. Note we must pass the SQL command via the command |
||||||
|
# line not stdin, else Perl may spit up trying to write to stdin of |
||||||
|
# an already-failed psql process. |
||||||
|
my ($ret, $stdout, $stderr) = |
||||||
|
$node1->psql('postgres', undef, |
||||||
|
extra_params => [ '-d', $connstr, '-c', 'SHOW port;' ]); |
||||||
|
if ($status == 0) |
||||||
|
{ |
||||||
|
is( $status == $ret && $stdout eq $target_port, |
||||||
|
1, |
||||||
|
"connect to node $target_name with mode \"$mode\" and $node1_name,$node2_name listed" |
||||||
|
); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
print "status = $status\n"; |
||||||
|
print "ret = $ret\n"; |
||||||
|
print "stdout = $stdout\n"; |
||||||
|
print "stderr = $stderr\n"; |
||||||
|
|
||||||
|
is( $status == $ret && !defined $target_node, |
||||||
|
1, |
||||||
|
"fail to connect with mode \"$mode\" and $node1_name,$node2_name listed" |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
# Connect to primary in "read-write" mode with primary,standby1 list. |
||||||
|
test_target_session_attrs($node_primary, $node_standby_1, $node_primary, |
||||||
|
"read-write", 0); |
||||||
|
|
||||||
|
# Connect to primary in "read-write" mode with standby1,primary list. |
||||||
|
test_target_session_attrs($node_standby_1, $node_primary, $node_primary, |
||||||
|
"read-write", 0); |
||||||
|
|
||||||
|
# Connect to primary in "any" mode with primary,standby1 list. |
||||||
|
test_target_session_attrs($node_primary, $node_standby_1, $node_primary, |
||||||
|
"any", 0); |
||||||
|
|
||||||
|
# Connect to standby1 in "any" mode with standby1,primary list. |
||||||
|
test_target_session_attrs($node_standby_1, $node_primary, $node_standby_1, |
||||||
|
"any", 0); |
||||||
|
|
||||||
|
# Connect to primary in "primary" mode with primary,standby1 list. |
||||||
|
test_target_session_attrs($node_primary, $node_standby_1, $node_primary, |
||||||
|
"primary", 0); |
||||||
|
|
||||||
|
# Connect to primary in "primary" mode with standby1,primary list. |
||||||
|
test_target_session_attrs($node_standby_1, $node_primary, $node_primary, |
||||||
|
"primary", 0); |
||||||
|
|
||||||
|
# Connect to standby1 in "read-only" mode with primary,standby1 list. |
||||||
|
test_target_session_attrs($node_primary, $node_standby_1, $node_standby_1, |
||||||
|
"read-only", 0); |
||||||
|
|
||||||
|
# Connect to standby1 in "read-only" mode with standby1,primary list. |
||||||
|
test_target_session_attrs($node_standby_1, $node_primary, $node_standby_1, |
||||||
|
"read-only", 0); |
||||||
|
|
||||||
|
# Connect to primary in "prefer-standby" mode with primary,primary list. |
||||||
|
test_target_session_attrs($node_primary, $node_primary, $node_primary, |
||||||
|
"prefer-standby", 0); |
||||||
|
|
||||||
|
# Connect to standby1 in "prefer-standby" mode with primary,standby1 list. |
||||||
|
test_target_session_attrs($node_primary, $node_standby_1, $node_standby_1, |
||||||
|
"prefer-standby", 0); |
||||||
|
|
||||||
|
# Connect to standby1 in "prefer-standby" mode with standby1,primary list. |
||||||
|
test_target_session_attrs($node_standby_1, $node_primary, $node_standby_1, |
||||||
|
"prefer-standby", 0); |
||||||
|
|
||||||
|
# Connect to standby1 in "standby" mode with primary,standby1 list. |
||||||
|
test_target_session_attrs($node_primary, $node_standby_1, $node_standby_1, |
||||||
|
"standby", 0); |
||||||
|
|
||||||
|
# Connect to standby1 in "standby" mode with standby1,primary list. |
||||||
|
test_target_session_attrs($node_standby_1, $node_primary, $node_standby_1, |
||||||
|
"standby", 0); |
||||||
|
|
||||||
|
# Fail to connect in "read-write" mode with standby1,standby2 list. |
||||||
|
test_target_session_attrs($node_standby_1, $node_standby_2, undef, |
||||||
|
"read-write", 2); |
||||||
|
|
||||||
|
# Fail to connect in "primary" mode with standby1,standby2 list. |
||||||
|
test_target_session_attrs($node_standby_1, $node_standby_2, undef, |
||||||
|
"primary", 2); |
||||||
|
|
||||||
|
# Fail to connect in "read-only" mode with primary,primary list. |
||||||
|
test_target_session_attrs($node_primary, $node_primary, undef, |
||||||
|
"read-only", 2); |
||||||
|
|
||||||
|
# Fail to connect in "standby" mode with primary,primary list. |
||||||
|
test_target_session_attrs($node_primary, $node_primary, undef, "standby", 2); |
||||||
|
|
||||||
|
# Test for SHOW commands using a WAL sender connection with a replication |
||||||
|
# role. |
||||||
|
note "testing SHOW commands for replication connection"; |
||||||
|
|
||||||
|
$node_primary->psql( |
||||||
|
'postgres', " |
||||||
|
CREATE ROLE repl_role REPLICATION LOGIN; |
||||||
|
GRANT pg_read_all_settings TO repl_role;"); |
||||||
|
my $primary_host = $node_primary->host; |
||||||
|
my $primary_port = $node_primary->port; |
||||||
|
my $connstr_common = "host=$primary_host port=$primary_port user=repl_role"; |
||||||
|
my $connstr_rep = "$connstr_common replication=1"; |
||||||
|
my $connstr_db = "$connstr_common replication=database dbname=postgres"; |
||||||
|
|
||||||
|
# Test SHOW ALL |
||||||
|
my ($ret, $stdout, $stderr) = $node_primary->psql( |
||||||
|
'postgres', 'SHOW ALL;', |
||||||
|
on_error_die => 1, |
||||||
|
extra_params => [ '-d', $connstr_rep ]); |
||||||
|
ok($ret == 0, "SHOW ALL with replication role and physical replication"); |
||||||
|
($ret, $stdout, $stderr) = $node_primary->psql( |
||||||
|
'postgres', 'SHOW ALL;', |
||||||
|
on_error_die => 1, |
||||||
|
extra_params => [ '-d', $connstr_db ]); |
||||||
|
ok($ret == 0, "SHOW ALL with replication role and logical replication"); |
||||||
|
|
||||||
|
# Test SHOW with a user-settable parameter |
||||||
|
($ret, $stdout, $stderr) = $node_primary->psql( |
||||||
|
'postgres', 'SHOW work_mem;', |
||||||
|
on_error_die => 1, |
||||||
|
extra_params => [ '-d', $connstr_rep ]); |
||||||
|
ok( $ret == 0, |
||||||
|
"SHOW with user-settable parameter, replication role and physical replication" |
||||||
|
); |
||||||
|
($ret, $stdout, $stderr) = $node_primary->psql( |
||||||
|
'postgres', 'SHOW work_mem;', |
||||||
|
on_error_die => 1, |
||||||
|
extra_params => [ '-d', $connstr_db ]); |
||||||
|
ok( $ret == 0, |
||||||
|
"SHOW with user-settable parameter, replication role and logical replication" |
||||||
|
); |
||||||
|
|
||||||
|
# Test SHOW with a superuser-settable parameter |
||||||
|
($ret, $stdout, $stderr) = $node_primary->psql( |
||||||
|
'postgres', 'SHOW primary_conninfo;', |
||||||
|
on_error_die => 1, |
||||||
|
extra_params => [ '-d', $connstr_rep ]); |
||||||
|
ok( $ret == 0, |
||||||
|
"SHOW with superuser-settable parameter, replication role and physical replication" |
||||||
|
); |
||||||
|
($ret, $stdout, $stderr) = $node_primary->psql( |
||||||
|
'postgres', 'SHOW primary_conninfo;', |
||||||
|
on_error_die => 1, |
||||||
|
extra_params => [ '-d', $connstr_db ]); |
||||||
|
ok( $ret == 0, |
||||||
|
"SHOW with superuser-settable parameter, replication role and logical replication" |
||||||
|
); |
||||||
|
|
||||||
|
note "testing READ_REPLICATION_SLOT command for replication connection"; |
||||||
|
|
||||||
|
my $slotname = 'test_read_replication_slot_physical'; |
||||||
|
|
||||||
|
($ret, $stdout, $stderr) = $node_primary->psql( |
||||||
|
'postgres', |
||||||
|
'READ_REPLICATION_SLOT non_existent_slot;', |
||||||
|
extra_params => [ '-d', $connstr_rep ]); |
||||||
|
ok($ret == 0, "READ_REPLICATION_SLOT exit code 0 on success"); |
||||||
|
like($stdout, qr/^\|\|$/, |
||||||
|
"READ_REPLICATION_SLOT returns NULL values if slot does not exist"); |
||||||
|
|
||||||
|
$node_primary->psql( |
||||||
|
'postgres', |
||||||
|
"CREATE_REPLICATION_SLOT $slotname PHYSICAL RESERVE_WAL;", |
||||||
|
extra_params => [ '-d', $connstr_rep ]); |
||||||
|
|
||||||
|
($ret, $stdout, $stderr) = $node_primary->psql( |
||||||
|
'postgres', |
||||||
|
"READ_REPLICATION_SLOT $slotname;", |
||||||
|
extra_params => [ '-d', $connstr_rep ]); |
||||||
|
ok($ret == 0, "READ_REPLICATION_SLOT success with existing slot"); |
||||||
|
like($stdout, qr/^physical\|[^|]*\|1$/, |
||||||
|
"READ_REPLICATION_SLOT returns tuple with slot information"); |
||||||
|
|
||||||
|
$node_primary->psql( |
||||||
|
'postgres', |
||||||
|
"DROP_REPLICATION_SLOT $slotname;", |
||||||
|
extra_params => [ '-d', $connstr_rep ]); |
||||||
|
|
||||||
|
note "switching to physical replication slot"; |
||||||
|
|
||||||
|
# Switch to using a physical replication slot. We can do this without a new |
||||||
|
# backup since physical slots can go backwards if needed. Do so on both |
||||||
|
# standbys. Since we're going to be testing things that affect the slot state, |
||||||
|
# also increase the standby feedback interval to ensure timely updates. |
||||||
|
my ($slotname_1, $slotname_2) = ('standby_1', 'standby_2'); |
||||||
|
$node_primary->append_conf('postgresql.conf', "max_replication_slots = 4"); |
||||||
|
$node_primary->restart; |
||||||
|
is( $node_primary->psql( |
||||||
|
'postgres', |
||||||
|
qq[SELECT pg_create_physical_replication_slot('$slotname_1');]), |
||||||
|
0, |
||||||
|
'physical slot created on primary'); |
||||||
|
$node_standby_1->append_conf('postgresql.conf', |
||||||
|
"primary_slot_name = $slotname_1"); |
||||||
|
$node_standby_1->append_conf('postgresql.conf', |
||||||
|
"wal_receiver_status_interval = 1"); |
||||||
|
$node_standby_1->append_conf('postgresql.conf', "max_replication_slots = 4"); |
||||||
|
$node_standby_1->restart; |
||||||
|
is( $node_standby_1->psql( |
||||||
|
'postgres', |
||||||
|
qq[SELECT pg_create_physical_replication_slot('$slotname_2');]), |
||||||
|
0, |
||||||
|
'physical slot created on intermediate replica'); |
||||||
|
$node_standby_2->append_conf('postgresql.conf', |
||||||
|
"primary_slot_name = $slotname_2"); |
||||||
|
$node_standby_2->append_conf('postgresql.conf', |
||||||
|
"wal_receiver_status_interval = 1"); |
||||||
|
# should be able change primary_slot_name without restart |
||||||
|
# will wait effect in get_slot_xmins above |
||||||
|
$node_standby_2->reload; |
||||||
|
|
||||||
|
# Fetch xmin columns from slot's pg_replication_slots row, after waiting for |
||||||
|
# given boolean condition to be true to ensure we've reached a quiescent state |
||||||
|
sub get_slot_xmins |
||||||
|
{ |
||||||
|
my ($node, $slotname, $check_expr) = @_; |
||||||
|
|
||||||
|
$node->poll_query_until( |
||||||
|
'postgres', qq[ |
||||||
|
SELECT $check_expr |
||||||
|
FROM pg_catalog.pg_replication_slots |
||||||
|
WHERE slot_name = '$slotname'; |
||||||
|
]) or die "Timed out waiting for slot xmins to advance"; |
||||||
|
|
||||||
|
my $slotinfo = $node->slot($slotname); |
||||||
|
return ($slotinfo->{'xmin'}, $slotinfo->{'catalog_xmin'}); |
||||||
|
} |
||||||
|
|
||||||
|
# There's no hot standby feedback and there are no logical slots on either peer |
||||||
|
# so xmin and catalog_xmin should be null on both slots. |
||||||
|
my ($xmin, $catalog_xmin) = get_slot_xmins($node_primary, $slotname_1, |
||||||
|
"xmin IS NULL AND catalog_xmin IS NULL"); |
||||||
|
is($xmin, '', 'xmin of non-cascaded slot null with no hs_feedback'); |
||||||
|
is($catalog_xmin, '', |
||||||
|
'catalog xmin of non-cascaded slot null with no hs_feedback'); |
||||||
|
|
||||||
|
($xmin, $catalog_xmin) = get_slot_xmins($node_standby_1, $slotname_2, |
||||||
|
"xmin IS NULL AND catalog_xmin IS NULL"); |
||||||
|
is($xmin, '', 'xmin of cascaded slot null with no hs_feedback'); |
||||||
|
is($catalog_xmin, '', |
||||||
|
'catalog xmin of cascaded slot null with no hs_feedback'); |
||||||
|
|
||||||
|
# Replication still works? |
||||||
|
$node_primary->safe_psql('postgres', 'CREATE TABLE replayed(val integer);'); |
||||||
|
|
||||||
|
sub replay_check |
||||||
|
{ |
||||||
|
my $newval = $node_primary->safe_psql('postgres', |
||||||
|
'INSERT INTO replayed(val) SELECT coalesce(max(val),0) + 1 AS newval FROM replayed RETURNING val' |
||||||
|
); |
||||||
|
$node_primary->wait_for_replay_catchup($node_standby_1); |
||||||
|
$node_standby_1->wait_for_replay_catchup($node_standby_2, $node_primary); |
||||||
|
|
||||||
|
$node_standby_1->safe_psql('postgres', |
||||||
|
qq[SELECT 1 FROM replayed WHERE val = $newval]) |
||||||
|
or die "standby_1 didn't replay primary value $newval"; |
||||||
|
$node_standby_2->safe_psql('postgres', |
||||||
|
qq[SELECT 1 FROM replayed WHERE val = $newval]) |
||||||
|
or die "standby_2 didn't replay standby_1 value $newval"; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
replay_check(); |
||||||
|
|
||||||
|
my $evttrig = $node_standby_1->safe_psql('postgres', |
||||||
|
"SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'"); |
||||||
|
is($evttrig, 'on_login_trigger', 'Name of login trigger'); |
||||||
|
$evttrig = $node_standby_2->safe_psql('postgres', |
||||||
|
"SELECT evtname FROM pg_event_trigger WHERE evtevent = 'login'"); |
||||||
|
is($evttrig, 'on_login_trigger', 'Name of login trigger'); |
||||||
|
|
||||||
|
note "enabling hot_standby_feedback"; |
||||||
|
|
||||||
|
# Enable hs_feedback. The slot should gain an xmin. We set the status interval |
||||||
|
# so we'll see the results promptly. |
||||||
|
$node_standby_1->safe_psql('postgres', |
||||||
|
'ALTER SYSTEM SET hot_standby_feedback = on;'); |
||||||
|
$node_standby_1->reload; |
||||||
|
$node_standby_2->safe_psql('postgres', |
||||||
|
'ALTER SYSTEM SET hot_standby_feedback = on;'); |
||||||
|
$node_standby_2->reload; |
||||||
|
replay_check(); |
||||||
|
|
||||||
|
($xmin, $catalog_xmin) = get_slot_xmins($node_primary, $slotname_1, |
||||||
|
"xmin IS NOT NULL AND catalog_xmin IS NULL"); |
||||||
|
isnt($xmin, '', 'xmin of non-cascaded slot non-null with hs feedback'); |
||||||
|
is($catalog_xmin, '', |
||||||
|
'catalog xmin of non-cascaded slot still null with hs_feedback'); |
||||||
|
|
||||||
|
my ($xmin1, $catalog_xmin1) = get_slot_xmins($node_standby_1, $slotname_2, |
||||||
|
"xmin IS NOT NULL AND catalog_xmin IS NULL"); |
||||||
|
isnt($xmin1, '', 'xmin of cascaded slot non-null with hs feedback'); |
||||||
|
is($catalog_xmin1, '', |
||||||
|
'catalog xmin of cascaded slot still null with hs_feedback'); |
||||||
|
|
||||||
|
note "doing some work to advance xmin"; |
||||||
|
$node_primary->safe_psql( |
||||||
|
'postgres', q{ |
||||||
|
do $$ |
||||||
|
begin |
||||||
|
for i in 10000..11000 loop |
||||||
|
-- use an exception block so that each iteration eats an XID |
||||||
|
begin |
||||||
|
insert into tab_int values (i); |
||||||
|
exception |
||||||
|
when division_by_zero then null; |
||||||
|
end; |
||||||
|
end loop; |
||||||
|
end$$; |
||||||
|
}); |
||||||
|
|
||||||
|
$node_primary->safe_psql('postgres', 'VACUUM;'); |
||||||
|
$node_primary->safe_psql('postgres', 'CHECKPOINT;'); |
||||||
|
|
||||||
|
my ($xmin2, $catalog_xmin2) = |
||||||
|
get_slot_xmins($node_primary, $slotname_1, "xmin <> '$xmin'"); |
||||||
|
note "primary slot's new xmin $xmin2, old xmin $xmin"; |
||||||
|
isnt($xmin2, $xmin, 'xmin of non-cascaded slot with hs feedback has changed'); |
||||||
|
is($catalog_xmin2, '', |
||||||
|
'catalog xmin of non-cascaded slot still null with hs_feedback unchanged' |
||||||
|
); |
||||||
|
|
||||||
|
($xmin2, $catalog_xmin2) = |
||||||
|
get_slot_xmins($node_standby_1, $slotname_2, "xmin <> '$xmin1'"); |
||||||
|
note "standby_1 slot's new xmin $xmin2, old xmin $xmin1"; |
||||||
|
isnt($xmin2, $xmin1, 'xmin of cascaded slot with hs feedback has changed'); |
||||||
|
is($catalog_xmin2, '', |
||||||
|
'catalog xmin of cascaded slot still null with hs_feedback unchanged'); |
||||||
|
|
||||||
|
note "disabling hot_standby_feedback"; |
||||||
|
|
||||||
|
# Disable hs_feedback. Xmin should be cleared. |
||||||
|
$node_standby_1->safe_psql('postgres', |
||||||
|
'ALTER SYSTEM SET hot_standby_feedback = off;'); |
||||||
|
$node_standby_1->reload; |
||||||
|
$node_standby_2->safe_psql('postgres', |
||||||
|
'ALTER SYSTEM SET hot_standby_feedback = off;'); |
||||||
|
$node_standby_2->reload; |
||||||
|
replay_check(); |
||||||
|
|
||||||
|
($xmin, $catalog_xmin) = get_slot_xmins($node_primary, $slotname_1, |
||||||
|
"xmin IS NULL AND catalog_xmin IS NULL"); |
||||||
|
is($xmin, '', 'xmin of non-cascaded slot null with hs feedback reset'); |
||||||
|
is($catalog_xmin, '', |
||||||
|
'catalog xmin of non-cascaded slot still null with hs_feedback reset'); |
||||||
|
|
||||||
|
($xmin, $catalog_xmin) = get_slot_xmins($node_standby_1, $slotname_2, |
||||||
|
"xmin IS NULL AND catalog_xmin IS NULL"); |
||||||
|
is($xmin, '', 'xmin of cascaded slot null with hs feedback reset'); |
||||||
|
is($catalog_xmin, '', |
||||||
|
'catalog xmin of cascaded slot still null with hs_feedback reset'); |
||||||
|
|
||||||
|
note "check change primary_conninfo without restart"; |
||||||
|
$node_standby_2->append_conf('postgresql.conf', "primary_slot_name = ''"); |
||||||
|
$node_standby_2->enable_streaming($node_primary); |
||||||
|
$node_standby_2->reload; |
||||||
|
|
||||||
|
# be sure do not streaming from cascade |
||||||
|
$node_standby_1->stop; |
||||||
|
|
||||||
|
my $newval = $node_primary->safe_psql('postgres', |
||||||
|
'INSERT INTO replayed(val) SELECT coalesce(max(val),0) + 1 AS newval FROM replayed RETURNING val' |
||||||
|
); |
||||||
|
$node_primary->wait_for_catchup($node_standby_2); |
||||||
|
my $is_replayed = $node_standby_2->safe_psql('postgres', |
||||||
|
qq[SELECT 1 FROM replayed WHERE val = $newval]); |
||||||
|
is($is_replayed, qq(1), "standby_2 didn't replay primary value $newval"); |
||||||
|
|
||||||
|
# Drop any existing slots on the primary, for the follow-up tests. |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
"SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots;"); |
||||||
|
|
||||||
|
# Test physical slot advancing and its durability. Create a new slot on |
||||||
|
# the primary, not used by any of the standbys. This reserves WAL at creation. |
||||||
|
my $phys_slot = 'phys_slot'; |
||||||
|
$node_primary->safe_psql('postgres', |
||||||
|
"SELECT pg_create_physical_replication_slot('$phys_slot', true);"); |
||||||
|
# Generate some WAL, and switch to a new segment, used to check that |
||||||
|
# the previous segment is correctly getting recycled as the slot advancing |
||||||
|
# would recompute the minimum LSN calculated across all slots. |
||||||
|
my $segment_removed = $node_primary->safe_psql('postgres', |
||||||
|
'SELECT pg_walfile_name(pg_current_wal_lsn())'); |
||||||
|
chomp($segment_removed); |
||||||
|
$node_primary->advance_wal(1); |
||||||
|
my $current_lsn = |
||||||
|
$node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn();"); |
||||||
|
chomp($current_lsn); |
||||||
|
my $psql_rc = $node_primary->psql('postgres', |
||||||
|
"SELECT pg_replication_slot_advance('$phys_slot', '$current_lsn'::pg_lsn);" |
||||||
|
); |
||||||
|
is($psql_rc, '0', 'slot advancing with physical slot'); |
||||||
|
my $phys_restart_lsn_pre = $node_primary->safe_psql('postgres', |
||||||
|
"SELECT restart_lsn from pg_replication_slots WHERE slot_name = '$phys_slot';" |
||||||
|
); |
||||||
|
chomp($phys_restart_lsn_pre); |
||||||
|
# Slot advance should persist across clean restarts. |
||||||
|
$node_primary->restart; |
||||||
|
my $phys_restart_lsn_post = $node_primary->safe_psql('postgres', |
||||||
|
"SELECT restart_lsn from pg_replication_slots WHERE slot_name = '$phys_slot';" |
||||||
|
); |
||||||
|
chomp($phys_restart_lsn_post); |
||||||
|
ok( ($phys_restart_lsn_pre cmp $phys_restart_lsn_post) == 0, |
||||||
|
"physical slot advance persists across restarts"); |
||||||
|
|
||||||
|
# Check if the previous segment gets correctly recycled after the |
||||||
|
# server stopped cleanly, causing a shutdown checkpoint to be generated. |
||||||
|
my $primary_data = $node_primary->data_dir; |
||||||
|
ok(!-f "$primary_data/pg_wal/$segment_removed", |
||||||
|
"WAL segment $segment_removed recycled after physical slot advancing"); |
||||||
|
|
||||||
|
note "testing pg_backup_start() followed by BASE_BACKUP"; |
||||||
|
my $connstr = $node_primary->connstr('postgres') . " replication=database"; |
||||||
|
|
||||||
|
# This test requires a replication connection with a database, as it mixes |
||||||
|
# a replication command and a SQL command. |
||||||
|
$node_primary->command_fails_like( |
||||||
|
[ |
||||||
|
'psql', '-X', '-c', "SELECT pg_backup_start('backup', true)", |
||||||
|
'-c', 'BASE_BACKUP', '-d', $connstr |
||||||
|
], |
||||||
|
qr/a backup is already in progress in this session/, |
||||||
|
'BASE_BACKUP cannot run in session already running backup'); |
||||||
|
|
||||||
|
note "testing BASE_BACKUP cancellation"; |
||||||
|
|
||||||
|
my $sigchld_bb_timeout = |
||||||
|
IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default); |
||||||
|
|
||||||
|
# This test requires a replication connection with a database, as it mixes |
||||||
|
# a replication command and a SQL command. The first BASE_BACKUP is throttled |
||||||
|
# to give enough room for the cancellation running below. The second command |
||||||
|
# for pg_backup_stop() should fail. |
||||||
|
my ($sigchld_bb_stdin, $sigchld_bb_stdout, $sigchld_bb_stderr) = ('', '', ''); |
||||||
|
my $sigchld_bb = IPC::Run::start( |
||||||
|
[ |
||||||
|
'psql', '-X', '-c', "BASE_BACKUP (CHECKPOINT 'fast', MAX_RATE 32);", |
||||||
|
'-c', 'SELECT pg_backup_stop()', |
||||||
|
'-d', $connstr |
||||||
|
], |
||||||
|
'<', |
||||||
|
\$sigchld_bb_stdin, |
||||||
|
'>', |
||||||
|
\$sigchld_bb_stdout, |
||||||
|
'2>', |
||||||
|
\$sigchld_bb_stderr, |
||||||
|
$sigchld_bb_timeout); |
||||||
|
|
||||||
|
# The cancellation is issued once the database files are streamed and |
||||||
|
# the checkpoint issued at backup start completes. |
||||||
|
is( $node_primary->poll_query_until( |
||||||
|
'postgres', |
||||||
|
"SELECT pg_cancel_backend(a.pid) FROM " |
||||||
|
. "pg_stat_activity a, pg_stat_progress_basebackup b WHERE " |
||||||
|
. "a.pid = b.pid AND a.query ~ 'BASE_BACKUP' AND " |
||||||
|
. "b.phase = 'streaming database files';"), |
||||||
|
"1", |
||||||
|
"WAL sender sending base backup killed"); |
||||||
|
|
||||||
|
# The psql command should fail on pg_backup_stop(). |
||||||
|
ok( pump_until( |
||||||
|
$sigchld_bb, $sigchld_bb_timeout, |
||||||
|
\$sigchld_bb_stderr, qr/backup is not in progress/), |
||||||
|
'base backup cleanly canceled'); |
||||||
|
$sigchld_bb->finish(); |
||||||
|
|
||||||
|
done_testing(); |
||||||
|
|
Loading…
Reference in new issue