mongo/jstests/sharding/commands_that_write_accept_...

554 lines
19 KiB
JavaScript

/**
* This file tests that commands that do writes accept a write concern in a sharded cluster. This
* test defines various database commands and what they expect to be true before and after the fact.
* It then runs the commands with an invalid writeConcern and a valid writeConcern and
* ensures that they succeed and fail appropriately. This only tests functions that aren't run
* on config servers.
*
* This test is labeled resource intensive because its total io_write is 58MB compared to a median
* of 5MB across all sharding tests in wiredTiger.
* @tags: [
* resource_intensive,
* requires_scripting
* ]
*/
import {ShardingTest} from "jstests/libs/shardingtest.js";
import {
assertWriteConcernError,
runCommandCheckAdmin,
shardCollectionWithChunks,
} from "jstests/libs/write_concern_util.js";
let st = new ShardingTest({
// Set priority of secondaries to zero to prevent spurious elections.
shards: {
rs0: {
nodes: [{}, {rsConfig: {priority: 0}}, {rsConfig: {priority: 0}}],
settings: {chainingAllowed: false},
},
rs1: {
nodes: [{}, {rsConfig: {priority: 0}}, {rsConfig: {priority: 0}}],
settings: {chainingAllowed: false},
},
},
configReplSetTestOptions: {settings: {chainingAllowed: false}},
mongos: 1,
});
let mongos = st.s;
let dbName = "wc-test-shards";
var db = mongos.getDB(dbName);
let collName = "leaves";
let coll = db[collName];
function dropTestDatabase() {
db.runCommand({dropDatabase: 1});
db.extra.insert({a: 1});
coll = db[collName];
assert.eq(0, coll.find().itcount(), "test collection not empty");
assert.eq(1, db.extra.find().itcount(), "extra collection should have 1 document");
}
let commands = [];
// Tests a runOnAllShardsCommand against a sharded collection.
commands.push({
req: {createIndexes: collName, indexes: [{key: {"type": 1}, name: "sharded_type_index"}]},
setupFunc: function () {
shardCollectionWithChunks(st, coll);
coll.insert({type: "oak", x: -3});
coll.insert({type: "maple", x: 23});
assert.eq(coll.getIndexes().length, 2);
},
confirmFunc: function () {
assert.eq(coll.getIndexes().length, 3);
},
admin: false,
isExpectedToWriteOnWriteConcernFailure: true,
});
// Tests a runOnAllShardsCommand.
commands.push({
req: {createIndexes: collName, indexes: [{key: {"type": 1}, name: "type_index"}]},
setupFunc: function () {
coll.insert({type: "oak"});
assert.eq(coll.getIndexes().length, 1);
},
confirmFunc: function () {
assert.eq(coll.getIndexes().length, 2);
},
admin: false,
isExpectedToWriteOnWriteConcernFailure: true,
});
// Tests a batched write command.
commands.push({
req: {
insert: collName,
documents: [
{x: -3, type: "maple"},
{x: 23, type: "maple"},
],
},
setupFunc: function () {
shardCollectionWithChunks(st, coll);
},
confirmFunc: function () {
assert.eq(coll.count({type: "maple"}), 2);
},
admin: false,
isExpectedToWriteOnWriteConcernFailure: true,
});
commands.push({
req: {renameCollection: "renameCollWC.leaves", to: "renameCollWC.pine_needles"},
setupFunc: function () {
db = db.getSiblingDB("renameCollWC");
// Ensure that database is created.
db.leaves.insert({type: "oak"});
db.leaves.drop();
db.pine_needles.drop();
db.leaves.insert({type: "oak"});
assert.eq(db.leaves.count(), 1);
assert.eq(db.pine_needles.count(), 0);
},
confirmFunc: function () {
assert.eq(db.leaves.count(), 0);
assert.eq(db.pine_needles.count(), 1);
},
admin: true,
isExpectedToWriteOnWriteConcernFailure: true,
});
commands.push({
req: {
update: collName,
updates: [
{
q: {type: "oak"},
u: [{$set: {type: "ginkgo"}}],
},
],
writeConcern: {w: "majority"},
},
setupFunc: function () {
coll.insert({type: "oak"});
assert.eq(coll.count({type: "ginkgo"}), 0);
assert.eq(coll.count({type: "oak"}), 1);
},
confirmFunc: function () {
assert.eq(coll.count({type: "ginkgo"}), 1);
assert.eq(coll.count({type: "oak"}), 0);
},
admin: false,
isExpectedToWriteOnWriteConcernFailure: true,
});
commands.push({
req: {
findAndModify: collName,
query: {type: "oak"},
update: {$set: {type: "ginkgo"}},
writeConcern: {w: "majority"},
},
setupFunc: function () {
coll.insert({type: "oak"});
assert.eq(coll.count({type: "ginkgo"}), 0);
assert.eq(coll.count({type: "oak"}), 1);
},
confirmFunc: function () {
assert.eq(coll.count({type: "ginkgo"}), 1);
assert.eq(coll.count({type: "oak"}), 0);
},
admin: false,
isExpectedToWriteOnWriteConcernFailure: true,
});
commands.push({
req: {
findAndModify: collName,
query: {type: "oak"},
update: [{$set: {type: "ginkgo"}}],
writeConcern: {w: "majority"},
},
setupFunc: function () {
coll.insert({type: "oak"});
assert.eq(coll.count({type: "ginkgo"}), 0);
assert.eq(coll.count({type: "oak"}), 1);
},
confirmFunc: function () {
assert.eq(coll.count({type: "ginkgo"}), 1);
assert.eq(coll.count({type: "oak"}), 0);
},
admin: false,
isExpectedToWriteOnWriteConcernFailure: true,
});
// Aggregate run against an unsharded collection.
commands.push({
req: {aggregate: collName, pipeline: [{$sort: {type: 1}}, {$out: "foo"}], cursor: {}},
setupFunc: function () {
coll.insert({_id: 1, x: 3, type: "oak"});
coll.insert({_id: 2, x: 13, type: "maple"});
},
confirmFunc: function (writesExpected = true) {
if (writesExpected) {
assert.eq(db.foo.find({type: "oak"}).itcount(), 1);
assert.eq(db.foo.find({type: "maple"}).itcount(), 1);
db.foo.drop();
} else {
assert.eq(0, db.foo.find().itcount());
}
},
admin: false,
// Aggregation pipelines with a $out stage are expected not to write on write concern error.
isExpectedToWriteOnWriteConcernFailure: false,
});
// Aggregate that only matches one shard.
commands.push({
req: {
aggregate: collName,
pipeline: [{$match: {x: -3}}, {$match: {type: {$exists: 1}}}, {$out: "foo"}],
cursor: {},
},
setupFunc: function () {
shardCollectionWithChunks(st, coll);
coll.insert({_id: 1, x: -3, type: "oak"});
coll.insert({_id: 2, x: -4, type: "maple"});
},
confirmFunc: function (writesExpected = true) {
if (writesExpected) {
assert.eq(db.foo.find().itcount(), 1);
assert.eq(db.foo.find({type: "oak"}).itcount(), 1);
assert.eq(db.foo.find({type: "maple"}).itcount(), 0);
db.foo.drop();
} else {
assert.eq(0, db.foo.find().itcount());
}
},
admin: false,
// Aggregation pipelines with a $out stage are expected not to write on write concern error.
isExpectedToWriteOnWriteConcernFailure: false,
});
// Aggregate that must go to multiple shards.
commands.push({
req: {
aggregate: collName,
pipeline: [{$match: {type: {$exists: 1}}}, {$sort: {type: 1}}, {$out: "foo"}],
cursor: {},
},
setupFunc: function () {
shardCollectionWithChunks(st, coll);
coll.insert({_id: 1, x: -3, type: "oak"});
coll.insert({_id: 2, x: 23, type: "maple"});
},
confirmFunc: function (writesExpected = true) {
if (writesExpected) {
assert.eq(db.foo.find().itcount(), 2);
assert.eq(db.foo.find({type: "oak"}).itcount(), 1);
assert.eq(db.foo.find({type: "maple"}).itcount(), 1);
db.foo.drop();
} else {
assert.eq(0, db.foo.find().itcount());
}
},
admin: false,
// Aggregation pipelines with a $out stage are expected not to write on write concern error.
isExpectedToWriteOnWriteConcernFailure: false,
});
const map = function () {
if (!this.tags) {
return;
}
this.tags.forEach(function (z) {
emit(z, 1);
});
};
const reduce = function (key, values) {
let count = 0;
values.forEach(function (v) {
count = count + v;
});
return count;
};
// MapReduce on an unsharded collection.
commands.push({
req: {mapReduce: collName, map: map, reduce: reduce, out: "foo"},
setupFunc: function () {
coll.insert({x: -3, tags: ["a", "b"]});
coll.insert({x: -7, tags: ["b", "c"]});
coll.insert({x: 23, tags: ["c", "a"]});
coll.insert({x: 27, tags: ["b", "c"]});
},
confirmFunc: function (writesExpected = true) {
if (writesExpected) {
assert.eq(db.foo.findOne({_id: "a"}).value, 2);
assert.eq(db.foo.findOne({_id: "b"}).value, 3);
assert.eq(db.foo.findOne({_id: "c"}).value, 3);
db.foo.drop();
} else {
assert.eq(0, db.foo.find().itcount());
}
},
admin: false,
// MapReduce with a replace output action is not expected to write on write concern error.
isExpectedToWriteOnWriteConcernFailure: false,
});
// MapReduce on an unsharded collection with an output to a sharded collection.
commands.push({
req: {mapReduce: collName, map: map, reduce: reduce, out: {merge: "foo", sharded: true}},
setupFunc: function () {
db.adminCommand({enablesharding: db.toString()});
assert.commandWorked(db.foo.createIndex({_id: "hashed"}));
st.shardColl(db.foo, {_id: "hashed"}, false);
coll.insert({x: -3, tags: ["a", "b"]});
coll.insert({x: -7, tags: ["b", "c"]});
coll.insert({x: 23, tags: ["c", "a"]});
coll.insert({x: 27, tags: ["b", "c"]});
},
confirmFunc: function (writesExpected = true) {
if (writesExpected) {
assert.eq(db.foo.findOne({_id: "a"}).value, 2);
assert.eq(db.foo.findOne({_id: "b"}).value, 3);
assert.eq(db.foo.findOne({_id: "c"}).value, 3);
db.foo.drop();
} else {
assert.eq(0, db.foo.find().itcount());
}
},
admin: false,
isExpectedToWriteOnWriteConcernFailure: true,
});
// MapReduce on a sharded collection.
commands.push({
req: {mapReduce: collName, map: map, reduce: reduce, out: "foo"},
setupFunc: function () {
shardCollectionWithChunks(st, coll);
coll.insert({x: -3, tags: ["a", "b"]});
coll.insert({x: -7, tags: ["b", "c"]});
coll.insert({x: 23, tags: ["c", "a"]});
coll.insert({x: 27, tags: ["b", "c"]});
},
confirmFunc: function (writesExpected = true) {
if (writesExpected) {
assert.eq(db.foo.findOne({_id: "a"}).value, 2);
assert.eq(db.foo.findOne({_id: "b"}).value, 3);
assert.eq(db.foo.findOne({_id: "c"}).value, 3);
db.foo.drop();
} else {
assert.eq(0, db.foo.find().itcount());
}
},
admin: false,
// MapReduce with a replace output action expected not to write on write concern error.
isExpectedToWriteOnWriteConcernFailure: false,
});
// MapReduce from a sharded collection with merge to a sharded collection.
commands.push({
req: {mapReduce: collName, map: map, reduce: reduce, out: {merge: "foo", sharded: true}},
setupFunc: function () {
shardCollectionWithChunks(st, coll);
assert.commandWorked(db.foo.createIndex({_id: "hashed"}));
st.shardColl(db.foo, {_id: "hashed"}, false);
coll.insert({x: -3, tags: ["a", "b"]});
coll.insert({x: -7, tags: ["b", "c"]});
coll.insert({x: 23, tags: ["c", "a"]});
coll.insert({x: 27, tags: ["b", "c"]});
},
confirmFunc: function (writesExpected = true) {
if (writesExpected) {
assert.eq(db.foo.findOne({_id: "a"}).value, 2);
assert.eq(db.foo.findOne({_id: "b"}).value, 3);
assert.eq(db.foo.findOne({_id: "c"}).value, 3);
db.foo.drop();
} else {
assert.eq(0, db.foo.find().itcount());
}
},
admin: false,
// When output action is "merge" writes to the output collection are expected to succeed,
// regardless of writeConcern error.
isExpectedToWriteOnWriteConcernFailure: true,
});
// MapReduce on an unsharded collection, output to different database.
commands.push({
req: {mapReduce: collName, map: map, reduce: reduce, out: {replace: "foo", db: "other"}},
setupFunc: function () {
db.getSiblingDB("other").createCollection("foo");
coll.insert({x: -3, tags: ["a", "b"]});
coll.insert({x: -7, tags: ["b", "c"]});
coll.insert({x: 23, tags: ["c", "a"]});
coll.insert({x: 27, tags: ["b", "c"]});
},
confirmFunc: function (writesExpected = true) {
const otherDB = db.getSiblingDB("other");
if (writesExpected) {
assert.eq(otherDB.foo.findOne({_id: "a"}).value, 2);
assert.eq(otherDB.foo.findOne({_id: "b"}).value, 3);
assert.eq(otherDB.foo.findOne({_id: "c"}).value, 3);
otherDB.foo.drop();
} else {
assert.eq(0, otherDB.foo.find().itcount());
}
},
admin: false,
// MapReduce with a replace output action is not expected to write on write concern error.
isExpectedToWriteOnWriteConcernFailure: false,
});
// MapReduce on a sharded collection, output to a different database.
commands.push({
req: {mapReduce: collName, map: map, reduce: reduce, out: {replace: "foo", db: "other"}},
setupFunc: function () {
db.getSiblingDB("other").createCollection("foo");
shardCollectionWithChunks(st, coll);
coll.insert({x: -3, tags: ["a", "b"]});
coll.insert({x: -7, tags: ["b", "c"]});
coll.insert({x: 23, tags: ["c", "a"]});
coll.insert({x: 27, tags: ["b", "c"]});
},
confirmFunc: function (writesExpected = true) {
const otherDB = db.getSiblingDB("other");
if (writesExpected) {
assert.eq(otherDB.foo.findOne({_id: "a"}).value, 2);
assert.eq(otherDB.foo.findOne({_id: "b"}).value, 3);
assert.eq(otherDB.foo.findOne({_id: "c"}).value, 3);
otherDB.foo.drop();
} else {
assert.eq(0, otherDB.foo.find().itcount());
}
},
admin: false,
// MapReduce with a replace output action expected not to write on write concern error.
isExpectedToWriteOnWriteConcernFailure: false,
});
// MapReduce on an unsharded collection with merge to a sharded collection in a different db.
commands.push({
req: {
mapReduce: collName,
map: map,
reduce: reduce,
out: {merge: "foo", db: "other", sharded: true},
},
setupFunc: function () {
const otherDB = db.getSiblingDB("other");
otherDB.createCollection("foo");
otherDB.adminCommand({enablesharding: otherDB.toString()});
assert.commandWorked(otherDB.foo.createIndex({_id: "hashed"}));
st.shardColl(otherDB.foo, {_id: "hashed"}, false);
coll.insert({x: -3, tags: ["a", "b"]});
coll.insert({x: -7, tags: ["b", "c"]});
coll.insert({x: 23, tags: ["c", "a"]});
coll.insert({x: 27, tags: ["b", "c"]});
},
confirmFunc: function (writesExpected = true) {
const otherDB = db.getSiblingDB("other");
if (writesExpected) {
assert.eq(otherDB.foo.findOne({_id: "a"}).value, 2);
assert.eq(otherDB.foo.findOne({_id: "b"}).value, 3);
assert.eq(otherDB.foo.findOne({_id: "c"}).value, 3);
otherDB.foo.drop();
} else {
assert.eq(0, otherDB.foo.find().itcount());
}
},
admin: false,
isExpectedToWriteOnWriteConcernFailure: true,
});
// MapReduce from a sharded collection with merge to a sharded collection in different db.
commands.push({
req: {
mapReduce: collName,
map: map,
reduce: reduce,
out: {merge: "foo", db: "other", sharded: true},
},
setupFunc: function () {
shardCollectionWithChunks(st, coll);
const otherDB = db.getSiblingDB("other");
otherDB.createCollection("foo");
assert.commandWorked(otherDB.foo.createIndex({_id: "hashed"}));
st.shardColl(otherDB.foo, {_id: "hashed"}, false);
coll.insert({x: -3, tags: ["a", "b"]});
coll.insert({x: -7, tags: ["b", "c"]});
coll.insert({x: 23, tags: ["c", "a"]});
coll.insert({x: 27, tags: ["b", "c"]});
},
confirmFunc: function (writesExpected = true) {
const otherDB = db.getSiblingDB("other");
if (writesExpected) {
assert.eq(otherDB.foo.findOne({_id: "a"}).value, 2);
assert.eq(otherDB.foo.findOne({_id: "b"}).value, 3);
assert.eq(otherDB.foo.findOne({_id: "c"}).value, 3);
otherDB.foo.drop();
} else {
assert.eq(0, otherDB.foo.find().itcount());
}
},
admin: false,
// When output action is "merge" writes to the output collection are expected to succeed,
// regardless of writeConcern error.
isExpectedToWriteOnWriteConcernFailure: true,
});
function testValidWriteConcern(cmd) {
cmd.req.writeConcern = {w: "majority", wtimeout: 5 * 60 * 1000};
jsTest.log("Testing " + tojson(cmd.req));
dropTestDatabase();
cmd.setupFunc();
let res = runCommandCheckAdmin(db, cmd);
assert.commandWorked(res);
assert(!res.writeConcernError, "command on a full cluster had writeConcernError: " + tojson(res));
cmd.confirmFunc();
}
function testInvalidWriteConcern(cmd) {
cmd.req.writeConcern = {w: "invalid"};
jsTest.log("Testing " + tojson(cmd.req));
dropTestDatabase();
cmd.setupFunc();
let res = runCommandCheckAdmin(db, cmd);
// When a cluster aggregate command with a write stage fails due to write concern error it will
// return a regular error response with "ok: 0" rather than "ok: 1" with a `writeConcernError`
// sub-object detailing the failure. This is motivated by the fact that the number of documents
// written is bounded only by the number of documents produced by the pipeline, which could lead
// a writeConcernError object to exceed maximum BSON size.
if (cmd.req.aggregate || cmd.req.mapReduce) {
assert.commandFailedWithCode(res, [ErrorCodes.WriteConcernTimeout, ErrorCodes.UnknownReplWriteConcern]);
cmd.confirmFunc(cmd.isExpectedToWriteOnWriteConcernFailure);
} else if (cmd.req.renameCollection) {
// The renameCollection spans multiple nodes and potentially performs writes to the config
// server, so the user-specified write concern has no effect.
assert.commandWorked(res);
assert(!res.writeConcernError, "command on a full cluster had writeConcernError: " + tojson(res));
cmd.confirmFunc();
} else {
assert.commandWorkedIgnoringWriteConcernErrors(res);
assertWriteConcernError(res, ErrorCodes.UnknownReplWriteConcern);
const writesExpected = true;
cmd.confirmFunc(writesExpected);
}
}
commands.forEach(function (cmd) {
testValidWriteConcern(cmd);
testInvalidWriteConcern(cmd);
});
st.stop();