1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2025-03-05 15:05:48 +02:00
David Steele 9be15d00f8 v0.82: Refactoring, Command-line Help, and Minor Bug Fixes
* Fixed an issue where resumed compressed backups were not preserving existing files.

* Fixed an issue where resume and incr/diff would not ensure that the prior backup had the same compression and hardlink settings.

* Fixed an issue where a cold backup using --no-start-stop could be started on a running PostgreSQL cluster without --force specified.

* Fixed an issue where a thread could be started even when none were requested.

* Fixed an issue where the pgBackRest version number was not being updated in backup.info and archive.info after an upgrade/downgrade.

* Fixed an issue where the info command was throwing an exception when the repository contained no stanzas.  Reported by Stephen Frost.

* Fixed an issue where the PostgreSQL pg_stop_backup() NOTICEs were being output to stderr.  Reported by Stephen Frost.

* Renamed recovery-setting option and section to recovery-option to be more consistent with pgBackRest naming conventions.

* Command-line help is now extracted from the same XML source that is used for the other documentation and includes much more detail.

* Code cleanup and refactoring to standardize on patterns that have evolved over time.

* Added dynamic module loading to speed up commands, especially asynchronous archiving.

* Expiration tests are now synthetic rather than based on actual backups.  This will allow development of more advanced expiration features.

* Experimental support for PostgreSQL 9.5 alpha2.  This may break when the control version or WAL magic changes in future versions but will be updated in each pgBackRest release to keep pace.  All regression tests pass except for --target-resume tests (this functionality has changed in 9.5) and there is no testing yet for .partial WAL segments.
2015-09-14 11:18:50 -04:00

690 lines
23 KiB
Perl

####################################################################################################################################
# DB MODULE
####################################################################################################################################
package BackRest::Db;
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);
use DBD::Pg ':async';
use DBI;
use Exporter qw(import);
our @EXPORT = qw();
use Fcntl qw(O_RDONLY);
use File::Basename qw(dirname);
use lib dirname($0);
use BackRest::Common::Exception;
use BackRest::Common::Log;
use BackRest::Common::String;
use BackRest::Common::Wait;
use BackRest::Config::Config;
use BackRest::File;
####################################################################################################################################
# Operation constants
####################################################################################################################################
use constant OP_DB => 'Db';
use constant OP_DB_NEW => OP_DB . "->new";
use constant OP_DB_BACKUP_START => OP_DB . "->backupStart";
use constant OP_DB_BACKUP_STOP => OP_DB . "->backupStop";
use constant OP_DB_DESTROY => OP_DB . "->DESTROY";
use constant OP_DB_EXECUTE_SQL => OP_DB . "->executeSql";
push @EXPORT, qw(OP_DB_EXECUTE_SQL);
use constant OP_DB_EXECUTE_SQL_ONE => OP_DB . "->executeSqlOne";
use constant OP_DB_EXECUTE_SQL_ROW => OP_DB . "->executeSqlRow";
use constant OP_DB_INFO => OP_DB . "->info";
push @EXPORT, qw(OP_DB_INFO);
use constant OP_DB_TABLESPACE_MAP_GET => OP_DB . "->tablespaceMapGet";
use constant OP_DB_VERSION_GET => OP_DB . "->versionGet";
use constant OP_DB_VERSION_SUPPORT => OP_DB . "->versionSupport";
####################################################################################################################################
# Postmaster process Id file
####################################################################################################################################
use constant FILE_POSTMASTER_PID => 'postmaster.pid';
push @EXPORT, qw(FILE_POSTMASTER_PID);
####################################################################################################################################
# Backup advisory lock
####################################################################################################################################
use constant DB_BACKUP_ADVISORY_LOCK => '12340078987004321';
push @EXPORT, qw(DB_BACKUP_ADVISORY_LOCK);
####################################################################################################################################
# CONSTRUCTOR
####################################################################################################################################
sub new
{
my $class = shift; # Class name
# Create the class hash
my $self = {};
bless $self, $class;
# Assign function parameters, defaults, and log debug info
(
my $strOperation
) =
logDebugParam
(
OP_DB_NEW
);
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'self', value => $self}
);
}
####################################################################################################################################
# DESTRUCTOR
####################################################################################################################################
sub DESTROY
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation
) =
logDebugParam
(
OP_DB_DESTROY
);
if (defined($self->{hDb}))
{
$self->{hDb}->disconnect();
undef($self->{hDb});
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation
);
}
####################################################################################################################################
# versionSupport
#
# Returns an array of the supported Postgres versions.
####################################################################################################################################
sub versionSupport
{
# Assign function parameters, defaults, and log debug info
my
(
$strOperation
) =
logDebugParam
(
OP_DB_VERSION_SUPPORT
);
my @strySupportVersion = ('8.3', '8.4', '9.0', '9.1', '9.2', '9.3', '9.4', '9.5');
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strySupportVersion', value => \@strySupportVersion}
);
}
push @EXPORT, qw(versionSupport);
####################################################################################################################################
# executeSql
####################################################################################################################################
sub executeSql
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strSql,
$bIgnoreError
) =
logDebugParam
(
OP_DB_EXECUTE_SQL, \@_,
{name => 'strSql'},
{name => 'bIgnoreError', default => false}
);
# Get the user-defined command for psql
my $strResult;
# Run remotely
if (optionRemoteTypeTest(DB))
{
# Build param hash
my %oParamHash;
$oParamHash{'script'} = $strSql;
$oParamHash{'ignore-error'} = $bIgnoreError;
# Execute the command
$strResult = protocolGet()->cmdExecute(OP_DB_EXECUTE_SQL, \%oParamHash, true);
}
# Else run locally
else
{
if (!defined($self->{hDb}))
{
# Connect to the db
my $strDbName = 'postgres';
my $strDbUser = getpwuid($<);
my $strDbSocketPath = optionGet(OPTION_DB_SOCKET_PATH, false);
if (defined($strDbSocketPath) && $strDbSocketPath !~ /^\//)
{
confess &log(ERROR, "'${strDbSocketPath}' is not valid for '" . OPTION_DB_SOCKET_PATH . "' option:" .
" path must be absolute", ERROR_OPTION_INVALID_VALUE);
}
my $strDbUri = "dbi:Pg:dbname=${strDbName};port=" . optionGet(OPTION_DB_PORT) .
(optionTest(OPTION_DB_SOCKET_PATH) ? ';host=' . optionGet(OPTION_DB_SOCKET_PATH) : '');
logDebugMisc
(
$strOperation, 'db connect',
{name => 'strDbUri', value => $strDbUri},
{name => 'strDbUser', value => $strDbUser}
);
$self->{hDb} = DBI->connect($strDbUri, $strDbUser, undef,
{AutoCommit => 1, RaiseError => 0, PrintError => 0, Warn => 0});
if (!$self->{hDb})
{
confess &log(ERROR, $DBI::errstr, ERROR_DB_CONNECT);
}
}
# Prepare the query
my $hStatement = $self->{hDb}->prepare($strSql, {pg_async => PG_ASYNC})
or confess &log(ERROR, $DBI::errstr, ERROR_DB_QUERY);
# Execute the query
$hStatement->execute();
# Wait for the query to return
my $oWait = waitInit(optionGet(OPTION_DB_TIMEOUT));
my $bTimeout = true;
do
{
# Is the statement done?
if ($hStatement->pg_ready())
{
if (!$hStatement->pg_result())
{
# Return if the error should be ignored
if ($bIgnoreError)
{
return '';
}
# Else report it
confess &log(ERROR, $DBI::errstr . ":\n${strSql}", ERROR_DB_QUERY);
}
# Get rows and return them
my @stryArray;
do
{
@stryArray = $hStatement->fetchrow_array;
if (!@stryArray && $hStatement->err)
{
confess &log(ERROR, $DBI::errstr . ":\n${strSql}", ERROR_DB_QUERY);
}
$strResult = (defined($strResult) ? "${strResult}\n" : '') . join("\t", @stryArray);
}
while (@stryArray);
$bTimeout = false;
}
} while ($bTimeout && waitMore($oWait));
# If timeout then cancel the query and confess
if ($bTimeout)
{
$hStatement->pg_cancel();
confess &log(ERROR, 'statement timed out after ' . waitInterval($oWait) .
" second(s):\n${strSql}", ERROR_DB_TIMEOUT);
}
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strResult', value => $strResult}
);
}
####################################################################################################################################
# executeSqlRow
####################################################################################################################################
sub executeSqlRow
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strSql
) =
logDebugParam
(
OP_DB_EXECUTE_SQL_ROW, \@_,
{name => 'strSql', trace => true}
);
# Return from function and log return values if any
my @stryResult = split("\t", trim($self->executeSql($strSql)));
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strResult', value => \@stryResult}
);
}
####################################################################################################################################
# executeSqlOne
####################################################################################################################################
sub executeSqlOne
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$strSql
) =
logDebugParam
(
OP_DB_EXECUTE_SQL_ONE, \@_,
{name => 'strSql', trace => true}
);
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strResult', value => ($self->executeSqlRow($strSql))[0], trace => true}
);
}
####################################################################################################################################
# tablespaceMapGet
#
# Get the mapping between oid and tablespace name.
####################################################################################################################################
sub tablespaceMapGet
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation
) =
logDebugParam
(
OP_DB_TABLESPACE_MAP_GET
);
dataHashBuild(my $oTablespaceMapRef = {}, "oid\tname\n" . $self->executeSql(
'select oid, spcname from pg_tablespace'), "\t");
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'oTablespaceMapRef', value => $oTablespaceMapRef}
);
}
####################################################################################################################################
# info
####################################################################################################################################
sub info
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$oFile,
$strDbPath
) =
logDebugParam
(
OP_DB_INFO, \@_,
{name => 'oFile'},
{name => 'strDbPath'}
);
# Return data from the cache if it exists
if (defined($self->{info}{$strDbPath}))
{
return $self->{info}{$strDbPath}{fDbVersion},
$self->{info}{$strDbPath}{iControlVersion},
$self->{info}{$strDbPath}{iCatalogVersion},
$self->{info}{$strDbPath}{ullDbSysId};
}
# Database info
my $iCatalogVersion;
my $iControlVersion;
my $ullDbSysId;
my $fDbVersion;
if ($oFile->isRemote(PATH_DB_ABSOLUTE))
{
# Build param hash
my %oParamHash;
$oParamHash{'db-path'} = ${strDbPath};
# Output remote trace info
&log(TRACE, OP_DB_INFO . ": remote (" . $oFile->{oProtocol}->commandParamString(\%oParamHash) . ')');
# Execute the command
my $strResult = $oFile->{oProtocol}->cmdExecute(OP_DB_INFO, \%oParamHash, true);
# Split the result into return values
my @stryToken = split(/\t/, $strResult);
$fDbVersion = $stryToken[0];
$iControlVersion = $stryToken[1];
$iCatalogVersion = $stryToken[2];
$ullDbSysId = $stryToken[3];
}
else
{
# Open the control file
my $strControlFile = "${strDbPath}/global/pg_control";
my $hFile;
my $tBlock;
sysopen($hFile, $strControlFile, O_RDONLY)
or confess &log(ERROR, "unable to open ${strControlFile}", ERROR_FILE_OPEN);
# Read system identifier
sysread($hFile, $tBlock, 8) == 8
or confess &log(ERROR, "unable to read database system identifier");
$ullDbSysId = unpack('Q', $tBlock);
# Read control version
sysread($hFile, $tBlock, 4) == 4
or confess &log(ERROR, "unable to read control version");
$iControlVersion = unpack('L', $tBlock);
# Read catalog version
sysread($hFile, $tBlock, 4) == 4
or confess &log(ERROR, "unable to read catalog version");
$iCatalogVersion = unpack('L', $tBlock);
# Close the control file
close($hFile);
# Make sure the control version is supported
if ($iControlVersion == 942 && $iCatalogVersion == 201409291)
{
$fDbVersion = '9.4';
}
# Leave 9.5 catalog version out until it stabilizes (then move 9.5 to the top of the list)
elsif ($iControlVersion == 942) # && $iCatalogVersion == 201505311)
{
$fDbVersion = '9.5';
}
elsif ($iControlVersion == 937 && $iCatalogVersion == 201306121)
{
$fDbVersion = '9.3';
}
elsif ($iControlVersion == 922 && $iCatalogVersion == 201204301)
{
$fDbVersion = '9.2';
}
elsif ($iControlVersion == 903 && $iCatalogVersion == 201105231)
{
$fDbVersion = '9.1';
}
elsif ($iControlVersion == 903 && $iCatalogVersion == 201008051)
{
$fDbVersion = '9.0';
}
elsif ($iControlVersion == 843 && $iCatalogVersion == 200904091)
{
$fDbVersion = '8.4';
}
elsif ($iControlVersion == 833 && $iCatalogVersion == 200711281)
{
$fDbVersion = '8.3';
}
elsif ($iControlVersion == 822 && $iCatalogVersion == 200611241)
{
$fDbVersion = '8.2';
}
elsif ($iControlVersion == 812 && $iCatalogVersion == 200510211)
{
$fDbVersion = '8.1';
}
elsif ($iControlVersion == 74 && $iCatalogVersion == 200411041)
{
$fDbVersion = '8.0';
}
else
{
confess &log(ERROR, "unexpected control version = ${iControlVersion} and catalog version = ${iCatalogVersion}" .
' (unsupported PostgreSQL version?)',
ERROR_VERSION_NOT_SUPPORTED);
}
}
# Store data in the cache
$self->{info}{$strDbPath}{fDbVersion} = $fDbVersion;
$self->{info}{$strDbPath}{iControlVersion} = $iControlVersion;
$self->{info}{$strDbPath}{iCatalogVersion} = $iCatalogVersion;
$self->{info}{$strDbPath}{ullDbSysId} = $ullDbSysId;
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'fDbVersion', value => $fDbVersion},
{name => 'iControlVersion', value => $iControlVersion},
{name => 'iCatalogVersion', value => $iCatalogVersion},
{name => 'ullDbSysId', value => $ullDbSysId}
);
}
####################################################################################################################################
# versionGet
####################################################################################################################################
sub versionGet
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation
) =
logDebugParam
(
OP_DB_VERSION_GET
);
# Get data from the cache if possible
if (defined($self->{fDbVersion}) && defined($self->{strDbPath}))
{
return $self->{fDbVersion}, $self->{strDbPath};
}
# Get version and db-path from
($self->{fDbVersion}, $self->{strDbPath}) =
$self->executeSqlRow("select (regexp_matches(split_part(version(), ' ', 2), '^[0-9]+\.[0-9]+'))[1], setting" .
" from pg_settings where name = 'data_directory'");
my @stryVersionSupport = versionSupport();
if ($self->{fDbVersion} < $stryVersionSupport[0])
{
confess &log(ERROR, 'unsupported Postgres version' . $self->{fDbVersion}, ERROR_VERSION_NOT_SUPPORTED);
}
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'fDbVersion', value => $self->{fDbVersion}},
{name => 'strDbPath', value => $self->{strDbPath}}
);
}
####################################################################################################################################
# backupStart
####################################################################################################################################
sub backupStart
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation,
$oFile,
$strDbPath,
$strLabel,
$bStartFast
) =
logDebugParam
(
OP_DB_BACKUP_START, \@_,
{name => 'oFile'},
{name => 'strDbPath'},
{name => 'strLabel'},
{name => 'bStartFast'}
);
# Get the version from the control file
my ($fDbVersion) = $self->info($oFile, $strDbPath);
# Get version and db path from the database
my ($fCompareDbVersion, $strCompareDbPath) = $self->versionGet();
# Error if they are not identical
if (!($fDbVersion == $fCompareDbVersion && $strDbPath eq $strCompareDbPath))
{
confess &log(ERROR, "version '${fCompareDbVersion}' and db-path '${strCompareDbPath}' queried from cluster does not match" .
" version '${fDbVersion}' and db-path '${strDbPath}' read from '${strDbPath}/global/pg_control'\n" .
"HINT: the db-path and db-port settings likely reference different clusters", ERROR_DB_MISMATCH);
}
# Only allow start-fast option for version >= 8.4
if ($self->{fDbVersion} < 8.4 && $bStartFast)
{
&log(WARN, OPTION_START_FAST . ' option is only available in PostgreSQL >= 8.4');
$bStartFast = false;
}
# Acquire the backup advisory lock
if (!$self->executeSqlOne('select pg_try_advisory_lock(' . DB_BACKUP_ADVISORY_LOCK . ')'))
{
confess &log(ERROR, "unable to acquire backup lock\n" .
'HINT: is another backup already running on this cluster?', ERROR_LOCK_ACQUIRE);
}
# Test if a backup is already running when version >= 9.3
if (optionGet(OPTION_STOP_AUTO))
{
if ($self->{fDbVersion} >= 9.3)
{
if ($self->executeSqlOne('select pg_is_in_backup()'))
{
&log(WARN, 'the cluster is already in backup mode but no backup process is running. pg_stop_backup() will be called' .
' so a new backup can be started.');
$self->backupStop();
}
}
else
{
&log(WARN, OPTION_STOP_AUTO . 'option is only available in PostgreSQL >= 9.3');
}
}
&log(INFO, "execute pg_start_backup() with label \"${strLabel}\": backup begins after " .
($bStartFast ? "the requested immediate checkpoint" : "the next regular checkpoint") . " completes");
my ($strTimestampDbStart, $strArchiveStart) =
$self->executeSqlRow("select to_char(current_timestamp, 'YYYY-MM-DD HH24:MI:SS.US TZ'), " .
"pg_xlogfile_name(xlog) from pg_start_backup('${strLabel}'" .
($bStartFast ? ', true' : '') . ') as xlog');
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strArchiveStart', value => $strArchiveStart},
{name => 'strTimestampDbStart', value => $strTimestampDbStart}
);
}
####################################################################################################################################
# backupStop
####################################################################################################################################
sub backupStop
{
my $self = shift;
# Assign function parameters, defaults, and log debug info
my
(
$strOperation
) =
logDebugParam
(
OP_DB_BACKUP_STOP
);
&log(INFO, 'execute pg_stop_backup() and wait for all WAL segments to archive');
my ($strTimestampDbStop, $strArchiveStop) =
$self->executeSqlRow("select to_char(clock_timestamp(), 'YYYY-MM-DD HH24:MI:SS.US TZ')," .
" pg_xlogfile_name(xlog) from pg_stop_backup() as xlog");
# Return from function and log return values if any
return logDebugReturn
(
$strOperation,
{name => 'strArchiveStop', value => $strArchiveStop},
{name => 'strTimestampDbStop', value => $strTimestampDbStop}
);
}
1;