395 lines
15 KiB
JavaScript
395 lines
15 KiB
JavaScript
load("jstests/libs/analyze_plan.js");
|
|
|
|
/**
|
|
* Get the URI of the wt collection file given the collection name.
|
|
*/
|
|
let getUriForColl = function(coll) {
|
|
assert(coll.exists()); // Collection must exist
|
|
return coll.stats().wiredTiger.uri.split("table:")[1];
|
|
};
|
|
|
|
/**
|
|
* Get the URI of the wt index file given the collection name and the index name.
|
|
*/
|
|
let getUriForIndex = function(coll, indexName) {
|
|
assert(coll.exists()); // Collection must exist
|
|
const ret = assert.commandWorked(coll.getDB().runCommand({collStats: coll.getName()}));
|
|
return ret.indexDetails[indexName].uri.split("table:")[1];
|
|
};
|
|
|
|
/**
|
|
* 'Corrupt' the file by replacing it with an empty file.
|
|
*/
|
|
let corruptFile = function(file) {
|
|
removeFile(file);
|
|
writeFile(file, "");
|
|
};
|
|
|
|
/**
|
|
* Starts a mongod on the provided data path without clearing data. Accepts 'options' as parameters
|
|
* to runMongod.
|
|
*/
|
|
let startMongodOnExistingPath = function(dbpath, options) {
|
|
let args = {dbpath: dbpath, noCleanData: true};
|
|
for (let attr in options) {
|
|
if (options.hasOwnProperty(attr))
|
|
args[attr] = options[attr];
|
|
}
|
|
return MongoRunner.runMongod(args);
|
|
};
|
|
|
|
let assertQueryUsesIndex = function(coll, query, indexName) {
|
|
let res = coll.find(query).explain();
|
|
assert.commandWorked(res);
|
|
|
|
let inputStage = getWinningPlan(res.queryPlanner).inputStage;
|
|
assert.eq(inputStage.stage, "IXSCAN");
|
|
assert.eq(inputStage.indexName, indexName);
|
|
};
|
|
|
|
/**
|
|
* Assert that running MongoDB with --repair on the provided dbpath exits cleanly.
|
|
*/
|
|
let assertRepairSucceeds = function(dbpath, port, opts) {
|
|
let args = ["mongod", "--repair", "--port", port, "--dbpath", dbpath, "--bind_ip_all"];
|
|
for (let a in opts) {
|
|
if (opts.hasOwnProperty(a))
|
|
args.push("--" + a);
|
|
|
|
if (opts[a].length > 0) {
|
|
args.push(opts[a]);
|
|
}
|
|
}
|
|
jsTestLog("Repairing the node");
|
|
assert.eq(0, runMongoProgram.apply(this, args));
|
|
};
|
|
|
|
let assertRepairFailsWithFailpoint = function(dbpath, port, failpoint) {
|
|
const param = "failpoint." + failpoint + "={'mode': 'alwaysOn'}";
|
|
jsTestLog("The node should fail to complete repair with --setParameter " + param);
|
|
|
|
assert.eq(
|
|
MongoRunner.EXIT_ABRUPT,
|
|
runMongoProgram(
|
|
"mongod", "--repair", "--port", port, "--dbpath", dbpath, "--setParameter", param));
|
|
};
|
|
|
|
/**
|
|
* Asserts that running MongoDB with --repair on the provided dbpath fails.
|
|
*/
|
|
let assertRepairFails = function(dbpath, port) {
|
|
jsTestLog("The node should complete repairing the node but fails.");
|
|
|
|
assert.neq(0, runMongoProgram("mongod", "--repair", "--port", port, "--dbpath", dbpath));
|
|
};
|
|
|
|
/**
|
|
* Assert that starting MongoDB with --replSet on an existing data path exits with a specific
|
|
* error.
|
|
*/
|
|
let assertErrorOnStartupWhenStartingAsReplSet = function(dbpath, port, rsName) {
|
|
jsTestLog("The repaired node should fail to start up with the --replSet option");
|
|
|
|
clearRawMongoProgramOutput();
|
|
let node = MongoRunner.runMongod(
|
|
{dbpath: dbpath, port: port, replSet: rsName, noCleanData: true, waitForConnect: false});
|
|
assert.soon(function() {
|
|
return rawMongoProgramOutput().search(/Fatal assertion.*50923/) >= 0;
|
|
});
|
|
MongoRunner.stopMongod(node, null, {allowedExitCode: MongoRunner.EXIT_ABRUPT});
|
|
};
|
|
|
|
/**
|
|
* Assert that starting MongoDB as a standalone on an existing data path exits with a specific
|
|
* error because the previous repair failed.
|
|
*/
|
|
let assertErrorOnStartupAfterIncompleteRepair = function(dbpath, port) {
|
|
jsTestLog("The node should fail to start up because a previous repair did not complete");
|
|
|
|
clearRawMongoProgramOutput();
|
|
let node = MongoRunner.runMongod(
|
|
{dbpath: dbpath, port: port, noCleanData: true, waitForConnect: false});
|
|
assert.soon(function() {
|
|
return rawMongoProgramOutput().search(/Fatal assertion.*50922/) >= 0;
|
|
});
|
|
MongoRunner.stopMongod(node, null, {allowedExitCode: MongoRunner.EXIT_ABRUPT});
|
|
};
|
|
|
|
/**
|
|
* Assert that starting MongoDB as a standalone on an existing data path succeeds. Uses a provided
|
|
* testFunc to run any caller-provided checks on the started node.
|
|
*/
|
|
let assertStartAndStopStandaloneOnExistingDbpath = function(dbpath, port, testFunc) {
|
|
jsTestLog("The repaired node should start up and serve reads as a standalone");
|
|
let node = MongoRunner.runMongod({dbpath: dbpath, port: port, noCleanData: true});
|
|
assert(node);
|
|
testFunc(node);
|
|
MongoRunner.stopMongod(node);
|
|
};
|
|
|
|
/**
|
|
* Assert that starting MongoDB with --replSet succeeds. Uses a provided testFunc to run any
|
|
* caller-provided checks on the started node.
|
|
*
|
|
* Returns the started node.
|
|
*/
|
|
let assertStartInReplSet = function(replSet, originalNode, cleanData, expectResync, testFunc) {
|
|
jsTestLog("The node should rejoin the replica set. Clean data: " + cleanData +
|
|
". Expect resync: " + expectResync);
|
|
// Skip clearing initial sync progress after a successful initial sync attempt so that we
|
|
// can check initialSyncStatus fields after initial sync is complete.
|
|
let node = replSet.start(originalNode, {
|
|
dbpath: originalNode.dbpath,
|
|
port: originalNode.port,
|
|
restart: !cleanData,
|
|
setParameter: {"failpoint.skipClearInitialSyncState": "{'mode':'alwaysOn'}"}
|
|
});
|
|
|
|
replSet.awaitSecondaryNodes();
|
|
|
|
// Ensure that an initial sync attempt was made and succeeded if the data directory was cleaned.
|
|
let res = assert.commandWorked(node.adminCommand({replSetGetStatus: 1}));
|
|
if (expectResync) {
|
|
assert.eq(1, res.initialSyncStatus.initialSyncAttempts.length);
|
|
assert.eq(0, res.initialSyncStatus.failedInitialSyncAttempts);
|
|
} else {
|
|
assert.eq(undefined, res.initialSyncStatus);
|
|
}
|
|
|
|
assert.commandWorked(
|
|
node.adminCommand({configureFailPoint: 'skipClearInitialSyncState', mode: 'off'}));
|
|
|
|
testFunc(node);
|
|
return node;
|
|
};
|
|
|
|
/**
|
|
* Assert certain error messages are thrown on startup when files are missing or corrupt.
|
|
*/
|
|
let assertErrorOnStartupWhenFilesAreCorruptOrMissing = function(
|
|
dbpath, dbName, collName, deleteOrCorruptFunc, errmsgRegExp) {
|
|
// Start a MongoDB instance, create the collection file.
|
|
const mongod = MongoRunner.runMongod({dbpath: dbpath, cleanData: true});
|
|
const testColl = mongod.getDB(dbName)[collName];
|
|
const doc = {a: 1};
|
|
assert.commandWorked(testColl.insert(doc));
|
|
|
|
// Stop MongoDB and corrupt/delete certain files.
|
|
deleteOrCorruptFunc(mongod, testColl);
|
|
|
|
// Restart the MongoDB instance and get an expected error message.
|
|
clearRawMongoProgramOutput();
|
|
assert.eq(MongoRunner.EXIT_ABRUPT,
|
|
runMongoProgram("mongod", "--port", mongod.port, "--dbpath", dbpath));
|
|
assert.gte(rawMongoProgramOutput().search(errmsgRegExp), 0);
|
|
};
|
|
|
|
/**
|
|
* Assert certain error messages are thrown on a specific request when files are missing or corrupt.
|
|
*/
|
|
let assertErrorOnRequestWhenFilesAreCorruptOrMissing = function(
|
|
dbpath, dbName, collName, deleteOrCorruptFunc, requestFunc, errmsgRegExp) {
|
|
// Start a MongoDB instance, create the collection file.
|
|
mongod = MongoRunner.runMongod({dbpath: dbpath, cleanData: true});
|
|
testColl = mongod.getDB(dbName)[collName];
|
|
const doc = {a: 1};
|
|
assert.commandWorked(testColl.insert(doc));
|
|
|
|
// Stop MongoDB and corrupt/delete certain files.
|
|
deleteOrCorruptFunc(mongod, testColl);
|
|
|
|
// Restart the MongoDB instance.
|
|
clearRawMongoProgramOutput();
|
|
mongod = MongoRunner.runMongod({dbpath: dbpath, port: mongod.port, noCleanData: true});
|
|
|
|
// This request crashes the server.
|
|
testColl = mongod.getDB(dbName)[collName];
|
|
requestFunc(testColl);
|
|
|
|
// Get an expected error message.
|
|
const rawLogs = rawMongoProgramOutput();
|
|
const matchedIndex = rawLogs.search(errmsgRegExp);
|
|
if (matchedIndex < 0) {
|
|
jsTestLog("String pattern not found in rawMongoProgramOutput(): " + rawLogs);
|
|
}
|
|
|
|
assert.gte(matchedIndex, 0);
|
|
MongoRunner.stopMongod(mongod, 9, {allowedExitCode: MongoRunner.EXIT_ABRUPT});
|
|
};
|
|
|
|
/**
|
|
* Runs the WiredTiger tool with the provided arguments.
|
|
*/
|
|
let runWiredTigerTool = function(...args) {
|
|
const cmd = ['wt'].concat(args);
|
|
// TODO (SERVER-67632): Check the return code on Windows variants again.
|
|
if (_isWindows()) {
|
|
run.apply(undefined, cmd);
|
|
} else {
|
|
assert.eq(run.apply(undefined, cmd), 0, "error executing: " + cmd.join(' '));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Stops the given mongod, runs the truncate command on the given uri using the WiredTiger tool, and
|
|
* starts mongod again on the same path.
|
|
*/
|
|
let truncateUriAndRestartMongod = function(uri, conn, mongodOptions) {
|
|
MongoRunner.stopMongod(conn, null, {skipValidation: true});
|
|
runWiredTigerTool("-h", conn.dbpath, "truncate", uri);
|
|
return startMongodOnExistingPath(conn.dbpath, mongodOptions);
|
|
};
|
|
|
|
/**
|
|
* Stops the given mongod and runs the alter command to modify the index table's metadata.
|
|
*/
|
|
let alterIndexFormatVersion = function(uri, conn, formatVersion) {
|
|
MongoRunner.stopMongod(conn, null, {skipValidation: true});
|
|
runWiredTigerTool(
|
|
"-h",
|
|
conn.dbpath,
|
|
"alter",
|
|
"table:" + uri,
|
|
"app_metadata=(formatVersion=" + formatVersion + "),exclusive_refreshed=false");
|
|
};
|
|
|
|
/**
|
|
* Stops the given mongod, dumps the table with the uri, modifies the content, and loads it back to
|
|
* the table.
|
|
*/
|
|
let count = 0;
|
|
let rewriteTable = function(uri, conn, modifyData) {
|
|
MongoRunner.stopMongod(conn, null, {skipValidation: true});
|
|
const separator = _isWindows() ? '\\' : '/';
|
|
const tempDumpFile = conn.dbpath + separator + "temp_dump";
|
|
const newTableFile = conn.dbpath + separator + "new_table_file" + count++;
|
|
runWiredTigerTool("-h",
|
|
conn.dbpath,
|
|
"-r",
|
|
"-C",
|
|
"log=(compressor=snappy,path=journal)",
|
|
"dump",
|
|
"-x",
|
|
"-f",
|
|
tempDumpFile,
|
|
"table:" + uri);
|
|
let dumpLines = cat(tempDumpFile).split("\n");
|
|
modifyData(dumpLines);
|
|
writeFile(newTableFile, dumpLines.join("\n"));
|
|
runWiredTigerTool("-h", conn.dbpath, "load", "-f", newTableFile, "-r", uri);
|
|
};
|
|
|
|
// In WiredTiger table dumps, the first seven lines are the header and key that we don't want to
|
|
// modify. We will skip them and start from the line containing the first value.
|
|
const wtHeaderLines = 7;
|
|
|
|
/**
|
|
* Inserts the documents with duplicate field names into the MongoDB server.
|
|
*/
|
|
let insertDocDuplicateFieldName = function(coll, uri, conn, numDocs) {
|
|
for (let i = 0; i < numDocs; ++i) {
|
|
coll.insert({a: "aaaaaaa", b: "bbbbbbb"});
|
|
}
|
|
// The format of the BSON documents will be {_id: ObjectId(), a: "aaaaaaa", a: "bbbbbbb"}.
|
|
let makeDuplicateFieldNames = function(lines) {
|
|
// The offset of the document's field name 'b' in the hex string dumped by wt tool.
|
|
const offsetToFieldB = 75;
|
|
// Each record takes two lines with a key and a value. We will only modify the values.
|
|
for (let i = wtHeaderLines; i < lines.length; i += 2) {
|
|
// Switch the field name 'b' to 'a' to create a duplicate field name.
|
|
lines[i] = lines[i].substring(0, offsetToFieldB) + "1" +
|
|
lines[i].substring(offsetToFieldB + 1);
|
|
}
|
|
};
|
|
rewriteTable(uri, conn, makeDuplicateFieldNames);
|
|
};
|
|
|
|
let insertDocSymbolField = function(coll, uri, conn, numDocs) {
|
|
for (let i = 0; i < numDocs; ++i) {
|
|
coll.insert({a: "aaaaaaa"});
|
|
}
|
|
let makeSymbolField = function(lines) {
|
|
// The offset of the type of field 'a' in the hex string dumped by wt tool.
|
|
const offsetToFieldAType = 43;
|
|
// Each record takes two lines with a key and a value. We will only modify the values.
|
|
for (let i = wtHeaderLines; i < lines.length; i += 2) {
|
|
// Switch the field type from string to symbol.
|
|
lines[i] = lines[i].substring(0, offsetToFieldAType) + "e" +
|
|
lines[i].substring(offsetToFieldAType + 1);
|
|
}
|
|
};
|
|
rewriteTable(uri, conn, makeSymbolField);
|
|
};
|
|
|
|
/**
|
|
* Inserts array document with non-sequential indexes into the MongoDB server.
|
|
*/
|
|
let insertNonSequentialArrayIndexes = function(coll, uri, conn, numDocs) {
|
|
for (let i = 0; i < numDocs; ++i) {
|
|
coll.insert({arr: [1, 2, [1, [1, 2], 2], 3]});
|
|
}
|
|
let makeNonSequentialIndexes = function(lines) {
|
|
// The offset of the 0th index of the innermost array in the hex string dumped by wt tool.
|
|
const offsetToNestedIndex0 = 179;
|
|
// Each record takes two lines with a key and a value. We will only modify the values.
|
|
for (let i = wtHeaderLines; i < lines.length; i += 2) {
|
|
lines[i] = lines[i].substring(0, offsetToNestedIndex0) + "4" +
|
|
lines[i].substring(offsetToNestedIndex0 + 1);
|
|
}
|
|
};
|
|
rewriteTable(uri, conn, makeNonSequentialIndexes);
|
|
};
|
|
|
|
/**
|
|
* Inserts documents with invalid regex options into the MongoDB server.
|
|
*/
|
|
let insertInvalidRegex = function(coll, mongod, nDocuments) {
|
|
const regex = "a*.conn";
|
|
const options = 'gimsuy';
|
|
|
|
// First, insert valid expressions which will not be rejected by the JS interpreter.
|
|
for (let i = 0; i < nDocuments; i++) {
|
|
coll.insert({a: RegExp(regex, options)});
|
|
}
|
|
|
|
// Inserts 4 types of invalid expressions.
|
|
let swapOptions = function(lines) {
|
|
const toInsert = ["imlsux", "imzsux", "xuslmi", "amlsux"];
|
|
const offsetToOptionStr = 64;
|
|
const toHexStr = function(str) {
|
|
return str.split('')
|
|
.map((a) => {
|
|
return a.charCodeAt(0).toString(16);
|
|
})
|
|
.join('');
|
|
};
|
|
|
|
let modifiedOptions;
|
|
for (let i = wtHeaderLines; i < lines.length; i += 2) {
|
|
modifiedOptions = toHexStr(toInsert[((i - wtHeaderLines) / 2) % toInsert.length]);
|
|
lines[i] = lines[i].substring(0, offsetToOptionStr) + modifiedOptions +
|
|
lines[i].substring(offsetToOptionStr + modifiedOptions.length);
|
|
}
|
|
};
|
|
rewriteTable(getUriForColl(coll), mongod, swapOptions);
|
|
};
|
|
|
|
/**
|
|
* Inserts document with invalid UTF-8 string into the MongoDB server.
|
|
*/
|
|
let insertInvalidUTF8 = function(coll, uri, conn, numDocs) {
|
|
for (let i = 0; i < numDocs; ++i) {
|
|
coll.insert({validString: "\x70"});
|
|
}
|
|
let makeInvalidUTF8 = function(lines) {
|
|
// The offset of the first byte of the string, flips \x70 to \x80 (10000000) - invalid
|
|
// because single byte UTF-8 cannot have a leading 1.
|
|
const offsetToString = 76;
|
|
// Each record takes two lines with a key and a value. We will only modify the values.
|
|
for (let i = wtHeaderLines; i < lines.length; i += 2) {
|
|
lines[i] = lines[i].substring(0, offsetToString) + "8" +
|
|
lines[i].substring(offsetToString + 1);
|
|
}
|
|
};
|
|
rewriteTable(uri, conn, makeInvalidUTF8);
|
|
}; |