Files
mongo/jstests/replsets/noop_writes_wait_for_write_concern.js

308 lines
11 KiB
JavaScript

/**
* This file tests that if a user initiates a write that becomes a noop either due to being a
* duplicate operation or due to errors relying on data reads, that we still wait for write concern.
* This is because we must wait for write concern on the write that made this a noop so that we can
* be sure it doesn't get rolled back if we acknowledge it.
*/
(function() {
"use strict";
load('jstests/libs/write_concern_util.js');
load('jstests/noPassthrough/libs/index_build.js');
var name = 'noop_writes_wait_for_write_concern';
var replTest = new ReplSetTest({
name: name,
nodes: [{}, {rsConfig: {priority: 0}}, {rsConfig: {priority: 0}}],
});
replTest.startSet();
replTest.initiate();
// Stops node 1 so that all w:3 write concerns time out. We have 3 data bearing nodes so that
// 'dropDatabase' can satisfy its implicit writeConcern: majority but still time out from the
// explicit w:3 write concern.
replTest.stop(1);
var primary = replTest.getPrimary();
assert.eq(primary, replTest.nodes[0]);
var dbName = 'testDB';
var db = primary.getDB(dbName);
var collName = 'testColl';
var coll = db[collName];
function dropTestCollection() {
coll.drop();
assert.eq(0, coll.find().itcount(), "test collection not empty");
}
// Each entry in this array contains a command whose noop write concern behavior needs to be
// tested. Entries have the following structure:
// {
// req: <object>, // Command request object that will result in a noop
// // write after the setup function is called.
//
// setupFunc: <function()>, // Function to run to ensure that the request is a
// // noop.
//
// confirmFunc: <function(res)>, // Function to run after the command is run to ensure
// // that it executed properly. Accepts the result of
// // the noop request to validate it.
// }
var commands = [];
// 'applyOps' where the update has already been done.
commands.push({
req: {applyOps: [{op: "u", ns: coll.getFullName(), o: {_id: 1}, o2: {_id: 1}}]},
setupFunc: function() {
assert.commandWorked(coll.insert({_id: 1}));
},
confirmFunc: function(res) {
assert.commandWorkedIgnoringWriteConcernErrors(res);
assert.eq(res.applied, 1);
assert.eq(res.results[0], true);
assert.eq(coll.find().itcount(), 1);
assert.eq(coll.count({_id: 1}), 1);
}
});
// 'applyOps' where the preCondition fails.
commands.push({
req: {
applyOps: [{op: "i", ns: coll.getFullName(), o: {_id: 2}}],
preCondition: [{ns: coll.getFullName(), q: {_id: 99}, res: {_id: 99}}]
},
setupFunc: function() {
assert.commandWorked(coll.insert({_id: 1}));
},
confirmFunc: function(res) {
assert.commandFailed(res,
"The applyOps command was expected to fail, but instead succeeded.");
assert.eq(
res.errmsg, "preCondition failed", "The applyOps command failed for the wrong reason.");
}
});
// 'update' where the document to update does not exist.
commands.push({
req: {update: collName, updates: [{q: {a: 1}, u: {b: 2}}]},
setupFunc: function() {
assert.commandWorked(coll.insert({a: 1}));
assert.commandWorked(coll.update({a: 1}, {b: 2}));
},
confirmFunc: function(res) {
assert.commandWorkedIgnoringWriteConcernErrors(res);
assert.eq(res.n, 0);
assert.eq(res.nModified, 0);
assert.eq(coll.find().itcount(), 1);
assert.eq(coll.count({b: 2}), 1);
}
});
// 'update' where the update has already been done.
commands.push({
req: {update: collName, updates: [{q: {a: 1}, u: {$set: {b: 2}}}]},
setupFunc: function() {
assert.commandWorked(coll.insert({a: 1}));
assert.commandWorked(coll.update({a: 1}, {$set: {b: 2}}));
},
confirmFunc: function(res) {
assert.commandWorkedIgnoringWriteConcernErrors(res);
assert.eq(res.n, 1);
assert.eq(res.nModified, 0);
assert.eq(coll.find().itcount(), 1);
assert.eq(coll.count({a: 1, b: 2}), 1);
}
});
// 'update' with immutable field error.
commands.push({
req: {update: collName, updates: [{q: {_id: 1}, u: {$set: {_id: 2}}}]},
setupFunc: function() {
assert.commandWorked(coll.insert({_id: 1}));
},
confirmFunc: function(res) {
assert.eq(res.n, 0);
assert.eq(res.nModified, 0);
assert.eq(coll.count({_id: 1}), 1);
}
});
// 'delete' where the document to delete does not exist.
commands.push({
req: {delete: collName, deletes: [{q: {a: 1}, limit: 1}]},
setupFunc: function() {
assert.commandWorked(coll.insert({a: 1}));
assert.commandWorked(coll.remove({a: 1}));
},
confirmFunc: function(res) {
assert.commandWorkedIgnoringWriteConcernErrors(res);
assert.eq(res.n, 0);
assert.eq(coll.count({a: 1}), 0);
}
});
// 'createIndexes' where the index has already been created.
// All voting data bearing nodes are not up for this test. So 'createIndexes' command can't succeed
// with the default index commitQuorum value "votingMembers". So, running createIndexes cmd using
// commit quorum "majority".
commands.push({
req: {createIndexes: collName, indexes: [{key: {a: 1}, name: "a_1"}], commitQuorum: "majority"},
setupFunc: function() {
assert.commandWorked(coll.insert({a: 1}));
assert.commandWorkedIgnoringWriteConcernErrors(db.runCommand({
createIndexes: collName,
indexes: [{key: {a: 1}, name: "a_1"}],
commitQuorum: "majority"
}));
},
confirmFunc: function(res) {
assert.commandWorkedIgnoringWriteConcernErrors(res);
assert.eq(res.numIndexesBefore, res.numIndexesAfter);
assert.eq(res.note, 'all indexes already exist');
}
});
// 'findAndModify' where the document to update does not exist.
commands.push({
req: {findAndModify: collName, query: {a: 1}, update: {b: 2}},
setupFunc: function() {
assert.commandWorked(coll.insert({a: 1}));
assert.commandWorkedIgnoringWriteConcernErrors(
db.runCommand({findAndModify: collName, query: {a: 1}, update: {b: 2}}));
},
confirmFunc: function(res) {
assert.commandWorkedIgnoringWriteConcernErrors(res);
assert.eq(res.lastErrorObject.updatedExisting, false);
assert.eq(coll.find().itcount(), 1);
assert.eq(coll.count({b: 2}), 1);
}
});
// 'findAndModify' where the update has already been done.
commands.push({
req: {findAndModify: collName, query: {a: 1}, update: {$set: {b: 2}}},
setupFunc: function() {
assert.commandWorked(coll.insert({a: 1}));
assert.commandWorkedIgnoringWriteConcernErrors(
db.runCommand({findAndModify: collName, query: {a: 1}, update: {$set: {b: 2}}}));
},
confirmFunc: function(res) {
assert.commandWorkedIgnoringWriteConcernErrors(res);
assert.eq(res.lastErrorObject.updatedExisting, true);
assert.eq(coll.find().itcount(), 1);
assert.eq(coll.count({a: 1, b: 2}), 1);
}
});
// 'findAndModify' with immutable field error.
commands.push({
req: {findAndModify: collName, query: {_id: 1}, update: {$set: {_id: 2}}},
setupFunc: function() {
assert.commandWorked(coll.insert({_id: 1}));
},
confirmFunc: function(res) {
assert.commandFailedWithCode(res, ErrorCodes.ImmutableField);
assert.eq(coll.find().itcount(), 1);
assert.eq(coll.count({_id: 1}), 1);
}
});
// 'findAndModify' where the document to delete does not exist.
commands.push({
req: {findAndModify: collName, query: {a: 1}, remove: true},
setupFunc: function() {
assert.commandWorked(coll.insert({a: 1}));
assert.commandWorked(coll.remove({a: 1}));
},
confirmFunc: function(res) {
assert.commandWorkedIgnoringWriteConcernErrors(res);
assert.eq(res.lastErrorObject.n, 0);
}
});
// 'dropDatabase' where the database has already been dropped.
commands.push({
req: {dropDatabase: 1},
setupFunc: function() {
assert.commandWorked(coll.insert({a: 1}));
assert.commandWorkedIgnoringWriteConcernErrors(db.runCommand({dropDatabase: 1}));
},
confirmFunc: function(res) {
assert.commandWorkedIgnoringWriteConcernErrors(res);
}
});
// 'drop' where the collection has already been dropped.
commands.push({
req: {drop: collName},
setupFunc: function() {
assert.commandWorked(coll.insert({a: 1}));
assert.commandWorkedIgnoringWriteConcernErrors(db.runCommand({drop: collName}));
},
confirmFunc: function(res) {
assert.commandFailedWithCode(res, ErrorCodes.NamespaceNotFound);
}
});
// 'create' where the collection has already been created.
commands.push({
req: {create: collName},
setupFunc: function() {
assert.commandWorkedIgnoringWriteConcernErrors(db.runCommand({create: collName}));
},
confirmFunc: function(res) {
assert.commandFailedWithCode(res, ErrorCodes.NamespaceExists);
}
});
// 'insert' where the document with the same _id has already been inserted.
commands.push({
req: {insert: collName, documents: [{_id: 1}]},
setupFunc: function() {
assert.commandWorked(coll.insert({_id: 1}));
},
confirmFunc: function(res) {
assert.commandWorkedIgnoringWriteErrorsAndWriteConcernErrors(res);
assert.eq(res.n, 0);
assert.eq(res.writeErrors[0].code, ErrorCodes.DuplicateKey);
assert.eq(coll.count({_id: 1}), 1);
}
});
function testCommandWithWriteConcern(cmd) {
// Provide a small wtimeout that we expect to time out.
cmd.req.writeConcern = {w: 3, wtimeout: 1000};
jsTest.log("Testing " + tojson(cmd.req));
dropTestCollection();
cmd.setupFunc();
// We run the command on a different connection. If the the command were run on the
// same connection, then the client last op for the noop write would be set by the setup
// operation. By using a fresh connection the client last op begins as null.
// This test explicitly tests that write concern for noop writes works when the
// client last op has not already been set by a duplicate operation.
var shell2 = new Mongo(primary.host);
// We check the error code of 'res' in the 'confirmFunc'.
var res = shell2.getDB(dbName).runCommand(cmd.req);
try {
// Tests that the command receives a write concern error. If we don't wait for write
// concern on noop writes then we won't get a write concern error.
assertWriteConcernError(res);
cmd.confirmFunc(res);
} catch (e) {
// Make sure that we print out the response.
printjson(res);
throw e;
}
}
commands.forEach(function(cmd) {
testCommandWithWriteConcern(cmd);
});
replTest.stopSet();
})();