1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2026-10-23 21:28:56 +03:00
Files
pgbackrest/test/src/module/command/backupTest.c
David Steele 6e28c0445f Add archive key rotation for repository format 6.
At format 6 the repository can hold more than one encryption key. This is used to rotate the archive key, which is currently the only bulk data key that never changes.

Keys are stored by id in the new CipherSpecMap, with one key marked current for encrypting new files. At format 6 the cipher section of an info file stores every key by id along with the digest it derives with, e.g. cipher-pass={"1":{"digest":"sha256","key":"..."}}, and archive.info also records the current key id in cipher-pass-current. Format 5 files keep their single key, which is loaded as the key with no id.

archive-push writes the id of the archive key it used into the header of each WAL file. The readers use that id to select the key: archive-get, verify, the restore timeline read, the backup archive copy, and repo-get. A pusher that loaded archive.info before a rotation keeps working since each file records the key it was encrypted with. The filter that writes the header now also works through a repository host.

stanza-create for format 6 creates the first archive key with id 1. stanza-upgrade from format 5 keeps the existing archive key as id 0 so WAL already archived stays readable, and adds a key at id 1 for new WAL.

The new repo-cipher-rotate option sets how old the current archive key may be before expire replaces it, with a default of 90 days and a minimum of 15. Expire adds the new key, makes it current, and records the time of the rotation in archive.info as cipher-pass-rotate. At format 5 the option has no effect and warns only when set explicitly.

Expire now saves archive.info on every run. Backup did this only to refresh the timestamps for object store lifecycle rules and no longer writes archive.info, so with expire-auto disabled the timestamps are refreshed only when expire runs.
2026-10-02 08:37:45 +02:00

4415 lines
235 KiB
C

/***********************************************************************************************************************************
Test Backup Command
***********************************************************************************************************************************/
#include "command/stanza/create.h"
#include "command/stanza/upgrade.h"
#include "common/crypto/hash.h"
#include "common/io/bufferRead.h"
#include "common/io/bufferWrite.h"
#include "postgres/interface/static.vendor.h"
#include "storage/helper.h"
#include "storage/posix/storage.h"
#include "harness/backup.h"
#include "harness/blockIncr.h"
#include "harness/config.h"
#include "harness/info.h"
#include "harness/manifest.h"
#include "harness/pack.h"
#include "harness/postgres.h"
#include "harness/pq.h"
#include "harness/protocol.h"
#include "harness/storage.h"
#include "harness/time.h"
/***********************************************************************************************************************************
Get a list of all files in the backup and a redacted version of the manifest that can be tested against a static string
***********************************************************************************************************************************/
static String *
testBackupValidateFile(
const Storage *const storage, const String *const path, Manifest *const manifest, const ManifestData *const manifestData,
const String *const fileName, uint64_t fileSize, ManifestFilePack **const filePack)
{
FUNCTION_HARNESS_BEGIN();
FUNCTION_HARNESS_PARAM(STORAGE, storage);
FUNCTION_HARNESS_PARAM(STRING, path);
FUNCTION_HARNESS_PARAM(MANIFEST, manifest);
FUNCTION_HARNESS_PARAM_P(VOID, manifestData);
FUNCTION_HARNESS_PARAM(STRING, fileName);
FUNCTION_HARNESS_PARAM(UINT64, fileSize);
FUNCTION_HARNESS_PARAM_P(VOID, filePack);
FUNCTION_HARNESS_END();
String *const result = strNew();
ManifestFile file = manifestFileUnpack(manifest, *filePack);
// Every backup in a set shares the passphrase stored in the manifest, including files referenced from a prior backup
const CipherSpec *const cipherSpecBackup = manifestCipherSpec(manifest);
// Output name and size
// -------------------------------------------------------------------------------------------------------------
if (file.bundleId != 0)
strCatFmt(result, "%s/%s {", strZ(fileName), strZ(file.name));
else
strCatFmt(result, "%s {", strZ(fileName));
strCatFmt(result, "s=%" PRIu64, file.size);
if (file.sizeOriginal != file.size)
strCatFmt(result, ", so=%" PRIu64, file.sizeOriginal);
// Validate repo checksum
// -------------------------------------------------------------------------------------------------------------
if (file.checksumRepoSha1 != NULL)
{
StorageRead *read = storageNewReadP(
storage, strNewFmt("%s/%s", strZ(path), strZ(fileName)), .offset = file.bundleOffset,
.limit = VARUINT64(file.sizeRepo));
const Buffer *const checksum = cryptoHashOne(hashTypeSha1, storageGetP(read));
if (!bufEq(checksum, BUF(file.checksumRepoSha1, HASH_TYPE_SHA1_SIZE)))
{
THROW_FMT(
AssertError, "'%s' repo checksum %s does not match manifest checksum %s", strZ(file.name),
strZ(strNewEncode(encodingHex, checksum)),
strZ(strNewEncode(encodingHex, BUF(file.checksumRepoSha1, HASH_TYPE_SHA1_SIZE))));
}
}
// Calculate checksum/size and decompress if needed
// -------------------------------------------------------------------------------------------------------------
uint64_t size = 0;
const Buffer *checksum = NULL;
// If block incremental
if (file.blockIncrMapSize != 0)
{
// Read block map
StorageRead *read = storageNewReadP(
storage, strNewFmt("%s/%s", strZ(path), strZ(fileName)),
.offset = file.bundleOffset + file.sizeRepo - file.blockIncrMapSize,
.limit = VARUINT64(file.blockIncrMapSize));
if (cipherSpecType(cipherSpecBackup) != cipherTypeNone)
{
ioFilterGroupAdd(
ioReadFilterGroup(storageReadIo(read)), cipherBlockNewP(
cipherModeDecrypt, cipherSpecBackup, .header = cipherBlockHeaderNone));
}
ioReadOpen(storageReadIo(read));
// Build map log
const BlockMap *const blockMap = blockMapNewRead(storageReadIo(read), file.blockIncrSize, file.blockIncrChecksumSize);
String *const mapLog = strNew();
const BlockMapItem *blockMapItemLast = NULL;
for (unsigned int blockMapIdx = 0; blockMapIdx < blockMapSize(blockMap); blockMapIdx++)
{
const BlockMapItem *const blockMapItem = blockMapGet(blockMap, blockMapIdx);
const bool superBlockChange =
blockMapItemLast == NULL || blockMapItemLast->reference != blockMapItem->reference ||
blockMapItemLast->offset != blockMapItem->offset;
if (superBlockChange && blockMapIdx != 0)
strCatChr(mapLog, '}');
if (!strEmpty(mapLog))
strCatChr(mapLog, ',');
if (superBlockChange)
strCatFmt(mapLog, "%u:{", blockMapItem->reference);
strCatFmt(mapLog, "%" PRIu64, blockMapItem->block);
blockMapItemLast = blockMapItem;
}
// Check blocks
Buffer *fileBuffer = bufNew((size_t)file.size);
bufUsedSet(fileBuffer, bufSize(fileBuffer));
BlockDelta *const blockDelta = blockDeltaNew(
blockMap, file.blockIncrSize, file.blockIncrChecksumSize, NULL, cipherSpecBackup,
manifestData->backupOptionCompressType);
for (unsigned int readIdx = 0; readIdx < blockDeltaReadSize(blockDelta); readIdx++)
{
const BlockDeltaRead *const read = blockDeltaReadGet(blockDelta, readIdx);
const String *const blockName = backupFileRepoPathP(
strLstGet(manifestReferenceList(manifest), read->reference), .manifestName = file.name,
.bundleId = read->bundleId, .blockIncr = true);
IoRead *blockRead = storageReadIo(
storageNewReadP(
storage, blockName, .offset = read->offset, .limit = VARUINT64(read->size)));
ioReadOpen(blockRead);
const BlockDeltaWrite *deltaWrite = blockDeltaNext(blockDelta, read, blockRead);
while (deltaWrite != NULL)
{
// Update size and file
size += bufUsed(deltaWrite->block);
memcpy(
bufPtr(fileBuffer) + deltaWrite->offset, bufPtr(deltaWrite->block),
bufUsed(deltaWrite->block));
deltaWrite = blockDeltaNext(blockDelta, read, blockRead);
}
}
strCatFmt(result, ", m=%s}", strZ(mapLog));
checksum = cryptoHashOne(hashTypeSha1, fileBuffer);
}
// Else normal file
else
{
StorageRead *read = storageNewReadP(
storage, strNewFmt("%s/%s", strZ(path), strZ(fileName)), .offset = file.bundleOffset,
.limit = VARUINT64(file.sizeRepo));
const bool raw = file.bundleId != 0 && manifest->pub.data.bundleRaw;
if (cipherSpecType(cipherSpecBackup) != cipherTypeNone)
{
ioFilterGroupAdd(
ioReadFilterGroup(storageReadIo(read)), cipherBlockNewP(
cipherModeDecrypt, cipherSpecBackup, .header = raw ? cipherBlockHeaderNone : cipherBlockHeaderMagic));
}
if (manifestData->backupOptionCompressType != compressTypeNone)
{
ioFilterGroupAdd(
ioReadFilterGroup(storageReadIo(read)),
decompressFilterP(manifestData->backupOptionCompressType, .raw = raw));
}
ioFilterGroupAdd(ioReadFilterGroup(storageReadIo(read)), cryptoHashNew(hashTypeSha1));
size = bufUsed(storageGetP(read));
checksum = pckReadBinP(
ioFilterGroupResultP(ioReadFilterGroup(storageReadIo(read)), CRYPTO_HASH_FILTER_TYPE));
}
// Validate checksum
if (!bufEq(checksum, BUF(file.checksumSha1, HASH_TYPE_SHA1_SIZE)))
THROW_FMT(AssertError, "'%s' checksum does not match manifest", strZ(file.name));
// Test size and repo-size
// -------------------------------------------------------------------------------------------------------------
if (size != file.size)
THROW_FMT(AssertError, "'%s' size does not match manifest", strZ(file.name));
// Repo size can only be compared to file size when not bundled
if (file.bundleId == 0 && fileSize != file.sizeRepo)
{
THROW_FMT(
AssertError, "'%s' repo size %" PRIu64 " does not match manifest size %" PRIu64, strZ(file.name), fileSize,
file.sizeRepo);
}
// Timestamp
// -------------------------------------------------------------------------------------------------------------
if (file.timestamp != manifestData->backupTimestampStart)
{
strCatFmt(
result, ", ts=%s%" PRId64, file.timestamp > manifestData->backupTimestampStart ? "+" : "",
(int64_t)file.timestamp - (int64_t)manifestData->backupTimestampStart);
}
// Page checksum
// -------------------------------------------------------------------------------------------------------------
if (file.checksumPage)
{
strCatZ(result, ", ckp=");
if (file.checksumPageError)
{
if (file.checksumPageErrorList != NULL)
strCat(result, file.checksumPageErrorList);
else
strCatZ(result, "t");
}
else
strCatZ(result, "t");
}
// pg_control and WAL headers have different checksums depending on cpu architecture so remove the checksum from
// the test output.
// -------------------------------------------------------------------------------------------------------------
if (strEqZ(file.name, MANIFEST_TARGET_PGDATA "/" PG_PATH_GLOBAL "/" PG_FILE_PGCONTROL) ||
strBeginsWith(
file.name, strNewFmt(MANIFEST_TARGET_PGDATA "/%s/", strZ(pgWalPath(manifestData->pgVersion)))))
{
file.checksumSha1 = NULL;
}
strCatZ(result, "}\n");
// Update changes to manifest file
manifestFilePackUpdate(manifest, filePack, &file);
FUNCTION_HARNESS_RETURN(STRING, result);
}
static String *
testBackupValidateList(
const Storage *const storage, const String *const path, Manifest *const manifest, const ManifestData *const manifestData,
StringList *const manifestFileList, String *const result)
{
FUNCTION_HARNESS_BEGIN();
FUNCTION_HARNESS_PARAM(STORAGE, storage);
FUNCTION_HARNESS_PARAM(STRING, path);
FUNCTION_HARNESS_PARAM(MANIFEST, manifest);
FUNCTION_HARNESS_PARAM_P(VOID, manifestData);
FUNCTION_HARNESS_PARAM(STRING_LIST, manifestFileList);
FUNCTION_HARNESS_PARAM(STRING, result);
FUNCTION_HARNESS_END();
// Output root path if it is a link so we can verify the destination
const StorageInfo dotInfo = storageInfoP(storage, path);
if (dotInfo.type == storageTypeLink)
strCatFmt(result, ".> {d=%s}\n", strZ(dotInfo.linkDestination));
// Output path contents
StorageIterator *const storageItr = storageNewItrP(storage, path, .recurse = true, .sortOrder = sortOrderAsc);
while (storageItrMore(storageItr))
{
const StorageInfo info = storageItrNext(storageItr);
// Don't include backup.manifest or copy. We'll test that they are present elsewhere
if (info.type == storageTypeFile &&
(strEqZ(info.name, BACKUP_MANIFEST_FILE) || strEqZ(info.name, BACKUP_MANIFEST_FILE INFO_COPY_EXT)))
{
continue;
}
switch (info.type)
{
case storageTypeFile:
{
// Test mode, user, group. These values are not in the manifest but we know what they should be based on the default
// mode and current user/group.
// -----------------------------------------------------------------------------------------------------------------
if (info.mode != 0640)
THROW_FMT(AssertError, "'%s' mode is not 0640", strZ(info.name));
if (!strEq(info.user, TEST_USER_STR))
THROW_FMT(AssertError, "'%s' user should be '" TEST_USER "'", strZ(info.name));
if (!strEq(info.group, TEST_GROUP_STR))
THROW_FMT(AssertError, "'%s' group should be '" TEST_GROUP "'", strZ(info.name));
// Build file list (needed because bundles can contain multiple files)
// -----------------------------------------------------------------------------------------------------------------
List *const fileList = lstNewP(sizeof(ManifestFilePack **));
bool bundle = strBeginsWithZ(info.name, "bundle/");
if (bundle)
{
const uint64_t bundleId = cvtZToUInt64(strZ(info.name) + sizeof("bundle"));
for (unsigned int fileIdx = 0; fileIdx < manifestFileTotal(manifest); fileIdx++)
{
ManifestFilePack **const filePack = lstGet(manifest->pub.fileList, fileIdx);
ManifestFile file = manifestFileUnpack(manifest, *filePack);
// File bundle is part of this backup
if (file.bundleId == bundleId && file.reference == NULL)
lstAdd(fileList, &filePack);
}
}
else
{
const String *manifestName = info.name;
// Remove block incremental extension
if (strEndsWithZ(info.name, BACKUP_BLOCK_INCR_EXT))
{
manifestName = strSubN(info.name, 0, strSize(info.name) - (sizeof(BACKUP_BLOCK_INCR_EXT) - 1));
}
// Else remove compression extension
else if (manifestData->backupOptionCompressType != compressTypeNone)
{
manifestName = strSubN(
info.name, 0, strSize(info.name) - strSize(compressExtStr(manifestData->backupOptionCompressType)));
}
ManifestFilePack **const filePack = manifestFilePackFindInternal(manifest, manifestName);
lstAdd(fileList, &filePack);
}
// Check files
// -----------------------------------------------------------------------------------------------------------------
for (unsigned int fileIdx = 0; fileIdx < lstSize(fileList); fileIdx++)
{
// Remove this file from manifest file list to track what has been updated
ManifestFilePack **const filePack = *(ManifestFilePack ***)lstGet(fileList, fileIdx);
const unsigned int manifestFileIdx = strLstFindIdxP(
manifestFileList, (String *const)*filePack, .required = true);
strLstRemoveIdx(manifestFileList, manifestFileIdx);
strCat(result, testBackupValidateFile(storage, path, manifest, manifestData, info.name, info.size, filePack));
}
break;
}
case storageTypeLink:
strCatFmt(result, "%s> {d=%s}\n", strZ(info.name), strZ(info.linkDestination));
break;
case storageTypePath:
{
// Add path to output only when it is empty -- otherwise it is implied by the files below it
if (strLstEmpty(storageListP(storage, storagePathP(storage, strNewFmt("%s/%s", strZ(path), strZ(info.name))))))
strCatFmt(result, "%s/\n", strZ(info.name));
// Check against the manifest
// -----------------------------------------------------------------------------------------------------------------
if (!strEq(info.name, STRDEF("bundle")))
manifestPathFind(manifest, info.name);
// Test mode, user, group. These values are not in the manifest but we know what they should be based on the default
// mode and current user/group.
if (info.mode != 0750)
THROW_FMT(AssertError, "'%s' mode is not 00750", strZ(info.name));
if (!strEq(info.user, TEST_USER_STR))
THROW_FMT(AssertError, "'%s' user should be '" TEST_USER "'", strZ(info.name));
if (!strEq(info.group, TEST_GROUP_STR))
THROW_FMT(AssertError, "'%s' group should be '" TEST_GROUP "'", strZ(info.name));
break;
}
case storageTypeSpecial:
THROW_FMT(AssertError, "unexpected special file '%s'", strZ(info.name));
}
}
FUNCTION_HARNESS_RETURN(STRING, result);
}
typedef struct TestBackupValidateParam
{
VAR_PARAM_HEADER;
const CipherSpec *cipherSpecMain; // Cipher spec the repo is configured with
} TestBackupValidateParam;
#define testBackupValidateP(storage, path, ...) \
testBackupValidate(storage, path, (TestBackupValidateParam){VAR_PARAM_INIT, __VA_ARGS__})
static String *
testBackupValidate(const Storage *const storage, const String *const path, const TestBackupValidateParam param)
{
FUNCTION_HARNESS_BEGIN();
FUNCTION_HARNESS_PARAM(STORAGE, storage);
FUNCTION_HARNESS_PARAM(STRING, path);
FUNCTION_HARNESS_PARAM(CIPHER_SPEC, param.cipherSpecMain);
FUNCTION_HARNESS_END();
ASSERT(storage != NULL);
ASSERT(path != NULL);
String *const result = strNew();
MEM_CONTEXT_TEMP_BEGIN()
{
// Build a list of files in the backup path and verify against the manifest
// -------------------------------------------------------------------------------------------------------------------------
const InfoBackup *const infoBackup = infoBackupLoadFile(
storageRepo(), INFO_BACKUP_PATH_FILE_STR, param.cipherSpecMain == NULL ? cipherSpecNewNone() : param.cipherSpecMain);
Manifest *manifest = manifestLoadFile(
storage, strNewFmt("%s/" BACKUP_MANIFEST_FILE, strZ(path)), infoBackupCipherSpec(infoBackup));
// Build list of files in the manifest
StringList *const manifestFileList = strLstNew();
for (unsigned int fileIdx = 0; fileIdx < manifestFileTotal(manifest); fileIdx++)
strLstAdd(manifestFileList, manifestFileUnpack(manifest, manifestFilePackGet(manifest, fileIdx)).name);
// Validate files on disk against the manifest
testBackupValidateList(storage, path, manifest, manifestData(manifest), manifestFileList, result);
// Check remaining files in the manifest -- these should all be references
for (unsigned int manifestFileIdx = 0; manifestFileIdx < strLstSize(manifestFileList); manifestFileIdx++)
{
ManifestFilePack **const filePack = manifestFilePackFindInternal(
manifest, strLstGet(manifestFileList, manifestFileIdx));
const ManifestFile file = manifestFileUnpack(manifest, *filePack);
// No need to check zero-length files in bundled backups
if (manifestData(manifest)->bundle && file.size == 0)
continue;
// Error if reference is NULL
if (file.reference == NULL)
THROW_FMT(AssertError, "manifest file '%s' not in backup but does not have a reference", strZ(file.name));
strCat(
result,
testBackupValidateFile(
storage, strPath(path), manifest, manifestData(manifest),
strSub(
backupFileRepoPathP(
file.reference,
.manifestName = file.name, .bundleId = file.bundleId,
.compressType = manifestData(manifest)->backupOptionCompressType,
.blockIncr = file.blockIncrMapSize != 0),
sizeof(STORAGE_REPO_BACKUP)),
file.sizeRepo, filePack));
}
// Make sure both backup.manifest files exist since we skipped them in the callback above
if (!storageExistsP(storage, strNewFmt("%s/" BACKUP_MANIFEST_FILE, strZ(path))))
THROW(AssertError, BACKUP_MANIFEST_FILE " is missing");
if (!storageExistsP(storage, strNewFmt("%s/" BACKUP_MANIFEST_FILE INFO_COPY_EXT, strZ(path))))
THROW(AssertError, BACKUP_MANIFEST_FILE INFO_COPY_EXT " is missing");
// Update manifest to make the output a bit simpler
// -------------------------------------------------------------------------------------------------------------------------
for (unsigned int fileIdx = 0; fileIdx < manifestFileTotal(manifest); fileIdx++)
{
ManifestFilePack **const filePack = lstGet(manifest->pub.fileList, fileIdx);
ManifestFile file = manifestFileUnpack(manifest, *filePack);
// If compressed or block incremental then set the repo-size to size so it will not be in test output. Even the same
// compression algorithm can give slightly different results based on the version so repo-size is not deterministic for
// compression. Block incremental maps increase repo-size in a non-obvious way.
if (manifestData(manifest)->backupOptionCompressType != compressTypeNone || file.blockIncrMapSize != 0)
file.sizeRepo = file.size;
// Bundle id/offset are too noisy so remove them. They are verified against size/checksum and listed with the files.
file.bundleId = 0;
file.bundleOffset = 0;
// Remove repo checksum since it has been validated
file.checksumRepoSha1 = NULL;
// Update changes to manifest file
manifestFilePackUpdate(manifest, filePack, &file);
}
// Output the manifest to a string and exclude sections that don't need validation. Note that each of these sections should
// be considered from automatic validation but adding them to the output will make the tests too noisy. One good technique
// would be to remove it from the output only after validation so new values will cause changes in the output.
// -------------------------------------------------------------------------------------------------------------------------
Buffer *manifestSaveBuffer = bufNew(0);
manifestSave(manifest, ioBufferWriteNew(manifestSaveBuffer));
String *manifestEdit = strNew();
StringList *manifestLine = strLstNewSplitZ(strTrim(strNewBuf(manifestSaveBuffer)), "\n");
bool bSkipSection = false;
for (unsigned int lineIdx = 0; lineIdx < strLstSize(manifestLine); lineIdx++)
{
const String *line = strTrim(strLstGet(manifestLine, lineIdx));
if (strChr(line, '[') == 0)
{
const String *section = strSubN(line, 1, strSize(line) - 2);
if (strEqZ(section, INFO_SECTION_BACKREST) ||
strEqZ(section, INFO_SECTION_CIPHER) ||
strEqZ(section, MANIFEST_SECTION_BACKUP) ||
strEqZ(section, MANIFEST_SECTION_BACKUP_DB) ||
strEqZ(section, MANIFEST_SECTION_BACKUP_OPTION) ||
strEqZ(section, MANIFEST_SECTION_DB) ||
strEqZ(section, MANIFEST_SECTION_TARGET_FILE) ||
strEqZ(section, MANIFEST_SECTION_TARGET_FILE_DEFAULT) ||
strEqZ(section, MANIFEST_SECTION_TARGET_LINK) ||
strEqZ(section, MANIFEST_SECTION_TARGET_LINK_DEFAULT) ||
strEqZ(section, MANIFEST_SECTION_TARGET_PATH) ||
strEqZ(section, MANIFEST_SECTION_TARGET_PATH_DEFAULT))
{
bSkipSection = true;
}
else
bSkipSection = false;
}
if (!bSkipSection)
strCatFmt(manifestEdit, "%s\n", strZ(line));
}
strCatFmt(result, "--------\n%s\n", strZ(strTrim(manifestEdit)));
}
MEM_CONTEXT_TEMP_END();
FUNCTION_HARNESS_RETURN(STRING, result);
}
/***********************************************************************************************************************************
Test Run
***********************************************************************************************************************************/
static void
testRun(void)
{
FUNCTION_HARNESS_VOID();
// Shim backupFileComparator to place pg_control at end of bundle
hrnBackupFileComparatorShim();
// Install local command handler shim
static const ProtocolServerHandler testLocalHandlerList[] = {PROTOCOL_SERVER_HANDLER_BACKUP_LIST};
hrnProtocolLocalShimInstall(LSTDEF(testLocalHandlerList));
// The tests expect the timezone to be UTC
hrnTzSet("UTC");
Storage *storageTest = storagePosixNewP(TEST_PATH_STR, .write = true);
// *****************************************************************************************************************************
if (testBegin("backupRegExp()"))
{
const String *full = STRDEF("20181119-152138F");
const String *incr = STRDEF("20181119-152138F_20181119-152152I");
const String *diff = STRDEF("20181119-152138F_20181119-152152D");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("regular expression - error");
TEST_ERROR(
backupRegExpP(0),
AssertError, "assertion 'param.full || param.differential || param.incremental' failed");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("regular expression - match full");
String *filter = backupRegExpP(.full = true);
TEST_RESULT_STR_Z(filter, "^[0-9]{8}\\-[0-9]{6}F$", "full backup regex with anchors");
TEST_RESULT_BOOL(regExpMatchOne(filter, incr), false, "does not exactly match incr");
TEST_RESULT_BOOL(regExpMatchOne(filter, diff), false, "does not exactly match diff");
TEST_RESULT_BOOL(regExpMatchOne(filter, full), true, "exactly matches full");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("regular expression - match full, incremental");
filter = backupRegExpP(.full = true, .incremental = true);
TEST_RESULT_STR_Z(
filter, "^[0-9]{8}\\-[0-9]{6}F(\\_[0-9]{8}\\-[0-9]{6}I){0,1}$", "full and optional incr backup regex with anchors");
TEST_RESULT_BOOL(regExpMatchOne(filter, incr), true, "match incr");
TEST_RESULT_BOOL(regExpMatchOne(filter, diff), false, "does not match diff");
TEST_RESULT_BOOL(regExpMatchOne(filter, full), true, "match full");
TEST_RESULT_BOOL(
regExpMatchOne(
filter, STRDEF("12341234-123123F_12341234-123123IG")), false, "does not match with trailing character");
TEST_RESULT_BOOL(
regExpMatchOne(
filter, STRDEF("A12341234-123123F_12341234-123123I")), false, "does not match with leading character");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("regular expression - match full, differential");
filter = backupRegExpP(.full = true, .differential = true);
TEST_RESULT_STR_Z(
filter, "^[0-9]{8}\\-[0-9]{6}F(\\_[0-9]{8}\\-[0-9]{6}D){0,1}$", "full and optional diff backup regex with anchors");
TEST_RESULT_BOOL(regExpMatchOne(filter, incr), false, "does not match incr");
TEST_RESULT_BOOL(regExpMatchOne(filter, diff), true, "match diff");
TEST_RESULT_BOOL(regExpMatchOne(filter, full), true, "match full");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("regular expression - match full, incremental, differential");
filter = backupRegExpP(.full = true, .incremental = true, .differential = true);
TEST_RESULT_STR_Z(
filter, "^[0-9]{8}\\-[0-9]{6}F(\\_[0-9]{8}\\-[0-9]{6}(D|I)){0,1}$",
"full, optional diff and incr backup regex with anchors");
TEST_RESULT_BOOL(regExpMatchOne(filter, incr), true, "match incr");
TEST_RESULT_BOOL(regExpMatchOne(filter, diff), true, "match diff");
TEST_RESULT_BOOL(regExpMatchOne(filter, full), true, "match full");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("regular expression - match incremental, differential without end anchor");
filter = backupRegExpP(.incremental = true, .differential = true, .noAnchorEnd = true);
TEST_RESULT_STR_Z(filter, "^[0-9]{8}\\-[0-9]{6}F\\_[0-9]{8}\\-[0-9]{6}(D|I)", "diff and incr backup regex with anchors");
TEST_RESULT_BOOL(regExpMatchOne(filter, incr), true, "match incr");
TEST_RESULT_BOOL(regExpMatchOne(filter, diff), true, "match diff");
TEST_RESULT_BOOL(regExpMatchOne(filter, full), false, "does not match full");
TEST_RESULT_BOOL(
regExpMatchOne(
filter, STRDEF("A12341234-123123F_12341234-123123I")), false, "does not match with leading character");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("regular expression - match incremental");
filter = backupRegExpP(.incremental = true);
TEST_RESULT_STR_Z(filter, "^[0-9]{8}\\-[0-9]{6}F\\_[0-9]{8}\\-[0-9]{6}I$", "incr backup regex with anchors");
TEST_RESULT_BOOL(regExpMatchOne(filter, incr), true, "match incr");
TEST_RESULT_BOOL(regExpMatchOne(filter, diff), false, "does not match diff");
TEST_RESULT_BOOL(regExpMatchOne(filter, full), false, "does not match full");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("regular expression - match differential");
filter = backupRegExpP(.differential = true);
TEST_RESULT_STR_Z(filter, "^[0-9]{8}\\-[0-9]{6}F\\_[0-9]{8}\\-[0-9]{6}D$", "diff backup regex with anchors");
TEST_RESULT_BOOL(regExpMatchOne(filter, incr), false, "does not match incr");
TEST_RESULT_BOOL(regExpMatchOne(filter, diff), true, "match diff");
TEST_RESULT_BOOL(regExpMatchOne(filter, full), false, "does not match full");
}
// *****************************************************************************************************************************
if (testBegin("PageChecksum"))
{
#define PG_SEGMENT_PAGE_DEFAULT (PG_SEGMENT_SIZE_DEFAULT / pgPageSize8)
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("segment page default");
TEST_RESULT_UINT(PG_SEGMENT_PAGE_DEFAULT, 131072, "check pages per segment");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("two misaligned buffers in a row");
Buffer *buffer = bufNew(513);
bufUsedSet(buffer, bufSize(buffer));
memset(bufPtr(buffer), 0, bufSize(buffer));
*(PageHeaderData *)(bufPtr(buffer) + (pgPageSize8 * 0x00)) = (PageHeaderData){.pd_upper = 0};
Buffer *bufferOut = bufNew(513);
IoWrite *write = ioBufferWriteNew(bufferOut);
ioFilterGroupAdd(
ioWriteFilterGroup(write),
pageChecksumNewPack(
ioFilterParamList(pageChecksumNew(0, PG_SEGMENT_PAGE_DEFAULT, pgPageSize8, true, STRDEF(BOGUS_STR)))));
ioWriteOpen(write);
ioWrite(write, buffer);
TEST_ERROR(ioWrite(write, buffer), AssertError, "should not be possible to see two misaligned pages in a row");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("retry a page with an invalid checksum");
// Write to file with valid checksums
buffer = bufNew(pgPageSize8 * 4);
memset(bufPtr(buffer), 0, bufSize(buffer));
bufUsedSet(buffer, bufSize(buffer));
*(PageHeaderData *)(bufPtr(buffer) + (pgPageSize8 * 0x00)) = (PageHeaderData){.pd_upper = 0x00};
*(PageHeaderData *)(bufPtr(buffer) + (pgPageSize8 * 0x01)) = (PageHeaderData){.pd_upper = 0xFF};
((PageHeaderData *)(bufPtr(buffer) + (pgPageSize8 * 0x01)))->pd_checksum = pgPageChecksum(
bufPtr(buffer) + (pgPageSize8 * 0x01), 1, pgPageSize8);
*(PageHeaderData *)(bufPtr(buffer) + (pgPageSize8 * 0x02)) = (PageHeaderData){.pd_upper = 0x00};
*(PageHeaderData *)(bufPtr(buffer) + (pgPageSize8 * 0x03)) = (PageHeaderData){.pd_upper = 0xFE};
((PageHeaderData *)(bufPtr(buffer) + (pgPageSize8 * 0x03)))->pd_checksum = pgPageChecksum(
bufPtr(buffer) + (pgPageSize8 * 0x03), 3, pgPageSize8);
HRN_STORAGE_PUT(storageTest, "relation", buffer);
// Now break the checksum to force a retry
((PageHeaderData *)(bufPtr(buffer) + (pgPageSize8 * 0x01)))->pd_checksum = 0;
((PageHeaderData *)(bufPtr(buffer) + (pgPageSize8 * 0x03)))->pd_checksum = 0;
write = ioBufferWriteNew(bufferOut);
ioFilterGroupAdd(
ioWriteFilterGroup(write),
pageChecksumNew(0, PG_SEGMENT_PAGE_DEFAULT, pgPageSize8, true, storagePathP(storageTest, STRDEF("relation"))));
ioWriteOpen(write);
ioWrite(write, buffer);
ioWriteClose(write);
TEST_RESULT_STR_Z(
hrnPackToStr(ioFilterGroupResultPackP(ioWriteFilterGroup(write), PAGE_CHECKSUM_FILTER_TYPE)),
"2:bool:true, 3:bool:true", "valid on retry");
}
// *****************************************************************************************************************************
if (testBegin("segmentNumber()"))
{
TEST_RESULT_UINT(segmentNumber(STRDEF("999")), 0, "No segment number");
TEST_RESULT_UINT(segmentNumber(STRDEF("999.123")), 123, "Segment number");
}
// *****************************************************************************************************************************
if (testBegin("BlockMap"))
{
TEST_TITLE("build equal block map");
BlockMap *blockMap = NULL;
TEST_ASSIGN(blockMap, blockMapNew(), "new");
BlockMapItem blockMapItem =
{
.reference = 128,
.superBlockSize = 1,
.bundleId = 0,
.offset = 0,
.size = 3,
.checksum = {0xee, 0xee, 0x01, 0xff, 0xff},
};
TEST_RESULT_UINT(blockMapAdd(blockMap, &blockMapItem)->reference, 128, "add");
TEST_RESULT_UINT(blockMapGet(blockMap, 0)->reference, 128, "get");
blockMapItem = (BlockMapItem)
{
.reference = 128,
.superBlockSize = 1,
.bundleId = 0,
.offset = 3,
.size = 5,
.checksum = {0xee, 0xee, 0x02, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
blockMapItem = (BlockMapItem)
{
.reference = 0,
.superBlockSize = 1,
.bundleId = 1,
.offset = 1,
.size = 5,
.checksum = {0xee, 0xee, 0x03, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
blockMapItem = (BlockMapItem)
{
.reference = 128,
.superBlockSize = 1,
.bundleId = 0,
.offset = 8,
.size = 99,
.checksum = {0xee, 0xee, 0x04, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
blockMapItem = (BlockMapItem)
{
.reference = 0,
.superBlockSize = 1,
.bundleId = 1,
.offset = 7,
.size = 99,
.checksum = {0xee, 0xee, 0x05, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
// Keep this last
blockMapItem = (BlockMapItem)
{
.reference = 4,
.superBlockSize = 1,
.bundleId = 0,
.offset = 0,
.size = 8,
.checksum = {0xee, 0xee, 0x88, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("write equal block map");
Buffer *buffer = bufNew(256);
IoWrite *write = ioBufferWriteNewOpen(buffer);
TEST_RESULT_VOID(blockMapWrite(blockMap, write, 1, 5), "save");
ioWriteClose(write);
TEST_RESULT_STR_Z(
strNewEncode(encodingHex, buffer),
"00" // Version 0
"8008" // reference 128
"18" // size 3
"eeee01ffff" // checksum
"21" // size
"eeee02ffff" // checksum
"06" // reference 0
"01" // bundle 1
"01" // offset 1
"01" // size 1
"eeee03ffff" // checksum
"8008" // reference 128
"e10b" // size 99
"eeee04ffff" // checksum
"04" // reference 0
"01" // offset 7
"01" // size 99
"eeee05ffff" // checksum
"21" // reference 0
"a90b" // size 8
"eeee88ffff", // checksum
"compare");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("read equal block map");
Buffer *bufferCompare = bufNew(256);
write = ioBufferWriteNewOpen(bufferCompare);
TEST_RESULT_VOID(blockMapWrite(blockMapNewRead(ioBufferReadNewOpen(buffer), 1, 5), write, 1, 5), "read and save");
ioWriteClose(write);
TEST_RESULT_STR(strNewEncode(encodingHex, bufferCompare), strNewEncode(encodingHex, buffer), "compare");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("equal block delta");
TEST_RESULT_STR_Z(
hrnBlockDeltaRender(blockMapNewRead(ioBufferReadNewOpen(buffer), 1, 5), 1, 5),
"read {reference: 128, bundleId: 0, offset: 0, size: 107}\n"
" super block {max: 1, size: 3}\n"
" block {no: 0, offset: 0}\n"
" super block {max: 1, size: 5}\n"
" block {no: 0, offset: 1}\n"
" super block {max: 1, size: 99}\n"
" block {no: 0, offset: 3}\n"
"read {reference: 4, bundleId: 0, offset: 0, size: 8}\n"
" super block {max: 1, size: 8}\n"
" block {no: 0, offset: 5}\n"
"read {reference: 0, bundleId: 1, offset: 1, size: 5}\n"
" super block {max: 1, size: 5}\n"
" block {no: 0, offset: 2}\n"
"read {reference: 0, bundleId: 1, offset: 7, size: 99}\n"
" super block {max: 1, size: 99}\n"
" block {no: 0, offset: 4}\n",
"check delta");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("build unequal block map");
TEST_ASSIGN(blockMap, blockMapNew(), "new");
blockMapItem = (BlockMapItem)
{
.reference = 0,
.superBlockSize = 6,
.offset = 0,
.size = 4,
.block = 0,
.checksum = {0xee, 0xee, 0x01, 0, 0, 0, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
blockMapItem = (BlockMapItem)
{
.reference = 0,
.superBlockSize = 6,
.offset = 0,
.size = 4,
.block = 1,
.checksum = {0xee, 0xee, 0x02, 0, 0, 0, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
blockMapItem = (BlockMapItem)
{
.reference = 0,
.superBlockSize = 6,
.offset = 4,
.size = 5,
.block = 0,
.checksum = {0xee, 0xee, 0x03, 0, 0, 0, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
blockMapItem = (BlockMapItem)
{
.reference = 1,
.superBlockSize = 3,
.offset = 0,
.size = 99,
.block = 0,
.checksum = {0xee, 0xee, 0x04, 0, 0, 0, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
blockMapItem = (BlockMapItem)
{
.reference = 0,
.superBlockSize = 6,
.offset = 4,
.size = 5,
.block = 3,
.checksum = {0xee, 0xee, 0x05, 0, 0, 0, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
blockMapItem = (BlockMapItem)
{
.reference = 2,
.superBlockSize = 2,
.offset = 0,
.size = 1,
.block = 0,
.checksum = {0xee, 0xee, 0x06, 0, 0, 0, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
blockMapItem = (BlockMapItem)
{
.reference = 0,
.superBlockSize = 6,
.offset = 4,
.size = 5,
.block = 5,
.checksum = {0xee, 0xee, 0x07, 0, 0, 0, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
blockMapItem = (BlockMapItem)
{
.reference = 0,
.superBlockSize = 2,
.offset = 9,
.size = 6,
.block = 0,
.checksum = {0xee, 0xee, 0x08, 0, 0, 0, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
blockMapItem = (BlockMapItem)
{
.reference = 1,
.superBlockSize = 3,
.offset = 99,
.size = 1,
.block = 1,
.checksum = {0xee, 0xee, 0x09, 0, 0, 0, 0xff, 0xff},
};
TEST_RESULT_VOID(blockMapAdd(blockMap, &blockMapItem), "add");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("write unequal block map");
buffer = bufNew(256);
write = ioBufferWriteNewOpen(buffer);
TEST_RESULT_VOID(blockMapWrite(blockMap, write, 3, 8), "save");
ioWriteClose(write);
TEST_RESULT_STR_Z(
strNewEncode(encodingHex, buffer),
"00" // Version 0
"00" // reference 0
"22" // size 4
"04" // super block size 6
"eeee01000000ffff" // checksum
"eeee02000000ffff" // checksum
"15" // size 5
"00" // block total 1
"eeee03000000ffff" // checksum
"08" // reference 1
"e10b" // size 99
"eeee04000000ffff" // checksum
"06" // reference 0
"01" // block total 1
"02" // block 3
"eeee05000000ffff" // checksum
"10" // reference 2
"3b01" // size 1
"02" // super block size 2
"eeee06000000ffff" // checksum
"02" // reference 0
"01" // block total 1
"01" // block 5
"eeee07000000ffff" // checksum
"13" // size 6
"0102" // size 2
"eeee08000000ffff" // checksum
"09" // reference 1
"4d" // size 1
"01" // block total 1
"01" // block 1
"eeee09000000ffff", // checksum
"compare");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("read unequal block map");
bufferCompare = bufNew(256);
write = ioBufferWriteNewOpen(bufferCompare);
TEST_RESULT_VOID(blockMapWrite(blockMapNewRead(ioBufferReadNewOpen(buffer), 3, 8), write, 3, 8), "read and save");
ioWriteClose(write);
TEST_RESULT_STR(strNewEncode(encodingHex, bufferCompare), strNewEncode(encodingHex, buffer), "compare");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("unequal block delta");
TEST_RESULT_STR_Z(
hrnBlockDeltaRender(blockMapNewRead(ioBufferReadNewOpen(buffer), 3, 8), 3, 8),
"read {reference: 2, bundleId: 0, offset: 0, size: 1}\n"
" super block {max: 2, size: 1}\n"
" block {no: 0, offset: 15}\n"
"read {reference: 1, bundleId: 0, offset: 0, size: 100}\n"
" super block {max: 3, size: 99}\n"
" block {no: 0, offset: 9}\n"
" super block {max: 3, size: 1}\n"
" block {no: 1, offset: 24}\n"
"read {reference: 0, bundleId: 0, offset: 0, size: 15}\n"
" super block {max: 6, size: 4}\n"
" block {no: 0, offset: 0}\n"
" block {no: 1, offset: 3}\n"
" super block {max: 6, size: 5}\n"
" block {no: 0, offset: 6}\n"
" block {no: 3, offset: 12}\n"
" block {no: 5, offset: 18}\n"
" super block {max: 2, size: 6}\n"
" block {no: 0, offset: 21}\n",
"check delta");
}
// *****************************************************************************************************************************
if (testBegin("BlockIncr"))
{
TEST_TITLE("block incremental config map");
// Configure repo2 rather than repo1 so the index in the option name must come from the repo key
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgKeyRawZ(argList, cfgOptRepoPath, 2, TEST_PATH "/repo");
hrnCfgArgKeyRawZ(argList, cfgOptRepoRetentionFull, 2, "1");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
HRN_CFG_LOAD(cfgCmdBackup, argList);
TEST_ERROR(
backupBlockIncrMapSize(cfgOptRepoBlockSizeMap, 0, STRDEF("0")), OptionInvalidValueError,
"'0' is not valid for 'repo2-block-size-map' option");
TEST_ERROR(
backupBlockIncrMapSize(cfgOptRepoBlockSizeMap, 0, STRDEF("Z")), OptionInvalidValueError,
"'Z' is not valid for 'repo2-block-size-map' option");
TEST_ERROR(
backupBlockIncrMapSize(cfgOptRepoBlockSizeMap, 0, STRDEF("5GiB")), OptionInvalidValueError,
"'5GiB' is not valid for 'repo2-block-size-map' option");
TEST_ERROR(
backupBlockIncrMapChecksumSize(cfgOptRepoBlockChecksumSizeMap, 0, VARSTRDEF("Z")), OptionInvalidValueError,
"'Z' is not valid for 'repo2-block-checksum-size-map' option");
TEST_ERROR(
backupBlockIncrMapChecksumSize(cfgOptRepoBlockChecksumSizeMap, 0, VARSTRDEF("5")), OptionInvalidValueError,
"'5' is not valid for 'repo2-block-checksum-size-map' option");
// A key of the checksum size map is reported against the checksum size map rather than the size map
hrnCfgArgKeyRawBool(argList, cfgOptRepoBundle, 2, true);
hrnCfgArgKeyRawBool(argList, cfgOptRepoBlock, 2, true);
hrnCfgArgKeyRawZ(argList, cfgOptRepoBlockChecksumSizeMap, 2, "Z=16");
HRN_CFG_LOAD(cfgCmdBackup, argList);
TEST_ERROR(
backupBlockIncrMap(), OptionInvalidValueError, "'Z' is not valid for 'repo2-block-checksum-size-map' option");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("full backup with zero block");
ioBufferSizeSet(2);
const Buffer *source = BUFSTRZ("");
Buffer *destination = bufNew(256);
IoWrite *write = ioBufferWriteNew(destination);
TEST_RESULT_VOID(
ioFilterGroupAdd(ioWriteFilterGroup(write), blockIncrNew(3, 3, 6, 0, 0, 0, NULL, NULL, NULL)), "block incr");
TEST_RESULT_VOID(ioWriteOpen(write), "open");
TEST_RESULT_VOID(ioWrite(write, source), "write");
TEST_RESULT_VOID(ioWriteClose(write), "close");
TEST_RESULT_UINT(pckReadU64P(ioFilterGroupResultP(ioWriteFilterGroup(write), BLOCK_INCR_FILTER_TYPE)), 0, "compare");
TEST_RESULT_STR_Z(strNewEncode(encodingHex, destination), "", "compare");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("full backup with partial block");
source = BUFSTRZ("12");
destination = bufNew(256);
write = ioBufferWriteNew(destination);
TEST_RESULT_VOID(
ioFilterGroupAdd(ioWriteFilterGroup(write), blockIncrNew(3, 3, 8, 0, 0, 0, NULL, NULL, NULL)), "block incr");
TEST_RESULT_VOID(ioWriteOpen(write), "open");
TEST_RESULT_VOID(ioWrite(write, source), "write");
TEST_RESULT_VOID(ioWriteClose(write), "close");
uint64_t mapSize;
TEST_ASSIGN(mapSize, pckReadU64P(ioFilterGroupResultP(ioWriteFilterGroup(write), BLOCK_INCR_FILTER_TYPE)), "map size");
TEST_RESULT_UINT(mapSize, 13, "map size");
TEST_RESULT_STR_Z(
strNewEncode(encodingHex, BUF(bufPtr(destination), bufUsed(destination) - (size_t)mapSize)),
"3132", // block 0
"block list");
const Buffer *map = BUF(bufPtr(destination) + (bufUsed(destination) - (size_t)mapSize), (size_t)mapSize);
TEST_RESULT_STR_Z(
hrnBlockDeltaRender(blockMapNewRead(ioBufferReadNewOpen(map), 3, 8), 3, 8),
"read {reference: 0, bundleId: 0, offset: 0, size: 2}\n"
" super block {max: 2, size: 2}\n"
" block {no: 0, offset: 0}\n",
"check delta");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("full backup");
source = BUFSTRZ("ABCXYZ123");
destination = bufNew(256);
write = ioBufferWriteNew(destination);
TEST_RESULT_VOID(
ioFilterGroupAdd(ioWriteFilterGroup(write), ioBufferNew()), "buffer to force internal buffer size");
TEST_RESULT_VOID(
ioFilterGroupAdd(
ioWriteFilterGroup(write),
blockIncrNewPack(ioFilterParamList(blockIncrNew(2, 3, 8, 2, 4, 5, NULL, NULL, NULL)))),
"block incr");
TEST_RESULT_VOID(ioWriteOpen(write), "open");
TEST_RESULT_VOID(ioWrite(write, source), "write");
TEST_RESULT_VOID(ioWriteClose(write), "close");
TEST_ASSIGN(mapSize, pckReadU64P(ioFilterGroupResultP(ioWriteFilterGroup(write), BLOCK_INCR_FILTER_TYPE)), "map size");
TEST_RESULT_UINT(mapSize, 31, "map size");
TEST_RESULT_STR_Z(
strNewEncode(encodingHex, BUF(bufPtr(destination), bufUsed(destination) - (size_t)mapSize)),
"414243" // block 0
"58595a" // block 1
"313233", // block 2
"block list");
map = BUF(bufPtr(destination) + (bufUsed(destination) - (size_t)mapSize), (size_t)mapSize);
TEST_RESULT_STR_Z(
hrnBlockDeltaRender(blockMapNewRead(ioBufferReadNewOpen(map), 3, 8), 3, 8),
"read {reference: 2, bundleId: 4, offset: 5, size: 9}\n"
" super block {max: 3, size: 3}\n"
" block {no: 0, offset: 0}\n"
" super block {max: 3, size: 3}\n"
" block {no: 0, offset: 3}\n"
" super block {max: 3, size: 3}\n"
" block {no: 0, offset: 6}\n",
"check delta");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("diff/incr backup");
ioBufferSizeSet(3);
source = BUFSTRZ("ACCXYZ123@");
destination = bufNew(256);
write = ioBufferWriteNew(destination);
TEST_RESULT_VOID(
ioFilterGroupAdd(ioWriteFilterGroup(write), ioBufferNew()), "buffer to force internal buffer size");
TEST_RESULT_VOID(
ioFilterGroupAdd(
ioWriteFilterGroup(write), blockIncrNewPack(ioFilterParamList(blockIncrNew(3, 3, 8, 3, 0, 0, map, NULL, NULL)))),
"block incr");
TEST_RESULT_VOID(ioWriteOpen(write), "open");
TEST_RESULT_VOID(ioWrite(write, source), "write");
TEST_RESULT_VOID(ioWriteClose(write), "close");
TEST_ASSIGN(mapSize, pckReadU64P(ioFilterGroupResultP(ioWriteFilterGroup(write), BLOCK_INCR_FILTER_TYPE)), "map size");
TEST_RESULT_UINT(mapSize, 44, "map size");
TEST_RESULT_STR_Z(
strNewEncode(encodingHex, BUF(bufPtr(destination), bufUsed(destination) - (size_t)mapSize)),
"414343" // block 0
"40", // block 3
"block list");
map = BUF(bufPtr(destination) + (bufUsed(destination) - (size_t)mapSize), (size_t)mapSize);
TEST_RESULT_STR_Z(
hrnBlockDeltaRender(blockMapNewRead(ioBufferReadNewOpen(map), 3, 8), 3, 8),
"read {reference: 3, bundleId: 0, offset: 0, size: 4}\n"
" super block {max: 3, size: 3}\n"
" block {no: 0, offset: 0}\n"
" super block {max: 1, size: 1}\n"
" block {no: 0, offset: 9}\n"
"read {reference: 2, bundleId: 4, offset: 8, size: 6}\n"
" super block {max: 3, size: 3}\n"
" block {no: 0, offset: 3}\n"
" super block {max: 3, size: 3}\n"
" block {no: 0, offset: 6}\n",
"check delta");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("diff/incr backup with identical data");
ioBufferSizeSet(3);
source = BUFSTRZ("ACCXYZ123@");
destination = bufNew(256);
write = ioBufferWriteNew(destination);
TEST_RESULT_VOID(
ioFilterGroupAdd(ioWriteFilterGroup(write), ioBufferNew()), "buffer to force internal buffer size");
TEST_RESULT_VOID(
ioFilterGroupAdd(
ioWriteFilterGroup(write), blockIncrNewPack(ioFilterParamList(blockIncrNew(3, 3, 8, 3, 0, 0, map, NULL, NULL)))),
"block incr");
TEST_RESULT_VOID(ioWriteOpen(write), "open");
TEST_RESULT_VOID(ioWrite(write, source), "write");
TEST_RESULT_VOID(ioWriteClose(write), "close");
TEST_ASSIGN(mapSize, pckReadU64P(ioFilterGroupResultP(ioWriteFilterGroup(write), BLOCK_INCR_FILTER_TYPE)), "map size");
TEST_RESULT_UINT(mapSize, 0, "map size is zero");
TEST_RESULT_UINT(bufUsed(destination), 0, "repo size is zero");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("full backup with larger super block");
ioBufferSizeSet(2);
source = BUFSTRZ("ABCXYZ123");
destination = bufNew(256);
write = ioBufferWriteNew(destination);
TEST_RESULT_VOID(
ioFilterGroupAdd(ioWriteFilterGroup(write), ioBufferNew()), "buffer to force internal buffer size");
TEST_RESULT_VOID(
ioFilterGroupAdd(
ioWriteFilterGroup(write), blockIncrNewPack(ioFilterParamList(blockIncrNew(6, 3, 8, 2, 4, 5, NULL, NULL, NULL)))),
"block incr");
TEST_RESULT_VOID(ioWriteOpen(write), "open");
TEST_RESULT_VOID(ioWrite(write, source), "write");
TEST_RESULT_VOID(ioWriteClose(write), "close");
TEST_ASSIGN(mapSize, pckReadU64P(ioFilterGroupResultP(ioWriteFilterGroup(write), BLOCK_INCR_FILTER_TYPE)), "map size");
TEST_RESULT_UINT(mapSize, 32, "map size");
TEST_RESULT_STR_Z(
strNewEncode(encodingHex, BUF(bufPtr(destination), bufUsed(destination) - (size_t)mapSize)),
"414243" // super block 0 / block 0
"58595a" // super block 0 / block 1
"313233", // block 2
"block list");
map = BUF(bufPtr(destination) + (bufUsed(destination) - (size_t)mapSize), (size_t)mapSize);
TEST_RESULT_STR_Z(
hrnBlockDeltaRender(blockMapNewRead(ioBufferReadNewOpen(map), 3, 8), 3, 8),
"read {reference: 2, bundleId: 4, offset: 5, size: 9}\n"
" super block {max: 6, size: 6}\n"
" block {no: 0, offset: 0}\n"
" block {no: 1, offset: 3}\n"
" super block {max: 3, size: 3}\n"
" block {no: 0, offset: 6}\n",
"check delta");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("new filter from pack");
TEST_RESULT_VOID(
blockIncrNewPack(
ioFilterParamList(
blockIncrNew(
3, 3, 8, 2, 4, 5, NULL, compressFilterP(compressTypeGz, 1, .raw = true),
cipherBlockNewP(
cipherModeEncrypt,
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF(TEST_CIPHER_PASS), .digest = hashTypeSha256),
.header = cipherBlockHeaderNone)))),
"block incr pack");
}
// *****************************************************************************************************************************
if (testBegin("backupLabelCreate()"))
{
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
HRN_CFG_LOAD(cfgCmdBackup, argList);
time_t timestamp = 1575401652;
String *backupLabel = backupLabelFormat(backupTypeFull, NULL, timestamp);
const String *const olderBackupLabel = backupLabelFormat(backupTypeFull, NULL, timestamp - 3);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("assign label when no history");
HRN_STORAGE_PATH_CREATE(storageRepoWrite(), STORAGE_REPO_BACKUP "/backup.history/2019");
TEST_RESULT_STR(backupLabelCreate(backupTypeFull, NULL, timestamp), backupLabel, "create label");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("assign label when history is older");
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(),
zNewFmt(
STORAGE_REPO_BACKUP "/backup.history/2019/%s.manifest.gz",
strZ(backupLabelFormat(backupTypeFull, NULL, timestamp - 4))));
TEST_RESULT_STR(backupLabelCreate(backupTypeFull, NULL, timestamp), backupLabel, "create label");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("assign label when backup is older");
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s", strZ(olderBackupLabel)));
TEST_RESULT_STR(backupLabelCreate(backupTypeFull, NULL, timestamp), backupLabel, "create label");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("differential and incremental backups read history related to full backup they are created on (full only)");
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(),
zNewFmt(
STORAGE_REPO_BACKUP "/backup.history/2019/%s.manifest.gz",
strZ(backupLabelFormat(backupTypeFull, NULL, timestamp - 1))));
// Add a full backup that sorts after the prior backup, e.g. a backup that has been removed but is still returned by the
// repository listing. It must not be considered when generating a diff/incr label.
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(),
zNewFmt(STORAGE_REPO_BACKUP "/%s", strZ(backupLabelFormat(backupTypeFull, NULL, timestamp - 2))));
TEST_RESULT_STR(
backupLabelCreate(backupTypeDiff, olderBackupLabel, timestamp),
backupLabelFormat(backupTypeDiff, olderBackupLabel, timestamp),
"create label");
TEST_RESULT_STR(
backupLabelCreate(backupTypeIncr, olderBackupLabel, timestamp),
backupLabelFormat(backupTypeIncr, olderBackupLabel, timestamp),
"create label");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("differential and incremental backups read history related to full backup they are created on (diff exists)");
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(),
zNewFmt(
STORAGE_REPO_BACKUP "/backup.history/2019/%s.manifest.gz",
strZ(backupLabelFormat(backupTypeDiff, olderBackupLabel, timestamp - 2))));
TEST_RESULT_STR(
backupLabelCreate(backupTypeDiff, olderBackupLabel, timestamp),
backupLabelFormat(backupTypeDiff, olderBackupLabel, timestamp),
"create label");
TEST_RESULT_STR(
backupLabelCreate(backupTypeIncr, olderBackupLabel, timestamp),
backupLabelFormat(backupTypeIncr, olderBackupLabel, timestamp),
"create label");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("differential and incremental backups read history related to full backup they are created on (diff and incr)");
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(),
zNewFmt(
STORAGE_REPO_BACKUP "/backup.history/2019/%s.manifest.gz",
strZ(backupLabelFormat(backupTypeIncr, olderBackupLabel, timestamp - 1))));
TEST_RESULT_STR(
backupLabelCreate(backupTypeDiff, olderBackupLabel, timestamp),
backupLabelFormat(backupTypeDiff, olderBackupLabel, timestamp),
"create label");
TEST_RESULT_STR(
backupLabelCreate(backupTypeIncr, olderBackupLabel, timestamp),
backupLabelFormat(backupTypeIncr, olderBackupLabel, timestamp),
"create label");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("differential and incremental backups read history related to full backup they are created on (skew)");
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(),
zNewFmt(
STORAGE_REPO_BACKUP "/backup.history/2019/%s.manifest.gz",
strZ(backupLabelFormat(backupTypeIncr, olderBackupLabel, timestamp + 3))));
TEST_ERROR(
backupLabelCreate(backupTypeDiff, olderBackupLabel, timestamp), ClockError,
"new backup label '20191203-193409F_20191203-193413D' is not later "
"than latest backup label '20191203-193409F_20191203-193415I'\n"
"HINT: has the timezone changed?\n"
"HINT: is there clock skew?");
TEST_ERROR(
backupLabelCreate(backupTypeIncr, olderBackupLabel, timestamp), ClockError,
"new backup label '20191203-193409F_20191203-193413I' is not later "
"than latest backup label '20191203-193409F_20191203-193415I'\n"
"HINT: has the timezone changed?\n"
"HINT: is there clock skew?");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("advance time when backup is same");
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s", strZ(backupLabelFormat(backupTypeFull, NULL, timestamp))));
TEST_RESULT_STR_Z(backupLabelCreate(backupTypeFull, NULL, timestamp), "20191203-193413F", "create label");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("error when new label is in the past even with advanced time");
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s", strZ(backupLabelFormat(backupTypeFull, NULL, timestamp + 1))));
TEST_ERROR(
backupLabelCreate(backupTypeFull, NULL, timestamp), ClockError,
"new backup label '20191203-193413F' is not later than latest backup label '20191203-193413F'\n"
"HINT: has the timezone changed?\n"
"HINT: is there clock skew?");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("error when new label is in the past even with advanced time (from history)");
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(),
zNewFmt(
STORAGE_REPO_BACKUP "/backup.history/2019/%s.manifest.gz",
strZ(backupLabelFormat(backupTypeFull, NULL, timestamp + 3600))));
TEST_ERROR(
backupLabelCreate(backupTypeFull, NULL, timestamp), ClockError,
"new backup label '20191203-193413F' is not later than latest backup label '20191203-203412F'\n"
"HINT: has the timezone changed?\n"
"HINT: is there clock skew?");
}
// *****************************************************************************************************************************
if (testBegin("backupInit()"))
{
// Set log level to detail
harnessLogLevelSet(logLevelDetail);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("warn and reset when backup from standby used in offline mode");
// Create pg_control
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_96);
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawBool(argList, cfgOptBackupStandby, true);
hrnCfgArgRawBool(argList, cfgOptOnline, false);
HRN_CFG_LOAD(cfgCmdBackup, argList);
TEST_RESULT_VOID(
backupInit(
infoBackupNew(
PG_VERSION_96, HRN_PG_SYSTEMID_96, hrnPgCatalogVersion(PG_VERSION_96), REPOSITORY_FORMAT_DEFAULT, NULL)),
"backup init");
TEST_RESULT_LOG(
"P00 WARN: option backup-standby is enabled but backup is offline - backups will be performed from the primary");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("error when pg_control does not match stanza");
// Create pg_control
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_10);
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawBool(argList, cfgOptOnline, false);
HRN_CFG_LOAD(cfgCmdBackup, argList);
TEST_ERROR(
backupInit(
infoBackupNew(
PG_VERSION_11, HRN_PG_SYSTEMID_11, hrnPgCatalogVersion(PG_VERSION_11), REPOSITORY_FORMAT_DEFAULT, NULL)),
BackupMismatchError,
"PostgreSQL version 10, system-id " HRN_PG_SYSTEMID_10_Z " do not match stanza version 11, system-id"
" " HRN_PG_SYSTEMID_11_Z "\n"
"HINT: is this the correct stanza?");
TEST_ERROR(
backupInit(
infoBackupNew(
PG_VERSION_10, HRN_PG_SYSTEMID_11, hrnPgCatalogVersion(PG_VERSION_10), REPOSITORY_FORMAT_DEFAULT, NULL)),
BackupMismatchError,
"PostgreSQL version 10, system-id " HRN_PG_SYSTEMID_10_Z " do not match stanza version 10, system-id"
" " HRN_PG_SYSTEMID_11_Z "\n"
"HINT: is this the correct stanza?");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("reset checksum-page when the cluster does not have checksums enabled");
// Create pg_control
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_18);
// Create stanza
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawBool(argList, cfgOptOnline, false);
HRN_CFG_LOAD(cfgCmdStanzaCreate, argList);
cmdStanzaCreate();
TEST_RESULT_LOG("P00 INFO: stanza-create for stanza 'test1' on repo1");
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawBool(argList, cfgOptChecksumPage, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
HRN_PQ_SCRIPT_SET(
// Connect to primary
HRN_PQ_SCRIPT_OPEN(1, "dbname='postgres' port=5432", PG_VERSION_18, TEST_PATH "/pg1", false, NULL, NULL));
TEST_RESULT_VOID(
dbFree(
backupInit(
infoBackupNew(
PG_VERSION_18, HRN_PG_SYSTEMID_18, hrnPgCatalogVersion(PG_VERSION_18), REPOSITORY_FORMAT_DEFAULT,
NULL))->dbPrimary),
"backup init");
TEST_RESULT_BOOL(cfgOptionBool(cfgOptChecksumPage), false, "check checksum-page");
TEST_RESULT_LOG(
"P00 WARN: checksum-page option set to true but checksums are not enabled on the cluster, resetting to false");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("ok if cluster checksums are enabled and checksum-page is any value");
// Create pg_control with page checksums
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_18, .pageChecksumVersion = 1);
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawBool(argList, cfgOptChecksumPage, false);
HRN_CFG_LOAD(cfgCmdBackup, argList);
HRN_PQ_SCRIPT_SET(
// Connect to primary
HRN_PQ_SCRIPT_OPEN(1, "dbname='postgres' port=5432", PG_VERSION_18, TEST_PATH "/pg1", false, NULL, NULL));
TEST_RESULT_VOID(
dbFree(
backupInit(
infoBackupNew(
PG_VERSION_18, HRN_PG_SYSTEMID_18, hrnPgCatalogVersion(PG_VERSION_18), REPOSITORY_FORMAT_DEFAULT,
NULL))->dbPrimary),
"backup init");
TEST_RESULT_BOOL(cfgOptionBool(cfgOptChecksumPage), false, "check checksum-page");
// Create pg_control without page checksums
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_18);
HRN_PQ_SCRIPT_SET(
// Connect to primary
HRN_PQ_SCRIPT_OPEN(1, "dbname='postgres' port=5432", PG_VERSION_18, TEST_PATH "/pg1", false, NULL, NULL));
TEST_RESULT_VOID(
dbFree(
backupInit(
infoBackupNew(
PG_VERSION_18, HRN_PG_SYSTEMID_18, hrnPgCatalogVersion(PG_VERSION_18), REPOSITORY_FORMAT_DEFAULT,
NULL))->dbPrimary),
"backup init");
TEST_RESULT_BOOL(cfgOptionBool(cfgOptChecksumPage), false, "check checksum-page");
}
// *****************************************************************************************************************************
if (testBegin("backupTime()"))
{
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("sleep retries and stall error");
// Create pg_control
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_18);
// Create stanza
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawBool(argList, cfgOptOnline, false);
HRN_CFG_LOAD(cfgCmdStanzaCreate, argList);
cmdStanzaCreate();
TEST_RESULT_LOG("P00 INFO: stanza-create for stanza 'test1' on repo1");
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
HRN_CFG_LOAD(cfgCmdBackup, argList);
HRN_PQ_SCRIPT_SET(
// Connect to primary
HRN_PQ_SCRIPT_OPEN(1, "dbname='postgres' port=5432", PG_VERSION_18, TEST_PATH "/pg1", false, NULL, NULL),
// Advance the time slowly to force retries
HRN_PQ_SCRIPT_TIME_QUERY(1, 1575392588998),
HRN_PQ_SCRIPT_TIME_QUERY(1, 1575392588999),
HRN_PQ_SCRIPT_TIME_QUERY(1, 1575392589001),
// Stall time to force an error
HRN_PQ_SCRIPT_TIME_QUERY(1, 1575392589998),
HRN_PQ_SCRIPT_TIME_QUERY(1, 1575392589997),
HRN_PQ_SCRIPT_TIME_QUERY(1, 1575392589998),
HRN_PQ_SCRIPT_TIME_QUERY(1, 1575392589999));
BackupData *backupData = backupInit(
infoBackupNew(PG_VERSION_18, HRN_PG_SYSTEMID_18, hrnPgCatalogVersion(PG_VERSION_18), REPOSITORY_FORMAT_DEFAULT, NULL));
TEST_RESULT_INT(backupTime(backupData, true), 1575392588, "multiple tries for sleep");
TEST_ERROR(backupTime(backupData, true), KernelError, "PostgreSQL clock has not advanced to the next second after 3 tries");
dbFree(backupData->dbPrimary);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("info files that do not agree on repository format");
// Migrate archive.info only, which is the state an upgrade interrupted between the two saves leaves behind
InfoArchive *const infoArchive = infoArchiveLoadFile(storageRepo(), INFO_ARCHIVE_PATH_FILE_STR, cipherSpecNewNone());
infoArchiveFormatSet(infoArchive, REPOSITORY_FORMAT_6);
infoArchiveSaveFile(infoArchive, storageRepoWrite(), INFO_ARCHIVE_PATH_FILE_STR, cipherSpecNewNone());
HRN_PQ_SCRIPT_SET(
// Connect to primary
HRN_PQ_SCRIPT_OPEN(1, "dbname='postgres' port=5432", PG_VERSION_18, TEST_PATH "/pg1", false, NULL, NULL));
TEST_ERROR(
backupInit(
infoBackupNew(
PG_VERSION_18, HRN_PG_SYSTEMID_18, hrnPgCatalogVersion(PG_VERSION_18), REPOSITORY_FORMAT_DEFAULT, NULL)),
FileInvalidError,
"backup info file and archive info file are at different repository formats\n"
"archive: format = 6\n"
"backup : format = 5\n"
"HINT: run stanza-upgrade with --repo1-format=6 to complete an interrupted upgrade.");
}
// *****************************************************************************************************************************
if (testBegin("backupResumeFind()"))
{
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, "/pg");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawBool(argList, cfgOptCompress, false);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("cannot resume when manifest and copy are missing");
HRN_STORAGE_PATH_CREATE(storageRepoWrite(), STORAGE_REPO_BACKUP "/20191003-105320F");
TEST_RESULT_PTR(backupResumeFind((Manifest *)1, cipherSpecNewNone()), NULL, "find resumable backup");
TEST_RESULT_LOG(
"P00 WARN: backup '20191003-105320F' cannot be resumed: partially deleted by prior resume or invalid");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("cannot resume when resume is disabled");
HRN_STORAGE_PATH_CREATE(storageRepoWrite(), STORAGE_REPO_BACKUP "/20191003-105320F");
cfgOptionSet(cfgOptResume, cfgSourceParam, BOOL_FALSE_VAR);
HRN_STORAGE_PUT_EMPTY(storageRepoWrite(), STORAGE_REPO_BACKUP "/20191003-105320F/" BACKUP_MANIFEST_FILE INFO_COPY_EXT);
TEST_RESULT_PTR(backupResumeFind((Manifest *)1, cipherSpecNewNone()), NULL, "find resumable backup");
TEST_RESULT_LOG("P00 INFO: backup '20191003-105320F' cannot be resumed: resume is disabled");
TEST_STORAGE_LIST_EMPTY(storageRepo(), STORAGE_REPO_BACKUP, .comment = "check backup path removed");
cfgOptionSet(cfgOptResume, cfgSourceParam, BOOL_TRUE_VAR);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("cannot resume when error on manifest load");
Manifest *manifest = NULL;
OBJ_NEW_BASE_BEGIN(Manifest, .childQty = MEM_CONTEXT_QTY_MAX)
{
manifest = manifestNewInternal();
manifest->pub.info = infoNew(REPOSITORY_FORMAT_DEFAULT, NULL);
manifest->pub.data.backupType = backupTypeFull;
manifest->pub.data.backrestVersion = strNewZ("BOGUS");
}
OBJ_NEW_END();
HRN_STORAGE_PUT_Z(storageRepoWrite(), STORAGE_REPO_BACKUP "/20191003-105320F/" BACKUP_MANIFEST_FILE INFO_COPY_EXT, "X");
TEST_RESULT_PTR(backupResumeFind(manifest, cipherSpecNewNone()), NULL, "find resumable backup");
TEST_RESULT_LOG(
"P00 WARN: backup '20191003-105320F' cannot be resumed: unable to read"
" <REPO:BACKUP>/20191003-105320F/backup.manifest.copy");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("cannot resume when pgBackRest version has changed");
Manifest *manifestResume = NULL;
OBJ_NEW_BASE_BEGIN(Manifest, .childQty = MEM_CONTEXT_QTY_MAX)
{
manifestResume = manifestNewInternal();
manifestResume->pub.info = infoNew(REPOSITORY_FORMAT_DEFAULT, NULL);
manifestResume->pub.data.backupType = backupTypeFull;
manifestResume->pub.data.backupLabel = strNewZ("20191003-105320F");
manifestResume->pub.data.backupTimestampStart = 1482182860;
manifestResume->pub.data.pgVersion = PG_VERSION_12;
}
OBJ_NEW_END();
HRN_MANIFEST_TARGET_ADD(manifestResume, .name = MANIFEST_TARGET_PGDATA, .path = "/pg");
HRN_MANIFEST_PATH_ADD(manifestResume, .name = MANIFEST_TARGET_PGDATA);
HRN_MANIFEST_FILE_ADD(manifestResume, .name = "pg_data/" PG_FILE_PGVERSION);
manifestSave(
manifestResume,
storageWriteIo(
storageNewWriteP(
storageRepoWrite(), STRDEF(STORAGE_REPO_BACKUP "/20191003-105320F/" BACKUP_MANIFEST_FILE INFO_COPY_EXT))));
TEST_RESULT_PTR(backupResumeFind(manifest, cipherSpecNewNone()), NULL, "find resumable backup");
TEST_RESULT_LOG(
"P00 WARN: backup '20191003-105320F' cannot be resumed:"
" new pgBackRest version 'BOGUS' does not match resumable pgBackRest version '" PROJECT_VERSION "'");
TEST_STORAGE_LIST_EMPTY(storageRepo(), STORAGE_REPO_BACKUP, .comment = "check backup path removed");
manifest->pub.data.backrestVersion = STRDEF(PROJECT_VERSION);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("cannot resume when repository format has changed");
infoFormatSet(manifestResume->pub.info, REPOSITORY_FORMAT_6);
manifestSave(
manifestResume,
storageWriteIo(
storageNewWriteP(
storageRepoWrite(), STRDEF(STORAGE_REPO_BACKUP "/20191003-105320F/" BACKUP_MANIFEST_FILE INFO_COPY_EXT))));
TEST_RESULT_PTR(backupResumeFind(manifest, cipherSpecNewNone()), NULL, "find resumable backup");
TEST_RESULT_LOG(
"P00 WARN: backup '20191003-105320F' cannot be resumed:"
" new repository format 5 does not match resumable repository format 6");
TEST_STORAGE_LIST_EMPTY(storageRepo(), STORAGE_REPO_BACKUP, .comment = "check backup path removed");
infoFormatSet(manifestResume->pub.info, REPOSITORY_FORMAT_DEFAULT);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("cannot resume when backup labels do not match (resumable is null)");
manifest->pub.data.backupType = backupTypeFull;
manifest->pub.data.backupLabelPrior = STRDEF("20191003-105320F");
manifestSave(
manifestResume,
storageWriteIo(
storageNewWriteP(
storageRepoWrite(), STRDEF(STORAGE_REPO_BACKUP "/20191003-105320F/" BACKUP_MANIFEST_FILE INFO_COPY_EXT))));
TEST_RESULT_PTR(backupResumeFind(manifest, cipherSpecNewNone()), NULL, "find resumable backup");
TEST_RESULT_LOG(
"P00 WARN: backup '20191003-105320F' cannot be resumed:"
" new prior backup label '<undef>' does not match resumable prior backup label '20191003-105320F'");
TEST_STORAGE_LIST_EMPTY(storageRepo(), STORAGE_REPO_BACKUP, .comment = "check backup path removed");
manifest->pub.data.backupLabelPrior = NULL;
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("cannot resume when backup labels do not match (new is null)");
manifest->pub.data.backupType = backupTypeFull;
manifestResume->pub.data.backupLabelPrior = STRDEF("20191003-105320F");
manifestSave(
manifestResume,
storageWriteIo(
storageNewWriteP(
storageRepoWrite(), STRDEF(STORAGE_REPO_BACKUP "/20191003-105320F/" BACKUP_MANIFEST_FILE INFO_COPY_EXT))));
TEST_RESULT_PTR(backupResumeFind(manifest, cipherSpecNewNone()), NULL, "find resumable backup");
TEST_RESULT_LOG(
"P00 WARN: backup '20191003-105320F' cannot be resumed:"
" new prior backup label '20191003-105320F' does not match resumable prior backup label '<undef>'");
TEST_STORAGE_LIST_EMPTY(storageRepo(), STORAGE_REPO_BACKUP, .comment = "check backup path removed");
manifestResume->pub.data.backupLabelPrior = NULL;
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("cannot resume when compression does not match");
manifestResume->pub.data.backupOptionCompressType = compressTypeGz;
manifestSave(
manifestResume,
storageWriteIo(
storageNewWriteP(
storageRepoWrite(), STRDEF(STORAGE_REPO_BACKUP "/20191003-105320F/" BACKUP_MANIFEST_FILE INFO_COPY_EXT))));
TEST_RESULT_PTR(backupResumeFind(manifest, cipherSpecNewNone()), NULL, "find resumable backup");
TEST_RESULT_LOG(
"P00 WARN: backup '20191003-105320F' cannot be resumed:"
" new compression 'none' does not match resumable compression 'gz'");
TEST_STORAGE_LIST_EMPTY(storageRepo(), STORAGE_REPO_BACKUP, .comment = "check backup path removed");
manifestResume->pub.data.backupOptionCompressType = compressTypeNone;
}
// *****************************************************************************************************************************
if (testBegin("backupJobResult()"))
{
// Set log level to detail
harnessLogLevelSet(logLevelDetail);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("report job error");
ProtocolParallelJob *job = protocolParallelJobNew(VARSTRDEF("key"), strIdFromZ("x"), NULL);
protocolParallelJobErrorSet(job, errorTypeCode(&AssertError), STRDEF("error message"));
unsigned int currentPercentComplete = 0;
unsigned int warningTotal = 0;
TEST_ERROR(
backupJobResult(
(Manifest *)1, NULL, storageTest, strLstNew(), job, pgPageSize8, &warningTotal, 0, NULL, &currentPercentComplete),
AssertError, "error message");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("report host/100% progress on noop result");
// Create job that skips file
job = protocolParallelJobNew(VARSTRDEF("pg_data/test"), strIdFromZ("x"), NULL);
PackWrite *const resultPack = protocolPackNew();
pckWriteStrP(resultPack, STRDEF("pg_data/test"));
pckWriteU32P(resultPack, backupCopyResultNoOp);
// No more fields need to be written since noop will ignore them anyway
pckWriteEndP(resultPack);
protocolParallelJobResultSet(job, pckReadNew(pckWriteResult(resultPack)));
// Create manifest with file
Manifest *manifest = NULL;
OBJ_NEW_BASE_BEGIN(Manifest, .childQty = MEM_CONTEXT_QTY_MAX)
{
manifest = manifestNewInternal();
manifest->pub.data.backupTimestampStart = 1482182860;
HRN_MANIFEST_FILE_ADD(manifest, .name = "pg_data/test");
}
OBJ_NEW_END();
uint64_t sizeProgress = 0;
currentPercentComplete = 4567;
lockInit(TEST_PATH_STR, cfgOptionStr(cfgOptExecId));
TEST_RESULT_VOID(cmdLockAcquireP(), "acquire backup lock");
TEST_RESULT_VOID(
backupJobResult(
manifest, STRDEF("host"), storageTest, strLstNew(), job, pgPageSize8, &warningTotal, 0, &sizeProgress,
&currentPercentComplete),
"log noop result");
TEST_RESULT_VOID(cmdLockReleaseP(), "release backup lock");
TEST_RESULT_LOG("P00 DETAIL: match file from prior backup host:" TEST_PATH "/test (0B, 100.00%)");
}
// *****************************************************************************************************************************
if (testBegin("backupFileComparator()"))
{
// Files sharing a prior map file always reach the comparator pre-sorted ascending by prior map offset (the process queue
// orders bundled files by ascending prior bundle offset), so whether the comparator is ever asked to order such a pair in
// descending order depends on the sort algorithm. The qsort our tests run against never compares pre-sorted input that way
// so a backup cannot cover the descending case, but a different sort algorithm in the field could, so exercise it directly.
const BackupFile file1 =
{
.pgFile = STRDEF("base/1/1"), .blockIncrSize = 1, .blockIncrMapPriorFile = STRDEF("bundle/1"),
.blockIncrMapPriorOffset = 100,
};
const BackupFile file2 =
{
.pgFile = STRDEF("base/1/2"), .blockIncrSize = 1, .blockIncrMapPriorFile = STRDEF("bundle/1"),
.blockIncrMapPriorOffset = 50,
};
TEST_RESULT_INT(backupFileComparator(&file1, &file2), 1, "larger prior map offset sorts after");
TEST_RESULT_INT(backupFileComparator(&file2, &file1), -1, "smaller prior map offset sorts before");
}
// Offline tests should only be used to test offline functionality and errors easily tested in offline mode
// *****************************************************************************************************************************
if (testBegin("cmdBackup() offline"))
{
// Set log level to detail
harnessLogLevelSet(logLevelDetail);
// Replace backup labels since the times are not deterministic
hrnLogReplaceAdd("[0-9]{8}-[0-9]{6}F_[0-9]{8}-[0-9]{6}I", NULL, "INCR", true);
hrnLogReplaceAdd("[0-9]{8}-[0-9]{6}F_[0-9]{8}-[0-9]{6}D", NULL, "DIFF", true);
hrnLogReplaceAdd("[0-9]{8}-[0-9]{6}F", NULL, "FULL", true);
// Create stanza
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawBool(argList, cfgOptOnline, false);
HRN_CFG_LOAD(cfgCmdStanzaCreate, argList);
// Create pg_control
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_18);
cmdStanzaCreate();
TEST_RESULT_LOG("P00 INFO: stanza-create for stanza 'test1' on repo1");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("error when pg appears to be running");
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawBool(argList, cfgOptOnline, false);
HRN_CFG_LOAD(cfgCmdBackup, argList);
HRN_STORAGE_PUT_Z(storagePgWrite(), PG_FILE_POSTMTRPID, "PID");
TEST_ERROR(
hrnCmdBackup(), PgRunningError,
"--no-online passed but " PG_FILE_POSTMTRPID " exists - looks like " PG_NAME " is running. Shut down " PG_NAME " and"
" try again, or use --force.");
TEST_RESULT_LOG("P00 WARN: no prior backup exists, incr backup has been changed to full");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("offline full backup");
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawBool(argList, cfgOptOnline, false);
hrnCfgArgRawBool(argList, cfgOptCompress, false);
hrnCfgArgRawBool(argList, cfgOptForce, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
HRN_STORAGE_PUT_Z(storagePgWrite(), "postgresql.conf", "CONFIGSTUFF");
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG_FMT(
"P00 WARN: no prior backup exists, incr backup has been changed to full\n"
"P00 WARN: --no-online passed and " PG_FILE_POSTMTRPID " exists but --force was passed so backup will continue though"
" it looks like " PG_NAME " is running and the backup will probably not be consistent\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (8KB, 99.87%%) checksum %s\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.conf (11B, 100.00%%) checksum"
" e3db315c260e79211b7b52587123b7aa060f30ab\n"
"P00 INFO: new backup label = [FULL-1]\n"
"P00 INFO: full backup size = 8KB, file total = 2",
TEST_64BIT() ?
(TEST_BIG_ENDIAN() ? "a73da1cfddf849880b4f6f0f4209ce71043f2ac1" : "807d4464138ef085e41844a9f488f067b3d46d46") :
"c09a7f1d2ff6131ec467e18b781970a86efd4e5b");
// Make pg no longer appear to be running
HRN_STORAGE_REMOVE(storagePgWrite(), PG_FILE_POSTMTRPID, .errorOnMissing = true);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("error when no files have changed");
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawBool(argList, cfgOptOnline, false);
hrnCfgArgRawBool(argList, cfgOptCompress, true);
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeDiff);
HRN_CFG_LOAD(cfgCmdBackup, argList);
TEST_ERROR(hrnCmdBackup(), FileMissingError, "no files have changed since the last backup - this seems unlikely");
TEST_RESULT_LOG(
"P00 INFO: last backup label = [FULL-1], version = " PROJECT_VERSION "\n"
"P00 WARN: diff backup cannot alter compress-type option to 'gz', reset to value in [FULL-1]");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("offline incr backup to test unresumable backup");
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawBool(argList, cfgOptOnline, false);
hrnCfgArgRawBool(argList, cfgOptCompress, false);
hrnCfgArgRawBool(argList, cfgOptChecksumPage, true);
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeIncr);
HRN_CFG_LOAD(cfgCmdBackup, argList);
HRN_STORAGE_PUT_Z(storagePgWrite(), PG_FILE_PGVERSION, "VER");
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: last backup label = [FULL-1], version = " PROJECT_VERSION "\n"
"P00 WARN: incr backup cannot alter 'checksum-page' option to 'true', reset to 'false' from [FULL-1]\n"
"P00 INFO: backup '[DIFF-1]' cannot be resumed: resume only valid for full backup\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (3B, 100.00%) checksum c8663c2525f44b6d9c687fbceb4aafc63ed8b451\n"
"P00 DETAIL: reference pg_data/global/pg_control to [FULL-1]\n"
"P00 DETAIL: reference pg_data/postgresql.conf to [FULL-1]\n"
"P00 INFO: new backup label = [INCR-1]\n"
"P00 INFO: incr backup size = 3B, file total = 3");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("offline diff backup to test prior backup must be full");
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawBool(argList, cfgOptOnline, false);
hrnCfgArgRawBool(argList, cfgOptCompress, false);
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeDiff);
HRN_CFG_LOAD(cfgCmdBackup, argList);
hrnSleepRemainder();
HRN_STORAGE_PUT_Z(storagePgWrite(), PG_FILE_PGVERSION, "VR2");
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: last backup label = [FULL-1], version = " PROJECT_VERSION "\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (3B, 100.00%) checksum 6f1894088c578e4f0b9888e8e8a997d93cbbc0c5\n"
"P00 DETAIL: reference pg_data/global/pg_control to [FULL-1]\n"
"P00 DETAIL: reference pg_data/postgresql.conf to [FULL-1]\n"
"P00 INFO: new backup label = [DIFF-2]\n"
"P00 INFO: diff backup size = 3B, file total = 3");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("only repo2 configured");
// Create stanza on a second repo
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgKeyRawZ(argList, cfgOptRepoPath, 2, TEST_PATH "/repo2");
hrnCfgArgKeyRawStrId(argList, cfgOptRepoCipherType, 2, cipherTypeAes256Cbc);
hrnCfgEnvKeyRawZ(cfgOptRepoCipherPass, 2, TEST_CIPHER_PASS);
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg1");
hrnCfgArgRawBool(argList, cfgOptOnline, false);
HRN_CFG_LOAD(cfgCmdStanzaCreate, argList);
cmdStanzaCreate();
TEST_RESULT_LOG("P00 INFO: stanza-create for stanza 'test1' on repo2");
// Set log level to warn
harnessLogLevelSet(logLevelWarn);
// With repo2 the only repo configured, ensure it is chosen by confirming diff is changed to full due to no prior backups
hrnCfgArgKeyRawZ(argList, cfgOptRepoRetentionFull, 2, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeDiff);
HRN_CFG_LOAD(cfgCmdBackup, argList);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG("P00 WARN: no prior backup exists, diff backup has been changed to full");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("multi-repo");
// Set log level to detail
harnessLogLevelSet(logLevelDetail);
// Add repo1 to the configuration
hrnCfgArgKeyRawZ(argList, cfgOptRepoPath, 1, TEST_PATH "/repo");
hrnCfgArgKeyRawZ(argList, cfgOptRepoRetentionFull, 1, "1");
HRN_CFG_LOAD(cfgCmdBackup, argList);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: repo option not specified, defaulting to repo1\n"
"P00 INFO: last backup label = [FULL-1], version = " PROJECT_VERSION "\n"
"P00 WARN: diff backup cannot alter compress-type option to 'gz', reset to value in [FULL-1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (3B, 100.00%) checksum 6f1894088c578e4f0b9888e8e8a997d93cbbc0c5\n"
"P00 DETAIL: reference pg_data/global/pg_control to [FULL-1]\n"
"P00 DETAIL: reference pg_data/postgresql.conf to [FULL-1]\n"
"P00 INFO: new backup label = [DIFF-3]\n"
"P00 INFO: diff backup size = 3B, file total = 3");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("multi-repo - specify repo");
hrnCfgArgRawZ(argList, cfgOptRepo, "2");
HRN_CFG_LOAD(cfgCmdBackup, argList);
HRN_STORAGE_PUT_Z(storagePgWrite(), PG_FILE_PGVERSION, "VER");
// Modify file postgresql.conf during the backup to demonstrate that percent complete will be updated correctly
HRN_STORAGE_PUT_Z(storagePgWrite(), "postgresql.conf", "CONFIGSTUFF2");
HRN_BACKUP_SCRIPT_SET(
{.op = hrnBackupScriptOpUpdate, .file = storagePathP(storagePg(), STRDEF("postgresql.conf")),
.content = BUFSTRDEF("CONFIG")});
unsigned int backupCount = strLstSize(storageListP(storageRepoIdx(1), strNewFmt(STORAGE_PATH_BACKUP "/test1")));
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: last backup label = [FULL-2], version = " PROJECT_VERSION "\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.conf (12B->6B, 80.00%) checksum"
" 2fb60054b43a25d7a958d3d19bdb1aa7809577a8\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (3B, 100.00%) checksum c8663c2525f44b6d9c687fbceb4aafc63ed8b451\n"
"P00 DETAIL: reference pg_data/global/pg_control to [FULL-2]\n"
"P00 INFO: new backup label = [DIFF-4]\n"
"P00 INFO: diff backup size = 9B, file total = 3");
TEST_RESULT_UINT(
strLstSize(storageListP(storageRepoIdx(1), strNewFmt(STORAGE_PATH_BACKUP "/test1"))), backupCount + 1,
"new backup repo2");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("diff changed to full when no prior backup is at the repository format");
// Set log level to warn so only the conversion is logged
harnessLogLevelSet(logLevelWarn);
// Upgrade the format of the repo, which leaves every backup in it at the prior format
InfoBackup *infoBackup = infoBackupLoadFile(storageRepoIdx(1), INFO_BACKUP_PATH_FILE_STR, cfgCipherSpecMainIdx(1));
infoBackupFormatSet(infoBackup, REPOSITORY_FORMAT_6);
infoBackupSaveFile(infoBackup, storageRepoIdxWrite(1), INFO_BACKUP_PATH_FILE_STR, cfgCipherSpecMainIdx(1));
HRN_STORAGE_PUT_Z(storagePgWrite(), PG_FILE_PGVERSION, "VR3");
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG("P00 WARN: no prior backup exists, diff backup has been changed to full");
// The new full backup adopts the upgraded format
infoBackup = infoBackupLoadFile(storageRepoIdx(1), INFO_BACKUP_PATH_FILE_STR, cfgCipherSpecMainIdx(1));
TEST_RESULT_UINT(
infoBackupData(infoBackup, infoBackupDataTotal(infoBackup) - 1).backrestFormat, REPOSITORY_FORMAT_6,
"new backup at upgraded format");
// Cleanup
hrnCfgEnvKeyRemoveRaw(cfgOptRepoCipherPass, 2);
harnessLogLevelReset();
}
// *****************************************************************************************************************************
if (testBegin("cmdBackup() online"))
{
const String *pg1Path = STRDEF(TEST_PATH "/pg1");
const String *repoPath = STRDEF(TEST_PATH "/repo");
const String *pg2Path = STRDEF(TEST_PATH "/pg2");
// Set log level to detail
harnessLogLevelSet(logLevelDetail);
// Replace percent complete and backup size since they can cause a lot of churn when files are added/removed
hrnLogReplaceAdd(", [0-9]{1,3}.[0-9]{1,2}%\\)", "[0-9].+%", "PCT", false);
hrnLogReplaceAdd(" backup size = [0-9.]+[A-Z]+", "[^ ]+$", "SIZE", false);
// Replace checksums since they can differ between architectures (e.g. 32/64 bit)
hrnLogReplaceAdd("\\) checksum [a-f0-9]{40}", "[a-f0-9]{40}$", "SHA1", false);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 14 resume uncompressed full backup");
time_t backupTimeStart = BACKUP_EPOCH;
{
// Create stanza
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawBool(argList, cfgOptOnline, false);
HRN_CFG_LOAD(cfgCmdStanzaCreate, argList);
// Create pg_control
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_14);
cmdStanzaCreate();
TEST_RESULT_LOG("P00 INFO: stanza-create for stanza 'test1' on repo1");
// Load options
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawBool(argList, cfgOptCompress, false);
hrnCfgArgRawBool(argList, cfgOptArchiveCheck, false);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Add files
HRN_STORAGE_PUT_Z(storagePgWrite(), "postgresql.conf", "CONFIGSTUFF", .timeModified = backupTimeStart);
HRN_STORAGE_PUT_Z(storagePgWrite(), PG_FILE_PGVERSION, PG_VERSION_14_Z, .timeModified = backupTimeStart);
HRN_STORAGE_PATH_CREATE(storagePgWrite(), strZ(pgWalPath(PG_VERSION_14)), .noParentCreate = true);
// Create a backup manifest that looks like a halted backup manifest
Manifest *manifestResume = manifestNewBuild(
storagePg(), PG_VERSION_14, hrnPgCatalogVersion(PG_VERSION_14), REPOSITORY_FORMAT_DEFAULT, backupTimeStart, true,
false, false, false, NULL, NULL, NULL);
manifestResume->pub.data.backupType = backupTypeFull;
const String *resumeLabel = backupLabelCreate(backupTypeFull, NULL, backupTimeStart);
manifestBackupLabelSet(manifestResume, resumeLabel);
// Copy a file to be resumed that has not changed in the repo
HRN_STORAGE_COPY(
storagePg(), PG_FILE_PGVERSION, storageRepoWrite(),
zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/PG_VERSION", strZ(resumeLabel)));
ManifestFilePack **const filePack = manifestFilePackFindInternal(manifestResume, STRDEF("pg_data/PG_VERSION"));
ManifestFile file = manifestFileUnpack(manifestResume, *filePack);
file.checksumSha1 = bufPtr(bufNewDecode(encodingHex, STRDEF("fa35e192121eabf3dabf9f5ea6abdbcbc107ac3b")));
file.checksumRepoSha1 = bufPtr(bufNewDecode(encodingHex, STRDEF("fa35e192121eabf3dabf9f5ea6abdbcbc107ac3b")));
manifestFilePackUpdate(manifestResume, filePack, &file);
// Save the resume manifest
manifestSave(
manifestResume,
storageWriteIo(
storageNewWriteP(
storageRepoWrite(),
strNewFmt(STORAGE_REPO_BACKUP "/%s/" BACKUP_MANIFEST_FILE INFO_COPY_EXT, strZ(resumeLabel)))));
// Run backup
hrnBackupPqScriptP(PG_VERSION_14, backupTimeStart, .noArchiveCheck = true, .noWal = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105D944C000000000, lsn = 5d944c0/0\n"
"P00 WARN: resumable backup 20191002-070640F of same type exists -- invalid files will be removed then the backup"
" will resume\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.conf (11B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: checksum resumed file " TEST_PATH "/pg1/PG_VERSION (2B, [PCT]) checksum [SHA1]\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105D944C000000000, lsn = 5d944c0/800000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: new backup label = 20191002-070640F\n"
"P00 INFO: full backup size = [SIZE], file total = 4");
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191002-070640F}\n"
"pg_data/PG_VERSION {s=2}\n"
"pg_data/backup_label {s=17, ts=+52427}\n"
"pg_data/global/pg_control {s=8192}\n"
"pg_data/pg_wal/\n"
"pg_data/postgresql.conf {s=11}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online resumed compressed 14 full backup");
// Backup start time
backupTimeStart = BACKUP_EPOCH + 100000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawBool(argList, cfgOptArchiveCopy, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Create a backup manifest that looks like a halted backup manifest
Manifest *manifestResume = manifestNewBuild(
storagePg(), PG_VERSION_14, hrnPgCatalogVersion(PG_VERSION_14), REPOSITORY_FORMAT_DEFAULT, backupTimeStart, true,
false, false, false, NULL, NULL, NULL);
manifestResume->pub.data.backupType = backupTypeFull;
manifestResume->pub.data.backupOptionCompressType = compressTypeGz;
const String *resumeLabel = backupLabelCreate(backupTypeFull, NULL, backupTimeStart);
manifestBackupLabelSet(manifestResume, resumeLabel);
// File exists in cluster and repo but not in the resume manifest
HRN_STORAGE_PUT_Z(storagePgWrite(), "not-in-resume", "TEST", .timeModified = backupTimeStart);
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/not-in-resume.gz", strZ(resumeLabel)));
// Remove checksum from file so it won't be resumed
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/global/pg_control.gz", strZ(resumeLabel)));
ManifestFilePack **const filePack = manifestFilePackFindInternal(manifestResume, STRDEF("pg_data/global/pg_control"));
ManifestFile file = manifestFileUnpack(manifestResume, *filePack);
file.checksumSha1 = NULL;
manifestFilePackUpdate(manifestResume, filePack, &file);
// Size does not match between cluster and resume manifest
HRN_STORAGE_PUT_Z(storagePgWrite(), "size-mismatch", "TEST", .timeModified = backupTimeStart);
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/size-mismatch.gz", strZ(resumeLabel)));
HRN_MANIFEST_FILE_ADD(
manifestResume, .name = "pg_data/size-mismatch", .size = 33,
.checksumSha1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
.checksumRepoSha1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
// Time does not match between cluster and resume manifest
HRN_STORAGE_PUT_Z(storagePgWrite(), "time-mismatch", "TEST", .timeModified = backupTimeStart);
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/time-mismatch.gz", strZ(resumeLabel)));
HRN_MANIFEST_FILE_ADD(
manifestResume, .name = "pg_data/time-mismatch", .size = 4, .timestamp = backupTimeStart - 1,
.checksumSha1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
.checksumRepoSha1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
// Size is zero in cluster and resume manifest. ??? We'd like to remove this requirement after the migration.
HRN_STORAGE_PUT_EMPTY(storagePgWrite(), "zero-size", .timeModified = backupTimeStart);
HRN_STORAGE_PUT_Z(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/zero-size.gz", strZ(resumeLabel)), "ZERO-SIZE");
HRN_MANIFEST_FILE_ADD(manifestResume, .name = "pg_data/zero-size", .size = 0, .timestamp = backupTimeStart);
// Path is not in manifest
HRN_STORAGE_PATH_CREATE(storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/bogus_path", strZ(resumeLabel)));
// File is not in manifest
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/global/bogus.gz", strZ(resumeLabel)));
// File has incorrect compression type
HRN_STORAGE_PUT_EMPTY(storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/global/bogus", strZ(resumeLabel)));
// Save the resume manifest
manifestSave(
manifestResume,
storageWriteIo(
storageNewWriteP(
storageRepoWrite(),
strNewFmt(STORAGE_REPO_BACKUP "/%s/" BACKUP_MANIFEST_FILE INFO_COPY_EXT, strZ(resumeLabel)))));
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-qual"
// Disable storageFeaturePath so paths will not be created before files are copied
((Storage *)storageRepoWrite())->pub.interface.feature ^= 1 << storageFeaturePath;
// Disable storageFeaturePathSync so paths will not be synced
((Storage *)storageRepoWrite())->pub.interface.feature ^= 1 << storageFeaturePathSync;
// Run backup
hrnBackupPqScriptP(PG_VERSION_14, backupTimeStart);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
// Enable storage features
((Storage *)storageRepoWrite())->pub.interface.feature |= 1 << storageFeaturePath;
((Storage *)storageRepoWrite())->pub.interface.feature |= 1 << storageFeaturePathSync;
#pragma GCC diagnostic pop
TEST_RESULT_LOG(
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105D95D3000000000, lsn = 5d95d30/0\n"
"P00 INFO: check archive for prior segment 0000000105D95D2F000000FF\n"
"P00 WARN: resumable backup 20191003-105320F of same type exists -- invalid files will be removed then the backup"
" will resume\n"
"P00 DETAIL: remove path '" TEST_PATH "/repo/backup/test1/20191003-105320F/pg_data/bogus_path' from resumed"
" backup\n"
"P00 DETAIL: remove file '" TEST_PATH "/repo/backup/test1/20191003-105320F/pg_data/global/bogus' from resumed"
" backup (mismatched compression type)\n"
"P00 DETAIL: remove file '" TEST_PATH "/repo/backup/test1/20191003-105320F/pg_data/global/bogus.gz' from resumed"
" backup (missing in manifest)\n"
"P00 DETAIL: remove file '" TEST_PATH "/repo/backup/test1/20191003-105320F/pg_data/global/pg_control.gz' from"
" resumed backup (no checksum in resumed manifest)\n"
"P00 DETAIL: remove file '" TEST_PATH "/repo/backup/test1/20191003-105320F/pg_data/not-in-resume.gz' from resumed"
" backup (missing in resumed manifest)\n"
"P00 DETAIL: remove file '" TEST_PATH "/repo/backup/test1/20191003-105320F/pg_data/size-mismatch.gz' from resumed"
" backup (mismatched size)\n"
"P00 DETAIL: remove file '" TEST_PATH "/repo/backup/test1/20191003-105320F/pg_data/time-mismatch.gz' from resumed"
" backup (mismatched timestamp)\n"
"P00 DETAIL: remove file '" TEST_PATH "/repo/backup/test1/20191003-105320F/pg_data/zero-size.gz' from resumed"
" backup (zero size)\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.conf (11B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/not-in-resume (4B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/size-mismatch (4B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/time-mismatch (4B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (2B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/zero-size (0B, [PCT])\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105D95D3000000000, lsn = 5d95d30/800000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105D95D3000000000:0000000105D95D3000000000\n"
"P00 DETAIL: copy segment 0000000105D95D3000000000 to backup\n"
"P00 INFO: new backup label = 20191003-105320F\n"
"P00 INFO: full backup size = [SIZE], file total = 9");
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191003-105320F}\n"
"pg_data/PG_VERSION.gz {s=2, ts=-100000}\n"
"pg_data/backup_label.gz {s=17, ts=+2}\n"
"pg_data/global/pg_control.gz {s=8192}\n"
"pg_data/not-in-resume.gz {s=4}\n"
"pg_data/pg_wal/0000000105D95D3000000000.gz {s=16777216, ts=+2}\n"
"pg_data/postgresql.conf.gz {s=11, ts=-100000}\n"
"pg_data/size-mismatch.gz {s=4}\n"
"pg_data/time-mismatch.gz {s=4}\n"
"pg_data/zero-size.gz {s=0}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
// Remove test files
HRN_STORAGE_REMOVE(storagePgWrite(), "not-in-resume", .errorOnMissing = true);
HRN_STORAGE_REMOVE(storagePgWrite(), "size-mismatch", .errorOnMissing = true);
HRN_STORAGE_REMOVE(storagePgWrite(), "time-mismatch", .errorOnMissing = true);
HRN_STORAGE_REMOVE(storagePgWrite(), "zero-size", .errorOnMissing = true);
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online resumed compressed 14 full delta backup");
backupTimeStart = BACKUP_EPOCH + 200000;
{
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawBool(argList, cfgOptDelta, true);
hrnCfgArgRawBool(argList, cfgOptRepoHardlink, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Create a backup manifest that looks like a halted backup manifest
Manifest *manifestResume = manifestNewBuild(
storagePg(), PG_VERSION_14, hrnPgCatalogVersion(PG_VERSION_14), REPOSITORY_FORMAT_DEFAULT, backupTimeStart, true,
false, false, false, NULL, NULL, NULL);
manifestResume->pub.data.backupOptionCompressType = compressTypeGz;
const String *resumeLabel = backupLabelCreate(backupTypeFull, NULL, backupTimeStart - 100000);
manifestBackupLabelSet(manifestResume, resumeLabel);
strLstAddZ(manifestResume->pub.referenceList, "BOGUS");
// Time does not match between cluster and resume manifest (but resume because time is in future so delta enabled). Note
// also that the repo file is intentionally corrupt to generate a warning about corruption in the repository.
HRN_STORAGE_PUT_Z(storagePgWrite(), "time-mismatch2", "TEST", .timeModified = backupTimeStart + 100);
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/time-mismatch2.gz", strZ(resumeLabel)));
HRN_MANIFEST_FILE_ADD(
manifestResume, .name = "pg_data/time-mismatch2", .size = 4, .timestamp = backupTimeStart,
.checksumSha1 = "984816fd329622876e14907634264e6f332e9fb3");
// File does not match what is in manifest
HRN_STORAGE_PUT_Z(storagePgWrite(), "content-mismatch", "TEST", .timeModified = backupTimeStart);
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/content-mismatch.gz", strZ(resumeLabel)));
HRN_MANIFEST_FILE_ADD(
manifestResume, .name = "pg_data/content-mismatch", .size = 4, .timestamp = backupTimeStart,
.checksumSha1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
.checksumRepoSha1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
// Repo size mismatch
HRN_STORAGE_PUT_Z(storagePgWrite(), "repo-size-mismatch", "TEST", .timeModified = backupTimeStart);
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/repo-size-mismatch.gz", strZ(resumeLabel)));
HRN_MANIFEST_FILE_ADD(
manifestResume, .name = "pg_data/repo-size-mismatch", .size = 4, .sizeRepo = 4, .timestamp = backupTimeStart,
.checksumSha1 = "984816fd329622876e14907634264e6f332e9fb3",
.checksumRepoSha1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
// File that will resumed but removed from pg during the backup (and then the repo)
HRN_STORAGE_PUT_Z(storagePgWrite(), "removed-during", "TEST", .timeModified = backupTimeStart);
HRN_STORAGE_PUT_EMPTY(
storageRepoWrite(), zNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/removed-during.gz", strZ(resumeLabel)));
HRN_MANIFEST_FILE_ADD(
manifestResume, .name = "pg_data/removed-during", .size = 4, .timestamp = backupTimeStart,
.checksumSha1 = "984816fd329622876e14907634264e6f332e9fb3");
// Links are always removed on resume
THROW_ON_SYS_ERROR(
symlink(
"..",
strZ(storagePathP(storageRepo(), strNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/link", strZ(resumeLabel))))) == -1,
FileOpenError, "unable to create symlink");
// Special files should not be in the repo
HRN_SYSTEM_FMT(
"mkfifo -m 666 %s",
strZ(storagePathP(storageRepo(), strNewFmt(STORAGE_REPO_BACKUP "/%s/pg_data/pipe", strZ(resumeLabel)))));
// Save the resume manifest with full type
manifestResume->pub.data.backupType = backupTypeFull;
manifestSave(
manifestResume,
storageWriteIo(
storageNewWriteP(
storageRepoWrite(),
strNewFmt(STORAGE_REPO_BACKUP "/%s/" BACKUP_MANIFEST_FILE INFO_COPY_EXT, strZ(resumeLabel)))));
// Run backup
HRN_BACKUP_SCRIPT_SET(
{.op = hrnBackupScriptOpRemove, .file = storagePathP(storagePg(), STRDEF("removed-during"))});
hrnBackupPqScriptP(PG_VERSION_14, backupTimeStart);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
// Check log
TEST_RESULT_LOG(
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105D9759000000000, lsn = 5d97590/0\n"
"P00 INFO: check archive for prior segment 0000000105D9758F000000FF\n"
"P00 WARN: resumable backup 20191003-105321F of same type exists -- invalid files will be removed then the backup"
" will resume\n"
"P00 WARN: remove special file '" TEST_PATH "/repo/backup/test1/20191003-105321F/pg_data/pipe' from resumed"
" backup\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.conf (11B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/content-mismatch (4B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: skip file removed by database " TEST_PATH "/pg1/removed-during\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/repo-size-mismatch (4B, [PCT]) checksum [SHA1]\n"
"P00 WARN: resumed backup file pg_data/repo-size-mismatch did not have expected checksum"
" 984816fd329622876e14907634264e6f332e9fb3. The file was recopied and backup will continue but this may be an issue"
" unless the resumed backup path in the repository is known to be corrupted.\n"
" NOTE: this does not indicate a problem with the PostgreSQL page checksums.\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/time-mismatch2 (4B, [PCT]) checksum [SHA1]\n"
"P00 WARN: resumed backup file pg_data/time-mismatch2 did not have expected checksum"
" 984816fd329622876e14907634264e6f332e9fb3. The file was recopied and backup will continue but this may be an issue"
" unless the resumed backup path in the repository is known to be corrupted.\n"
" NOTE: this does not indicate a problem with the PostgreSQL page checksums.\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (2B, [PCT]) checksum [SHA1]\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105D9759000000000, lsn = 5d97590/800000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105D9759000000000:0000000105D9759000000000\n"
"P00 INFO: new backup label = 20191003-105321F\n"
"P00 INFO: full backup size = [SIZE], file total = 7");
// Check repo directory
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191003-105321F}\n"
"pg_data/PG_VERSION.gz {s=2, ts=-200000}\n"
"pg_data/backup_label.gz {s=17, ts=+2}\n"
"pg_data/content-mismatch.gz {s=4}\n"
"pg_data/global/pg_control.gz {s=8192}\n"
"pg_data/pg_wal/\n"
"pg_data/postgresql.conf.gz {s=11, ts=-200000}\n"
"pg_data/repo-size-mismatch.gz {s=4}\n"
"pg_data/time-mismatch2.gz {s=4, ts=+100}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
// Remove test files
HRN_STORAGE_REMOVE(storagePgWrite(), "time-mismatch2", .errorOnMissing = true);
HRN_STORAGE_REMOVE(storagePgWrite(), "content-mismatch", .errorOnMissing = true);
HRN_STORAGE_REMOVE(storagePgWrite(), "repo-size-mismatch", .errorOnMissing = true);
// Remove main manifest to make this backup look resumable
HRN_STORAGE_REMOVE(storageRepoWrite(), "backup/test1/20191003-105321F/backup.manifest");
// Save the resume manifest with diff type
manifestResume->pub.data.backupType = backupTypeDiff;
manifestSave(
manifestResume,
storageWriteIo(
storageNewWriteP(
storageRepoWrite(),
strNewFmt(STORAGE_REPO_BACKUP "/%s/" BACKUP_MANIFEST_FILE INFO_COPY_EXT, strZ(resumeLabel)))));
// Back errors on backup type
hrnBackupPqScriptP(PG_VERSION_14, backupTimeStart);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
// Check log
TEST_RESULT_LOG(
"P00 WARN: backup '20191003-105321F' missing manifest removed from backup.info\n"
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105D9759000000000, lsn = 5d97590/0\n"
"P00 INFO: check archive for prior segment 0000000105D9758F000000FF\n"
"P00 WARN: backup '20191003-105321F' cannot be resumed: new backup type 'full' does not match resumable backup"
" type 'diff'\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.conf (11B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (2B, [PCT]) checksum [SHA1]\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105D9759000000000, lsn = 5d97590/800000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105D9759000000000:0000000105D9759000000000\n"
"P00 INFO: new backup label = 20191004-144000F\n"
"P00 INFO: full backup size = [SIZE], file total = 4");
}
HRN_STORAGE_PATH_REMOVE(storagePgWrite(), "pg_wal", .errorOnMissing = true);
HRN_STORAGE_PATH_CREATE(storagePgWrite(), "pg_xlog");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 9.6 backup-standby full backup");
backupTimeStart = BACKUP_EPOCH + 1200000;
{
// Update pg_control
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_96);
// Update version
HRN_STORAGE_PUT_Z(storagePgWrite(), PG_FILE_PGVERSION, PG_VERSION_96_Z, .timeModified = backupTimeStart);
// Upgrade stanza
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawBool(argList, cfgOptOnline, false);
HRN_CFG_LOAD(cfgCmdStanzaUpgrade, argList);
cmdStanzaUpgrade();
TEST_RESULT_LOG("P00 INFO: stanza-upgrade for stanza 'test1' on repo1");
// Load options
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgKeyRaw(argList, cfgOptPgPath, 1, pg1Path);
hrnCfgArgKeyRaw(argList, cfgOptPgPath, 2, pg2Path);
hrnCfgArgKeyRawZ(argList, cfgOptPgPort, 2, "5433");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawBool(argList, cfgOptCompress, false);
hrnCfgArgRawBool(argList, cfgOptBackupStandby, true);
hrnCfgArgRawBool(argList, cfgOptStartFast, true);
hrnCfgArgRawBool(argList, cfgOptArchiveCopy, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Add pg_control to standby
HRN_PG_CONTROL_PUT(storagePgIdxWrite(1), PG_VERSION_96);
// Create file to copy from the standby. This file will be zero-length on the primary and non-zero-length on the standby
// but no bytes will be copied.
HRN_STORAGE_PUT_EMPTY(storagePgIdxWrite(0), PG_PATH_BASE "/1/1", .timeModified = backupTimeStart);
HRN_STORAGE_PUT_Z(storagePgIdxWrite(1), PG_PATH_BASE "/1/1", "1234");
// Create file to copy from the standby. This file will be smaller on the primary than the standby and have no common
// data in the bytes that exist on primary and standby. If the file is copied from the primary instead of the standby
// the checksum will change but not the size.
HRN_STORAGE_PUT_Z(storagePgIdxWrite(0), PG_PATH_BASE "/1/2", "DA", .timeModified = backupTimeStart);
HRN_STORAGE_PUT_Z(storagePgIdxWrite(1), PG_PATH_BASE "/1/2", "5678");
// Create file to copy from the standby. This file will be larger on the primary than the standby and have no common
// data in the bytes that exist on primary and standby. If the file is copied from the primary instead of the standby
// the checksum and size will change.
HRN_STORAGE_PUT_Z(storagePgIdxWrite(0), PG_PATH_BASE "/1/3", "TEST", .timeModified = backupTimeStart);
HRN_STORAGE_PUT_Z(storagePgIdxWrite(1), PG_PATH_BASE "/1/3", "ABC");
// Create a file on the primary that does not exist on the standby to test that the file is removed from the manifest
HRN_STORAGE_PUT_Z(storagePgIdxWrite(0), PG_PATH_BASE "/1/0", "DATA", .timeModified = backupTimeStart);
// Set log level to warn because the following test uses multiple processes so the log order will not be deterministic
harnessLogLevelSet(logLevelWarn);
// Run backup but error on first archive check
hrnBackupPqScriptP(
PG_VERSION_96, backupTimeStart, .noPriorWal = true, .backupStandby = true, .walCompressType = compressTypeGz,
.startFast = true);
TEST_ERROR(
hrnCmdBackup(), ArchiveTimeoutError,
"WAL segment 0000000105DA69BF000000FF was not archived before the 100ms timeout\n"
"HINT: check the archive_command to ensure that all options are correct (especially --stanza).\n"
"HINT: check the PostgreSQL server log for errors.\n"
"HINT: run the 'start' command if the stanza was previously stopped.");
// Run backup but error on archive check
hrnBackupPqScriptP(
PG_VERSION_96, backupTimeStart, .noWal = true, .backupStandby = true, .walCompressType = compressTypeGz,
.startFast = true);
TEST_ERROR(
hrnCmdBackup(), ArchiveTimeoutError,
"WAL segment 0000000105DA69C000000000 was not archived before the 100ms timeout\n"
"HINT: check the archive_command to ensure that all options are correct (especially --stanza).\n"
"HINT: check the PostgreSQL server log for errors.\n"
"HINT: run the 'start' command if the stanza was previously stopped.");
// Remove halted backup so there's no resume
HRN_STORAGE_PATH_REMOVE(storageRepoWrite(), STORAGE_REPO_BACKUP "/20191016-042640F", .recurse = true);
// Run backup
hrnBackupPqScriptP(
PG_VERSION_96, backupTimeStart, .backupStandby = true, .walCompressType = compressTypeGz, .startFast = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
// Set log level back to detail
harnessLogLevelSet(logLevelDetail);
TEST_RESULT_LOG(
"P00 WARN: no prior backup exists, incr backup has been changed to full");
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191016-042640F}\n"
"pg_data/PG_VERSION {s=3}\n"
"pg_data/backup_label {s=17, ts=+2}\n"
"pg_data/base/1/1 {s=0}\n"
"pg_data/base/1/2 {s=2}\n"
"pg_data/base/1/3 {s=3, so=4}\n"
"pg_data/global/pg_control {s=8192}\n"
"pg_data/pg_xlog/0000000105DA69C000000000 {s=16777216, ts=+2}\n"
"pg_data/postgresql.conf {s=11, ts=-1200000}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
// Remove test files
HRN_STORAGE_PATH_REMOVE(storagePgIdxWrite(1), NULL, .recurse = true);
HRN_STORAGE_PATH_REMOVE(storagePgWrite(), "base/1", .recurse = true);
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 9.6 backup-standby full backup with block incremental");
backupTimeStart = BACKUP_EPOCH + 1600000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgKeyRaw(argList, cfgOptPgPath, 1, pg1Path);
hrnCfgArgKeyRaw(argList, cfgOptPgPath, 2, pg2Path);
hrnCfgArgKeyRawZ(argList, cfgOptPgPort, 2, "5433");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawBool(argList, cfgOptRepoBlock, true);
hrnCfgArgRawBool(argList, cfgOptCompress, false);
hrnCfgArgRawBool(argList, cfgOptBackupStandby, true);
hrnCfgArgRawBool(argList, cfgOptStartFast, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Add file that is large enough for block incremental but larger on the primary than the standby. The standby size will
// be increased before the next backup.
Buffer *relation = bufNew(pgPageSize8 * 5);
memset(bufPtr(relation), 0, bufSize(relation));
bufUsedSet(relation, bufSize(relation));
HRN_STORAGE_PUT(storagePgIdxWrite(0), PG_PATH_BASE "/1/3", relation, .timeModified = backupTimeStart);
bufUsedSet(relation, bufSize(relation) - pgPageSize8 * 2);
HRN_STORAGE_PUT(storagePgIdxWrite(1), PG_PATH_BASE "/1/3", relation);
// Add file that is large enough for block incremental but larger on the primary than the standby. This tests that file
// size is set correctly when a file is referenced to a prior backup but the original size is different. Zero the buffer
// again since the area beyond used was invalidated when the buffer was made smaller above.
memset(bufPtr(relation), 0, bufSize(relation));
bufUsedSet(relation, bufSize(relation));
HRN_STORAGE_PUT(storagePgIdxWrite(0), PG_PATH_BASE "/1/4", relation, .timeModified = backupTimeStart);
bufUsedSet(relation, bufSize(relation) - pgPageSize8);
HRN_STORAGE_PUT(storagePgIdxWrite(1), PG_PATH_BASE "/1/4", relation);
// Set log level to warn because the following test uses multiple processes so the log order will not be deterministic
harnessLogLevelSet(logLevelWarn);
// Run backup
hrnBackupPqScriptP(PG_VERSION_96, backupTimeStart, .backupStandby = true, .startFast = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
// Set log level back to detail
harnessLogLevelSet(logLevelDetail);
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191020-193320F}\n"
"bundle/1/pg_data/PG_VERSION {s=3, ts=-400000}\n"
"bundle/1/pg_data/global/pg_control {s=8192}\n"
"bundle/1/pg_data/postgresql.conf {s=11, ts=-1600000}\n"
"bundle/2/pg_data/base/1/3 {s=24576, so=40960, m=0:{0,1,2}}\n"
"bundle/2/pg_data/base/1/4 {s=32768, so=40960, m=0:{0,1,2,3}}\n"
"pg_data/backup_label {s=17, ts=+2}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 9.6 backup-standby incr backup with block incremental");
backupTimeStart = BACKUP_EPOCH + 1700000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgKeyRaw(argList, cfgOptPgPath, 1, pg1Path);
hrnCfgArgKeyRaw(argList, cfgOptPgPath, 2, pg2Path);
hrnCfgArgKeyRawZ(argList, cfgOptPgPort, 2, "5433");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeIncr);
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawBool(argList, cfgOptRepoBlock, true);
hrnCfgArgRawBool(argList, cfgOptCompress, false);
hrnCfgArgRawZ(argList, cfgOptBackupStandby, "prefer");
hrnCfgArgRawBool(argList, cfgOptStartFast, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Increase size of file on standby. This demonstrates that copy is using the larger file from the primary as the basis
// for how far to read.
Buffer *relation = bufNew(pgPageSize8 * 4);
memset(bufPtr(relation), 0, bufSize(relation));
bufUsedSet(relation, bufSize(relation));
HRN_STORAGE_PUT(storagePgIdxWrite(1), PG_PATH_BASE "/1/3", relation);
// Set log level to warn because the following test uses multiple processes so the log order will not be deterministic
harnessLogLevelSet(logLevelWarn);
// Run backup
hrnBackupPqScriptP(PG_VERSION_96, backupTimeStart, .backupStandby = true, .startFast = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
// Set log level back to detail
harnessLogLevelSet(logLevelDetail);
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191020-193320F_20191021-232000I}\n"
"bundle/1/pg_data/global/pg_control {s=8192}\n"
"bundle/2/pg_data/base/1/3 {s=32768, so=40960, m=0:{0,1,2},1:{0}, ts=-100000}\n"
"pg_data/backup_label {s=17, ts=+2}\n"
"20191020-193320F/bundle/1/pg_data/PG_VERSION {s=3, ts=-500000}\n"
"20191020-193320F/bundle/2/pg_data/base/1/4 {s=32768, so=40960, m=0:{0,1,2,3}, ts=-100000}\n"
"20191020-193320F/bundle/1/pg_data/postgresql.conf {s=11, ts=-1700000}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 9.6 backup-standby (prefer) incr backup with block incremental");
backupTimeStart = BACKUP_EPOCH + 1800000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgKeyRaw(argList, cfgOptPgPath, 1, pg1Path);
hrnCfgArgKeyRaw(argList, cfgOptPgPath, 2, pg2Path);
hrnCfgArgKeyRawZ(argList, cfgOptPgPort, 2, "5433");
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeIncr);
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawBool(argList, cfgOptRepoBlock, true);
hrnCfgArgRawBool(argList, cfgOptCompress, false);
hrnCfgArgRawZ(argList, cfgOptBackupStandby, "prefer");
hrnCfgArgRawBool(argList, cfgOptStartFast, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Run backup
hrnBackupPqScriptP(
PG_VERSION_96, backupTimeStart, .backupStandby = true, .backupStandbyError = true, .startFast = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 WARN: unable to check pg2: [DbConnectError] unable to connect to 'dbname='postgres' port=5433': error\n"
"P00 WARN: unable to find a standby to perform the backup, using primary instead\n"
"P00 INFO: last backup label = 20191020-193320F_20191021-232000I, version = " PROJECT_VERSION "\n"
"P00 INFO: execute backup start: backup begins after the requested immediate checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DAFC3000000000, lsn = 5dafc30/0\n"
"P00 INFO: check archive for prior segment 0000000105DAFC2F000000FF\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/4 (bundle 1/0, 40KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/3 (bundle 1/8237, 40KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/16476, 8KB, [PCT]) checksum [SHA1]\n"
"P00 DETAIL: reference pg_data/PG_VERSION to 20191020-193320F\n"
"P00 DETAIL: reference pg_data/postgresql.conf to 20191020-193320F\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DAFC3000000000, lsn = 5dafc30/800000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DAFC3000000000:0000000105DAFC3000000000\n"
"P00 INFO: new backup label = 20191020-193320F_20191023-030640I\n"
"P00 INFO: incr backup size = [SIZE], file total = 6");
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191020-193320F_20191023-030640I}\n"
"bundle/1/pg_data/base/1/3 {s=40960, m=0:{0,1,2},1:{0},2:{0}, ts=-200000}\n"
"bundle/1/pg_data/base/1/4 {s=40960, m=0:{0,1,2,3},2:{0}, ts=-200000}\n"
"bundle/1/pg_data/global/pg_control {s=8192}\n"
"pg_data/backup_label {s=17, ts=+2}\n"
"20191020-193320F/bundle/1/pg_data/PG_VERSION {s=3, ts=-600000}\n"
"20191020-193320F/bundle/1/pg_data/postgresql.conf {s=11, ts=-1800000}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
HRN_STORAGE_PATH_REMOVE(storagePgWrite(), "pg_xlog", .errorOnMissing = true);
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 full backup with tablespaces and page checksums");
backupTimeStart = BACKUP_EPOCH + 2200000;
{
// Update pg_control
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_11, .pageChecksumVersion = 1, .walSegmentSize = 1024 * 1024);
// Update version
HRN_STORAGE_PUT_Z(storagePgWrite(), PG_FILE_PGVERSION, PG_VERSION_11_Z, .timeModified = backupTimeStart);
// Update wal path
HRN_STORAGE_PATH_REMOVE(storagePgWrite(), strZ(pgWalPath(PG_VERSION_14)));
HRN_STORAGE_PATH_CREATE(storagePgWrite(), strZ(pgWalPath(PG_VERSION_11)), .noParentCreate = true);
// Upgrade stanza to format 6
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRawZ(argList, cfgOptRepoFormat, "6");
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawBool(argList, cfgOptOnline, false);
HRN_CFG_LOAD(cfgCmdStanzaUpgrade, argList);
cmdStanzaUpgrade();
TEST_RESULT_LOG(
"P00 INFO: stanza-upgrade for stanza 'test1' on repo1\n"
"P00 INFO: upgrade repository format from 5 to 6");
// Load options
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawBool(argList, cfgOptRepoSymlink, false);
// Do not specify type=full since the update to repo format 6 should force a full upgrade
hrnCfgArgRawBool(argList, cfgOptRepoHardlink, true);
hrnCfgArgRawZ(argList, cfgOptManifestSaveThreshold, "1");
hrnCfgArgRawBool(argList, cfgOptArchiveCopy, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Move pg1-path and put a link in its place. This tests that backup works when pg1-path is a symlink yet should be
// completely invisible in the manifest and logging.
HRN_SYSTEM_FMT("mv %s %s-data", strZ(pg1Path), strZ(pg1Path));
HRN_SYSTEM_FMT("ln -s %s-data %s ", strZ(pg1Path), strZ(pg1Path));
// Zeroed file which passes page checksums
Buffer *relation = bufNew(pgPageSize8 * 2);
memset(bufPtr(relation), 0, bufSize(relation));
bufUsedSet(relation, bufSize(relation));
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x00)) = (PageHeaderData){.pd_upper = 0};
HRN_STORAGE_PUT(storagePgWrite(), PG_PATH_BASE "/1/1", relation, .timeModified = backupTimeStart);
// File which will fail on alignment
relation = bufNew(pgPageSize8 + 512);
memset(bufPtr(relation), 0, bufSize(relation));
bufUsedSet(relation, bufSize(relation));
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x00)) = (PageHeaderData){.pd_upper = 0xFE};
((PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x00)))->pd_checksum = pgPageChecksum(
bufPtr(relation) + (pgPageSize8 * 0x00), 0, pgPageSize8);
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x01)) = (PageHeaderData){.pd_upper = 0xFF};
HRN_STORAGE_PUT(storagePgWrite(), PG_PATH_BASE "/1/2", relation, .timeModified = backupTimeStart);
// File with bad page checksums
relation = bufNew(pgPageSize8 * 5);
memset(bufPtr(relation), 0, bufSize(relation));
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x00)) = (PageHeaderData){.pd_upper = 0xFF};
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x01)) = (PageHeaderData){.pd_upper = 0x00};
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x02)) = (PageHeaderData){.pd_upper = 0xFE};
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x03)) = (PageHeaderData){.pd_upper = 0xEF};
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x04)) = (PageHeaderData){.pd_upper = 0x00};
(bufPtr(relation) + (pgPageSize8 * 0x04))[pgPageSize8 - 1] = 0xFF;
bufUsedSet(relation, bufSize(relation));
HRN_STORAGE_PUT(storagePgWrite(), PG_PATH_BASE "/1/3", relation, .timeModified = backupTimeStart);
// File with bad page checksum
relation = bufNew(pgPageSize8 * 3);
memset(bufPtr(relation), 0, bufSize(relation));
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x00)) = (PageHeaderData){.pd_upper = 0x00};
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x01)) = (PageHeaderData){.pd_upper = 0x08};
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x02)) = (PageHeaderData){.pd_upper = 0xFF};
((PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x02)))->pd_checksum = pgPageChecksum(
bufPtr(relation) + (pgPageSize8 * 0x02), 2, pgPageSize8);
bufUsedSet(relation, bufSize(relation));
HRN_STORAGE_PUT(storagePgWrite(), PG_PATH_BASE "/1/4", relation, .timeModified = backupTimeStart);
// Add a tablespace
HRN_STORAGE_PATH_CREATE(storagePgWrite(), PG_PATH_PGTBLSPC);
THROW_ON_SYS_ERROR(
symlink("../../pg1-tblspc/32768", strZ(storagePathP(storagePg(), STRDEF(PG_PATH_PGTBLSPC "/32768")))) == -1,
FileOpenError, "unable to create symlink");
HRN_STORAGE_PUT_EMPTY(
storageTest,
zNewFmt("pg1-tblspc/32768/%s/1/5", strZ(pgTablespaceId(PG_VERSION_11, hrnPgCatalogVersion(PG_VERSION_11)))),
.timeModified = backupTimeStart);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-qual"
// Disable storageFeatureHardLink so hardlinks will not be created
((Storage *)storageRepoWrite())->pub.interface.feature ^= 1 << storageFeatureHardLink;
// Run backup
hrnBackupPqScriptP(PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeGz, .walTotal = 3, .walSwitch = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
// Reset storage features
((Storage *)storageRepoWrite())->pub.interface.feature |= 1 << storageFeatureHardLink;
#pragma GCC diagnostic pop
TEST_RESULT_LOG(
"P00 WARN: no prior backup exists, incr backup has been changed to full\n"
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DB5DE000000000, lsn = 5db5de0/0\n"
"P00 INFO: check archive for segment 0000000105DB5DE000000000\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/3 (40KB, [PCT]) checksum [SHA1]\n"
"P00 WARN: invalid page checksums found in file " TEST_PATH "/pg1/base/1/3 at pages 0, 2-4\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/4 (24KB, [PCT]) checksum [SHA1]\n"
"P00 WARN: invalid page checksum found in file " TEST_PATH "/pg1/base/1/4 at page 1\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/1 (16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/2 (8.5KB, [PCT]) checksum [SHA1]\n"
"P00 WARN: page misalignment in file " TEST_PATH "/pg1/base/1/2: file size 8704 is not divisible by page size"
" 8192\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.conf (11B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (2B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/pg_tblspc/32768/PG_11_201809051/1/5 (0B, [PCT])\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DB5DE000000002, lsn = 5db5de0/280000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 DETAIL: wrote 'tablespace_map' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DB5DE000000000:0000000105DB5DE000000002\n"
"P00 DETAIL: copy segment 0000000105DB5DE000000000 to backup\n"
"P00 DETAIL: copy segment 0000000105DB5DE000000001 to backup\n"
"P00 DETAIL: copy segment 0000000105DB5DE000000002 to backup\n"
"P00 INFO: new backup label = 20191027-181320F\n"
"P00 INFO: full backup size = [SIZE], file total = 13\n"
"P00 WARN: backup command encountered 3 checksum warning(s), check the log file for details");
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/20191027-181320F")),
"pg_data/PG_VERSION.gz {s=2}\n"
"pg_data/backup_label.gz {s=17, ts=+2}\n"
"pg_data/base/1/1.gz {s=16384, ckp=t}\n"
"pg_data/base/1/2.gz {s=8704, ckp=t}\n"
"pg_data/base/1/3.gz {s=40960, ckp=[0,[2,4]]}\n"
"pg_data/base/1/4.gz {s=24576, ckp=[1]}\n"
"pg_data/global/pg_control.gz {s=8192}\n"
"pg_data/pg_tblspc/\n"
"pg_data/pg_wal/0000000105DB5DE000000000.gz {s=1048576, ts=+2}\n"
"pg_data/pg_wal/0000000105DB5DE000000001.gz {s=1048576, ts=+2}\n"
"pg_data/pg_wal/0000000105DB5DE000000002.gz {s=1048576, ts=+2}\n"
"pg_data/postgresql.conf.gz {s=11, ts=-2200000}\n"
"pg_data/tablespace_map.gz {s=19, ts=+2}\n"
"pg_tblspc/32768/PG_11_201809051/1/5.gz {s=0, ckp=t}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n"
"pg_tblspc/32768={\"path\":\"../../pg1-tblspc/32768\",\"tablespace-id\":\"32768\""
",\"tablespace-name\":\"tblspc32768\",\"type\":\"link\"}\n",
"compare file list");
// Remove test files but base/1/3 (with its invalid page checksums) for the following test
HRN_STORAGE_REMOVE(storagePgWrite(), "base/1/2", .errorOnMissing = true);
HRN_STORAGE_REMOVE(storagePgWrite(), "base/1/4", .errorOnMissing = true);
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("full backup fails immediately on invalid page checksum when checksum-page-error=y");
backupTimeStart = BACKUP_EPOCH + 2400000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawBool(argList, cfgOptChecksumPageError, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Run backup. It should abort at the first invalid page checksum. Reuses base/1/3 (with invalid pages 0, 2-4)
// left over from the prior test
hrnBackupPqScriptP(
PG_VERSION_11, backupTimeStart, .timeline = 0x2C, .walTotal = 2, .walSwitch = true, .errorAfterCopyStart = true);
TEST_ERROR(
hrnCmdBackup(), ChecksumError,
"invalid page checksums found in file " TEST_PATH "/pg1/base/1/3 at pages 0, 2-4");
TEST_RESULT_LOG(
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000002C05DB8EB000000000, lsn = 5db8eb0/0\n"
"P00 INFO: check archive for segment 0000002C05DB8EB000000000\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/3 (40KB, [PCT]) checksum [SHA1]");
// Remove partial backup so it won't be resumed by the following test
HRN_STORAGE_PATH_REMOVE(storageRepoWrite(), STORAGE_REPO_BACKUP "/20191030-014640F", .recurse = true);
// Remove test file
HRN_STORAGE_REMOVE(storagePgWrite(), "base/1/3", .errorOnMissing = true);
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("error when pg_control not present");
// Restore backupTimeStart to the time used by the last successful backup
backupTimeStart = BACKUP_EPOCH + 2200000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeIncr);
hrnCfgArgRawBool(argList, cfgOptRepoHardlink, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Preserve prior timestamp on pg_control
hrnBackupPqScriptP(PG_VERSION_11, BACKUP_EPOCH + 2300000, .errorAfterStart = true);
HRN_PG_CONTROL_TIME(storagePg(), backupTimeStart);
// Run backup
TEST_ERROR(
hrnCmdBackup(), FileMissingError,
"pg_control must be present in all online backups\n"
"HINT: is something wrong with the clock or filesystem timestamps?");
// Check log
TEST_RESULT_LOG(
"P00 INFO: last backup label = 20191027-181320F, version = " PROJECT_VERSION "\n"
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DB764000000000, lsn = 5db7640/0\n"
"P00 INFO: check archive for prior segment 0000000105DB763F00000FFF");
// Remove partial backup so it won't be resumed (since it errored before any checksums were written)
HRN_STORAGE_PATH_REMOVE(storageRepoWrite(), STORAGE_REPO_BACKUP "/20191027-181320F_20191028-220000I", .recurse = true);
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 incr backup with tablespaces");
backupTimeStart = BACKUP_EPOCH + 2400000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgKeyRawZ(argList, cfgOptRepoPath, 1, "/repo-bogus");
hrnCfgArgKeyRaw(argList, cfgOptRepoPath, 2, repoPath);
hrnCfgArgKeyRawZ(argList, cfgOptRepoRetentionFull, 2, "1");
hrnCfgArgKeyRawBool(argList, cfgOptRepoHardlink, 2, true);
hrnCfgArgRawZ(argList, cfgOptRepo, "2");
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeIncr);
hrnCfgArgRawBool(argList, cfgOptDelta, true);
hrnCfgArgRawBool(argList, cfgOptPageHeaderCheck, false);
hrnCfgArgRawBool(argList, cfgOptRepoHardlink, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// File with bad page checksum and header errors that will be ignored
Buffer *relation = bufNew(pgPageSize8 * 4);
memset(bufPtr(relation), 0, bufSize(relation));
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x00)) = (PageHeaderData){.pd_upper = 0xFF};
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x01)) = (PageHeaderData){.pd_upper = 0x00};
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x02)) = (PageHeaderData){.pd_upper = 0x00};
(bufPtr(relation) + (pgPageSize8 * 0x02))[pgPageSize8 - 1] = 0xFF;
((PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x02)))->pd_checksum = pgPageChecksum(
bufPtr(relation) + (pgPageSize8 * 0x02), 2, pgPageSize8);
*(PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x03)) = (PageHeaderData){.pd_upper = 0x00};
(bufPtr(relation) + (pgPageSize8 * 0x03))[pgPageSize8 - 1] = 0xEE;
((PageHeaderData *)(bufPtr(relation) + (pgPageSize8 * 0x03)))->pd_checksum = 1;
bufUsedSet(relation, bufSize(relation));
HRN_STORAGE_PUT(storagePgWrite(), PG_PATH_BASE "/1/3", relation, .timeModified = backupTimeStart);
// File will be truncated during backup to show that actual file size is copied no matter what original size is. This
// will also cause an alignment error.
Buffer *relationAfter = bufNew(pgPageSize8 + 15);
memset(bufPtr(relationAfter), 0, bufSize(relationAfter));
bufUsedSet(relationAfter, bufSize(relationAfter));
// Run backup. Make sure that the timeline selected converts to hexadecimal that can't be interpreted as decimal.
HRN_BACKUP_SCRIPT_SET(
{.op = hrnBackupScriptOpUpdate, .file = storagePathP(storagePg(), STRDEF(PG_PATH_BASE "/1/1")),
.time = backupTimeStart, .content = relationAfter});
hrnBackupPqScriptP(PG_VERSION_11, backupTimeStart, .timeline = 0x2C, .walTotal = 2, .walSwitch = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: last backup label = 20191027-181320F, version = " PROJECT_VERSION "\n"
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000002C05DB8EB000000000, lsn = 5db8eb0/0\n"
"P00 INFO: check archive for segment 0000002C05DB8EB000000000\n"
"P00 WARN: a timeline switch has occurred since the 20191027-181320F backup, enabling delta checksum\n"
" HINT: this is normal after restoring from backup or promoting a standby.\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/3 (32KB, [PCT]) checksum [SHA1]\n"
"P00 WARN: invalid page checksums found in file " TEST_PATH "/pg1/base/1/3 at pages 0, 3\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/1 (16KB->8KB, [PCT]) checksum [SHA1]\n"
"P00 WARN: page misalignment in file " TEST_PATH "/pg1/base/1/1: file size 8207 is not divisible by page size"
" 8192\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: match file from prior backup " TEST_PATH "/pg1/postgresql.conf (11B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: match file from prior backup " TEST_PATH "/pg1/PG_VERSION (2B, [PCT]) checksum [SHA1]\n"
"P00 DETAIL: hardlink pg_data/PG_VERSION to 20191027-181320F\n"
"P00 DETAIL: hardlink pg_data/postgresql.conf to 20191027-181320F\n"
"P00 DETAIL: hardlink pg_tblspc/32768/PG_11_201809051/1/5 to 20191027-181320F\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000002C05DB8EB000000001, lsn = 5db8eb0/180000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 DETAIL: wrote 'tablespace_map' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000002C05DB8EB000000000:0000002C05DB8EB000000001\n"
"P00 INFO: new backup label = 20191027-181320F_20191030-014640I\n"
"P00 INFO: incr backup size = [SIZE], file total = 8\n"
"P00 WARN: backup command encountered 2 checksum warning(s), check the log file for details");
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191027-181320F_20191030-014640I}\n"
"pg_data/PG_VERSION.gz {s=2, ts=-200000}\n"
"pg_data/backup_label.gz {s=17, ts=+2}\n"
"pg_data/base/1/1.gz {s=8207, so=16384, ts=-200000, ckp=t}\n"
"pg_data/base/1/3.gz {s=32768, ckp=[0,3]}\n"
"pg_data/global/pg_control.gz {s=8192}\n"
"pg_data/pg_tblspc/32768> {d=../../pg_tblspc/32768}\n"
"pg_data/pg_wal/\n"
"pg_data/postgresql.conf.gz {s=11, ts=-2400000}\n"
"pg_data/tablespace_map.gz {s=19, ts=+2}\n"
"pg_tblspc/32768/PG_11_201809051/1/5.gz {s=0, ts=-200000, ckp=t}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n"
"pg_tblspc/32768={\"path\":\"../../pg1-tblspc/32768\",\"tablespace-id\":\"32768\""
",\"tablespace-name\":\"tblspc32768\",\"type\":\"link\"}\n",
"compare file list");
// Remove test files
HRN_STORAGE_REMOVE(storagePgWrite(), "base/1/3", .errorOnMissing = true);
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 full backup with tablespaces, bundles, annotations and unexpected pg_control/pg_catalog version");
backupTimeStart = BACKUP_EPOCH + 2400000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawZ(argList, cfgOptManifestSaveThreshold, "1");
hrnCfgArgRawBool(argList, cfgOptArchiveCopy, true);
hrnCfgArgRawZ(argList, cfgOptBufferSize, "16K");
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawBool(argList, cfgOptResume, false);
hrnCfgArgRawZ(argList, cfgOptAnnotation, "extra key=this is an annotation");
hrnCfgArgRawZ(argList, cfgOptAnnotation, "source=this is another annotation");
hrnCfgArgRawZ(argList, cfgOptPgVersionForce, "11");
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Create pg_control with unexpected catalog and control version
HRN_PG_CONTROL_OVERRIDE_VERSION_PUT(
storagePgWrite(), PG_VERSION_11, 1501, .catalogVersion = 202211110, .pageChecksumVersion = 1,
.walSegmentSize = 1024 * 1024);
// Set to a smaller values than the defaults allow
cfgOptionSet(cfgOptRepoBundleSize, cfgSourceParam, VARINT64(pgPageSize8));
cfgOptionSet(cfgOptRepoBundleLimit, cfgSourceParam, VARINT64(pgPageSize8));
// Zero-length file to be stored
HRN_STORAGE_PUT_EMPTY(storagePgWrite(), "zero", .timeModified = backupTimeStart);
// Zeroed file which passes page checksums
Buffer *relation = bufNew(pgPageSize8 * 3);
memset(bufPtr(relation), 0, bufSize(relation));
bufUsedSet(relation, bufSize(relation));
HRN_STORAGE_PUT(storagePgWrite(), PG_PATH_BASE "/1/2", relation, .timeModified = backupTimeStart);
// Old files
HRN_STORAGE_PUT_Z(storagePgWrite(), "postgresql.auto.conf", "CONFIGSTUFF2", .timeModified = 1500000000);
HRN_STORAGE_PUT_Z(storagePgWrite(), "stuff.conf", "CONFIGSTUFF3", .timeModified = 1500000000);
// File that will get skipped while bundling smaller files and end up a bundle by itself
Buffer *bigish = bufNew(pgPageSize8 - 1);
memset(bufPtr(bigish), 0, bufSize(bigish));
bufUsedSet(bigish, bufSize(bigish));
HRN_STORAGE_PUT(storagePgWrite(), "bigish.dat", bigish, .timeModified = 1500000001);
// Run backup
hrnBackupPqScriptP(
PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeGz, .walTotal = 2, .pgVersionForce = STRDEF("11"),
.walSwitch = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DB8EB000000000, lsn = 5db8eb0/0\n"
"P00 INFO: check archive for segment 0000000105DB8EB000000000\n"
"P00 DETAIL: store zero-length file " TEST_PATH "/pg1/zero\n"
"P00 DETAIL: store zero-length file " TEST_PATH "/pg1/pg_tblspc/32768/PG_11_201809051/1/5\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/2 (24KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/base/1/1 (8KB, [PCT]) checksum [SHA1]\n"
"P00 WARN: page misalignment in file " TEST_PATH "/pg1/base/1/1: file size 8207 is not divisible by page size"
" 8192\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.auto.conf (bundle 1/0, 12B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/stuff.conf (bundle 1/32, 12B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/bigish.dat (bundle 1/64, 8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (bundle 2/0, 2B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/postgresql.conf (bundle 2/22, 11B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 2/53, 8KB, [PCT]) checksum [SHA1]\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DB8EB000000001, lsn = 5db8eb0/180000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 DETAIL: wrote 'tablespace_map' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DB8EB000000000:0000000105DB8EB000000001\n"
"P00 DETAIL: copy segment 0000000105DB8EB000000000 to backup\n"
"P00 DETAIL: copy segment 0000000105DB8EB000000001 to backup\n"
"P00 INFO: new backup label = 20191030-014640F\n"
"P00 INFO: full backup size = [SIZE], file total = 14\n"
"P00 WARN: backup command encountered 1 checksum warning(s), check the log file for details");
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191030-014640F}\n"
"bundle/1/pg_data/bigish.dat {s=8191, ts=-72399999}\n"
"bundle/1/pg_data/postgresql.auto.conf {s=12, ts=-72400000}\n"
"bundle/1/pg_data/stuff.conf {s=12, ts=-72400000}\n"
"bundle/2/pg_data/PG_VERSION {s=2, ts=-200000}\n"
"bundle/2/pg_data/global/pg_control {s=8192}\n"
"bundle/2/pg_data/postgresql.conf {s=11, ts=-2400000}\n"
"pg_data/backup_label.gz {s=17, ts=+2}\n"
"pg_data/base/1/1.gz {s=8207, ckp=t}\n"
"pg_data/base/1/2.gz {s=24576, ckp=t}\n"
"pg_data/pg_wal/0000000105DB8EB000000000.gz {s=1048576, ts=+2}\n"
"pg_data/pg_wal/0000000105DB8EB000000001.gz {s=1048576, ts=+2}\n"
"pg_data/tablespace_map.gz {s=19, ts=+2}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n"
"pg_tblspc/32768={\"path\":\"../../pg1-tblspc/32768\",\"tablespace-id\":\"32768\""
",\"tablespace-name\":\"tblspc32768\",\"type\":\"link\"}\n"
"\n"
"[metadata]\n"
"annotation={\"extra key\":\"this is an annotation\",\"source\":\"this is another annotation\"}\n",
"compare file list");
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 diff backup with bundles");
backupTimeStart = BACKUP_EPOCH + 2600000;
{
// Remove old pg data
HRN_STORAGE_PATH_REMOVE(storageTest, "pg1-data", .recurse = true);
HRN_STORAGE_PATH_REMOVE(storageTest, "pg1-tblspc", .recurse = true);
HRN_STORAGE_REMOVE(storageTest, "pg1");
// Update pg_control
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_11, .walSegmentSize = 2 * 1024 * 1024);
// Update version
HRN_STORAGE_PUT_Z(storagePgWrite(), PG_FILE_PGVERSION, PG_VERSION_11_Z, .timeModified = backupTimeStart);
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeDiff);
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawBool(argList, cfgOptDelta, true);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Zero-length file to be stored
HRN_STORAGE_PUT_EMPTY(storagePgWrite(), "zero", .timeModified = backupTimeStart);
// Run backup
hrnBackupPqScriptP(PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeGz, .walTotal = 2, .walSwitch = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: last backup label = 20191030-014640F, version = " PROJECT_VERSION "\n"
"P00 WARN: diff backup cannot alter 'checksum-page' option to 'false', reset to 'true' from 20191030-014640F\n"
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DBBF8000000000, lsn = 5dbbf80/0\n"
"P00 INFO: check archive for segment 0000000105DBBF8000000000\n"
"P00 DETAIL: store zero-length file " TEST_PATH "/pg1/zero\n"
"P01 DETAIL: match file from prior backup " TEST_PATH "/pg1/PG_VERSION (2B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/0, 8KB, [PCT]) checksum [SHA1]\n"
"P00 DETAIL: reference pg_data/PG_VERSION to 20191030-014640F\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DBBF8000000001, lsn = 5dbbf80/300000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DBBF8000000000:0000000105DBBF8000000001\n"
"P00 INFO: new backup label = 20191030-014640F_20191101-092000D\n"
"P00 INFO: diff backup size = [SIZE], file total = 4");
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191030-014640F_20191101-092000D}\n"
"bundle/1/pg_data/global/pg_control {s=8192}\n"
"pg_data/backup_label.gz {s=17, ts=+2}\n"
"20191030-014640F/bundle/2/pg_data/PG_VERSION {s=2}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 full backup with block incr");
backupTimeStart = BACKUP_EPOCH + 2800000;
// Block sizes for testing
#define BLOCK_MIN_SIZE 8192
#define BLOCK_MIN_FILE_SIZE 16384
#define BLOCK_MID_SIZE 16384
#define BLOCK_MID_FILE_SIZE 131072
#define BLOCK_MAX_SIZE 65536
#define BLOCK_MAX_FILE_SIZE 1507328
{
// Remove old pg data
HRN_STORAGE_PATH_REMOVE(storageTest, "pg1", .recurse = true);
// Update pg_control
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_11, .pageChecksumVersion = 0, .walSegmentSize = 2 * 1024 * 1024);
// Update version
HRN_STORAGE_PUT_Z(storagePgWrite(), PG_FILE_PGVERSION, PG_VERSION_11_Z, .timeModified = backupTimeStart);
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawZ(argList, cfgOptCompressType, "none");
hrnCfgArgRawBool(argList, cfgOptResume, false);
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawZ(argList, cfgOptRepoBundleLimit, "23kB");
hrnCfgArgRawBool(argList, cfgOptRepoBlock, true);
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MAX_FILE_SIZE) "b=" STRINGIFY(BLOCK_MAX_SIZE) "b");
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MIN_FILE_SIZE) "=" STRINGIFY(BLOCK_MIN_SIZE));
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MID_FILE_SIZE) "=" STRINGIFY(BLOCK_MID_SIZE));
HRN_CFG_LOAD(cfgCmdBackup, argList);
// File that uses block incr and will grow
Buffer *file = bufNew(BLOCK_MIN_SIZE * 3);
memset(bufPtr(file), 0, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-grow", file, .timeModified = backupTimeStart);
// File that uses block incr and will not be resumed
file = bufNew(BLOCK_MIN_SIZE * 3);
memset(bufPtr(file), 0, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-no-resume", file, .timeModified = backupTimeStart);
// Error when pg_control is missing after backup start
HRN_BACKUP_SCRIPT_SET(
{.op = hrnBackupScriptOpRemove, .file = storagePathP(storagePg(), STRDEF("global/pg_control"))});
hrnBackupPqScriptP(
PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeGz, .walTotal = 2, .walSwitch = true,
.errorAfterCopyStart = true);
TEST_ERROR(
hrnCmdBackup(), FileMissingError,
"raised from local-1 shim protocol: unable to open missing file '" TEST_PATH "/pg1/global/pg_control' for read");
TEST_RESULT_LOG(
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DBF06000000000, lsn = 5dbf060/0\n"
"P00 INFO: check archive for segment 0000000105DBF06000000000\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-grow (24KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-no-resume (24KB, [PCT]) checksum [SHA1]");
HRN_PG_CONTROL_PUT(storagePgWrite(), PG_VERSION_11, .pageChecksumVersion = 0, .walSegmentSize = 2 * 1024 * 1024);
// Run backup
hrnBackupPqScriptP(PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeGz, .walTotal = 2, .walSwitch = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DBF06000000000, lsn = 5dbf060/0\n"
"P00 INFO: check archive for segment 0000000105DBF06000000000\n"
"P00 INFO: backup '20191103-165320F' cannot be resumed: partially deleted by prior resume or invalid\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-grow (24KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-no-resume (24KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (bundle 1/0, 2B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/2, 8KB, [PCT]) checksum [SHA1]\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DBF06000000001, lsn = 5dbf060/300000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DBF06000000000:0000000105DBF06000000001\n"
"P00 INFO: new backup label = 20191103-165320F\n"
"P00 INFO: full backup size = [SIZE], file total = 5");
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191103-165320F}\n"
"bundle/1/pg_data/PG_VERSION {s=2}\n"
"bundle/1/pg_data/global/pg_control {s=8192}\n"
"pg_data/backup_label {s=17, ts=+2}\n"
"pg_data/block-incr-grow.pgbi {s=24576, m=0:{0,1,2}}\n"
"pg_data/block-incr-no-resume.pgbi {s=24576, m=0:{0,1,2}}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 full backup with block incr resume");
backupTimeStart = BACKUP_EPOCH + 2900000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawZ(argList, cfgOptCompressType, "none");
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawZ(argList, cfgOptRepoBundleLimit, "23kB");
hrnCfgArgRawBool(argList, cfgOptRepoBlock, true);
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MAX_FILE_SIZE) "b=" STRINGIFY(BLOCK_MAX_SIZE) "b");
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MIN_FILE_SIZE) "=" STRINGIFY(BLOCK_MIN_SIZE));
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MID_FILE_SIZE) "=" STRINGIFY(BLOCK_MID_SIZE));
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Make this backup look resumable
HRN_STORAGE_REMOVE(storageTest, "repo/backup/test1/20191103-165320F/backup.manifest");
// Corrupt file that uses block incr and will not be resumed
Buffer *file = bufNew(BLOCK_MIN_SIZE * 3);
memset(bufPtr(file), 99, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storageRepoWrite(), "backup/test1/20191103-165320F/pg_data/block-incr-no-resume.pgbi", file);
// File that shrinks below the limit where it would get block incremental if it were new
file = bufNew(BLOCK_MIN_FILE_SIZE + 1);
memset(bufPtr(file), 55, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-shrink", file, .timeModified = backupTimeStart);
// File that shrinks to the size of a single block
file = bufNew(BLOCK_MIN_FILE_SIZE);
memset(bufPtr(file), 44, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-shrink-block", file, .timeModified = backupTimeStart);
// File that shrinks below the size of a single block
file = bufNew(BLOCK_MIN_FILE_SIZE);
memset(bufPtr(file), 43, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-shrink-below", file, .timeModified = backupTimeStart);
// Block incremental file that remains the same between backups
file = bufNew(BLOCK_MIN_FILE_SIZE);
memset(bufPtr(file), 33, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-same", file, .timeModified = backupTimeStart);
// Normal file that remains the same between backups
HRN_STORAGE_PUT_Z(storagePgWrite(), "normal-same", "SAME", .timeModified = backupTimeStart);
// Block incremental file that is too large to bundle and will be truncated to zero during the next backup
file = bufNew(BLOCK_MIN_SIZE * 3);
memset(bufPtr(file), 22, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-truncate", file, .timeModified = backupTimeStart);
// File that grows above the limit
file = bufNew(BLOCK_MIN_FILE_SIZE - 1);
memset(bufPtr(file), 77, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "grow-to-block-incr", file, .timeModified = backupTimeStart);
// Block incremental file within the bundle limit that will grow past it
file = bufNew(BLOCK_MIN_FILE_SIZE);
memset(bufPtr(file), 88, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "grow-past-bundle-limit", file, .timeModified = backupTimeStart);
// Run backup
hrnBackupPqScriptP(PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeGz, .walTotal = 2, .walSwitch = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 WARN: backup '20191103-165320F' missing manifest removed from backup.info\n"
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DC08C000000000, lsn = 5dc08c0/0\n"
"P00 INFO: check archive for segment 0000000105DC08C000000000\n"
"P00 WARN: resumable backup 20191103-165320F of same type exists -- invalid files will be removed then the backup"
" will resume\n"
"P00 DETAIL: remove path '" TEST_PATH "/repo/backup/test1/20191103-165320F/bundle' from resumed backup\n"
"P00 DETAIL: remove file '" TEST_PATH "/repo/backup/test1/20191103-165320F/pg_data/backup_label' from resumed"
" backup (missing in manifest)\n"
"P01 DETAIL: checksum resumed file " TEST_PATH "/pg1/block-incr-grow (24KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-no-resume (24KB, [PCT]) checksum [SHA1]\n"
"P00 WARN: resumed backup file pg_data/block-incr-no-resume did not have expected checksum"
" ebdd38b69cd5b9f2d00d273c981e16960fbbb4f7. The file was recopied and backup will continue but this may be an issue"
" unless the resumed backup path in the repository is known to be corrupted.\n"
" NOTE: this does not indicate a problem with the PostgreSQL page checksums.\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-truncate (24KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-same (bundle 1/0, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink-below (bundle 1/16403, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink-block (bundle 1/32809, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/grow-past-bundle-limit (bundle 1/49215, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink (bundle 1/65621, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (bundle 1/82035, 2B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/normal-same (bundle 1/82037, 4B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/grow-to-block-incr (bundle 1/82041, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/98424, 8KB, [PCT]) checksum [SHA1]\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DC08C000000001, lsn = 5dc08c0/300000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DC08C000000000:0000000105DC08C000000001\n"
"P00 INFO: new backup label = 20191103-165320F\n"
"P00 INFO: full backup size = [SIZE], file total = 13");
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191103-165320F}\n"
"bundle/1/pg_data/PG_VERSION {s=2, ts=-100000}\n"
"bundle/1/pg_data/block-incr-same {s=16384, m=0:{0,1}}\n"
"bundle/1/pg_data/block-incr-shrink {s=16385, m=0:{0,1,2}}\n"
"bundle/1/pg_data/block-incr-shrink-below {s=16384, m=0:{0,1}}\n"
"bundle/1/pg_data/block-incr-shrink-block {s=16384, m=0:{0,1}}\n"
"bundle/1/pg_data/global/pg_control {s=8192}\n"
"bundle/1/pg_data/grow-past-bundle-limit {s=16384, m=0:{0,1}}\n"
"bundle/1/pg_data/grow-to-block-incr {s=16383}\n"
"bundle/1/pg_data/normal-same {s=4}\n"
"pg_data/backup_label {s=17, ts=+2}\n"
"pg_data/block-incr-grow.pgbi {s=24576, m=0:{0,1,2}, ts=-100000}\n"
"pg_data/block-incr-no-resume.pgbi {s=24576, m=0:{0,1,2}, ts=-100000}\n"
"pg_data/block-incr-truncate.pgbi {s=24576, m=0:{0,1,2}}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
HRN_STORAGE_REMOVE(storagePgWrite(), "block-incr-no-resume");
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 diff backup with block incr");
backupTimeStart = BACKUP_EPOCH + 3000000;
{
// Load the previous manifest and null out the checksum-page option to be sure it gets set to false in this backup
const String *manifestPriorFile = STRDEF(STORAGE_REPO_BACKUP "/20191103-165320F/" BACKUP_MANIFEST_FILE);
Manifest *manifestPrior = manifestNewLoad(
storageReadIo(storageNewReadP(storageRepo(), manifestPriorFile)), cipherSpecNewNone());
manifestPrior->pub.data.backupOptionChecksumPage = NULL;
manifestSave(manifestPrior, storageWriteIo(storageNewWriteP(storageRepoWrite(), manifestPriorFile)));
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeDiff);
hrnCfgArgRawZ(argList, cfgOptCompressType, "none");
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawZ(argList, cfgOptRepoBundleLimit, "23kB");
hrnCfgArgRawBool(argList, cfgOptRepoBlock, true);
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MAX_FILE_SIZE) "=" STRINGIFY(BLOCK_MAX_SIZE));
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MIN_FILE_SIZE) "=" STRINGIFY(BLOCK_MIN_SIZE));
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MID_FILE_SIZE) "=" STRINGIFY(BLOCK_MID_SIZE));
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeSuper, "1MiB");
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Grow file size to check block incr delta. This is large enough that it would get a new block size if it were a new
// file rather than a delta. Also split the first super block.
Buffer *file = bufNew(BLOCK_MID_FILE_SIZE);
memset(bufPtr(file), 0, bufSize(file));
memset(bufPtr(file) + BLOCK_MIN_SIZE, 1, BLOCK_MIN_SIZE);
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-grow", file, .timeModified = backupTimeStart);
// File that gets a larger block size and multiple super blocks
file = bufNew(BLOCK_MAX_FILE_SIZE);
memset(bufPtr(file), 0, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-larger", file, .timeModified = backupTimeStart);
// Shrink file below the limit where it would get block incremental if it were new
file = bufNew(BLOCK_MIN_FILE_SIZE - 1);
memset(bufPtr(file), 55, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-shrink", file, .timeModified = backupTimeStart);
// Shrink file to the size of a single block
file = bufNew(BLOCK_MIN_SIZE);
memset(bufPtr(file), 44, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-shrink-block", file, .timeModified = backupTimeStart);
// Shrinks file below the size of a single block
file = bufNew(8);
memset(bufPtr(file), 44, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-shrink-below", file, .timeModified = backupTimeStart);
// Update timestamps without changing contents of the files
HRN_STORAGE_TIME(storagePgWrite(), "block-incr-same", backupTimeStart);
HRN_STORAGE_TIME(storagePgWrite(), "normal-same", backupTimeStart);
// Update timestamp so the block incremental file that is too large to bundle will be copied
HRN_STORAGE_TIME(storagePgWrite(), "block-incr-truncate", backupTimeStart);
// Grow file above the limit
file = bufNew(BLOCK_MIN_FILE_SIZE + 1);
memset(bufPtr(file), 77, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "grow-to-block-incr", file, .timeModified = backupTimeStart);
// Grow file past the bundle limit. The prior size is within the limit but the file must be stored unbundled since the
// original size is what will be copied.
file = bufNew(BLOCK_MIN_FILE_SIZE * 2);
memset(bufPtr(file), 88, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "grow-past-bundle-limit", file, .timeModified = backupTimeStart);
// File that gets truncated to zero during the backup
HRN_STORAGE_PUT(storagePgWrite(), "truncate-to-zero", BUFSTRDEF("DATA"), .timeModified = backupTimeStart);
// Error when the prior block map is not where the manifest says it is. Truncate the file that contains the map so the
// read comes up short, then restore it before running the backup for real.
const String *const blockIncrGrowFile = STRDEF(
STORAGE_REPO_BACKUP "/20191103-165320F/pg_data/block-incr-grow" BACKUP_BLOCK_INCR_EXT);
const Buffer *const blockIncrGrowBuffer = storageGetP(storageNewReadP(storageRepo(), blockIncrGrowFile));
HRN_STORAGE_PUT_Z(storageRepoWrite(), strZ(blockIncrGrowFile), "");
hrnBackupPqScriptP(
PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeGz, .walTotal = 2, .walSwitch = true,
.errorAfterCopyStart = true);
TEST_ERROR(
hrnCmdBackup(), FileReadError,
"raised from local-1 shim protocol: unable to read block map for 'pg_data/block-incr-grow' from '" TEST_PATH
"/repo/backup/test1/20191103-165320F/pg_data/block-incr-grow.pgbi' at offset 24576 size 24: expected 24 byte(s) but"
" read 0");
TEST_RESULT_LOG(
"P00 INFO: last backup label = 20191103-165320F, version = " PROJECT_VERSION "\n"
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DC213000000000, lsn = 5dc2130/0\n"
"P00 INFO: check archive for segment 0000000105DC213000000000\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-larger (1.4MB, [PCT]) checksum [SHA1]");
HRN_STORAGE_PUT(storageRepoWrite(), strZ(blockIncrGrowFile), blockIncrGrowBuffer);
// Run backup
HRN_BACKUP_SCRIPT_SET(
{.op = hrnBackupScriptOpUpdate, .file = storagePathP(storagePg(), STRDEF("truncate-to-zero")),
.time = backupTimeStart + 1},
{.op = hrnBackupScriptOpUpdate, .file = storagePathP(storagePg(), STRDEF("block-incr-truncate")),
.time = backupTimeStart + 1});
hrnBackupPqScriptP(PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeGz, .walTotal = 2, .walSwitch = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: last backup label = 20191103-165320F, version = " PROJECT_VERSION "\n"
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DC213000000000, lsn = 5dc2130/0\n"
"P00 INFO: check archive for segment 0000000105DC213000000000\n"
"P00 INFO: backup '20191103-165320F_20191106-002640D' cannot be resumed: resume only valid for full backup\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-larger (1.4MB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-grow (128KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/grow-past-bundle-limit (32KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: store truncated file " TEST_PATH "/pg1/block-incr-truncate (24KB->0B, [PCT])\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/grow-to-block-incr (bundle 1/0, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: match file from prior backup " TEST_PATH "/pg1/block-incr-same (16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink-block (bundle 1/16411, 8KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink (bundle 1/16428, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: match file from prior backup " TEST_PATH "/pg1/normal-same (4B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: store truncated file " TEST_PATH "/pg1/truncate-to-zero (4B->0B, [PCT])\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-shrink-below (bundle 1/24654, 8B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/24662, 8KB, [PCT]) checksum [SHA1]\n"
"P00 DETAIL: reference pg_data/PG_VERSION to 20191103-165320F\n"
"P00 DETAIL: reference pg_data/block-incr-same to 20191103-165320F\n"
"P00 DETAIL: reference pg_data/normal-same to 20191103-165320F\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DC213000000001, lsn = 5dc2130/300000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DC213000000000:0000000105DC213000000001\n"
"P00 INFO: new backup label = 20191103-165320F_20191106-002640D\n"
"P00 INFO: diff backup size = [SIZE], file total = 14");
TEST_RESULT_STR_Z(
testBackupValidateP(storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest")),
".> {d=20191103-165320F_20191106-002640D}\n"
"bundle/1/pg_data/block-incr-shrink {s=16383, m=0:{0},1:{0}}\n"
"bundle/1/pg_data/block-incr-shrink-below {s=8}\n"
"bundle/1/pg_data/block-incr-shrink-block {s=8192, m=0:{0}}\n"
"bundle/1/pg_data/global/pg_control {s=8192}\n"
"bundle/1/pg_data/grow-to-block-incr {s=16385, m=1:{0,1,2}}\n"
"pg_data/backup_label {s=17, ts=+2}\n"
"pg_data/block-incr-grow.pgbi {s=131072, m=0:{0},1:{0},0:{2},1:{1,2,3,4,5,6,7,8,9,10,11,12,13}}\n"
"pg_data/block-incr-larger.pgbi {s=1507328, m=1:{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},1:{0,1,2,3,4,5,6}}\n"
"pg_data/grow-past-bundle-limit.pgbi {s=32768, m=0:{0,1},1:{0,1}}\n"
"20191103-165320F/bundle/1/pg_data/PG_VERSION {s=2, ts=-200000}\n"
"20191103-165320F/bundle/1/pg_data/block-incr-same {s=16384, m=0:{0,1}}\n"
"20191103-165320F/bundle/1/pg_data/normal-same {s=4}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
HRN_STORAGE_REMOVE(storagePgWrite(), "block-incr-grow");
HRN_STORAGE_REMOVE(storagePgWrite(), "block-incr-larger");
HRN_STORAGE_REMOVE(storagePgWrite(), "block-incr-same");
HRN_STORAGE_REMOVE(storagePgWrite(), "block-incr-shrink");
HRN_STORAGE_REMOVE(storagePgWrite(), "block-incr-shrink-below");
HRN_STORAGE_REMOVE(storagePgWrite(), "block-incr-shrink-block");
HRN_STORAGE_REMOVE(storagePgWrite(), "block-incr-truncate");
HRN_STORAGE_REMOVE(storagePgWrite(), "grow-past-bundle-limit");
HRN_STORAGE_REMOVE(storagePgWrite(), "grow-to-block-incr");
HRN_STORAGE_REMOVE(storagePgWrite(), "normal-same");
HRN_STORAGE_REMOVE(storagePgWrite(), "truncate-to-zero");
}
// It is better to put as few tests here as possible because cmp/enc makes tests more expensive (especially with valgrind)
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 full backup with comp/enc");
backupTimeStart = BACKUP_EPOCH + 3200000;
{
// Create encrypted stanza
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawBool(argList, cfgOptOnline, false);
hrnCfgArgRawZ(argList, cfgOptRepoCipherType, "aes-256-cbc");
hrnCfgEnvRawZ(cfgOptRepoCipherPass, TEST_CIPHER_PASS);
HRN_CFG_LOAD(cfgCmdStanzaCreate, argList);
HRN_STORAGE_PATH_REMOVE(storageRepoIdxWrite(0), NULL, .recurse = true);
cmdStanzaCreate();
TEST_RESULT_LOG("P00 INFO: stanza-create for stanza 'test1' on repo1");
// Load options
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawZ(argList, cfgOptRepoBundleLimit, "8KiB");
hrnCfgArgRawBool(argList, cfgOptRepoBlock, true);
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MAX_FILE_SIZE) "=" STRINGIFY(BLOCK_MAX_SIZE));
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MIN_FILE_SIZE) "=" STRINGIFY(BLOCK_MIN_SIZE));
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MID_FILE_SIZE) "=" STRINGIFY(BLOCK_MID_SIZE));
hrnCfgArgRawZ(argList, cfgOptBufferSize, "16KiB");
hrnCfgArgRawZ(argList, cfgOptRepoCipherType, "aes-256-cbc");
hrnCfgEnvRawZ(cfgOptRepoCipherPass, TEST_CIPHER_PASS);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// File that uses block incr and will grow
Buffer *file = bufNew((size_t)(BLOCK_MIN_FILE_SIZE * 1.5));
memset(bufPtr(file), 0, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-grow", file, .timeModified = backupTimeStart);
// File that uses block incr and will not be resumed
file = bufNew((size_t)(BLOCK_MIN_FILE_SIZE * 1.5));
memset(bufPtr(file), 0, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-no-resume", file, .timeModified = backupTimeStart);
// Run backup
hrnBackupPqScriptP(
PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeNone, .cipherSpecMain = TEST_CIPHER_SPEC,
.walTotal = 2, .walSwitch = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DC520000000000, lsn = 5dc5200/0\n"
"P00 INFO: check archive for segment 0000000105DC520000000000\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-grow (24KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-no-resume (24KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (bundle 1/0, 2B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/24, 8KB, [PCT]) checksum [SHA1]\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DC520000000001, lsn = 5dc5200/300000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DC520000000000:0000000105DC520000000001\n"
"P00 INFO: new backup label = 20191108-080000F\n"
"P00 INFO: full backup size = [SIZE], file total = 5");
TEST_RESULT_STR_Z(
testBackupValidateP(
storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest"), .cipherSpecMain = TEST_CIPHER_SPEC),
".> {d=20191108-080000F}\n"
"bundle/1/pg_data/PG_VERSION {s=2, ts=-400000}\n"
"bundle/1/pg_data/global/pg_control {s=8192}\n"
"pg_data/backup_label.gz {s=17, ts=+2}\n"
"pg_data/block-incr-grow.pgbi {s=24576, m=0:{0,1,2}}\n"
"pg_data/block-incr-no-resume.pgbi {s=24576, m=0:{0,1,2}}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 full backup with comp/enc resume");
backupTimeStart = BACKUP_EPOCH + 3300000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawBool(argList, cfgOptDelta, true);
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawZ(argList, cfgOptRepoBundleLimit, "8KiB");
hrnCfgArgRawBool(argList, cfgOptRepoBlock, true);
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MAX_FILE_SIZE) "=" STRINGIFY(BLOCK_MAX_SIZE));
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MIN_FILE_SIZE) "=" STRINGIFY(BLOCK_MIN_SIZE));
hrnCfgArgRawZ(argList, cfgOptRepoBlockSizeMap, STRINGIFY(BLOCK_MID_FILE_SIZE) "=" STRINGIFY(BLOCK_MID_SIZE));
hrnCfgArgRawZ(argList, cfgOptBufferSize, "16KiB");
hrnCfgArgRawZ(argList, cfgOptRepoCipherType, "aes-256-cbc");
hrnCfgEnvRawZ(cfgOptRepoCipherPass, TEST_CIPHER_PASS);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Make this backup look resumable
HRN_STORAGE_REMOVE(storageTest, "repo/backup/test1/20191108-080000F/backup.manifest");
// File that will later have a timestamp far enough in the past to make the block size zero
Buffer *file = bufNew((size_t)(BLOCK_MIN_FILE_SIZE));
memset(bufPtr(file), 0, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-wayback", file, .timeModified = backupTimeStart);
// Modify file that uses block incr and will not be resumed so it is caught by delta
file = bufNew((size_t)(BLOCK_MIN_FILE_SIZE * 1.5));
memset(bufPtr(file), 0xFE, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-no-resume", file, .timeModified = backupTimeStart - 100000);
// Run backup
hrnBackupPqScriptP(
PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeNone, .cipherSpecMain = TEST_CIPHER_SPEC,
.walTotal = 2, .walSwitch = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 WARN: backup '20191108-080000F' missing manifest removed from backup.info\n"
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DC6A7000000000, lsn = 5dc6a70/0\n"
"P00 INFO: check archive for segment 0000000105DC6A7000000000\n"
"P00 WARN: resumable backup 20191108-080000F of same type exists -- invalid files will be removed then the backup"
" will resume\n"
"P00 DETAIL: remove path '" TEST_PATH "/repo/backup/test1/20191108-080000F/bundle' from resumed backup\n"
"P00 DETAIL: remove file '" TEST_PATH "/repo/backup/test1/20191108-080000F/pg_data/backup_label.gz' from resumed"
" backup (missing in manifest)\n"
"P01 DETAIL: checksum resumed file " TEST_PATH "/pg1/block-incr-grow (24KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-no-resume (24KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-wayback (16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (bundle 1/0, 2B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/24, 8KB, [PCT]) checksum [SHA1]\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DC6A7000000001, lsn = 5dc6a70/300000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DC6A7000000000:0000000105DC6A7000000001\n"
"P00 INFO: new backup label = 20191108-080000F\n"
"P00 INFO: full backup size = [SIZE], file total = 6");
TEST_RESULT_STR_Z(
testBackupValidateP(
storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest"), .cipherSpecMain = TEST_CIPHER_SPEC),
".> {d=20191108-080000F}\n"
"bundle/1/pg_data/PG_VERSION {s=2, ts=-500000}\n"
"bundle/1/pg_data/global/pg_control {s=8192}\n"
"pg_data/backup_label.gz {s=17, ts=+2}\n"
"pg_data/block-incr-grow.pgbi {s=24576, m=0:{0,1,2}, ts=-100000}\n"
"pg_data/block-incr-no-resume.pgbi {s=24576, m=0:{0,1,2}, ts=-100000}\n"
"pg_data/block-incr-wayback.pgbi {s=16384, m=0:{0,1}}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
HRN_STORAGE_REMOVE(storagePgWrite(), "block-incr-no-resume");
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 diff backup with comp/enc and delta");
backupTimeStart = BACKUP_EPOCH + 3400000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeDiff);
hrnCfgArgRawZ(argList, cfgOptCompressType, "bz2");
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawZ(argList, cfgOptRepoBundleLimit, "4MiB");
hrnCfgArgRawBool(argList, cfgOptRepoBlock, true);
hrnCfgArgRawZ(argList, cfgOptRepoCipherType, "aes-256-cbc");
hrnCfgArgRawZ(argList, cfgOptRepoBlockAgeMap, "1=2");
hrnCfgArgRawZ(argList, cfgOptRepoBlockAgeMap, "2=0");
hrnCfgArgRawZ(argList, cfgOptRepoBlockChecksumSizeMap, "16KiB=16");
hrnCfgArgRawZ(argList, cfgOptRepoBlockChecksumSizeMap, "8KiB=12");
hrnCfgEnvRawZ(cfgOptRepoCipherPass, TEST_CIPHER_PASS);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// File that uses block incr and grows and overwrites last block of prior map
Buffer *file = bufNew(BLOCK_MIN_FILE_SIZE * 3);
memset(bufPtr(file), 0, bufSize(file));
memset(bufPtr(file) + (BLOCK_MIN_SIZE * 2), 1, BLOCK_MIN_SIZE);
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-incr-grow", file, .timeModified = backupTimeStart - 200000);
// File with age multiplier
file = bufNew(BLOCK_MIN_FILE_SIZE * 2);
memset(bufPtr(file), 0, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-age-multiplier", file, .timeModified = backupTimeStart - SEC_PER_DAY);
// File with age old enough to not have block incr
file = bufNew(BLOCK_MIN_FILE_SIZE);
memset(bufPtr(file), 0, bufSize(file));
bufUsedSet(file, bufSize(file));
HRN_STORAGE_PUT(storagePgWrite(), "block-age-to-zero", file, .timeModified = backupTimeStart - 2 * SEC_PER_DAY);
// File goes back in time far enough to not need block incr if it was new
HRN_STORAGE_TIME(storagePgWrite(), "block-incr-wayback", backupTimeStart - 2 * SEC_PER_DAY);
// Run backup
hrnBackupPqScriptP(
PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeNone, .cipherSpecMain = TEST_CIPHER_SPEC,
.walTotal = 2, .walSwitch = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: last backup label = 20191108-080000F, version = " PROJECT_VERSION "\n"
"P00 WARN: diff backup cannot alter compress-type option to 'bz2', reset to value in 20191108-080000F\n"
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DC82D000000000, lsn = 5dc82d0/0\n"
"P00 INFO: check archive for segment 0000000105DC82D000000000\n"
"P00 WARN: file 'block-incr-grow' has same timestamp (1573200000) as prior but different size (prior 24576,"
" current 49152), enabling delta checksum\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-age-multiplier (bundle 1/0, 32KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-incr-grow (bundle 1/128, 48KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: match file from prior backup " TEST_PATH "/pg1/block-incr-wayback (16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: match file from prior backup " TEST_PATH "/pg1/PG_VERSION (2B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/block-age-to-zero (bundle 1/272, 16KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/328, 8KB, [PCT]) checksum [SHA1]\n"
"P00 DETAIL: reference pg_data/PG_VERSION to 20191108-080000F\n"
"P00 DETAIL: reference pg_data/block-incr-wayback to 20191108-080000F\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DC82D000000001, lsn = 5dc82d0/300000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DC82D000000000:0000000105DC82D000000001\n"
"P00 INFO: new backup label = 20191108-080000F_20191110-153320D\n"
"P00 INFO: diff backup size = [SIZE], file total = 7");
TEST_RESULT_STR_Z(
testBackupValidateP(
storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest"), .cipherSpecMain = TEST_CIPHER_SPEC),
".> {d=20191108-080000F_20191110-153320D}\n"
"bundle/1/pg_data/block-age-multiplier {s=32768, m=1:{0,1}, ts=-86400}\n"
"bundle/1/pg_data/block-age-to-zero {s=16384, ts=-172800}\n"
"bundle/1/pg_data/block-incr-grow {s=49152, m=0:{0,1},1:{0,1,2,3}, ts=-200000}\n"
"bundle/1/pg_data/global/pg_control {s=8192}\n"
"pg_data/backup_label.gz {s=17, ts=+2}\n"
"20191108-080000F/bundle/1/pg_data/PG_VERSION {s=2, ts=-600000}\n"
"20191108-080000F/pg_data/block-incr-wayback.pgbi {s=16384, m=0:{0,1}, ts=-172800}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
}
// Ensure that disabling bundling does not break the backup. In particular this ensures the bundleRaw setting is preserved.
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 incr backup with comp/enc which disables bundling");
backupTimeStart = BACKUP_EPOCH + 3450000;
{
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeIncr);
hrnCfgArgRawZ(argList, cfgOptCompressType, "bz2");
hrnCfgArgRawZ(argList, cfgOptRepoCipherType, "aes-256-cbc");
hrnCfgEnvRawZ(cfgOptRepoCipherPass, TEST_CIPHER_PASS);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// Run backup
hrnBackupPqScriptP(
PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeNone, .cipherSpecMain = TEST_CIPHER_SPEC,
.walTotal = 1, .walSwitch = false);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
TEST_RESULT_LOG(
"P00 INFO: last backup label = 20191108-080000F_20191110-153320D, version = " PROJECT_VERSION "\n"
"P00 WARN: incr backup cannot alter compress-type option to 'bz2', reset to value in"
" 20191108-080000F_20191110-153320D\n"
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DC8F1000000000, lsn = 5dc8f10/0\n"
"P00 INFO: check archive for prior segment 0000000105DC8F0F000007FF\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (8KB, [PCT]) checksum [SHA1]\n"
"P00 DETAIL: reference pg_data/PG_VERSION to 20191108-080000F\n"
"P00 DETAIL: reference pg_data/block-age-multiplier to 20191108-080000F_20191110-153320D\n"
"P00 DETAIL: reference pg_data/block-age-to-zero to 20191108-080000F_20191110-153320D\n"
"P00 DETAIL: reference pg_data/block-incr-grow to 20191108-080000F_20191110-153320D\n"
"P00 DETAIL: reference pg_data/block-incr-wayback to 20191108-080000F\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DC8F1000000000, lsn = 5dc8f10/100000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DC8F1000000000:0000000105DC8F1000000000\n"
"P00 INFO: new backup label = 20191108-080000F_20191111-052640I\n"
"P00 INFO: incr backup size = [SIZE], file total = 7");
TEST_RESULT_STR_Z(
testBackupValidateP(
storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest"), .cipherSpecMain = TEST_CIPHER_SPEC),
".> {d=20191108-080000F_20191111-052640I}\n"
"pg_data/backup_label.gz {s=17, ts=+2}\n"
"pg_data/global/pg_control.gz {s=8192}\n"
"20191108-080000F/bundle/1/pg_data/PG_VERSION {s=2, ts=-650000}\n"
"20191108-080000F_20191110-153320D/bundle/1/pg_data/block-age-multiplier {s=32768, m=1:{0,1}, ts=-136400}\n"
"20191108-080000F_20191110-153320D/bundle/1/pg_data/block-age-to-zero {s=16384, ts=-222800}\n"
"20191108-080000F_20191110-153320D/bundle/1/pg_data/block-incr-grow {s=49152, m=0:{0,1},1:{0,1,2,3}, ts=-250000}\n"
"20191108-080000F/pg_data/block-incr-wayback.pgbi {s=16384, m=0:{0,1}, ts=-222800}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
}
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("online 11 full backup with enc");
backupTimeStart = BACKUP_EPOCH + 3500000;
{
// Remove old pg data
HRN_STORAGE_PATH_REMOVE(storageTest, "pg1", .recurse = true);
// Update pg_control
HRN_PG_CONTROL_PUT(
storagePgWrite(), PG_VERSION_11, .pageChecksumVersion = 1, .walSegmentSize = 2 * 1024 * 1024,
.pageSize = pgPageSize4);
// Update version
HRN_STORAGE_PUT_Z(storagePgWrite(), PG_FILE_PGVERSION, PG_VERSION_11_Z, .timeModified = backupTimeStart);
// Load options
StringList *argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test1");
hrnCfgArgRaw(argList, cfgOptRepoPath, repoPath);
hrnCfgArgRaw(argList, cfgOptPgPath, pg1Path);
hrnCfgArgRawZ(argList, cfgOptRepoRetentionFull, "1");
hrnCfgArgRawStrId(argList, cfgOptType, backupTypeFull);
hrnCfgArgRawBool(argList, cfgOptRepoBundle, true);
hrnCfgArgRawZ(argList, cfgOptRepoBundleLimit, "8KiB");
hrnCfgArgRawZ(argList, cfgOptCompressType, "none");
hrnCfgArgRawZ(argList, cfgOptRepoCipherType, "aes-256-cbc");
hrnCfgEnvRawZ(cfgOptRepoCipherPass, TEST_CIPHER_PASS);
HRN_CFG_LOAD(cfgCmdBackup, argList);
// File that will grow during the backup
Buffer *const fileGrow = bufNew(pgPageSize4 * 4);
memset(bufPtr(fileGrow), 0, pgPageSize4 * 3);
bufUsedSet(fileGrow, pgPageSize4 * 3);
HRN_STORAGE_PUT(storagePgWrite(), "global/1", fileGrow, .timeModified = backupTimeStart);
memset(bufPtr(fileGrow) + pgPageSize4 * 3, 0xFF, pgPageSize4);
bufUsedSet(fileGrow, bufSize(fileGrow));
// Also write a copy of it that will get a checksum error, just to be sure the read limit on global/1 is working
HRN_STORAGE_PUT(storagePgWrite(), "global/2", fileGrow, .timeModified = backupTimeStart);
// Run backup
HRN_BACKUP_SCRIPT_SET(
{.op = hrnBackupScriptOpUpdate, .file = storagePathP(storagePg(), STRDEF("global/1")),
.time = backupTimeStart + 1, .content = fileGrow});
hrnBackupPqScriptP(
PG_VERSION_11, backupTimeStart, .walCompressType = compressTypeNone, .cipherSpecMain = TEST_CIPHER_SPEC,
.walTotal = 2, .walSwitch = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
// Make sure that global/1 grew as expected but the extra bytes were not copied
TEST_RESULT_UINT(storageInfoP(storagePgWrite(), STRDEF("global/1")).size, 16384, "check global/1 grew");
TEST_RESULT_LOG(
"P00 INFO: execute backup start: backup begins after the next regular checkpoint completes\n"
"P00 INFO: backup start archive = 0000000105DC9B4000000000, lsn = 5dc9b40/0\n"
"P00 INFO: check archive for segment 0000000105DC9B4000000000\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/2 (16KB, [PCT]) checksum [SHA1]\n"
"P00 WARN: invalid page checksum found in file " TEST_PATH "/pg1/global/2 at page 3\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/1 (12KB, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/PG_VERSION (bundle 1/0, 2B, [PCT]) checksum [SHA1]\n"
"P01 DETAIL: backup file " TEST_PATH "/pg1/global/pg_control (bundle 1/32, 8KB, [PCT]) checksum [SHA1]\n"
"P00 INFO: execute backup stop and wait for all WAL segments to archive\n"
"P00 INFO: backup stop archive = 0000000105DC9B4000000001, lsn = 5dc9b40/300000\n"
"P00 DETAIL: wrote 'backup_label' file returned from backup stop function\n"
"P00 INFO: check archive for segment(s) 0000000105DC9B4000000000:0000000105DC9B4000000001\n"
"P00 INFO: new backup label = 20191111-192000F\n"
"P00 INFO: full backup size = [SIZE], file total = 5\n"
"P00 WARN: backup command encountered 1 checksum warning(s), check the log file for details");
TEST_RESULT_STR_Z(
testBackupValidateP(
storageRepo(), STRDEF(STORAGE_REPO_BACKUP "/latest"), .cipherSpecMain = TEST_CIPHER_SPEC),
".> {d=20191111-192000F}\n"
"bundle/1/pg_data/PG_VERSION {s=2}\n"
"bundle/1/pg_data/global/pg_control {s=8192}\n"
"pg_data/backup_label {s=17, ts=+2}\n"
"pg_data/global/1 {s=12288, ckp=t}\n"
"pg_data/global/2 {s=16384, ckp=[3]}\n"
"--------\n"
"[backup:target]\n"
"pg_data={\"path\":\"" TEST_PATH "/pg1\",\"type\":\"path\"}\n",
"compare file list");
}
}
FUNCTION_HARNESS_RETURN_VOID();
}