Files
mongo/jstests/libs/backup_utils.js
seanzimm 569dd253d0 SERVER-87635: Add sharded passthrough suite for magic restore (#19930)
GitOrigin-RevId: 10eb71cb1055c04d063fff6d6007e6a570baa717
2024-03-19 14:48:45 +00:00

202 lines
7.4 KiB
JavaScript

import {Thread} from "jstests/libs/parallelTester.js";
export function backupData(mongo, destinationDirectory) {
let backupCursor = openBackupCursor(mongo.getDB("admin"));
let metadata = getBackupCursorMetadata(backupCursor);
copyBackupCursorFiles(
backupCursor, /*namespacesToSkip=*/[], metadata.dbpath, destinationDirectory);
backupCursor.close();
return metadata;
}
export function openBackupCursor(db, backupOptions, aggregateOptions) {
// Opening a backup cursor can race with taking a checkpoint, resulting in a transient
// error. Retry until it succeeds.
backupOptions = backupOptions || {};
aggregateOptions = aggregateOptions || {};
while (true) {
try {
return db.aggregate([{$backupCursor: backupOptions}], aggregateOptions);
} catch (exc) {
jsTestLog({"Failed to open a backup cursor, retrying.": exc});
}
}
}
export function extendBackupCursor(mongo, backupId, extendTo) {
return mongo.getDB("admin").aggregate(
[{$backupCursorExtend: {backupId: backupId, timestamp: extendTo}}],
{maxTimeMS: 180 * 1000});
}
export function startHeartbeatThread(host, backupCursor, session, stopCounter) {
let cursorId = tojson(backupCursor._cursorid);
let lsid = tojson(session.getSessionId());
let heartbeatBackupCursor = function(host, cursorId, lsid, stopCounter) {
const conn = new Mongo(host);
const db = conn.getDB("admin");
while (stopCounter.getCount() > 0) {
let res = assert.commandWorked(db.runCommand({
getMore: eval("(" + cursorId + ")"),
collection: "$cmd.aggregate",
lsid: eval("(" + lsid + ")")
}));
sleep(10 * 1000);
}
};
const heartbeater = new Thread(heartbeatBackupCursor, host, cursorId, lsid, stopCounter);
heartbeater.start();
return heartbeater;
}
export function getBackupCursorMetadata(backupCursor) {
assert(backupCursor.hasNext());
let doc = backupCursor.next();
assert(doc.hasOwnProperty("metadata"));
return doc["metadata"];
}
/**
* Exhaust the backup cursor and copy all the listed files to the destination directory. If `async`
* is true, this function will spawn a Thread doing the copy work and return the thread along
* with the backup cursor metadata. The caller should `join` the thread when appropriate.
*/
export function copyBackupCursorFiles(
backupCursor, namespacesToSkip, dbpath, destinationDirectory, async, fileCopiedCallback) {
resetDbpath(destinationDirectory);
mkdir(destinationDirectory + "/journal");
let copyThread = copyBackupCursorExtendFiles(
backupCursor, namespacesToSkip, dbpath, destinationDirectory, async, fileCopiedCallback);
return copyThread;
}
export function copyBackupCursorExtendFiles(
cursor, namespacesToSkip, dbpath, destinationDirectory, async, fileCopiedCallback) {
let files = _cursorToFiles(cursor, namespacesToSkip, fileCopiedCallback);
let copyThread;
if (async) {
copyThread = new Thread(_copyFiles, files, dbpath, destinationDirectory, _copyFileHelper);
copyThread.start();
} else {
_copyFiles(files, dbpath, destinationDirectory, _copyFileHelper);
}
jsTestLog({
msg: "Destination",
destination: destinationDirectory,
dbpath: ls(destinationDirectory),
journal: ls(destinationDirectory + "/journal")
});
return copyThread;
}
export function _cursorToFiles(cursor, namespacesToSkip, fileCopiedCallback) {
let files = [];
while (cursor.hasNext()) {
let doc = cursor.next();
assert(doc.hasOwnProperty("filename"));
if (namespacesToSkip.includes(doc.ns)) {
jsTestLog("Skipping file during backup: " + tojson(doc));
continue;
}
if (fileCopiedCallback) {
fileCopiedCallback(doc);
}
files.push(doc.filename);
}
return files;
}
export function _copyFiles(files, dbpath, destinationDirectory, copyFileHelper) {
files.forEach((file) => {
let dbgDoc = copyFileHelper(file, dbpath, destinationDirectory);
dbgDoc["msg"] = "File copy";
jsTestLog(dbgDoc);
});
}
export function _copyFileHelper(absoluteFilePath, sourceDbPath, destinationDirectory) {
// Ensure the dbpath ends with an OS appropriate slash.
let separator = '/';
if (_isWindows()) {
separator = '\\';
}
let lastChar = sourceDbPath[sourceDbPath.length - 1];
if (lastChar !== '/' && lastChar !== '\\') {
sourceDbPath += separator;
}
// Ensure that the full path starts with the returned dbpath.
assert.eq(0, absoluteFilePath.indexOf(sourceDbPath));
// Grab the file path relative to the dbpath. Maintain that relation when copying
// to the `hiddenDbpath`.
let relativePath = absoluteFilePath.substr(sourceDbPath.length);
let destination = destinationDirectory + separator + relativePath;
const newFileDirectory = destination.substring(0, destination.lastIndexOf(separator));
mkdir(newFileDirectory);
copyFile(absoluteFilePath, destination);
return {fileSource: absoluteFilePath, relativePath: relativePath, fileDestination: destination};
}
// Magic restore utility functions
/**
* Helper function that generates the magic restore named pipe path for testing. 'pipeDir'
* is the directory to create the named pipe in the filesystem.
*/
function _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/")) {
assert(mkdir(pipeDir + "/tmp/").created);
}
return {pipeName, pipePath};
}
/**
* Helper function that writes an array of JavaScript objects into a named pipe. 'objs' will be
* serialized into BSON and written into the named pipe path generated by
* '_generateMagicRestorePipePath'.
*/
export function _writeObjsToMagicRestorePipe(objs, pipeDir) {
const {pipeName, pipePath} = _generateMagicRestorePipePath(pipeDir);
_writeTestPipeObjects(pipeName, objs.length, objs, pipeDir + "/tmp/");
// Creating the named pipe is async, so we should wait until the file exists.
assert.soon(() => fileExists(pipePath));
}
/**
* Helper function that starts and completes a magic restore node on the provided 'backupDbPath'.
*/
export function _runMagicRestoreNode(backupDbPath, pipeDir, options = {}) {
const {pipePath} = _generateMagicRestorePipePath(pipeDir);
// Magic restore will exit the mongod process cleanly. 'runMongod' may acquire a connection to
// mongod before it exits, and so we wait for the process to exit in the 'assert.soon' below. If
// mongod exits before we acquire a connection, 'conn' will be null. In this case, if mongod
// exits with non-zero exit code, the runner will throw a StopError.
const conn = MongoRunner.runMongod({
dbpath: backupDbPath,
noCleanData: true,
magicRestore: "",
env: {namedPipeInput: pipePath},
...options
});
if (conn) {
assert.soon(() => {
const res = checkProgram(conn.pid);
return !res.alive && res.exitCode == MongoRunner.EXIT_CLEAN;
}, "Expected magic restore to exit mongod cleanly");
}
}