summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBilly Donahue <billy.donahue@mongodb.com>2018-03-26 17:03:57 -0400
committerBilly Donahue <billy.donahue@mongodb.com>2018-04-13 14:13:27 -0400
commit64f24ed205a4d83aece37c5f2c64af26624113c1 (patch)
treedba4eb4288562a5b75ab4201390f500bf665bc38 /src
parent431ebb76fb1dfd296afd913f4d83b0387c89e992 (diff)
downloadmongo-64f24ed205a4d83aece37c5f2c64af26624113c1.tar.gz
SERVER-34148 TypedCommand
Diffstat (limited to 'src')
-rw-r--r--src/mongo/db/SConscript2
-rw-r--r--src/mongo/db/commands.cpp8
-rw-r--r--src/mongo/db/commands.h198
-rw-r--r--src/mongo/db/commands_test.cpp302
-rw-r--r--src/mongo/db/commands_test_example.idl44
5 files changed, 549 insertions, 5 deletions
diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript
index 12b1cc9830d..04346773588 100644
--- a/src/mongo/db/SConscript
+++ b/src/mongo/db/SConscript
@@ -571,8 +571,10 @@ env.CppUnitTest(
target="commands_test",
source=[
"commands_test.cpp",
+ env.Idlc('commands_test_example.idl')[0],
],
LIBDEPS=[
+ '$BUILD_DIR/mongo/idl/idl_parser',
"commands",
"service_context_noop_init",
"auth/authorization_manager_mock_init",
diff --git a/src/mongo/db/commands.cpp b/src/mongo/db/commands.cpp
index 47b3cc15df6..276a471d5a0 100644
--- a/src/mongo/db/commands.cpp
+++ b/src/mongo/db/commands.cpp
@@ -334,6 +334,14 @@ void CommandReplyBuilder::reset() {
getBodyBuilder().resetToEmpty();
}
+void CommandReplyBuilder::fillFrom(const Status& status) {
+ if (!status.isOK()) {
+ reset();
+ }
+ auto bob = getBodyBuilder();
+ CommandHelpers::appendCommandStatus(bob, status);
+}
+
//////////////////////////////////////////////////////////////
// CommandInvocation
diff --git a/src/mongo/db/commands.h b/src/mongo/db/commands.h
index b01d366d866..a1708a1f733 100644
--- a/src/mongo/db/commands.h
+++ b/src/mongo/db/commands.h
@@ -402,6 +402,50 @@ public:
void reset();
+ /**
+ * Appends a key:object field to this reply.
+ */
+ template <typename T>
+ void append(StringData key, const T& object) {
+ getBodyBuilder() << key << object;
+ }
+
+ /**
+ * Write the specified 'status' and associated fields into this reply body, as with
+ * CommandHelpers::appendCommandStatus. Appends the "ok" and related fields if they
+ * haven't already been set.
+ * - If 'status' is not OK, this reply is reset before adding result.
+ * - Otherwise, any data previously written to the body is left in place.
+ */
+ void fillFrom(const Status& status);
+
+ /**
+ * The specified 'object' must be BSON-serializable.
+ * Appends the "ok" and related fields if they haven't already been set.
+ *
+ * BSONSerializable 'x' means 'x.serialize(bob)' appends a representation of 'x'
+ * into 'BSONObjBuilder* bob'.
+ */
+ template <typename T>
+ void fillFrom(const T& object) {
+ auto bob = getBodyBuilder();
+ object.serialize(&bob);
+ CommandHelpers::appendCommandStatus(bob, Status::OK());
+ }
+
+ /**
+ * Equivalent to calling fillFrom with sw.getValue() or sw.getStatus(), whichever
+ * 'sw' is holding.
+ */
+ template <typename T>
+ void fillFrom(const StatusWith<T>& sw) {
+ if (sw.isOK()) {
+ fillFrom(sw.getValue());
+ } else {
+ fillFrom(sw.getStatus());
+ }
+ }
+
private:
BufBuilder* const _bodyBuf;
const std::size_t _bodyOffset;
@@ -427,7 +471,10 @@ public:
virtual void explain(OperationContext* opCtx,
ExplainOptions::Verbosity verbosity,
- BSONObjBuilder* result) = 0;
+ BSONObjBuilder* result) {
+ uasserted(ErrorCodes::IllegalOperation,
+ str::stream() << "Cannot explain cmd: " << definition()->getName());
+ }
/**
* The primary namespace on which this command operates. May just be the db.
@@ -630,7 +677,150 @@ class ErrmsgCommandDeprecated : public BasicCommand {
BSONObjBuilder& result) = 0;
};
-// See the 'globalCommandRegistry()' singleton accessor.
+/**
+ * A CRTP base class for typed commands, which simplifies writing commands that
+ * accept requests generated by IDL. Derive from it as follows:
+ *
+ * class MyCommand : public TypedCommand<MyCommand> {...};
+ *
+ * The 'Derived' type paramter must have:
+ *
+ * - 'Request' naming a usable request type.
+ * A usable Request type must have:
+ *
+ * - a static member factory function 'parse', callable as:
+ *
+ * const IDLParserErrorContext& idlCtx = ...;
+ * const OpMsgRequest& opMsgRequest = ...;
+ * Request r = Request::parse(idlCtx, opMsgRequest);
+ *
+ * which enables it to be parsed as an IDL command.
+ *
+ * - a 'constexpr StringData kCommandName' member.
+ *
+ * Any type generated by the "commands:" section in the IDL syntax meets these
+ * requirements. Note that IDL "structs:" will not. This is the recommended way to
+ * provide this Derived::Request type rather than writing it by hand.
+ *
+ * - 'Invocation' - names a type derived from either of the nested invocation
+ * base classes provided: InvocationBase or MinimalInvocationBase.
+ */
+template <typename Derived>
+class TypedCommand : public Command {
+public:
+ std::unique_ptr<CommandInvocation> parse(OperationContext* opCtx,
+ const OpMsgRequest& opMsgRequest) final;
+
+protected:
+ class InvocationBase;
+
+ // Used instead of InvocationBase when a command must customize the 'run()' member.
+ class MinimalInvocationBase;
+
+ // Commands that only have a single name don't need to define any constructors.
+ TypedCommand() : TypedCommand(Derived::Request::kCommandName) {}
+ explicit TypedCommand(StringData name) : TypedCommand(name, {}) {}
+ TypedCommand(StringData name, StringData altName) : Command(name, altName) {}
+
+private:
+ class InvocationBaseInternal;
+};
+
+template <typename Derived>
+class TypedCommand<Derived>::InvocationBaseInternal : public CommandInvocation {
+public:
+ using RequestType = typename Derived::Request;
+
+ InvocationBaseInternal(OperationContext*,
+ const Command* command,
+ const OpMsgRequest& opMsgRequest)
+ : CommandInvocation(command), _request{_parseRequest(command->getName(), opMsgRequest)} {}
+
+protected:
+ const RequestType& request() const {
+ return _request;
+ }
+
+private:
+ static RequestType _parseRequest(StringData name, const OpMsgRequest& opMsgRequest) {
+ return RequestType::parse(IDLParserErrorContext(name), opMsgRequest);
+ }
+
+ RequestType _request;
+};
+
+template <typename Derived>
+class TypedCommand<Derived>::MinimalInvocationBase : public InvocationBaseInternal {
+ // Implemented as just a strong typedef for InvocationBaseInternal.
+ using InvocationBaseInternal::InvocationBaseInternal;
+};
+
+/*
+ * Classes derived from TypedCommand::InvocationBase must:
+ *
+ * - inherit constructors with 'using InvocationBase::InvocationBase;'.
+ *
+ * - define a 'typedRun' method like:
+ *
+ * R typedRun(OperationContext* opCtx);
+ *
+ * where R is either void or usable as an argument to 'CommandReplyBuilder::fillFrom'.
+ * So it's one of:
+ * - void
+ * - mongo::Status
+ * - T, where T is usable with fillFrom.
+ * - mongo::StatusWith<T>, where T usable with fillFrom.
+ *
+ * Note: a void typedRun produces a "pass-fail" command. If it runs to completion
+ * the result will be considered and formatted as an "ok".
+ *
+ * If the TypedCommand's Request type was specified with the IDL attribute:
+ *
+ * namespace: concatenate_with_db
+ *
+ * then the ns() method of its Invocation class method should be:
+ *
+ * NamespaceString ns() const override {
+ * return request.getNamespace();
+ * }
+ */
+template <typename Derived>
+class TypedCommand<Derived>::InvocationBase : public InvocationBaseInternal {
+public:
+ using InvocationBaseInternal::InvocationBaseInternal;
+
+private:
+ using Invocation = typename Derived::Invocation;
+
+ /**
+ * _callTypedRun and _runImpl implement the tagged dispatch from 'run'.
+ */
+ decltype(auto) _callTypedRun(OperationContext* opCtx) {
+ return static_cast<Invocation*>(this)->typedRun(opCtx);
+ }
+ void _runImpl(std::true_type, OperationContext* opCtx, CommandReplyBuilder*) {
+ _callTypedRun(opCtx);
+ }
+ void _runImpl(std::false_type, OperationContext* opCtx, CommandReplyBuilder* reply) {
+ reply->fillFrom(_callTypedRun(opCtx));
+ }
+
+ void run(OperationContext* opCtx, CommandReplyBuilder* reply) final {
+ using VoidResultTag = std::is_void<decltype(_callTypedRun(opCtx))>;
+ _runImpl(VoidResultTag{}, opCtx, reply);
+ }
+};
+
+template <typename Derived>
+std::unique_ptr<CommandInvocation> TypedCommand<Derived>::parse(OperationContext* opCtx,
+ const OpMsgRequest& opMsgRequest) {
+ return std::make_unique<typename Derived::Invocation>(opCtx, this, opMsgRequest);
+}
+
+
+/**
+ * See the 'globalCommandRegistry()' singleton accessor.
+ */
class CommandRegistry {
public:
using CommandMap = Command::CommandMap;
@@ -659,7 +849,9 @@ private:
CommandMap _commands;
};
-// Accessor to the command registry, an always-valid singleton.
+/**
+ * Accessor to the command registry, an always-valid singleton.
+ */
CommandRegistry* globalCommandRegistry();
} // namespace mongo
diff --git a/src/mongo/db/commands_test.cpp b/src/mongo/db/commands_test.cpp
index cd74dff528e..9a7f36cd60a 100644
--- a/src/mongo/db/commands_test.cpp
+++ b/src/mongo/db/commands_test.cpp
@@ -26,11 +26,13 @@
* then also delete it in the license file.
*/
-#include "mongo/db/commands.h"
+#include "mongo/platform/basic.h"
+
#include "mongo/db/catalog/collection_mock.h"
+#include "mongo/db/commands.h"
+#include "mongo/db/commands_test_example_gen.h"
#include "mongo/db/dbmessage.h"
#include "mongo/db/service_context_noop.h"
-#include "mongo/platform/basic.h"
#include "mongo/unittest/unittest.h"
namespace mongo {
@@ -140,5 +142,301 @@ TEST_F(ParseNsOrUUID, ParseValidUUID) {
ASSERT_EQUALS(uuid, *parsedNsOrUUID.uuid());
}
+/**
+ * TypedCommand test
+ */
+class ExampleIncrementCommand final : public TypedCommand<ExampleIncrementCommand> {
+private:
+ AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
+ return Command::AllowedOnSecondary::kNever;
+ }
+
+ std::string help() const override {
+ return "Return an incremented request.i. Example of a simple TypedCommand.";
+ }
+
+public:
+ using Request = commands_test_example::ExampleIncrement;
+
+ class Invocation final : public InvocationBase {
+ public:
+ using InvocationBase::InvocationBase;
+
+ /**
+ * Reply with an incremented 'request.i'.
+ */
+ auto typedRun(OperationContext* opCtx) {
+ commands_test_example::ExampleIncrementReply r;
+ r.setIPlusOne(request().getI() + 1);
+ return r;
+ }
+
+ private:
+ bool supportsWriteConcern() const override {
+ return true;
+ }
+
+ AllowedOnSecondary secondaryAllowed(ServiceContext* context) const override {
+ return definition()->secondaryAllowed(context);
+ }
+
+ void doCheckAuthorization(OperationContext*) const override {}
+
+ /**
+ * The ns() for when Request's IDL specifies "namespace: concatenate_with_db".
+ */
+ NamespaceString ns() const override {
+ return request().getNamespace();
+ }
+ };
+};
+
+// Just like ExampleIncrementCommand, but using the MinimalInvocationBase.
+class ExampleMinimalCommand final : public TypedCommand<ExampleMinimalCommand> {
+private:
+ AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
+ return Command::AllowedOnSecondary::kNever;
+ }
+
+ std::string help() const override {
+ return "Return an incremented request.i. Example of a simple TypedCommand.";
+ }
+
+public:
+ using Request = commands_test_example::ExampleMinimal;
+
+ class Invocation final : public MinimalInvocationBase {
+ public:
+ using MinimalInvocationBase::MinimalInvocationBase;
+
+ /**
+ * Reply with an incremented 'request.i'.
+ */
+ void run(OperationContext* opCtx, CommandReplyBuilder* reply) override {
+ commands_test_example::ExampleIncrementReply r;
+ r.setIPlusOne(request().getI() + 1);
+ reply->fillFrom(r);
+ }
+
+ private:
+ bool supportsWriteConcern() const override {
+ return true;
+ }
+
+ AllowedOnSecondary secondaryAllowed(ServiceContext* context) const override {
+ return definition()->secondaryAllowed(context);
+ }
+
+ void explain(OperationContext* opCtx,
+ ExplainOptions::Verbosity verbosity,
+ BSONObjBuilder* result) override {}
+
+ void doCheckAuthorization(OperationContext*) const override {}
+
+ /**
+ * The ns() for when Request's IDL specifies "namespace: concatenate_with_db".
+ */
+ NamespaceString ns() const override {
+ return request().getNamespace();
+ }
+ };
+};
+
+// Just like ExampleIncrementCommand, but with a void typedRun.
+class ExampleVoidCommand final : public TypedCommand<ExampleVoidCommand> {
+private:
+ AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
+ return Command::AllowedOnSecondary::kNever;
+ }
+
+ std::string help() const override {
+ return "Accepts Request and returns void.";
+ }
+
+public:
+ using Request = commands_test_example::ExampleVoid;
+
+ class Invocation final : public InvocationBase {
+ public:
+ using InvocationBase::InvocationBase;
+
+ /**
+ * Have some testable side-effect.
+ */
+ void typedRun(OperationContext*) {
+ static_cast<const ExampleVoidCommand*>(definition())->iCapture = request().getI() + 1;
+ }
+
+ private:
+ bool supportsWriteConcern() const override {
+ return true;
+ }
+
+ AllowedOnSecondary secondaryAllowed(ServiceContext* context) const override {
+ return definition()->secondaryAllowed(context);
+ }
+
+ void explain(OperationContext* opCtx,
+ ExplainOptions::Verbosity verbosity,
+ BSONObjBuilder* result) override {}
+
+ void doCheckAuthorization(OperationContext*) const override {}
+
+ /**
+ * The ns() for when Request's IDL specifies "namespace: concatenate_with_db".
+ */
+ NamespaceString ns() const override {
+ return request().getNamespace();
+ }
+ };
+
+ mutable std::int32_t iCapture = 0;
+};
+
+template <typename Fn>
+class MyCommand final : public TypedCommand<MyCommand<Fn>> {
+public:
+ class Invocation final : public TypedCommand<MyCommand>::InvocationBase {
+ public:
+ using Base = typename TypedCommand<MyCommand>::InvocationBase;
+ using Base::Base;
+
+ auto typedRun(OperationContext*) const {
+ return _command()->_fn();
+ }
+
+ private:
+ NamespaceString ns() const override {
+ return Base::request().getNamespace();
+ }
+ bool supportsWriteConcern() const override {
+ return false;
+ }
+ Command::AllowedOnSecondary secondaryAllowed(ServiceContext* context) const override {
+ return Base::definition()->secondaryAllowed(context);
+ }
+ void doCheckAuthorization(OperationContext* opCtx) const override {}
+
+ const MyCommand* _command() const {
+ return static_cast<const MyCommand*>(Base::definition());
+ }
+ };
+
+ using Request = commands_test_example::ExampleVoid;
+
+ MyCommand(StringData name, Fn fn) : TypedCommand<MyCommand<Fn>>(name), _fn{std::move(fn)} {}
+
+private:
+ Command::AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
+ return Command::AllowedOnSecondary::kAlways;
+ }
+
+ std::string help() const override {
+ return "Accepts Request and returns void.";
+ }
+
+ Fn _fn;
+};
+
+template <typename Fn>
+using CmdT = MyCommand<typename std::decay<Fn>::type>;
+
+auto okFn = [] { return Status::OK(); };
+auto errFn = [] { return Status(ErrorCodes::UnknownError, "some error"); };
+auto throwFn = []() -> Status { uasserted(ErrorCodes::UnknownError, "some error"); };
+
+ExampleIncrementCommand exampleIncrementCommand;
+ExampleMinimalCommand exampleMinimalCommand;
+ExampleVoidCommand exampleVoidCommand;
+CmdT<decltype(okFn)> okStatusCommand("okStatus", okFn);
+CmdT<decltype(errFn)> errStatusCommand("notOkStatus", errFn);
+CmdT<decltype(throwFn)> throwStatusCommand("throwsStatus", throwFn);
+
+struct IncrementTestCommon {
+ template <typename T>
+ void run(T& command, std::function<void(int, const BSONObj&)> postAssert) {
+ const NamespaceString ns("testdb.coll");
+ auto client = getGlobalServiceContext()->makeClient("commands_test");
+ for (std::int32_t i : {123, 12345, 0, -456}) {
+ const OpMsgRequest request = [&] {
+ typename T::Request incr(ns);
+ incr.setI(i);
+ return incr.serialize(BSON("$db" << ns.db()));
+ }();
+ auto opCtx = client->makeOperationContext();
+ auto invocation = command.parse(opCtx.get(), request);
+
+ ASSERT_EQ(invocation->ns(), ns);
+
+ const BSONObj reply = [&] {
+ BufBuilder bb;
+ CommandReplyBuilder crb{BSONObjBuilder{bb}};
+ try {
+ invocation->run(opCtx.get(), &crb);
+ auto bob = crb.getBodyBuilder();
+ CommandHelpers::extractOrAppendOk(bob);
+ } catch (const DBException& e) {
+ auto bob = crb.getBodyBuilder();
+ CommandHelpers::appendCommandStatus(bob, e.toStatus());
+ }
+ return BSONObj(bb.release());
+ }();
+
+ postAssert(i, reply);
+ }
+ }
+};
+
+TEST(TypedCommand, runTyped) {
+ IncrementTestCommon{}.run(exampleIncrementCommand, [](int i, const BSONObj& reply) {
+ ASSERT_EQ(reply["ok"].Double(), 1.0);
+ ASSERT_EQ(reply["iPlusOne"].Int(), i + 1);
+ });
+}
+
+TEST(TypedCommand, runMinimal) {
+ IncrementTestCommon{}.run(exampleMinimalCommand, [](int i, const BSONObj& reply) {
+ ASSERT_EQ(reply["ok"].Double(), 1.0);
+ ASSERT_EQ(reply["iPlusOne"].Int(), i + 1);
+ });
+}
+
+TEST(TypedCommand, runVoid) {
+ IncrementTestCommon{}.run(exampleVoidCommand, [](int i, const BSONObj& reply) {
+ ASSERT_EQ(reply["ok"].Double(), 1.0);
+ ASSERT_EQ(exampleVoidCommand.iCapture, i + 1);
+ });
+}
+
+TEST(TypedCommand, runOkStatus) {
+ IncrementTestCommon{}.run(
+ okStatusCommand, [](int i, const BSONObj& reply) { ASSERT_EQ(reply["ok"].Double(), 1.0); });
+}
+
+TEST(TypedCommand, runErrStatus) {
+ IncrementTestCommon{}.run(errStatusCommand, [](int i, const BSONObj& reply) {
+ Status status = errFn();
+ ASSERT_EQ(reply["ok"].Double(), 0.0);
+ ASSERT_EQ(reply["errmsg"].String(), status.reason());
+ ASSERT_EQ(reply["code"].Int(), status.code());
+ ASSERT_EQ(reply["codeName"].String(), ErrorCodes::errorString(status.code()));
+ });
+}
+
+TEST(TypedCommand, runThrowStatus) {
+ IncrementTestCommon{}.run(throwStatusCommand, [](int i, const BSONObj& reply) {
+ Status status = Status::OK();
+ try {
+ (void)throwFn();
+ } catch (const DBException& e) {
+ status = e.toStatus();
+ }
+ ASSERT_EQ(reply["ok"].Double(), 0.0);
+ ASSERT_EQ(reply["errmsg"].String(), status.reason());
+ ASSERT_EQ(reply["code"].Int(), status.code());
+ ASSERT_EQ(reply["codeName"].String(), ErrorCodes::errorString(status.code()));
+ });
+}
+
} // namespace
} // namespace mongo
diff --git a/src/mongo/db/commands_test_example.idl b/src/mongo/db/commands_test_example.idl
new file mode 100644
index 00000000000..09b9c06d06e
--- /dev/null
+++ b/src/mongo/db/commands_test_example.idl
@@ -0,0 +1,44 @@
+# Copyright (C) 2018 MongoDB Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3,
+# as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Specifies the BSON format for the example TypedCommand used in commands_test.cpp.
+
+global:
+ cpp_namespace: "mongo::commands_test_example"
+
+imports:
+ - "mongo/idl/basic_types.idl"
+
+commands:
+ exampleIncrement:
+ description: "increment an integer (TypedCommand example)"
+ namespace: concatenate_with_db
+ fields:
+ i: int
+ exampleVoid:
+ description: "no return, just side effects"
+ namespace: concatenate_with_db
+ fields:
+ i: int
+ exampleMinimal:
+ description: "like exampleIncrement, but use MinimalInvocationBase"
+ namespace: concatenate_with_db
+ fields:
+ i: int
+
+structs:
+ exampleIncrementReply:
+ description: "reply to exampleIncrement"
+ fields:
+ iPlusOne: int