diff options
3 files changed, 176 insertions, 37 deletions
diff --git a/jstests/sharding/database_versioning_all_commands.js b/jstests/sharding/database_versioning_all_commands.js index d932a2f8312..2121fdcc632 100644 --- a/jstests/sharding/database_versioning_all_commands.js +++ b/jstests/sharding/database_versioning_all_commands.js @@ -117,6 +117,9 @@ function testCommandAfterMovePrimary(testCase, st, dbName, collName) { // Run the test case's command. if (testCase.runsAgainstAdminDb) { assert.commandWorked(st.s0.adminCommand(command)); + } else if (testCase.expectedFailureCode) { + assert.commandFailedWithCode(st.s0.getDB(dbName).runCommand(command), + testCase.expectedFailureCode); } else { assert.commandWorked(st.s0.getDB(dbName).runCommand(command)); } @@ -198,6 +201,9 @@ function testCommandAfterDropRecreateDatabase(testCase, st) { // Run the test case's command. if (testCase.runsAgainstAdminDb) { assert.commandWorked(st.s0.adminCommand(command)); + } else if (testCase.expectedFailureCode) { + assert.commandFailedWithCode(st.s0.getDB(dbName).runCommand(command), + testCase.expectedFailureCode); } else { assert.commandWorked(st.s0.getDB(dbName).runCommand(command)); } @@ -606,6 +612,8 @@ let testCases = { run: { sendsDbVersion: true, explicitlyCreateCollection: true, + // The command should fail if there is no active index build on the collection. + expectedFailureCode: ErrorCodes.IndexNotFound, command: function(dbName, collName) { return { setIndexCommitQuorum: collName, diff --git a/jstests/sharding/set_index_commit_quorum_through_mongos.js b/jstests/sharding/set_index_commit_quorum_through_mongos.js new file mode 100644 index 00000000000..fabcc3e161b --- /dev/null +++ b/jstests/sharding/set_index_commit_quorum_through_mongos.js @@ -0,0 +1,118 @@ +/** + * Tests the behavior of setIndexCommitQuorum when routed through a mongos. + * + * @tags: [requires_fcv_44] + */ +(function() { +"use strict"; +load("jstests/libs/fail_point_util.js"); +load('jstests/libs/parallelTester.js'); + +const st = new ShardingTest({shards: 2, mongos: 1, rs: {nodes: 2}}); +const dbName = "testDB"; +const collName = "coll"; +const mongosDB = st.s0.getDB(dbName); +const shard0 = st.shard0.rs.getPrimary(); + +const generateCreateIndexThread = (host, dbName, collName) => { + return new Thread(function(host, dbName, collName) { + const mongos = new Mongo(host); + const db = mongos.getDB(dbName); + // Use the index builds coordinator for a two-phase index build. + assert.commandWorked(db.runCommand({ + createIndexes: collName, + indexes: [{key: {idxKey: 1}, name: 'idxKey_1'}], + commitQuorum: "majority" + })); + }, host, dbName, collName); +}; + +// Create a sharded collection with primary shard shard0. +const shardCollectionWithPrimaryShard0 = (dbName, collName, shardKeyPattern) => { + assert.commandWorked(st.s.adminCommand({enableSharding: dbName})); + st.ensurePrimaryShard(dbName, st.shard0.shardName); + assert.commandWorked(st.s.adminCommand( + {shardCollection: mongosDB[collName].getFullName(), key: shardKeyPattern})); +}; + +// When setIndexCommitQuorum is sent by the mongos, the response follows the format: +// { +// raw: { +// "<shard0URL>": { +// <response from setIndexCommitQuorum being run on shard> +// }, +// "<shard1URL>": {....} +// } +// ok: <> +// ... +// } +// +// Returns the shard's corresponding entry in the "raw" field of the mongosResponse. +const extractShardReplyFromResponse = (shard, mongosResponse) => { + assert(mongosResponse.raw); + const shardURL = shard.rs.getURL(); + return mongosResponse.raw[shardURL]; +}; + +jsTest.log("Testing setIndexCommitQuorum from a mongos can succeed"); + +// Create a sharded collection where only shard0 owns chunks. +shardCollectionWithPrimaryShard0(dbName, collName, {_id: 1}); +assert.commandWorked(mongosDB[collName].insert({_id: 1})); + +let createIndexThread = generateCreateIndexThread(st.s0.host, dbName, collName); +let createIndexFailpoint = configureFailPoint(shard0, "hangAfterIndexBuildFirstDrain"); +createIndexThread.start(); +createIndexFailpoint.wait(); + +// Confirm mongos succeeds when only one shard owns chunks (and data for the collection). +assert.commandWorked(mongosDB.runCommand( + {setIndexCommitQuorum: collName, indexNames: ["idxKey_1"], commitQuorum: 2})); +createIndexFailpoint.off(); +createIndexThread.join(); +assert(mongosDB[collName].drop()); + +jsTest.log("Testing setIndexCommitQuorum from a mongos fails but reports partial success"); +// In the event that setIndexCommitQuorum succeeds on one shard, but fails on another, the mongos +// setIndexCommitQuorum should return an error and include information on what shards +// failed/succeeded. + +// Create a sharded collection where both shards own chunks but only shard0 bears data. +shardCollectionWithPrimaryShard0(dbName, collName, {_id: 1}); +const ns = mongosDB[collName].getFullName(); + +// Chunk distribution - shard0: [minKey, 10), shard1: [10, maxKey). +assert.commandWorked(st.s.adminCommand({split: ns, middle: {_id: 10}})); +assert.commandWorked(st.s.adminCommand( + {movechunk: ns, find: {_id: 10}, to: st.shard1.shardName, waitForDelete: true})); + +// Insert data into shard0 only. +assert.commandWorked(mongosDB[collName].insert({_id: 1})); + +createIndexThread = generateCreateIndexThread(st.s0.host, dbName, collName); +createIndexFailpoint = configureFailPoint(shard0, "hangAfterIndexBuildFirstDrain"); + +createIndexThread.start(); +createIndexFailpoint.wait(); + +const res = assert.commandFailedWithCode( + mongosDB.runCommand( + {setIndexCommitQuorum: collName, indexNames: ["idxKey_1"], commitQuorum: 2}), + ErrorCodes.IndexNotFound); + +// Confirm the mongos reports shard0 successfully set its index commit quorum despite the mongos +// command failing. +const shard0Reply = extractShardReplyFromResponse(st.shard0, res); +assert.eq(shard0Reply.ok, 1); + +// Confirm shard1 caused the command to fail since it didn't actually own any data for the +// collection. +const shard1Reply = extractShardReplyFromResponse(st.shard1, res); +assert.eq(shard1Reply.ok, 0); +assert.eq(shard1Reply.code, ErrorCodes.IndexNotFound); + +createIndexFailpoint.off(); +createIndexThread.join(); + +st.stop(); +}()); diff --git a/src/mongo/s/commands/cluster_set_index_commit_quorum_cmd.cpp b/src/mongo/s/commands/cluster_set_index_commit_quorum_cmd.cpp index 974510960e2..8727a9da078 100644 --- a/src/mongo/s/commands/cluster_set_index_commit_quorum_cmd.cpp +++ b/src/mongo/s/commands/cluster_set_index_commit_quorum_cmd.cpp @@ -36,9 +36,9 @@ #include "mongo/db/auth/authorization_session.h" #include "mongo/db/commands.h" -#include "mongo/db/commands/set_index_commit_quorum_gen.h" #include "mongo/logv2/log.h" #include "mongo/s/cluster_commands_helpers.h" +#include "mongo/s/grid.h" namespace mongo { namespace { @@ -53,9 +53,9 @@ namespace { * commitQuorum: "majority" / 3 / {"replTagName": "replTagValue"}, * } */ -class SetIndexCommitQuorumCommand final : public TypedCommand<SetIndexCommitQuorumCommand> { +class SetIndexCommitQuorumCommand : public BasicCommand { public: - using Request = SetIndexCommitQuorum; + SetIndexCommitQuorumCommand() : BasicCommand("setIndexCommitQuorum") {} std::string help() const override { std::stringstream ss; @@ -79,45 +79,58 @@ public: return AllowedOnSecondary::kNever; } - class Invocation final : public InvocationBase { - public: - using InvocationBase::InvocationBase; - - void typedRun(OperationContext* opCtx) { - BSONObj cmdObj = request().toBSON(BSONObj()); - LOGV2_DEBUG(22757, - 1, - "setIndexCommitQuorum", - "namespace"_attr = request().getNamespace(), - "command"_attr = redact(cmdObj)); - - scatterGatherOnlyVersionIfUnsharded( - opCtx, - request().getNamespace(), - applyReadWriteConcern( - opCtx, this, CommandHelpers::filterCommandRequestForPassthrough(cmdObj)), - ReadPreferenceSetting::get(opCtx), - Shard::RetryPolicy::kNotIdempotent); - } + bool supportsWriteConcern(const BSONObj& cmd) const final { + return true; + } - private: - NamespaceString ns() const override { - return request().getNamespace(); + Status checkAuthForOperation(OperationContext* opCtx, + const std::string& dbName, + const BSONObj& cmdObj) const override { + const NamespaceString nss(CommandHelpers::parseNsCollectionRequired(dbName, cmdObj)); + if (!AuthorizationSession::get(opCtx->getClient()) + ->isAuthorizedForActionsOnResource(ResourcePattern::forExactNamespace(nss), + ActionType::createIndex)) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); } + return Status::OK(); + } - bool supportsWriteConcern() const override { - return true; + bool run(OperationContext* opCtx, + const std::string& dbName, + const BSONObj& cmdObj, + BSONObjBuilder& result) override { + const NamespaceString nss(CommandHelpers::parseNsCollectionRequired(dbName, cmdObj)); + LOGV2_DEBUG(22757, + 1, + "setIndexCommitQuorum", + "namespace"_attr = nss, + "command"_attr = redact(cmdObj)); + + auto routingInfo = + uassertStatusOK(Grid::get(opCtx)->catalogCache()->getCollectionRoutingInfo(opCtx, nss)); + auto shardResponses = scatterGatherVersionedTargetByRoutingTable( + opCtx, + nss.db(), + nss, + routingInfo, + applyReadWriteConcern( + opCtx, this, CommandHelpers::filterCommandRequestForPassthrough(cmdObj)), + ReadPreferenceSetting::get(opCtx), + Shard::RetryPolicy::kNotIdempotent, + BSONObj() /* query */, + BSONObj() /* collation */); + + std::string errmsg; + const bool ok = + appendRawResponses(opCtx, &errmsg, &result, std::move(shardResponses)).responseOK; + CommandHelpers::appendSimpleCommandStatus(result, ok, errmsg); + + if (ok) { + LOGV2(5688700, "Index commit quorums set", "namespace"_attr = nss); } - void doCheckAuthorization(OperationContext* opCtx) const override { - uassert(ErrorCodes::Unauthorized, - "Unauthorized", - AuthorizationSession::get(opCtx->getClient()) - ->isAuthorizedForActionsOnResource( - ResourcePattern::forExactNamespace(request().getNamespace()), - ActionType::createIndex)); - } - }; + return ok; + } } setCommitQuorumCmd; |