forked from huawei/openGauss-server
2020 lines
71 KiB
C++
2020 lines
71 KiB
C++
/*-------------------------------------------------------------------------
|
|
*
|
|
* restore.c: restore DB cluster and archived WAL.
|
|
*
|
|
* Portions Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
|
* Portions Copyright (c) 2009-2013, NIPPON TELEGRAPH AND TELEPHONE CORPORATION
|
|
* Portions Copyright (c) 2015-2019, Postgres Professional
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
|
|
#include "pg_probackup.h"
|
|
|
|
|
|
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include "thread.h"
|
|
#include "common/fe_memutils.h"
|
|
|
|
#define RESTORE_ARRAY_LEN 100
|
|
|
|
typedef struct
|
|
{
|
|
parray *pgdata_files;
|
|
parray *dest_files;
|
|
pgBackup *dest_backup;
|
|
parray *dest_external_dirs;
|
|
parray *parent_chain;
|
|
bool skip_external_dirs;
|
|
const char *to_root;
|
|
size_t restored_bytes;
|
|
bool use_bitmap;
|
|
IncrRestoreMode incremental_mode;
|
|
XLogRecPtr shift_lsn; /* used only in LSN incremental_mode */
|
|
|
|
/*
|
|
* Return value from the thread.
|
|
* 0 means there is no error, 1 - there is an error.
|
|
*/
|
|
int ret;
|
|
} restore_files_arg;
|
|
|
|
static void create_recovery_conf(time_t backup_id,
|
|
pgRecoveryTarget *rt,
|
|
pgBackup *backup,
|
|
pgRestoreParams *params);
|
|
static void construct_restore_cmd(FILE *fp, pgRecoveryTarget *rt,
|
|
bool restore_command_provided,
|
|
bool target_immediate);
|
|
static void *restore_files(void *arg);
|
|
static void set_orphan_status(parray *backups, pgBackup *parent_backup);
|
|
static void pg12_recovery_config(pgBackup *backup, bool add_include);
|
|
|
|
static void restore_chain(pgBackup *dest_backup, parray *parent_chain,
|
|
pgRestoreParams *params,
|
|
const char *pgdata_path, bool no_sync);
|
|
static void check_incremental_compatibility(const char *pgdata, uint64 system_identifier,
|
|
IncrRestoreMode incremental_mode);
|
|
static pgBackup *find_backup_range(parray *backups,
|
|
time_t target_backup_id,
|
|
pgRecoveryTarget *rt,
|
|
pgRestoreParams *params);
|
|
static void check_backup_status(pgBackup *current_backup, pgRestoreParams *params);
|
|
static pgBackup * find_full_backup(parray *backups,
|
|
pgBackup *dest_backup,
|
|
const char *action);
|
|
static XLogRecPtr determine_shift_lsn(pgBackup *dest_backup);
|
|
static void get_pgdata_files(const char *pgdata_path,
|
|
parray *pgdata_files,
|
|
parray *external_dirs);
|
|
static void remove_redundant_files(const char *pgdata_path,
|
|
parray *pgdata_files,
|
|
pgBackup *dest_backup);
|
|
static void threads_handle(pthread_t *threads,
|
|
restore_files_arg *threads_args,
|
|
pgBackup *dest_backup,
|
|
parray *dest_files,
|
|
parray *pgdata_files,
|
|
parray *external_dirs,
|
|
parray *parent_chain,
|
|
pgRestoreParams *params,
|
|
const char *pgdata_path,
|
|
bool use_bitmap,
|
|
size_t total_bytes);
|
|
static void sync_restored_files(parray *dest_files,
|
|
parray *external_dirs,
|
|
pgRestoreParams *params,
|
|
const char *pgdata_path);
|
|
#ifdef SUPPORT_MULTI_TIMELINE
|
|
static void parse_file(parray *result, FILE *fd,
|
|
TimeLineHistoryEntry *entry,
|
|
TimeLineHistoryEntry *last_timeline);
|
|
#endif
|
|
static void parse_other_options(pgRecoveryTarget *rt,
|
|
int recovery_target_specified,
|
|
const char *target_inclusive,
|
|
const char *target_stop,
|
|
const char *target_action);
|
|
|
|
/*
|
|
* Iterate over backup list to find all ancestors of the broken parent_backup
|
|
* and update their status to BACKUP_STATUS_ORPHAN
|
|
*/
|
|
static void
|
|
set_orphan_status(parray *backups, pgBackup *parent_backup)
|
|
{
|
|
/* chain is intact, but at least one parent is invalid */
|
|
char *parent_backup_id;
|
|
size_t j;
|
|
|
|
/* parent_backup_id is a human-readable backup ID */
|
|
parent_backup_id = base36enc_dup(parent_backup->start_time);
|
|
|
|
for (j = 0; j < parray_num(backups); j++)
|
|
{
|
|
|
|
pgBackup *backup = (pgBackup *) parray_get(backups, j);
|
|
|
|
if (is_parent(parent_backup->start_time, backup, false))
|
|
{
|
|
if (backup->status == BACKUP_STATUS_OK ||
|
|
backup->status == BACKUP_STATUS_DONE)
|
|
{
|
|
write_backup_status(backup, BACKUP_STATUS_ORPHAN, instance_name, true);
|
|
|
|
elog(WARNING,
|
|
"Backup %s is orphaned because his parent %s has status: %s",
|
|
base36enc(backup->start_time),
|
|
parent_backup_id,
|
|
status2str(parent_backup->status));
|
|
}
|
|
else
|
|
{
|
|
elog(WARNING, "Backup %s has parent %s with status: %s",
|
|
base36enc(backup->start_time), parent_backup_id,
|
|
status2str(parent_backup->status));
|
|
}
|
|
}
|
|
}
|
|
pg_free(parent_backup_id);
|
|
}
|
|
|
|
/*
|
|
* Entry point of pg_probackup RESTORE and VALIDATE subcommands.
|
|
*/
|
|
int
|
|
do_restore_or_validate(time_t target_backup_id, pgRecoveryTarget *rt,
|
|
pgRestoreParams *params, bool no_sync)
|
|
{
|
|
int i = 0;
|
|
parray *backups = NULL;
|
|
pgBackup *tmp_backup = NULL;
|
|
pgBackup *dest_backup = NULL;
|
|
pgBackup *base_full_backup = NULL;
|
|
pgBackup *corrupted_backup = NULL;
|
|
const char *action = (const char *)(params->is_restore ? "Restore":"Validate");
|
|
parray *parent_chain = NULL;
|
|
bool pgdata_is_empty = true;
|
|
bool tblspaces_are_empty = true;
|
|
|
|
if (params->is_restore)
|
|
{
|
|
if (instance_config.pgdata == NULL)
|
|
elog(ERROR,
|
|
"required parameter not specified: PGDATA (-D, --pgdata)");
|
|
/* Check if restore destination empty */
|
|
if (!dir_is_empty(instance_config.pgdata, FIO_DB_HOST))
|
|
{
|
|
/* Check that remote system is NOT running and systemd id is the same as ours */
|
|
if (params->incremental_mode != INCR_NONE)
|
|
{
|
|
elog(INFO, "Running incremental restore into nonempty directory: \"%s\"",
|
|
instance_config.pgdata);
|
|
|
|
check_incremental_compatibility(instance_config.pgdata,
|
|
instance_config.system_identifier,
|
|
params->incremental_mode);
|
|
}
|
|
else
|
|
elog(ERROR, "Restore destination is not empty: \"%s\"",
|
|
instance_config.pgdata);
|
|
|
|
/* if destination directory is empty, then incremental restore may be disabled */
|
|
pgdata_is_empty = false;
|
|
}
|
|
}
|
|
|
|
if (instance_name == NULL)
|
|
elog(ERROR, "required parameter not specified: --instance");
|
|
|
|
elog(LOG, "%s begin.", action);
|
|
|
|
/* Get list of all backups sorted in order of descending start time */
|
|
backups = catalog_get_backup_list(instance_name, INVALID_BACKUP_ID);
|
|
|
|
dest_backup = find_backup_range(backups, target_backup_id, rt, params);
|
|
|
|
/* TODO: Show latest possible target */
|
|
if (dest_backup == NULL)
|
|
{
|
|
/* Failed to find target backup */
|
|
if (target_backup_id)
|
|
elog(ERROR, "Requested backup %s is not found.", base36enc(target_backup_id));
|
|
else
|
|
elog(ERROR, "Backup satisfying target options is not found.");
|
|
/* TODO: check if user asked PITR or just restore of latest backup */
|
|
}
|
|
|
|
/* If we already found dest_backup, look for full backup. */
|
|
if (dest_backup->backup_mode == BACKUP_MODE_FULL)
|
|
base_full_backup = dest_backup;
|
|
else
|
|
{
|
|
base_full_backup = find_full_backup(backups, dest_backup, action);
|
|
}
|
|
|
|
if (base_full_backup == NULL)
|
|
elog(ERROR, "Full backup satisfying target options is not found.");
|
|
|
|
/*
|
|
* Ensure that directories provided in tablespace mapping are valid
|
|
* i.e. empty or not exist.
|
|
*/
|
|
if (params->is_restore)
|
|
{
|
|
check_tablespace_mapping(dest_backup, params->incremental_mode != INCR_NONE, &tblspaces_are_empty);
|
|
|
|
if (params->incremental_mode != INCR_NONE && pgdata_is_empty && tblspaces_are_empty)
|
|
{
|
|
elog(INFO, "Destination directory and tablespace directories are empty, "
|
|
"disable incremental restore");
|
|
params->incremental_mode = INCR_NONE;
|
|
}
|
|
|
|
/* no point in checking external directories if their restore is not requested */
|
|
if (!params->skip_external_dirs)
|
|
check_external_dir_mapping(dest_backup, params->incremental_mode != INCR_NONE);
|
|
}
|
|
|
|
/* At this point we are sure that parent chain is whole
|
|
* so we can build separate array, containing all needed backups,
|
|
* to simplify validation and restore
|
|
*/
|
|
parent_chain = parray_new();
|
|
|
|
/* Take every backup that is a child of base_backup AND parent of dest_backup
|
|
* including base_backup and dest_backup
|
|
*/
|
|
|
|
tmp_backup = dest_backup;
|
|
while (tmp_backup)
|
|
{
|
|
parray_append(parent_chain, tmp_backup);
|
|
tmp_backup = tmp_backup->parent_backup_link;
|
|
}
|
|
|
|
/*
|
|
* Determine the shift-LSN
|
|
* Consider the example A:
|
|
*
|
|
*
|
|
* /----D----------F->
|
|
* -A--B---C---*-------X----->
|
|
*
|
|
* [A,F] - incremental chain
|
|
* X - the state of pgdata
|
|
* F - destination backup
|
|
* * - switch point
|
|
*
|
|
* When running incremental restore in 'lsn' mode, we get a bitmap of pages,
|
|
* whose LSN is less than shift-LSN (backup C stop_lsn).
|
|
* So when restoring file, we can skip restore of pages coming from
|
|
* A, B and C.
|
|
* Pages from D and F cannot be skipped due to incremental restore.
|
|
*
|
|
* Consider the example B:
|
|
*
|
|
*
|
|
* /----------X---->
|
|
* ----*---A---B---C-->
|
|
*
|
|
* [A,C] - incremental chain
|
|
* X - the state of pgdata
|
|
* C - destination backup
|
|
* * - switch point
|
|
*
|
|
* Incremental restore in shift mode IS NOT POSSIBLE in this case.
|
|
* We must be able to differentiate the scenario A and scenario B.
|
|
*
|
|
*/
|
|
if (params->is_restore && params->incremental_mode == INCR_LSN)
|
|
{
|
|
params->shift_lsn = determine_shift_lsn(dest_backup);
|
|
}
|
|
|
|
/* for validation or restore with enabled validation */
|
|
if (!params->is_restore || !params->no_validate)
|
|
{
|
|
if (dest_backup->backup_mode != BACKUP_MODE_FULL)
|
|
elog(INFO, "Validating parents for backup %s", base36enc(dest_backup->start_time));
|
|
|
|
/*
|
|
* Validate backups from base_full_backup to dest_backup.
|
|
*/
|
|
for (i = parray_num(parent_chain) - 1; i >= 0; i--)
|
|
{
|
|
tmp_backup = (pgBackup *) parray_get(parent_chain, i);
|
|
|
|
/* Do not interrupt, validate the next backup */
|
|
if (!lock_backup(tmp_backup, true))
|
|
{
|
|
if (params->is_restore)
|
|
elog(ERROR, "Cannot lock backup %s directory",
|
|
base36enc(tmp_backup->start_time));
|
|
else
|
|
{
|
|
elog(WARNING, "Cannot lock backup %s directory, skip validation",
|
|
base36enc(tmp_backup->start_time));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* validate datafiles only */
|
|
pgBackupValidate(tmp_backup, params);
|
|
|
|
/* After pgBackupValidate() only following backup
|
|
* states are possible: ERROR, RUNNING, CORRUPT and OK.
|
|
* Validate WAL only for OK, because there is no point
|
|
* in WAL validation for corrupted, errored or running backups.
|
|
*/
|
|
if (tmp_backup->status != BACKUP_STATUS_OK)
|
|
{
|
|
corrupted_backup = tmp_backup;
|
|
break;
|
|
}
|
|
/* We do not validate WAL files of intermediate backups
|
|
* It`s done to speed up restore
|
|
*/
|
|
}
|
|
|
|
/* There is no point in wal validation of corrupted backups */
|
|
// TODO: there should be a way for a user to request only(!) WAL validation
|
|
if (!corrupted_backup)
|
|
{
|
|
/*
|
|
* Validate corresponding WAL files.
|
|
* We pass base_full_backup timeline as last argument to this function,
|
|
* because it's needed to form the name of xlog file.
|
|
*/
|
|
validate_wal(dest_backup, arclog_path, rt->target_time,
|
|
rt->target_xid, rt->target_lsn,
|
|
dest_backup->tli, instance_config.xlog_seg_size);
|
|
}
|
|
/* Orphanize every OK descendant of corrupted backup */
|
|
else
|
|
set_orphan_status(backups, corrupted_backup);
|
|
}
|
|
|
|
/*
|
|
* If dest backup is corrupted or was orphaned in previous check
|
|
* produce corresponding error message
|
|
*/
|
|
if (dest_backup->status == BACKUP_STATUS_OK ||
|
|
dest_backup->status == BACKUP_STATUS_DONE)
|
|
{
|
|
if (params->no_validate)
|
|
elog(WARNING, "Backup %s is used without validation.", base36enc(dest_backup->start_time));
|
|
else
|
|
elog(INFO, "Backup %s is valid.", base36enc(dest_backup->start_time));
|
|
}
|
|
else if (dest_backup->status == BACKUP_STATUS_CORRUPT)
|
|
{
|
|
if (params->force)
|
|
elog(WARNING, "Backup %s is corrupt.", base36enc(dest_backup->start_time));
|
|
else
|
|
elog(ERROR, "Backup %s is corrupt.", base36enc(dest_backup->start_time));
|
|
}
|
|
else if (dest_backup->status == BACKUP_STATUS_ORPHAN)
|
|
{
|
|
if (params->force)
|
|
elog(WARNING, "Backup %s is orphan.", base36enc(dest_backup->start_time));
|
|
else
|
|
elog(ERROR, "Backup %s is orphan.", base36enc(dest_backup->start_time));
|
|
}
|
|
else
|
|
elog(ERROR, "Backup %s has status: %s",
|
|
base36enc(dest_backup->start_time), status2str(dest_backup->status));
|
|
|
|
/* We ensured that all backups are valid, now restore if required
|
|
*/
|
|
if (params->is_restore)
|
|
{
|
|
restore_chain(dest_backup, parent_chain,
|
|
params, instance_config.pgdata, no_sync);
|
|
|
|
/* Create recovery.conf with given recovery target parameters */
|
|
create_recovery_conf(target_backup_id, rt, dest_backup, params);
|
|
}
|
|
|
|
/* ssh connection to longer needed */
|
|
fio_disconnect();
|
|
|
|
elog(INFO, "%s of backup %s completed.",
|
|
action, base36enc(dest_backup->start_time));
|
|
|
|
/* cleanup */
|
|
parray_walk(backups, pgBackupFree);
|
|
parray_free(backups);
|
|
parray_free(parent_chain);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static pgBackup *find_backup_range(parray *backups,
|
|
time_t target_backup_id,
|
|
pgRecoveryTarget *rt,
|
|
pgRestoreParams *params)
|
|
{
|
|
size_t i = 0;
|
|
pgBackup *dest_backup = NULL;
|
|
pgBackup *current_backup = NULL;
|
|
|
|
/* Find backup range we should restore or validate. */
|
|
while ((i < parray_num(backups)) && !dest_backup)
|
|
{
|
|
current_backup = (pgBackup *) parray_get(backups, i);
|
|
i++;
|
|
|
|
/* Skip all backups which started after target backup */
|
|
if (target_backup_id && current_backup->start_time > target_backup_id)
|
|
continue;
|
|
|
|
/*
|
|
* [PGPRO-1164] If BACKUP_ID is not provided for restore command,
|
|
* we must find the first valid(!) backup.
|
|
|
|
* If target_backup_id is not provided, we can be sure that
|
|
* PITR for restore or validate is requested.
|
|
* So we can assume that user is more interested in recovery to specific point
|
|
* in time and NOT interested in revalidation of invalid backups.
|
|
* So based on that assumptions we should choose only OK and DONE backups
|
|
* as candidates for validate and restore.
|
|
*/
|
|
|
|
if (target_backup_id == INVALID_BACKUP_ID &&
|
|
(current_backup->status != BACKUP_STATUS_OK &&
|
|
current_backup->status != BACKUP_STATUS_DONE))
|
|
{
|
|
elog(WARNING, "Skipping backup %s, because it has non-valid status: %s",
|
|
base36enc(current_backup->start_time), status2str(current_backup->status));
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* We found target backup. Check its status and
|
|
* ensure that it satisfies recovery target.
|
|
*/
|
|
if ((target_backup_id == current_backup->start_time
|
|
|| target_backup_id == INVALID_BACKUP_ID))
|
|
{
|
|
check_backup_status(current_backup, params);
|
|
|
|
if (rt->target_tli)
|
|
{
|
|
parray *timelines;
|
|
|
|
/* Read timeline history files from archives */
|
|
timelines = read_timeline_history(arclog_path, rt->target_tli, true);
|
|
|
|
if (!satisfy_timeline(timelines, current_backup))
|
|
{
|
|
if (target_backup_id != INVALID_BACKUP_ID)
|
|
elog(ERROR, "target backup %s does not satisfy target timeline",
|
|
base36enc(target_backup_id));
|
|
else
|
|
/* Try to find another backup that satisfies target timeline */
|
|
continue;
|
|
}
|
|
|
|
parray_walk(timelines, pfree);
|
|
parray_free(timelines);
|
|
}
|
|
|
|
if (!satisfy_recovery_target(current_backup, rt))
|
|
{
|
|
if (target_backup_id != INVALID_BACKUP_ID)
|
|
elog(ERROR, "Requested backup %s does not satisfy restore options",
|
|
base36enc(target_backup_id));
|
|
else
|
|
/* Try to find another backup that satisfies target options */
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* Backup is fine and satisfies all recovery options.
|
|
* Save it as dest_backup
|
|
*/
|
|
dest_backup = current_backup;
|
|
return dest_backup;
|
|
}
|
|
}
|
|
|
|
return dest_backup;
|
|
}
|
|
|
|
static void check_backup_status(pgBackup *current_backup, pgRestoreParams *params)
|
|
{
|
|
/* backup is not ok,
|
|
* but in case of CORRUPT or ORPHAN revalidation is possible
|
|
* unless --no-validate is used,
|
|
* in other cases throw an error.
|
|
*/
|
|
// 1. validate
|
|
// 2. validate -i INVALID_ID <- allowed revalidate
|
|
// 3. restore -i INVALID_ID <- allowed revalidate and restore
|
|
// 4. restore <- impossible
|
|
// 5. restore --no-validate <- forbidden
|
|
if (current_backup->status != BACKUP_STATUS_OK &&
|
|
current_backup->status != BACKUP_STATUS_DONE)
|
|
{
|
|
if ((current_backup->status == BACKUP_STATUS_ORPHAN ||
|
|
current_backup->status == BACKUP_STATUS_CORRUPT ||
|
|
current_backup->status == BACKUP_STATUS_RUNNING)
|
|
&& (!params->no_validate || params->force)) {
|
|
elog(WARNING, "Backup %s has status: %s",
|
|
base36enc(current_backup->start_time), status2str(current_backup->status));
|
|
} else {
|
|
elog(ERROR, "Backup %s has status: %s",
|
|
base36enc(current_backup->start_time), status2str(current_backup->status));
|
|
}
|
|
}
|
|
}
|
|
|
|
static pgBackup *find_full_backup(parray *backups,
|
|
pgBackup *dest_backup,
|
|
const char *action)
|
|
{
|
|
pgBackup *tmp_backup = NULL;
|
|
int result;
|
|
|
|
result = scan_parent_chain(dest_backup, &tmp_backup);
|
|
|
|
if (result == ChainIsBroken)
|
|
{
|
|
/* chain is broken, determine missing backup ID
|
|
* and orphinize all his descendants
|
|
*/
|
|
char *missing_backup_id;
|
|
time_t missing_backup_start_time;
|
|
|
|
missing_backup_start_time = tmp_backup->parent_backup;
|
|
missing_backup_id = base36enc_dup(tmp_backup->parent_backup);
|
|
|
|
for (size_t j = 0; j < parray_num(backups); j++)
|
|
{
|
|
pgBackup *backup = (pgBackup *) parray_get(backups, j);
|
|
|
|
/* use parent backup start_time because he is missing
|
|
* and we must orphinize his descendants
|
|
*/
|
|
if (is_parent(missing_backup_start_time, backup, false))
|
|
{
|
|
if (backup->status == BACKUP_STATUS_OK ||
|
|
backup->status == BACKUP_STATUS_DONE)
|
|
{
|
|
write_backup_status(backup, BACKUP_STATUS_ORPHAN, instance_name, true);
|
|
|
|
elog(WARNING, "Backup %s is orphaned because his parent %s is missing",
|
|
base36enc(backup->start_time), missing_backup_id);
|
|
}
|
|
else
|
|
{
|
|
elog(WARNING, "Backup %s has missing parent %s",
|
|
base36enc(backup->start_time), missing_backup_id);
|
|
}
|
|
}
|
|
}
|
|
pg_free(missing_backup_id);
|
|
/* No point in doing futher */
|
|
elog(ERROR, "%s of backup %s failed.", action, base36enc(dest_backup->start_time));
|
|
}
|
|
else if (result == ChainIsInvalid)
|
|
{
|
|
/* chain is intact, but at least one parent is invalid */
|
|
set_orphan_status(backups, tmp_backup);
|
|
tmp_backup = find_parent_full_backup(dest_backup);
|
|
|
|
/* sanity */
|
|
if (!tmp_backup)
|
|
elog(ERROR, "Parent full backup for the given backup %s was not found",
|
|
base36enc(dest_backup->start_time));
|
|
}
|
|
|
|
/* We have found full backup */
|
|
return tmp_backup;
|
|
}
|
|
|
|
static XLogRecPtr determine_shift_lsn(pgBackup *dest_backup)
|
|
{
|
|
RedoParams redo;
|
|
parray *timelines = NULL;
|
|
pgBackup *tmp_backup = NULL;
|
|
XLogRecPtr shift_lsn = InvalidXLogRecPtr;
|
|
|
|
get_redo(instance_config.pgdata, &redo);
|
|
|
|
if (redo.checksum_version == 0)
|
|
elog(INFO, "Incremental restore in 'lsn' mode require "
|
|
"data_checksums to be enabled in destination data directory");
|
|
|
|
timelines = read_timeline_history(arclog_path, redo.tli, false);
|
|
|
|
if (!timelines)
|
|
elog(WARNING, "Failed to get history for redo timeline %i, "
|
|
"multi-timeline incremental restore in 'lsn' mode is impossible", redo.tli);
|
|
|
|
tmp_backup = dest_backup;
|
|
|
|
while (tmp_backup)
|
|
{
|
|
/* Candidate, whose stop_lsn if less than shift LSN, is found */
|
|
if (tmp_backup->stop_lsn < redo.lsn)
|
|
{
|
|
/* if candidate timeline is the same as redo TLI,
|
|
* then we are good to go.
|
|
*/
|
|
if (redo.tli == tmp_backup->tli)
|
|
{
|
|
elog(INFO, "Backup %s is chosen as shiftpoint, its Stop LSN will be used as shift LSN",
|
|
base36enc(tmp_backup->start_time));
|
|
|
|
shift_lsn = tmp_backup->stop_lsn;
|
|
break;
|
|
}
|
|
|
|
if (!timelines)
|
|
{
|
|
elog(WARNING, "Redo timeline %i differs from target timeline %i, "
|
|
"in this case, to safely run incremental restore in 'lsn' mode, "
|
|
"the history file for timeline %i is mandatory",
|
|
redo.tli, tmp_backup->tli, redo.tli);
|
|
break;
|
|
}
|
|
|
|
/* check whether the candidate tli is a part of redo TLI history */
|
|
if (tliIsPartOfHistory(timelines, tmp_backup->tli))
|
|
{
|
|
shift_lsn = tmp_backup->stop_lsn;
|
|
break;
|
|
}
|
|
else
|
|
elog(INFO, "Backup %s cannot be a shiftpoint, "
|
|
"because its tli %i is not in history of redo timeline %i",
|
|
base36enc(tmp_backup->start_time), tmp_backup->tli, redo.tli);
|
|
}
|
|
|
|
tmp_backup = tmp_backup->parent_backup_link;
|
|
}
|
|
|
|
if (XLogRecPtrIsInvalid(shift_lsn))
|
|
elog(ERROR, "Cannot perform incremental restore of backup chain %s in 'lsn' mode, "
|
|
"because destination directory redo point %X/%X on tli %i is out of reach",
|
|
base36enc(dest_backup->start_time),
|
|
(uint32) (redo.lsn >> 32), (uint32) redo.lsn, redo.tli);
|
|
else
|
|
elog(INFO, "Destination directory redo point %X/%X on tli %i is "
|
|
"within reach of backup %s with Stop LSN %X/%X on tli %i",
|
|
(uint32) (redo.lsn >> 32), (uint32) redo.lsn, redo.tli,
|
|
base36enc(tmp_backup->start_time),
|
|
(uint32) (tmp_backup->stop_lsn >> 32), (uint32) tmp_backup->stop_lsn,
|
|
tmp_backup->tli);
|
|
|
|
elog(INFO, "shift LSN: %X/%X",
|
|
(uint32) (shift_lsn >> 32), (uint32) shift_lsn);
|
|
|
|
return shift_lsn;
|
|
}
|
|
|
|
/*
|
|
* Restore backup chain.
|
|
*/
|
|
void
|
|
restore_chain(pgBackup *dest_backup, parray *parent_chain,
|
|
pgRestoreParams *params,
|
|
const char *pgdata_path, bool no_sync)
|
|
{
|
|
int i;
|
|
char timestamp[100];
|
|
parray *pgdata_files = NULL;
|
|
parray *dest_files = NULL;
|
|
parray *external_dirs = NULL;
|
|
/* arrays with meta info for multi threaded backup */
|
|
pthread_t *threads;
|
|
restore_files_arg *threads_args;
|
|
bool use_bitmap = true;
|
|
/* fancy reporting */
|
|
size_t total_bytes = 0;
|
|
|
|
/* Preparations for actual restoring */
|
|
time2iso(timestamp, lengthof(timestamp), dest_backup->start_time);
|
|
elog(INFO, "Restoring the database from backup at %s", timestamp);
|
|
|
|
dest_files = get_backup_filelist(dest_backup, true);
|
|
|
|
/* Lock backup chain and make sanity checks */
|
|
for (i = parray_num(parent_chain) - 1; i >= 0; i--)
|
|
{
|
|
pgBackup *backup = (pgBackup *) parray_get(parent_chain, i);
|
|
|
|
if (!lock_backup(backup, true))
|
|
elog(ERROR, "Cannot lock backup %s", base36enc(backup->start_time));
|
|
|
|
if (backup->status != BACKUP_STATUS_OK &&
|
|
backup->status != BACKUP_STATUS_DONE)
|
|
{
|
|
if (params->force)
|
|
elog(WARNING, "Backup %s is not valid, restore is forced",
|
|
base36enc(backup->start_time));
|
|
else
|
|
elog(ERROR, "Backup %s cannot be restored because it is not valid",
|
|
base36enc(backup->start_time));
|
|
}
|
|
|
|
/* confirm block size compatibility */
|
|
if (backup->block_size != BLCKSZ)
|
|
elog(ERROR,
|
|
"BLCKSZ(%d) is not compatible(%d expected)",
|
|
backup->block_size, BLCKSZ);
|
|
|
|
if (backup->wal_block_size != XLOG_BLCKSZ)
|
|
elog(ERROR,
|
|
"XLOG_BLCKSZ(%d) is not compatible(%d expected)",
|
|
backup->wal_block_size, XLOG_BLCKSZ);
|
|
|
|
/* populate backup filelist */
|
|
if (backup->start_time != dest_backup->start_time)
|
|
backup->files = get_backup_filelist(backup, true);
|
|
else
|
|
backup->files = dest_files;
|
|
|
|
/*
|
|
* this sorting is important, because we rely on it to find
|
|
* destination file in intermediate backups file lists
|
|
* using bsearch.
|
|
*/
|
|
parray_qsort(backup->files, pgFileCompareRelPathWithExternal);
|
|
}
|
|
|
|
/* If dest backup version is older than 2.4.0, then bitmap optimization
|
|
* is impossible to use, because bitmap restore rely on pgFile.n_blocks,
|
|
* which is not always available in old backups.
|
|
*/
|
|
if (parse_program_version(dest_backup->program_version) < 20400)
|
|
{
|
|
use_bitmap = false;
|
|
|
|
if (params->incremental_mode != INCR_NONE)
|
|
elog(ERROR, "incremental restore is not possible for backups older than 2.3.0 version");
|
|
}
|
|
|
|
/* There is no point in bitmap restore, when restoring a single FULL backup,
|
|
* unless we are running incremental-lsn restore, then bitmap is mandatory.
|
|
*/
|
|
if (use_bitmap && parray_num(parent_chain) == 1)
|
|
{
|
|
if (params->incremental_mode == INCR_NONE)
|
|
use_bitmap = false;
|
|
else
|
|
use_bitmap = true;
|
|
}
|
|
|
|
/*
|
|
* Restore dest_backup internal directories.
|
|
*/
|
|
create_data_directories(dest_files, instance_config.pgdata,
|
|
dest_backup->root_dir, true,
|
|
params->incremental_mode != INCR_NONE,
|
|
FIO_DB_HOST);
|
|
|
|
/*
|
|
* Restore dest_backup external directories.
|
|
*/
|
|
if (dest_backup->external_dir_str && !params->skip_external_dirs)
|
|
{
|
|
external_dirs = make_external_directory_list(dest_backup->external_dir_str, true);
|
|
|
|
if (!external_dirs)
|
|
elog(ERROR, "Failed to get a list of external directories");
|
|
|
|
if (parray_num(external_dirs) > 0)
|
|
elog(LOG, "Restore external directories");
|
|
|
|
for (i = 0; (size_t)i < parray_num(external_dirs); i++)
|
|
fio_mkdir((const char *)parray_get(external_dirs, i),
|
|
DIR_PERMISSION, FIO_DB_HOST);
|
|
}
|
|
|
|
/* Get list of files in destination directory and remove redundant files */
|
|
if (params->incremental_mode != INCR_NONE)
|
|
{
|
|
pgdata_files = parray_new();
|
|
get_pgdata_files(pgdata_path, pgdata_files, external_dirs);
|
|
remove_redundant_files(pgdata_path, pgdata_files, dest_backup);
|
|
}
|
|
|
|
/*
|
|
* Setup directory structure for external directories and file locks
|
|
*/
|
|
for (i = 0; (size_t)i < parray_num(dest_files); i++)
|
|
{
|
|
pgFile *file = (pgFile *) parray_get(dest_files, i);
|
|
|
|
if (S_ISDIR(file->mode))
|
|
total_bytes += 4096;
|
|
|
|
if (!params->skip_external_dirs &&
|
|
file->external_dir_num && S_ISDIR(file->mode))
|
|
{
|
|
char *external_path;
|
|
char dirpath[MAXPGPATH];
|
|
|
|
if ((int)parray_num(external_dirs) < file->external_dir_num - 1)
|
|
elog(ERROR, "Inconsistent external directory backup metadata");
|
|
|
|
external_path = (char *)parray_get(external_dirs, file->external_dir_num - 1);
|
|
join_path_components(dirpath, external_path, file->rel_path);
|
|
|
|
elog(VERBOSE, "Create external directory \"%s\"", dirpath);
|
|
fio_mkdir(dirpath, file->mode, FIO_DB_HOST);
|
|
}
|
|
|
|
/* setup threads */
|
|
pg_atomic_clear_flag(&file->lock);
|
|
}
|
|
|
|
|
|
/*
|
|
* Close ssh connection belonging to the main thread
|
|
* to avoid the possibility of been killed for idleness
|
|
*/
|
|
fio_disconnect();
|
|
|
|
threads = (pthread_t *) palloc(sizeof(pthread_t) * num_threads);
|
|
threads_args = (restore_files_arg *) palloc(sizeof(restore_files_arg) *
|
|
num_threads);
|
|
|
|
threads_handle(threads, threads_args, dest_backup,
|
|
dest_files, pgdata_files, external_dirs, parent_chain,
|
|
params, pgdata_path, use_bitmap, total_bytes);
|
|
|
|
/* Close page header maps */
|
|
for (i = parray_num(parent_chain) - 1; i >= 0; i--)
|
|
{
|
|
pgBackup *backup = (pgBackup *)parray_get(parent_chain, i);
|
|
cleanup_header_map(&(backup->hdr_map));
|
|
}
|
|
|
|
if (no_sync)
|
|
elog(WARNING, "Restored files are not synced to disk");
|
|
else
|
|
{
|
|
sync_restored_files(dest_files, external_dirs, params, pgdata_path);
|
|
}
|
|
|
|
/* cleanup */
|
|
pfree(threads);
|
|
pfree(threads_args);
|
|
|
|
if (external_dirs != NULL)
|
|
free_dir_list(external_dirs);
|
|
|
|
if (pgdata_files)
|
|
{
|
|
parray_walk(pgdata_files, pgFileFree);
|
|
parray_free(pgdata_files);
|
|
}
|
|
|
|
for (i = parray_num(parent_chain) - 1; i >= 0; i--)
|
|
{
|
|
pgBackup *backup = (pgBackup *)parray_get(parent_chain, i);
|
|
|
|
parray_walk(backup->files, pgFileFree);
|
|
parray_free(backup->files);
|
|
}
|
|
}
|
|
|
|
static void get_pgdata_files(const char *pgdata_path,
|
|
parray *pgdata_files,
|
|
parray *external_dirs)
|
|
{
|
|
char pretty_time[20];
|
|
time_t start_time, end_time;
|
|
|
|
elog(INFO, "Extracting the content of destination directory for incremental restore");
|
|
|
|
time(&start_time);
|
|
if (fio_is_remote(FIO_DB_HOST))
|
|
fio_list_dir(pgdata_files, pgdata_path, false, true, false, false, true, 0);
|
|
else
|
|
dir_list_file(pgdata_files, pgdata_path,
|
|
false, true, false, false, true, 0, FIO_LOCAL_HOST);
|
|
|
|
/* get external dirs content */
|
|
if (external_dirs)
|
|
{
|
|
for (int i = 0; (size_t)i < parray_num(external_dirs); i++)
|
|
{
|
|
char *external_path = (char *)parray_get(external_dirs, i);
|
|
parray *external_files = parray_new();
|
|
|
|
if (fio_is_remote(FIO_DB_HOST))
|
|
fio_list_dir(external_files, external_path,
|
|
false, true, false, false, true, i+1);
|
|
else
|
|
dir_list_file(external_files, external_path,
|
|
false, true, false, false, true, i+1,
|
|
FIO_LOCAL_HOST);
|
|
|
|
parray_concat(pgdata_files, external_files);
|
|
parray_free(external_files);
|
|
}
|
|
}
|
|
|
|
parray_qsort(pgdata_files, pgFileCompareRelPathWithExternalDesc);
|
|
|
|
time(&end_time);
|
|
pretty_time_interval(difftime(end_time, start_time),
|
|
pretty_time, lengthof(pretty_time));
|
|
|
|
elog(INFO, "Destination directory content extracted, time elapsed: %s",
|
|
pretty_time);
|
|
}
|
|
|
|
static void remove_redundant_files(const char *pgdata_path,
|
|
parray *pgdata_files,
|
|
pgBackup *dest_backup)
|
|
{
|
|
char pretty_time[20];
|
|
time_t start_time, end_time;
|
|
|
|
elog(INFO, "Removing redundant files in destination directory");
|
|
time(&start_time);
|
|
for (int i = 0; (size_t)i < parray_num(pgdata_files); i++)
|
|
{
|
|
pgFile *file = (pgFile *)parray_get(pgdata_files, i);
|
|
|
|
/* if file does not exists in destination list, then we can safely unlink it */
|
|
if (parray_bsearch(dest_backup->files, file, pgFileCompareRelPathWithExternal) == NULL)
|
|
{
|
|
char fullpath[MAXPGPATH];
|
|
|
|
join_path_components(fullpath, pgdata_path, file->rel_path);
|
|
|
|
fio_delete(file->mode, fullpath, FIO_DB_HOST);
|
|
elog(VERBOSE, "Deleted file \"%s\"", fullpath);
|
|
|
|
/* shrink pgdata list */
|
|
parray_remove(pgdata_files, i);
|
|
i--;
|
|
}
|
|
}
|
|
|
|
time(&end_time);
|
|
pretty_time_interval(difftime(end_time, start_time),
|
|
pretty_time, lengthof(pretty_time));
|
|
|
|
/* At this point PDATA do not contain files, that do not exists in dest backup file list */
|
|
elog(INFO, "Redundant files are removed, time elapsed: %s", pretty_time);
|
|
}
|
|
|
|
static void threads_handle(pthread_t *threads,
|
|
restore_files_arg *threads_args,
|
|
pgBackup *dest_backup,
|
|
parray *dest_files,
|
|
parray *pgdata_files,
|
|
parray *external_dirs,
|
|
parray *parent_chain,
|
|
pgRestoreParams *params,
|
|
const char *pgdata_path,
|
|
bool use_bitmap,
|
|
size_t total_bytes)
|
|
{
|
|
int i = 0;
|
|
size_t dest_bytes = 0;
|
|
char pretty_dest_bytes[20];
|
|
char pretty_total_bytes[20];
|
|
char pretty_time[20];
|
|
time_t start_time, end_time;
|
|
bool restore_isok = true;
|
|
|
|
if (dest_backup->stream)
|
|
dest_bytes = dest_backup->pgdata_bytes + dest_backup->wal_bytes;
|
|
else
|
|
dest_bytes = dest_backup->pgdata_bytes;
|
|
|
|
pretty_size(dest_bytes, pretty_dest_bytes, lengthof(pretty_dest_bytes));
|
|
elog(INFO, "Start restoring backup files. PGDATA size: %s", pretty_dest_bytes);
|
|
time(&start_time);
|
|
thread_interrupted = false;
|
|
|
|
/* Restore files into target directory */
|
|
for (i = 0; i < num_threads; i++)
|
|
{
|
|
restore_files_arg *arg = &(threads_args[i]);
|
|
|
|
arg->dest_files = dest_files;
|
|
arg->pgdata_files = pgdata_files;
|
|
arg->dest_backup = dest_backup;
|
|
arg->dest_external_dirs = external_dirs;
|
|
arg->parent_chain = parent_chain;
|
|
arg->skip_external_dirs = params->skip_external_dirs;
|
|
arg->to_root = pgdata_path;
|
|
arg->use_bitmap = use_bitmap;
|
|
arg->incremental_mode = params->incremental_mode;
|
|
arg->shift_lsn = params->shift_lsn;
|
|
threads_args[i].restored_bytes = 0;
|
|
/* By default there are some error */
|
|
threads_args[i].ret = 1;
|
|
|
|
/* Useless message TODO: rewrite */
|
|
elog(LOG, "Start thread %i", i + 1);
|
|
|
|
pthread_create(&threads[i], NULL, restore_files, arg);
|
|
}
|
|
|
|
/* Wait theads */
|
|
for (i = 0; i < num_threads; i++)
|
|
{
|
|
pthread_join(threads[i], NULL);
|
|
if (threads_args[i].ret == 1)
|
|
restore_isok = false;
|
|
|
|
total_bytes += threads_args[i].restored_bytes;
|
|
}
|
|
|
|
time(&end_time);
|
|
pretty_time_interval(difftime(end_time, start_time),
|
|
pretty_time, lengthof(pretty_time));
|
|
pretty_size(total_bytes, pretty_total_bytes, lengthof(pretty_total_bytes));
|
|
|
|
if (restore_isok)
|
|
{
|
|
elog(INFO, "Backup files are restored. Transfered bytes: %s, time elapsed: %s",
|
|
pretty_total_bytes, pretty_time);
|
|
|
|
elog(INFO, "Restore incremental ratio (less is better): %.f%% (%s/%s)",
|
|
((float) total_bytes / dest_bytes) * 100,
|
|
pretty_total_bytes, pretty_dest_bytes);
|
|
}
|
|
else
|
|
elog(ERROR, "Backup files restoring failed. Transfered bytes: %s, time elapsed: %s",
|
|
pretty_total_bytes, pretty_time);
|
|
}
|
|
|
|
static void sync_restored_files(parray *dest_files,
|
|
parray *external_dirs,
|
|
pgRestoreParams *params,
|
|
const char *pgdata_path)
|
|
{
|
|
char pretty_time[20];
|
|
time_t start_time, end_time;
|
|
|
|
elog(INFO, "Syncing restored files to disk");
|
|
time(&start_time);
|
|
|
|
for (size_t i = 0; i < parray_num(dest_files); i++)
|
|
{
|
|
char to_fullpath[MAXPGPATH];
|
|
pgFile *dest_file = (pgFile *)parray_get(dest_files, i);
|
|
|
|
if (S_ISDIR(dest_file->mode))
|
|
continue;
|
|
|
|
/* skip external files if ordered to do so */
|
|
if (dest_file->external_dir_num > 0 &&
|
|
params->skip_external_dirs)
|
|
continue;
|
|
|
|
/* construct fullpath */
|
|
if (dest_file->external_dir_num == 0)
|
|
{
|
|
if (strcmp(PG_TABLESPACE_MAP_FILE, dest_file->rel_path) == 0)
|
|
continue;
|
|
if (strcmp(DATABASE_MAP, dest_file->rel_path) == 0)
|
|
continue;
|
|
join_path_components(to_fullpath, pgdata_path, dest_file->rel_path);
|
|
}
|
|
else
|
|
{
|
|
char *external_path = (char *)parray_get(external_dirs, dest_file->external_dir_num - 1);
|
|
join_path_components(to_fullpath, external_path, dest_file->rel_path);
|
|
}
|
|
|
|
/* TODO: write test for case: file to be synced is missing */
|
|
if (fio_sync(to_fullpath, FIO_DB_HOST) != 0)
|
|
elog(ERROR, "Failed to sync file \"%s\": %s", to_fullpath, strerror(errno));
|
|
}
|
|
|
|
time(&end_time);
|
|
pretty_time_interval(difftime(end_time, start_time),
|
|
pretty_time, lengthof(pretty_time));
|
|
elog(INFO, "Restored backup files are synced, time elapsed: %s", pretty_time);
|
|
}
|
|
|
|
inline void RestoreCompressFile(FILE *out, char *to_fullpath, size_t pathLen, pgFile *dest_file)
|
|
{
|
|
if (dest_file->is_datafile && dest_file->compressedFile && !dest_file->is_cfs) {
|
|
if (!fio_is_remote_file(out)) {
|
|
auto result = ConstructCompressedFile(to_fullpath, dest_file->segno, dest_file->compressedChunkSize,
|
|
dest_file->compressedAlgorithm);
|
|
if (result != SUCCESS) {
|
|
elog(ERROR, "Cannot copy compressed file \"%s\": %s", to_fullpath, strerror(errno));
|
|
}
|
|
} else {
|
|
CompressCommunicate communicate;
|
|
errno_t rc = memcpy_s(communicate.path, MAXPGPATH, to_fullpath, MAXPGPATH);
|
|
securec_check(rc, "", "");
|
|
communicate.chunkSize = dest_file->compressedChunkSize;
|
|
communicate.segmentNo = dest_file->segno;
|
|
communicate.algorithm = dest_file->compressedAlgorithm;
|
|
fio_construct_compressed((void*)&communicate, sizeof(communicate));
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Restore files into $PGDATA.
|
|
*/
|
|
static void *
|
|
restore_files(void *arg)
|
|
{
|
|
int i;
|
|
uint64 n_files;
|
|
char to_fullpath[MAXPGPATH];
|
|
FILE *out = NULL;
|
|
char *out_buf = (char *)pgut_malloc(STDIO_BUFSIZE);
|
|
|
|
restore_files_arg *arguments = (restore_files_arg *) arg;
|
|
|
|
n_files = (unsigned long) parray_num(arguments->dest_files);
|
|
|
|
for (i = 0; (size_t)i < parray_num(arguments->dest_files); i++)
|
|
{
|
|
bool already_exists = false;
|
|
PageState *checksum_map = NULL; /* it should take ~1.5MB at most */
|
|
datapagemap_t *lsn_map = NULL; /* it should take 16kB at most */
|
|
pgFile *dest_file = (pgFile *)parray_get(arguments->dest_files, i);
|
|
|
|
/* Directories were created before */
|
|
if (S_ISDIR(dest_file->mode))
|
|
continue;
|
|
|
|
if (!pg_atomic_test_set_flag(&dest_file->lock))
|
|
continue;
|
|
|
|
/* check for interrupt */
|
|
if (interrupted || thread_interrupted)
|
|
elog(ERROR, "Interrupted during restore");
|
|
|
|
if (progress)
|
|
elog(INFO, "Progress: (%d/%lu). Restore file \"%s\"",
|
|
i + 1, n_files, dest_file->rel_path);
|
|
|
|
/* Do not restore tablespace_map file */
|
|
if ((dest_file->external_dir_num == 0) &&
|
|
strcmp(PG_TABLESPACE_MAP_FILE, dest_file->rel_path) == 0)
|
|
{
|
|
elog(VERBOSE, "Skip tablespace_map");
|
|
continue;
|
|
}
|
|
|
|
/* Do not restore database_map file */
|
|
if ((dest_file->external_dir_num == 0) &&
|
|
strcmp(DATABASE_MAP, dest_file->rel_path) == 0)
|
|
{
|
|
elog(VERBOSE, "Skip database_map");
|
|
continue;
|
|
}
|
|
|
|
/* Do no restore external directory file if a user doesn't want */
|
|
if (arguments->skip_external_dirs && dest_file->external_dir_num > 0)
|
|
continue;
|
|
|
|
/* set fullpath of destination file */
|
|
if (dest_file->external_dir_num == 0)
|
|
join_path_components(to_fullpath, arguments->to_root, dest_file->rel_path);
|
|
else
|
|
{
|
|
char *external_path = (char *)parray_get(arguments->dest_external_dirs,
|
|
dest_file->external_dir_num - 1);
|
|
join_path_components(to_fullpath, external_path, dest_file->rel_path);
|
|
}
|
|
|
|
if (arguments->incremental_mode != INCR_NONE &&
|
|
parray_bsearch(arguments->pgdata_files, dest_file, pgFileCompareRelPathWithExternalDesc))
|
|
{
|
|
already_exists = true;
|
|
}
|
|
|
|
/*
|
|
* Handle incremental restore case for data files.
|
|
* If file is already exists in pgdata, then
|
|
* we scan it block by block and get
|
|
* array of checksums for every page.
|
|
*/
|
|
if (already_exists &&
|
|
dest_file->is_datafile && !dest_file->is_cfs &&
|
|
dest_file->n_blocks > 0)
|
|
{
|
|
if (arguments->incremental_mode == INCR_LSN)
|
|
{
|
|
lsn_map = fio_get_lsn_map(to_fullpath, arguments->dest_backup->checksum_version,
|
|
dest_file->n_blocks, arguments->shift_lsn,
|
|
dest_file->segno * RELSEG_SIZE, FIO_DB_HOST);
|
|
}
|
|
else if (arguments->incremental_mode == INCR_CHECKSUM)
|
|
{
|
|
checksum_map = fio_get_checksum_map(to_fullpath, arguments->dest_backup->checksum_version,
|
|
dest_file->n_blocks, arguments->dest_backup->stop_lsn,
|
|
dest_file->segno * RELSEG_SIZE, FIO_DB_HOST);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Open dest file and truncate it to zero, if destination
|
|
* file already exists and dest file size is zero, or
|
|
* if file do not exist
|
|
*/
|
|
if ((already_exists && dest_file->write_size == 0) || !already_exists)
|
|
out = fio_fopen(to_fullpath, PG_BINARY_W, FIO_DB_HOST);
|
|
/*
|
|
* If file already exists and dest size is not zero,
|
|
* then open it for reading and writing.
|
|
*/
|
|
else
|
|
out = fio_fopen(to_fullpath, PG_BINARY_R "+", FIO_DB_HOST);
|
|
|
|
if (out == NULL)
|
|
elog(ERROR, "Cannot open restore target file \"%s\": %s",
|
|
to_fullpath, strerror(errno));
|
|
|
|
/* update file permission */
|
|
if (fio_chmod(to_fullpath, dest_file->mode, FIO_DB_HOST) == -1)
|
|
elog(ERROR, "Cannot change mode of \"%s\": %s", to_fullpath,
|
|
strerror(errno));
|
|
|
|
|
|
|
|
// If destination file is 0 sized, then just close it and go for the next
|
|
if (dest_file->write_size == 0)
|
|
goto done;
|
|
|
|
/* Restore destination file */
|
|
if (dest_file->is_datafile && !dest_file->is_cfs)
|
|
{
|
|
/* enable stdio buffering for local destination data file */
|
|
if (!fio_is_remote_file(out))
|
|
setvbuf(out, out_buf, _IOFBF, STDIO_BUFSIZE);
|
|
/* Destination file is data file */
|
|
arguments->restored_bytes += restore_data_file(arguments->parent_chain,
|
|
dest_file, out, to_fullpath,
|
|
arguments->use_bitmap, checksum_map,
|
|
arguments->shift_lsn, lsn_map, true);
|
|
}
|
|
else
|
|
{
|
|
/* disable stdio buffering for local destination nonedata file */
|
|
if (!fio_is_remote_file(out))
|
|
setvbuf(out, NULL, _IONBF, BUFSIZ);
|
|
/* Destination file is nonedata file */
|
|
arguments->restored_bytes += restore_non_data_file(arguments->parent_chain,
|
|
arguments->dest_backup, dest_file, out, to_fullpath,
|
|
already_exists);
|
|
}
|
|
|
|
done:
|
|
/* close file */
|
|
if (fio_fclose(out) != 0)
|
|
elog(ERROR, "Cannot close file \"%s\": %s", to_fullpath,
|
|
strerror(errno));
|
|
|
|
RestoreCompressFile(out, to_fullpath, MAXPGPATH, dest_file);
|
|
/* free pagemap used for restore optimization */
|
|
pg_free(dest_file->pagemap.bitmap);
|
|
|
|
if (lsn_map)
|
|
pg_free(lsn_map->bitmap);
|
|
|
|
pg_free(lsn_map);
|
|
pg_free(checksum_map);
|
|
}
|
|
|
|
free(out_buf);
|
|
|
|
/* ssh connection to longer needed */
|
|
fio_disconnect();
|
|
|
|
/* Data files restoring is successful */
|
|
arguments->ret = 0;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* Create recovery.conf (probackup_recovery.conf in case of PG12)
|
|
* with given recovery target parameters
|
|
*/
|
|
static void
|
|
create_recovery_conf(time_t backup_id,
|
|
pgRecoveryTarget *rt,
|
|
pgBackup *backup,
|
|
pgRestoreParams *params)
|
|
{
|
|
char path[MAXPGPATH];
|
|
FILE *fp;
|
|
bool pitr_requested;
|
|
bool target_latest;
|
|
bool target_immediate;
|
|
bool restore_command_provided = false;
|
|
errno_t rc = 0;
|
|
|
|
if (instance_config.restore_command &&
|
|
(pg_strcasecmp(instance_config.restore_command, "none") != 0))
|
|
{
|
|
restore_command_provided = true;
|
|
}
|
|
|
|
/* restore-target='latest' support */
|
|
target_latest = rt->target_stop != NULL &&
|
|
strcmp(rt->target_stop, "latest") == 0;
|
|
|
|
target_immediate = rt->target_stop != NULL &&
|
|
strcmp(rt->target_stop, "immediate") == 0;
|
|
|
|
/*
|
|
* Note that setting restore_command alone interpreted
|
|
* as PITR with target - "until all available WAL is replayed".
|
|
* We do this because of the following case:
|
|
* The user is restoring STREAM backup as replica but
|
|
* also relies on WAL archive to catch-up with master.
|
|
* If restore_command is provided, then it should be
|
|
* added to recovery config.
|
|
* In this scenario, "would be" replica will replay
|
|
* all WAL segments available in WAL archive, after that
|
|
* it will try to connect to master via repprotocol.
|
|
*
|
|
* The risk is obvious, what if masters current state is
|
|
* in "the past" relatively to latest state in the archive?
|
|
* We will get a replica that is "in the future" to the master.
|
|
* We accept this risk because its probability is low.
|
|
*/
|
|
pitr_requested = !backup->stream || rt->time_string ||
|
|
rt->xid_string || rt->lsn_string || rt->target_name ||
|
|
target_immediate || target_latest || restore_command_provided;
|
|
|
|
/* No need to generate recovery.conf at all. */
|
|
if (!pitr_requested)
|
|
{
|
|
/*
|
|
* Restoring STREAM backup without PITR and not as replica,
|
|
* recovery.signal and standby.signal for PG12 are not needed
|
|
*
|
|
* We do not add "include" option in this case because
|
|
* here we are creating empty "probackup_recovery.conf"
|
|
* to handle possible already existing "include"
|
|
* directive pointing to "probackup_recovery.conf".
|
|
* If don`t do that, recovery will fail.
|
|
*/
|
|
pg12_recovery_config(backup, false);
|
|
return;
|
|
}
|
|
|
|
elog(LOG, "----------------------------------------");
|
|
#if PG_VERSION_NUM >= 120000
|
|
elog(LOG, "creating probackup_recovery.conf");
|
|
pg12_recovery_config(backup, true);
|
|
rc = snprintf_s(path, lengthof(path), lengthof(path) - 1, "%s/probackup_recovery.conf", instance_config.pgdata);
|
|
securec_check_ss_c(rc, "\0", "\0");
|
|
#else
|
|
elog(LOG, "creating recovery.conf");
|
|
rc = snprintf_s(path, lengthof(path), lengthof(path) - 1, "%s/recovery.conf", instance_config.pgdata);
|
|
securec_check_ss_c(rc, "\0", "\0");
|
|
#endif
|
|
|
|
fp = fio_fopen(path, "w", FIO_DB_HOST);
|
|
if (fp == NULL)
|
|
elog(ERROR, "cannot open file \"%s\": %s", path,
|
|
strerror(errno));
|
|
|
|
if (fio_chmod(path, FILE_PERMISSION, FIO_DB_HOST) == -1)
|
|
elog(ERROR, "Cannot change mode of \"%s\": %s", path, strerror(errno));
|
|
|
|
#if PG_VERSION_NUM >= 120000
|
|
fio_fprintf(fp, "# probackup_recovery.conf generated by gs_probackup %s\n",
|
|
PROGRAM_VERSION);
|
|
#else
|
|
fio_fprintf(fp, "# recovery.conf generated by gs_probackup %s\n",
|
|
PROGRAM_VERSION);
|
|
#endif
|
|
|
|
/* construct restore_command */
|
|
if (pitr_requested)
|
|
{
|
|
char *timestamp = NULL;
|
|
const char *oldtime = NULL;
|
|
timestamp = (char *)pg_malloc(RESTORE_ARRAY_LEN);
|
|
time2iso(timestamp, RESTORE_ARRAY_LEN, backup->end_time);
|
|
oldtime = rt->time_string;
|
|
if (rt->time_string) {
|
|
rt->time_string = timestamp;
|
|
}
|
|
construct_restore_cmd(fp, rt, restore_command_provided, target_immediate);
|
|
rt->time_string = oldtime;
|
|
free(timestamp);
|
|
}
|
|
|
|
if (fio_fflush(fp) != 0 ||
|
|
fio_fclose(fp))
|
|
elog(ERROR, "cannot write file \"%s\": %s", path,
|
|
strerror(errno));
|
|
|
|
#if PG_VERSION_NUM >= 120000
|
|
/*
|
|
* Create "recovery.signal" to mark this recovery as PITR for openGauss.
|
|
* In older versions presense of recovery.conf alone was enough.
|
|
* To keep behaviour consistent with older versions,
|
|
* we are forced to create "recovery.signal"
|
|
* even when only restore_command is provided.
|
|
* Presense of "recovery.signal" by itself determine only
|
|
* one thing: do openGauss must switch to a new timeline
|
|
* after successfull recovery or not?
|
|
*/
|
|
if (pitr_requested)
|
|
{
|
|
elog(LOG, "creating recovery.signal file");
|
|
rc = snprintf_s(path, lengthof(path), lengthof(path) - 1, "%s/recovery.signal",
|
|
instance_config.pgdata);
|
|
securec_check_ss_c(rc, "\0", "\0");
|
|
fp = fio_fopen(path, "w", FIO_DB_HOST);
|
|
if (fp == NULL)
|
|
elog(ERROR, "cannot open file \"%s\": %s", path,
|
|
strerror(errno));
|
|
|
|
if (fio_fflush(fp) != 0 ||
|
|
fio_fclose(fp))
|
|
elog(ERROR, "cannot write file \"%s\": %s", path,
|
|
strerror(errno));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void construct_restore_cmd(FILE *fp, pgRecoveryTarget *rt,
|
|
bool restore_command_provided,
|
|
bool target_immediate)
|
|
{
|
|
char restore_command_guc[16384];
|
|
errno_t rc = 0;
|
|
|
|
fio_fprintf(fp, "\n## recovery settings\n");
|
|
/* If restore_command is provided, use it. Otherwise construct it from scratch. */
|
|
if (restore_command_provided)
|
|
{
|
|
rc = sprintf_s(restore_command_guc, sizeof(restore_command_guc), "%s",
|
|
instance_config.restore_command);
|
|
securec_check_ss_c(rc, "\0", "\0");
|
|
}
|
|
else
|
|
{
|
|
/* default cmdline, ok for local restore */
|
|
rc = sprintf_s(restore_command_guc, sizeof(restore_command_guc),
|
|
"%s archive-get -B %s --instance %s "
|
|
"--wal-file-path=%%p --wal-file-name=%%f",
|
|
PROGRAM_FULL_PATH ? PROGRAM_FULL_PATH : PROGRAM_NAME,
|
|
backup_path, instance_name);
|
|
securec_check_ss_c(rc, "\0", "\0");
|
|
/* append --remote-* parameters provided via --archive-* settings */
|
|
if (instance_config.archive.host)
|
|
{
|
|
rc = strcat_s(restore_command_guc, sizeof(restore_command_guc), " --remote-host=");
|
|
securec_check_c(rc, "\0", "\0");
|
|
rc = strcat_s(restore_command_guc, sizeof(restore_command_guc), instance_config.archive.host);
|
|
securec_check_c(rc, "\0", "\0");
|
|
}
|
|
|
|
if (instance_config.archive.port)
|
|
{
|
|
rc = strcat_s(restore_command_guc, sizeof(restore_command_guc), " --remote-port=");
|
|
securec_check_c(rc, "\0", "\0");
|
|
rc = strcat_s(restore_command_guc, sizeof(restore_command_guc), instance_config.archive.port);
|
|
securec_check_c(rc, "\0", "\0");
|
|
}
|
|
|
|
if (instance_config.archive.user)
|
|
{
|
|
rc = strcat_s(restore_command_guc, sizeof(restore_command_guc), " --remote-user=");
|
|
securec_check_c(rc, "\0", "\0");
|
|
rc = strcat_s(restore_command_guc, sizeof(restore_command_guc), instance_config.archive.user);
|
|
securec_check_c(rc, "\0", "\0");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We've already checked that only one of the four following mutually
|
|
* exclusive options is specified, so the order of calls is insignificant.
|
|
*/
|
|
if (rt->target_name)
|
|
fio_fprintf(fp, "recovery_target_name = '%s'\n", rt->target_name);
|
|
|
|
if (rt->time_string)
|
|
fio_fprintf(fp, "recovery_target_time = '%s'\n", rt->time_string);
|
|
|
|
if (rt->xid_string)
|
|
fio_fprintf(fp, "recovery_target_xid = '%s'\n", rt->xid_string);
|
|
|
|
if (rt->lsn_string)
|
|
fio_fprintf(fp, "recovery_target_lsn = '%s'\n", rt->lsn_string);
|
|
|
|
if (rt->target_stop && target_immediate)
|
|
fio_fprintf(fp, "recovery_target = '%s'\n", rt->target_stop);
|
|
|
|
if (rt->inclusive_specified)
|
|
fio_fprintf(fp, "recovery_target_inclusive = '%s'\n",
|
|
rt->target_inclusive ? "true" : "false");
|
|
|
|
(void)fio_fprintf(fp, "pause_at_recovery_target = '%s'\n", "false");
|
|
|
|
if (rt->target_tli)
|
|
fio_fprintf(fp, "recovery_target_timeline = '%u'\n", rt->target_tli);
|
|
else
|
|
{
|
|
/*
|
|
* In PG12 default recovery target timeline was changed to 'latest', which
|
|
* is extremely risky. Explicitly preserve old behavior of recovering to current
|
|
* timneline for PG12.
|
|
*/
|
|
#if PG_VERSION_NUM >= 120000
|
|
fio_fprintf(fp, "recovery_target_timeline = 'current'\n");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Create empty probackup_recovery.conf in PGDATA and
|
|
* add "include" directive to postgresql.auto.conf
|
|
|
|
* When restoring PG12 we always(!) must do this, even
|
|
* when restoring STREAM backup without PITR or replica options
|
|
* because restored instance may have been previously backed up
|
|
* and restored again and user didn`t cleaned up postgresql.auto.conf.
|
|
|
|
* So for recovery to work regardless of all this factors
|
|
* we must always create empty probackup_recovery.conf file.
|
|
*/
|
|
static void
|
|
pg12_recovery_config(pgBackup *backup, bool add_include)
|
|
{
|
|
#if PG_VERSION_NUM >= 120000
|
|
char probackup_recovery_path[MAXPGPATH];
|
|
char postgres_auto_path[MAXPGPATH];
|
|
FILE *fp;
|
|
errno_t rc = 0;
|
|
|
|
if (add_include)
|
|
{
|
|
char current_time_str[100];
|
|
|
|
time2iso(current_time_str, lengthof(current_time_str), current_time);
|
|
|
|
rc = snprintf_s(postgres_auto_path, lengthof(postgres_auto_path), lengthof(postgres_auto_path) - 1,
|
|
"%s/postgresql.auto.conf", instance_config.pgdata);
|
|
securec_check_ss_c(rc, "\0", "\0");
|
|
fp = fio_fopen(postgres_auto_path, "a", FIO_DB_HOST);
|
|
if (fp == NULL)
|
|
elog(ERROR, "cannot write to file \"%s\": %s", postgres_auto_path,
|
|
strerror(errno));
|
|
|
|
// TODO: check if include 'probackup_recovery.conf' already exists
|
|
fio_fprintf(fp, "\n# created by gs_probackup restore of backup %s at '%s'\n",
|
|
base36enc(backup->start_time), current_time_str);
|
|
fio_fprintf(fp, "include '%s'\n", "probackup_recovery.conf");
|
|
|
|
if (fio_fflush(fp) != 0 ||
|
|
fio_fclose(fp))
|
|
elog(ERROR, "cannot write to file \"%s\": %s", postgres_auto_path,
|
|
strerror(errno));
|
|
}
|
|
|
|
/* Create empty probackup_recovery.conf */
|
|
rc = snprintf_s(probackup_recovery_path, lengthof(probackup_recovery_path), lengthof(probackup_recovery_path) - 1,
|
|
"%s/probackup_recovery.conf", instance_config.pgdata);
|
|
securec_check_ss_c(rc, "\0", "\0");
|
|
fp = fio_fopen(probackup_recovery_path, "w", FIO_DB_HOST);
|
|
if (fp == NULL)
|
|
elog(ERROR, "cannot open file \"%s\": %s", probackup_recovery_path,
|
|
strerror(errno));
|
|
|
|
if (fio_fflush(fp) != 0 ||
|
|
fio_fclose(fp))
|
|
elog(ERROR, "cannot write to file \"%s\": %s", probackup_recovery_path,
|
|
strerror(errno));
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Try to read a timeline's history file.
|
|
*
|
|
* If successful, return the list of component TLIs (the ancestor
|
|
* timelines followed by target timeline). If we cannot find the history file,
|
|
* assume that the timeline has no parents, and return a list of just the
|
|
* specified timeline ID.
|
|
* based on readTimeLineHistory() in timeline.c
|
|
*/
|
|
parray *
|
|
read_timeline_history(const char *arclog_path, TimeLineID targetTLI, bool strict)
|
|
{
|
|
#ifdef SUPPORT_MULTI_TIMELINE
|
|
parray *result;
|
|
char path[MAXPGPATH];
|
|
FILE *fd = NULL;
|
|
TimeLineHistoryEntry *entry;
|
|
TimeLineHistoryEntry *last_timeline = NULL;
|
|
|
|
/* Look for timeline history file in archlog_path */
|
|
errno_t rc = snprintf_s(path, lengthof(path), lengthof(path) - 1, "%s/%08X.history", arclog_path,
|
|
targetTLI);
|
|
securec_check_ss_c(rc, "\0", "\0");
|
|
/* Timeline 1 does not have a history file */
|
|
if (targetTLI != 1)
|
|
{
|
|
fd = fopen(path, "rt");
|
|
if (fd == NULL)
|
|
{
|
|
if (errno != ENOENT)
|
|
elog(ERROR, "could not open file \"%s\": %s", path,
|
|
strerror(errno));
|
|
|
|
/* There is no history file for target timeline */
|
|
if (strict)
|
|
elog(ERROR, "recovery target timeline %u does not exist",
|
|
targetTLI);
|
|
else
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
result = parray_new();
|
|
|
|
parse_file(result, fd, entry, last_timeline);
|
|
|
|
if (fd && (ferror(fd)))
|
|
elog(ERROR, "Failed to read from file: \"%s\"", path);
|
|
|
|
if (fd)
|
|
fclose(fd);
|
|
|
|
if (last_timeline && targetTLI <= last_timeline->tli)
|
|
elog(ERROR, "Timeline IDs must be less than child timeline's ID.");
|
|
|
|
/* append target timeline */
|
|
entry = pgut_new(TimeLineHistoryEntry);
|
|
entry->tli = targetTLI;
|
|
/* LSN in target timeline is valid */
|
|
entry->end = InvalidXLogRecPtr;
|
|
parray_insert(result, 0, entry);
|
|
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef SUPPORT_MULTI_TIMELINE
|
|
static void parse_file(parray *result, FILE *fd,
|
|
TimeLineHistoryEntry *entry,
|
|
TimeLineHistoryEntry *last_timeline)
|
|
{
|
|
char fline[MAXPGPATH];
|
|
|
|
/*
|
|
* Parse the file...
|
|
*/
|
|
while (fd && fgets(fline, sizeof(fline), fd) != NULL)
|
|
{
|
|
char *ptr;
|
|
TimeLineID tli;
|
|
uint32 switchpoint_hi;
|
|
uint32 switchpoint_lo;
|
|
int nfields;
|
|
|
|
for (ptr = fline; *ptr; ptr++)
|
|
{
|
|
if (!isspace((unsigned char) *ptr))
|
|
break;
|
|
}
|
|
if (*ptr == '\0' || *ptr == '#')
|
|
continue;
|
|
|
|
nfields = sscanf_s(fline, "%u\t%X/%X", &tli, &switchpoint_hi, &switchpoint_lo);
|
|
|
|
if (nfields < 1)
|
|
{
|
|
/* expect a numeric timeline ID as first field of line */
|
|
elog(ERROR,
|
|
"syntax error in history file: %s. Expected a numeric timeline ID.",
|
|
fline);
|
|
}
|
|
if (nfields != 3)
|
|
elog(ERROR,
|
|
"syntax error in history file: %s. Expected a transaction log switchpoint location.",
|
|
fline);
|
|
|
|
if (last_timeline && tli <= last_timeline->tli)
|
|
elog(ERROR,
|
|
"Timeline IDs must be in increasing sequence.");
|
|
|
|
entry = pgut_new(TimeLineHistoryEntry);
|
|
entry->tli = tli;
|
|
entry->end = ((uint64) switchpoint_hi << 32) | switchpoint_lo;
|
|
|
|
last_timeline = entry;
|
|
/* Build list with newest item first */
|
|
parray_insert(result, 0, entry);
|
|
|
|
/* we ignore the remainder of each line */
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* TODO: do not ignore timelines. What if requested target located in different timeline? */
|
|
bool
|
|
satisfy_recovery_target(const pgBackup *backup, const pgRecoveryTarget *rt)
|
|
{
|
|
if (rt->xid_string)
|
|
return backup->recovery_xid <= rt->target_xid;
|
|
|
|
if (rt->time_string)
|
|
return backup->recovery_time <= rt->target_time;
|
|
|
|
if (rt->lsn_string)
|
|
return backup->stop_lsn <= rt->target_lsn;
|
|
|
|
if (rt->target_name)
|
|
return strcmp(backup->recovery_name, rt->target_name) == 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/* TODO description */
|
|
bool
|
|
satisfy_timeline(const parray *timelines, const pgBackup *backup)
|
|
{
|
|
|
|
#ifdef SUPPORT_MULTI_TIMELINE
|
|
int i;
|
|
|
|
for (i = 0; i < parray_num(timelines); i++)
|
|
{
|
|
TimeLineHistoryEntry *timeline;
|
|
|
|
timeline = (TimeLineHistoryEntry *) parray_get(timelines, i);
|
|
if (backup->tli == timeline->tli &&
|
|
(XLogRecPtrIsInvalid(timeline->end) ||
|
|
backup->stop_lsn <= timeline->end))
|
|
return true;
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
/* timelines represents a history of one particular timeline,
|
|
* we must determine whether a target tli is part of that history.
|
|
*
|
|
* /--------*
|
|
* ---------*-------------->
|
|
*/
|
|
bool
|
|
tliIsPartOfHistory(const parray *timelines, TimeLineID tli)
|
|
{
|
|
#ifdef SUPPORT_MULTI_TIMELINE
|
|
int i;
|
|
|
|
for (i = 0; i < parray_num(timelines); i++)
|
|
{
|
|
TimeLineHistoryEntry *timeline = (TimeLineHistoryEntry *) parray_get(timelines, i);
|
|
|
|
if (tli == timeline->tli)
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Get recovery options in the string format, parse them
|
|
* and fill up the pgRecoveryTarget structure.
|
|
*/
|
|
pgRecoveryTarget *
|
|
parseRecoveryTargetOptions(const char *target_time,
|
|
const char *target_xid,
|
|
const char *target_inclusive,
|
|
TimeLineID target_tli,
|
|
const char *target_lsn,
|
|
const char *target_stop,
|
|
const char *target_name,
|
|
const char *target_action)
|
|
{
|
|
/*
|
|
* count the number of the mutually exclusive options which may specify
|
|
* recovery target. If final value > 1, throw an error.
|
|
*/
|
|
int recovery_target_specified = 0;
|
|
pgRecoveryTarget *rt = pgut_new(pgRecoveryTarget);
|
|
|
|
/* fill all options with default values */
|
|
errno_t rc = memset_s(rt, sizeof(pgRecoveryTarget), 0, sizeof(pgRecoveryTarget));
|
|
securec_check(rc, "\0", "\0");
|
|
/* parse given options */
|
|
if (target_time)
|
|
{
|
|
time_t dummy_time;
|
|
|
|
recovery_target_specified++;
|
|
rt->time_string = target_time;
|
|
|
|
if (parse_time(target_time, &dummy_time, false))
|
|
rt->target_time = dummy_time;
|
|
else
|
|
elog(ERROR, "Invalid value for '--recovery-target-time' option '%s'",
|
|
target_time);
|
|
}
|
|
|
|
if (target_xid)
|
|
{
|
|
TransactionId dummy_xid;
|
|
|
|
recovery_target_specified++;
|
|
rt->xid_string = target_xid;
|
|
|
|
#ifdef PGPRO_EE
|
|
if (parse_uint64(target_xid, &dummy_xid, 0))
|
|
#else
|
|
if (parse_uint64(target_xid,&dummy_xid, 0))
|
|
#endif
|
|
rt->target_xid = dummy_xid;
|
|
else
|
|
elog(ERROR, "Invalid value for '--recovery-target-xid' option '%s'",
|
|
target_xid);
|
|
}
|
|
|
|
if (target_lsn)
|
|
{
|
|
XLogRecPtr dummy_lsn;
|
|
|
|
recovery_target_specified++;
|
|
rt->lsn_string = target_lsn;
|
|
if (parse_lsn(target_lsn, &dummy_lsn))
|
|
rt->target_lsn = dummy_lsn;
|
|
else
|
|
elog(ERROR, "Invalid value of '--recovery-target-lsn' option '%s'",
|
|
target_lsn);
|
|
}
|
|
|
|
rt->target_tli = target_tli;
|
|
|
|
if (target_name)
|
|
{
|
|
recovery_target_specified++;
|
|
rt->target_name = target_name;
|
|
}
|
|
|
|
parse_other_options(rt, recovery_target_specified,
|
|
target_inclusive, target_stop, target_action);
|
|
|
|
/* More than one mutually exclusive option was defined. */
|
|
if (recovery_target_specified > 1)
|
|
elog(ERROR, "At most one of '--recovery-target', '--recovery-target-name', "
|
|
"'--recovery-target-time', '--recovery-target-xid' or "
|
|
"'--recovery-target-lsn' options can be specified");
|
|
|
|
/*
|
|
* If none of the options is defined, '--recovery-target-inclusive' option
|
|
* is meaningless.
|
|
*/
|
|
if (!(rt->xid_string || rt->time_string || rt->lsn_string) &&
|
|
rt->target_inclusive)
|
|
elog(ERROR, "The '--recovery-target-inclusive' option can be applied only when "
|
|
"either of '--recovery-target-time', '--recovery-target-xid' or "
|
|
"'--recovery-target-lsn' options is specified");
|
|
|
|
/* If none of the options is defined, '--recovery-target-action' is meaningless */
|
|
if (rt->target_action && recovery_target_specified == 0)
|
|
elog(ERROR, "The '--recovery-target-action' option can be applied only when "
|
|
"either of '--recovery-target', '--recovery-target-time', '--recovery-target-xid', "
|
|
"'--recovery-target-lsn' or '--recovery-target-name' options is specified");
|
|
|
|
/* TODO: sanity for recovery-target-timeline */
|
|
|
|
return rt;
|
|
}
|
|
|
|
static void parse_other_options(pgRecoveryTarget *rt,
|
|
int recovery_target_specified,
|
|
const char *target_inclusive,
|
|
const char *target_stop,
|
|
const char *target_action)
|
|
{
|
|
bool dummy_bool = false;
|
|
|
|
if (target_inclusive)
|
|
{
|
|
rt->inclusive_specified = true;
|
|
if (parse_bool(target_inclusive, &dummy_bool))
|
|
rt->target_inclusive = dummy_bool;
|
|
else
|
|
elog(ERROR, "Invalid value for '--recovery-target-inclusive' option '%s'",
|
|
target_inclusive);
|
|
}
|
|
|
|
if (target_stop)
|
|
{
|
|
if ((strcmp(target_stop, "immediate") != 0)
|
|
&& (strcmp(target_stop, "latest") != 0))
|
|
elog(ERROR, "Invalid value for '--recovery-target' option '%s'",
|
|
target_stop);
|
|
|
|
recovery_target_specified++;
|
|
rt->target_stop = target_stop;
|
|
}
|
|
|
|
if (target_action)
|
|
{
|
|
if ((strcmp(target_action, "pause") != 0)
|
|
&& (strcmp(target_action, "promote") != 0)
|
|
&& (strcmp(target_action, "shutdown") != 0))
|
|
elog(ERROR, "Invalid value for '--recovery-target-action' option '%s'",
|
|
target_action);
|
|
|
|
rt->target_action = target_action;
|
|
}
|
|
}
|
|
|
|
/* Check that instance is suitable for incremental restore
|
|
* Depending on type of incremental restore requirements are differs.
|
|
*/
|
|
void
|
|
check_incremental_compatibility(const char *pgdata, uint64 system_identifier,
|
|
IncrRestoreMode incremental_mode)
|
|
{
|
|
uint64 system_id_pgdata;
|
|
bool success = true;
|
|
pid_t pid;
|
|
char backup_label[MAXPGPATH];
|
|
errno_t rc = 0;
|
|
|
|
/* slurp pg_control and check that system ID is the same */
|
|
/* check that instance is not running */
|
|
/* if lsn_based, check that there is no backup_label files is around AND
|
|
* get redo point lsn from destination pg_control.
|
|
|
|
* It is really important to be sure that pg_control is in cohesion with
|
|
* data files content, because based on pg_control information we will
|
|
* choose a backup suitable for lsn based incremental restore.
|
|
*/
|
|
|
|
system_id_pgdata = get_system_identifier(pgdata);
|
|
|
|
if (system_id_pgdata != instance_config.system_identifier)
|
|
{
|
|
elog(WARNING, "Backup catalog was initialized for system id %lu, "
|
|
"but destination directory system id is %lu",
|
|
system_identifier, system_id_pgdata);
|
|
success = false;
|
|
}
|
|
|
|
/* check postmaster pid */
|
|
pid = fio_check_postmaster(pgdata, FIO_DB_HOST);
|
|
|
|
if (pid == 1) /* postmaster.pid is mangled */
|
|
{
|
|
char pid_file[MAXPGPATH];
|
|
|
|
rc = snprintf_s(pid_file, MAXPGPATH, MAXPGPATH - 1, "%s/postmaster.pid", pgdata);
|
|
securec_check_ss_c(rc, "\0", "\0");
|
|
elog(WARNING, "Pid file \"%s\" is mangled, cannot determine whether postmaster is running or not",
|
|
pid_file);
|
|
success = false;
|
|
}
|
|
else if (pid > 1) /* postmaster is up */
|
|
{
|
|
elog(WARNING, "Postmaster with pid %u is running in destination directory \"%s\"",
|
|
pid, pgdata);
|
|
success = false;
|
|
}
|
|
|
|
/*
|
|
* TODO: maybe there should be some other signs, pointing to pg_control
|
|
* desynchronization with cluster state.
|
|
*/
|
|
if (incremental_mode == INCR_LSN)
|
|
{
|
|
rc = snprintf_s(backup_label, MAXPGPATH, MAXPGPATH - 1, "%s/backup_label", pgdata);
|
|
securec_check_ss_c(rc, "\0", "\0");
|
|
if (fio_access(backup_label, F_OK, FIO_DB_HOST) == 0)
|
|
{
|
|
elog(WARNING, "Destination directory contains \"backup_control\" file. "
|
|
"This does NOT mean that you should delete this file and retry, only that "
|
|
"incremental restore in 'lsn' mode may produce incorrect result, when applied "
|
|
"to cluster with pg_control not synchronized with cluster state."
|
|
"Consider to use incremental restore in 'checksum' mode");
|
|
success = false;
|
|
}
|
|
}
|
|
|
|
if (!success)
|
|
elog(ERROR, "Incremental restore is impossible");
|
|
}
|