diff --git a/doc/pgprobackup.xml b/doc/pgprobackup.xml index ccc81fe2..efd73ee9 100644 --- a/doc/pgprobackup.xml +++ b/doc/pgprobackup.xml @@ -1880,6 +1880,14 @@ pg_probackup restore -B backup_dir --instance template1 databases are always restored. + + + Due to how recovery works in PostgreSQL < 12 it is advisable to + disable option, when running partial + restore of PostgreSQL cluster of version less than . + Otherwise recovery may fail. + + @@ -4411,6 +4419,10 @@ pg_probackup archive-get -B backup_dir --instance Specifies the timestamp up to which recovery will proceed. + If timezone offset is not specified, local timezone is used. + + + Example: --recovery-target-time='2020-01-01 00:00:00+03' @@ -4597,6 +4609,7 @@ pg_probackup archive-get -B backup_dir --instance Specifies the timestamp up to which the backup will stay pinned. Must be an ISO-8601 complaint timestamp. + If timezone offset is not specified, local timezone is used. Example: --expire-time='2020-01-01 00:00:00+03' diff --git a/gen_probackup_project.pl b/gen_probackup_project.pl index b78f4699..abc779a4 100644 --- a/gen_probackup_project.pl +++ b/gen_probackup_project.pl @@ -167,6 +167,7 @@ sub build_pgprobackup 'pg_probackup.c', 'restore.c', 'show.c', + 'stream.c', 'util.c', 'validate.c', 'checkdb.c', diff --git a/src/backup.c b/src/backup.c index 17dedcae..7731c8af 100644 --- a/src/backup.c +++ b/src/backup.c @@ -133,7 +133,7 @@ do_backup_instance(PGconn *backup_conn, PGNodeInfo *nodeInfo, bool no_sync, bool pg_ptrack_clear(backup_conn, nodeInfo->ptrack_version_num); /* notify start of backup to PostgreSQL server */ - time2iso(label, lengthof(label), current.start_time); + time2iso(label, lengthof(label), current.start_time, false); strncat(label, " with pg_probackup", lengthof(label) - strlen(" with pg_probackup")); @@ -572,7 +572,6 @@ do_backup_instance(PGconn *backup_conn, PGNodeInfo *nodeInfo, bool no_sync, bool * NOTHING TO DO HERE */ - /* write database map to file and add it to control file */ if (database_map) { @@ -765,7 +764,7 @@ do_backup(time_t start_time, pgSetBackupParams *set_backup_params, /* Create backup directory and BACKUP_CONTROL_FILE */ if (pgBackupCreateDir(¤t)) elog(ERROR, "Cannot create backup directory"); - if (!lock_backup(¤t, true)) + if (!lock_backup(¤t, true, true)) elog(ERROR, "Cannot lock backup %s directory", base36enc(current.start_time)); write_backup(¤t, true); @@ -1383,7 +1382,7 @@ wait_wal_lsn(XLogRecPtr target_lsn, bool is_start_lsn, TimeLineID tli, XLogRecPtr res; res = get_prior_record_lsn(wal_segment_dir, current.start_lsn, target_lsn, tli, - in_prev_segment, instance_config.xlog_seg_size); + in_prev_segment, instance_config.xlog_seg_size); if (!XLogRecPtrIsInvalid(res)) { diff --git a/src/catalog.c b/src/catalog.c index 3525524a..b3b01877 100644 --- a/src/catalog.c +++ b/src/catalog.c @@ -24,9 +24,14 @@ static pgBackup* get_oldest_backup(timelineInfo *tlinfo); static const char *backupModes[] = {"", "PAGE", "PTRACK", "DELTA", "FULL"}; static pgBackup *readBackupControlFile(const char *path); -static bool exit_hook_registered = false; +static bool backup_lock_exit_hook_registered = false; static parray *lock_files = NULL; +static int lock_backup_exclusive(pgBackup *backup, bool strict); +static bool lock_backup_internal(pgBackup *backup, bool exclusive); +static bool lock_backup_read_only(pgBackup *backup); +static bool wait_read_only_owners(pgBackup *backup); + static timelineInfo * timelineInfoNew(TimeLineID tli) { @@ -131,29 +136,140 @@ write_backup_status(pgBackup *backup, BackupStatus status, tmp->status = backup->status; tmp->root_dir = pgut_strdup(backup->root_dir); + /* lock backup in exclusive mode */ + if (!lock_backup(tmp, strict, true)) + elog(ERROR, "Cannot lock backup %s directory", base36enc(backup->start_time)); + write_backup(tmp, strict); pgBackupFree(tmp); } /* - * Create exclusive lockfile in the backup's directory. + * Lock backup in either exclusive or non-exclusive (read-only) mode. + * "strict" flag allows to ignore "out of space" errors and should be + * used only by DELETE command to free disk space on filled up + * filesystem. + * + * Only read only tasks (validate, restore) are allowed to take non-exclusive locks. + * Changing backup metadata must be done with exclusive lock. + * + * Only one process can hold exclusive lock at any time. + * Exlusive lock - PID of process, holding the lock - is placed in + * lock file: BACKUP_LOCK_FILE. + * + * Multiple proccess are allowed to take non-exclusive locks simultaneously. + * Non-exclusive locks - PIDs of proccesses, holding the lock - are placed in + * separate lock file: BACKUP_RO_LOCK_FILE. + * When taking RO lock, a brief exclusive lock is taken. + * + * TODO: lock-timeout as parameter + * TODO: we must think about more fine grain unlock mechanism - separate unlock_backup() function. */ bool -lock_backup(pgBackup *backup, bool strict) +lock_backup(pgBackup *backup, bool strict, bool exclusive) { - char lock_file[MAXPGPATH]; - int fd; - char buffer[MAXPGPATH * 2 + 256]; - int ntries; - int len; - int encoded_pid; - pid_t my_pid, - my_p_pid; + int rc; + char lock_file[MAXPGPATH]; + bool enospc_detected = false; - join_path_components(lock_file, backup->root_dir, BACKUP_CATALOG_PID); + join_path_components(lock_file, backup->root_dir, BACKUP_LOCK_FILE); + + rc = lock_backup_exclusive(backup, strict); + + if (rc == 1) + return false; + else if (rc == 2) + { + enospc_detected = true; + if (strict) + return false; + } /* + * We have exclusive lock, now there are following scenarios: + * + * 1. If we are for exlusive lock, then we must open the RO lock file + * and check if any of the processes listed there are still alive. + * If some processes are alive and are not going away in lock_timeout, + * then return false. + * + * 2. If we are here for non-exlusive lock, then write the pid + * into RO lock list and release the exclusive lock. + */ + + if (lock_backup_internal(backup, exclusive)) + { + if (!exclusive) + { + /* release exclusive lock */ + if (fio_unlink(lock_file, FIO_BACKUP_HOST) < 0) + elog(ERROR, "Could not remove old lock file \"%s\": %s", + lock_file, strerror(errno)); + + /* we are done */ + return true; + } + + /* When locking backup in lax exclusive mode, + * we should wait until all RO locks owners are gone. + */ + if (!strict && enospc_detected) + { + /* We are in lax mode and EONSPC was encountered: once again try to grab exclusive lock, + * because there is a chance that lock_backup_read_only may have freed some space on filesystem, + * thanks to unlinking of BACKUP_RO_LOCK_FILE. + * If somebody concurrently acquired exclusive lock first, then we should give up. + */ + if (lock_backup_exclusive(backup, strict) == 1) + return false; + + return true; + } + } + else + return false; + + /* + * Arrange to unlink the lock file(s) at proc_exit. + */ + if (!backup_lock_exit_hook_registered) + { + atexit(unlink_lock_atexit); + backup_lock_exit_hook_registered = true; + } + + /* Use parray so that the lock files are unlinked in a loop */ + if (lock_files == NULL) + lock_files = parray_new(); + parray_append(lock_files, pgut_strdup(lock_file)); + + return true; +} + +/* Lock backup in exclusive mode + * Result codes: + * 0 Success + * 1 Failed to acquire lock in lock_timeout time + * 2 Failed to acquire lock due to ENOSPC + */ +int +lock_backup_exclusive(pgBackup *backup, bool strict) +{ + char lock_file[MAXPGPATH]; + int fd = 0; + char buffer[MAXPGPATH * 2 + 256]; + int ntries = LOCK_TIMEOUT; + int log_freq = ntries / 5; + int len; + int encoded_pid; + pid_t my_p_pid; + + join_path_components(lock_file, backup->root_dir, BACKUP_LOCK_FILE); + + /* + * TODO: is this stuff with ppid below is relevant for us ? + * * If the PID in the lockfile is our own PID or our parent's or * grandparent's PID, then the file must be stale (probably left over from * a previous system boot cycle). We need to check this because of the @@ -171,7 +287,6 @@ lock_backup(pgBackup *backup, bool strict) * would surely never launch a competing postmaster or pg_ctl process * directly. */ - my_pid = getpid(); #ifndef WIN32 my_p_pid = getppid(); #else @@ -188,8 +303,14 @@ lock_backup(pgBackup *backup, bool strict) * (for example, a non-writable $backup_instance_path directory might cause a failure * that won't go away). 100 tries seems like plenty. */ - for (ntries = 0;; ntries++) + do { + FILE *fp_out = NULL; + + if (interrupted) + elog(ERROR, "Interrupted while locking backup %s", + base36enc(backup->start_time)); + /* * Try to create the lock file --- O_EXCL makes this atomic. * @@ -202,8 +323,11 @@ lock_backup(pgBackup *backup, bool strict) /* * Couldn't create the pid file. Probably it already exists. + * If file already exists or we have some permission problem (???), + * then retry; */ - if ((errno != EEXIST && errno != EACCES) || ntries > 100) +// if ((errno != EEXIST && errno != EACCES)) + if (errno != EEXIST) elog(ERROR, "Could not create lock file \"%s\": %s", lock_file, strerror(errno)); @@ -211,28 +335,38 @@ lock_backup(pgBackup *backup, bool strict) * Read the file to get the old owner's PID. Note race condition * here: file might have been deleted since we tried to create it. */ - fd = fio_open(lock_file, O_RDONLY, FIO_BACKUP_HOST); - if (fd < 0) + + fp_out = fopen(lock_file, "r"); + if (fp_out == NULL) { if (errno == ENOENT) - continue; /* race condition; try again */ - elog(ERROR, "Could not open lock file \"%s\": %s", - lock_file, strerror(errno)); + continue; /* race condition; try again */ + elog(ERROR, "Cannot open lock file \"%s\": %s", lock_file, strerror(errno)); } - if ((len = fio_read(fd, buffer, sizeof(buffer) - 1)) < 0) - elog(ERROR, "Could not read lock file \"%s\": %s", - lock_file, strerror(errno)); - fio_close(fd); + len = fread(buffer, 1, sizeof(buffer) - 1, fp_out); + if (ferror(fp_out)) + elog(ERROR, "Cannot read from lock file: \"%s\"", lock_file); + fclose(fp_out); + + /* + * It should be possible only as a result of system crash, + * so its hypothetical owner should be dead by now + */ if (len == 0) - elog(ERROR, "Lock file \"%s\" is empty", lock_file); + { + elog(WARNING, "Lock file \"%s\" is empty", lock_file); + goto grab_lock; + } - buffer[len] = '\0'; encoded_pid = atoi(buffer); if (encoded_pid <= 0) - elog(ERROR, "Bogus data in lock file \"%s\": \"%s\"", + { + elog(WARNING, "Bogus data in lock file \"%s\": \"%s\"", lock_file, buffer); + goto grab_lock; + } /* * Check to see if the other process still exists @@ -247,9 +381,19 @@ lock_backup(pgBackup *backup, bool strict) { if (kill(encoded_pid, 0) == 0) { - elog(WARNING, "Process %d is using backup %s and still is running", - encoded_pid, base36enc(backup->start_time)); - return false; + /* complain every fifth interval */ + if ((ntries % log_freq) == 0) + { + elog(WARNING, "Process %d is using backup %s, and is still running", + encoded_pid, base36enc(backup->start_time)); + + elog(WARNING, "Waiting %u seconds on lock for backup %s", ntries, base36enc(backup->start_time)); + } + + sleep(1); + + /* try again */ + continue; } else { @@ -262,15 +406,25 @@ lock_backup(pgBackup *backup, bool strict) } } +grab_lock: /* * Looks like nobody's home. Unlink the file and try again to create * it. Need a loop because of possible race condition against other * would-be creators. */ if (fio_unlink(lock_file, FIO_BACKUP_HOST) < 0) + { + if (errno == ENOENT) + continue; /* race condition, again */ elog(ERROR, "Could not remove old lock file \"%s\": %s", lock_file, strerror(errno)); - } + } + + } while (ntries--); + + /* Failed to acquire exclusive lock in time */ + if (fd <= 0) + return 1; /* * Successfully created the file, now fill it. @@ -284,52 +438,215 @@ lock_backup(pgBackup *backup, bool strict) fio_close(fd); fio_unlink(lock_file, FIO_BACKUP_HOST); - /* if write didn't set errno, assume problem is no disk space */ - errno = save_errno ? save_errno : ENOSPC; /* In lax mode if we failed to grab lock because of 'out of space error', * then treat backup as locked. * Only delete command should be run in lax mode. */ - if (!strict && errno == ENOSPC) - return true; - - elog(ERROR, "Could not write lock file \"%s\": %s", - lock_file, strerror(errno)); + if (!strict && save_errno == ENOSPC) + return 2; + else + elog(ERROR, "Could not write lock file \"%s\": %s", + lock_file, strerror(save_errno)); } + if (fio_flush(fd) != 0) { - int save_errno = errno; + int save_errno = errno; fio_close(fd); fio_unlink(lock_file, FIO_BACKUP_HOST); - errno = save_errno; - elog(ERROR, "Could not write lock file \"%s\": %s", - lock_file, strerror(errno)); + + /* In lax mode if we failed to grab lock because of 'out of space error', + * then treat backup as locked. + * Only delete command should be run in lax mode. + */ + if (!strict && save_errno == ENOSPC) + return 2; + else + elog(ERROR, "Could not flush lock file \"%s\": %s", + lock_file, strerror(save_errno)); } + if (fio_close(fd) != 0) { int save_errno = errno; fio_unlink(lock_file, FIO_BACKUP_HOST); - errno = save_errno; - elog(ERROR, "Could not write lock file \"%s\": %s", - lock_file, strerror(errno)); + + if (!strict && errno == ENOSPC) + return 2; + else + elog(ERROR, "Could not close lock file \"%s\": %s", + lock_file, strerror(save_errno)); } - /* - * Arrange to unlink the lock file(s) at proc_exit. - */ - if (!exit_hook_registered) + return 0; +} + +/* Wait until all read-only lock owners are gone */ +bool +wait_read_only_owners(pgBackup *backup) +{ + FILE *fp = NULL; + char buffer[256]; + pid_t encoded_pid; + int ntries = LOCK_TIMEOUT; + int log_freq = ntries / 5; + char lock_file[MAXPGPATH]; + + join_path_components(lock_file, backup->root_dir, BACKUP_RO_LOCK_FILE); + + fp = fopen(lock_file, "r"); + if (fp == NULL && errno != ENOENT) + elog(ERROR, "Cannot open lock file \"%s\": %s", lock_file, strerror(errno)); + + /* iterate over pids in lock file */ + while (fp && fgets(buffer, sizeof(buffer), fp)) + { + encoded_pid = atoi(buffer); + if (encoded_pid <= 0) + { + elog(WARNING, "Bogus data in lock file \"%s\": \"%s\"", lock_file, buffer); + continue; + } + + /* wait until RO lock owners go away */ + do + { + if (interrupted) + elog(ERROR, "Interrupted while locking backup %s", + base36enc(backup->start_time)); + + if (encoded_pid != my_pid) + { + if (kill(encoded_pid, 0) == 0) + { + if ((ntries % log_freq) == 0) + { + elog(WARNING, "Process %d is using backup %s in read only mode, and is still running", + encoded_pid, base36enc(backup->start_time)); + + elog(WARNING, "Waiting %u seconds on lock for backup %s", ntries, + base36enc(backup->start_time)); + } + + sleep(1); + + /* try again */ + continue; + } + else if (errno != ESRCH) + elog(ERROR, "Failed to send signal 0 to a process %d: %s", + encoded_pid, strerror(errno)); + } + + /* locker is dead */ + break; + + } while (ntries--); + + if (ntries <= 0) + { + elog(WARNING, "Cannot to lock backup %s in exclusive mode, because process %u owns read-only lock", + base36enc(backup->start_time), encoded_pid); + return false; + } + } + + if (fp && ferror(fp)) + elog(ERROR, "Cannot read from lock file: \"%s\"", lock_file); + + if (fp) + fclose(fp); + + /* unlink RO lock list */ + fio_unlink(lock_file, FIO_BACKUP_HOST); + return true; +} + +bool +lock_backup_internal(pgBackup *backup, bool exclusive) +{ + if (exclusive) + return wait_read_only_owners(backup); + else + return lock_backup_read_only(backup); +} + +bool +lock_backup_read_only(pgBackup *backup) +{ + FILE *fp_in = NULL; + FILE *fp_out = NULL; + char buf_in[256]; + pid_t encoded_pid; + char lock_file[MAXPGPATH]; + + char buffer[8192]; /*TODO: should be enough, but maybe malloc+realloc is better ? */ + char lock_file_tmp[MAXPGPATH]; + int buffer_len = 0; + + join_path_components(lock_file, backup->root_dir, BACKUP_RO_LOCK_FILE); + snprintf(lock_file_tmp, MAXPGPATH, "%s%s", lock_file, "tmp"); + + /* open already existing lock files */ + fp_in = fopen(lock_file, "r"); + if (fp_in == NULL && errno != ENOENT) + elog(ERROR, "Cannot open lock file \"%s\": %s", lock_file, strerror(errno)); + + /* read PIDs of owners */ + while (fp_in && fgets(buf_in, sizeof(buf_in), fp_in)) { - atexit(unlink_lock_atexit); - exit_hook_registered = true; + encoded_pid = atoi(buf_in); + if (encoded_pid <= 0) + { + elog(WARNING, "Bogus data in lock file \"%s\": \"%s\"", lock_file, buf_in); + continue; + } + + if (encoded_pid != my_pid) + { + if (kill(encoded_pid, 0) == 0) + { + /* + * Somebody is still using this backup in RO mode, + * copy this pid into a new file. + */ + buffer_len += snprintf(buffer+buffer_len, 4096, "%u\n", encoded_pid); + } + else if (errno != ESRCH) + elog(ERROR, "Failed to send signal 0 to a process %d: %s", + encoded_pid, strerror(errno)); + } + } + + if (fp_in) + { + if (ferror(fp_in)) + elog(ERROR, "Cannot read from lock file: \"%s\"", lock_file); + fclose(fp_in); } - /* Use parray so that the lock files are unlinked in a loop */ - if (lock_files == NULL) - lock_files = parray_new(); - parray_append(lock_files, pgut_strdup(lock_file)); + fp_out = fopen(lock_file_tmp, "w"); + if (fp_out == NULL) + elog(ERROR, "Cannot open temp lock file \"%s\": %s", lock_file_tmp, strerror(errno)); + + /* add my own pid */ + buffer_len += snprintf(buffer+buffer_len, sizeof(buffer), "%u\n", my_pid); + + /* write out the collected PIDs to temp lock file */ + fwrite(buffer, 1, buffer_len, fp_out); + + if (ferror(fp_out)) + elog(ERROR, "Cannot write to lock file: \"%s\"", lock_file_tmp); + + if (fclose(fp_out) != 0) + elog(ERROR, "Cannot close temp lock file \"%s\": %s", lock_file_tmp, strerror(errno)); + + if (rename(lock_file_tmp, lock_file) < 0) + elog(ERROR, "Cannot rename file \"%s\" to \"%s\": %s", + lock_file_tmp, lock_file, strerror(errno)); return true; } @@ -485,7 +802,7 @@ catalog_get_backup_list(const char *instance_name, time_t requested_backup_id) } else if (strcmp(base36enc(backup->start_time), data_ent->d_name) != 0) { - elog(VERBOSE, "backup ID in control file \"%s\" doesn't match name of the backup folder \"%s\"", + elog(WARNING, "backup ID in control file \"%s\" doesn't match name of the backup folder \"%s\"", base36enc(backup->start_time), backup_conf_path); } @@ -583,7 +900,7 @@ get_backup_filelist(pgBackup *backup, bool strict) * Lock list of backups. Function goes in backward direction. */ void -catalog_lock_backup_list(parray *backup_list, int from_idx, int to_idx, bool strict) +catalog_lock_backup_list(parray *backup_list, int from_idx, int to_idx, bool strict, bool exclusive) { int start_idx, end_idx; @@ -598,7 +915,7 @@ catalog_lock_backup_list(parray *backup_list, int from_idx, int to_idx, bool str for (i = start_idx; i >= end_idx; i--) { pgBackup *backup = (pgBackup *) parray_get(backup_list, i); - if (!lock_backup(backup, strict)) + if (!lock_backup(backup, strict, exclusive)) elog(ERROR, "Cannot lock backup %s directory", base36enc(backup->start_time)); } @@ -1648,7 +1965,7 @@ pin_backup(pgBackup *target_backup, pgSetBackupParams *set_backup_params) { char expire_timestamp[100]; - time2iso(expire_timestamp, lengthof(expire_timestamp), target_backup->expire_time); + time2iso(expire_timestamp, lengthof(expire_timestamp), target_backup->expire_time, false); elog(INFO, "Backup %s is pinned until '%s'", base36enc(target_backup->start_time), expire_timestamp); } @@ -1699,7 +2016,7 @@ add_note(pgBackup *target_backup, char *note) * Write information about backup.in to stream "out". */ void -pgBackupWriteControl(FILE *out, pgBackup *backup) +pgBackupWriteControl(FILE *out, pgBackup *backup, bool utc) { char timestamp[100]; @@ -1731,27 +2048,27 @@ pgBackupWriteControl(FILE *out, pgBackup *backup) (uint32) (backup->stop_lsn >> 32), (uint32) backup->stop_lsn); - time2iso(timestamp, lengthof(timestamp), backup->start_time); + time2iso(timestamp, lengthof(timestamp), backup->start_time, utc); fio_fprintf(out, "start-time = '%s'\n", timestamp); if (backup->merge_time > 0) { - time2iso(timestamp, lengthof(timestamp), backup->merge_time); + time2iso(timestamp, lengthof(timestamp), backup->merge_time, utc); fio_fprintf(out, "merge-time = '%s'\n", timestamp); } if (backup->end_time > 0) { - time2iso(timestamp, lengthof(timestamp), backup->end_time); + time2iso(timestamp, lengthof(timestamp), backup->end_time, utc); fio_fprintf(out, "end-time = '%s'\n", timestamp); } fio_fprintf(out, "recovery-xid = " XID_FMT "\n", backup->recovery_xid); if (backup->recovery_time > 0) { - time2iso(timestamp, lengthof(timestamp), backup->recovery_time); + time2iso(timestamp, lengthof(timestamp), backup->recovery_time, utc); fio_fprintf(out, "recovery-time = '%s'\n", timestamp); } if (backup->expire_time > 0) { - time2iso(timestamp, lengthof(timestamp), backup->expire_time); + time2iso(timestamp, lengthof(timestamp), backup->expire_time, utc); fio_fprintf(out, "expire-time = '%s'\n", timestamp); } @@ -1798,7 +2115,9 @@ pgBackupWriteControl(FILE *out, pgBackup *backup) /* * Save the backup content into BACKUP_CONTROL_FILE. - * TODO: honor the strict flag + * Flag strict allows to ignore "out of space" error + * when attempting to lock backup. Only delete is allowed + * to use this functionality. */ void write_backup(pgBackup *backup, bool strict) @@ -1806,7 +2125,7 @@ write_backup(pgBackup *backup, bool strict) FILE *fp = NULL; char path[MAXPGPATH]; char path_temp[MAXPGPATH]; - char buf[4096]; + char buf[8192]; join_path_components(path, backup->root_dir, BACKUP_CONTROL_FILE); snprintf(path_temp, sizeof(path_temp), "%s.tmp", path); @@ -1822,20 +2141,36 @@ write_backup(pgBackup *backup, bool strict) setvbuf(fp, buf, _IOFBF, sizeof(buf)); - pgBackupWriteControl(fp, backup); + pgBackupWriteControl(fp, backup, true); + /* Ignore 'out of space' error in lax mode */ if (fflush(fp) != 0) - elog(ERROR, "Cannot flush control file \"%s\": %s", - path_temp, strerror(errno)); + { + int elevel = ERROR; + int save_errno = errno; - if (fsync(fileno(fp)) < 0) - elog(ERROR, "Cannot sync control file \"%s\": %s", - path_temp, strerror(errno)); + if (!strict && (errno == ENOSPC)) + elevel = WARNING; + + elog(elevel, "Cannot flush control file \"%s\": %s", + path_temp, strerror(save_errno)); + + if (!strict && (save_errno == ENOSPC)) + { + fclose(fp); + fio_unlink(path_temp, FIO_BACKUP_HOST); + return; + } + } if (fclose(fp) != 0) elog(ERROR, "Cannot close control file \"%s\": %s", path_temp, strerror(errno)); + if (fio_sync(path_temp, FIO_BACKUP_HOST) < 0) + elog(ERROR, "Cannot sync control file \"%s\": %s", + path_temp, strerror(errno)); + if (rename(path_temp, path) < 0) elog(ERROR, "Cannot rename file \"%s\" to \"%s\": %s", path_temp, path, strerror(errno)); diff --git a/src/delete.c b/src/delete.c index 798a1653..e06d6808 100644 --- a/src/delete.c +++ b/src/delete.c @@ -89,7 +89,7 @@ do_delete(time_t backup_id) if (!dry_run) { /* Lock marked for delete backups */ - catalog_lock_backup_list(delete_list, parray_num(delete_list) - 1, 0, false); + catalog_lock_backup_list(delete_list, parray_num(delete_list) - 1, 0, false, true); /* Delete backups from the end of list */ for (i = (int) parray_num(delete_list) - 1; i >= 0; i--) @@ -316,7 +316,7 @@ do_retention_internal(parray *backup_list, parray *to_keep_list, parray *to_purg (backup->expire_time > current_time)) { char expire_timestamp[100]; - time2iso(expire_timestamp, lengthof(expire_timestamp), backup->expire_time); + time2iso(expire_timestamp, lengthof(expire_timestamp), backup->expire_time, false); elog(LOG, "Backup %s is pinned until '%s', retain", base36enc(backup->start_time), expire_timestamp); @@ -513,7 +513,7 @@ do_retention_merge(parray *backup_list, parray *to_keep_list, parray *to_purge_l parray_rm(to_purge_list, full_backup, pgBackupCompareId); /* Lock merge chain */ - catalog_lock_backup_list(merge_list, parray_num(merge_list) - 1, 0, true); + catalog_lock_backup_list(merge_list, parray_num(merge_list) - 1, 0, true, true); /* Consider this extreme case */ // PAGEa1 PAGEb1 both valid @@ -630,7 +630,7 @@ do_retention_purge(parray *to_keep_list, parray *to_purge_list) continue; /* Actual purge */ - if (!lock_backup(delete_backup, false)) + if (!lock_backup(delete_backup, false, true)) { /* If the backup still is used, do not interrupt and go to the next */ elog(WARNING, "Cannot lock backup %s directory, skip purging", @@ -740,7 +740,7 @@ delete_backup_files(pgBackup *backup) return; } - time2iso(timestamp, lengthof(timestamp), backup->recovery_time); + time2iso(timestamp, lengthof(timestamp), backup->recovery_time, false); elog(INFO, "Delete: %s %s", base36enc(backup->start_time), timestamp); @@ -975,7 +975,7 @@ do_delete_instance(void) /* Delete all backups. */ backup_list = catalog_get_backup_list(instance_name, INVALID_BACKUP_ID); - catalog_lock_backup_list(backup_list, 0, parray_num(backup_list) - 1, true); + catalog_lock_backup_list(backup_list, 0, parray_num(backup_list) - 1, true, true); for (i = 0; i < parray_num(backup_list); i++) { @@ -1081,7 +1081,7 @@ do_delete_status(InstanceConfig *instance_config, const char *status) if (backup->stream) size_to_delete += backup->wal_bytes; - if (!dry_run && lock_backup(backup, false)) + if (!dry_run && lock_backup(backup, false, true)) delete_backup_files(backup); n_deleted++; diff --git a/src/merge.c b/src/merge.c index a453a073..05220a5d 100644 --- a/src/merge.c +++ b/src/merge.c @@ -58,6 +58,8 @@ merge_non_data_file(parray *parent_chain, pgBackup *full_backup, pgFile *tmp_file, const char *full_database_dir, const char *full_external_prefix); +static bool is_forward_compatible(parray *parent_chain); + /* * Implementation of MERGE command. * @@ -400,7 +402,7 @@ do_merge(time_t backup_id) parray_append(merge_list, full_backup); /* Lock merge chain */ - catalog_lock_backup_list(merge_list, parray_num(merge_list) - 1, 0, true); + catalog_lock_backup_list(merge_list, parray_num(merge_list) - 1, 0, true, true); /* do actual merge */ merge_chain(merge_list, full_backup, dest_backup); @@ -532,19 +534,7 @@ merge_chain(parray *parent_chain, pgBackup *full_backup, pgBackup *dest_backup) * If current program version differs from destination backup version, * then in-place merge is not possible. */ - if ((parse_program_version(full_backup->program_version) == - parse_program_version(dest_backup->program_version)) && - (parse_program_version(dest_backup->program_version) == - parse_program_version(PROGRAM_VERSION))) - program_version_match = true; - else - elog(WARNING, "In-place merge is disabled because of program " - "versions mismatch. Full backup version: %s, " - "destination backup version: %s, " - "current program version: %s", - full_backup->program_version, - dest_backup->program_version, - PROGRAM_VERSION); + program_version_match = is_forward_compatible(parent_chain); /* Forbid merge retry for failed merges between 2.4.0 and any * older version. Several format changes makes it impossible @@ -1398,3 +1388,58 @@ merge_non_data_file(parray *parent_chain, pgBackup *full_backup, to_fullpath_tmp, to_fullpath, strerror(errno)); } + +/* + * If file format in incremental chain is compatible + * with current storage format. + * If not, then in-place merge is not possible. + * + * Consider the following examples: + * STORAGE_FORMAT_VERSION = 2.4.4 + * 2.3.3 \ + * 2.3.4 \ disable in-place merge, because + * 2.4.1 / current STORAGE_FORMAT_VERSION > 2.3.3 + * 2.4.3 / + * + * 2.4.4 \ enable in_place merge, because + * 2.4.5 / current STORAGE_FORMAT_VERSION == 2.4.4 + * + * 2.4.5 \ enable in_place merge, because + * 2.4.6 / current STORAGE_FORMAT_VERSION < 2.4.5 + * + */ +bool +is_forward_compatible(parray *parent_chain) +{ + int i; + pgBackup *oldest_ver_backup = NULL; + uint32 oldest_ver_in_chain = parse_program_version(PROGRAM_VERSION); + + for (i = 0; i < parray_num(parent_chain); i++) + { + pgBackup *backup = (pgBackup *) parray_get(parent_chain, i); + uint32 current_version = parse_program_version(backup->program_version); + + if (!oldest_ver_backup) + oldest_ver_backup = backup; + + if (current_version < oldest_ver_in_chain) + { + oldest_ver_in_chain = current_version; + oldest_ver_backup = backup; + } + } + + if (oldest_ver_in_chain < parse_program_version(STORAGE_FORMAT_VERSION)) + { + elog(WARNING, "In-place merge is disabled because of storage format incompatibility. " + "Backup %s storage format version: %s, " + "current storage format version: %s", + base36enc(oldest_ver_backup->start_time), + oldest_ver_backup->program_version, + STORAGE_FORMAT_VERSION); + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/parsexlog.c b/src/parsexlog.c index 53597523..41a410d3 100644 --- a/src/parsexlog.c +++ b/src/parsexlog.c @@ -480,7 +480,7 @@ validate_wal(pgBackup *backup, const char *archivedir, last_rec.rec_xid = backup->recovery_xid; last_rec.rec_lsn = backup->stop_lsn; - time2iso(last_timestamp, lengthof(last_timestamp), backup->recovery_time); + time2iso(last_timestamp, lengthof(last_timestamp), backup->recovery_time, false); if ((TransactionIdIsValid(target_xid) && target_xid == last_rec.rec_xid) || (target_time != 0 && backup->recovery_time >= target_time) @@ -493,7 +493,7 @@ validate_wal(pgBackup *backup, const char *archivedir, InvalidXLogRecPtr, true, validateXLogRecord, &last_rec, true); if (last_rec.rec_time > 0) time2iso(last_timestamp, lengthof(last_timestamp), - timestamptz_to_time_t(last_rec.rec_time)); + timestamptz_to_time_t(last_rec.rec_time), false); /* There are all needed WAL records */ if (all_wal) @@ -508,7 +508,7 @@ validate_wal(pgBackup *backup, const char *archivedir, (uint32) (last_rec.rec_lsn >> 32), (uint32) last_rec.rec_lsn); if (target_time > 0) - time2iso(target_timestamp, lengthof(target_timestamp), target_time); + time2iso(target_timestamp, lengthof(target_timestamp), target_time, false); if (TransactionIdIsValid(target_xid) && target_time != 0) elog(ERROR, "Not enough WAL records to time %s and xid " XID_FMT, target_timestamp, target_xid); @@ -846,7 +846,14 @@ get_prior_record_lsn(const char *archivedir, XLogRecPtr start_lsn, */ GetXLogSegNo(start_lsn, start_segno, wal_seg_size); if (start_segno == segno) + { startpoint = start_lsn; +#if PG_VERSION_NUM >= 130000 + if (XLogRecPtrIsInvalid(startpoint)) + startpoint = SizeOfXLogShortPHD; + XLogBeginRead(xlogreader, startpoint); +#endif + } else { XLogRecPtr found; diff --git a/src/pg_probackup.c b/src/pg_probackup.c index bbc2a5bf..6fdf8baf 100644 --- a/src/pg_probackup.c +++ b/src/pg_probackup.c @@ -101,7 +101,7 @@ static pgRecoveryTarget *recovery_target_options = NULL; static pgRestoreParams *restore_params = NULL; time_t current_time = 0; -bool restore_as_replica = false; +static bool restore_as_replica = false; bool no_validate = false; IncrRestoreMode incremental_mode = INCR_NONE; @@ -714,15 +714,14 @@ main(int argc, char *argv[]) if (force) no_validate = true; - if (replication_slot != NULL) - restore_as_replica = true; - /* keep all params in one structure */ restore_params = pgut_new(pgRestoreParams); restore_params->is_restore = (backup_subcmd == RESTORE_CMD); restore_params->force = force; restore_params->no_validate = no_validate; restore_params->restore_as_replica = restore_as_replica; + restore_params->recovery_settings_mode = DEFAULT; + restore_params->primary_slot_name = replication_slot; restore_params->skip_block_validation = skip_block_validation; restore_params->skip_external_dirs = skip_external_dirs; diff --git a/src/pg_probackup.h b/src/pg_probackup.h index 67fcb54c..c53d31e9 100644 --- a/src/pg_probackup.h +++ b/src/pg_probackup.h @@ -66,7 +66,8 @@ extern const char *PROGRAM_EMAIL; #define PG_GLOBAL_DIR "global" #define BACKUP_CONTROL_FILE "backup.control" #define BACKUP_CATALOG_CONF_FILE "pg_probackup.conf" -#define BACKUP_CATALOG_PID "backup.pid" +#define BACKUP_LOCK_FILE "backup.pid" +#define BACKUP_RO_LOCK_FILE "backup_ro.pid" #define DATABASE_FILE_LIST "backup_content.control" #define PG_BACKUP_LABEL_FILE "backup_label" #define PG_TABLESPACE_MAP_FILE "tablespace_map" @@ -78,6 +79,7 @@ extern const char *PROGRAM_EMAIL; /* Timeout defaults */ #define ARCHIVE_TIMEOUT_DEFAULT 300 #define REPLICA_TIMEOUT_DEFAULT 300 +#define LOCK_TIMEOUT 30 /* Directory/File permission */ #define DIR_PERMISSION (0700) @@ -160,6 +162,16 @@ typedef enum PartialRestoreType EXCLUDE, } PartialRestoreType; +typedef enum RecoverySettingsMode +{ + DEFAULT, /* not set */ + DONTWRITE, /* explicitly forbid to update recovery settings */ + //TODO Should we always clean/preserve old recovery settings, + // or make it configurable? + PITR_REQUESTED, /* can be set based on other parameters + * if not explicitly forbidden */ +} RecoverySettingsMode; + typedef enum CompressAlg { NOT_DEFINED_COMPRESS = 0, @@ -295,6 +307,8 @@ typedef enum ShowFormat #define PROGRAM_VERSION "2.5.0" #define AGENT_PROTOCOL_VERSION 20500 +/* update only when changing storage format */ +#define STORAGE_FORMAT_VERSION "2.5.0" typedef struct ConnectionOptions { @@ -408,10 +422,9 @@ struct pgBackup TimeLineID tli; /* timeline of start and stop backup lsns */ XLogRecPtr start_lsn; /* backup's starting transaction log location */ XLogRecPtr stop_lsn; /* backup's finishing transaction log location */ - time_t start_time; /* since this moment backup has status - * BACKUP_STATUS_RUNNING */ - time_t merge_dest_backup; /* start_time of incremental backup, - * this backup is merging with. + time_t start_time; /* UTC time of backup creation */ + time_t merge_dest_backup; /* start_time of incremental backup with + * which this backup is merging with. * Only available for FULL backups * with MERGING or MERGED statuses */ time_t merge_time; /* the moment when merge was started or 0 */ @@ -505,6 +518,8 @@ typedef struct pgRestoreParams bool is_restore; bool no_validate; bool restore_as_replica; + //TODO maybe somehow add restore_as_replica as one of RecoverySettingsModes + RecoverySettingsMode recovery_settings_mode; bool skip_external_dirs; bool skip_block_validation; //Start using it const char *restore_command; @@ -665,6 +680,9 @@ typedef struct BackupPageHeader2 strcmp((fname) + XLOG_FNAME_LEN, ".gz") == 0) #if PG_VERSION_NUM >= 110000 + +#define WalSegmentOffset(xlogptr, wal_segsz_bytes) \ + XLogSegmentOffset(xlogptr, wal_segsz_bytes) #define GetXLogSegNo(xlrp, logSegNo, wal_segsz_bytes) \ XLByteToSeg(xlrp, logSegNo, wal_segsz_bytes) #define GetXLogRecPtr(segno, offset, wal_segsz_bytes, dest) \ @@ -684,6 +702,8 @@ typedef struct BackupPageHeader2 #define GetXLogFromFileName(fname, tli, logSegNo, wal_segsz_bytes) \ XLogFromFileName(fname, tli, logSegNo, wal_segsz_bytes) #else +#define WalSegmentOffset(xlogptr, wal_segsz_bytes) \ + ((xlogptr) & ((XLogSegSize) - 1)) #define GetXLogSegNo(xlrp, logSegNo, wal_segsz_bytes) \ XLByteToSeg(xlrp, logSegNo) #define GetXLogRecPtr(segno, offset, wal_segsz_bytes, dest) \ @@ -881,7 +901,7 @@ extern void write_backup(pgBackup *backup, bool strict); extern void write_backup_status(pgBackup *backup, BackupStatus status, const char *instance_name, bool strict); extern void write_backup_data_bytes(pgBackup *backup); -extern bool lock_backup(pgBackup *backup, bool strict); +extern bool lock_backup(pgBackup *backup, bool strict, bool exclusive); extern const char *pgBackupGetBackupMode(pgBackup *backup, bool show_color); extern void pgBackupGetBackupModeColor(pgBackup *backup, char *mode); @@ -889,7 +909,7 @@ extern void pgBackupGetBackupModeColor(pgBackup *backup, char *mode); extern parray *catalog_get_instance_list(void); extern parray *catalog_get_backup_list(const char *instance_name, time_t requested_backup_id); extern void catalog_lock_backup_list(parray *backup_list, int from_idx, - int to_idx, bool strict); + int to_idx, bool strict, bool exclusive); extern pgBackup *catalog_get_last_data_backup(parray *backup_list, TimeLineID tli, time_t current_start_time); @@ -903,7 +923,7 @@ extern void do_set_backup(const char *instance_name, time_t backup_id, extern void pin_backup(pgBackup *target_backup, pgSetBackupParams *set_backup_params); extern void add_note(pgBackup *target_backup, char *note); -extern void pgBackupWriteControl(FILE *out, pgBackup *backup); +extern void pgBackupWriteControl(FILE *out, pgBackup *backup, bool utc); extern void write_backup_filelist(pgBackup *backup, parray *files, const char *root, parray *external_list, bool sync); @@ -993,8 +1013,8 @@ extern void fio_pgFileDelete(pgFile *file, const char *full_path); extern void pgFileFree(void *file); -extern pg_crc32 pgFileGetCRC(const char *file_path, bool missing_ok, bool use_crc32c); -extern pg_crc32 pgFileGetCRCgz(const char *file_path, bool missing_ok, bool use_crc32c); +extern pg_crc32 pgFileGetCRC(const char *file_path, bool use_crc32c, bool missing_ok); +extern pg_crc32 pgFileGetCRCgz(const char *file_path, bool use_crc32c, bool missing_ok); extern int pgFileMapComparePath(const void *f1, const void *f2); extern int pgFileCompareName(const void *f1, const void *f2); @@ -1092,7 +1112,7 @@ extern void set_min_recovery_point(pgFile *file, const char *backup_path, extern void copy_pgcontrol_file(const char *from_fullpath, fio_location from_location, const char *to_fullpath, fio_location to_location, pgFile *file); -extern void time2iso(char *buf, size_t len, time_t time); +extern void time2iso(char *buf, size_t len, time_t time, bool utc); extern const char *status2str(BackupStatus status); const char *status2str_color(BackupStatus status); extern BackupStatus str2status(const char *status); @@ -1157,6 +1177,9 @@ extern void fio_list_dir(parray *files, const char *root, bool exclude, bool fol extern bool pgut_rmtree(const char *path, bool rmtopdir, bool strict); +extern void pgut_setenv(const char *key, const char *val); +extern void pgut_unsetenv(const char *key); + extern PageState *fio_get_checksum_map(const char *fullpath, uint32 checksum_version, int n_blocks, XLogRecPtr dest_stop_lsn, BlockNumber segmentno, fio_location location); diff --git a/src/restore.c b/src/restore.c index 2ade54fa..bf278f8b 100644 --- a/src/restore.c +++ b/src/restore.c @@ -39,13 +39,29 @@ typedef struct int ret; } restore_files_arg; + +static void +print_recovery_settings(FILE *fp, pgBackup *backup, + pgRestoreParams *params, pgRecoveryTarget *rt); +static void +print_standby_settings_common(FILE *fp, pgBackup *backup, pgRestoreParams *params); + +#if PG_VERSION_NUM >= 120000 +static void +update_recovery_options(pgBackup *backup, + pgRestoreParams *params, pgRecoveryTarget *rt); +#else +static void +update_recovery_options_before_v12(pgBackup *backup, + pgRestoreParams *params, pgRecoveryTarget *rt); +#endif + static void create_recovery_conf(time_t backup_id, pgRecoveryTarget *rt, pgBackup *backup, pgRestoreParams *params); 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, parray *dbOid_exclude_list, pgRestoreParams *params, @@ -496,18 +512,11 @@ do_restore_or_validate(time_t target_backup_id, pgRecoveryTarget *rt, { tmp_backup = (pgBackup *) parray_get(parent_chain, i); - /* Do not interrupt, validate the next backup */ - if (!lock_backup(tmp_backup, true)) + /* lock every backup in chain in read-only mode */ + if (!lock_backup(tmp_backup, true, false)) { - 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; - } + elog(ERROR, "Cannot lock backup %s directory", + base36enc(tmp_backup->start_time)); } /* validate datafiles only */ @@ -604,6 +613,7 @@ do_restore_or_validate(time_t target_backup_id, pgRecoveryTarget *rt, restore_chain(dest_backup, parent_chain, dbOid_exclude_list, params, instance_config.pgdata, no_sync); + //TODO rename and update comment /* Create recovery.conf with given recovery target parameters */ create_recovery_conf(target_backup_id, rt, dest_backup, params); } @@ -650,7 +660,7 @@ restore_chain(pgBackup *dest_backup, parray *parent_chain, time_t start_time, end_time; /* Preparations for actual restoring */ - time2iso(timestamp, lengthof(timestamp), dest_backup->start_time); + time2iso(timestamp, lengthof(timestamp), dest_backup->start_time, false); elog(INFO, "Restoring the database from backup at %s", timestamp); dest_files = get_backup_filelist(dest_backup, true); @@ -660,7 +670,7 @@ restore_chain(pgBackup *dest_backup, parray *parent_chain, { pgBackup *backup = (pgBackup *) parray_get(parent_chain, i); - if (!lock_backup(backup, true)) + if (!lock_backup(backup, true, false)) elog(ERROR, "Cannot lock backup %s", base36enc(backup->start_time)); if (backup->status != BACKUP_STATUS_OK && @@ -1202,7 +1212,7 @@ done: } /* - * Create recovery.conf (probackup_recovery.conf in case of PG12) + * Create recovery.conf (postgresql.auto.conf in case of PG12) * with given recovery target parameters */ static void @@ -1211,13 +1221,9 @@ create_recovery_conf(time_t backup_id, pgBackup *backup, pgRestoreParams *params) { - char path[MAXPGPATH]; - FILE *fp; - bool pitr_requested; bool target_latest; bool target_immediate; bool restore_command_provided = false; - char restore_command_guc[16384]; if (instance_config.restore_command && (pg_strcasecmp(instance_config.restore_command, "none") != 0)) @@ -1249,36 +1255,142 @@ create_recovery_conf(time_t backup_id, * 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 || + if (!backup->stream || rt->time_string || rt->xid_string || rt->lsn_string || rt->target_name || - target_immediate || target_latest || restore_command_provided; + target_immediate || target_latest || restore_command_provided) + params->recovery_settings_mode = PITR_REQUESTED; - /* No need to generate recovery.conf at all. */ - if (!(pitr_requested || params->restore_as_replica)) + elog(LOG, "----------------------------------------"); + +#if PG_VERSION_NUM >= 120000 + update_recovery_options(backup, params, rt); +#else + update_recovery_options_before_v12(backup, params, rt); +#endif +} + + +/* TODO get rid of using global variables: instance_config, backup_path, instance_name */ +static void +print_recovery_settings(FILE *fp, pgBackup *backup, + pgRestoreParams *params, pgRecoveryTarget *rt) +{ + char restore_command_guc[16384]; + fio_fprintf(fp, "## recovery settings\n"); + + /* If restore_command is provided, use it. Otherwise construct it from scratch. */ + if (instance_config.restore_command && + (pg_strcasecmp(instance_config.restore_command, "none") != 0)) + sprintf(restore_command_guc, "%s", instance_config.restore_command); + else + { + /* default cmdline, ok for local restore */ + sprintf(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); + + /* append --remote-* parameters provided via --archive-* settings */ + if (instance_config.archive.host) + { + strcat(restore_command_guc, " --remote-host="); + strcat(restore_command_guc, instance_config.archive.host); + } + + if (instance_config.archive.port) + { + strcat(restore_command_guc, " --remote-port="); + strcat(restore_command_guc, instance_config.archive.port); + } + + if (instance_config.archive.user) + { + strcat(restore_command_guc, " --remote-user="); + strcat(restore_command_guc, instance_config.archive.user); + } + } + + /* + * 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 && (strcmp(rt->target_stop, "immediate") == 0)) + 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"); + + if (rt->target_tli) + fio_fprintf(fp, "recovery_target_timeline = '%u'\n", rt->target_tli); + else + { +#if PG_VERSION_NUM >= 120000 + + /* + * 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. + */ + fio_fprintf(fp, "recovery_target_timeline = 'current'\n"); +#endif + } + + if (rt->target_action) + fio_fprintf(fp, "recovery_target_action = '%s'\n", rt->target_action); + else + /* default recovery_target_action is 'pause' */ + fio_fprintf(fp, "recovery_target_action = '%s'\n", "pause"); + + elog(LOG, "Setting restore_command to '%s'", restore_command_guc); + fio_fprintf(fp, "restore_command = '%s'\n", restore_command_guc); +} + +static void +print_standby_settings_common(FILE *fp, pgBackup *backup, pgRestoreParams *params) +{ + fio_fprintf(fp, "\n## standby settings\n"); + if (params->primary_conninfo) + fio_fprintf(fp, "primary_conninfo = '%s'\n", params->primary_conninfo); + else if (backup->primary_conninfo) + fio_fprintf(fp, "primary_conninfo = '%s'\n", backup->primary_conninfo); + + if (params->primary_slot_name != NULL) + fio_fprintf(fp, "primary_slot_name = '%s'\n", params->primary_slot_name); +} + +#if PG_VERSION_NUM < 120000 +static void +update_recovery_options_before_v12(pgBackup *backup, + pgRestoreParams *params, pgRecoveryTarget *rt) +{ + FILE *fp; + char path[MAXPGPATH]; + + /* + * If PITR is not requested and instance is not restored as replica, + * then recovery.conf should not be created. + */ + if (params->recovery_settings_mode != PITR_REQUESTED && + !params->restore_as_replica) { - /* - * 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); - snprintf(path, lengthof(path), "%s/probackup_recovery.conf", instance_config.pgdata); -#else - elog(LOG, "creating recovery.conf"); + elog(LOG, "update recovery settings in recovery.conf"); snprintf(path, lengthof(path), "%s/recovery.conf", instance_config.pgdata); -#endif fp = fio_fopen(path, "w", FIO_DB_HOST); if (fp == NULL) @@ -1288,226 +1400,188 @@ create_recovery_conf(time_t backup_id, 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 pg_probackup %s\n", - PROGRAM_VERSION); -#else fio_fprintf(fp, "# recovery.conf generated by pg_probackup %s\n", PROGRAM_VERSION); -#endif - /* construct restore_command */ - if (pitr_requested) - { - fio_fprintf(fp, "\n## recovery settings\n"); - /* If restore_command is provided, use it. Otherwise construct it from scratch. */ - if (restore_command_provided) - sprintf(restore_command_guc, "%s", instance_config.restore_command); - else - { - /* default cmdline, ok for local restore */ - sprintf(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); - - /* append --remote-* parameters provided via --archive-* settings */ - if (instance_config.archive.host) - { - strcat(restore_command_guc, " --remote-host="); - strcat(restore_command_guc, instance_config.archive.host); - } - - if (instance_config.archive.port) - { - strcat(restore_command_guc, " --remote-port="); - strcat(restore_command_guc, instance_config.archive.port); - } - - if (instance_config.archive.user) - { - strcat(restore_command_guc, " --remote-user="); - strcat(restore_command_guc, instance_config.archive.user); - } - } - - /* - * 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"); - - 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 - } - - if (rt->target_action) - fio_fprintf(fp, "recovery_target_action = '%s'\n", rt->target_action); - else - /* default recovery_target_action is 'pause' */ - fio_fprintf(fp, "recovery_target_action = '%s'\n", "pause"); - } - - if (pitr_requested) - { - elog(LOG, "Setting restore_command to '%s'", restore_command_guc); - fio_fprintf(fp, "restore_command = '%s'\n", restore_command_guc); - } + if (params->recovery_settings_mode == PITR_REQUESTED) + print_recovery_settings(fp, backup, params, rt); if (params->restore_as_replica) { - fio_fprintf(fp, "\n## standby settings\n"); - /* standby_mode was removed in PG12 */ -#if PG_VERSION_NUM < 120000 + print_standby_settings_common(fp, backup, params); fio_fprintf(fp, "standby_mode = 'on'\n"); -#endif - - if (params->primary_conninfo) - fio_fprintf(fp, "primary_conninfo = '%s'\n", params->primary_conninfo); - else if (backup->primary_conninfo) - fio_fprintf(fp, "primary_conninfo = '%s'\n", backup->primary_conninfo); - - if (params->primary_slot_name != NULL) - fio_fprintf(fp, "primary_slot_name = '%s'\n", params->primary_slot_name); } 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 PostgreSQL. - * 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 PostgreSQL must switch to a new timeline - * after successfull recovery or not? - */ - if (pitr_requested) - { - elog(LOG, "creating recovery.signal file"); - snprintf(path, lengthof(path), "%s/recovery.signal", instance_config.pgdata); - - 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)); - } - - if (params->restore_as_replica) - { - elog(LOG, "creating standby.signal file"); - snprintf(path, lengthof(path), "%s/standby.signal", instance_config.pgdata); - - 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 } +#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. + * Read postgresql.auto.conf, clean old recovery options, + * to avoid unexpected intersections. + * Write recovery options for this backup. */ -static void -pg12_recovery_config(pgBackup *backup, bool add_include) -{ #if PG_VERSION_NUM >= 120000 - char probackup_recovery_path[MAXPGPATH]; +static void +update_recovery_options(pgBackup *backup, + pgRestoreParams *params, pgRecoveryTarget *rt) + +{ char postgres_auto_path[MAXPGPATH]; + char postgres_auto_path_tmp[MAXPGPATH]; + char path[MAXPGPATH]; FILE *fp; + FILE *fp_tmp; + struct stat st; + char current_time_str[100]; + /* postgresql.auto.conf parsing */ + char line[16384] = "\0"; + char *buf = NULL; + int buf_len = 0; + int buf_len_max = 16384; - if (add_include) + elog(LOG, "update recovery settings in postgresql.auto.conf"); + + time2iso(current_time_str, lengthof(current_time_str), current_time, false); + + snprintf(postgres_auto_path, lengthof(postgres_auto_path), + "%s/postgresql.auto.conf", instance_config.pgdata); + + if (fio_stat(postgres_auto_path, &st, false, FIO_DB_HOST) < 0) { - char current_time_str[100]; + /* file not found is not an error case */ + if (errno != ENOENT) + elog(ERROR, "cannot stat file \"%s\": %s", postgres_auto_path, + strerror(errno)); + } - time2iso(current_time_str, lengthof(current_time_str), current_time); + fp = fio_open_stream(postgres_auto_path, FIO_DB_HOST); + if (fp == NULL && errno != ENOENT) + elog(ERROR, "cannot open \"%s\": %s", postgres_auto_path, strerror(errno)); - snprintf(postgres_auto_path, lengthof(postgres_auto_path), - "%s/postgresql.auto.conf", instance_config.pgdata); + sprintf(postgres_auto_path_tmp, "%s.tmp", postgres_auto_path); + fp_tmp = fio_fopen(postgres_auto_path_tmp, "w", FIO_DB_HOST); + if (fp_tmp == NULL) + elog(ERROR, "cannot open \"%s\": %s", postgres_auto_path_tmp, strerror(errno)); + while (fp && fgets(line, lengthof(line), fp)) + { + /* ignore "include 'probackup_recovery.conf'" directive */ + if (strstr(line, "include") && + strstr(line, "probackup_recovery.conf")) + { + continue; + } + + /* ignore already existing recovery options */ + if (strstr(line, "restore_command") || + strstr(line, "recovery_target")) + { + continue; + } + + if (!buf) + buf = pgut_malloc(buf_len_max); + + /* avoid buffer overflow */ + if ((buf_len + strlen(line)) >= buf_len_max) + { + buf_len_max += (buf_len + strlen(line)) *2; + buf = pgut_realloc(buf, buf_len_max); + } + + buf_len += snprintf(buf+buf_len, sizeof(line), "%s", line); + } + + /* close input postgresql.auto.conf */ + if (fp) + fio_close_stream(fp); + + /* TODO: detect remote error */ + if (buf_len > 0) + fio_fwrite(fp_tmp, buf, buf_len); + + if (fio_fflush(fp_tmp) != 0 || + fio_fclose(fp_tmp)) + elog(ERROR, "Cannot write file \"%s\": %s", postgres_auto_path_tmp, + strerror(errno)); + pg_free(buf); + + if (fio_rename(postgres_auto_path_tmp, postgres_auto_path, FIO_DB_HOST) < 0) + elog(ERROR, "Cannot rename file \"%s\" to \"%s\": %s", + postgres_auto_path_tmp, postgres_auto_path, strerror(errno)); + + if (fio_chmod(postgres_auto_path, FILE_PERMISSION, FIO_DB_HOST) == -1) + elog(ERROR, "Cannot change mode of \"%s\": %s", postgres_auto_path, strerror(errno)); + + if (params) + { fp = fio_fopen(postgres_auto_path, "a", FIO_DB_HOST); if (fp == NULL) - elog(ERROR, "cannot write to file \"%s\": %s", postgres_auto_path, + elog(ERROR, "cannot open file \"%s\": %s", postgres_auto_path, strerror(errno)); - // TODO: check if include 'probackup_recovery.conf' already exists - fio_fprintf(fp, "\n# created by pg_probackup restore of backup %s at '%s'\n", - base36enc(backup->start_time), current_time_str); - fio_fprintf(fp, "include '%s'\n", "probackup_recovery.conf"); + fio_fprintf(fp, "\n# recovery settings added by pg_probackup restore of backup %s at '%s'\n", + base36enc(backup->start_time), current_time_str); + + if (params->recovery_settings_mode == PITR_REQUESTED) + print_recovery_settings(fp, backup, params, rt); + + if (params->restore_as_replica) + print_standby_settings_common(fp, backup, params); if (fio_fflush(fp) != 0 || fio_fclose(fp)) - elog(ERROR, "cannot write to file \"%s\": %s", postgres_auto_path, + elog(ERROR, "cannot write file \"%s\": %s", postgres_auto_path, strerror(errno)); + + /* + * Create "recovery.signal" to mark this recovery as PITR for PostgreSQL. + * 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 PostgreSQL must switch to a new timeline + * after successfull recovery or not? + */ + if (params->recovery_settings_mode == PITR_REQUESTED) + { + elog(LOG, "creating recovery.signal file"); + snprintf(path, lengthof(path), "%s/recovery.signal", instance_config.pgdata); + + fp = fio_fopen(path, PG_BINARY_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)); + } + + if (params->restore_as_replica) + { + elog(LOG, "creating standby.signal file"); + snprintf(path, lengthof(path), "%s/standby.signal", instance_config.pgdata); + + fp = fio_fopen(path, PG_BINARY_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)); + } } - - /* Create empty probackup_recovery.conf */ - snprintf(probackup_recovery_path, lengthof(probackup_recovery_path), - "%s/probackup_recovery.conf", instance_config.pgdata); - 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; } +#endif /* * Try to read a timeline's history file. diff --git a/src/show.c b/src/show.c index a11a115e..61bde9ef 100644 --- a/src/show.c +++ b/src/show.c @@ -374,12 +374,12 @@ print_backup_json_object(PQExpBuffer buf, pgBackup *backup) (uint32) (backup->stop_lsn >> 32), (uint32) backup->stop_lsn); json_add_value(buf, "stop-lsn", lsn, json_level, true); - time2iso(timestamp, lengthof(timestamp), backup->start_time); + time2iso(timestamp, lengthof(timestamp), backup->start_time, false); json_add_value(buf, "start-time", timestamp, json_level, true); if (backup->end_time) { - time2iso(timestamp, lengthof(timestamp), backup->end_time); + time2iso(timestamp, lengthof(timestamp), backup->end_time, false); json_add_value(buf, "end-time", timestamp, json_level, true); } @@ -388,13 +388,13 @@ print_backup_json_object(PQExpBuffer buf, pgBackup *backup) if (backup->recovery_time > 0) { - time2iso(timestamp, lengthof(timestamp), backup->recovery_time); + time2iso(timestamp, lengthof(timestamp), backup->recovery_time, false); json_add_value(buf, "recovery-time", timestamp, json_level, true); } if (backup->expire_time > 0) { - time2iso(timestamp, lengthof(timestamp), backup->expire_time); + time2iso(timestamp, lengthof(timestamp), backup->expire_time, false); json_add_value(buf, "expire-time", timestamp, json_level, true); } @@ -482,7 +482,7 @@ show_backup(const char *instance_name, time_t requested_backup_id) } if (show_format == SHOW_PLAIN) - pgBackupWriteControl(stdout, backup); + pgBackupWriteControl(stdout, backup, false); else elog(ERROR, "Invalid show format %d", (int) show_format); @@ -550,7 +550,7 @@ show_instance_plain(const char *instance_name, parray *backup_list, bool show_na /* Recovery Time */ if (backup->recovery_time != (time_t) 0) time2iso(row->recovery_time, lengthof(row->recovery_time), - backup->recovery_time); + backup->recovery_time, false); else StrNCpy(row->recovery_time, "----", sizeof(row->recovery_time)); widths[cur] = Max(widths[cur], strlen(row->recovery_time)); diff --git a/src/stream.c b/src/stream.c index db9ffe13..825aa0e7 100644 --- a/src/stream.c +++ b/src/stream.c @@ -64,6 +64,11 @@ static int checkpoint_timeout(PGconn *backup_conn); static void *StreamLog(void *arg); static bool stop_streaming(XLogRecPtr xlogpos, uint32 timeline, bool segment_finished); +static void add_walsegment_to_filelist(parray *filelist, uint32 timeline, + XLogRecPtr xlogpos, char *basedir, + uint32 xlog_seg_size); +static void add_history_file_to_filelist(parray *filelist, uint32 timeline, + char *basedir); /* * Run IDENTIFY_SYSTEM through a given connection and @@ -166,6 +171,8 @@ StreamLog(void *arg) */ stream_arg->startpos -= stream_arg->startpos % instance_config.xlog_seg_size; + xlog_files_list = parray_new(); + /* Initialize timeout */ stream_stop_begin = 0; @@ -205,7 +212,7 @@ StreamLog(void *arg) stream_arg->basedir, // (instance_config.compress_alg == NONE_COMPRESS) ? 0 : instance_config.compress_level, 0, - true); + false); ctl.replication_slot = replication_slot; ctl.stop_socket = PGINVALID_SOCKET; ctl.do_sync = false; /* We sync all files at the end of backup */ @@ -239,6 +246,25 @@ StreamLog(void *arg) elog(ERROR, "Problem in receivexlog"); #endif + /* be paranoid and sort xlog_files_list, + * so if stop_lsn segno is already in the list, + * then list must be sorted to detect duplicates. + */ + parray_qsort(xlog_files_list, pgFileCompareRelPathWithExternal); + + /* Add the last segment to the list */ + add_walsegment_to_filelist(xlog_files_list, stream_arg->starttli, + stop_stream_lsn, (char *) stream_arg->basedir, + instance_config.xlog_seg_size); + + /* append history file to walsegment filelist */ + add_history_file_to_filelist(xlog_files_list, stream_arg->starttli, (char *) stream_arg->basedir); + + /* + * TODO: remove redundant WAL segments + * walk pg_wal and remove files with segno greater that of stop_lsn`s segno +1 + */ + elog(LOG, "finished streaming WAL at %X/%X (timeline %u)", (uint32) (stop_stream_lsn >> 32), (uint32) stop_stream_lsn, stream_arg->starttli); stream_arg->ret = 0; @@ -275,41 +301,12 @@ stop_streaming(XLogRecPtr xlogpos, uint32 timeline, bool segment_finished) /* we assume that we get called once at the end of each segment */ if (segment_finished) { - XLogSegNo xlog_segno; - char wal_segment_name[MAXNAMLEN]; - char wal_segment_relpath[MAXPGPATH]; - char wal_segment_fullpath[MAXPGPATH]; - pgFile *file = NULL; - elog(VERBOSE, _("finished segment at %X/%X (timeline %u)"), (uint32) (xlogpos >> 32), (uint32) xlogpos, timeline); - /* Add streamed xlog file into the backup's list of files */ - if (!xlog_files_list) - xlog_files_list = parray_new(); - - GetXLogSegNo(xlogpos, xlog_segno, instance_config.xlog_seg_size); - - /* xlogpos points to the current segment, and we need the finished - previous one */ - xlog_segno--; - GetXLogFileName(wal_segment_name, timeline, xlog_segno, - instance_config.xlog_seg_size); - - join_path_components(wal_segment_fullpath, - stream_thread_arg.basedir, wal_segment_name); - - join_path_components(wal_segment_relpath, - PG_XLOG_DIR, wal_segment_name); - - /* append file to filelist */ - file = pgFileNew(wal_segment_fullpath, wal_segment_relpath, false, 0, FIO_BACKUP_HOST); - file->name = file->rel_path; - file->crc = pgFileGetCRC(wal_segment_fullpath, true, false); - - /* Should we recheck it using stat? */ - file->write_size = instance_config.xlog_seg_size; - file->uncompressed_size = instance_config.xlog_seg_size; - parray_append(xlog_files_list, file); + add_walsegment_to_filelist(xlog_files_list, timeline, xlogpos, + (char*) stream_thread_arg.basedir, + instance_config.xlog_seg_size); } /* @@ -395,9 +392,94 @@ start_WAL_streaming(PGconn *backup_conn, char *stream_dst_path, ConnectionOption int wait_WAL_streaming_end(parray *backup_files_list) { - parray_concat(backup_files_list, xlog_files_list); - parray_free(xlog_files_list); + pthread_join(stream_thread, NULL); - pthread_join(stream_thread, NULL); - return stream_thread_arg.ret; + parray_concat(backup_files_list, xlog_files_list); + parray_free(xlog_files_list); + return stream_thread_arg.ret; +} + +/* Append streamed WAL segment to filelist */ +void +add_walsegment_to_filelist(parray *filelist, uint32 timeline, XLogRecPtr xlogpos, char *basedir, uint32 xlog_seg_size) +{ + XLogSegNo xlog_segno; + char wal_segment_name[MAXFNAMELEN]; + char wal_segment_relpath[MAXPGPATH]; + char wal_segment_fullpath[MAXPGPATH]; + pgFile *file = NULL; + pgFile **existing_file = NULL; + + GetXLogSegNo(xlogpos, xlog_segno, xlog_seg_size); + + /* + * When xlogpos points to the zero offset (0/3000000), + * it means that previous segment was just successfully streamed. + * When xlogpos points to the positive offset, + * then current segment is successfully streamed. + */ + if (WalSegmentOffset(xlogpos, xlog_seg_size) == 0) + xlog_segno--; + + GetXLogFileName(wal_segment_name, timeline, xlog_segno, xlog_seg_size); + + join_path_components(wal_segment_fullpath, basedir, wal_segment_name); + join_path_components(wal_segment_relpath, PG_XLOG_DIR, wal_segment_name); + + file = pgFileNew(wal_segment_fullpath, wal_segment_relpath, false, 0, FIO_BACKUP_HOST); + + /* + * Check if file is already in the list + * stop_lsn segment can be added to this list twice, so + * try not to add duplicates + */ + + existing_file = (pgFile **) parray_bsearch(filelist, file, pgFileCompareRelPathWithExternal); + + if (existing_file) + { + (*existing_file)->crc = pgFileGetCRC(wal_segment_fullpath, true, false); + (*existing_file)->write_size = xlog_seg_size; + (*existing_file)->uncompressed_size = xlog_seg_size; + + return; + } + + /* calculate crc */ + file->crc = pgFileGetCRC(wal_segment_fullpath, true, false); + + /* Should we recheck it using stat? */ + file->write_size = xlog_seg_size; + file->uncompressed_size = xlog_seg_size; + + /* append file to filelist */ + parray_append(filelist, file); +} + +/* Append history file to filelist */ +void +add_history_file_to_filelist(parray *filelist, uint32 timeline, char *basedir) +{ + char filename[MAXFNAMELEN]; + char fullpath[MAXPGPATH]; + char relpath[MAXPGPATH]; + pgFile *file = NULL; + + /* Timeline 1 does not have a history file */ + if (timeline == 1) + return; + + snprintf(filename, lengthof(filename), "%08X.history", timeline); + join_path_components(fullpath, basedir, filename); + join_path_components(relpath, PG_XLOG_DIR, filename); + + file = pgFileNew(fullpath, relpath, false, 0, FIO_BACKUP_HOST); + + /* calculate crc */ + file->crc = pgFileGetCRC(fullpath, true, false); + file->write_size = file->size; + file->uncompressed_size = file->size; + + /* append file to filelist */ + parray_append(filelist, file); } diff --git a/src/utils/configuration.c b/src/utils/configuration.c index 1ef332ed..d6a7d069 100644 --- a/src/utils/configuration.c +++ b/src/utils/configuration.c @@ -490,9 +490,9 @@ config_read_opt(const char *path, ConfigOption options[], int elevel, bool strict, bool missing_ok) { FILE *fp; - char buf[1024]; + char buf[4096]; char key[1024]; - char value[1024]; + char value[2048]; int parsed_options = 0; if (!options) @@ -679,7 +679,7 @@ option_get_value(ConfigOption *opt) if (t > 0) { timestamp = palloc(100); - time2iso(timestamp, 100, t); + time2iso(timestamp, 100, t, false); } else timestamp = palloc0(1 /* just null termination */); @@ -1111,6 +1111,8 @@ parse_time(const char *value, time_t *result, bool utc_default) struct tm tm; char junk[2]; + char *local_tz = getenv("TZ"); + /* tmp = replace( value, !isalnum, ' ' ) */ tmp = pgut_malloc(strlen(value) + + 1); len = 0; @@ -1221,24 +1223,37 @@ parse_time(const char *value, time_t *result, bool utc_default) /* determine whether Daylight Saving Time is in effect */ tm.tm_isdst = -1; + /* + * If tz is not set, + * treat it as UTC if requested, otherwise as local timezone + */ + if (tz_set || utc_default) + { + /* set timezone to UTC */ + pgut_setenv("TZ", "UTC"); +#ifdef WIN32 + tzset(); +#endif + } + + /* convert time to utc unix time */ *result = mktime(&tm); + /* return old timezone back if any */ + if (local_tz) + pgut_setenv("TZ", local_tz); + else + pgut_unsetenv("TZ"); + +#ifdef WIN32 + tzset(); +#endif + /* adjust time zone */ if (tz_set || utc_default) { - time_t ltime = time(NULL); - struct tm *ptm = gmtime(<ime); - time_t gmt = mktime(ptm); - time_t offset; - /* UTC time */ *result -= tz; - - /* Get local time */ - ptm = localtime(<ime); - offset = ltime - gmt + (ptm->tm_isdst ? 3600 : 0); - - *result += offset; } return true; @@ -1462,14 +1477,40 @@ convert_from_base_unit_u(uint64 base_value, int base_unit, * Convert time_t value to ISO-8601 format string. Always set timezone offset. */ void -time2iso(char *buf, size_t len, time_t time) +time2iso(char *buf, size_t len, time_t time, bool utc) { - struct tm *ptm = gmtime(&time); - time_t gmt = mktime(ptm); + struct tm *ptm = NULL; + time_t gmt; time_t offset; char *ptr = buf; + char *local_tz = getenv("TZ"); + /* set timezone to UTC if requested */ + if (utc) + { + pgut_setenv("TZ", "UTC"); +#ifdef WIN32 + tzset(); +#endif + } + + ptm = gmtime(&time); + gmt = mktime(ptm); ptm = localtime(&time); + + if (utc) + { + /* return old timezone back if any */ + if (local_tz) + pgut_setenv("TZ", local_tz); + else + pgut_unsetenv("TZ"); +#ifdef WIN32 + tzset(); +#endif + } + + /* adjust timezone offset */ offset = time - gmt + (ptm->tm_isdst ? 3600 : 0); strftime(ptr, len, "%Y-%m-%d %H:%M:%S", ptm); diff --git a/src/utils/configuration.h b/src/utils/configuration.h index 46b5d6c1..eea8c774 100644 --- a/src/utils/configuration.h +++ b/src/utils/configuration.h @@ -96,7 +96,7 @@ extern bool parse_int(const char *value, int *result, int flags, const char **hintmsg); extern bool parse_lsn(const char *value, XLogRecPtr *result); -extern void time2iso(char *buf, size_t len, time_t time); +extern void time2iso(char *buf, size_t len, time_t time, bool utc); extern void convert_from_base_unit(int64 base_value, int base_unit, int64 *value, const char **unit); diff --git a/src/utils/pgut.c b/src/utils/pgut.c index 27cfe6d1..bc35a24f 100644 --- a/src/utils/pgut.c +++ b/src/utils/pgut.c @@ -1213,3 +1213,61 @@ pgut_rmtree(const char *path, bool rmtopdir, bool strict) return result; } + +/* cross-platform setenv */ +void +pgut_setenv(const char *key, const char *val) +{ +#ifdef WIN32 + char *envstr = NULL; + envstr = psprintf("%s=%s", key, val); + putenv(envstr); +#else + setenv(key, val, 1); +#endif +} + +/* stolen from unsetenv.c */ +void +pgut_unsetenv(const char *key) +{ +#ifdef WIN32 + char *envstr = NULL; + + if (getenv(key) == NULL) + return; /* no work */ + + /* + * The technique embodied here works if libc follows the Single Unix Spec + * and actually uses the storage passed to putenv() to hold the environ + * entry. When we clobber the entry in the second step we are ensuring + * that we zap the actual environ member. However, there are some libc + * implementations (notably recent BSDs) that do not obey SUS but copy the + * presented string. This method fails on such platforms. Hopefully all + * such platforms have unsetenv() and thus won't be using this hack. See: + * http://www.greenend.org.uk/rjk/2008/putenv.html + * + * Note that repeatedly setting and unsetting a var using this code will + * leak memory. + */ + + envstr = (char *) pgut_malloc(strlen(key) + 2); + if (!envstr) /* not much we can do if no memory */ + return; + + /* Override the existing setting by forcibly defining the var */ + sprintf(envstr, "%s=", key); + putenv(envstr); + + /* Now we can clobber the variable definition this way: */ + strcpy(envstr, "="); + + /* + * This last putenv cleans up if we have multiple zero-length names as a + * result of unsetting multiple things. + */ + putenv(envstr); +#else + unsetenv(key); +#endif +} diff --git a/src/validate.c b/src/validate.c index 8159881a..30bfbf3e 100644 --- a/src/validate.c +++ b/src/validate.c @@ -192,7 +192,7 @@ pgBackupValidate(pgBackup *backup, pgRestoreParams *params) backup->status = BACKUP_STATUS_CORRUPT; write_backup_status(backup, corrupted ? BACKUP_STATUS_CORRUPT : - BACKUP_STATUS_OK, instance_name, true); + BACKUP_STATUS_OK, instance_name, true); if (corrupted) elog(WARNING, "Backup %s data files are corrupted", base36enc(backup->start_time)); @@ -570,7 +570,7 @@ do_validate_instance(void) base_full_backup = current_backup; /* Do not interrupt, validate the next backup */ - if (!lock_backup(current_backup, true)) + if (!lock_backup(current_backup, true, false)) { elog(WARNING, "Cannot lock backup %s directory, skip validation", base36enc(current_backup->start_time)); @@ -665,7 +665,7 @@ do_validate_instance(void) if (backup->status == BACKUP_STATUS_ORPHAN) { /* Do not interrupt, validate the next backup */ - if (!lock_backup(backup, true)) + if (!lock_backup(backup, true, false)) { elog(WARNING, "Cannot lock backup %s directory, skip validation", base36enc(backup->start_time)); diff --git a/tests/archive.py b/tests/archive.py index 5a09bc8c..4551fda9 100644 --- a/tests/archive.py +++ b/tests/archive.py @@ -1612,7 +1612,7 @@ class ArchiveTest(ProbackupTest, unittest.TestCase): ]) if self.get_version(node) >= self.version_to_num('12.0'): - recovery_conf = os.path.join(node.data_dir, 'probackup_recovery.conf') + recovery_conf = os.path.join(node.data_dir, 'postgresql.auto.conf') else: recovery_conf = os.path.join(node.data_dir, 'recovery.conf') @@ -1687,7 +1687,7 @@ class ArchiveTest(ProbackupTest, unittest.TestCase): self.restore_node(backup_dir, 'node', node) if self.get_version(node) >= self.version_to_num('12.0'): - recovery_conf = os.path.join(node.data_dir, 'probackup_recovery.conf') + recovery_conf = os.path.join(node.data_dir, 'postgresql.auto.conf') else: recovery_conf = os.path.join(node.data_dir, 'recovery.conf') @@ -2101,7 +2101,7 @@ class ArchiveTest(ProbackupTest, unittest.TestCase): if node.major_version >= 12: node.append_conf( - 'probackup_recovery.conf', "restore_command = '{0}'".format(restore_command)) + 'postgresql.auto.conf', "restore_command = '{0}'".format(restore_command)) else: node.append_conf( 'recovery.conf', "restore_command = '{0}'".format(restore_command)) diff --git a/tests/backup.py b/tests/backup.py index 957231cb..bf0cbca8 100644 --- a/tests/backup.py +++ b/tests/backup.py @@ -212,9 +212,7 @@ class BackupTest(ProbackupTest, unittest.TestCase): except ProbackupException as e: self.assertTrue( "INFO: Validate backups of the instance 'node'" in e.message and - "WARNING: Backup file".format( - file) in e.message and - "is not found".format(file) in e.message and + "WARNING: Backup file" in e.message and "is not found" in e.message and "WARNING: Backup {0} data files are corrupted".format( backup_id) in e.message and "WARNING: Some backups are not valid" in e.message, diff --git a/tests/checkdb.py b/tests/checkdb.py index a7527d57..3349ad2e 100644 --- a/tests/checkdb.py +++ b/tests/checkdb.py @@ -210,6 +210,7 @@ class CheckdbTest(ProbackupTest, unittest.TestCase): log_file_content) # Clean after yourself + gdb.kill() self.del_test_dir(module_name, fname) # @unittest.skip("skip") @@ -495,6 +496,7 @@ class CheckdbTest(ProbackupTest, unittest.TestCase): self.assertNotIn('connection to client lost', output) # Clean after yourself + gdb.kill() self.del_test_dir(module_name, fname) # @unittest.skip("skip") @@ -588,7 +590,7 @@ class CheckdbTest(ProbackupTest, unittest.TestCase): 'GRANT EXECUTE ON FUNCTION pg_catalog.charne("char", "char") TO backup; ' 'GRANT EXECUTE ON FUNCTION pg_catalog.pg_is_in_recovery() TO backup; ' 'GRANT EXECUTE ON FUNCTION pg_catalog.pg_control_system() TO backup; ' - 'GRANT EXECUTE ON FUNCTION bt_index_check(regclass) TO backup; ' +# 'GRANT EXECUTE ON FUNCTION bt_index_check(regclass) TO backup; ' 'GRANT EXECUTE ON FUNCTION bt_index_check(regclass, bool) TO backup;' ) # >= 10 @@ -618,14 +620,10 @@ class CheckdbTest(ProbackupTest, unittest.TestCase): 'GRANT EXECUTE ON FUNCTION bt_index_check(regclass, bool) TO backup;' ) -# if ProbackupTest.enterprise: -# node.safe_psql( -# "backupdb", -# "GRANT EXECUTE ON FUNCTION pg_catalog.pgpro_edition() TO backup") -# -# node.safe_psql( -# "backupdb", -# "GRANT EXECUTE ON FUNCTION pg_catalog.pgpro_version() TO backup") + if ProbackupTest.enterprise: + node.safe_psql( + "backupdb", + "GRANT EXECUTE ON FUNCTION pg_catalog.pgpro_edition() TO backup") # checkdb try: @@ -659,3 +657,6 @@ class CheckdbTest(ProbackupTest, unittest.TestCase): e.message, "\n Unexpected Error Message: {0}\n CMD: {1}".format( repr(e.message), self.cmd)) + + # Clean after yourself + self.del_test_dir(module_name, fname) diff --git a/tests/compatibility.py b/tests/compatibility.py index d2db2be2..54793334 100644 --- a/tests/compatibility.py +++ b/tests/compatibility.py @@ -675,7 +675,7 @@ class CompatibilityTest(ProbackupTest, unittest.TestCase): # check that in-place is disabled self.assertIn( "WARNING: In-place merge is disabled " - "because of program versions mismatch", output) + "because of storage format incompatibility", output) # restore merged backup node_restored = self.make_simple_node( @@ -1011,6 +1011,87 @@ class CompatibilityTest(ProbackupTest, unittest.TestCase): # Clean after yourself self.del_test_dir(module_name, fname) + # @unittest.expectedFailure + # @unittest.skip("skip") + def test_backward_compatibility_merge_5(self): + """ + Create node, take FULL and PAGE backups with old binary, + merge them with new binary. + old binary version >= STORAGE_FORMAT_VERSION (2.4.4) + """ + + if self.version_to_num(self.old_probackup_version) < self.version_to_num('2.4.4'): + return unittest.skip('OLD pg_probackup binary must be == 2.4.4 for this test') + + self.assertNotEqual( + self.version_to_num(self.old_probackup_version), + self.version_to_num(self.probackup_version)) + + fname = self.id().split('.')[3] + backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') + node = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node'), + set_replication=True, + initdb_params=['--data-checksums'], + pg_options={'autovacuum': 'off'}) + + self.init_pb(backup_dir, old_binary=True) + self.add_instance(backup_dir, 'node', node, old_binary=True) + + self.set_archiving(backup_dir, 'node', node, old_binary=True) + node.slow_start() + + node.pgbench_init(scale=20) + + # FULL backup with OLD binary + self.backup_node(backup_dir, 'node', node, old_binary=True) + + pgbench = node.pgbench( + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + options=["-c", "1", "-T", "10", "--no-vacuum"]) + pgbench.wait() + pgbench.stdout.close() + + # PAGE1 backup with OLD binary + self.backup_node( + backup_dir, 'node', node, backup_type='page', old_binary=True) + + node.safe_psql( + 'postgres', + 'DELETE from pgbench_accounts') + + node.safe_psql( + 'postgres', + 'VACUUM pgbench_accounts') + + # PAGE2 backup with OLD binary + backup_id = self.backup_node( + backup_dir, 'node', node, backup_type='page', old_binary=True) + + pgdata = self.pgdata_content(node.data_dir) + + # merge chain created by old binary with new binary + output = self.merge_backup(backup_dir, "node", backup_id) + + # check that in-place is disabled + self.assertNotIn( + "WARNING: In-place merge is disabled " + "because of storage format incompatibility", output) + + # restore merged backup + node_restored = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node_restored')) + node_restored.cleanup() + + self.restore_node(backup_dir, 'node', node_restored) + + pgdata_restored = self.pgdata_content(node_restored.data_dir) + self.compare_pgdata(pgdata, pgdata_restored) + + # Clean after yourself + self.del_test_dir(module_name, fname) + # @unittest.skip("skip") def test_page_vacuum_truncate(self): """ diff --git a/tests/delta.py b/tests/delta.py index 47e6278c..daa423d4 100644 --- a/tests/delta.py +++ b/tests/delta.py @@ -433,11 +433,11 @@ class DeltaTest(ProbackupTest, unittest.TestCase): node.safe_psql("postgres", "checkpoint") # GET LOGICAL CONTENT FROM NODE - result = node.safe_psql("postgres", "select * from pgbench_accounts") + result = node.safe_psql("postgres", "select count(*) from pgbench_accounts") # delta BACKUP self.backup_node( - backup_dir, 'node', node, backup_type='delta', - options=['--stream']) + backup_dir, 'node', node, + backup_type='delta', options=['--stream']) # GET PHYSICAL CONTENT FROM NODE pgdata = self.pgdata_content(node.data_dir) @@ -450,8 +450,10 @@ class DeltaTest(ProbackupTest, unittest.TestCase): restored_node, 'somedata_restored') self.restore_node( - backup_dir, 'node', restored_node, options=[ - "-j", "4", "-T", "{0}={1}".format(tblspc_path, tblspc_path_new)]) + backup_dir, 'node', restored_node, + options=[ + "-j", "4", "-T", "{0}={1}".format( + tblspc_path, tblspc_path_new)]) # GET PHYSICAL CONTENT FROM NODE_RESTORED pgdata_restored = self.pgdata_content(restored_node.data_dir) @@ -461,7 +463,8 @@ class DeltaTest(ProbackupTest, unittest.TestCase): restored_node.slow_start() result_new = restored_node.safe_psql( - "postgres", "select * from pgbench_accounts") + "postgres", + "select count(*) from pgbench_accounts") # COMPARE RESTORED FILES self.assertEqual(result, result_new, 'data is lost') diff --git a/tests/expected/option_version.out b/tests/expected/option_version.out index 342842b2..55d7c7f0 100644 --- a/tests/expected/option_version.out +++ b/tests/expected/option_version.out @@ -1 +1,5 @@ -pg_probackup 2.5.0 \ No newline at end of file +<<<<<<< HEAD +pg_probackup 2.5.0 +======= +pg_probackup 2.4.8 +>>>>>>> master diff --git a/tests/helpers/ptrack_helpers.py b/tests/helpers/ptrack_helpers.py index 5ba43e92..d340213c 100644 --- a/tests/helpers/ptrack_helpers.py +++ b/tests/helpers/ptrack_helpers.py @@ -246,18 +246,6 @@ class ProbackupTest(object): print('pg_probackup binary is not found') exit(1) - self.probackup_version = None - - try: - self.probackup_version_output = subprocess.check_output( - [self.probackup_path, "--version"], - stderr=subprocess.STDOUT, - ).decode('utf-8') - except subprocess.CalledProcessError as e: - raise ProbackupException(e.output.decode('utf-8')) - - self.probackup_version = re.search(r"\d+\.\d+\.\d+", self.probackup_version_output).group(0) - if os.name == 'posix': self.EXTERNAL_DIRECTORY_DELIMITER = ':' os.environ['PATH'] = os.path.dirname( @@ -280,6 +268,32 @@ class ProbackupTest(object): if self.verbose: print('PGPROBACKUPBIN_OLD is not an executable file') + self.probackup_version = None + self.old_probackup_version = None + + try: + self.probackup_version_output = subprocess.check_output( + [self.probackup_path, "--version"], + stderr=subprocess.STDOUT, + ).decode('utf-8') + except subprocess.CalledProcessError as e: + raise ProbackupException(e.output.decode('utf-8')) + + if self.probackup_old_path: + old_probackup_version_output = subprocess.check_output( + [self.probackup_old_path, "--version"], + stderr=subprocess.STDOUT, + ).decode('utf-8') + self.old_probackup_version = re.search( + r"\d+\.\d+\.\d+", + subprocess.check_output( + [self.probackup_old_path, "--version"], + stderr=subprocess.STDOUT, + ).decode('utf-8') + ).group(0) + + self.probackup_version = re.search(r"\d+\.\d+\.\d+", self.probackup_version_output).group(0) + self.remote = False self.remote_host = None self.remote_port = None @@ -693,7 +707,7 @@ class ProbackupTest(object): ) ) - def run_pb(self, command, asynchronous=False, gdb=False, old_binary=False, return_id=True): + def run_pb(self, command, asynchronous=False, gdb=False, old_binary=False, return_id=True, env=None): if not self.probackup_old_path and old_binary: print('PGPROBACKUPBIN_OLD is not set') exit(1) @@ -703,6 +717,9 @@ class ProbackupTest(object): else: binary_path = self.probackup_path + if not env: + env=self.test_env + try: self.cmd = [' '.join(map(str, [binary_path] + command))] if self.verbose: @@ -714,13 +731,13 @@ class ProbackupTest(object): self.cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - env=self.test_env + env=env ) else: self.output = subprocess.check_output( [binary_path] + command, stderr=subprocess.STDOUT, - env=self.test_env + env=env ).decode('utf-8') if command[0] == 'backup' and return_id: # return backup ID @@ -831,7 +848,8 @@ class ProbackupTest(object): self, backup_dir, instance, node, data_dir=False, backup_type='full', datname=False, options=[], asynchronous=False, gdb=False, - old_binary=False, return_id=True, no_remote=False + old_binary=False, return_id=True, no_remote=False, + env=None ): if not node and not data_dir: print('You must provide ether node or data_dir for backup') @@ -864,7 +882,7 @@ class ProbackupTest(object): if not old_binary: cmd_list += ['--no-sync'] - return self.run_pb(cmd_list + options, asynchronous, gdb, old_binary, return_id) + return self.run_pb(cmd_list + options, asynchronous, gdb, old_binary, return_id, env=env) def checkdb_node( self, backup_dir=False, instance=False, data_dir=False, @@ -928,7 +946,8 @@ class ProbackupTest(object): def show_pb( self, backup_dir, instance=None, backup_id=None, - options=[], as_text=False, as_json=True, old_binary=False + options=[], as_text=False, as_json=True, old_binary=False, + env=None ): backup_list = [] @@ -949,7 +968,7 @@ class ProbackupTest(object): if as_text: # You should print it when calling as_text=true - return self.run_pb(cmd_list + options, old_binary=old_binary) + return self.run_pb(cmd_list + options, old_binary=old_binary, env=env) # get show result as list of lines if as_json: @@ -974,7 +993,7 @@ class ProbackupTest(object): return backup_list else: show_splitted = self.run_pb( - cmd_list + options, old_binary=old_binary).splitlines() + cmd_list + options, old_binary=old_binary, env=env).splitlines() if instance is not None and backup_id is None: # cut header(ID, Mode, etc) from show as single string header = show_splitted[1:2][0] @@ -1102,7 +1121,7 @@ class ProbackupTest(object): def delete_pb( self, backup_dir, instance, - backup_id=None, options=[], old_binary=False): + backup_id=None, options=[], old_binary=False, gdb=False): cmd_list = [ 'delete', '-B', backup_dir @@ -1112,7 +1131,7 @@ class ProbackupTest(object): if backup_id: cmd_list += ['-i', backup_id] - return self.run_pb(cmd_list + options, old_binary=old_binary) + return self.run_pb(cmd_list + options, old_binary=old_binary, gdb=gdb) def delete_expired( self, backup_dir, instance, options=[], old_binary=False): @@ -1142,8 +1161,9 @@ class ProbackupTest(object): out_dict = {} if self.get_version(node) >= self.version_to_num('12.0'): - recovery_conf_path = os.path.join( - node.data_dir, 'probackup_recovery.conf') + recovery_conf_path = os.path.join(node.data_dir, 'postgresql.auto.conf') + with open(recovery_conf_path, 'r') as f: + print(f.read()) else: recovery_conf_path = os.path.join(node.data_dir, 'recovery.conf') @@ -1311,10 +1331,6 @@ class ProbackupTest(object): f.close() config = 'postgresql.auto.conf' - probackup_recovery_path = os.path.join(replica.data_dir, 'probackup_recovery.conf') - if os.path.exists(probackup_recovery_path): - if os.stat(probackup_recovery_path).st_size > 0: - config = 'probackup_recovery.conf' if not log_shipping: self.set_auto_conf( @@ -1505,7 +1521,8 @@ class ProbackupTest(object): 'backup_label', 'tablespace_map', 'recovery.conf', 'ptrack_control', 'ptrack_init', 'pg_control', 'probackup_recovery.conf', 'recovery.signal', - 'standby.signal', 'ptrack.map', 'ptrack.map.mmap' + 'standby.signal', 'ptrack.map', 'ptrack.map.mmap', + 'ptrack.map.tmp' ] if exclude_dirs: @@ -1531,6 +1548,7 @@ class ProbackupTest(object): directory_dict['files'][file_relpath] = {'is_datafile': False} with open(file_fullpath, 'rb') as f: directory_dict['files'][file_relpath]['md5'] = hashlib.md5(f.read()).hexdigest() + f.close() # directory_dict['files'][file_relpath]['md5'] = hashlib.md5( # f = open(file_fullpath, 'rb').read()).hexdigest() diff --git a/tests/incr_restore.py b/tests/incr_restore.py index 6fb0edbc..c71b952c 100644 --- a/tests/incr_restore.py +++ b/tests/incr_restore.py @@ -142,6 +142,7 @@ class IncrRestoreTest(ProbackupTest, unittest.TestCase): fname = self.id().split('.')[3] node = self.make_simple_node( base_dir=os.path.join(module_name, fname, 'node'), + set_replication=True, initdb_params=['--data-checksums']) backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') @@ -184,6 +185,7 @@ class IncrRestoreTest(ProbackupTest, unittest.TestCase): node = self.make_simple_node( base_dir=os.path.join(module_name, fname, 'node'), initdb_params=['--data-checksums'], + set_replication=True, pg_options={'autovacuum': 'off'}) backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') @@ -245,6 +247,7 @@ class IncrRestoreTest(ProbackupTest, unittest.TestCase): node = self.make_simple_node( base_dir=os.path.join(module_name, fname, 'node'), initdb_params=['--data-checksums'], + set_replication=True, pg_options={'autovacuum': 'off'}) backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') @@ -1320,7 +1323,7 @@ class IncrRestoreTest(ProbackupTest, unittest.TestCase): self.assertEqual( node.safe_psql( 'postgres', - 'select count(*) from t1').rstrip(), + 'select count(*) from t1').decode('utf-8').rstrip(), '1') # Clean after yourself @@ -1488,7 +1491,7 @@ class IncrRestoreTest(ProbackupTest, unittest.TestCase): self.assertEqual( node.safe_psql( 'postgres', - 'select count(*) from t1').rstrip(), + 'select count(*) from t1').decode('utf-8').rstrip(), '1') # Clean after yourself diff --git a/tests/locking.py b/tests/locking.py index 2da2415e..92c779c8 100644 --- a/tests/locking.py +++ b/tests/locking.py @@ -50,7 +50,7 @@ class LockingTest(ProbackupTest, unittest.TestCase): backup_id = self.show_pb(backup_dir, 'node')[1]['id'] self.assertIn( - "is using backup {0} and still is running".format(backup_id), + "is using backup {0}, and is still running".format(backup_id), validate_output, '\n Unexpected Validate Output: {0}\n'.format(repr(validate_output))) @@ -61,7 +61,8 @@ class LockingTest(ProbackupTest, unittest.TestCase): 'RUNNING', self.show_pb(backup_dir, 'node')[1]['status']) # Clean after yourself - # self.del_test_dir(module_name, fname) + gdb.kill() + self.del_test_dir(module_name, fname) def test_locking_running_validate_2(self): """ @@ -129,6 +130,7 @@ class LockingTest(ProbackupTest, unittest.TestCase): 'ERROR', self.show_pb(backup_dir, 'node')[1]['status']) # Clean after yourself + gdb.kill() self.del_test_dir(module_name, fname) def test_locking_running_validate_2_specific_id(self): @@ -227,6 +229,7 @@ class LockingTest(ProbackupTest, unittest.TestCase): repr(e.message), self.cmd)) # Clean after yourself + gdb.kill() self.del_test_dir(module_name, fname) def test_locking_running_3(self): @@ -296,6 +299,7 @@ class LockingTest(ProbackupTest, unittest.TestCase): 'ERROR', self.show_pb(backup_dir, 'node')[1]['status']) # Clean after yourself + gdb.kill() self.del_test_dir(module_name, fname) def test_locking_restore_locked(self): @@ -303,8 +307,8 @@ class LockingTest(ProbackupTest, unittest.TestCase): make node, take full backup, take two page backups, launch validate on PAGE1 and stop it in the middle, launch restore of PAGE2. - Expect restore to fail because validation of - intermediate backup is impossible + Expect restore to sucseed because read-only locks + do not conflict """ fname = self.id().split('.')[3] node = self.make_simple_node( @@ -334,24 +338,13 @@ class LockingTest(ProbackupTest, unittest.TestCase): node.cleanup() - try: - self.restore_node(backup_dir, 'node', node) - self.assertEqual( - 1, 0, - "Expecting Error because restore without whole chain validation " - "is prohibited unless --no-validate provided.\n " - "Output: {0} \n CMD: {1}".format( - repr(self.output), self.cmd)) - except ProbackupException as e: - self.assertTrue( - "ERROR: Cannot lock backup {0} directory\n".format(full_id) in e.message, - '\n Unexpected Error Message: {0}\n CMD: {1}'.format( - repr(e.message), self.cmd)) + self.restore_node(backup_dir, 'node', node) # Clean after yourself + gdb.kill() self.del_test_dir(module_name, fname) - def test_locking_restore_locked_without_validation(self): + def test_concurrent_delete_and_restore(self): """ make node, take full backup, take page backup, launch validate on FULL and stop it in the middle, @@ -376,10 +369,11 @@ class LockingTest(ProbackupTest, unittest.TestCase): # PAGE1 restore_id = self.backup_node(backup_dir, 'node', node, backup_type='page') - gdb = self.validate_pb( + gdb = self.delete_pb( backup_dir, 'node', backup_id=backup_id, gdb=True) - gdb.set_breakpoint('pgBackupValidate') + # gdb.set_breakpoint('pgFileDelete') + gdb.set_breakpoint('delete_backup_files') gdb.run_until_break() node.cleanup() @@ -397,13 +391,14 @@ class LockingTest(ProbackupTest, unittest.TestCase): self.assertTrue( "Backup {0} is used without validation".format( restore_id) in e.message and - 'is using backup {0} and still is running'.format( + 'is using backup {0}, and is still running'.format( backup_id) in e.message and 'ERROR: Cannot lock backup' in e.message, '\n Unexpected Error Message: {0}\n CMD: {1}'.format( repr(e.message), self.cmd)) # Clean after yourself + gdb.kill() self.del_test_dir(module_name, fname) def test_locking_concurrent_validate_and_backup(self): @@ -439,6 +434,7 @@ class LockingTest(ProbackupTest, unittest.TestCase): self.backup_node(backup_dir, 'node', node, backup_type='page') # Clean after yourself + gdb.kill() self.del_test_dir(module_name, fname) def test_locking_concurren_restore_and_delete(self): @@ -467,7 +463,6 @@ class LockingTest(ProbackupTest, unittest.TestCase): gdb.set_breakpoint('create_data_directories') gdb.run_until_break() - # This PAGE backup is expected to be successfull try: self.delete_pb(backup_dir, 'node', full_id) self.assertEqual( @@ -483,6 +478,7 @@ class LockingTest(ProbackupTest, unittest.TestCase): repr(e.message), self.cmd)) # Clean after yourself + gdb.kill() self.del_test_dir(module_name, fname) def test_backup_directory_name(self): @@ -538,3 +534,7 @@ class LockingTest(ProbackupTest, unittest.TestCase): # Clean after yourself self.del_test_dir(module_name, fname) + +# TODO: +# test that concurrent validation and restore are not locking each other +# check that quick exclusive lock, when taking RO-lock, is really quick diff --git a/tests/merge.py b/tests/merge.py index d7dbae20..44652066 100644 --- a/tests/merge.py +++ b/tests/merge.py @@ -1475,7 +1475,7 @@ class MergeTest(ProbackupTest, unittest.TestCase): self.del_test_dir(module_name, fname) - @unittest.skip("skip") + # @unittest.skip("skip") def test_crash_after_opening_backup_control_2(self): """ check that crashing after opening backup_content.control @@ -1529,8 +1529,8 @@ class MergeTest(ProbackupTest, unittest.TestCase): gdb.set_breakpoint('write_backup_filelist') gdb.run_until_break() - gdb.set_breakpoint('sprintf') - gdb.continue_execution_until_break(1) +# gdb.set_breakpoint('sprintf') +# gdb.continue_execution_until_break(1) gdb._execute('signal SIGKILL') @@ -1567,7 +1567,7 @@ class MergeTest(ProbackupTest, unittest.TestCase): self.del_test_dir(module_name, fname) - @unittest.skip("skip") + # @unittest.skip("skip") def test_losing_file_after_failed_merge(self): """ check that crashing after opening backup_content.control @@ -1622,8 +1622,8 @@ class MergeTest(ProbackupTest, unittest.TestCase): gdb.set_breakpoint('write_backup_filelist') gdb.run_until_break() - gdb.set_breakpoint('sprintf') - gdb.continue_execution_until_break(20) +# gdb.set_breakpoint('sprintf') +# gdb.continue_execution_until_break(20) gdb._execute('signal SIGKILL') diff --git a/tests/page.py b/tests/page.py index 201f825e..323c0a6d 100644 --- a/tests/page.py +++ b/tests/page.py @@ -393,7 +393,7 @@ class PageTest(ProbackupTest, unittest.TestCase): pgbench.wait() # GET LOGICAL CONTENT FROM NODE - result = node.safe_psql("postgres", "select * from pgbench_accounts") + result = node.safe_psql("postgres", "select count(*) from pgbench_accounts") # PAGE BACKUP self.backup_node(backup_dir, 'node', node, backup_type='page') @@ -422,7 +422,7 @@ class PageTest(ProbackupTest, unittest.TestCase): restored_node.slow_start() result_new = restored_node.safe_psql( - "postgres", "select * from pgbench_accounts") + "postgres", "select count(*) from pgbench_accounts") # COMPARE RESTORED FILES self.assertEqual(result, result_new, 'data is lost') diff --git a/tests/ptrack.py b/tests/ptrack.py index 18425237..c45ecd6e 100644 --- a/tests/ptrack.py +++ b/tests/ptrack.py @@ -3031,7 +3031,7 @@ class PtrackTest(ProbackupTest, unittest.TestCase): pg_options={ 'max_wal_size': '32MB', 'archive_timeout': '10s', - 'checkpoint_timeout': '30s', + 'checkpoint_timeout': '5min', 'autovacuum': 'off'}) backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') @@ -3108,6 +3108,15 @@ class PtrackTest(ProbackupTest, unittest.TestCase): # Sync master and replica self.wait_until_replica_catch_with_master(master, replica) + if replica.major_version < 10: + replica.safe_psql( + "postgres", + "select pg_xlog_replay_pause()") + else: + replica.safe_psql( + "postgres", + "select pg_wal_replay_pause()") + self.backup_node( backup_dir, 'replica', replica, backup_type='ptrack', options=[ @@ -3130,8 +3139,16 @@ class PtrackTest(ProbackupTest, unittest.TestCase): if self.paranoia: self.compare_pgdata(pgdata, pgdata_restored) + self.set_auto_conf(node, {'port': node.port}) + + node.slow_start() + + node.safe_psql( + 'postgres', + 'select 1') + # Clean after yourself - self.del_test_dir(module_name, fname, [master, replica]) + self.del_test_dir(module_name, fname, [master, replica, node]) # @unittest.skip("skip") # @unittest.expectedFailure diff --git a/tests/replica.py b/tests/replica.py index 9a75a7aa..f664ca88 100644 --- a/tests/replica.py +++ b/tests/replica.py @@ -1073,6 +1073,87 @@ class ReplicaTest(ProbackupTest, unittest.TestCase): # Clean after yourself self.del_test_dir(module_name, fname) + # @unittest.skip("skip") + def test_start_stop_lsn_in_the_same_segno(self): + """ + """ + fname = self.id().split('.')[3] + backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') + master = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'master'), + set_replication=True, + initdb_params=['--data-checksums'], + pg_options={ + 'autovacuum': 'off', + 'checkpoint_timeout': '1h', + 'wal_level': 'replica', + 'shared_buffers': '128MB'}) + + if self.get_version(master) < self.version_to_num('9.6.0'): + self.del_test_dir(module_name, fname) + return unittest.skip( + 'Skipped because backup from replica is not supported in PG 9.5') + + self.init_pb(backup_dir) + self.add_instance(backup_dir, 'master', master) + master.slow_start() + + # freeze bgwriter to get rid of RUNNING XACTS records + bgwriter_pid = master.auxiliary_pids[ProcessType.BackgroundWriter][0] + gdb_checkpointer = self.gdb_attach(bgwriter_pid) + + self.backup_node(backup_dir, 'master', master, options=['--stream']) + + # Create replica + replica = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'replica')) + replica.cleanup() + self.restore_node(backup_dir, 'master', replica) + + # Settings for Replica + self.add_instance(backup_dir, 'replica', replica) + self.set_replica(master, replica, synchronous=True) + + replica.slow_start(replica=True) + + self.switch_wal_segment(master) + self.switch_wal_segment(master) + + master.safe_psql( + 'postgres', + 'CREATE TABLE t1 AS ' + 'SELECT i, repeat(md5(i::text),5006056) AS fat_attr ' + 'FROM generate_series(0,10) i') + + master.safe_psql( + 'postgres', + 'CHECKPOINT') + + self.wait_until_replica_catch_with_master(master, replica) + + sleep(60) + + self.backup_node( + backup_dir, 'replica', replica, + options=[ + '--archive-timeout=30', + '--log-level-console=LOG', + '--no-validate', + '--stream'], + return_id=False) + + self.backup_node( + backup_dir, 'replica', replica, + options=[ + '--archive-timeout=30', + '--log-level-console=LOG', + '--no-validate', + '--stream'], + return_id=False) + + # Clean after yourself + self.del_test_dir(module_name, fname) + @unittest.skip("skip") def test_replica_promote_1(self): """ @@ -1647,6 +1728,80 @@ class ReplicaTest(ProbackupTest, unittest.TestCase): # Clean after yourself self.del_test_dir(module_name, fname) + # @unittest.skip("skip") + def test_replica_via_basebackup(self): + """ + """ + fname = self.id().split('.')[3] + backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') + node = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node'), + set_replication=True, + initdb_params=['--data-checksums'], + pg_options={'autovacuum': 'off', 'hot_standby': 'on'}) + + self.init_pb(backup_dir) + self.add_instance(backup_dir, 'node', node) + self.set_archiving(backup_dir, 'node', node) + + node.slow_start() + + node.pgbench_init(scale=10) + + #FULL backup + full_id = self.backup_node(backup_dir, 'node', node) + + pgbench = node.pgbench( + options=['-T', '10', '-c', '1', '--no-vacuum']) + pgbench.wait() + + node.cleanup() + + self.restore_node( + backup_dir, 'node', node, + options=['--recovery-target=latest', '--recovery-target-action=promote']) + node.slow_start() + + # Timeline 2 + # Take stream page backup from instance in timeline2 + self.backup_node( + backup_dir, 'node', node, backup_type='full', + options=['--stream', '--log-level-file=verbose']) + + node.cleanup() + + # restore stream backup + self.restore_node(backup_dir, 'node', node) + + xlog_dir = 'pg_wal' + if self.get_version(node) < 100000: + xlog_dir = 'pg_xlog' + + filepath = os.path.join(node.data_dir, xlog_dir, "00000002.history") + self.assertTrue( + os.path.exists(filepath), + "History file do not exists: {0}".format(filepath)) + + node.slow_start() + + node_restored = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node_restored')) + node_restored.cleanup() + + pg_basebackup_path = self.get_bin_path('pg_basebackup') + + self.run_binary( + [ + pg_basebackup_path, '-p', str(node.port), '-h', 'localhost', + '-R', '-X', 'stream', '-D', node_restored.data_dir + ]) + + self.set_auto_conf(node_restored, {'port': node_restored.port}) + node_restored.slow_start(replica=True) + + # Clean after yourself + self.del_test_dir(module_name, fname) + # TODO: # null offset STOP LSN and latest record in previous segment is conrecord (manual only) # archiving from promoted delayed replica diff --git a/tests/restore.py b/tests/restore.py index 2a5fac6a..35fc6dce 100644 --- a/tests/restore.py +++ b/tests/restore.py @@ -51,8 +51,11 @@ class RestoreTest(ProbackupTest, unittest.TestCase): repr(self.output), self.cmd)) # 2 - Test that recovery.conf was created + # TODO update test if self.get_version(node) >= self.version_to_num('12.0'): - recovery_conf = os.path.join(node.data_dir, 'probackup_recovery.conf') + recovery_conf = os.path.join(node.data_dir, 'postgresql.auto.conf') + with open(recovery_conf, 'r') as f: + print(f.read()) else: recovery_conf = os.path.join(node.data_dir, 'recovery.conf') self.assertEqual(os.path.isfile(recovery_conf), True) @@ -1807,8 +1810,11 @@ class RestoreTest(ProbackupTest, unittest.TestCase): pgdata = self.pgdata_content(node.data_dir) + # TODO update test if self.get_version(node) >= self.version_to_num('12.0'): - recovery_conf = os.path.join(node.data_dir, 'probackup_recovery.conf') + recovery_conf = os.path.join(node.data_dir, 'postgresql.auto.conf') + with open(recovery_conf, 'r') as f: + print(f.read()) else: recovery_conf = os.path.join(node.data_dir, 'recovery.conf') @@ -1862,8 +1868,11 @@ class RestoreTest(ProbackupTest, unittest.TestCase): pgdata = self.pgdata_content(node.data_dir) + # TODO update test if self.get_version(node) >= self.version_to_num('12.0'): - recovery_conf = os.path.join(node.data_dir, 'probackup_recovery.conf') + recovery_conf = os.path.join(node.data_dir, 'postgresql.auto.conf') + with open(recovery_conf, 'r') as f: + print(f.read()) else: recovery_conf = os.path.join(node.data_dir, 'recovery.conf') @@ -1912,7 +1921,7 @@ class RestoreTest(ProbackupTest, unittest.TestCase): self.backup_node(backup_dir, 'node', node) if self.get_version(node) >= self.version_to_num('12.0'): - recovery_conf = os.path.join(node.data_dir, 'probackup_recovery.conf') + recovery_conf = os.path.join(node.data_dir, 'postgresql.auto.conf') else: recovery_conf = os.path.join(node.data_dir, 'recovery.conf') @@ -1924,23 +1933,35 @@ class RestoreTest(ProbackupTest, unittest.TestCase): # open(recovery_conf, 'rb').read()).hexdigest() with open(recovery_conf, 'r') as f: - content_1 = f.read() + content_1 = '' + while True: + line = f.readline() + + if not line: + break + if line.startswith("#"): + continue + content_1 += line - # restore node.cleanup() - self.restore_node(backup_dir, 'node', node, options=['--recovery-target=latest']) # hash_2 = hashlib.md5( # open(recovery_conf, 'rb').read()).hexdigest() with open(recovery_conf, 'r') as f: - content_2 = f.read() + content_2 = '' + while True: + line = f.readline() + + if not line: + break + if line.startswith("#"): + continue + content_2 += line self.assertEqual(content_1, content_2) - # self.assertEqual(hash_1, hash_2) - # Clean after yourself self.del_test_dir(module_name, fname) @@ -1965,8 +1986,11 @@ class RestoreTest(ProbackupTest, unittest.TestCase): # Take FULL self.backup_node(backup_dir, 'node', node) + # TODO update test if self.get_version(node) >= self.version_to_num('12.0'): - recovery_conf = os.path.join(node.data_dir, 'probackup_recovery.conf') + recovery_conf = os.path.join(node.data_dir, 'postgresql.auto.conf') + with open(recovery_conf, 'r') as f: + print(f.read()) else: recovery_conf = os.path.join(node.data_dir, 'recovery.conf') @@ -3093,6 +3117,7 @@ class RestoreTest(ProbackupTest, unittest.TestCase): "GRANT CONNECT ON DATABASE backupdb to backup; " "GRANT USAGE ON SCHEMA pg_catalog TO backup; " "GRANT SELECT ON TABLE pg_catalog.pg_proc TO backup; " + "GRANT SELECT ON TABLE pg_catalog.pg_extension TO backup; " "GRANT SELECT ON TABLE pg_catalog.pg_database TO backup; " # for partial restore, checkdb and ptrack "GRANT EXECUTE ON FUNCTION pg_catalog.nameeq(name, name) TO backup; " "GRANT EXECUTE ON FUNCTION pg_catalog.textout(text) TO backup; " @@ -3263,8 +3288,11 @@ class RestoreTest(ProbackupTest, unittest.TestCase): self.set_archiving(backup_dir, 'node', node) node.slow_start() + # TODO update test if self.get_version(node) >= self.version_to_num('12.0'): - recovery_conf = os.path.join(node.data_dir, 'probackup_recovery.conf') + recovery_conf = os.path.join(node.data_dir, 'postgresql.auto.conf') + with open(recovery_conf, 'r') as f: + print(f.read()) else: recovery_conf = os.path.join(node.data_dir, 'recovery.conf') @@ -3351,8 +3379,11 @@ class RestoreTest(ProbackupTest, unittest.TestCase): os.path.isfile(standby_signal), "File '{0}' do not exists".format(standby_signal)) + # TODO update test if self.get_version(node) >= self.version_to_num('12.0'): - recovery_conf = os.path.join(replica.data_dir, 'probackup_recovery.conf') + recovery_conf = os.path.join(replica.data_dir, 'postgresql.auto.conf') + with open(recovery_conf, 'r') as f: + print(f.read()) else: recovery_conf = os.path.join(replica.data_dir, 'recovery.conf') @@ -3461,7 +3492,7 @@ class RestoreTest(ProbackupTest, unittest.TestCase): self.set_auto_conf( node_restored_1, - {'port': node_restored_1.port, 'hot_standby': 'on'}) + {'port': node_restored_1.port, 'hot_standby': 'off'}) node_restored_1.slow_start() @@ -3478,3 +3509,228 @@ class RestoreTest(ProbackupTest, unittest.TestCase): # Clean after yourself self.del_test_dir(module_name, fname) + + def test_pg_12_probackup_recovery_conf_compatibility(self): + """ + https://github.com/postgrespro/pg_probackup/issues/249 + + pg_probackup version must be 12 or greater + """ + + if self.old_probackup_version: + if self.version_to_num(self.old_probackup_version) >= self.version_to_num('2.4.5'): + return unittest.skip('You need pg_probackup < 2.4.5 for this test') + + if self.pg_config_version < self.version_to_num('12.0'): + return unittest.skip('You need PostgreSQL >= 12 for this test') + + fname = self.id().split('.')[3] + backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') + node = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node'), + initdb_params=['--data-checksums'], + pg_options={'autovacuum': 'off'}) + + self.init_pb(backup_dir) + self.add_instance(backup_dir, 'node', node) + self.set_archiving(backup_dir, 'node', node) + node.slow_start() + + # FULL backup + self.backup_node(backup_dir, 'node', node, old_binary=True) + + node.pgbench_init(scale=5) + + node.safe_psql( + 'postgres', + 'CREATE TABLE t1 as SELECT * from pgbench_accounts where aid > 200000 and aid < 450000') + + time = node.safe_psql( + 'SELECT current_timestamp(0)::timestamptz;').decode('utf-8').rstrip() + + node.safe_psql( + 'postgres', + 'DELETE from pgbench_accounts where aid > 200000 and aid < 450000') + + node.cleanup() + + self.restore_node( + backup_dir, 'node',node, + options=[ + "--recovery-target-time={0}".format(time), + "--recovery-target-action=promote"], + old_binary=True) + + node.slow_start() + + self.backup_node(backup_dir, 'node', node, old_binary=True) + + node.pgbench_init(scale=5) + + xid = node.safe_psql( + 'SELECT txid_current()').decode('utf-8').rstrip() + node.pgbench_init(scale=1) + + node.cleanup() + + self.restore_node( + backup_dir, 'node',node, + options=[ + "--recovery-target-xid={0}".format(xid), + "--recovery-target-action=promote"]) + + node.slow_start() + + # Clean after yourself + self.del_test_dir(module_name, fname) + + def test_drop_postgresql_auto_conf(self): + """ + https://github.com/postgrespro/pg_probackup/issues/249 + + pg_probackup version must be 12 or greater + """ + + if self.pg_config_version < self.version_to_num('12.0'): + return unittest.skip('You need PostgreSQL >= 12 for this test') + + fname = self.id().split('.')[3] + backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') + node = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node'), + initdb_params=['--data-checksums'], + pg_options={'autovacuum': 'off'}) + + self.init_pb(backup_dir) + self.add_instance(backup_dir, 'node', node) + self.set_archiving(backup_dir, 'node', node) + node.slow_start() + + # FULL backup + self.backup_node(backup_dir, 'node', node) + + # drop postgresql.auto.conf + auto_path = os.path.join(node.data_dir, "postgresql.auto.conf") + os.remove(auto_path) + + self.backup_node(backup_dir, 'node', node, backup_type='page') + + node.cleanup() + + self.restore_node( + backup_dir, 'node',node, + options=[ + "--recovery-target=latest", + "--recovery-target-action=promote"]) + + node.slow_start() + + self.assertTrue(os.path.exists(auto_path)) + + # Clean after yourself + self.del_test_dir(module_name, fname) + + def test_truncate_postgresql_auto_conf(self): + """ + https://github.com/postgrespro/pg_probackup/issues/249 + + pg_probackup version must be 12 or greater + """ + + if self.pg_config_version < self.version_to_num('12.0'): + return unittest.skip('You need PostgreSQL >= 12 for this test') + + fname = self.id().split('.')[3] + backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') + node = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node'), + initdb_params=['--data-checksums'], + pg_options={'autovacuum': 'off'}) + + self.init_pb(backup_dir) + self.add_instance(backup_dir, 'node', node) + self.set_archiving(backup_dir, 'node', node) + node.slow_start() + + # FULL backup + self.backup_node(backup_dir, 'node', node) + + # truncate postgresql.auto.conf + auto_path = os.path.join(node.data_dir, "postgresql.auto.conf") + with open(auto_path, "w+") as f: + f.truncate() + + self.backup_node(backup_dir, 'node', node, backup_type='page') + + node.cleanup() + + self.restore_node( + backup_dir, 'node',node, + options=[ + "--recovery-target=latest", + "--recovery-target-action=promote"]) + node.slow_start() + + self.assertTrue(os.path.exists(auto_path)) + + # Clean after yourself + self.del_test_dir(module_name, fname) + + # @unittest.skip("skip") + def test_concurrent_restore(self): + """""" + fname = self.id().split('.')[3] + backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') + node = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node'), + set_replication=True, + initdb_params=['--data-checksums']) + + self.init_pb(backup_dir) + self.add_instance(backup_dir, 'node', node) + self.set_archiving(backup_dir, 'node', node) + node.slow_start() + + node.pgbench_init(scale=1) + + # FULL backup + self.backup_node( + backup_dir, 'node', node, + options=['--stream', '--compress']) + + pgbench = node.pgbench(options=['-T', '7', '-c', '1', '--no-vacuum']) + pgbench.wait() + + # DELTA backup + self.backup_node( + backup_dir, 'node', node, backup_type='delta', + options=['--stream', '--compress', '--no-validate']) + + pgdata1 = self.pgdata_content(node.data_dir) + + node_restored = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node_restored')) + + node.cleanup() + node_restored.cleanup() + + gdb = self.restore_node( + backup_dir, 'node', node, options=['--no-validate'], gdb=True) + + gdb.set_breakpoint('restore_data_file') + gdb.run_until_break() + + self.restore_node( + backup_dir, 'node', node_restored, options=['--no-validate']) + + gdb.remove_all_breakpoints() + gdb.continue_execution_until_exit() + + pgdata2 = self.pgdata_content(node.data_dir) + pgdata3 = self.pgdata_content(node_restored.data_dir) + + self.compare_pgdata(pgdata1, pgdata2) + self.compare_pgdata(pgdata2, pgdata3) + + # Clean after yourself + self.del_test_dir(module_name, fname) diff --git a/tests/retention.py b/tests/retention.py index 6ab796b4..6dc8536c 100644 --- a/tests/retention.py +++ b/tests/retention.py @@ -41,7 +41,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): output_before = self.show_archive(backup_dir, 'node', tli=1) # Purge backups - log = self.delete_expired( + self.delete_expired( backup_dir, 'node', options=['--expired', '--wal']) self.assertEqual(len(self.show_pb(backup_dir, 'node')), 2) @@ -142,13 +142,13 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node.slow_start() # take FULL BACKUP - backup_id_1 = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) # Take second FULL BACKUP - backup_id_2 = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) # Take third FULL BACKUP - backup_id_3 = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) backups = os.path.join(backup_dir, 'backups', 'node') for backup in os.listdir(backups): @@ -189,7 +189,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node.slow_start() # take FULL BACKUPs - backup_id_1 = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) backup_id_2 = self.backup_node(backup_dir, 'node', node) @@ -444,8 +444,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): # PAGEa1 OK # FULLb OK # FULLa ERROR - page_id_b2 = self.backup_node( - backup_dir, 'node', node, backup_type='page') + self.backup_node(backup_dir, 'node', node, backup_type='page') # Change PAGEa2 and FULLa status to OK self.change_backup_status(backup_dir, 'node', page_id_a2, 'OK') @@ -632,7 +631,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node.pgbench_init(scale=5) # Take FULL BACKUPs - backup_id_a = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) pgbench = node.pgbench(options=['-t', '20', '-c', '1']) pgbench.wait() @@ -663,13 +662,13 @@ class RetentionTest(ProbackupTest, unittest.TestCase): # PAGEa1 ERROR # FULLb OK # FULLa OK - page_id_b1 = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='page') pgbench = node.pgbench(options=['-t', '20', '-c', '1']) pgbench.wait() - page_id_b2 = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='page') pgbench = node.pgbench(options=['-t', '20', '-c', '1']) @@ -711,7 +710,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): conf.write("recovery_time='{:%Y-%m-%d %H:%M:%S}'\n".format( datetime.now() - timedelta(days=3))) - output = self.delete_expired( + self.delete_expired( backup_dir, 'node', options=['--retention-window=1', '--expired', '--merge-expired']) @@ -1305,26 +1304,26 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node.pgbench_init(scale=3) # Chain A - backup_id_a = self.backup_node(backup_dir, 'node', node) - page_id_a1 = self.backup_node( + self.backup_node(backup_dir, 'node', node) + self.backup_node( backup_dir, 'node', node, backup_type='page') - page_id_a2 = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='page') # Chain B - backup_id_b = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) pgbench = node.pgbench(options=['-T', '10', '-c', '2']) pgbench.wait() - page_id_b1 = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='delta') pgbench = node.pgbench(options=['-T', '10', '-c', '2']) pgbench.wait() - page_id_b2 = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='page') pgbench = node.pgbench(options=['-T', '10', '-c', '2']) @@ -1347,7 +1346,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): conf.write("recovery_time='{:%Y-%m-%d %H:%M:%S}'\n".format( datetime.now() - timedelta(days=3))) - output = self.delete_expired( + self.delete_expired( backup_dir, 'node', options=[ '--retention-window=1', '--expired', @@ -1391,26 +1390,26 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node.pgbench_init(scale=3) # Chain A - backup_id_a = self.backup_node(backup_dir, 'node', node) - page_id_a1 = self.backup_node( + self.backup_node(backup_dir, 'node', node) + self.backup_node( backup_dir, 'node', node, backup_type='page') - page_id_a2 = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='page') # Chain B - backup_id_b = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) - page_id_b1 = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='delta') - page_id_b2 = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='page') page_id_b3 = self.backup_node( backup_dir, 'node', node, backup_type='delta') - pgdata = self.pgdata_content(node.data_dir) + self.pgdata_content(node.data_dir) # Purge backups backups = os.path.join(backup_dir, 'backups', 'node') @@ -1483,15 +1482,15 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node.slow_start() # Take FULL BACKUPs - backup_id_a1 = self.backup_node(backup_dir, 'node', node) - gdb = self.backup_node( - backup_dir, 'node', node, backup_type='page', gdb=True) + self.backup_node(backup_dir, 'node', node) + self.backup_node( + backup_dir, 'node', node, backup_type='page') - page_id_a3 = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='page') # Change FULLb backup status to ERROR - self.change_backup_status(backup_dir, 'node', backup_id_b, 'ERROR') + # self.change_backup_status(backup_dir, 'node', backup_id_b, 'ERROR') # Clean after yourself self.del_test_dir(module_name, fname) @@ -1516,7 +1515,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node.slow_start() # Take FULL BACKUP - full_id = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) # Take PAGE BACKUP gdb = self.backup_node( @@ -1528,15 +1527,15 @@ class RetentionTest(ProbackupTest, unittest.TestCase): gdb._execute('signal SIGINT') gdb.continue_execution_until_error() - page_id = self.show_pb(backup_dir, 'node')[1]['id'] + self.show_pb(backup_dir, 'node')[1]['id'] # Take DELTA backup - delta_id = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='delta', options=['--retention-window=2', '--delete-expired']) # Take FULL BACKUP - full2_id = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) self.assertEqual(len(self.show_pb(backup_dir, 'node')), 4) @@ -1563,7 +1562,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node.slow_start() # Take FULL BACKUP - full_id = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) # Take PAGE BACKUP gdb = self.backup_node( @@ -1574,7 +1573,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): gdb._execute('signal SIGKILL') gdb.continue_execution_until_error() - page_id = self.show_pb(backup_dir, 'node')[1]['id'] + self.show_pb(backup_dir, 'node')[1]['id'] if self.get_version(node) < 90600: node.safe_psql( @@ -1582,7 +1581,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): 'SELECT pg_catalog.pg_stop_backup()') # Take DELTA backup - delta_id = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='delta', options=['--retention-window=2', '--delete-expired']) @@ -1630,7 +1629,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): self.backup_node(backup_dir, 'node', node, backup_type="page") # Purge backups - log = self.delete_expired( + self.delete_expired( backup_dir, 'node', options=['--expired', '--wal']) self.assertEqual(len(self.show_pb(backup_dir, 'node')), 2) @@ -1639,7 +1638,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): # Clean after yourself self.del_test_dir(module_name, fname) - def test_retention_redundancy_overlapping_chains(self): + def test_retention_redundancy_overlapping_chains_1(self): """""" fname = self.id().split('.')[3] node = self.make_simple_node( @@ -1678,7 +1677,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): self.backup_node(backup_dir, 'node', node, backup_type="page") # Purge backups - log = self.delete_expired( + self.delete_expired( backup_dir, 'node', options=['--expired', '--wal']) self.assertEqual(len(self.show_pb(backup_dir, 'node')), 2) @@ -1869,7 +1868,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): # FULL node.pgbench_init(scale=1) - B1 = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) # PAGE node.pgbench_init(scale=1) @@ -1885,12 +1884,12 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node.pgbench_init(scale=1) - B3 = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='page') node.pgbench_init(scale=1) - B4 = self.backup_node( + self.backup_node( backup_dir, 'node', node, backup_type='page') # Timeline 2 @@ -1940,11 +1939,11 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node_restored.slow_start() node_restored.pgbench_init(scale=1) - B5 = self.backup_node( + self.backup_node( backup_dir, 'node', node_restored, data_dir=node_restored.data_dir) node.pgbench_init(scale=1) - B6 = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) lsn = self.show_archive(backup_dir, 'node', tli=2)['switchpoint'] @@ -2007,9 +2006,9 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node.pgbench_init(scale=5) # B2 FULL on TLI1 - B2 = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) node.pgbench_init(scale=4) - B3 = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) node.pgbench_init(scale=4) self.delete_pb(backup_dir, 'node', options=['--delete-wal']) @@ -2023,7 +2022,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): backup_dir, 'node', node_tli2, options=[ '--recovery-target-xid={0}'.format(target_xid), - '--recovery-target-timeline=1'.format(target_xid), + '--recovery-target-timeline=1', '--recovery-target-action=promote']) self.assertIn( @@ -2039,11 +2038,11 @@ class RetentionTest(ProbackupTest, unittest.TestCase): "select txid_current()").decode('utf-8').rstrip() node_tli2.pgbench_init(scale=1) - B4 = self.backup_node( + self.backup_node( backup_dir, 'node', node_tli2, data_dir=node_tli2.data_dir) node_tli2.pgbench_init(scale=3) - B5 = self.backup_node( + self.backup_node( backup_dir, 'node', node_tli2, data_dir=node_tli2.data_dir) node_tli2.pgbench_init(scale=1) node_tli2.cleanup() @@ -2086,7 +2085,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node_tli4.pgbench_init(scale=5) - B6 = self.backup_node( + self.backup_node( backup_dir, 'node', node_tli4, data_dir=node_tli4.data_dir) node_tli4.pgbench_init(scale=5) node_tli4.cleanup() @@ -2232,7 +2231,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): # B2 FULL on TLI1 B2 = self.backup_node(backup_dir, 'node', node) node.pgbench_init(scale=4) - B3 = self.backup_node(backup_dir, 'node', node) + self.backup_node(backup_dir, 'node', node) node.pgbench_init(scale=4) # TLI 2 @@ -2244,7 +2243,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): backup_dir, 'node', node_tli2, options=[ '--recovery-target-xid={0}'.format(target_xid), - '--recovery-target-timeline=1'.format(target_xid), + '--recovery-target-timeline=1', '--recovery-target-action=promote']) self.assertIn( @@ -2264,7 +2263,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): backup_dir, 'node', node_tli2, data_dir=node_tli2.data_dir) node_tli2.pgbench_init(scale=3) - B5 = self.backup_node( + self.backup_node( backup_dir, 'node', node_tli2, data_dir=node_tli2.data_dir) node_tli2.pgbench_init(scale=1) node_tli2.cleanup() @@ -2307,7 +2306,7 @@ class RetentionTest(ProbackupTest, unittest.TestCase): node_tli4.pgbench_init(scale=5) - B6 = self.backup_node( + self.backup_node( backup_dir, 'node', node_tli4, data_dir=node_tli4.data_dir) node_tli4.pgbench_init(scale=5) node_tli4.cleanup() diff --git a/tests/set_backup.py b/tests/set_backup.py index daba9a21..02ce007b 100644 --- a/tests/set_backup.py +++ b/tests/set_backup.py @@ -473,4 +473,37 @@ class SetBackupTest(ProbackupTest, unittest.TestCase): self.assertEqual(backup_meta['note'], note) # Clean after yourself - self.del_test_dir(module_name, fname) \ No newline at end of file + self.del_test_dir(module_name, fname) + + # @unittest.skip("skip") + def test_add_big_note_1(self): + """""" + fname = self.id().split('.')[3] + node = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node'), + set_replication=True, + initdb_params=['--data-checksums']) + + backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') + self.init_pb(backup_dir) + self.add_instance(backup_dir, 'node', node) + node.slow_start() + + note = node.safe_psql( + "postgres", + "SELECT repeat('q', 1024)").decode('utf-8').rstrip() + + # FULL + backup_id = self.backup_node(backup_dir, 'node', node, options=['--stream']) + + self.set_backup( + backup_dir, 'node', backup_id, + options=['--note={0}'.format(note)]) + + backup_meta = self.show_pb(backup_dir, 'node', backup_id) + + print(backup_meta) + self.assertEqual(backup_meta['note'], note) + + # Clean after yourself + self.del_test_dir(module_name, fname) diff --git a/tests/show.py b/tests/show.py index 92ef392d..ab418792 100644 --- a/tests/show.py +++ b/tests/show.py @@ -6,7 +6,7 @@ from .helpers.ptrack_helpers import ProbackupTest, ProbackupException module_name = 'show' -class OptionTest(ProbackupTest, unittest.TestCase): +class ShowTest(ProbackupTest, unittest.TestCase): # @unittest.skip("skip") # @unittest.expectedFailure diff --git a/tests/time_stamp.py b/tests/time_stamp.py index 8abd55a2..bf6e91e0 100644 --- a/tests/time_stamp.py +++ b/tests/time_stamp.py @@ -1,11 +1,13 @@ import os import unittest from .helpers.ptrack_helpers import ProbackupTest, ProbackupException +import subprocess +from time import sleep module_name = 'time_stamp' -class CheckTimeStamp(ProbackupTest, unittest.TestCase): +class TimeStamp(ProbackupTest, unittest.TestCase): def test_start_time_format(self): """Test backup ID changing after start-time editing in backup.control. @@ -47,8 +49,14 @@ class CheckTimeStamp(ProbackupTest, unittest.TestCase): show_backup = show_backup + self.show_pb(backup_dir, 'node') i += 1 + print(show_backup[1]['id']) + print(show_backup[2]['id']) + self.assertTrue(show_backup[1]['id'] == show_backup[2]['id'], "ERROR: Localtime format using instead of UTC") + output = self.show_pb(backup_dir, as_json=False, as_text=True) + self.assertNotIn("backup ID in control file", output) + node.stop() # Clean after yourself self.del_test_dir(module_name, fname) @@ -72,3 +80,180 @@ class CheckTimeStamp(ProbackupTest, unittest.TestCase): # Clean after yourself self.del_test_dir(module_name, fname) + + def test_handling_of_TZ_env_variable(self): + """Issue #112""" + fname = self.id().split('.')[3] + node = self.make_simple_node( + base_dir="{0}/{1}/node".format(module_name, fname), + set_replication=True, + initdb_params=['--data-checksums']) + + backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') + self.init_pb(backup_dir) + self.add_instance(backup_dir, 'node', node) + node.start() + + my_env = os.environ.copy() + my_env["TZ"] = "America/Detroit" + + self.backup_node( + backup_dir, 'node', node, options=['--stream', '-j 2'], env=my_env) + + output = self.show_pb(backup_dir, 'node', as_json=False, as_text=True, env=my_env) + + self.assertNotIn("backup ID in control file", output) + + # Clean after yourself + self.del_test_dir(module_name, fname) + + @unittest.skip("skip") + # @unittest.expectedFailure + def test_dst_timezone_handling(self): + """for manual testing""" + fname = self.id().split('.')[3] + backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') + node = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node'), + initdb_params=['--data-checksums'], + pg_options={'autovacuum': 'off'}) + + self.init_pb(backup_dir) + self.add_instance(backup_dir, 'node', node) + self.set_archiving(backup_dir, 'node', node) + node.slow_start() + + print(subprocess.Popen( + ['sudo', 'timedatectl', 'set-timezone', 'America/Detroit'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate()) + + subprocess.Popen( + ['sudo', 'timedatectl', 'set-ntp', 'false'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + subprocess.Popen( + ['sudo', 'timedatectl', 'set-time', '2020-05-25 12:00:00'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + # FULL + output = self.backup_node(backup_dir, 'node', node, return_id=False) + self.assertNotIn("backup ID in control file", output) + + # move to dst + subprocess.Popen( + ['sudo', 'timedatectl', 'set-time', '2020-10-25 12:00:00'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + # DELTA + output = self.backup_node( + backup_dir, 'node', node, backup_type='delta', return_id=False) + self.assertNotIn("backup ID in control file", output) + + subprocess.Popen( + ['sudo', 'timedatectl', 'set-time', '2020-12-01 12:00:00'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + # DELTA + self.backup_node(backup_dir, 'node', node, backup_type='delta') + + output = self.show_pb(backup_dir, as_json=False, as_text=True) + self.assertNotIn("backup ID in control file", output) + + subprocess.Popen( + ['sudo', 'timedatectl', 'set-ntp', 'true'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + sleep(10) + + self.backup_node(backup_dir, 'node', node, backup_type='delta') + + output = self.show_pb(backup_dir, as_json=False, as_text=True) + self.assertNotIn("backup ID in control file", output) + + subprocess.Popen( + ['sudo', 'timedatectl', 'set-timezone', 'US/Moscow'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + # Clean after yourself + self.del_test_dir(module_name, fname) + + @unittest.skip("skip") + def test_dst_timezone_handling_backward_compatibilty(self): + """for manual testing""" + fname = self.id().split('.')[3] + backup_dir = os.path.join(self.tmp_path, module_name, fname, 'backup') + node = self.make_simple_node( + base_dir=os.path.join(module_name, fname, 'node'), + initdb_params=['--data-checksums'], + pg_options={'autovacuum': 'off'}) + + self.init_pb(backup_dir) + self.add_instance(backup_dir, 'node', node) + self.set_archiving(backup_dir, 'node', node) + node.slow_start() + + subprocess.Popen( + ['sudo', 'timedatectl', 'set-timezone', 'America/Detroit'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + subprocess.Popen( + ['sudo', 'timedatectl', 'set-ntp', 'false'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + subprocess.Popen( + ['sudo', 'timedatectl', 'set-time', '2020-05-25 12:00:00'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + # FULL + self.backup_node(backup_dir, 'node', node, old_binary=True, return_id=False) + + # move to dst + subprocess.Popen( + ['sudo', 'timedatectl', 'set-time', '2020-10-25 12:00:00'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + # DELTA + output = self.backup_node( + backup_dir, 'node', node, backup_type='delta', old_binary=True, return_id=False) + + subprocess.Popen( + ['sudo', 'timedatectl', 'set-time', '2020-12-01 12:00:00'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + # DELTA + self.backup_node(backup_dir, 'node', node, backup_type='delta') + + output = self.show_pb(backup_dir, as_json=False, as_text=True) + self.assertNotIn("backup ID in control file", output) + + subprocess.Popen( + ['sudo', 'timedatectl', 'set-ntp', 'true'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + sleep(10) + + self.backup_node(backup_dir, 'node', node, backup_type='delta') + + output = self.show_pb(backup_dir, as_json=False, as_text=True) + self.assertNotIn("backup ID in control file", output) + + subprocess.Popen( + ['sudo', 'timedatectl', 'set-timezone', 'US/Moscow'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + + # Clean after yourself + self.del_test_dir(module_name, fname)