270 lines
10 KiB
JavaScript
270 lines
10 KiB
JavaScript
/**
|
|
* @tags: [
|
|
* featureFlagRecordIdsReplicated,
|
|
* ]
|
|
*/
|
|
import {ReplSetTest} from "jstests/libs/replsettest.js";
|
|
|
|
const debug = 0;
|
|
|
|
let rst = new ReplSetTest({name: "applyOpsIdempotency", nodes: 1});
|
|
rst.startSet();
|
|
rst.initiate();
|
|
|
|
/**
|
|
* Apply ops on mydb, asserting success.
|
|
*/
|
|
function assertApplyOpsWorks(testdbs, ops) {
|
|
// Remaining operations in ops must still be applied
|
|
while (ops.length) {
|
|
let cmd = {applyOps: ops};
|
|
let res = testdbs[0].adminCommand(cmd);
|
|
if (debug) {
|
|
printjson({applyOps: ops, res});
|
|
}
|
|
|
|
// If the entire operation succeeded, we're done.
|
|
if (res.ok == 1) return res;
|
|
|
|
// Skip any operations that succeeded.
|
|
while (res.applied-- && res.results.shift()) ops.shift();
|
|
|
|
// These errors are expected when replaying operations and should be ignored.
|
|
if (
|
|
res.code == ErrorCodes.NamespaceNotFound ||
|
|
res.code == ErrorCodes.DuplicateKey ||
|
|
res.code == ErrorCodes.UnknownError
|
|
) {
|
|
ops.shift();
|
|
continue;
|
|
}
|
|
|
|
// Generate the appropriate error message.
|
|
assert.commandWorked(res, tojson(cmd));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run the dbHash command on mydb, assert it worked and return the md5.
|
|
*/
|
|
function dbHash(mydb) {
|
|
let cmd = {dbHash: 1};
|
|
let res = mydb.runCommand(cmd);
|
|
assert.commandWorked(res, tojson(cmd));
|
|
return res.md5;
|
|
}
|
|
|
|
/**
|
|
* Gather collection info and dbHash results of each of the passed databases.
|
|
*/
|
|
function dbInfo(dbs) {
|
|
return dbs.map((db) => {
|
|
return {name: db.getName(), info: db.getCollectionInfos(), md5: dbHash(db)};
|
|
});
|
|
}
|
|
|
|
let getCollections = (mydb, prefixes) => prefixes.map((prefix) => mydb[prefix]);
|
|
|
|
/**
|
|
* Test functions to run and test using replay of oplog.
|
|
*/
|
|
let tests = {
|
|
crud: (mydb) => {
|
|
let [x, y, z] = getCollections(mydb, ["x", "y", "z"]);
|
|
assert.commandWorked(x.insert({_id: 1}));
|
|
assert.commandWorked(x.update({_id: 1}, {$set: {x: 1}}));
|
|
assert.commandWorked(x.remove({_id: 1}));
|
|
|
|
assert.commandWorked(y.update({_id: 1}, {y: 1}));
|
|
assert.commandWorked(y.insert({_id: 2, y: false, z: false}));
|
|
assert.commandWorked(y.update({_id: 2}, {y: 2}));
|
|
|
|
assert.commandWorked(z.insert({_id: 1, z: 1}));
|
|
assert.commandWorked(z.remove({_id: 1}));
|
|
assert.commandWorked(z.insert({_id: 1}));
|
|
assert.commandWorked(z.insert({_id: 2, z: 2}));
|
|
},
|
|
arrayAndSubdocumentFields: (mydb) => {
|
|
let [x, y] = getCollections(mydb, ["x", "y"]);
|
|
// Array field.
|
|
assert.commandWorked(x.insert({_id: 1, x: 1, y: [0]}));
|
|
assert.commandWorked(x.update({_id: 1}, {$set: {x: 2, "y.0": 2}}));
|
|
assert.commandWorked(x.update({_id: 1}, {$set: {y: 3}}));
|
|
|
|
// Subdocument field.
|
|
assert.commandWorked(y.insert({_id: 1, x: 1, y: {field: 0}}));
|
|
assert.commandWorked(y.update({_id: 1}, {$set: {x: 2, "y.field": 2}}));
|
|
assert.commandWorked(y.update({_id: 1}, {$set: {y: 3}}));
|
|
},
|
|
renameCollectionWithinDatabase: (mydb) => {
|
|
let [x, y, z] = getCollections(mydb, ["x", "y", "z"]);
|
|
assert.commandWorked(x.insert({_id: 1, x: 1}));
|
|
assert.commandWorked(y.insert({_id: 1, y: 1}));
|
|
|
|
assert.commandWorked(x.renameCollection(z.getName()));
|
|
assert.commandWorked(z.insert({_id: 2, x: 2}));
|
|
assert.commandWorked(x.insert({_id: 2, x: false}));
|
|
assert.commandWorked(y.insert({y: 2}));
|
|
|
|
assert.commandWorked(y.renameCollection(x.getName(), true));
|
|
assert.commandWorked(z.renameCollection(y.getName()));
|
|
},
|
|
renameCollectionWithinDatabaseDroppingTargetByUUID: (mydb) => {
|
|
assert.commandWorked(mydb.createCollection("x"));
|
|
assert.commandWorked(mydb.createCollection("y"));
|
|
assert.commandWorked(mydb.createCollection("z"));
|
|
|
|
assert.commandWorked(mydb.x.renameCollection("xx"));
|
|
// When replayed on a up-to-date db, this oplog entry may drop
|
|
// collection z rather than collection x if the dropTarget is not
|
|
// specified by UUID. (See SERVER-33087)
|
|
assert.commandWorked(mydb.y.renameCollection("xx", true));
|
|
assert.commandWorked(mydb.xx.renameCollection("yy"));
|
|
assert.commandWorked(mydb.z.renameCollection("xx"));
|
|
},
|
|
renameCollectionWithinDatabaseDropTargetEvenWhenSourceIsEmpty: (mydb) => {
|
|
assert.commandWorked(mydb.createCollection("x"));
|
|
assert.commandWorked(mydb.createCollection("y"));
|
|
assert.commandWorked(mydb.x.renameCollection("y", true));
|
|
assert(mydb.y.drop());
|
|
},
|
|
renameCollectionAcrossDatabases: (mydb) => {
|
|
let otherdb = mydb.getSiblingDB(mydb + "_");
|
|
let [x, y] = getCollections(mydb, ["x", "y"]);
|
|
let [z] = getCollections(otherdb, ["z"]);
|
|
assert.commandWorked(x.insert({_id: 1, x: 1}));
|
|
assert.commandWorked(y.insert({_id: 1, y: 1}));
|
|
|
|
assert.commandWorked(mydb.adminCommand({renameCollection: x.getFullName(), to: z.getFullName()})); // across databases
|
|
assert.commandWorked(z.insert({_id: 2, x: 2}));
|
|
assert.commandWorked(x.insert({_id: 2, x: false}));
|
|
assert.commandWorked(y.insert({y: 2}));
|
|
|
|
assert.commandWorked(
|
|
mydb.adminCommand({
|
|
renameCollection: y.getFullName(),
|
|
to: x.getFullName(),
|
|
dropTarget: true,
|
|
}),
|
|
); // within database
|
|
assert.commandWorked(mydb.adminCommand({renameCollection: z.getFullName(), to: y.getFullName()})); // across databases
|
|
return [mydb, otherdb];
|
|
},
|
|
renameCollectionAcrossDatabasesWithDropAndConvertToCapped: (db1) => {
|
|
let db2 = db1.getSiblingDB(db1 + "_");
|
|
assert.commandWorked(db1.createCollection("a"));
|
|
assert.commandWorked(db1.createCollection("b"));
|
|
assert.commandWorked(db2.createCollection("c"));
|
|
assert.commandWorked(db2.createCollection("d"));
|
|
let [a, b] = getCollections(db1, ["a", "b"]);
|
|
let [c, d] = getCollections(db2, ["c", "d"]);
|
|
|
|
assert.commandWorked(
|
|
db1.adminCommand({renameCollection: a.getFullName(), to: c.getFullName(), dropTarget: true}),
|
|
);
|
|
|
|
assert(d.drop());
|
|
|
|
assert.commandWorked(
|
|
db1.adminCommand({renameCollection: c.getFullName(), to: d.getFullName(), dropTarget: false}),
|
|
);
|
|
|
|
assert.commandWorked(
|
|
db1.adminCommand({renameCollection: b.getFullName(), to: c.getFullName(), dropTarget: false}),
|
|
);
|
|
assert.commandWorked(db2.runCommand({convertToCapped: "d", size: 1000}));
|
|
|
|
return [db1, db2];
|
|
},
|
|
createIndex: (mydb) => {
|
|
let [x, y] = getCollections(mydb, ["x", "y"]);
|
|
assert.commandWorked(x.createIndex({x: 1}));
|
|
assert.commandWorked(x.insert({_id: 1, x: 1}));
|
|
assert.commandWorked(y.insert({_id: 1, y: 1}));
|
|
assert.commandWorked(y.createIndex({y: 1}));
|
|
assert.commandWorked(y.insert({_id: 2, y: 2}));
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Create a new uniquely named database, execute testFun and compute the dbHash. Then replay
|
|
* all different suffixes of the oplog and check for the correct hash. If testFun creates
|
|
* additional databases, it should return an array with all databases to check.
|
|
*/
|
|
function testIdempotency(primary, testFun, testName) {
|
|
// It is not possible to test createIndexes in applyOps because that command is not accepted
|
|
// by applyOps in that mode.
|
|
if ("createIndex" === testName) {
|
|
return;
|
|
}
|
|
|
|
jsTestLog(`Execute ${testName}`);
|
|
|
|
// Create a new database name, so it's easier to filter out our oplog records later.
|
|
let dbname = new Date()
|
|
.toISOString()
|
|
.match(/[-0-9T]/g)
|
|
.join(""); // 2017-05-30T155055713
|
|
let mydb = primary.getDB(dbname);
|
|
|
|
// Allow testFun to return the array of databases to check (default is mydb).
|
|
let testdbs = testFun(mydb) || [mydb];
|
|
let expectedInfo = dbInfo(testdbs);
|
|
|
|
let oplog = mydb.getSiblingDB("local").oplog.rs;
|
|
let ops = oplog
|
|
.find(
|
|
{
|
|
op: {$ne: "n"},
|
|
// admin.$cmd needed for cross-db rename applyOps
|
|
ns: new RegExp("^" + mydb.getName() + "|^admin\.[$]cmd$"),
|
|
"o.startIndexBuild": {$exists: false},
|
|
"o.abortIndexBuild": {$exists: false},
|
|
"o.commitIndexBuild": {$exists: false},
|
|
// TODO (SERVER-117265): Revisit if this is needed.
|
|
"o.collMod": {$exists: false},
|
|
},
|
|
{ts: 0, t: 0, h: 0, v: 0},
|
|
)
|
|
.toArray();
|
|
assert.gt(ops.length, 0, "Could not find any matching ops in the oplog");
|
|
testdbs.forEach((db) => assert.commandWorked(db.dropDatabase()));
|
|
|
|
// In actual initial sync, oplog application will never try to create a collection with an ident
|
|
// that is drop pending, as the first phase won't create and then drop the collection. Reapplying
|
|
// the same oplog to a database multiple times does, however. Rather than test something which
|
|
// doesn't happen in practice (and doesn't work), remove the replicated idents from the oplog we reapply.
|
|
// TODO(SERVER-107069): Once initial sync replicates idents we may want to reevaluate this. We
|
|
// could instead wait for pending drops to complete, but that significantly slows down this test (20s -> 200s).
|
|
for (let op of ops) {
|
|
if (op.op == "c") {
|
|
delete op.o2;
|
|
}
|
|
}
|
|
|
|
if (debug) {
|
|
print(testName + ": replaying suffixes of " + ops.length + " operations");
|
|
printjson(ops);
|
|
}
|
|
|
|
for (let j = 0; j < ops.length; j++) {
|
|
let replayOps = ops.slice(j);
|
|
assertApplyOpsWorks(testdbs, replayOps);
|
|
let actualInfo = dbInfo(testdbs);
|
|
assert.eq(
|
|
actualInfo,
|
|
expectedInfo,
|
|
"unexpected differences between databases after replaying final " +
|
|
replayOps.length +
|
|
" ops in test " +
|
|
testName +
|
|
": " +
|
|
tojson(replayOps),
|
|
);
|
|
}
|
|
}
|
|
|
|
for (let f in tests) testIdempotency(rst.getPrimary(), tests[f], f);
|
|
|
|
rst.stopSet();
|