1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2024-12-14 10:13:05 +02:00
pgbackrest/lib/BackRest/Backup.pm
David Steele 49fe40850f v0.65: Improved resume and restore logging, compact restores
* Better resume support.  Resumed files are checked to be sure they have not been modified and the manifest is saved more often to preserve checksums as the backup progresses.  More unit tests to verify each resume case.

* Resume is now optional.  Use the `resume` setting or `--no-resume` from the command line to disable.

* More info messages during restore.  Previously, most of the restore messages were debug level so not a lot was output in the log.

* Fixed an issue where an absolute path was not written into recovery.conf when the restore was run with a relative path.

* Added `tablespace` setting to allow tablespaces to be restored into the `pg_tblspc` path.  This produces compact restores that are convenient for development, staging, etc.  Currently these restores cannot be backed up as PgBackRest expects only links in the `pg_tblspc` path.
2015-05-11 18:29:40 -04:00

1074 lines
41 KiB
Perl

####################################################################################################################################
# BACKUP MODULE
####################################################################################################################################
package BackRest::Backup;
use threads;
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);
use File::Basename;
use File::Path qw(remove_tree);
use Scalar::Util qw(looks_like_number);
use Fcntl 'SEEK_CUR';
use Thread::Queue;
use lib dirname($0);
use BackRest::Utility;
use BackRest::Exception;
use BackRest::Config;
use BackRest::Manifest;
use BackRest::File;
use BackRest::Db;
use BackRest::ThreadGroup;
use BackRest::Archive;
use BackRest::BackupFile;
use Exporter qw(import);
our @EXPORT = qw(backup_init backup_cleanup backup backup_expire archive_list_get);
my $oDb;
my $oFile;
my $strType; # Type of backup: full, differential (diff), incremental (incr)
my $bCompress;
my $bHardLink;
my $bNoStartStop;
my $bForce;
my $iThreadMax;
my $iThreadTimeout;
####################################################################################################################################
# BACKUP_INIT
####################################################################################################################################
sub backup_init
{
my $oDbParam = shift;
my $oFileParam = shift;
my $strTypeParam = shift;
my $bCompressParam = shift;
my $bHardLinkParam = shift;
my $iThreadMaxParam = shift;
my $iThreadTimeoutParam = shift;
my $bNoStartStopParam = shift;
my $bForceParam = shift;
$oDb = $oDbParam;
$oFile = $oFileParam;
$strType = $strTypeParam;
$bCompress = $bCompressParam;
$bHardLink = $bHardLinkParam;
$iThreadMax = $iThreadMaxParam;
$iThreadTimeout = $iThreadTimeoutParam;
$bNoStartStop = $bNoStartStopParam;
$bForce = $bForceParam;
}
####################################################################################################################################
# BACKUP_CLEANUP
####################################################################################################################################
sub backup_cleanup
{
undef($oFile);
}
####################################################################################################################################
# BACKUP_REGEXP_GET - Generate a regexp depending on the backups that need to be found
####################################################################################################################################
sub backup_regexp_get
{
my $bFull = shift;
my $bDifferential = shift;
my $bIncremental = shift;
if (!$bFull && !$bDifferential && !$bIncremental)
{
confess &log(ERROR, 'one parameter must be true');
}
my $strDateTimeRegExp = "[0-9]{8}\\-[0-9]{6}";
my $strRegExp = '^';
if ($bFull || $bDifferential || $bIncremental)
{
$strRegExp .= $strDateTimeRegExp . 'F';
}
if ($bDifferential || $bIncremental)
{
if ($bFull)
{
$strRegExp .= "(\\_";
}
else
{
$strRegExp .= "\\_";
}
$strRegExp .= $strDateTimeRegExp;
if ($bDifferential && $bIncremental)
{
$strRegExp .= '(D|I)';
}
elsif ($bDifferential)
{
$strRegExp .= 'D';
}
else
{
$strRegExp .= 'I';
}
if ($bFull)
{
$strRegExp .= '){0,1}';
}
}
$strRegExp .= "\$";
&log(DEBUG, "backup_regexp_get($bFull, $bDifferential, $bIncremental): $strRegExp");
return $strRegExp;
}
####################################################################################################################################
# BACKUP_TYPE_FIND - Find the last backup depending on the type
####################################################################################################################################
sub backup_type_find
{
my $strType = shift;
my $strBackupClusterPath = shift;
my $strDirectory;
if ($strType eq BACKUP_TYPE_INCR)
{
$strDirectory = ($oFile->list(PATH_BACKUP_CLUSTER, undef, backup_regexp_get(1, 1, 1), 'reverse'))[0];
}
if (!defined($strDirectory) && $strType ne BACKUP_TYPE_FULL)
{
$strDirectory = ($oFile->list(PATH_BACKUP_CLUSTER, undef, backup_regexp_get(1, 0, 0), 'reverse'))[0];
}
return $strDirectory;
}
####################################################################################################################################
# BACKUP_FILE_NOT_IN_MANIFEST - Find all files in a backup path that are not in the supplied manifest
####################################################################################################################################
sub backup_file_not_in_manifest
{
my $strPathType = shift;
my $oManifest = shift;
my $oAbortedManifest = shift;
my %oFileHash;
$oFile->manifest($strPathType, undef, \%oFileHash);
my @stryFile;
foreach my $strName (sort(keys $oFileHash{name}))
{
# Ignore certain files that will never be in the manifest
if ($strName eq 'backup.manifest' ||
$strName eq '.')
{
next;
}
# Get the base directory
my $strBasePath = (split('/', $strName))[0];
if ($strBasePath eq $strName)
{
my $strSection = $strBasePath eq 'tablespace' ? 'backup:tablespace' : "${strBasePath}:path";
if ($oManifest->test($strSection))
{
next;
}
}
else
{
my $strPath = substr($strName, length($strBasePath) + 1);
# Create the section from the base path
my $strSection = $strBasePath;
if ($strSection eq 'tablespace')
{
my $strTablespace = (split('/', $strPath))[0];
$strSection = $strSection . ':' . $strTablespace;
if ($strTablespace eq $strPath)
{
if ($oManifest->test("${strSection}:path"))
{
next;
}
}
$strPath = substr($strPath, length($strTablespace) + 1);
}
my $cType = $oFileHash{name}{"${strName}"}{type};
if ($cType eq 'd')
{
if ($oManifest->test("${strSection}:path", "${strPath}"))
{
next;
}
}
elsif ($cType eq 'f')
{
if ($oManifest->test("${strSection}:file", "${strPath}") &&
!$oManifest->test("${strSection}:file", $strPath, MANIFEST_SUBKEY_REFERENCE))
{
my $strChecksum = $oAbortedManifest->get("${strSection}:file", $strPath, MANIFEST_SUBKEY_CHECKSUM, false);
if (defined($strChecksum) &&
$oManifest->get("${strSection}:file", $strPath, MANIFEST_SUBKEY_SIZE) ==
$oFileHash{name}{$strName}{size} &&
$oManifest->get("${strSection}:file", $strPath, MANIFEST_SUBKEY_MODIFICATION_TIME) ==
$oFileHash{name}{$strName}{modification_time})
{
$oManifest->set("${strSection}:file", $strPath, MANIFEST_SUBKEY_CHECKSUM, $strChecksum);
next;
}
}
}
}
push @stryFile, $strName;
}
return @stryFile;
}
####################################################################################################################################
# BACKUP_TMP_CLEAN
#
# Cleans the temp directory from a previous failed backup so it can be reused
####################################################################################################################################
sub backup_tmp_clean
{
my $oManifest = shift;
my $oAbortedManifest = shift;
&log(INFO, 'cleaning backup tmp path');
# Remove the pg_xlog directory since it contains nothing useful for the new backup
if (-e $oFile->path_get(PATH_BACKUP_TMP, 'base/pg_xlog'))
{
remove_tree($oFile->path_get(PATH_BACKUP_TMP, 'base/pg_xlog')) or confess &log(ERROR, 'unable to delete tmp pg_xlog path');
}
# Remove the pg_tblspc directory since it is trivial to rebuild, but hard to compare
if (-e $oFile->path_get(PATH_BACKUP_TMP, 'base/pg_tblspc'))
{
remove_tree($oFile->path_get(PATH_BACKUP_TMP, 'base/pg_tblspc')) or confess &log(ERROR, 'unable to delete tmp pg_tblspc path');
}
# Get the list of files that should be deleted from temp
my @stryFile = backup_file_not_in_manifest(PATH_BACKUP_TMP, $oManifest, $oAbortedManifest);
foreach my $strFile (sort {$b cmp $a} @stryFile)
{
my $strDelete = $oFile->path_get(PATH_BACKUP_TMP, $strFile);
# If a path then delete it, all the files should have already been deleted since we are going in reverse order
if (-d $strDelete)
{
&log(DEBUG, "remove path ${strDelete}");
rmdir($strDelete) or confess &log(ERROR, "unable to delete path ${strDelete}, is it empty?");
}
# Else delete a file
else
{
&log(DEBUG, "remove file ${strDelete}");
unlink($strDelete) or confess &log(ERROR, "unable to delete file ${strDelete}");
}
}
}
####################################################################################################################################
# BACKUP_FILE - Performs the file level backup
#
# Uses the information in the manifest to determine which files need to be copied. Directories and tablespace links are only
# created when needed, except in the case of a full backup or if hardlinks are requested.
####################################################################################################################################
sub backup_file
{
my $strDbClusterPath = shift; # Database base data path
my $oBackupManifest = shift; # Manifest for the current backup
# Variables used for parallel copy
my %oFileCopyMap;
my $lFileTotal = 0;
my $lSizeTotal = 0;
# Determine whether all paths and links will be created
my $bFullCreate = $bHardLink || $strType eq BACKUP_TYPE_FULL;
# Iterate through the path sections of the manifest to backup
foreach my $strPathKey ($oBackupManifest->keys(MANIFEST_SECTION_BACKUP_PATH))
{
# Determine the source and destination backup paths
my $strBackupSourcePath; # Absolute path to the database base directory or tablespace to backup
my $strBackupDestinationPath; # Relative path to the backup directory where the data will be stored
# Process the base database directory
if ($strPathKey =~ /^base$/)
{
$strBackupSourcePath = $strDbClusterPath;
$strBackupDestinationPath = 'base';
}
# Process each tablespace
elsif ($strPathKey =~ /^tablespace\:/)
{
my $strTablespaceName = (split(':', $strPathKey))[1];
$strBackupSourcePath = $oBackupManifest->get(MANIFEST_SECTION_BACKUP_TABLESPACE, $strTablespaceName,
MANIFEST_SUBKEY_PATH);
$strBackupDestinationPath = "tablespace/${strTablespaceName}";
# Create the tablespace directory and link
if ($bFullCreate)
{
$oFile->link_create(PATH_BACKUP_TMP, $strBackupDestinationPath,
PATH_BACKUP_TMP,
'base/pg_tblspc/' . $oBackupManifest->get(MANIFEST_SECTION_BACKUP_TABLESPACE,
$strTablespaceName, MANIFEST_SUBKEY_LINK),
false, true, true);
}
}
else
{
confess &log(ASSERT, "cannot find type for path ${strPathKey}");
}
# If this is a full backup or hard-linked then create all paths and links
if ($bFullCreate)
{
# Create paths
my $strSectionPath = "$strPathKey:path";
if ($oBackupManifest->test($strSectionPath))
{
foreach my $strPath ($oBackupManifest->keys($strSectionPath))
{
if ($strPath ne '.')
{
$oFile->path_create(PATH_BACKUP_TMP, "${strBackupDestinationPath}/${strPath}");
}
}
}
# Create links
my $strSectionLink = "$strPathKey:link";
if ($oBackupManifest->test($strSectionLink))
{
foreach my $strLink ($oBackupManifest->keys($strSectionLink))
{
# Create links except in pg_tblspc because they have already been created
if (!($strPathKey eq 'base' && $strLink =~ /^pg_tblspc\/.*/))
{
$oFile->link_create(PATH_BACKUP_ABSOLUTE,
$oBackupManifest->get($strSectionLink, $strLink, MANIFEST_SUBKEY_DESTINATION),
PATH_BACKUP_TMP, "${strBackupDestinationPath}/${strLink}",
false, false, false);
}
}
}
}
# Possible for the file section to exist with no files (i.e. empty tablespace)
my $strSectionFile = "$strPathKey:file";
if (!$oBackupManifest->test($strSectionFile))
{
next;
}
# Iterate through the files for each backup source path
foreach my $strFile ($oBackupManifest->keys($strSectionFile))
{
my $strBackupSourceFile = "${strBackupSourcePath}/${strFile}";
# If the file has a reference it does not need to be copied since it can be retrieved from the referenced backup.
# However, if hard-linking is turned on the link will need to be created
my $bProcess = true;
my $strReference = $oBackupManifest->get($strSectionFile, $strFile, MANIFEST_SUBKEY_REFERENCE, false);
if (defined($strReference))
{
# If hardlinking is turned on then create a hardlink for files that have not changed since the last backup
if ($bHardLink)
{
&log(DEBUG, "hard-linking ${strBackupSourceFile} from ${strReference}");
$oFile->link_create(PATH_BACKUP_CLUSTER, "${strReference}/${strBackupDestinationPath}/${strFile}",
PATH_BACKUP_TMP, "${strBackupDestinationPath}/${strFile}", true, false, true);
}
$bProcess = false;
}
if ($bProcess)
{
my $lFileSize = $oBackupManifest->get($strSectionFile, $strFile, MANIFEST_SUBKEY_SIZE);
# Setup variables needed for threaded copy
$lFileTotal++;
$lSizeTotal += $lFileSize;
$oFileCopyMap{$strPathKey}{$strFile}{db_file} = $strBackupSourceFile;
$oFileCopyMap{$strPathKey}{$strFile}{file_section} = $strSectionFile;
$oFileCopyMap{$strPathKey}{$strFile}{file} = ${strFile};
$oFileCopyMap{$strPathKey}{$strFile}{backup_file} = "${strBackupDestinationPath}/${strFile}";
$oFileCopyMap{$strPathKey}{$strFile}{size} = $lFileSize;
$oFileCopyMap{$strPathKey}{$strFile}{modification_time} =
$oBackupManifest->get($strSectionFile, $strFile, MANIFEST_SUBKEY_MODIFICATION_TIME, false);
$oFileCopyMap{$strPathKey}{$strFile}{checksum} =
$oBackupManifest->get($strSectionFile, $strFile, MANIFEST_SUBKEY_CHECKSUM, false);
}
}
}
# If there are no files to backup then we'll exit with a warning unless in test mode. The other way this could happen is if
# the database is down and backup is called with --no-start-stop twice in a row.
if ($lFileTotal == 0)
{
if (!optionGet(OPTION_TEST))
{
confess &log(ERROR, "no files have changed since the last backup - this seems unlikely");
}
return;
}
# Create backup and result queues
my $oResultQueue = Thread::Queue->new();
my @oyBackupQueue;
# Variables used for local copy
my $lSizeCurrent = 0; # Running total of bytes copied
my $bCopied; # Was the file copied?
my $lCopySize; # Size reported by copy
my $strCopyChecksum; # Checksum reported by copy
# Determine how often the manifest will be saved
my $lManifestSaveCurrent = 0;
my $lManifestSaveSize = int($lSizeTotal / 100);
if ($lManifestSaveSize < optionGet(OPTION_MANIFEST_SAVE_THRESHOLD))
{
$lManifestSaveSize = optionGet(OPTION_MANIFEST_SAVE_THRESHOLD);
}
# Iterate all backup files
foreach my $strPathKey (sort (keys %oFileCopyMap))
{
if ($iThreadMax > 1)
{
$oyBackupQueue[@oyBackupQueue] = Thread::Queue->new();
}
foreach my $strFile (sort (keys $oFileCopyMap{$strPathKey}))
{
my $oFileCopy = $oFileCopyMap{$strPathKey}{$strFile};
if ($iThreadMax > 1)
{
$oyBackupQueue[@oyBackupQueue - 1]->enqueue($oFileCopy);
}
else
{
# Backup the file
($bCopied, $lSizeCurrent, $lCopySize, $strCopyChecksum) =
backupFile($oFile, $$oFileCopy{db_file}, $$oFileCopy{backup_file}, $bCompress,
$$oFileCopy{checksum}, $$oFileCopy{modification_time},
$$oFileCopy{size}, $lSizeTotal, $lSizeCurrent);
$lManifestSaveCurrent = backupManifestUpdate($oBackupManifest, $$oFileCopy{file_section}, $$oFileCopy{file},
$bCopied, $lCopySize, $strCopyChecksum, $lManifestSaveSize,
$lManifestSaveCurrent);
}
}
}
# If multi-threaded then create threads to copy files
if ($iThreadMax > 1)
{
for (my $iThreadIdx = 0; $iThreadIdx < $iThreadMax; $iThreadIdx++)
{
my %oParam;
$oParam{compress} = $bCompress;
$oParam{size_total} = $lSizeTotal;
$oParam{queue} = \@oyBackupQueue;
$oParam{result_queue} = $oResultQueue;
threadGroupRun($iThreadIdx, 'backup', \%oParam);
}
# Complete thread queues
my $bDone = false;
do
{
$bDone = threadGroupComplete();
# Read the messages that are passed back from the backup threads
while (my $oMessage = $oResultQueue->dequeue_nb())
{
&log(TRACE, "message received in master queue: section = $$oMessage{file_section}, file = $$oMessage{file}" .
", copied = $$oMessage{copied}");
$lManifestSaveCurrent = backupManifestUpdate($oBackupManifest, $$oMessage{file_section}, $$oMessage{file},
$$oMessage{copied}, $$oMessage{size}, $$oMessage{checksum},
$lManifestSaveSize, $lManifestSaveCurrent);
}
}
while (!$bDone);
}
&log(INFO, file_size_format($lSizeTotal) . ' total backup');
}
####################################################################################################################################
# BACKUP
#
# Performs the entire database backup.
####################################################################################################################################
sub backup
{
my $strDbClusterPath = shift;
my $bStartFast = shift;
# Record timestamp start
my $strTimestampStart = timestamp_string_get();
# Not supporting remote backup hosts yet
if ($oFile->is_remote(PATH_BACKUP))
{
confess &log(ERROR, 'remote backup host not currently supported');
}
if (!defined($strDbClusterPath))
{
confess &log(ERROR, 'cluster data path is not defined');
}
&log(DEBUG, "cluster path is $strDbClusterPath");
# Create the cluster backup path
$oFile->path_create(PATH_BACKUP_CLUSTER, undef, undef, true);
# Build backup tmp and config
my $strBackupTmpPath = $oFile->path_get(PATH_BACKUP_TMP);
my $strBackupConfFile = $oFile->path_get(PATH_BACKUP_TMP, 'backup.manifest');
# Declare the backup manifest
my $oBackupManifest = new BackRest::Manifest($strBackupConfFile, false);
# Find the previous backup based on the type
my $oLastManifest = undef;
my $strBackupLastPath = backup_type_find($strType, $oFile->path_get(PATH_BACKUP_CLUSTER));
if (defined($strBackupLastPath))
{
$oLastManifest = new BackRest::Manifest($oFile->path_get(PATH_BACKUP_CLUSTER) . "/${strBackupLastPath}/backup.manifest");
&log(INFO, 'last backup label = ' . $oLastManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_LABEL) .
', version = ' . $oLastManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_VERSION));
}
else
{
if ($strType eq BACKUP_TYPE_DIFF)
{
&log(WARN, 'No full backup exists, differential backup has been changed to full');
}
elsif ($strType eq BACKUP_TYPE_INCR)
{
&log(WARN, 'No prior backup exists, incremental backup has been changed to full');
}
$strType = BACKUP_TYPE_FULL;
}
# Backup settings
$oBackupManifest->set(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_TYPE, undef, $strType);
$oBackupManifest->set(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_TIMESTAMP_START, undef, $strTimestampStart);
$oBackupManifest->set(MANIFEST_SECTION_BACKUP_OPTION, MANIFEST_KEY_COMPRESS, undef, $bCompress ? 'y' : 'n');
$oBackupManifest->set(MANIFEST_SECTION_BACKUP_OPTION, MANIFEST_KEY_HARDLINK, undef, $bHardLink ? 'y' : 'n');
# Start backup (unless no-start-stop is set)
my $strArchiveStart;
if ($bNoStartStop)
{
if ($oFile->exists(PATH_DB_ABSOLUTE, $strDbClusterPath . '/' . FILE_POSTMASTER_PID))
{
if ($bForce)
{
&log(WARN, '--no-start-stop passed and ' . FILE_POSTMASTER_PID . ' exists but --force was passed so backup will ' .
'continue though it looks like the postmaster is running and the backup will probably not be ' .
'consistent');
}
else
{
&log(ERROR, '--no-start-stop passed but ' . FILE_POSTMASTER_PID . ' exists - looks like the postmaster is ' .
'running. Shutdown the postmaster and try again, or use --force.');
exit 1;
}
}
}
else
{
my $strTimestampDbStart;
($strArchiveStart, $strTimestampDbStart) =
$oDb->backup_start('pg_backrest backup started ' . $strTimestampStart, $bStartFast);
$oBackupManifest->set(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_ARCHIVE_START, undef, $strArchiveStart);
&log(INFO, "archive start: ${strArchiveStart}");
}
$oBackupManifest->set(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_VERSION, undef, version_get());
# Build the backup manifest
my %oTablespaceMap;
if (!$bNoStartStop)
{
$oDb->tablespace_map_get(\%oTablespaceMap);
}
$oBackupManifest->build($oFile, $strDbClusterPath, $oLastManifest, $bNoStartStop, \%oTablespaceMap);
&log(TEST, TEST_MANIFEST_BUILD);
# Check if an aborted backup exists for this stanza
if (-e $strBackupTmpPath)
{
my $bUsable = false;
my $strType = $oBackupManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_TYPE);
my $strPrior = $oBackupManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_PRIOR, undef, false, '<undef>');
my $strVersion = $oBackupManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_VERSION);
my $strAbortedType = '<undef>';
my $strAbortedPrior = '<undef>';
my $strAbortedVersion = '<undef>';
my $oAbortedManifest;
# Attempt to read the manifest file in the aborted backup to see if the backup type and prior backup are the same as the
# new backup that is being started. If any error at all occurs then the backup will be considered unusable and a resume
# will not be attempted.
eval
{
# Load the aborted manifest
$oAbortedManifest = new BackRest::Manifest("${strBackupTmpPath}/backup.manifest");
# Default values if they are not set
$strAbortedType = $oAbortedManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_TYPE);
$strAbortedPrior = $oAbortedManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_PRIOR, undef, false, '<undef>');
$strAbortedVersion = $oAbortedManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_VERSION);
# The backup is usable if between the current backup and the aborted backup:
# 1) The version matches
# 2) The type of both is full or the types match and prior matches
if ($strAbortedVersion eq $strVersion)
{
if ($strAbortedType eq BACKUP_TYPE_FULL
&& $oBackupManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_TYPE) eq BACKUP_TYPE_FULL)
{
$bUsable = true;
}
elsif ($strAbortedType eq $oBackupManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_TYPE) &&
$strAbortedPrior eq $oBackupManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_PRIOR))
{
$bUsable = true;
}
}
};
# If the aborted backup is usable then clean it
if ($bUsable && optionGet(OPTION_RESUME))
{
&log(WARN, 'aborted backup of same type exists, will be cleaned to remove invalid files and resumed');
&log(TEST, TEST_BACKUP_RESUME);
# Clean the old backup tmp path
backup_tmp_clean($oBackupManifest, $oAbortedManifest);
}
# Else remove it
else
{
my $strReason = "resume is disabled";
if (optionGet(OPTION_RESUME))
{
if ($strVersion eq $strAbortedVersion)
{
if ($strType ne $strAbortedType)
{
$strReason = "new type '${strType}' does not match aborted type '${strAbortedType}'";
}
else
{
$strReason = "new prior '${strPrior}' does not match aborted prior '${strAbortedPrior}'";
}
}
else
{
$strReason = "new version '${strVersion}' does not match aborted version '${strVersion}'";
}
}
&log(WARN, "aborted backup exists, but cannot be resumed (${strReason}) - will be dropped and recreated");
&log(TEST, TEST_BACKUP_NORESUME);
remove_tree($oFile->path_get(PATH_BACKUP_TMP))
or confess &log(ERROR, "unable to delete tmp path: ${strBackupTmpPath}");
$oFile->path_create(PATH_BACKUP_TMP);
}
}
# Else create the backup tmp path
else
{
&log(DEBUG, "creating backup path ${strBackupTmpPath}");
$oFile->path_create(PATH_BACKUP_TMP);
}
# Save the backup manifest
$oBackupManifest->save();
# Perform the backup
backup_file($strDbClusterPath, $oBackupManifest);
# Stop backup (unless no-start-stop is set)
my $strArchiveStop;
if (!$bNoStartStop)
{
my $strTimestampDbStop;
($strArchiveStop, $strTimestampDbStop) = $oDb->backup_stop();
$oBackupManifest->set(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_ARCHIVE_STOP, undef, $strArchiveStop);
&log(INFO, 'archive stop: ' . $strArchiveStop);
}
# If archive logs are required to complete the backup, then check them. This is the default, but can be overridden if the
# archive logs are going to a different server. Be careful here because there is no way to verify that the backup will be
# consistent - at least not here.
if (!optionGet(OPTION_NO_START_STOP) && optionGet(OPTION_BACKUP_ARCHIVE_CHECK))
{
# Save the backup manifest a second time - before getting archive logs in case that fails
$oBackupManifest->save();
# Create the modification time for the archive logs
my $lModificationTime = time();
# After the backup has been stopped, need to make a copy of the archive logs need to make the db consistent
&log(DEBUG, "retrieving archive logs ${strArchiveStart}:${strArchiveStop}");
my $oArchive = new BackRest::Archive();
my @stryArchive = $oArchive->range($strArchiveStart, $strArchiveStop, $oDb->db_version_get() < 9.3);
foreach my $strArchive (@stryArchive)
{
my $strArchiveFile = $oArchive->walFileName($oFile, $strArchive, 600);
if (optionGet(OPTION_BACKUP_ARCHIVE_COPY))
{
&log(DEBUG, "archiving: ${strArchive} (${strArchiveFile})");
# Copy the log file from the archive repo to the backup
my $strDestinationFile = "base/pg_xlog/${strArchive}" . ($bCompress ? ".$oFile->{strCompressExtension}" : '');
my ($bCopyResult, $strCopyChecksum, $lCopySize) =
$oFile->copy(PATH_BACKUP_ARCHIVE, $strArchiveFile,
PATH_BACKUP_TMP, $strDestinationFile,
$strArchiveFile =~ "^.*\.$oFile->{strCompressExtension}\$",
$bCompress, undef, $lModificationTime);
# Add the archive file to the manifest so it can be part of the restore and checked in validation
my $strPathSection = 'base:path';
my $strPathLog = 'pg_xlog';
my $strFileSection = 'base:file';
my $strFileLog = "pg_xlog/${strArchive}";
# Compare the checksum against the one already in the archive log name
if ($strArchiveFile !~ "^${strArchive}-${strCopyChecksum}(\\.$oFile->{strCompressExtension}){0,1}\$")
{
confess &log(ERROR, "error copying WAL segment '${strArchiveFile}' to backup - checksum recorded with " .
"file does not match actual checksum of '${strCopyChecksum}'", ERROR_CHECKSUM);
}
# Set manifest values
$oBackupManifest->set($strFileSection, $strFileLog, MANIFEST_SUBKEY_USER,
$oBackupManifest->get($strPathSection, $strPathLog, MANIFEST_SUBKEY_USER));
$oBackupManifest->set($strFileSection, $strFileLog, MANIFEST_SUBKEY_GROUP,
$oBackupManifest->get($strPathSection, $strPathLog, MANIFEST_SUBKEY_GROUP));
$oBackupManifest->set($strFileSection, $strFileLog, MANIFEST_SUBKEY_MODE, '0700');
$oBackupManifest->set($strFileSection, $strFileLog, MANIFEST_SUBKEY_MODIFICATION_TIME, $lModificationTime);
$oBackupManifest->set($strFileSection, $strFileLog, MANIFEST_SUBKEY_SIZE, $lCopySize);
$oBackupManifest->set($strFileSection, $strFileLog, MANIFEST_SUBKEY_CHECKSUM, $strCopyChecksum);
}
}
}
# Create the path for the new backup
my $strBackupPath;
if ($strType eq BACKUP_TYPE_FULL || !defined($strBackupLastPath))
{
$strBackupPath = timestamp_file_string_get() . 'F';
$strType = BACKUP_TYPE_FULL;
}
else
{
$strBackupPath = substr($strBackupLastPath, 0, 16);
$strBackupPath .= '_' . timestamp_file_string_get();
if ($strType eq BACKUP_TYPE_DIFF)
{
$strBackupPath .= 'D';
}
else
{
$strBackupPath .= 'I';
}
}
# Record timestamp stop in the config
$oBackupManifest->set(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_TIMESTAMP_STOP, undef, timestamp_string_get());
$oBackupManifest->set(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_LABEL, undef, $strBackupPath);
# Save the backup manifest final time
$oBackupManifest->save();
&log(INFO, "new backup label: ${strBackupPath}");
# Rename the backup tmp path to complete the backup
&log(DEBUG, "moving ${strBackupTmpPath} to " . $oFile->path_get(PATH_BACKUP_CLUSTER, $strBackupPath));
$oFile->move(PATH_BACKUP_TMP, undef, PATH_BACKUP_CLUSTER, $strBackupPath);
# Create a link to the most recent backup
$oFile->remove(PATH_BACKUP_CLUSTER, "latest");
$oFile->link_create(PATH_BACKUP_CLUSTER, $strBackupPath, PATH_BACKUP_CLUSTER, "latest", undef, true);
}
####################################################################################################################################
# BACKUP_EXPIRE
#
# Removes expired backups and archive logs from the backup directory. Partial backups are not counted for expiration, so if full
# or differential retention is set to 2, there must be three complete backups before the oldest one can be deleted.
#
# iFullRetention - Optional, must be greater than 0 when supplied.
# iDifferentialRetention - Optional, must be greater than 0 when supplied.
# strArchiveRetention - Optional, must be (full,differential/diff,incremental/incr) when supplied
# iArchiveRetention - Required when strArchiveRetention is supplied. Must be greater than 0.
####################################################################################################################################
sub backup_expire
{
my $strBackupClusterPath = shift; # Base path to cluster backup
my $iFullRetention = shift; # Number of full backups to keep
my $iDifferentialRetention = shift; # Number of differential backups to keep
my $strArchiveRetentionType = shift; # Type of backup to base archive retention on
my $iArchiveRetention = shift; # Number of backups worth of archive to keep
my $strPath;
my @stryPath;
# Find all the expired full backups
if (defined($iFullRetention))
{
# Make sure iFullRetention is valid
if (!looks_like_number($iFullRetention) || $iFullRetention < 1)
{
confess &log(ERROR, 'full_rentention must be a number >= 1');
}
my $iIndex = $iFullRetention;
@stryPath = $oFile->list(PATH_BACKUP_CLUSTER, undef, backup_regexp_get(1, 0, 0), 'reverse');
while (defined($stryPath[$iIndex]))
{
# Delete all backups that depend on the full backup. Done in reverse order so that remaining backups will still
# be consistent if the process dies
foreach $strPath ($oFile->list(PATH_BACKUP_CLUSTER, undef, '^' . $stryPath[$iIndex] . '.*', 'reverse'))
{
system("rm -rf ${strBackupClusterPath}/${strPath}") == 0
or confess &log(ERROR, "unable to delete backup ${strPath}");
}
&log(INFO, 'removed expired full backup: ' . $stryPath[$iIndex]);
$iIndex++;
}
}
# Find all the expired differential backups
if (defined($iDifferentialRetention))
{
# Make sure iDifferentialRetention is valid
if (!looks_like_number($iDifferentialRetention) || $iDifferentialRetention < 1)
{
confess &log(ERROR, 'differential_rentention must be a number >= 1');
}
@stryPath = $oFile->list(PATH_BACKUP_CLUSTER, undef, backup_regexp_get(0, 1, 0), 'reverse');
if (defined($stryPath[$iDifferentialRetention - 1]))
{
&log(DEBUG, 'differential expiration based on ' . $stryPath[$iDifferentialRetention - 1]);
# Get a list of all differential and incremental backups
foreach $strPath ($oFile->list(PATH_BACKUP_CLUSTER, undef, backup_regexp_get(0, 1, 1), 'reverse'))
{
&log(DEBUG, "checking ${strPath} for differential expiration");
# Remove all differential and incremental backups before the oldest valid differential
if ($strPath lt $stryPath[$iDifferentialRetention - 1])
{
system("rm -rf ${strBackupClusterPath}/${strPath}") == 0
or confess &log(ERROR, "unable to delete backup ${strPath}");
&log(INFO, "removed expired diff/incr backup ${strPath}");
}
}
}
}
# If no archive retention type is set then exit
if (!defined($strArchiveRetentionType))
{
&log(INFO, 'archive rentention type not set - archive logs will not be expired');
return;
}
# Determine which backup type to use for archive retention (full, differential, incremental)
if ($strArchiveRetentionType eq BACKUP_TYPE_FULL)
{
if (!defined($iArchiveRetention))
{
$iArchiveRetention = $iFullRetention;
}
@stryPath = $oFile->list(PATH_BACKUP_CLUSTER, undef, backup_regexp_get(1, 0, 0), 'reverse');
}
elsif ($strArchiveRetentionType eq BACKUP_TYPE_DIFF)
{
if (!defined($iArchiveRetention))
{
$iArchiveRetention = $iDifferentialRetention;
}
@stryPath = $oFile->list(PATH_BACKUP_CLUSTER, undef, backup_regexp_get(1, 1, 0), 'reverse');
}
elsif ($strArchiveRetentionType eq BACKUP_TYPE_INCR)
{
@stryPath = $oFile->list(PATH_BACKUP_CLUSTER, undef, backup_regexp_get(1, 1, 1), 'reverse');
}
else
{
confess &log(ERROR, "unknown archive_retention_type '${strArchiveRetentionType}'");
}
# Make sure that iArchiveRetention is set and valid
if (!defined($iArchiveRetention))
{
confess &log(ERROR, 'archive_rentention must be set if archive_retention_type is set');
return;
}
if (!looks_like_number($iArchiveRetention) || $iArchiveRetention < 1)
{
confess &log(ERROR, 'archive_rentention must be a number >= 1');
}
# if no backups were found then preserve current archive logs - too scary to delete them!
my $iBackupTotal = scalar @stryPath;
if ($iBackupTotal == 0)
{
return;
}
# See if enough backups exist for expiration to start
my $strArchiveRetentionBackup = $stryPath[$iArchiveRetention - 1];
if (!defined($strArchiveRetentionBackup))
{
if ($strArchiveRetentionType eq BACKUP_TYPE_FULL && scalar @stryPath > 0)
{
&log(INFO, 'fewer than required backups for retention, but since archive_retention_type = full using oldest full backup');
$strArchiveRetentionBackup = $stryPath[scalar @stryPath - 1];
}
if (!defined($strArchiveRetentionBackup))
{
return;
}
}
# Get the archive logs that need to be kept. To be cautious we will keep all the archive logs starting from this backup
# even though they are also in the pg_xlog directory (since they have been copied more than once).
&log(INFO, 'archive retention based on backup ' . $strArchiveRetentionBackup);
my $oManifest = new BackRest::Manifest($oFile->path_get(PATH_BACKUP_CLUSTER) . "/${strArchiveRetentionBackup}/backup.manifest");
my $strArchiveLast = $oManifest->get(MANIFEST_SECTION_BACKUP, MANIFEST_KEY_ARCHIVE_START);
if (!defined($strArchiveLast))
{
confess &log(ERROR, "invalid archive location retrieved ${strArchiveRetentionBackup}");
}
&log(INFO, 'archive retention starts at ' . $strArchiveLast);
# Remove any archive directories or files that are out of date
foreach $strPath ($oFile->list(PATH_BACKUP_ARCHIVE, undef, "^[0-F]{16}\$"))
{
&log(DEBUG, 'found major archive path ' . $strPath);
# If less than first 16 characters of current archive file, then remove the directory
if ($strPath lt substr($strArchiveLast, 0, 16))
{
my $strFullPath = $oFile->path_get(PATH_BACKUP_ARCHIVE) . "/${strPath}";
remove_tree($strFullPath) > 0 or confess &log(ERROR, "unable to remove ${strFullPath}");
&log(DEBUG, 'removed major archive path ' . $strFullPath);
}
# If equals the first 16 characters of the current archive file, then delete individual files instead
elsif ($strPath eq substr($strArchiveLast, 0, 16))
{
my $strSubPath;
# Look for archive files in the archive directory
foreach $strSubPath ($oFile->list(PATH_BACKUP_ARCHIVE, $strPath, "^[0-F]{24}.*\$"))
{
# Delete if the first 24 characters less than the current archive file
if ($strSubPath lt substr($strArchiveLast, 0, 24))
{
unlink($oFile->path_get(PATH_BACKUP_ARCHIVE, $strSubPath))
or confess &log(ERROR, 'unable to remove ' . $strSubPath);
&log(DEBUG, 'removed expired archive file ' . $strSubPath);
}
}
}
}
}
1;