diff --git a/doc/xml/release.xml b/doc/xml/release.xml index ae39844dd..837f3957e 100644 --- a/doc/xml/release.xml +++ b/doc/xml/release.xml @@ -14,6 +14,18 @@ + + + + + + +

Auto-select backup set on restore when time target is specified.

+ +

Auto-selection is performed only when --set is not specified. If a backup set for the given target time cannot not be found, the latest (default) backup set will be used.

+
+
+ diff --git a/doc/xml/user-guide.xml b/doc/xml/user-guide.xml index 2cd7d2563..db4103698 100644 --- a/doc/xml/user-guide.xml +++ b/doc/xml/user-guide.xml @@ -1944,18 +1944,27 @@ -

Now take a new backup and attempt recovery from the new backup.

+

Now take a new backup and attempt recovery from the new backup by specifying the {[dash]}-set option. The info command can be used to find the new backup label.

- Perform a backup then attempt recovery from that backup + Perform a backup and get backup info + + + {[project-exe]} {[dash]}-stanza={[postgres-cluster-demo]} {[dash]}-type=incr backup + {[cmd-backup-last]} - - {[project-exe]} {[dash]}-stanza={[postgres-cluster-demo]} {[dash]}-type=incr backup + + {[project-exe]} info + {[backup-last]} + + + + Attempt recovery from the specified backup {[pg-cluster-stop]} @@ -1963,7 +1972,8 @@ {[project-exe]} {[dash]}-stanza={[postgres-cluster-demo]} {[dash]}-delta - {[dash]}-type=time "{[dash]}-target={[time-recovery-timestamp]}" --target-action=promote restore + {[dash]}-set={[backup-last]} + {[dash]}-type=time "{[dash]}-target={[time-recovery-timestamp]}" {[dash]}-target-action=promote restore @@ -1995,21 +2005,10 @@ -

Using an earlier backup will allow to play forward to the correct time. The info command can be used to find the next to last backup.

+

The default behavior for time-based restore, if the {[dash]}-set option is not specified, is to attempt to discover an earlier backup to play forward from. If a backup set cannot be found, then restore will default to the latest backup which, as shown earlier, may not give the desired result.

- Get backup info for the {[postgres-cluster-demo]} cluster - - - {[project-exe]} info - {[backup-last]} - - - -

The default behavior for restore is to use the last backup but an earlier backup can be specified with the {[dash]}-set option.

- - - Stop <postgres/>, restore from the selected backup, and start <postgres/> + Stop <postgres/>, restore from auto-selected backup, and start <postgres/> {[pg-cluster-stop]} @@ -2019,7 +2018,7 @@ {[project-exe]} {[dash]}-stanza={[postgres-cluster-demo]} {[dash]}-delta {[dash]}-type=time "{[dash]}-target={[time-recovery-timestamp]}" - {[dash]}-set={[backup-last]} --target-action=promote restore + {[dash]}-target-action=promote restore diff --git a/src/command/restore/restore.c b/src/command/restore/restore.c index ba8a4ff7a..b3ad93369 100644 --- a/src/command/restore/restore.c +++ b/src/command/restore/restore.c @@ -59,6 +59,8 @@ Recovery constants STRING_STATIC(RECOVERY_TYPE_PRESERVE_STR, RECOVERY_TYPE_PRESERVE); #define RECOVERY_TYPE_STANDBY "standby" STRING_STATIC(RECOVERY_TYPE_STANDBY_STR, RECOVERY_TYPE_STANDBY); +#define RECOVERY_TYPE_TIME "time" + STRING_STATIC(RECOVERY_TYPE_TIME_STR, RECOVERY_TYPE_TIME); /*********************************************************************************************************************************** Validate restore path @@ -106,6 +108,95 @@ restorePathValidate(void) FUNCTION_LOG_RETURN_VOID(); } +/*********************************************************************************************************************************** +Get epoch time from formatted string +***********************************************************************************************************************************/ +static time_t +getEpoch(const String *targetTime) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(STRING, targetTime); + FUNCTION_LOG_END(); + + ASSERT(targetTime != NULL); + + time_t result = 0; + + MEM_CONTEXT_TEMP_BEGIN() + { + // Build the regex to accept formats: YYYY-MM-DD HH:MM:SS with optional msec (up to 6 digits and separated from minutes by + // a comma or period), optional timezone offset +/- HH or HHMM or HH:MM, where offset boundaries are UTC-12 to UTC+14 + const String *expression = STRDEF( + "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}((\\,|\\.)[0-9]{1,6})?((\\+|\\-)[0-9]{2}(:?)([0-9]{2})?)?$"); + + RegExp *regExp = regExpNew(expression); + + // If the target-recovery time matches the regular expression then validate it + if (regExpMatch(regExp, targetTime)) + { + // Strip off the date and time and put the remainder into another string + String *datetime = strSubN(targetTime, 0, 19); + + int dtYear = cvtZToInt(strPtr(strSubN(datetime, 0, 4))); + int dtMonth = cvtZToInt(strPtr(strSubN(datetime, 5, 2))); + int dtDay = cvtZToInt(strPtr(strSubN(datetime, 8, 2))); + int dtHour = cvtZToInt(strPtr(strSubN(datetime, 11, 2))); + int dtMinute = cvtZToInt(strPtr(strSubN(datetime, 14, 2))); + int dtSecond = cvtZToInt(strPtr(strSubN(datetime, 17, 2))); + + // Confirm date and time parts are valid + datePartsValid(dtYear, dtMonth, dtDay); + timePartsValid(dtHour, dtMinute, dtSecond); + + String *timeTargetZone = strSub(targetTime, 19); + + // Find the + or - indicating a timezone offset was provided (there may be milliseconds before the timezone, so need to + // skip). If a timezone offset was not provided, then local time is assumed. + int idxSign = strChr(timeTargetZone, '+'); + + if (idxSign == -1) + idxSign = strChr(timeTargetZone, '-'); + + if (idxSign != -1) + { + String *timezoneOffset = strSub(timeTargetZone, (size_t)idxSign); + + // Include the sign with the hour + int tzHour = cvtZToInt(strPtr(strSubN(timezoneOffset, 0, 3))); + int tzMinute = 0; + + // If minutes are included in timezone offset then extract the minutes based on whether a colon separates them from + // the hour + if (strSize(timezoneOffset) > 3) + tzMinute = cvtZToInt(strPtr(strSubN(timezoneOffset, 3 + (strChr(timezoneOffset, ':') == -1 ? 0 : 1), 2))); + + result = epochFromParts(dtYear, dtMonth, dtDay, dtHour, dtMinute, dtSecond, tzOffsetSeconds(tzHour, tzMinute)); + } + // If there is no timezone offset, then assume it is local time + else + { + // Set tm_isdst to -1 to force mktime to consider if DST. For example, if system time is America/New_York then + // 2019-09-14 20:02:49 was a time in DST so the Epoch value should be 1568505769 (and not 1568509369 which would be + // 2019-09-14 21:02:49 - an hour too late) + result = mktime( + &(struct tm){.tm_sec = dtSecond, .tm_min = dtMinute, .tm_hour = dtHour, .tm_mday = dtDay, .tm_mon = dtMonth - 1, + .tm_year = dtYear - 1900, .tm_isdst = -1}); + } + } + else + { + LOG_WARN_FMT( + "automatic backup set selection cannot be performed with provided time '%s', latest backup set will be used" + "\nHINT: time format must be YYYY-MM-DD HH:MM:SS with optional msec and optional timezone (+/- HH or HHMM or HH:MM)" + " - if timezone is ommitted, local time is assumed (for UTC use +00)", + strPtr(targetTime)); + } + } + MEM_CONTEXT_TEMP_END(); + + FUNCTION_LOG_RETURN(TIME, result); +} + /*********************************************************************************************************************************** Get the backup set to restore ***********************************************************************************************************************************/ @@ -130,7 +221,42 @@ restoreBackupSet(InfoBackup *infoBackup) if (infoBackupDataTotal(infoBackup) == 0) THROW(BackupSetInvalidError, "no backup sets to restore"); - backupSet = infoBackupData(infoBackup, infoBackupDataTotal(infoBackup) - 1).backupLabel; + time_t timeTargetEpoch = 0; + + // If the recovery type is time, attempt to determine the backup set + if (strEq(cfgOptionStr(cfgOptType), RECOVERY_TYPE_TIME_STR)) + { + timeTargetEpoch = getEpoch(cfgOptionStr(cfgOptTarget)); + + // Try to find the newest backup set with a stop time before the target recovery time + if (timeTargetEpoch != 0) + { + // Search current backups from newest to oldest + for (unsigned int keyIdx = infoBackupDataTotal(infoBackup) - 1; (int)keyIdx >= 0; keyIdx--) + { + // Get the backup data + InfoBackupData backupData = infoBackupData(infoBackup, keyIdx); + + // If the end of the backup is before the target time, then select this backup + if (backupData.backupTimestampStop < timeTargetEpoch) + { + backupSet = backupData.backupLabel; + break; + } + } + + if (backupSet == NULL) + { + LOG_WARN_FMT( + "unable to find backup set with stop time less than '%s', latest backup set will be used", + strPtr(cfgOptionStr(cfgOptTarget))); + } + } + } + + // If a backup set was not found or the recovery type was not time, then use the latest backup + if (backupSet == NULL) + backupSet = infoBackupData(infoBackup, infoBackupDataTotal(infoBackup) - 1).backupLabel; } // Otherwise check to make sure the specified backup set is valid else diff --git a/test/define.yaml b/test/define.yaml index 674fc24a8..28a2d0dac 100644 --- a/test/define.yaml +++ b/test/define.yaml @@ -623,7 +623,7 @@ unit: # ---------------------------------------------------------------------------------------------------------------------------- - name: restore - total: 11 + total: 12 containerReq: true binReq: true diff --git a/test/expect/real-all-001.log b/test/expect/real-all-001.log index 9de88d8a5..6ca78b82d 100644 --- a/test/expect/real-all-001.log +++ b/test/expect/real-all-001.log @@ -202,8 +202,8 @@ restore_command = '[BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf recovery_target_xid = '[XID-TARGET-1]' recovery_target_action = 'promote' -restore delta, backup '[BACKUP-FULL-1]', type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) -> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --set=[BACKUP-FULL-1] --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore +restore delta, type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) +> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore ------------------------------------------------------------------------------------------------------------------------------------ + supplemental file: [TEST_PATH]/db-master/db/base/recovery.conf diff --git a/test/expect/real-all-002.log b/test/expect/real-all-002.log index bfc7c98ab..768d4e2cd 100644 --- a/test/expect/real-all-002.log +++ b/test/expect/real-all-002.log @@ -251,8 +251,8 @@ restore_command = '[BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf recovery_target = 'immediate' recovery_target_action = 'promote' -restore delta, backup '[BACKUP-FULL-1]', type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) -> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --set=[BACKUP-FULL-1] --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore +restore delta, type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) +> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore ------------------------------------------------------------------------------------------------------------------------------------ + supplemental file: [TEST_PATH]/db-master/db/base/recovery.conf diff --git a/test/expect/real-all-003.log b/test/expect/real-all-003.log index ea9228bac..c914b8d9f 100644 --- a/test/expect/real-all-003.log +++ b/test/expect/real-all-003.log @@ -253,8 +253,8 @@ restore_command = '[BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf recovery_target = 'immediate' recovery_target_action = 'promote' -restore delta, backup '[BACKUP-FULL-1]', type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) -> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --set=[BACKUP-FULL-1] --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore +restore delta, type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) +> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore ------------------------------------------------------------------------------------------------------------------------------------ + supplemental file: [TEST_PATH]/db-master/db/base/recovery.conf diff --git a/test/expect/real-all-004.log b/test/expect/real-all-004.log index 594d810cd..2050bfb66 100644 --- a/test/expect/real-all-004.log +++ b/test/expect/real-all-004.log @@ -241,8 +241,8 @@ restore, type 'default' (db-master host) # Recovery settings generated by pgBackRest restore on [TIMESTAMP] restore_command = '[BACKREST-BIN] --buffer-size=[BUFFER-SIZE] --config=[TEST_PATH]/db-master/pgbackrest.conf --stanza=db archive-get %f "%p"' -restore delta, backup '[BACKUP-FULL-1]', type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) -> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --set=[BACKUP-FULL-1] --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore +restore delta, type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) +> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore ------------------------------------------------------------------------------------------------------------------------------------ + supplemental file: [TEST_PATH]/db-master/db/base/recovery.conf diff --git a/test/expect/real-all-005.log b/test/expect/real-all-005.log index cbd67ccee..072c9b86e 100644 --- a/test/expect/real-all-005.log +++ b/test/expect/real-all-005.log @@ -347,8 +347,8 @@ restore_command = '[BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf recovery_target = 'immediate' recovery_target_action = 'promote' -restore delta, backup '[BACKUP-FULL-1]', type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) -> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --set=[BACKUP-FULL-1] --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore +restore delta, type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) +> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore ------------------------------------------------------------------------------------------------------------------------------------ + supplemental file: [TEST_PATH]/db-master/db/base/recovery.conf diff --git a/test/expect/real-all-006.log b/test/expect/real-all-006.log index 1af5c0d1b..ecdb630f1 100644 --- a/test/expect/real-all-006.log +++ b/test/expect/real-all-006.log @@ -243,8 +243,8 @@ restore, type 'default' (db-master host) # Recovery settings generated by pgBackRest restore on [TIMESTAMP] restore_command = '[BACKREST-BIN] --buffer-size=[BUFFER-SIZE] --config=[TEST_PATH]/db-master/pgbackrest.conf --stanza=db archive-get %f "%p"' -restore delta, backup '[BACKUP-FULL-1]', type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) -> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --set=[BACKUP-FULL-1] --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore +restore delta, type 'time', target '[TIMESTAMP-TARGET-1]', target-action=promote (db-master host) +> [CONTAINER-EXEC] db-master [BACKREST-BIN] --config=[TEST_PATH]/db-master/pgbackrest.conf --delta --type=time --target="[TIMESTAMP-TARGET-1]" --link-all --target-action=promote --stanza=db restore ------------------------------------------------------------------------------------------------------------------------------------ + supplemental file: [TEST_PATH]/db-master/db/base/recovery.conf diff --git a/test/lib/pgBackRestTest/Common/LogTest.pm b/test/lib/pgBackRestTest/Common/LogTest.pm index 90d0ff82d..19ea2bc49 100644 --- a/test/lib/pgBackRestTest/Common/LogTest.pm +++ b/test/lib/pgBackRestTest/Common/LogTest.pm @@ -464,6 +464,10 @@ sub regExpReplaceAll $strLine = $self->regExpReplace($strLine, 'TIMESTAMP-TARGET', " \\-\\-target\\=\\\".*UTC", "[^\\\"]+UTC\$"); $strLine = $self->regExpReplace($strLine, 'TIMESTAMP-TARGET', "^recovery_target_time \\= \\'.*UTC", "[^\\']+UTC\$"); + $strLine = $self->regExpReplace($strLine, 'TIMESTAMP-TARGET', "\\, target \\'.*\\+00", "[^\\']+\\+00\$"); + $strLine = $self->regExpReplace($strLine, 'TIMESTAMP-TARGET', " \\-\\-target\\=\\\".*\\+00", "[^\\\"]+\\+00\$"); + $strLine = $self->regExpReplace($strLine, 'TIMESTAMP-TARGET', "^recovery_target_time \\= \\'.*\\+00", "[^\\']+\\+00\$"); + # Full test xid-based recovery (this expressions only work when time-based expressions above have already been applied $strLine = $self->regExpReplace($strLine, 'XID-TARGET', "\\, target \\'[0-9]+", "[0-9]+\$"); $strLine = $self->regExpReplace($strLine, 'XID-TARGET', " \\-\\-target\\=\\\"[0-9]+", "[0-9]+\$"); diff --git a/test/lib/pgBackRestTest/Env/Host/HostBackupTest.pm b/test/lib/pgBackRestTest/Env/Host/HostBackupTest.pm index 53ddb6d6a..e256064d0 100644 --- a/test/lib/pgBackRestTest/Env/Host/HostBackupTest.pm +++ b/test/lib/pgBackRestTest/Env/Host/HostBackupTest.pm @@ -1537,6 +1537,7 @@ sub restore $strOptionalParam, $bTablespace, $strUser, + $strBackupExpected, ) = logDebugParam ( @@ -1557,6 +1558,7 @@ sub restore {name => 'strOptionalParam', optional => true}, {name => 'bTablespace', optional => true}, {name => 'strUser', optional => true}, + {name => 'strBackupExpected', optional => true}, ); # Build link map options @@ -1593,10 +1595,15 @@ sub restore if (!defined($rhExpectedManifest)) { + if (!defined($strBackupExpected)) + { + $strBackupExpected = $strBackup eq 'latest' ? $oHostBackup->backupLast() : $strBackup; + } + # Load the manifest my $oExpectedManifest = new pgBackRest::Manifest( storageRepo()->pathGet( - STORAGE_REPO_BACKUP . qw{/} . ($strBackup eq 'latest' ? $oHostBackup->backupLast() : $strBackup) . qw{/} . + STORAGE_REPO_BACKUP . qw{/} . $strBackupExpected. qw{/} . FILE_MANIFEST), {strCipherPass => $oHostBackup->cipherPassManifest()}); diff --git a/test/lib/pgBackRestTest/Module/Real/RealAllTest.pm b/test/lib/pgBackRestTest/Module/Real/RealAllTest.pm index 0b6f3f621..f807621e9 100644 --- a/test/lib/pgBackRestTest/Module/Real/RealAllTest.pm +++ b/test/lib/pgBackRestTest/Module/Real/RealAllTest.pm @@ -543,11 +543,11 @@ sub run $oHostDbMaster->start(); } - # Setup the time target + # Setup the time targets #--------------------------------------------------------------------------------------------------------------------------- $oHostDbMaster->sqlExecute("update test set message = '$strTimeMessage'"); $oHostDbMaster->sqlWalRotate(); - my $strTimeTarget = $oHostDbMaster->sqlSelectOne("select to_char(current_timestamp, 'YYYY-MM-DD HH24:MI:SS.US TZ')"); + my $strTimeTarget = $oHostDbMaster->sqlSelectOne("select current_timestamp"); &log(INFO, " time target is ${strTimeTarget}"); # Incr backup - fail on archive_mode=always when version >= 9.5 @@ -904,18 +904,19 @@ sub run $oHostDbMaster->sqlExecute("update test set message = '$strTimelineMessage'"); } - # Restore (restore type = time, inclusive) - there is no exclusive time test because I can't find a way to find the - # exact commit time of a transaction. + # Restore (restore type = time, inclusive, automatically select backup) - there is no exclusive time test because I can't + # find a way to find the exact commit time of a transaction. #--------------------------------------------------------------------------------------------------------------------------- &log(INFO, ' testing recovery type = ' . CFGOPTVAL_RESTORE_TYPE_TIME); $oHostDbMaster->clusterStop(); $oHostDbMaster->restore( - undef, $strFullBackup, + undef, cfgDefOptionDefault(CFGCMD_RESTORE, CFGOPT_SET), {bDelta => true, strType => CFGOPTVAL_RESTORE_TYPE_TIME, strTarget => $strTimeTarget, strTargetAction => $oHostDbMaster->pgVersion() >= PG_VERSION_91 ? 'promote' : undef, - strTargetTimeline => $oHostDbMaster->pgVersion() >= PG_VERSION_12 ? 'current' : undef}); + strTargetTimeline => $oHostDbMaster->pgVersion() >= PG_VERSION_12 ? 'current' : undef, + strBackupExpected => $strFullBackup}); $oHostDbMaster->clusterStart(); $oHostDbMaster->sqlSelectOneTest('select message from test', $strTimeMessage); diff --git a/test/src/module/command/restoreTest.c b/test/src/module/command/restoreTest.c index b9c5d474a..e77232c52 100644 --- a/test/src/module/command/restoreTest.c +++ b/test/src/module/command/restoreTest.c @@ -55,7 +55,7 @@ Test data for backup.info "\"backup-info-repo-size\":3159811,\"backup-info-repo-size-delta\":15765,\"backup-info-size\":26897030," \ "\"backup-info-size-delta\":163866,\"backup-prior\":\"20161219-212741F\",\"backup-reference\":[\"20161219-212741F\"," \ "\"20161219-212741F_20161219-212803D\"]," \ - "\"backup-timestamp-start\":1482182877,\"backup-timestamp-stop\":1482182883,\"backup-type\":\"incr\",\"db-id\":1," \ + "\"backup-timestamp-start\":1482182884,\"backup-timestamp-stop\":1482182985,\"backup-type\":\"incr\",\"db-id\":1," \ "\"option-archive-check\":true,\"option-archive-copy\":false,\"option-backup-standby\":false," \ "\"option-checksum-page\":false,\"option-compress\":true,\"option-hardlink\":false,\"option-online\":true}\n" @@ -446,6 +446,44 @@ testRun(void) storageRemoveP(storagePgWrite(), strNew(PG_FILE_PGVERSION), .errorOnMissing = true); } + // ***************************************************************************************************************************** + if (testBegin("getEpoch()")) + { + TEST_TITLE("system time UTC"); + + setenv("TZ", "UTC", true); + TEST_RESULT_UINT(getEpoch(strNew("2020-01-08 09:18:15-0700")), 1578500295, "epoch with timezone"); + TEST_RESULT_UINT(getEpoch(strNew("2020-01-08 16:18:15.0000")), 1578500295, "same epoch no timezone"); + TEST_RESULT_UINT(getEpoch(strNew("2020-01-08 16:18:15.0000+00")), 1578500295, "same epoch timezone 0"); + TEST_ERROR_FMT(getEpoch(strNew("2020-13-08 16:18:15")), FormatError, "invalid date 2020-13-08"); + TEST_ERROR_FMT(getEpoch(strNew("2020-01-08 16:68:15")), FormatError, "invalid time 16:68:15"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("system time America/New_York"); + + setenv("TZ", "America/New_York", true); + time_t testTime = 1573754569; + char timeBuffer[20]; + strftime(timeBuffer, sizeof(timeBuffer), "%Y-%m-%d %H:%M:%S", localtime(&testTime)); + TEST_RESULT_Z(timeBuffer, "2019-11-14 13:02:49", "check timezone set"); + TEST_RESULT_UINT(getEpoch(strNew("2019-11-14 13:02:49-0500")), 1573754569, "offset same as local"); + TEST_RESULT_UINT(getEpoch(strNew("2019-11-14 13:02:49")), 1573754569, "GMT-0500 (EST)"); + TEST_RESULT_UINT(getEpoch(strNew("2019-09-14 20:02:49")), 1568505769, "GMT-0400 (EDT)"); + TEST_RESULT_UINT(getEpoch(strNew("2018-04-27 04:29:00+04:30")), 1524787140, "GMT+0430"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("invalid target time format"); + + TEST_RESULT_UINT(getEpoch(strNew("Tue, 15 Nov 1994 12:45:26")), 0, "invalid date time format"); + TEST_RESULT_LOG( + "P00 WARN: automatic backup set selection cannot be performed with provided time 'Tue, 15 Nov 1994 12:45:26'," + " latest backup set will be used\n" + " HINT: time format must be YYYY-MM-DD HH:MM:SS with optional msec and optional timezone" + " (+/- HH or HHMM or HH:MM) - if timezone is ommitted, local time is assumed (for UTC use +00)"); + + setenv("TZ", "UTC", true); + } + // ***************************************************************************************************************************** if (testBegin("restoreBackupSet()")) { @@ -478,6 +516,54 @@ testRun(void) harnessCfgLoad(cfgCmdRestore, argList); TEST_ERROR(restoreBackupSet(infoBackup), BackupSetInvalidError, "backup set BOGUS is not valid"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("target time"); + setenv("TZ", "UTC", true); + + infoBackup = infoBackupNewLoad( + ioBufferReadNew(harnessInfoChecksumZ(TEST_RESTORE_BACKUP_INFO "\n" TEST_RESTORE_BACKUP_INFO_DB))); + + argList = strLstNew(); + strLstAddZ(argList, "--stanza=test1"); + strLstAdd(argList, strNewFmt("--repo1-path=%s", strPtr(repoPath))); + strLstAdd(argList, strNewFmt("--pg1-path=%s", strPtr(pgPath))); + strLstAddZ(argList, "--type=time"); + strLstAddZ(argList, "--target=2016-12-19 16:28:04-0500"); + + harnessCfgLoad(cfgCmdRestore, argList); + + TEST_RESULT_STR_Z(restoreBackupSet(infoBackup), "20161219-212741F_20161219-212803D", "backup set found"); + + argList = strLstNew(); + strLstAddZ(argList, "--stanza=test1"); + strLstAdd(argList, strNewFmt("--repo1-path=%s", strPtr(repoPath))); + strLstAdd(argList, strNewFmt("--pg1-path=%s", strPtr(pgPath))); + strLstAddZ(argList, "--type=time"); + strLstAddZ(argList, "--target=2016-12-19 16:27:30-0500"); + + harnessCfgLoad(cfgCmdRestore, argList); + + TEST_RESULT_STR_Z(restoreBackupSet(infoBackup), "20161219-212741F_20161219-212918I", "default to latest backup set"); + TEST_RESULT_LOG( + "P00 WARN: unable to find backup set with stop time less than '2016-12-19 16:27:30-0500', latest backup set will be" + " used"); + + argList = strLstNew(); + strLstAddZ(argList, "--stanza=test1"); + strLstAdd(argList, strNewFmt("--repo1-path=%s", strPtr(repoPath))); + strLstAdd(argList, strNewFmt("--pg1-path=%s", strPtr(pgPath))); + strLstAddZ(argList, "--type=time"); + strLstAddZ(argList, "--target=Tue, 15 Nov 1994 12:45:26"); + + harnessCfgLoad(cfgCmdRestore, argList); + + TEST_RESULT_STR_Z(restoreBackupSet(infoBackup), "20161219-212741F_20161219-212918I", "time invalid format, default latest"); + TEST_RESULT_LOG( + "P00 WARN: automatic backup set selection cannot be performed with provided time 'Tue, 15 Nov 1994 12:45:26'," + " latest backup set will be used\n" + " HINT: time format must be YYYY-MM-DD HH:MM:SS with optional msec and optional timezone" + " (+/- HH or HHMM or HH:MM) - if timezone is ommitted, local time is assumed (for UTC use +00)"); } // *****************************************************************************************************************************