diff --git a/doc/src/sgml/recovery-config.sgml b/doc/src/sgml/recovery-config.sgml index a91864b..73ef387 100644 --- a/doc/src/sgml/recovery-config.sgml +++ b/doc/src/sgml/recovery-config.sgml @@ -211,6 +211,11 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows The precise stopping point is also influenced by . + + This parameter also checks if the checkpoint time of the backup being used is earlier than the specified recovery_target_time. + In other words, the recovery will not proceed further if the checkpoint time of the backup is later than the specified recovery_target_time. + + @@ -231,6 +236,11 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows The precise stopping point is also influenced by . + + This parameter also checks if the latest completed xid of the backup is earlier than the specified recovery target xid. + In other words, the recovery will not proceed further if the latest completed xid of the backup is later than the specified target xid. + + @@ -248,6 +258,11 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows parameter is parsed using the system data type pg_lsn. + + This parameter also checks if the current checkpoint's redo position of the backup is earlier than the specified recovery target lsn. + In other words, the recovery will not proceed further if the latest checkpoint's redo position of the backup is later than the specified target lsn. + + diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 8973583..f030baa 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -834,6 +834,7 @@ static MemoryContext walDebugCxt = NULL; #endif static void readRecoveryCommandFile(void); +static void recoveryStartsHere(void); static void exitArchiveRecovery(TimeLineID endTLI, XLogRecPtr endOfLog); static bool recoveryStopsBefore(XLogReaderState *record); static bool recoveryStopsAfter(XLogReaderState *record); @@ -5544,6 +5545,66 @@ getRecordTimestamp(XLogReaderState *record, TimestampTz *recordXtime) return false; } +/* When performing point-in-time-recovery, this function identifies if + * the specified recovery target (recovery_target_time, recovery_target_lsn and recovery_target_xid) is prior to that of the backup. + * Which means, recovery cannot proceed if the recovery target point is prior to backup start point. + */ + +static void +recoveryStartsHere(void) +{ + /* + * Check if the recovery target xid is older than the latest completed xid + * of the backup + */ + + if (recoveryTarget == RECOVERY_TARGET_XID) + { + if (TransactionIdPrecedes(recoveryTargetXid, + ControlFile->checkPointCopy.latestCompletedXid)) + { + ereport(ERROR, + (errmsg("Specified recovery_target_xid is older than the latest Completed Xid which is %u", ControlFile->checkPointCopy.latestCompletedXid), + errhint("This means that the backup being used is much later than the recovery target position.\n" + "You might need to use a backup taken prior to the recovery target point."))); + } + } + + /* + * Check if the recovery target lsn is prior to the latest checkpointi's + * redo position of the backup + */ + + if (recoveryTarget == RECOVERY_TARGET_LSN) + { + if (recoveryTargetLSN < ControlFile->checkPointCopy.redo) + { + ereport(ERROR, + (errmsg("Specified recovery_target_lsn is older than the backup start LSN which is \"%X/%X\"", + (uint32) (ControlFile->checkPointCopy.redo >> 32), (uint32) ControlFile->checkPointCopy.redo), + errhint("This means that the backup being used is much later than the recovery target position.\n" + "You might need to use a backup taken prior to the recovery target point."))); + } + } + + /* + * Check if the recovery target time is prior to the current timestamp of + * the backup + */ + + if (recoveryTarget == RECOVERY_TARGET_TIME) + { + if (recoveryTargetTime < time_t_to_timestamptz(ControlFile->checkPointCopy.time)) + { + ereport(ERROR, + (errmsg("Specified recovery_target_time is older than the backup's latest checkpoint time which is %s", + pstrdup(str_time(ControlFile->checkPointCopy.time))), + errhint("This means that the backup being used is much later than the recovery target position.\n" + "You might need to use a backup taken prior to the recovery target point."))); + } + } +} + /* * For point-in-time recovery, this function decides whether we want to * stop applying the XLOG before the current record. @@ -6277,6 +6338,9 @@ StartupXLOG(void) (errmsg("starting archive recovery"))); } + /* Check if archive recovery can start at all */ + recoveryStartsHere(); + /* * Take ownership of the wakeup latch if we're going to sleep during * recovery. @@ -8642,6 +8706,7 @@ CreateCheckPoint(int flags) checkPoint.nextXid = ShmemVariableCache->nextXid; checkPoint.oldestXid = ShmemVariableCache->oldestXid; checkPoint.oldestXidDB = ShmemVariableCache->oldestXidDB; + checkPoint.latestCompletedXid = ShmemVariableCache->latestCompletedXid; LWLockRelease(XidGenLock); LWLockAcquire(CommitTsLock, LW_SHARED); diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c index f47171d..5ba170c 100644 --- a/src/bin/pg_controldata/pg_controldata.c +++ b/src/bin/pg_controldata/pg_controldata.c @@ -222,6 +222,8 @@ main(int argc, char *argv[]) printf(_("Latest checkpoint's NextXID: %u:%u\n"), ControlFile->checkPointCopy.nextXidEpoch, ControlFile->checkPointCopy.nextXid); + printf(_("Latest checkpoint's latestCompletedXID: %u\n"), + ControlFile->checkPointCopy.latestCompletedXid); printf(_("Latest checkpoint's NextOID: %u\n"), ControlFile->checkPointCopy.nextOid); printf(_("Latest checkpoint's NextMultiXactId: %u\n"), diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h index e4194b9..a8c9101 100644 --- a/src/include/catalog/pg_control.h +++ b/src/include/catalog/pg_control.h @@ -58,6 +58,7 @@ typedef struct CheckPoint * set to InvalidTransactionId. */ TransactionId oldestActiveXid; + TransactionId latestCompletedXid; } CheckPoint; /* XLOG info values for XLOG rmgr */ diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm index 4018f0a..92c5367 100644 --- a/src/test/perl/PostgresNode.pm +++ b/src/test/perl/PostgresNode.pm @@ -601,6 +601,11 @@ Restoring WAL segments from archives using restore_command can be enabled by passing the keyword parameter has_restoring => 1. This is disabled by default. +Restoring WAL segments from archives using restore command to perform PITR +can be enabled by passing the keyword parameter has_restoring_pitr => 1. This +is disabled by default. By enabling this parameter, the standby database will +not operate in standby mode. + The backup is copied, leaving the original unmodified. pg_hba.conf is unconditionally set to enable replication connections. @@ -618,6 +623,7 @@ sub init_from_backup $params{hba_permit_replication} = 1 unless defined $params{hba_permit_replication}; $params{has_restoring} = 0 unless defined $params{has_restoring}; + $params{has_restoring_pitr} = 0 unless defined $params{has_restoring_pitr}; print "# Initializing node \"$node_name\" from backup \"$backup_name\" of node \"$root_name\"\n"; @@ -641,6 +647,7 @@ port = $port $self->set_replication_conf if $params{hba_permit_replication}; $self->enable_streaming($root_node) if $params{has_streaming}; $self->enable_restoring($root_node) if $params{has_restoring}; + $self->enable_restoring_pitr($root_node) if $params{has_restoring_pitr}; } =pod @@ -673,6 +680,31 @@ sub start $self->_update_pid; } + +=pod + +=item $node->start_pitr() + +Wrapper for pg_ctl start + +Start the node and wait until it is ready to accept connections. + +=cut + +sub start_pitr +{ + my ($self) = @_; + my $port = $self->port; + my $pgdata = $self->data_dir; + my $name = $self->name; + my $log = $self->logfile; + print("### Starting node \"$name\"\n"); + my $ret = TestLib::system_log('pg_ctl', '-D', $self->data_dir, '-l', + $self->logfile, 'start'); + + return $ret; +} + =pod =item $node->stop(mode) @@ -797,6 +829,32 @@ standby_mode = on )); } +# Internal routine to enable archive recovery command on a standby node for PITR +sub enable_restoring_pitr +{ + my ($self, $root_node) = @_; + my $path = $root_node->archive_dir; + my $name = $self->name; + + print "### Enabling WAL restore for node \"$name\"\n"; + + # On Windows, the path specified in the restore command needs to use + # double back-slashes to work properly and to be able to detect properly + # the file targeted by the copy command, so the directory value used + # in this routine, using only one back-slash, need to be properly changed + # first. Paths also need to be double-quoted to prevent failures where + # the path contains spaces. + $path =~ s{\\}{\\\\}g if ($TestLib::windows_os); + my $copy_command = + $TestLib::windows_os + ? qq{copy "$path\\\\%f" "%p"} + : qq{cp "$path/%f" "%p"}; + $self->append_conf( + 'recovery.conf', qq( +restore_command = '$copy_command' +)); +} + # Internal routine to enable archiving sub enable_archiving { diff --git a/src/test/recovery/t/003_recovery_targets.pl b/src/test/recovery/t/003_recovery_targets.pl index b7b0caa..ec6b9fe 100644 --- a/src/test/recovery/t/003_recovery_targets.pl +++ b/src/test/recovery/t/003_recovery_targets.pl @@ -3,7 +3,7 @@ use strict; use warnings; use PostgresNode; use TestLib; -use Test::More tests => 9; +use Test::More tests => 12; # Create and test a standby from given backup, with a certain # recovery target. @@ -45,6 +45,69 @@ sub test_recovery_standby $node_standby->teardown_node; } +# Test recovery start point for various recovery targets + +sub test_recovery_pitr +{ + my $test_name = shift; + my $node_name = shift; + my $node_master = shift; + my $recovery_params = shift; + my $num_rows = shift; + + my $node_standby = get_new_node($node_name); + $node_standby->init_from_backup($node_master, 'my_pitr', + has_restoring_pitr => 1); + + foreach my $param_item (@$recovery_params) + { + $node_standby->append_conf( + 'recovery.conf', + qq($param_item +)); + } + my $ret = $node_standby->start_pitr; + + my $log = $node_standby->logfile; + my $irx = "Specified recovery_target_xid is older than the latest Completed Xid"; + my $irl = "Specified recovery_target_lsn is older than the backup start LSN"; + my $irt = "Specified recovery_target_time is older than the backup's latest checkpoint time"; + + my @rx = `grep -inr "$irx" $log`; + my @rl = `grep -inr "$irl" $log`; + my @rt = `grep -inr "$irt" $log`; + + if ($ret != 0) + { + if (@rx) + { + print "Recovery could not start as the specified target XID was not legitimate. Test Passed\n"; + isnt($ret,0, "Successful $test_name."); + goto TEST_RECOVERY_LSN; + + } + elsif(@rl) + { + print "Recovery could not start as the specified target LSN was not legitimate. Test Passed\n"; + isnt($ret,0, "Successful $test_name."); + goto TEST_RECOVERY_TIME; + + } + elsif(@rt) + { + print "Recovery could not start as the specified target TIME was not legitimate. Test Passed\n"; + isnt($ret,0, "Successful $test_name."); + exit 0; + } + else + { + print "# pg_ctl failed; logfile:\n"; + print TestLib::slurp_file($node_standby->logfile); + BAIL_OUT("pg_ctl failed"); + } + } +} + # Initialize master node my $node_master = get_new_node('master'); $node_master->init(has_archiving => 1, allows_streaming => 1); @@ -144,3 +207,57 @@ test_recovery_standby('XID + time + name', "recovery_target_lsn = '$recovery_lsn'",); test_recovery_standby('XID + time + name + LSN', 'standby_9', $node_master, \@recovery_params, "5000", $lsn5); + +# Check if the recovery start point is legitimate +# If the recovery start point is not appropriate, recovery should not start at all + +# Generate some data on master to advance the database position + +$node_master->safe_psql('postgres', + "INSERT INTO tab_int VALUES (generate_series(4001,5000))"); +$node_master->safe_psql('postgres', + "INSERT INTO tab_int VALUES (generate_series(5001,6000))"); +$node_master->safe_psql('postgres', + "INSERT INTO tab_int VALUES (generate_series(6001,7000))"); +$node_master->safe_psql('postgres', + "INSERT INTO tab_int VALUES (generate_series(7001,8000))"); +$node_master->safe_psql('postgres', + "INSERT INTO tab_int VALUES (generate_series(8001,9000))"); +$node_master->safe_psql('postgres', + "INSERT INTO tab_int VALUES (generate_series(9001,10000))"); + +print "required data generated.\n"; + +my $rxid = $node_master->safe_psql('postgres', + "SELECT pg_current_wal_location(), txid_current();"); +my ($lsn10, $recovery_lxid) = split /\|/, $rxid; + +# Now take a fresh backup + +$node_master->backup('my_pitr'); + +# Test the recovery start point for XID. Specify an old XID which falls prior to the backup's latest XID +@recovery_params = ( + "recovery_target_xid = '$recovery_txid'",); + +test_recovery_pitr('Recovery Start Point check for XID', + 'pitr_1', $node_master, \@recovery_params, "10000"); + +# Test the checking of the recovery start point for target LSN. Specify an old recovery target LSN which falls prior to the backup's latest LSN. +TEST_RECOVERY_LSN: +my $recovery_olsn = $lsn1; +@recovery_params = ( + "recovery_target_lsn = '$recovery_olsn'",); + +test_recovery_pitr('Recovery Start Point check for LSN', + 'pitr_2', $node_master, \@recovery_params, "10000"); + +# Test the checking of the recovery start point for target TIME. Specify an old recovery target TIME which falls prior to the backup's latest Timestamp. +TEST_RECOVERY_TIME: + +my $recovery_otime = $recovery_time; +@recovery_params = ( + "recovery_target_time = '$recovery_otime'",); + +test_recovery_pitr('Recovery Start Point check for TIME', + 'pitr_3', $node_master, \@recovery_params, "10000");