summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMathias Stearn <mathias@10gen.com>2017-12-11 19:35:05 -0500
committerMathias Stearn <mathias@10gen.com>2018-01-04 14:52:27 -0500
commit7f56cb7f21a4d13dd4c4d39d85f23ec77cd28f9b (patch)
tree6ebe17c53fba868d4ce80b3ae1516db3412ee485 /src
parent6b1a6cfe77e3c5a3b2d62a3346d19d26694ed2ed (diff)
downloadmongo-7f56cb7f21a4d13dd4c4d39d85f23ec77cd28f9b.tar.gz
SERVER-31734 Core ErrorExtraInfo implementation
Diffstat (limited to 'src')
-rw-r--r--src/mongo/SConscript1
-rw-r--r--src/mongo/base/error_codes.err1
-rw-r--r--src/mongo/base/error_codes.tpl.cpp54
-rw-r--r--src/mongo/base/error_codes.tpl.h33
-rw-r--r--src/mongo/base/error_extra_info.cpp53
-rw-r--r--src/mongo/base/error_extra_info.h136
-rw-r--r--src/mongo/base/generate_error_codes.py9
-rw-r--r--src/mongo/base/status.cpp85
-rw-r--r--src/mongo/base/status.h77
-rw-r--r--src/mongo/base/status_test.cpp52
-rw-r--r--src/mongo/db/commands.cpp3
-rw-r--r--src/mongo/db/commands_test.cpp15
-rw-r--r--src/mongo/db/db.cpp2
-rw-r--r--src/mongo/rpc/SConscript1
-rw-r--r--src/mongo/rpc/get_status_from_command_result.cpp2
-rw-r--r--src/mongo/rpc/get_status_from_command_result_test.cpp82
-rw-r--r--src/mongo/rpc/reply_builder_interface.cpp4
-rw-r--r--src/mongo/rpc/reply_builder_test.cpp42
-rw-r--r--src/mongo/s/server.cpp2
-rw-r--r--src/mongo/util/assert_util.h54
-rw-r--r--src/mongo/util/assert_util_test.cpp59
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);