mirror of https://github.com/mongodb/mongo
SERVER-91914 Refactor MagicRestoreUtils to manage restore for all nodes in a replica set (#25299)
GitOrigin-RevId: 662d40c06e8bc1cf7ff408f15f8ff83ca86f1de3
This commit is contained in:
parent
900c17bbde
commit
8326ec3a84
|
|
@ -156,6 +156,7 @@ globals:
|
|||
pathExists: true
|
||||
umask: true
|
||||
getFileMode: true
|
||||
copyDir: true
|
||||
|
||||
# likely could be replaced with `child_process`
|
||||
MongoRunner: true
|
||||
|
|
|
|||
|
|
@ -233,17 +233,25 @@ export function checkBackup(backupCursor) {
|
|||
|
||||
// Magic restore utility class
|
||||
|
||||
// This class implements helpers for testing the magic restore proces. It maintains the state of the
|
||||
// backup cursor and handles writing objects to named pipes and running magic restore on a single
|
||||
// node. It exposes some of this state so that individual tests can make specific assertions as
|
||||
// needed.
|
||||
// This class implements helpers for testing the magic restore process. It wraps a ReplSetTest
|
||||
// object and maintains the state of the backup cursor, handles writing objects to named pipes and
|
||||
// running the restore process. It exposes some of this state so that individual tests can make
|
||||
// specific assertions as needed.
|
||||
|
||||
export class MagicRestoreUtils {
|
||||
constructor({backupSource, pipeDir, insertHigherTermOplogEntry, backupDbPathSuffix}) {
|
||||
this.backupSource = backupSource;
|
||||
constructor({rst, pipeDir, insertHigherTermOplogEntry = false}) {
|
||||
this.rst = rst;
|
||||
this.pipeDir = pipeDir;
|
||||
this.backupDbPath =
|
||||
pipeDir + "/backup" + (backupDbPathSuffix != undefined ? backupDbPathSuffix : "");
|
||||
|
||||
this.backupSource = this._selectBackupSource();
|
||||
// Data files are backed up from the source into 'backupDbPath'. We'll copy these data files
|
||||
// into separate dbpaths for each node, ending with 'restore_{nodeId}'.
|
||||
this.backupDbPath = pipeDir + "/backup";
|
||||
this.restoreDbPaths = [];
|
||||
this.rst.nodes.forEach((node) => {
|
||||
const restoreDbPath = pipeDir + "/restore_" + this.rst.getNodeId(node);
|
||||
this.restoreDbPaths.push(restoreDbPath);
|
||||
});
|
||||
|
||||
// isPit is set when we receive the restoreConfiguration.
|
||||
this.isPit = false;
|
||||
|
|
@ -251,6 +259,8 @@ export class MagicRestoreUtils {
|
|||
// Default high term value.
|
||||
this.restoreToHigherTermThan = 100;
|
||||
|
||||
// The replica set config will be the same across nodes in a cluster.
|
||||
this.expectedConfig = this.rst.getPrimary().adminCommand({replSetGetConfig: 1}).config;
|
||||
// These fields are set during the restore process.
|
||||
this.backupCursor = undefined;
|
||||
this.backupId = undefined;
|
||||
|
|
@ -258,6 +268,23 @@ export class MagicRestoreUtils {
|
|||
this.pointInTimeTimestamp = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that selects the node to use for data files. For single-node sets we'll use
|
||||
* the primary, but for multi-node sets we'll use the first secondary. In production, we often
|
||||
* retrieve the backup from a secondary node to reduce load on the primary.
|
||||
*/
|
||||
_selectBackupSource() {
|
||||
let backupSource;
|
||||
if (this.rst.nodes.length === 1) {
|
||||
backupSource = this.rst.getPrimary();
|
||||
jsTestLog(`Selecting primary ${backupSource.host} as backup source.`);
|
||||
return backupSource;
|
||||
}
|
||||
jsTestLog(`Selecting secondary ${backupSource.host} as backup source.`);
|
||||
backupSource = this.rst.getSecondary();
|
||||
return backupSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that returns the checkpoint timestamp from the backup cursor. Used in tests
|
||||
* that need this timestamp to make assertions about data before and after the backup time.
|
||||
|
|
@ -268,10 +295,17 @@ export class MagicRestoreUtils {
|
|||
|
||||
/**
|
||||
* Helper function that returns the dbpath for the backup. Used to start a regular node after
|
||||
* magic restore completes.
|
||||
* magic restore completes. Parameterizes the dbpath to allow for multi-node clusters.
|
||||
*/
|
||||
getBackupDbPath() {
|
||||
return this.backupDbPath;
|
||||
return MagicRestoreUtils.parameterizeDbpath(this.restoreDbPaths[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that returns the expected config after the restore.
|
||||
*/
|
||||
getExpectedConfig() {
|
||||
return this.expectedConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -283,10 +317,6 @@ export class MagicRestoreUtils {
|
|||
// Take the initial checkpoint.
|
||||
assert.commandWorked(this.backupSource.adminCommand({fsync: 1}));
|
||||
|
||||
resetDbpath(this.backupDbPath);
|
||||
// TODO(SERVER-13455): Replace `journal/` with the configurable journal path.
|
||||
mkdir(this.backupDbPath + "/journal");
|
||||
|
||||
// Open a backup cursor on the checkpoint.
|
||||
this.backupCursor = openBackupCursor(this.backupSource.getDB("admin"), backupCursorOpts);
|
||||
// Print the backup metadata document.
|
||||
|
|
@ -302,6 +332,9 @@ export class MagicRestoreUtils {
|
|||
* Copies data files from the source dbpath to the backup dbpath.
|
||||
*/
|
||||
copyFiles() {
|
||||
resetDbpath(this.backupDbPath);
|
||||
// TODO(SERVER-13455): Replace `journal/` with the configurable journal path.
|
||||
mkdir(this.backupDbPath + "/journal");
|
||||
while (this.backupCursor.hasNext()) {
|
||||
const doc = this.backupCursor.next();
|
||||
jsTestLog("Copying for backup: " + tojson(doc));
|
||||
|
|
@ -312,11 +345,16 @@ export class MagicRestoreUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* Copies data files from the source dbpath to the backup dbpath. Closes the backup cursor.
|
||||
* Copies data files from the source dbpath to the backup dbpath, and then closes the backup
|
||||
* cursor. Copies the data files from the backup path to each node's restore db path.
|
||||
*/
|
||||
copyFilesAndCloseBackup() {
|
||||
this.copyFiles();
|
||||
this.backupCursor.close();
|
||||
this.restoreDbPaths.forEach((restoreDbPath) => {
|
||||
resetDbpath(restoreDbPath);
|
||||
MagicRestoreUtils.copyBackupFilesToDir(this.backupDbPath, restoreDbPath);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -332,16 +370,24 @@ export class MagicRestoreUtils {
|
|||
this.backupCursor.close();
|
||||
}
|
||||
|
||||
// Copies backup cursor data files from directory to another. Makes the destination directory if
|
||||
// needed. Used to copy one set of backup files to multiple nodes.
|
||||
static copyBackupFilesToDir(source, dest) {
|
||||
if (!fileExists(dest)) {
|
||||
assert(mkdir(dest).created);
|
||||
}
|
||||
jsTestLog(`Copying data files from source path ${source} to destination path ${dest}`);
|
||||
copyDir(source, dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that generates the magic restore named pipe path for testing. 'pipeDir'
|
||||
* is the directory in the filesystem in which we create the named pipe.
|
||||
*/
|
||||
static _generateMagicRestorePipePath(pipeDir) {
|
||||
const pipeName = "magic_restore_named_pipe";
|
||||
// On Windows, the pipe path prefix is ignored. "//./pipe/" is the required path start of
|
||||
// all named pipes on Windows.
|
||||
const pipePath = _isWindows() ? "//./pipe/" + pipeName : `${pipeDir}/tmp/${pipeName}`;
|
||||
if (!_isWindows() && !fileExists(pipeDir + "/tmp/")) {
|
||||
const pipePath = `${pipeDir}/tmp/${pipeName}`;
|
||||
if (!fileExists(pipeDir + "/tmp/")) {
|
||||
assert(mkdir(pipeDir + "/tmp/").created);
|
||||
}
|
||||
return {pipeName, pipePath};
|
||||
|
|
@ -483,7 +529,6 @@ export class MagicRestoreUtils {
|
|||
*/
|
||||
postRestoreChecks({
|
||||
node,
|
||||
expectedConfig,
|
||||
dbName,
|
||||
collName,
|
||||
expectedOplogCountForNs,
|
||||
|
|
@ -497,9 +542,9 @@ export class MagicRestoreUtils {
|
|||
node.setSecondaryOk();
|
||||
const restoredConfig =
|
||||
assert.commandWorked(node.adminCommand({replSetGetConfig: 1})).config;
|
||||
this._assertConfigIsCorrect(expectedConfig, restoredConfig);
|
||||
this._assertConfigIsCorrect(this.expectedConfig, restoredConfig);
|
||||
this.assertOplogCountForNamespace(
|
||||
node, dbName + "." + collName, expectedOplogCountForNs, opFilter);
|
||||
node, {ns: dbName + "." + collName, op: opFilter}, expectedOplogCountForNs);
|
||||
this._assertMinValidIsCorrect(node);
|
||||
this._assertStableCheckpointIsCorrectAfterRestore(node, shardLastOplogEntryTs);
|
||||
this._assertCannotDoSnapshotRead(
|
||||
|
|
@ -517,11 +562,7 @@ export class MagicRestoreUtils {
|
|||
* Performs a find on the oplog for the given name space and asserts that the expected number of
|
||||
* entries exists. Optionally takes an op type to filter.
|
||||
*/
|
||||
assertOplogCountForNamespace(node, ns, expectedNumEntries, op) {
|
||||
let findObj = {ns: ns};
|
||||
if (op) {
|
||||
findObj.op = op;
|
||||
}
|
||||
assertOplogCountForNamespace(node, findObj, expectedNumEntries) {
|
||||
const entries =
|
||||
node.getDB("local").getCollection('oplog.rs').find(findObj).sort({ts: -1}).toArray();
|
||||
assert.eq(entries.length, expectedNumEntries);
|
||||
|
|
@ -546,9 +587,15 @@ export class MagicRestoreUtils {
|
|||
if (this.pointInTimeTimestamp) {
|
||||
this.isPit = true;
|
||||
}
|
||||
MagicRestoreUtils.writeObjsToMagicRestorePipe(
|
||||
this.pipeDir, [restoreConfiguration, ...entriesAfterBackup]);
|
||||
MagicRestoreUtils.runMagicRestoreNode(this.pipeDir, this.backupDbPath, options);
|
||||
jsTestLog("Restore configuration: " + tojson(restoreConfiguration));
|
||||
|
||||
// Restore each node in serial.
|
||||
this.rst.nodes.forEach((node, idx) => {
|
||||
jsTestLog(`Restoring node ${node.host}`);
|
||||
MagicRestoreUtils.writeObjsToMagicRestorePipe(
|
||||
this.pipeDir, [restoreConfiguration, ...entriesAfterBackup]);
|
||||
MagicRestoreUtils.runMagicRestoreNode(this.pipeDir, this.restoreDbPaths[idx], options);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue