diff options
author | Billy Donahue <billy.donahue@mongodb.com> | 2018-03-26 17:03:57 -0400 |
---|---|---|
committer | Billy Donahue <billy.donahue@mongodb.com> | 2018-04-13 14:13:27 -0400 |
commit | 64f24ed205a4d83aece37c5f2c64af26624113c1 (patch) | |
tree | dba4eb4288562a5b75ab4201390f500bf665bc38 /src | |
parent | 431ebb76fb1dfd296afd913f4d83b0387c89e992 (diff) | |
download | mongo-64f24ed205a4d83aece37c5f2c64af26624113c1.tar.gz |
SERVER-34148 TypedCommand
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/db/SConscript | 2 | ||||
-rw-r--r-- | src/mongo/db/commands.cpp | 8 | ||||
-rw-r--r-- | src/mongo/db/commands.h | 198 | ||||
-rw-r--r-- | src/mongo/db/commands_test.cpp | 302 | ||||
-rw-r--r-- | src/mongo/db/commands_test_example.idl | 44 |
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 |