mirror of
https://github.com/pgbackrest/pgbackrest.git
synced 2024-12-14 10:13:05 +02:00
6e88f93991
The check to verify that pg-path and data_directory are equal was not working because pg-path was getting overwritten with data_directory before validation took place. Reported by James Chanco Jr.
1195 lines
44 KiB
Perl
1195 lines
44 KiB
Perl
####################################################################################################################################
|
|
# DB MODULE
|
|
####################################################################################################################################
|
|
package pgBackRest::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 pgBackRest::DbVersion;
|
|
use pgBackRest::Common::Exception;
|
|
use pgBackRest::Common::Log;
|
|
use pgBackRest::Common::String;
|
|
use pgBackRest::Common::Wait;
|
|
use pgBackRest::Config::Config;
|
|
use pgBackRest::Manifest;
|
|
use pgBackRest::Protocol::Helper;
|
|
use pgBackRest::Protocol::Storage::Helper;
|
|
use pgBackRest::Version;
|
|
|
|
####################################################################################################################################
|
|
# PostgreSQL 8.3 WAL size
|
|
#
|
|
# WAL segment size in 8.3 cannot be determined from pg_control, so use this constant instead.
|
|
####################################################################################################################################
|
|
use constant PG_WAL_SIZE_83 => 16777216;
|
|
|
|
####################################################################################################################################
|
|
# Backup advisory lock
|
|
####################################################################################################################################
|
|
use constant DB_BACKUP_ADVISORY_LOCK => '12340078987004321';
|
|
push @EXPORT, qw(DB_BACKUP_ADVISORY_LOCK);
|
|
|
|
####################################################################################################################################
|
|
# Map the control and catalog versions to PostgreSQL version.
|
|
#
|
|
# The control version can be found in src/include/catalog/pg_control.h and may not change with every version of PostgreSQL but is
|
|
# still checked to detect custom builds which change the structure. The catalog version can be found in
|
|
# src/include/catalog/catversion.h and should change with every release.
|
|
####################################################################################################################################
|
|
my $oPgControlVersionHash =
|
|
{
|
|
# iControlVersion => {iCatalogVersion => strDbVersion}
|
|
833 => {200711281 => PG_VERSION_83},
|
|
843 => {200904091 => PG_VERSION_84},
|
|
903 =>
|
|
{
|
|
201008051 => PG_VERSION_90,
|
|
201105231 => PG_VERSION_91,
|
|
},
|
|
922 => {201204301 => PG_VERSION_92},
|
|
937 => {201306121 => PG_VERSION_93},
|
|
942 =>
|
|
{
|
|
201409291 => PG_VERSION_94,
|
|
201510051 => PG_VERSION_95,
|
|
},
|
|
960 =>
|
|
{
|
|
201608131 => PG_VERSION_96,
|
|
},
|
|
1002 =>
|
|
{
|
|
201707211 => PG_VERSION_10,
|
|
},
|
|
1100 =>
|
|
{
|
|
201809051 => PG_VERSION_11,
|
|
},
|
|
};
|
|
|
|
####################################################################################################################################
|
|
# 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,
|
|
$self->{iRemoteIdx},
|
|
) =
|
|
logDebugParam
|
|
(
|
|
__PACKAGE__ . '->new', \@_,
|
|
{name => 'iRemoteIdx', required => false},
|
|
);
|
|
|
|
if (defined($self->{iRemoteIdx}))
|
|
{
|
|
$self->{strDbPath} = cfgOption(cfgOptionIdFromIndex(CFGOPT_PG_PATH, $self->{iRemoteIdx}));
|
|
|
|
if (!isDbLocal({iRemoteIdx => $self->{iRemoteIdx}}))
|
|
{
|
|
$self->{oProtocol} = protocolGet(CFGOPTVAL_REMOTE_TYPE_DB, $self->{iRemoteIdx});
|
|
}
|
|
}
|
|
|
|
# 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(__PACKAGE__ . '->DESTROY');
|
|
|
|
if (defined($self->{hDb}))
|
|
{
|
|
$self->{hDb}->disconnect();
|
|
undef($self->{hDb});
|
|
}
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn($strOperation);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# connect
|
|
####################################################################################################################################
|
|
sub connect
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my
|
|
(
|
|
$strOperation,
|
|
$bWarnOnError,
|
|
) =
|
|
logDebugParam
|
|
(
|
|
__PACKAGE__ . '::connect', \@_,
|
|
{name => 'bWarnOnError', default => false},
|
|
);
|
|
|
|
# Only connect if not already connected
|
|
my $bResult = true;
|
|
|
|
# Run remotely
|
|
if (defined($self->{oProtocol}))
|
|
{
|
|
# Set bResult to false if undef is returned
|
|
$bResult = $self->{oProtocol}->cmdExecute(OP_DB_CONNECT, undef, false, $bWarnOnError) ? true : false;
|
|
}
|
|
# Else run locally
|
|
else
|
|
{
|
|
if (!defined($self->{hDb}))
|
|
{
|
|
# Connect to the db
|
|
my $strDbName = 'postgres';
|
|
my $strDbUser = getpwuid($<);
|
|
my $strDbSocketPath = cfgOption(cfgOptionIdFromIndex(CFGOPT_PG_SOCKET_PATH, $self->{iRemoteIdx}), false);
|
|
|
|
# Make sure the socket path is absolute
|
|
if (defined($strDbSocketPath) && $strDbSocketPath !~ /^\//)
|
|
{
|
|
confess &log(ERROR, "'${strDbSocketPath}' is not valid for '" . cfgOptionName(CFGOPT_PG_SOCKET_PATH) . "' option:" .
|
|
" path must be absolute", ERROR_OPTION_INVALID_VALUE);
|
|
}
|
|
|
|
# Construct the URI
|
|
my $strDbUri =
|
|
"dbi:Pg:dbname=${strDbName};port=" . cfgOption(cfgOptionIdFromIndex(CFGOPT_PG_PORT, $self->{iRemoteIdx})) .
|
|
(defined($strDbSocketPath) ? ";host=${strDbSocketPath}" : '');
|
|
|
|
logDebugMisc
|
|
(
|
|
$strOperation, undef,
|
|
{name => 'strDbUri', value => $strDbUri},
|
|
{name => 'strDbUser', value => $strDbUser}
|
|
);
|
|
|
|
$self->{hDb} = DBI->connect($strDbUri, $strDbUser, undef,
|
|
{AutoCommit => 1, RaiseError => 0, PrintError => 0, Warn => 0});
|
|
|
|
# If db handle is not valid then check error
|
|
if (!$self->{hDb})
|
|
{
|
|
# Throw an error unless a warning was requested
|
|
if (!$bWarnOnError)
|
|
{
|
|
confess &log(ERROR, $DBI::errstr, ERROR_DB_CONNECT);
|
|
}
|
|
|
|
# Log a warning
|
|
&log(WARN, $DBI::errstr);
|
|
|
|
$bResult = false;
|
|
undef($self->{hDb});
|
|
}
|
|
else
|
|
{
|
|
my ($fDbVersion) = $self->versionGet();
|
|
|
|
if ($fDbVersion >= PG_VERSION_APPLICATION_NAME)
|
|
{
|
|
# Set application name for monitoring and debugging
|
|
$self->{hDb}->do(
|
|
"set application_name = '" . PROJECT_NAME . ' [' .
|
|
(cfgOptionValid(CFGOPT_COMMAND) ? cfgOption(CFGOPT_COMMAND) : cfgCommandName(cfgCommandGet())) . "]'")
|
|
or confess &log(ERROR, $self->{hDb}->errstr, ERROR_DB_QUERY);
|
|
|
|
# Clear search path to prevent possible function overrides
|
|
$self->{hDb}->do("set search_path = 'pg_catalog'")
|
|
or confess &log(ERROR, $self->{hDb}->errstr, ERROR_DB_QUERY);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'bResult', value => $bResult}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# executeSql
|
|
####################################################################################################################################
|
|
sub executeSql
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my
|
|
(
|
|
$strOperation,
|
|
$strSql,
|
|
$bIgnoreError,
|
|
$bResult,
|
|
) =
|
|
logDebugParam
|
|
(
|
|
__PACKAGE__ . '::executeSql', \@_,
|
|
{name => 'strSql'},
|
|
{name => 'bIgnoreError', default => false},
|
|
{name => 'bResult', default => true},
|
|
);
|
|
|
|
# Get the user-defined command for psql
|
|
my @stryResult;
|
|
|
|
# Run remotely
|
|
if (defined($self->{oProtocol}))
|
|
{
|
|
# Execute the command
|
|
@stryResult = @{$self->{oProtocol}->cmdExecute(OP_DB_EXECUTE_SQL, [$strSql, $bIgnoreError, $bResult], $bResult)};
|
|
}
|
|
# Else run locally
|
|
else
|
|
{
|
|
$self->connect();
|
|
|
|
# Prepare the query
|
|
my $hStatement = $self->{hDb}->prepare($strSql, {pg_async => PG_ASYNC})
|
|
or confess &log(ERROR, $DBI::errstr . ":\n${strSql}", ERROR_DB_QUERY);
|
|
|
|
# Execute the query
|
|
$hStatement->execute()
|
|
or confess &log(ERROR, $DBI::errstr. ":\n${strSql}", ERROR_DB_QUERY);
|
|
|
|
# Wait for the query to return
|
|
my $oWait = waitInit(cfgOption(CFGOPT_DB_TIMEOUT));
|
|
my $bTimeout = true;
|
|
|
|
do
|
|
{
|
|
# Is the statement done?
|
|
if ($hStatement->pg_ready())
|
|
{
|
|
# return now if there is no result expected
|
|
if (!$bResult)
|
|
{
|
|
return \@stryResult;
|
|
}
|
|
|
|
if (!$hStatement->pg_result())
|
|
{
|
|
# Return if the error should be ignored
|
|
if ($bIgnoreError)
|
|
{
|
|
return \@stryResult;
|
|
}
|
|
|
|
# Else report it
|
|
confess &log(ERROR, $DBI::errstr . ":\n${strSql}", ERROR_DB_QUERY);
|
|
}
|
|
|
|
# Get rows and return them
|
|
my @stryRow;
|
|
|
|
do
|
|
{
|
|
# Get next row
|
|
@stryRow = $hStatement->fetchrow_array;
|
|
|
|
# If the row has data then add it to the result
|
|
if (@stryRow)
|
|
{
|
|
push(@{$stryResult[@stryResult]}, @stryRow);
|
|
}
|
|
# Else check for error
|
|
elsif ($hStatement->err)
|
|
{
|
|
confess &log(ERROR, $DBI::errstr . ":\n${strSql}", ERROR_DB_QUERY);
|
|
}
|
|
}
|
|
while (@stryRow);
|
|
|
|
$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 => 'stryResult', value => \@stryResult, ref => true}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# executeSqlRow
|
|
####################################################################################################################################
|
|
sub executeSqlRow
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my
|
|
(
|
|
$strOperation,
|
|
$strSql
|
|
) =
|
|
logDebugParam
|
|
(
|
|
__PACKAGE__ . '->executeSqlRow', \@_,
|
|
{name => 'strSql'}
|
|
);
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'stryResult', value => @{$self->executeSql($strSql)}[0]}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# executeSqlOne
|
|
####################################################################################################################################
|
|
sub executeSqlOne
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my
|
|
(
|
|
$strOperation,
|
|
$strSql
|
|
) =
|
|
logDebugParam
|
|
(
|
|
__PACKAGE__ . '->executeSqlOne', \@_,
|
|
{name => 'strSql'}
|
|
);
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'strResult', value => @{@{$self->executeSql($strSql)}[0]}[0]}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# 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(__PACKAGE__ . '->tablespaceMapGet');
|
|
|
|
my $hTablespaceMap = {};
|
|
|
|
for my $strRow (@{$self->executeSql('select oid, spcname from pg_tablespace')})
|
|
{
|
|
$hTablespaceMap->{@{$strRow}[0]} = @{$strRow}[1];
|
|
}
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'hTablespaceMap', value => $hTablespaceMap}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# databaseMapGet
|
|
#
|
|
# Get the mapping between oid and database name.
|
|
####################################################################################################################################
|
|
sub databaseMapGet
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my ($strOperation) = logDebugParam(__PACKAGE__ . '->databaseMapGet');
|
|
|
|
my $hDatabaseMap = {};
|
|
|
|
for my $strRow (@{$self->executeSql('select datname, oid, datlastsysoid from pg_database')})
|
|
{
|
|
$hDatabaseMap->{@{$strRow}[0]}{&MANIFEST_KEY_DB_ID} = @{$strRow}[1];
|
|
$hDatabaseMap->{@{$strRow}[0]}{&MANIFEST_KEY_DB_LAST_SYSTEM_ID} = @{$strRow}[2];
|
|
}
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'hDatabaseMap', value => $hDatabaseMap}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# info
|
|
####################################################################################################################################
|
|
sub info
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my
|
|
(
|
|
$strOperation,
|
|
$strDbPath
|
|
) =
|
|
logDebugParam
|
|
(
|
|
__PACKAGE__ . '->info', \@_,
|
|
{name => 'strDbPath', default => $self->{strDbPath}}
|
|
);
|
|
|
|
# Get info if it is not cached
|
|
#-------------------------------------------------------------------------------------------------------------------------------
|
|
if (!defined($self->{info}{$strDbPath}))
|
|
{
|
|
# Get info from remote
|
|
#---------------------------------------------------------------------------------------------------------------------------
|
|
if (defined($self->{oProtocol}))
|
|
{
|
|
# Execute the command
|
|
($self->{info}{$strDbPath}{strDbVersion}, $self->{info}{$strDbPath}{iDbControlVersion},
|
|
$self->{info}{$strDbPath}{iDbCatalogVersion}, $self->{info}{$strDbPath}{ullDbSysId}) =
|
|
$self->{oProtocol}->cmdExecute(OP_DB_INFO, [$strDbPath], true);
|
|
}
|
|
# Get info locally
|
|
#---------------------------------------------------------------------------------------------------------------------------
|
|
else
|
|
{
|
|
# Open the control file and read system id and versions
|
|
#-----------------------------------------------------------------------------------------------------------------------
|
|
my $strControlFile = "${strDbPath}/" . DB_FILE_PGCONTROL;
|
|
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");
|
|
|
|
$self->{info}{$strDbPath}{ullDbSysId} = unpack('Q', $tBlock);
|
|
|
|
# Read control version
|
|
sysread($hFile, $tBlock, 4) == 4
|
|
or confess &log(ERROR, "unable to read control version");
|
|
|
|
$self->{info}{$strDbPath}{iDbControlVersion} = unpack('L', $tBlock);
|
|
|
|
# Read catalog version
|
|
sysread($hFile, $tBlock, 4) == 4
|
|
or confess &log(ERROR, "unable to read catalog version");
|
|
|
|
$self->{info}{$strDbPath}{iDbCatalogVersion} = unpack('L', $tBlock);
|
|
|
|
# Close the control file
|
|
close($hFile);
|
|
|
|
# Get PostgreSQL version
|
|
$self->{info}{$strDbPath}{strDbVersion} =
|
|
$oPgControlVersionHash->{$self->{info}{$strDbPath}{iDbControlVersion}}
|
|
{$self->{info}{$strDbPath}{iDbCatalogVersion}};
|
|
|
|
if (!defined($self->{info}{$strDbPath}{strDbVersion}))
|
|
{
|
|
confess &log(
|
|
ERROR,
|
|
'unexpected control version = ' . $self->{info}{$strDbPath}{iDbControlVersion} .
|
|
' and catalog version = ' . $self->{info}{$strDbPath}{iDbCatalogVersion} . "\n" .
|
|
'HINT: is this version of PostgreSQL supported?',
|
|
ERROR_VERSION_NOT_SUPPORTED);
|
|
}
|
|
}
|
|
}
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'strDbVersion', value => $self->{info}{$strDbPath}{strDbVersion}},
|
|
{name => 'iDbControlVersion', value => $self->{info}{$strDbPath}{iDbControlVersion}},
|
|
{name => 'iDbCatalogVersion', value => $self->{info}{$strDbPath}{iDbCatalogVersion}},
|
|
{name => 'ullDbSysId', value => $self->{info}{$strDbPath}{ullDbSysId}}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# versionGet
|
|
####################################################################################################################################
|
|
sub versionGet
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my ($strOperation) = logDebugParam(__PACKAGE__ . '->versionGet');
|
|
|
|
# Get data from the cache if possible
|
|
if (defined($self->{strDbVersion}) && defined($self->{strDbPath}))
|
|
{
|
|
return $self->{strDbVersion}, $self->{strDbPath};
|
|
}
|
|
|
|
# Get version and pg-path from
|
|
my ($strVersionNum, $strDbPath) =
|
|
$self->executeSqlRow(
|
|
"select (select setting from pg_settings where name = 'server_version_num'), " .
|
|
" (select setting from pg_settings where name = 'data_directory')");
|
|
|
|
# Get first part of the major version - for 10 and above there will only be one part
|
|
$self->{strDbVersion} = substr($strVersionNum, 0, length($strVersionNum) - 4);
|
|
|
|
# Now retrieve the second part of the major version for versions less than 10
|
|
if ($self->{strDbVersion} < PG_VERSION_10)
|
|
{
|
|
$self->{strDbVersion} .= qw{.} . int(substr($strVersionNum, 1, 2));
|
|
}
|
|
|
|
# Check that the version is supported
|
|
my @stryVersionSupport = versionSupport();
|
|
|
|
if ($self->{strDbVersion} < $stryVersionSupport[0])
|
|
{
|
|
confess &log(ERROR, 'unsupported Postgres version' . $self->{strDbVersion}, ERROR_VERSION_NOT_SUPPORTED);
|
|
}
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'strDbVersion', value => $self->{strDbVersion}},
|
|
{name => 'strDbPath', value => $strDbPath}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# backupStart
|
|
####################################################################################################################################
|
|
sub backupStart
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my
|
|
(
|
|
$strOperation,
|
|
$strLabel,
|
|
$bStartFast
|
|
) =
|
|
logDebugParam
|
|
(
|
|
__PACKAGE__ . '->backupStart', \@_,
|
|
{name => 'strLabel'},
|
|
{name => 'bStartFast'}
|
|
);
|
|
|
|
# Validate the database configuration
|
|
$self->configValidate();
|
|
|
|
# Only allow start-fast option for version >= 8.4
|
|
if ($self->{strDbVersion} < PG_VERSION_84 && $bStartFast)
|
|
{
|
|
&log(WARN, cfgOptionName(CFGOPT_START_FAST) . ' option is only available in PostgreSQL >= ' . PG_VERSION_84);
|
|
$bStartFast = false;
|
|
}
|
|
|
|
# Determine if page checksums can be enabled
|
|
my $bChecksumPage =
|
|
$self->executeSqlOne("select count(*) = 1 from pg_settings where name = 'data_checksums' and setting = 'on'");
|
|
|
|
# If checksum page option is not explictly set then set it to whatever the database says
|
|
if (!cfgOptionTest(CFGOPT_CHECKSUM_PAGE))
|
|
{
|
|
cfgOptionSet(CFGOPT_CHECKSUM_PAGE, $bChecksumPage);
|
|
}
|
|
# Else if enabled make sure they are in the database as well, else throw a warning
|
|
elsif (cfgOption(CFGOPT_CHECKSUM_PAGE) && !$bChecksumPage)
|
|
{
|
|
&log(WARN, 'unable to enable page checksums since they are not enabled in the database');
|
|
cfgOptionSet(CFGOPT_CHECKSUM_PAGE, false);
|
|
}
|
|
|
|
# Acquire the backup advisory lock to make sure that backups are not running from multiple backup servers against the same
|
|
# database cluster. This lock helps make the stop-auto option safe.
|
|
if (!$self->executeSqlOne('select pg_try_advisory_lock(' . DB_BACKUP_ADVISORY_LOCK . ')'))
|
|
{
|
|
confess &log(ERROR, 'unable to acquire ' . PROJECT_NAME . " advisory lock\n" .
|
|
'HINT: is another ' . PROJECT_NAME . ' backup already running on this cluster?', ERROR_LOCK_ACQUIRE);
|
|
}
|
|
|
|
# If stop-auto is enabled check for a running backup. This feature is not supported for PostgreSQL >= 9.6 since backups are
|
|
# run in non-exclusive mode.
|
|
if (cfgOption(CFGOPT_STOP_AUTO) && $self->{strDbVersion} < PG_VERSION_96)
|
|
{
|
|
# Running backups can only be detected in PostgreSQL >= 9.3
|
|
if ($self->{strDbVersion} >= PG_VERSION_93)
|
|
{
|
|
# If a backup is currently in progress emit a warning and then stop it
|
|
if ($self->executeSqlOne('select pg_is_in_backup()'))
|
|
{
|
|
&log(WARN, 'the cluster is already in backup mode but no ' . PROJECT_NAME . ' backup process is running.' .
|
|
' pg_stop_backup() will be called so a new backup can be started.');
|
|
$self->backupStop();
|
|
}
|
|
}
|
|
# Else emit a warning that the feature is not supported and continue. If a backup is running then an error will be
|
|
# generated later on.
|
|
else
|
|
{
|
|
&log(WARN, cfgOptionName(CFGOPT_STOP_AUTO) . ' option is only available in PostgreSQL >= ' . PG_VERSION_93);
|
|
}
|
|
}
|
|
|
|
# Start the backup
|
|
&log(INFO, 'execute ' . ($self->{strDbVersion} >= PG_VERSION_96 ? 'non-' : '') .
|
|
"exclusive pg_start_backup() with label \"${strLabel}\": backup begins after " .
|
|
($bStartFast ? "the requested immediate checkpoint" : "the next regular checkpoint") . " completes");
|
|
|
|
my ($strTimestampDbStart, $strArchiveStart, $strLsnStart, $iWalSegmentSize) = $self->executeSqlRow(
|
|
"select to_char(current_timestamp, 'YYYY-MM-DD HH24:MI:SS.US TZ'), pg_" . $self->walId() . "file_name(lsn), lsn::text," .
|
|
($self->{strDbVersion} < PG_VERSION_84 ? PG_WAL_SIZE_83 :
|
|
" (select setting::int8 from pg_settings where name = 'wal_segment_size')" .
|
|
# In Pre-11 versions the wal_segment_sise was expressed in terms of blocks rather than total size
|
|
($self->{strDbVersion} < PG_VERSION_11 ?
|
|
" * (select setting::int8 from pg_settings where name = 'wal_block_size')" : '')) .
|
|
" from pg_start_backup('${strLabel}'" .
|
|
($bStartFast ? ', true' : $self->{strDbVersion} >= PG_VERSION_84 ? ', false' : '') .
|
|
($self->{strDbVersion} >= PG_VERSION_96 ? ', false' : '') . ') as lsn');
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'strArchiveStart', value => $strArchiveStart},
|
|
{name => 'strLsnStart', value => $strLsnStart},
|
|
{name => 'iWalSegmentSize', value => $iWalSegmentSize},
|
|
{name => 'strTimestampDbStart', value => $strTimestampDbStart}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# backupStop
|
|
####################################################################################################################################
|
|
sub backupStop
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my ($strOperation) = logDebugParam(__PACKAGE__ . '->backupStop');
|
|
|
|
# Stop the backup
|
|
&log(INFO, 'execute ' . ($self->{strDbVersion} >= PG_VERSION_96 ? 'non-' : '') .
|
|
'exclusive pg_stop_backup() and wait for all WAL segments to archive');
|
|
|
|
my ($strTimestampDbStop, $strArchiveStop, $strLsnStop, $strLabel, $strTablespaceMap) =
|
|
$self->executeSqlRow(
|
|
"select to_char(clock_timestamp(), 'YYYY-MM-DD HH24:MI:SS.US TZ'), pg_" .
|
|
$self->walId() . "file_name(lsn), lsn::text, " .
|
|
($self->{strDbVersion} >= PG_VERSION_96 ?
|
|
'labelfile, ' .
|
|
'case when length(trim(both \'\t\n \' from spcmapfile)) = 0 then null else spcmapfile end as spcmapfile' :
|
|
'null as labelfile, null as spcmapfile') .
|
|
' from pg_stop_backup(' .
|
|
# Add flag to use non-exclusive backup
|
|
($self->{strDbVersion} >= PG_VERSION_96 ? 'false' : '') .
|
|
# Add flag to exit immediately after backup stop rather than waiting for WAL to archive (this is checked later)
|
|
($self->{strDbVersion} >= PG_VERSION_10 ? ', false' : '') . ') as lsn');
|
|
|
|
# Build a hash of the files that need to be written to the backup
|
|
my $oFileHash =
|
|
{
|
|
&MANIFEST_FILE_BACKUPLABEL => $strLabel,
|
|
&MANIFEST_FILE_TABLESPACEMAP => $strTablespaceMap
|
|
};
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'strArchiveStop', value => $strArchiveStop},
|
|
{name => 'strLsnStop', value => $strLsnStop},
|
|
{name => 'strTimestampDbStop', value => $strTimestampDbStop},
|
|
{name => 'oFileHash', value => $oFileHash}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# configValidate
|
|
#
|
|
# Validate the database configuration and archiving.
|
|
####################################################################################################################################
|
|
sub configValidate
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my
|
|
(
|
|
$strOperation,
|
|
) =
|
|
logDebugParam
|
|
(
|
|
__PACKAGE__ . '->configValidate', \@_,
|
|
);
|
|
|
|
# Get the version from the control file
|
|
my ($strDbVersion) = $self->info();
|
|
|
|
# Get version and db path from the database
|
|
my ($fCompareDbVersion, $strCompareDbPath) = $self->versionGet();
|
|
|
|
# Error if the version from the control file and the configured pg-path do not match the values obtained from the database
|
|
if (!($strDbVersion == $fCompareDbVersion && $self->{strDbPath} eq $strCompareDbPath))
|
|
{
|
|
confess &log(ERROR,
|
|
"version '${fCompareDbVersion}' and path '${strCompareDbPath}' queried from cluster do not match version" .
|
|
" '${strDbVersion}' and " . cfgOptionName(CFGOPT_PG_PATH) . " '$self->{strDbPath}' read from" .
|
|
" '$self->{strDbPath}/" . DB_FILE_PGCONTROL . "'\n" .
|
|
"HINT: the " . cfgOptionName(CFGOPT_PG_PATH) . " and " . cfgOptionName(CFGOPT_PG_PORT) .
|
|
" settings likely reference different clusters",
|
|
ERROR_DB_MISMATCH);
|
|
}
|
|
|
|
# If cluster is not a standby and archive checking is enabled, then perform various validations
|
|
if (!$self->isStandby() && cfgOptionValid(CFGOPT_ARCHIVE_CHECK) && cfgOption(CFGOPT_ARCHIVE_CHECK))
|
|
{
|
|
my $strArchiveMode = $self->executeSqlOne('show archive_mode');
|
|
|
|
# Error if archive_mode = off since pg_start_backup () will fail
|
|
if ($strArchiveMode eq 'off')
|
|
{
|
|
confess &log(ERROR, 'archive_mode must be enabled', ERROR_ARCHIVE_DISABLED);
|
|
}
|
|
|
|
# Error if archive_mode = always (support has not been added yet)
|
|
if ($strArchiveMode eq 'always')
|
|
{
|
|
confess &log(ERROR, "archive_mode=always not supported", ERROR_FEATURE_NOT_SUPPORTED);
|
|
}
|
|
|
|
# Check if archive_command is set
|
|
my $strArchiveCommand = $self->executeSqlOne('show archive_command');
|
|
|
|
if (index($strArchiveCommand, PROJECT_EXE) == -1)
|
|
{
|
|
confess &log(ERROR,
|
|
'archive_command ' . (defined($strArchiveCommand) ? "'${strArchiveCommand}'" : '[null]') . ' must contain \'' .
|
|
PROJECT_EXE . '\'', ERROR_ARCHIVE_COMMAND_INVALID);
|
|
}
|
|
}
|
|
|
|
return logDebugReturn
|
|
(
|
|
$strOperation
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# walId
|
|
#
|
|
# Returns 'wal' or 'xlog' depending on the version of PostgreSQL.
|
|
####################################################################################################################################
|
|
sub walId
|
|
{
|
|
my $self = shift;
|
|
|
|
return $self->{strDbVersion} >= PG_VERSION_10 ? 'wal' : 'xlog';
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# lsnId
|
|
#
|
|
# Returns 'lsn' or 'location' depending on the version of PostgreSQL.
|
|
####################################################################################################################################
|
|
sub lsnId
|
|
{
|
|
my $self = shift;
|
|
|
|
return $self->{strDbVersion} >= PG_VERSION_10 ? 'lsn' : 'location';
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# walSwitch
|
|
#
|
|
# Forces a switch to the next transaction log in order to archive the current log.
|
|
####################################################################################################################################
|
|
sub walSwitch
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my $strOperation = logDebugParam(__PACKAGE__ . '->walSwitch');
|
|
|
|
# Create a restore point to ensure current WAL will be archived. For versions <= 9.0 activity will need to be generated by
|
|
# the user if there have been no writes since the last WAL switch.
|
|
if ($self->{strDbVersion} >= PG_VERSION_91)
|
|
{
|
|
$self->executeSql("select pg_create_restore_point('" . PROJECT_NAME . " Archive Check');");
|
|
}
|
|
|
|
my $strWalFileName = $self->executeSqlOne(
|
|
'select pg_' . $self->walId() . 'file_name from pg_' . $self->walId() . 'file_name(pg_switch_' . $self->walId() . '());');
|
|
|
|
&log(INFO, "switch WAL ${strWalFileName}");
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'strWalFileName', value => $strWalFileName}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# isStandby
|
|
#
|
|
# Determines if a database is a standby by testing if it is in recovery mode.
|
|
####################################################################################################################################
|
|
sub isStandby
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my ($strOperation) = logDebugParam(__PACKAGE__ . '->isStandby');
|
|
|
|
if (!defined($self->{bStandby}))
|
|
{
|
|
my ($strDbVersion) = $self->versionGet();
|
|
|
|
if ($strDbVersion <= PG_VERSION_90)
|
|
{
|
|
$self->{bStandby} = false;
|
|
}
|
|
else
|
|
{
|
|
$self->{bStandby} = $self->executeSqlOne('select pg_is_in_recovery()') ? true : false;
|
|
}
|
|
}
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'bStandby', value => $self->{bStandby}}
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# replayWait
|
|
#
|
|
# Waits for replay on the standby to equal specified LSN
|
|
####################################################################################################################################
|
|
sub replayWait
|
|
{
|
|
my $self = shift;
|
|
|
|
# Assign function parameters, defaults, and log debug info
|
|
my
|
|
(
|
|
$strOperation,
|
|
$strTargetLSN,
|
|
) =
|
|
logDebugParam
|
|
(
|
|
__PACKAGE__ . '->replayWait', \@_,
|
|
{name => 'strTargetLSN'}
|
|
);
|
|
|
|
# Load ArchiveCommon Module
|
|
require pgBackRest::Archive::Common;
|
|
pgBackRest::Archive::Common->import();
|
|
|
|
# Initialize working variables
|
|
my $oWait = waitInit(cfgOption(CFGOPT_ARCHIVE_TIMEOUT));
|
|
my $bTimeout = true;
|
|
my $strReplayedLSN = undef;
|
|
|
|
# Monitor the replay location
|
|
do
|
|
{
|
|
my $strLastWalReplayLsnFunction =
|
|
'pg_last_' . $self->walId() . '_replay_' . $self->lsnId() . '()';
|
|
|
|
# Get the replay location
|
|
my $strLastReplayedLSN = $self->executeSqlOne(
|
|
"select coalesce(${strLastWalReplayLsnFunction}::text, '<NONE>')");
|
|
|
|
# Error if the replay location could not be retrieved
|
|
if ($strLastReplayedLSN eq '<NONE>')
|
|
{
|
|
confess &log(
|
|
ERROR,
|
|
"unable to query replay lsn on the standby using ${strLastWalReplayLsnFunction}\n" .
|
|
"Hint: Is this a standby?",
|
|
ERROR_ARCHIVE_TIMEOUT);
|
|
}
|
|
|
|
# Is the replay lsn > target lsn? It needs to be greater because the checkpoint record is directly after the LSN returned
|
|
# by pg_start_backup().
|
|
if (lsnNormalize($strLastReplayedLSN) ge lsnNormalize($strTargetLSN))
|
|
{
|
|
$bTimeout = false;
|
|
}
|
|
else
|
|
{
|
|
# Reset the timer if the LSN is advancing
|
|
if (defined($strReplayedLSN) &&
|
|
lsnNormalize($strLastReplayedLSN) gt lsnNormalize($strReplayedLSN) &&
|
|
!waitMore($oWait))
|
|
{
|
|
$oWait = waitInit(cfgOption(CFGOPT_ARCHIVE_TIMEOUT));
|
|
}
|
|
}
|
|
|
|
# Assigned last replayed to replayed
|
|
$strReplayedLSN = $strLastReplayedLSN;
|
|
|
|
} while ($bTimeout && waitMore($oWait));
|
|
|
|
# Error if a timeout occurred before the target lsn was reached
|
|
if ($bTimeout == true)
|
|
{
|
|
confess &log(
|
|
ERROR, "timeout before standby replayed ${strTargetLSN} - only reached ${strReplayedLSN}", ERROR_ARCHIVE_TIMEOUT);
|
|
}
|
|
|
|
# Perform a checkpoint
|
|
$self->executeSql('checkpoint', undef, false);
|
|
|
|
# On PostgreSQL >= 9.6 the checkpoint location can be verified
|
|
my $strCheckpointLSN = undef;
|
|
|
|
if ($self->{strDbVersion} >= PG_VERSION_96)
|
|
{
|
|
$strCheckpointLSN = $self->executeSqlOne('select checkpoint_' . $self->lsnId() .' from pg_control_checkpoint()');
|
|
|
|
if (lsnNormalize($strCheckpointLSN) le lsnNormalize($strTargetLSN))
|
|
{
|
|
confess &log(
|
|
ERROR,
|
|
"the checkpoint location ${strCheckpointLSN} is less than the target location ${strTargetLSN} even though the" .
|
|
" replay location is ${strReplayedLSN}\n" .
|
|
"Hint: This should not be possible and may indicate a bug in PostgreSQL.",
|
|
ERROR_ARCHIVE_TIMEOUT);
|
|
}
|
|
}
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'strReplayedLSN', value => $strReplayedLSN},
|
|
{name => 'strCheckpointLSN', value => $strCheckpointLSN},
|
|
);
|
|
}
|
|
|
|
####################################################################################################################################
|
|
# dbObjectGet
|
|
#
|
|
# Gets the database objects(s) and indexes. The databases required for the backup type must be online. A connection to the available
|
|
# databases will be established to determine which is the master and which, if any, is the standby. If there is a master and a
|
|
# standby to which a connection can be established, it returns both, else just the master.
|
|
####################################################################################################################################
|
|
sub dbObjectGet
|
|
{
|
|
# Assign function parameters, defaults, and log debug info
|
|
my (
|
|
$strOperation,
|
|
$bMasterOnly,
|
|
) =
|
|
logDebugParam
|
|
(
|
|
__PACKAGE__ . '::dbObjectGet', \@_,
|
|
{name => 'bMasterOnly', optional => true, default => false},
|
|
);
|
|
|
|
my $iStandbyIdx = undef;
|
|
my $iMasterRemoteIdx = 1;
|
|
my $oDbMaster = undef;
|
|
my $oDbStandby = undef;
|
|
|
|
# Only iterate databases if online and more than one is defined. It might be better to check the version of each database but
|
|
# this is simple and works.
|
|
if (!$bMasterOnly && cfgOptionTest(CFGOPT_ONLINE) && cfgOption(CFGOPT_ONLINE) && multipleDb())
|
|
{
|
|
for (my $iRemoteIdx = 1; $iRemoteIdx <= cfgOptionIndexTotal(CFGOPT_PG_HOST); $iRemoteIdx++)
|
|
{
|
|
# Make sure a db is defined for this index
|
|
if (cfgOptionTest(cfgOptionIdFromIndex(CFGOPT_PG_PATH, $iRemoteIdx)) ||
|
|
cfgOptionTest(cfgOptionIdFromIndex(CFGOPT_PG_HOST, $iRemoteIdx)))
|
|
{
|
|
# Create the db object
|
|
my $oDb;
|
|
|
|
logWarnOnErrorEnable();
|
|
eval
|
|
{
|
|
$oDb = new pgBackRest::Db($iRemoteIdx);
|
|
return true;
|
|
}
|
|
or do {};
|
|
|
|
logWarnOnErrorDisable();
|
|
my $bAssigned = false;
|
|
|
|
if (defined($oDb))
|
|
{
|
|
# If able to connect then test if the database is a master or a standby. It's OK if some databases cannot be
|
|
# reached as long as the databases required for the backup type are present.
|
|
if ($oDb->connect(true))
|
|
{
|
|
# If this db is a standby
|
|
if ($oDb->isStandby())
|
|
{
|
|
# If standby backup is requested then use the first standby found
|
|
if (cfgOption(CFGOPT_BACKUP_STANDBY) && !defined($oDbStandby))
|
|
{
|
|
$oDbStandby = $oDb;
|
|
$iStandbyIdx = $iRemoteIdx;
|
|
$bAssigned = true;
|
|
}
|
|
}
|
|
# Else this db is a master
|
|
else
|
|
{
|
|
# Error if more than one master is found
|
|
if (defined($oDbMaster))
|
|
{
|
|
confess &log(ERROR, 'more than one master database found');
|
|
}
|
|
|
|
$oDbMaster = $oDb;
|
|
$iMasterRemoteIdx = $iRemoteIdx;
|
|
$bAssigned = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
# If the db was not used then destroy the protocol object underneath it
|
|
if (!$bAssigned)
|
|
{
|
|
protocolDestroy(CFGOPTVAL_REMOTE_TYPE_DB, $iRemoteIdx, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
# Make sure a standby database is defined when backup from standby option is set
|
|
if (cfgOption(CFGOPT_BACKUP_STANDBY) && !defined($oDbStandby))
|
|
{
|
|
# Throw an error that is distinct from connecting to the master for testing purposes
|
|
confess &log(ERROR, 'unable to find standby database - cannot proceed', ERROR_HOST_CONNECT);
|
|
}
|
|
|
|
# A master database is always required
|
|
if (!defined($oDbMaster))
|
|
{
|
|
# Throw an error that is distinct from connecting to a standy for testing purposes
|
|
confess &log(ERROR, 'unable to find master database - cannot proceed', ERROR_DB_CONNECT);
|
|
}
|
|
}
|
|
|
|
# If master db is not already defined then set to default
|
|
if (!defined($oDbMaster))
|
|
{
|
|
$oDbMaster = new pgBackRest::Db($iMasterRemoteIdx);
|
|
}
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'oDbMaster', value => $oDbMaster},
|
|
{name => 'iDbMasterIdx', value => $iMasterRemoteIdx},
|
|
{name => 'oDbStandby', value => $oDbStandby},
|
|
{name => 'iDbStandbyIdx', value => $iStandbyIdx},
|
|
);
|
|
}
|
|
|
|
push @EXPORT, qw(dbObjectGet);
|
|
|
|
####################################################################################################################################
|
|
# dbMasterGet
|
|
#
|
|
# Usually only the master database is required so this function makes getting it simple. If in offline mode (which is true for a
|
|
# lot of archive operations) then the database returned is simply the first configured.
|
|
####################################################################################################################################
|
|
sub dbMasterGet
|
|
{
|
|
# Assign function parameters, defaults, and log debug info
|
|
my ($strOperation) = logDebugParam(__PACKAGE__ . '::dbMasterGet');
|
|
|
|
my ($oDbMaster) = dbObjectGet({bMasterOnly => true});
|
|
|
|
# Return from function and log return values if any
|
|
return logDebugReturn
|
|
(
|
|
$strOperation,
|
|
{name => 'oDbMaster', value => $oDbMaster, trace => true},
|
|
);
|
|
}
|
|
|
|
push @EXPORT, qw(dbMasterGet);
|
|
|
|
####################################################################################################################################
|
|
# multipleDb
|
|
#
|
|
# Helper function to determine if there is more than one database defined.
|
|
####################################################################################################################################
|
|
sub multipleDb
|
|
{
|
|
for (my $iDbPathIdx = 2; $iDbPathIdx <= cfgOptionIndexTotal(CFGOPT_PG_PATH); $iDbPathIdx++)
|
|
{
|
|
# If an index exists above 1 then return true
|
|
if (cfgOptionTest(cfgOptionIdFromIndex(CFGOPT_PG_PATH, $iDbPathIdx)))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
1;
|