diff options
author | Mathias Stearn <mathias@10gen.com> | 2017-12-11 19:35:05 -0500 |
---|---|---|
committer | Mathias Stearn <mathias@10gen.com> | 2018-01-04 14:52:27 -0500 |
commit | 7f56cb7f21a4d13dd4c4d39d85f23ec77cd28f9b (patch) | |
tree | 6ebe17c53fba868d4ce80b3ae1516db3412ee485 /src | |
parent | 6b1a6cfe77e3c5a3b2d62a3346d19d26694ed2ed (diff) | |
download | mongo-7f56cb7f21a4d13dd4c4d39d85f23ec77cd28f9b.tar.gz |
SERVER-31734 Core ErrorExtraInfo implementation
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/base/error_codes.err | 1 | ||||
-rw-r--r-- | src/mongo/base/error_codes.tpl.cpp | 54 | ||||
-rw-r--r-- | src/mongo/base/error_codes.tpl.h | 33 | ||||
-rw-r--r-- | src/mongo/base/error_extra_info.cpp | 53 | ||||
-rw-r--r-- | src/mongo/base/error_extra_info.h | 136 | ||||
-rw-r--r-- | src/mongo/base/generate_error_codes.py | 9 | ||||
-rw-r--r-- | src/mongo/base/status.cpp | 85 | ||||
-rw-r--r-- | src/mongo/base/status.h | 77 | ||||
-rw-r--r-- | src/mongo/base/status_test.cpp | 52 | ||||
-rw-r--r-- | src/mongo/db/commands.cpp | 3 | ||||
-rw-r--r-- | src/mongo/db/commands_test.cpp | 15 | ||||
-rw-r--r-- | src/mongo/db/db.cpp | 2 | ||||
-rw-r--r-- | src/mongo/rpc/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/rpc/get_status_from_command_result.cpp | 2 | ||||
-rw-r--r-- | src/mongo/rpc/get_status_from_command_result_test.cpp | 82 | ||||
-rw-r--r-- | src/mongo/rpc/reply_builder_interface.cpp | 4 | ||||
-rw-r--r-- | src/mongo/rpc/reply_builder_test.cpp | 42 | ||||
-rw-r--r-- | src/mongo/s/server.cpp | 2 | ||||
-rw-r--r-- | src/mongo/util/assert_util.h | 54 | ||||
-rw-r--r-- | src/mongo/util/assert_util_test.cpp | 59 |
21 files changed, 729 insertions, 38 deletions
diff --git a/src/mongo/SConscript b/src/mongo/SConscript index c3f223ce72e..47a7a78a2f2 100644 --- a/src/mongo/SConscript +++ b/src/mongo/SConscript @@ -56,6 +56,7 @@ baseSource=[ 'base/data_type_string_data.cpp', 'base/data_type_terminated.cpp', 'base/error_codes.cpp', + 'base/error_extra_info.cpp', 'base/global_initializer.cpp', 'base/global_initializer_registerer.cpp', 'base/init.cpp', diff --git a/src/mongo/base/error_codes.err b/src/mongo/base/error_codes.err index 0ed414b02c8..b71429bb874 100644 --- a/src/mongo/base/error_codes.err +++ b/src/mongo/base/error_codes.err @@ -233,6 +233,7 @@ error_code("MaxSubPipelineDepthExceeded", 232) error_code("TooManyDocumentSequences", 233) error_code("RetryChangeStream", 234) error_code("InternalErrorNotSupported", 235) # this function or module is not available on this platform or configuration +error_code("ForTestingErrorExtraInfo", 236, extra="ErrorExtraInfoExample") # Error codes 4000-8999 are reserved. diff --git a/src/mongo/base/error_codes.tpl.cpp b/src/mongo/base/error_codes.tpl.cpp index 3c8d777dc45..ba32be4a9f3 100644 --- a/src/mongo/base/error_codes.tpl.cpp +++ b/src/mongo/base/error_codes.tpl.cpp @@ -34,8 +34,20 @@ #include "mongo/util/assert_util.h" #include "mongo/util/mongoutils/str.h" +//#set $codes_with_extra = [ec for ec in $codes if ec.extra] + namespace mongo { +namespace { +// You can thing of this namespace as a compile-time map<ErrorCodes::Error, ErrorExtraInfoParser*>. +namespace parsers { +//#for $ec in $codes_with_extra +ErrorExtraInfo::Parser* $ec.name = nullptr; +//#end for +} // namespace parsers +} // namespace + + MONGO_STATIC_ASSERT(sizeof(ErrorCodes::Error) == sizeof(int)); std::string ErrorCodes::errorString(Error err) { @@ -74,6 +86,48 @@ bool ErrorCodes::is${cat.name}(Error err) { } //#end for +bool ErrorCodes::shouldHaveExtraInfo(Error code) { + switch (code) { + //#for $ec in $codes_with_extra + case ErrorCodes::$ec.name: + return true; + //#end for + default: + return false; + } +} + +ErrorExtraInfo::Parser* ErrorExtraInfo::parserFor(ErrorCodes::Error code) { + switch (code) { + //#for $ec in $codes_with_extra + case ErrorCodes::$ec.name: + invariant(parsers::$ec.name); + return parsers::$ec.name; + //#end for + default: + return nullptr; + } +} + +void ErrorExtraInfo::registerParser(ErrorCodes::Error code, Parser* parser) { + switch (code) { + //#for $ec in $codes_with_extra + case ErrorCodes::$ec.name: + invariant(!parsers::$ec.name); + parsers::$ec.name = parser; + break; + //#end for + default: + MONGO_UNREACHABLE; + } +} + +void ErrorExtraInfo::invariantHaveAllParsers() { + //#for $ec in $codes_with_extra + invariant(parsers::$ec.name); + //#end for +} + void error_details::throwExceptionForStatus(const Status& status) { /** * This type is used for all exceptions that don't have a more specific type. It is defined diff --git a/src/mongo/base/error_codes.tpl.h b/src/mongo/base/error_codes.tpl.h index 893f61b19a8..472c6d72f70 100644 --- a/src/mongo/base/error_codes.tpl.h +++ b/src/mongo/base/error_codes.tpl.h @@ -39,6 +39,13 @@ namespace mongo { class Status; +// ErrorExtraInfo subclasses: +//#for $ec in $codes: +//#if $ec.extra +class $ec.extra; +//#end if +//#end for + enum class ErrorCategory { //#for $cat in $categories ${cat.name}, @@ -95,6 +102,8 @@ public: //#for $cat in $categories static bool is${cat.name}(Error code); //#end for + + static bool shouldHaveExtraInfo(Error code); }; std::ostream& operator<<(std::ostream& stream, ErrorCodes::Error code); @@ -121,6 +130,10 @@ constexpr bool isNamedCode<ErrorCodes::$ec.name> = true; MONGO_COMPILER_NORETURN void throwExceptionForStatus(const Status& status); +// +// ErrorCategoriesFor +// + template <ErrorCategory... categories> struct CategoryList; @@ -146,6 +159,26 @@ struct ErrorCategoriesForImpl<ErrorCodes::$ec.name> { template <ErrorCodes::Error code> using ErrorCategoriesFor = typename ErrorCategoriesForImpl<code>::type; +// +// ErrorExtraInfoFor +// + +template <ErrorCodes::Error code> +struct ErrorExtraInfoForImpl {}; + +//#for $code in $codes +//#if $code.extra +template <> +struct ErrorExtraInfoForImpl<ErrorCodes::$code.name> { + using type = $code.extra; +}; + +//#end if +//#end for + +template <ErrorCodes::Error code> +using ErrorExtraInfoFor = typename ErrorExtraInfoForImpl<code>::type; + } // namespace error_details } // namespace mongo diff --git a/src/mongo/base/error_extra_info.cpp b/src/mongo/base/error_extra_info.cpp new file mode 100644 index 00000000000..41ce5859a40 --- /dev/null +++ b/src/mongo/base/error_extra_info.cpp @@ -0,0 +1,53 @@ +/** + * Copyright 2017 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/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/base/error_extra_info.h" + +#include "mongo/base/init.h" +#include "mongo/db/jsobj.h" + +namespace mongo { + +bool ErrorExtraInfoExample::isParserEnabledForTest = false; + +void ErrorExtraInfoExample::serialize(BSONObjBuilder* builder) const { + builder->append("data", data); +} + +std::shared_ptr<const ErrorExtraInfo> ErrorExtraInfoExample::parse(const BSONObj& obj) { + uassert( + 40681, "ErrorCodes::ForTestingErrorExtraInfo is only for testing", isParserEnabledForTest); + + return std::make_shared<ErrorExtraInfoExample>(obj["data"].Int()); +} + +MONGO_INIT_REGISTER_ERROR_EXTRA_INFO(ErrorExtraInfoExample); + +} // namespace mongo diff --git a/src/mongo/base/error_extra_info.h b/src/mongo/base/error_extra_info.h new file mode 100644 index 00000000000..58b5cb2fbd3 --- /dev/null +++ b/src/mongo/base/error_extra_info.h @@ -0,0 +1,136 @@ +/** + * Copyright 2017 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/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#pragma once + +#include <memory> + +// This file is included by many low-level headers including status.h, so it isn't able to include +// much without creating a cycle. +#include "mongo/base/error_codes.h" +#include "mongo/base/static_assert.h" + +namespace mongo { + +class BSONObj; +class BSONObjBuilder; + +/** + * Base class for the extra info that can be attached to commands. + * + * Actual implementations must have a 'constexpr ErrorCode::Error code' to indicate which + * error code they bind to, and a static method with the following signature: + * static std::shared_ptr<const ErrorExtraInfo> parse(const BSONObj&); + * You must call the MONGO_INIT_REGISTER_ERROR_EXTRA_INFO(type) macro in the cpp file that contains + * the implementation for your subtype. + */ +class ErrorExtraInfo { +public: + using Parser = std::shared_ptr<const ErrorExtraInfo>(const BSONObj&); + + ErrorExtraInfo() = default; + virtual ~ErrorExtraInfo() = default; + + /** + * Puts the extra info (and just the extra info) into builder. + */ + virtual void serialize(BSONObjBuilder* builder) const = 0; + + /** + * Returns the registered parser for a given code or nullptr if none is registered. + */ + static Parser* parserFor(ErrorCodes::Error); + + /** + * Use the MONGO_INIT_REGISTER_ERROR_EXTRA_INFO(type) macro below rather than calling this + * directly. + */ + template <typename T> + static void registerType() { + MONGO_STATIC_ASSERT(std::is_base_of<ErrorExtraInfo, T>()); + MONGO_STATIC_ASSERT(std::is_same<error_details::ErrorExtraInfoFor<T::code>, T>()); + MONGO_STATIC_ASSERT(std::is_final<T>()); + MONGO_STATIC_ASSERT(std::is_move_constructible<T>()); + registerParser(T::code, T::parse); + } + + /** + * Fails fatally if any error codes that should have parsers registered don't. Call this during + * startup of any shipping executable to prevent failures at runtime. + */ + static void invariantHaveAllParsers(); + +private: + static void registerParser(ErrorCodes::Error code, Parser* parser); +}; + +/** + * Registers the parser for an ErrorExtraInfo subclass. This must be called at namespace scope in + * the same cpp file as the virtual methods for that type. + * + * You must separately #include "mongo/base/init.h" since including it here would create an include + * cycle. + */ +#define MONGO_INIT_REGISTER_ERROR_EXTRA_INFO(type) \ + MONGO_INITIALIZER_GENERAL( \ + RegisterErrorExtraInfoFor##type, MONGO_NO_PREREQUISITES, ("default")) \ + (InitializerContext * context) { \ + ErrorExtraInfo::registerType<type>(); \ + return Status::OK(); \ + } + +/** + * This is an example ErrorExtraInfo subclass. It is used for testing the ErrorExtraInfoHandling. + * + * The default parser just throws a numeric code since this class should never be encountered in + * production. A normal parser is activated while an EnableParserForTesting object is in scope. + */ +class ErrorExtraInfoExample final : public ErrorExtraInfo { +public: + static constexpr auto code = ErrorCodes::ForTestingErrorExtraInfo; + + void serialize(BSONObjBuilder*) const override; + static std::shared_ptr<const ErrorExtraInfo> parse(const BSONObj&); + + // Everything else in this class is just for testing and shouldn't by copied by users. + + struct EnableParserForTest { + EnableParserForTest() { + isParserEnabledForTest = true; + } + ~EnableParserForTest() { + isParserEnabledForTest = false; + } + }; + + ErrorExtraInfoExample(int data) : data(data) {} + int data; // This uses the fieldname "data". +private: + static bool isParserEnabledForTest; +}; +} // namespace mongo diff --git a/src/mongo/base/generate_error_codes.py b/src/mongo/base/generate_error_codes.py index 420ee964ffe..577108c7ec0 100644 --- a/src/mongo/base/generate_error_codes.py +++ b/src/mongo/base/generate_error_codes.py @@ -51,15 +51,20 @@ def render_template(template_path, **kw): template = Template.compile( file=template_path, - compilerSettings=dict(directiveStartToken="//#",directiveEndToken="//#"), + compilerSettings=dict( + directiveStartToken="//#", + directiveEndToken="//#", + commentStartToken="//##" + ), baseclass=dict, useCache=False) return str(template(**kw)) class ErrorCode: - def __init__(self, name, code): + def __init__(self, name, code, extra=None): self.name = name self.code = code + self.extra = extra self.categories = [] class ErrorClass: diff --git a/src/mongo/base/status.cpp b/src/mongo/base/status.cpp index 8b615c5b98b..71425db7291 100644 --- a/src/mongo/base/status.cpp +++ b/src/mongo/base/status.cpp @@ -25,7 +25,11 @@ * then also delete it in the license file. */ +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kControl + #include "mongo/base/status.h" +#include "mongo/db/jsobj.h" +#include "mongo/util/log.h" #include "mongo/util/mongoutils/str.h" #include <ostream> @@ -33,28 +37,69 @@ namespace mongo { -Status::ErrorInfo::ErrorInfo(ErrorCodes::Error aCode, std::string aReason) - : code(aCode), reason(std::move(aReason)) {} +Status::ErrorInfo::ErrorInfo(ErrorCodes::Error code, + StringData reason, + std::shared_ptr<const ErrorExtraInfo> extra) + : code(code), reason(reason.toString()), extra(std::move(extra)) {} -Status::ErrorInfo* Status::ErrorInfo::create(ErrorCodes::Error c, std::string r) { - if (c == ErrorCodes::OK) +Status::ErrorInfo* Status::ErrorInfo::create(ErrorCodes::Error code, + StringData reason, + std::shared_ptr<const ErrorExtraInfo> extra) { + if (code == ErrorCodes::OK) return nullptr; - return new ErrorInfo(c, std::move(r)); + if (extra) { + // The public API prevents getting in to this state. + invariant(ErrorCodes::shouldHaveExtraInfo(code)); + } else if (ErrorCodes::shouldHaveExtraInfo(code)) { + // This is possible if code calls a 2-argument Status constructor with a code that should + // have extra info. + if (kDebugBuild) { + // Make it easier to find this issue by fatally failing in debug builds. + severe() << "Code " << code << " is supposed to have extra info"; + fassertFailed(40680); + } + + // In release builds, replace the error code. This maintains the invariant that all Statuses + // for a code that is supposed to hold extra info hold correctly-typed extra info, without + // crashing the server. + return new ErrorInfo{ErrorCodes::Error(40671), + str::stream() << "Missing required extra info for error code " << code, + std::move(extra)}; + } + return new ErrorInfo{code, reason, std::move(extra)}; } -Status::Status(ErrorCodes::Error code, std::string reason) - : _error(ErrorInfo::create(code, std::move(reason))) { + +Status::Status(ErrorCodes::Error code, + StringData reason, + std::shared_ptr<const ErrorExtraInfo> extra) + : _error(ErrorInfo::create(code, reason, std::move(extra))) { ref(_error); } -Status::Status(ErrorCodes::Error code, const char* reason) : Status(code, std::string(reason)) {} -Status::Status(ErrorCodes::Error code, StringData reason) : Status(code, reason.toString()) {} +Status::Status(ErrorCodes::Error code, const std::string& reason) : Status(code, reason, nullptr) {} +Status::Status(ErrorCodes::Error code, const char* reason) + : Status(code, StringData(reason), nullptr) {} +Status::Status(ErrorCodes::Error code, StringData reason) : Status(code, reason, nullptr) {} + +Status::Status(ErrorCodes::Error code, StringData reason, const BSONObj& extraInfoHolder) + : Status(OK()) { + if (auto parser = ErrorExtraInfo::parserFor(code)) { + try { + *this = Status(code, reason, parser(extraInfoHolder)); + } catch (const DBException& ex) { + *this = ex.toStatus(str::stream() << "Error parsing extra info for " << code); + } + } else { + *this = Status(code, reason); + } +} Status::Status(ErrorCodes::Error code, const mongoutils::str::stream& reason) : Status(code, std::string(reason)) {} Status Status::withContext(StringData reasonPrefix) const { - return isOK() ? Status::OK() : Status(code(), reasonPrefix + causedBy(reason())); + return isOK() ? OK() : Status(code(), reasonPrefix + causedBy(reason()), _error->extra); } std::ostream& operator<<(std::ostream& os, const Status& status) { @@ -62,10 +107,26 @@ std::ostream& operator<<(std::ostream& os, const Status& status) { } std::string Status::toString() const { - std::ostringstream ss; + StringBuilder ss; ss << codeString(); - if (!isOK()) + if (!isOK()) { + try { + if (auto extra = extraInfo()) { + BSONObjBuilder bob; + extra->serialize(&bob); + ss << bob.obj(); + } + } catch (const DBException&) { + // This really shouldn't happen but it would be really annoying if it broke error + // logging in production. + if (kDebugBuild) { + severe() << "Error serializing extra info for " << code() + << " in Status::toString()"; + std::terminate(); + } + } ss << ": " << reason(); + } return ss.str(); } diff --git a/src/mongo/base/status.h b/src/mongo/base/status.h index dffb661923a..59cf03009b4 100644 --- a/src/mongo/base/status.h +++ b/src/mongo/base/status.h @@ -31,6 +31,7 @@ #include <string> #include "mongo/base/error_codes.h" +#include "mongo/base/error_extra_info.h" #include "mongo/platform/atomic_word.h" #include "mongo/platform/compiler.h" @@ -45,24 +46,15 @@ namespace mongo { /** * Status represents an error state or the absence thereof. * - * A Status uses the standardized error codes -- from file 'error_codes.h' -- to + * A Status uses the standardized error codes -- from file 'error_codes.err' -- to * determine an error's cause. It further clarifies the error with a textual - * description. - * - * Example usage: - * - * Status sumAB(int a, int b, int* c) { - * if (overflowIfSum(a,b)) { - * return Status(ErrorCodes::ERROR_OVERFLOW, "overflow in sumAB", 16494); - * } - * - * *c = a+b; - * return Status::OK(); - * } + * description, and code-specific extra info (a subclass of ErrorExtraInfo). */ class MONGO_WARN_UNUSED_RESULT_CLASS Status { public: - // Short-hand for returning an OK status. + /** + * This is the best way to construct an OK status. + */ static inline Status OK(); /** @@ -74,12 +66,30 @@ public: * * For adding context to the reason string, use withContext/addContext rather than making a new * Status manually. + * + * If the status comes from a command reply, use getStatusFromCommandResult() instead of manual + * parsing. If the status is round-tripping through non-command BSON, use the constructor that + * takes a BSONObj so that it can extract the extra info if the code is supposed to have any. */ - MONGO_COMPILER_COLD_FUNCTION Status(ErrorCodes::Error code, std::string reason); + MONGO_COMPILER_COLD_FUNCTION Status(ErrorCodes::Error code, const std::string& reason); MONGO_COMPILER_COLD_FUNCTION Status(ErrorCodes::Error code, const char* reason); MONGO_COMPILER_COLD_FUNCTION Status(ErrorCodes::Error code, StringData reason); MONGO_COMPILER_COLD_FUNCTION Status(ErrorCodes::Error code, const mongoutils::str::stream& reason); + MONGO_COMPILER_COLD_FUNCTION Status(ErrorCodes::Error code, + StringData message, + const BSONObj& extraInfoHolder); + + /** + * Constructs a Status with a subclass of ErrorExtraInfo. + */ + template <typename T, typename = stdx::enable_if_t<std::is_base_of<ErrorExtraInfo, T>::value>> + MONGO_COMPILER_COLD_FUNCTION Status(T&& detail, StringData message) + : Status(T::code, + message, + std::make_shared<const std::remove_reference_t<T>>(std::forward<T>(detail))) { + MONGO_STATIC_ASSERT(std::is_same<error_details::ErrorExtraInfoFor<T::code>, T>()); + } inline Status(const Status& other); inline Status& operator=(const Status& other); @@ -143,6 +153,32 @@ public: return empty; } + /** + * Returns the generic ErrorExtraInfo if present. + */ + const ErrorExtraInfo* extraInfo() const { + return isOK() ? nullptr : _error->extra.get(); + } + + /** + * Returns a specific subclass of ErrorExtraInfo if the error code matches that type. + */ + template <typename T> + const T* extraInfo() const { + MONGO_STATIC_ASSERT(std::is_base_of<ErrorExtraInfo, T>()); + MONGO_STATIC_ASSERT(std::is_same<error_details::ErrorExtraInfoFor<T::code>, T>()); + + if (isOK()) + return nullptr; + if (code() != T::code) + return nullptr; + + // Can't use checked_cast due to include cycle. + invariant(_error->extra); + dassert(dynamic_cast<const T*>(_error->extra.get())); + return static_cast<const T*>(_error->extra.get()); + } + std::string toString() const; /** @@ -178,16 +214,23 @@ public: inline AtomicUInt32::WordType refCount() const; private: + // Private since it could result in a type mismatch between code and extraInfo. + MONGO_COMPILER_COLD_FUNCTION Status(ErrorCodes::Error code, + StringData reason, + std::shared_ptr<const ErrorExtraInfo>); inline Status(); struct ErrorInfo { AtomicUInt32 refs; // reference counter const ErrorCodes::Error code; // error code const std::string reason; // description of error cause + const std::shared_ptr<const ErrorExtraInfo> extra; - static ErrorInfo* create(ErrorCodes::Error code, std::string reason); + static ErrorInfo* create(ErrorCodes::Error code, + StringData reason, + std::shared_ptr<const ErrorExtraInfo> extra); - ErrorInfo(ErrorCodes::Error code, std::string reason); + ErrorInfo(ErrorCodes::Error code, StringData reason, std::shared_ptr<const ErrorExtraInfo>); }; ErrorInfo* _error; diff --git a/src/mongo/base/status_test.cpp b/src/mongo/base/status_test.cpp index 149f712cd15..f99b0844a7b 100644 --- a/src/mongo/base/status_test.cpp +++ b/src/mongo/base/status_test.cpp @@ -33,6 +33,9 @@ #include <boost/exception/exception.hpp> #include "mongo/base/status.h" +#include "mongo/config.h" +#include "mongo/db/json.h" +#include "mongo/unittest/death_test.h" #include "mongo/unittest/unittest.h" #include "mongo/util/assert_util.h" @@ -265,5 +268,54 @@ TEST(Transformers, ExceptionToStatus) { ASSERT_TRUE(fromBoostExcept.reason().find("boost::exception") != std::string::npos); } +// TODO enable this once the next ErrorExtraInfo subclass is registered +#if false +DEATH_TEST(ErrorExtraInfo, InvariantAllRegistered, "Invariant failure parser_for::") { + ErrorExtraInfo::invariantHaveAllParsers(); +} +#else +TEST(ErrorExtraInfo, MakeSureInvariantAllRegisteredGetsEnabled) { + // This will be the first "real" use of ErrorExtraInfo, and it won't be linked into this test. + // This just exists to ensure that once that work is done, the above test gets enabled. + invariant( + !ErrorCodes::shouldHaveExtraInfo(ErrorCodes::CommandOnShardedViewNotSupportedOnMongod)); +} +#endif + +#ifdef MONGO_CONFIG_DEBUG_BUILD +DEATH_TEST(ErrorExtraInfo, DassertShouldHaveExtraInfo, "Fatal Assertion 40680") { + Status(ErrorCodes::ForTestingErrorExtraInfo, ""); +} +#else +TEST(ErrorExtraInfo, ConvertCodeOnMissingExtraInfo) { + auto status = Status(ErrorCodes::ForTestingErrorExtraInfo, ""); + ASSERT_EQ(status, ErrorCodes::duplicateCodeForTest(40671)); +} +#endif + +TEST(ErrorExtraInfo, TypedConstructorWorks) { + const auto status = Status(ErrorExtraInfoExample(123), ""); + ASSERT_EQ(status, ErrorCodes::ForTestingErrorExtraInfo); + ASSERT(status.extraInfo()); + ASSERT(status.extraInfo<ErrorExtraInfoExample>()); + ASSERT_EQ(status.extraInfo<ErrorExtraInfoExample>()->data, 123); +} + +TEST(ErrorExtraInfo, StatusWhenParserThrows) { + auto status = Status(ErrorCodes::ForTestingErrorExtraInfo, "", fromjson("{data: 123}")); + ASSERT_EQ(status, ErrorCodes::duplicateCodeForTest(40681)); + ASSERT(!status.extraInfo()); + ASSERT(!status.extraInfo<ErrorExtraInfoExample>()); +} + +TEST(ErrorExtraInfo, StatusParserWorks) { + ErrorExtraInfoExample::EnableParserForTest whenInScope; + auto status = Status(ErrorCodes::ForTestingErrorExtraInfo, "", fromjson("{data: 123}")); + ASSERT_EQ(status, ErrorCodes::ForTestingErrorExtraInfo); + ASSERT(status.extraInfo()); + ASSERT(status.extraInfo<ErrorExtraInfoExample>()); + ASSERT_EQ(status.extraInfo<ErrorExtraInfoExample>()->data, 123); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/commands.cpp b/src/mongo/db/commands.cpp index eee3f6c625a..165d77127fc 100644 --- a/src/mongo/db/commands.cpp +++ b/src/mongo/db/commands.cpp @@ -240,6 +240,9 @@ bool Command::appendCommandStatus(BSONObjBuilder& result, const Status& status) result.append("code", status.code()); result.append("codeName", ErrorCodes::errorString(status.code())); } + if (auto extraInfo = status.extraInfo()) { + extraInfo->serialize(&result); + } return status.isOK(); } diff --git a/src/mongo/db/commands_test.cpp b/src/mongo/db/commands_test.cpp index b6cc5c622f8..85fd263d4c2 100644 --- a/src/mongo/db/commands_test.cpp +++ b/src/mongo/db/commands_test.cpp @@ -79,6 +79,21 @@ TEST(Commands, appendCommandStatusNoOverwrite) { ASSERT_BSONOBJ_EQ(actualResult.obj(), expectedResult.obj()); } +TEST(Commands, appendCommandStatusErrorExtraInfo) { + BSONObjBuilder actualResult; + const Status status(ErrorExtraInfoExample(123), "not again!"); + Command::appendCommandStatus(actualResult, status); + + BSONObjBuilder expectedResult; + expectedResult.append("ok", 0.0); + expectedResult.append("errmsg", status.reason()); + expectedResult.append("code", status.code()); + expectedResult.append("codeName", ErrorCodes::errorString(status.code())); + expectedResult.append("data", 123); + + ASSERT_BSONOBJ_EQ(actualResult.obj(), expectedResult.obj()); +} + class ParseNsOrUUID : public unittest::Test { public: ParseNsOrUUID() diff --git a/src/mongo/db/db.cpp b/src/mongo/db/db.cpp index 5265561720d..f7d37050f54 100644 --- a/src/mongo/db/db.cpp +++ b/src/mongo/db/db.cpp @@ -1364,6 +1364,8 @@ int mongoDbMain(int argc, char* argv[], char** envp) { quickExit(EXIT_FAILURE); } + ErrorExtraInfo::invariantHaveAllParsers(); + startupConfigActions(std::vector<std::string>(argv, argv + argc)); cmdline_utils::censorArgvArray(argc, argv); diff --git a/src/mongo/rpc/SConscript b/src/mongo/rpc/SConscript index 58ded79d586..98ea0e6d64c 100644 --- a/src/mongo/rpc/SConscript +++ b/src/mongo/rpc/SConscript @@ -178,6 +178,7 @@ env.CppUnitTest( 'command_reply_test.cpp', 'command_request_builder_test.cpp', 'command_request_test.cpp', + 'get_status_from_command_result_test.cpp', 'legacy_request_test.cpp', 'object_check_test.cpp', 'protocol_test.cpp', diff --git a/src/mongo/rpc/get_status_from_command_result.cpp b/src/mongo/rpc/get_status_from_command_result.cpp index 3b13923c049..ed0ad38824d 100644 --- a/src/mongo/rpc/get_status_from_command_result.cpp +++ b/src/mongo/rpc/get_status_from_command_result.cpp @@ -77,7 +77,7 @@ Status getStatusFromCommandResult(const BSONObj& result) { code = ErrorCodes::CommandNotFound; } - return Status(ErrorCodes::Error(code), errmsg); + return Status(ErrorCodes::Error(code), errmsg, result); } Status getWriteConcernStatusFromCommandResult(const BSONObj& obj) { diff --git a/src/mongo/rpc/get_status_from_command_result_test.cpp b/src/mongo/rpc/get_status_from_command_result_test.cpp new file mode 100644 index 00000000000..0c5e260de7d --- /dev/null +++ b/src/mongo/rpc/get_status_from_command_result_test.cpp @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2017 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/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/jsobj.h" +#include "mongo/db/json.h" +#include "mongo/rpc/get_status_from_command_result.h" +#include "mongo/unittest/unittest.h" +#include "mongo/unittest/unittest_helpers.h" + +namespace mongo { + +namespace { +Status statusFor(const std::string& json) { + return getStatusFromCommandResult(fromjson(json)); +} +} // namespace + +TEST(GetStatusFromCommandResult, Ok) { + ASSERT_OK(statusFor("{ok: 1.0}")); +} + +TEST(GetStatusFromCommandResult, OkIgnoresCode) { + ASSERT_OK(statusFor("{ok: 1.0, code: 12345, errmsg: 'oh no!'}")); +} + +TEST(GetStatusFromCommandResult, SimpleBad) { + const auto status = statusFor("{ok: 0.0, code: 12345, errmsg: 'oh no!'}"); + ASSERT_EQ(status, ErrorCodes::duplicateCodeForTest(12345)); + ASSERT_EQ(status.reason(), "oh no!"); +} + +TEST(GetStatusFromCommandResult, SimpleNoCode) { + const auto status = statusFor("{ok: 0.0, errmsg: 'oh no!'}"); + ASSERT_EQ(status, ErrorCodes::UnknownError); + ASSERT_EQ(status.reason(), "oh no!"); + ASSERT(!status.extraInfo()); +} + +TEST(GetStatusFromCommandResult, ExtraInfoParserFails) { + const auto status = statusFor("{ok: 0.0, code: 236, errmsg: 'oh no!', data: 123}"); + ASSERT_EQ(status, ErrorCodes::duplicateCodeForTest(40681)); + ASSERT(!status.extraInfo()); +} + +TEST(GetStatusFromCommandResult, ExtraInfoParserSucceeds) { + ErrorExtraInfoExample::EnableParserForTest whenInScope; + const auto status = statusFor("{ok: 0.0, code: 236, errmsg: 'oh no!', data: 123}"); + ASSERT_EQ(status, ErrorCodes::ForTestingErrorExtraInfo); + ASSERT_EQ(status.reason(), "oh no!"); + ASSERT(status.extraInfo()); + ASSERT(status.extraInfo<ErrorExtraInfoExample>()); + ASSERT_EQ(status.extraInfo<ErrorExtraInfoExample>()->data, 123); +} + +} // namespace mongo diff --git a/src/mongo/rpc/reply_builder_interface.cpp b/src/mongo/rpc/reply_builder_interface.cpp index 99fe2edcb1e..9c5b2998e2f 100644 --- a/src/mongo/rpc/reply_builder_interface.cpp +++ b/src/mongo/rpc/reply_builder_interface.cpp @@ -71,6 +71,10 @@ BSONObj augmentReplyWithStatus(const Status& status, BSONObj reply) { bob.append(kCodeNameField, ErrorCodes::errorString(status.code())); } + if (auto extraInfo = status.extraInfo()) { + extraInfo->serialize(&bob); + } + return bob.obj(); } } diff --git a/src/mongo/rpc/reply_builder_test.cpp b/src/mongo/rpc/reply_builder_test.cpp index 83a449f686b..f4f8ac58ef4 100644 --- a/src/mongo/rpc/reply_builder_test.cpp +++ b/src/mongo/rpc/reply_builder_test.cpp @@ -33,6 +33,7 @@ #include "mongo/db/json.h" #include "mongo/rpc/command_reply.h" #include "mongo/rpc/command_reply_builder.h" +#include "mongo/rpc/get_status_from_command_result.h" #include "mongo/rpc/legacy_reply.h" #include "mongo/rpc/legacy_reply_builder.h" #include "mongo/rpc/op_msg_rpc_impls.h" @@ -46,6 +47,9 @@ using namespace mongo; template <typename T> void testRoundTrip(rpc::ReplyBuilderInterface& replyBuilder, bool unifiedBodyAndMetadata); +template <typename T> +void testErrors(rpc::ReplyBuilderInterface& replyBuilder); + TEST(LegacyReplyBuilder, RoundTrip) { rpc::LegacyReplyBuilder r; testRoundTrip<rpc::LegacyReply>(r, true); @@ -61,6 +65,24 @@ TEST(OpMsgReplyBuilder, RoundTrip) { testRoundTrip<rpc::OpMsgReply>(r, true); } +template <typename T> +void testErrors(rpc::ReplyBuilderInterface& replyBuilder); + +TEST(LegacyReplyBuilder, Errors) { + rpc::LegacyReplyBuilder r; + testErrors<rpc::LegacyReply>(r); +} + +TEST(CommandReplyBuilder, Errors) { + rpc::CommandReplyBuilder r; + testErrors<rpc::CommandReply>(r); +} + +TEST(OpMsgReplyBuilder, Errors) { + rpc::OpMsgReplyBuilder r; + testErrors<rpc::OpMsgReply>(r); +} + BSONObj buildMetadata() { BSONObjBuilder metadataTop; { @@ -195,4 +217,24 @@ void testRoundTrip(rpc::ReplyBuilderInterface& replyBuilder, bool unifiedBodyAnd } } +template <typename T> +void testErrors(rpc::ReplyBuilderInterface& replyBuilder) { + ErrorExtraInfoExample::EnableParserForTest whenInScope; + + const auto status = Status(ErrorExtraInfoExample(123), "Why does this keep failing!"); + + replyBuilder.setCommandReply(status); + replyBuilder.setMetadata(buildMetadata()); + + const auto msg = replyBuilder.done(); + + T parsed(&msg); + const Status result = getStatusFromCommandResult(parsed.getCommandReply()); + ASSERT_EQ(result, status.code()); + ASSERT_EQ(result.reason(), status.reason()); + ASSERT(result.extraInfo()); + ASSERT(result.extraInfo<ErrorExtraInfoExample>()); + ASSERT_EQ(result.extraInfo<ErrorExtraInfoExample>()->data, 123); +} + } // namespace diff --git a/src/mongo/s/server.cpp b/src/mongo/s/server.cpp index e0a629c36e2..c1e4af13eb3 100644 --- a/src/mongo/s/server.cpp +++ b/src/mongo/s/server.cpp @@ -589,6 +589,8 @@ int mongoSMain(int argc, char* argv[], char** envp) { quickExit(EXIT_FAILURE); } + ErrorExtraInfo::invariantHaveAllParsers(); + startupConfigActions(std::vector<std::string>(argv, argv + argc)); cmdline_utils::censorArgvArray(argc, argv); diff --git a/src/mongo/util/assert_util.h b/src/mongo/util/assert_util.h index 0e571f3846a..1907d9446da 100644 --- a/src/mongo/util/assert_util.h +++ b/src/mongo/util/assert_util.h @@ -105,6 +105,21 @@ public: return ErrorCodes::isA<category>(code()); } + /** + * Returns the generic ErrorExtraInfo if present. + */ + const ErrorExtraInfo* extraInfo() const { + return _status.extraInfo(); + } + + /** + * Returns a specific subclass of ErrorExtraInfo if the error code matches that type. + */ + template <typename ErrorDetail> + const ErrorDetail* extraInfo() const { + return _status.extraInfo<ErrorDetail>(); + } + static AtomicBool traceExceptions; protected: @@ -162,6 +177,14 @@ public: invariant(status.code() == kCode); } + // This is only a template to enable SFINAE. It will only be instantiated with the default + // value. + template <ErrorCodes::Error code_copy = kCode> + const ErrorExtraInfoFor<code_copy>* operator->() const { + MONGO_STATIC_ASSERT(code_copy == kCode); + return this->template extraInfo<ErrorExtraInfoFor<kCode>>(); + } + private: void defineOnlyInFinalSubclassToPreventSlicing() final {} }; @@ -271,6 +294,25 @@ inline void fassertNoTraceWithLocation(int msgid, } } +namespace error_details { + +// This function exists so that uassert/massert can take plain int literals rather than requiring +// ErrorCodes::Error wrapping. +template <typename StringLike> +Status makeStatus(int code, StringLike&& message) { + return Status(ErrorCodes::Error(code), std::forward<StringLike>(message)); +} + +template <typename ErrorDetail, + typename StringLike, + typename = stdx::enable_if_t< + std::is_base_of<ErrorExtraInfo, std::remove_reference_t<ErrorDetail>>::value>> +Status makeStatus(ErrorDetail&& detail, StringLike&& message) { + return Status(std::forward<ErrorDetail>(detail), std::forward<StringLike>(message)); +} + +} // namespace error_details + /** * Common implementation for assert and assertFailed macros. Not for direct use. * @@ -279,12 +321,12 @@ inline void fassertNoTraceWithLocation(int msgid, * complex error message in the expansion of msg. The call to the lambda is followed by * MONGO_COMPILER_UNREACHABLE as it is impossible to mark a lambda noreturn. */ -#define MONGO_BASE_ASSERT_FAILED(fail_func, code, msg) \ - do { \ - [&]() MONGO_COMPILER_COLD_FUNCTION { \ - fail_func(Status(ErrorCodes::Error(code), msg), __FILE__, __LINE__); \ - }(); \ - MONGO_COMPILER_UNREACHABLE; \ +#define MONGO_BASE_ASSERT_FAILED(fail_func, code, msg) \ + do { \ + [&]() MONGO_COMPILER_COLD_FUNCTION { \ + fail_func(::mongo::error_details::makeStatus(code, msg), __FILE__, __LINE__); \ + }(); \ + MONGO_COMPILER_UNREACHABLE; \ } while (false) #define MONGO_BASE_ASSERT(fail_func, code, msg, cond) \ diff --git a/src/mongo/util/assert_util_test.cpp b/src/mongo/util/assert_util_test.cpp index 841dc37ba00..e14fab9d643 100644 --- a/src/mongo/util/assert_util_test.cpp +++ b/src/mongo/util/assert_util_test.cpp @@ -147,6 +147,65 @@ TEST(AssertUtils, UassertNumericCode) { ASSERT_NOT_CATCHES(19999, ExceptionForCat<ErrorCategory::Interruption>); } +TEST(AssertUtils, UassertStatusOKPreservesExtraInfo) { + const auto status = Status(ErrorExtraInfoExample(123), ""); + + try { + uassertStatusOK(status); + } catch (const DBException& ex) { + ASSERT(ex.extraInfo()); + ASSERT(ex.extraInfo<ErrorExtraInfoExample>()); + ASSERT_EQ(ex.extraInfo<ErrorExtraInfoExample>()->data, 123); + } + + try { + uassertStatusOK(status); + } catch (const ExceptionFor<ErrorCodes::ForTestingErrorExtraInfo>& ex) { + ASSERT(ex.extraInfo()); + ASSERT(ex.extraInfo<ErrorExtraInfoExample>()); + ASSERT_EQ(ex.extraInfo<ErrorExtraInfoExample>()->data, 123); + ASSERT_EQ(ex->data, 123); + } +} + +TEST(AssertUtils, UassertTypedExtraInfoWorks) { + try { + uasserted(ErrorExtraInfoExample(123), ""); + } catch (const DBException& ex) { + ASSERT(ex.extraInfo()); + ASSERT(ex.extraInfo<ErrorExtraInfoExample>()); + ASSERT_EQ(ex.extraInfo<ErrorExtraInfoExample>()->data, 123); + } + + try { + uassert(ErrorExtraInfoExample(123), "", false); + } catch (const ExceptionFor<ErrorCodes::ForTestingErrorExtraInfo>& ex) { + ASSERT(ex.extraInfo()); + ASSERT(ex.extraInfo<ErrorExtraInfoExample>()); + ASSERT_EQ(ex.extraInfo<ErrorExtraInfoExample>()->data, 123); + ASSERT_EQ(ex->data, 123); + } +} + +TEST(AssertUtils, MassertTypedExtraInfoWorks) { + try { + msgasserted(ErrorExtraInfoExample(123), ""); + } catch (const DBException& ex) { + ASSERT(ex.extraInfo()); + ASSERT(ex.extraInfo<ErrorExtraInfoExample>()); + ASSERT_EQ(ex.extraInfo<ErrorExtraInfoExample>()->data, 123); + } + + try { + massert(ErrorExtraInfoExample(123), "", false); + } catch (const ExceptionFor<ErrorCodes::ForTestingErrorExtraInfo>& ex) { + ASSERT(ex.extraInfo()); + ASSERT(ex.extraInfo<ErrorExtraInfoExample>()); + ASSERT_EQ(ex.extraInfo<ErrorExtraInfoExample>()->data, 123); + ASSERT_EQ(ex->data, 123); + } +} + // uassert and its friends DEATH_TEST(UassertionTerminationTest, uassert, "Terminating with uassert") { uassert(40204, "Terminating with uassert", false); |