diff options
author | Adam Midvidy <amidvidy@gmail.com> | 2015-06-01 11:19:53 -0400 |
---|---|---|
committer | Adam Midvidy <amidvidy@gmail.com> | 2015-06-03 11:13:31 -0400 |
commit | b9ed79089841c5c05086a490c1323514f342bb42 (patch) | |
tree | edaf31999ac6c6266254e9a41e54674c4d9120a4 /src/mongo/rpc | |
parent | f9685f7f8ed8240e763da406e3e97d94e7a919e6 (diff) | |
download | mongo-b9ed79089841c5c05086a490c1323514f342bb42.tar.gz |
SERVER-18236 hang slaveOk/secondaryOk and readPreference off OpCtx
- secondaryOk is upconverted/downconverted from QueryOption_SlaveOk
- readPreference is upconverted/downconverted from $query wrapped commands or $queryOptions
- both are now accessed via OperationContext instead of being read from a mutable command object
- removed logic for parsing secondaryOk and readPreference from the command execution pipeline in dbcommands.cpp
Diffstat (limited to 'src/mongo/rpc')
-rw-r--r-- | src/mongo/rpc/SConscript | 19 | ||||
-rw-r--r-- | src/mongo/rpc/legacy_request.cpp | 4 | ||||
-rw-r--r-- | src/mongo/rpc/legacy_request_builder.cpp | 4 | ||||
-rw-r--r-- | src/mongo/rpc/metadata.cpp | 55 | ||||
-rw-r--r-- | src/mongo/rpc/metadata.h | 19 | ||||
-rw-r--r-- | src/mongo/rpc/metadata/server_selection_metadata.cpp | 291 | ||||
-rw-r--r-- | src/mongo/rpc/metadata/server_selection_metadata.h | 119 | ||||
-rw-r--r-- | src/mongo/rpc/metadata/server_selection_metadata_test.cpp | 175 |
8 files changed, 660 insertions, 26 deletions
diff --git a/src/mongo/rpc/SConscript b/src/mongo/rpc/SConscript index 20279380881..e59e959f6ea 100644 --- a/src/mongo/rpc/SConscript +++ b/src/mongo/rpc/SConscript @@ -113,7 +113,24 @@ env.Library( 'metadata', ], source=[ - 'metadata.cpp' + 'metadata.cpp', + 'metadata/server_selection_metadata.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/client/read_preference', + '$BUILD_DIR/mongo/util/decorable', + ], +) + +env.CppUnitTest( + target=[ + 'metadata_test', + ], + source=[ + 'metadata/server_selection_metadata_test.cpp', + ], + LIBDEPS=[ + 'metadata', ], ) diff --git a/src/mongo/rpc/legacy_request.cpp b/src/mongo/rpc/legacy_request.cpp index bb79928abb9..459bc5f3d83 100644 --- a/src/mongo/rpc/legacy_request.cpp +++ b/src/mongo/rpc/legacy_request.cpp @@ -45,8 +45,8 @@ namespace rpc { , _database(NamespaceString(_queryMessage.ns).db().toString()) { std::tie(_upconvertedCommandArgs, _upconvertedMetadata) = uassertStatusOK( - metadata::upconvertRequest(std::move(_queryMessage.query), - std::move(_queryMessage.queryOptions)) + rpc::upconvertRequestMetadata(std::move(_queryMessage.query), + std::move(_queryMessage.queryOptions)) ); } diff --git a/src/mongo/rpc/legacy_request_builder.cpp b/src/mongo/rpc/legacy_request_builder.cpp index f1978583df8..6997bf739ea 100644 --- a/src/mongo/rpc/legacy_request_builder.cpp +++ b/src/mongo/rpc/legacy_request_builder.cpp @@ -80,8 +80,8 @@ namespace rpc { int queryOptions; std::tie(legacyCommandArgs, queryOptions) = uassertStatusOK( - metadata::downconvertRequest(std::move(commandArgs), - std::move(_metadata)) + rpc::downconvertRequestMetadata(std::move(commandArgs), + std::move(_metadata)) ); _builder.appendNum(queryOptions); // queryOptions diff --git a/src/mongo/rpc/metadata.cpp b/src/mongo/rpc/metadata.cpp index 055dfd07984..595ca571f16 100644 --- a/src/mongo/rpc/metadata.cpp +++ b/src/mongo/rpc/metadata.cpp @@ -32,38 +32,65 @@ #include "mongo/client/dbclientinterface.h" #include "mongo/db/jsobj.h" +#include "mongo/rpc/metadata/server_selection_metadata.h" namespace mongo { namespace rpc { -namespace metadata { - BSONObj empty() { + BSONObj makeEmptyMetadata() { return BSONObj(); } - const char kSecondaryOk[] = "$secondaryOk"; + Status readRequestMetadata(OperationContext* txn, const BSONObj& metadataObj) { + auto swServerSelectionMetadata = ServerSelectionMetadata::readFromMetadata(metadataObj); + if (!swServerSelectionMetadata.isOK()) { + return swServerSelectionMetadata.getStatus(); + } + ServerSelectionMetadata::get(txn) = std::move(swServerSelectionMetadata.getValue()); + + return Status::OK(); + } + + Status writeRequestMetadata(OperationContext* txn, BSONObjBuilder* metadataBob) { + auto ssStatus = ServerSelectionMetadata::writeToMetadata( + ServerSelectionMetadata::get(txn), + metadataBob + ); + if (!ssStatus.isOK()) { + return ssStatus; + } + return Status::OK(); + } - StatusWith<CommandAndMetadata> upconvertRequest(BSONObj legacyCmdObj, int queryFlags) { + StatusWith<CommandAndMetadata> upconvertRequestMetadata(BSONObj legacyCmdObj, int queryFlags) { + BSONObjBuilder commandBob; BSONObjBuilder metadataBob; - // note second check may be erroneous: see SERVER-18194 - if ((queryFlags & QueryOption_SlaveOk)) { - metadataBob.append(kSecondaryOk, 1); + auto upconvertStatus = ServerSelectionMetadata::upconvert(legacyCmdObj, + queryFlags, + &commandBob, + &metadataBob); + if (!upconvertStatus.isOK()) { + return upconvertStatus; } - return std::make_tuple(std::move(legacyCmdObj), std::move(metadataBob.obj())); + return std::make_tuple(commandBob.obj(), metadataBob.obj()); } - StatusWith<LegacyCommandAndFlags> downconvertRequest(BSONObj cmdObj, BSONObj metadata) { - int flags = 0; + StatusWith<LegacyCommandAndFlags> downconvertRequestMetadata(BSONObj cmdObj, BSONObj metadata) { + int legacyQueryFlags = 0; + BSONObjBuilder legacyCommandBob; - if (metadata.hasField(kSecondaryOk)) { - flags |= QueryOption_SlaveOk; + auto downconvertStatus = ServerSelectionMetadata::downconvert(cmdObj, + metadata, + &legacyCommandBob, + &legacyQueryFlags); + if (!downconvertStatus.isOK()) { + return downconvertStatus; } - return std::make_tuple(std::move(cmdObj), flags); + return std::make_tuple(legacyCommandBob.obj(), std::move(legacyQueryFlags)); } -} // namespace metadata } // namespace rpc } // namespace mongo diff --git a/src/mongo/rpc/metadata.h b/src/mongo/rpc/metadata.h index 183dedffc5d..ca281066cb9 100644 --- a/src/mongo/rpc/metadata.h +++ b/src/mongo/rpc/metadata.h @@ -34,6 +34,8 @@ namespace mongo { class BSONObj; + class BSONObjBuilder; + class OperationContext; /** * Utilities for converting metadata between the legacy OP_QUERY format and the new @@ -53,17 +55,21 @@ namespace mongo { * TODO: currently only $secondaryOk (request only) is handled. SERVER-18236 will cover the rest. */ namespace rpc { -namespace metadata { /** * Returns an empty metadata object. */ - BSONObj empty(); + BSONObj makeEmptyMetadata(); /** - * The field name for the secondaryOk metadata field. + * Reads metadata from a metadata object and sets it on this OperationContext. */ - extern const char kSecondaryOk[]; + Status readRequestMetadata(OperationContext* txn, const BSONObj& metadataObj); + + /** + * Writes metadata from an OperationContext to a metadata object. + */ + Status writeRequestMetadata(OperationContext* txn, BSONObjBuilder* metadataBob); /** * A command object and a corresponding metadata object. @@ -80,14 +86,13 @@ namespace metadata { * Given a legacy command object and a query flags bitfield, attempts to parse and remove * the metadata from the command object and construct a corresponding metadata object. */ - StatusWith<CommandAndMetadata> upconvertRequest(BSONObj legacyCmdObj, int queryFlags); + StatusWith<CommandAndMetadata> upconvertRequestMetadata(BSONObj legacyCmdObj, int queryFlags); /** * Given a command object and a metadata object, attempts to construct a legacy command * object and query flags bitfield augmented with the given metadata. */ - StatusWith<LegacyCommandAndFlags> downconvertRequest(BSONObj cmdObj, BSONObj metadata); + StatusWith<LegacyCommandAndFlags> downconvertRequestMetadata(BSONObj cmdObj, BSONObj metadata); -} // namespace metadata } // namespace rpc } // namespace mongo diff --git a/src/mongo/rpc/metadata/server_selection_metadata.cpp b/src/mongo/rpc/metadata/server_selection_metadata.cpp new file mode 100644 index 00000000000..290fd83fb8d --- /dev/null +++ b/src/mongo/rpc/metadata/server_selection_metadata.cpp @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2015 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/rpc/metadata/server_selection_metadata.h" + +#include <utility> +#include <tuple> + +#include "mongo/base/status_with.h" +#include "mongo/bson/util/bson_extract.h" +#include "mongo/client/dbclientinterface.h" +#include "mongo/db/jsobj.h" +#include "mongo/db/operation_context.h" +#include "mongo/util/assert_util.h" + +namespace mongo { +namespace rpc { + +namespace { + + const char kSecondaryOkFieldName[] = "$secondaryOk"; + const char kReadPreferenceFieldName[] = "$readPreference"; + + const char kQueryOptionsFieldName[] = "$queryOptions"; + + const char kDollarQueryWrapper[] = "$query"; + const char kQueryWrapper[] = "query"; + + /** + * Utility to unwrap a '$query' or 'query' wrapped command object. The first element of the + * return value indicates whether the command was unwrapped, and the second element is either + * the unwrapped command (if it was wrapped), or the original command if it was not. + */ + std::tuple<bool, BSONObj> unwrapCommand(const BSONObj& maybeWrapped) { + const auto firstElFieldName = maybeWrapped.firstElementFieldName(); + if ((firstElFieldName == StringData(kDollarQueryWrapper)) || + (firstElFieldName == StringData(kQueryWrapper))) { + // TODO: do we need getOwned here? + return std::make_tuple(true, maybeWrapped.firstElement().embeddedObject()); + } + return std::make_tuple(false, maybeWrapped); + } + + /** + * Reads a top-level $readPreference field from a wrapped command. + */ + Status extractWrappedReadPreference(const BSONObj& wrappedCommand, + BSONObjBuilder* metadataBob) { + BSONElement readPrefEl; + auto rpExtractStatus = bsonExtractTypedField(wrappedCommand, + kReadPreferenceFieldName, + mongo::Object, + &readPrefEl); + if (rpExtractStatus.isOK()) { + metadataBob->append(readPrefEl); + } + else if (rpExtractStatus != ErrorCodes::NoSuchKey) { + return rpExtractStatus; + } + + return Status::OK(); + } + + /** + * Reads a $readPreference from a $queryOptions subobject, if it exists, and writes it to + * metadataBob. Writes out the original command excluding the $queryOptions subobject. + */ + Status extractUnwrappedReadPreference(const BSONObj& unwrappedCommand, + BSONObjBuilder* commandBob, + BSONObjBuilder* metadataBob) { + BSONElement queryOptionsEl; + BSONElement readPrefEl; + + auto queryOptionsExtractStatus = bsonExtractTypedField(unwrappedCommand, + kQueryOptionsFieldName, + mongo::Object, + &queryOptionsEl); + + // If there is no queryOptions subobject, we write out the command and return. + if (queryOptionsExtractStatus == ErrorCodes::NoSuchKey) { + commandBob->appendElements(unwrappedCommand); + return Status::OK(); + } + else if (!queryOptionsExtractStatus.isOK()) { + return queryOptionsExtractStatus; + } + + // Write out the command excluding the $queryOptions field. + for (const auto& elem : unwrappedCommand) { + if (elem.fieldNameStringData() != kQueryOptionsFieldName) { + commandBob->append(elem); + } + } + + auto rpExtractStatus = bsonExtractTypedField(queryOptionsEl.embeddedObject(), + kReadPreferenceFieldName, + mongo::Object, + &readPrefEl); + + // If there is a $queryOptions field, we expect there to be a $readPreference. + if (!rpExtractStatus.isOK()) { + return rpExtractStatus; + } + + metadataBob->append(readPrefEl); + return Status::OK(); + } + +} // namespace + + const OperationContext::Decoration<ServerSelectionMetadata> ServerSelectionMetadata::get = + OperationContext::declareDecoration<ServerSelectionMetadata>(); + + ServerSelectionMetadata::ServerSelectionMetadata( + bool secondaryOk, + boost::optional<ReadPreferenceSetting> readPreference + ) + : _secondaryOk(secondaryOk) + , _readPreference(std::move(readPreference)) + {} + + StatusWith<ServerSelectionMetadata> + ServerSelectionMetadata::readFromMetadata(const BSONObj& metadata) { + auto secondaryOkField = metadata.getField(kSecondaryOkFieldName); + auto readPrefField = metadata.getField(kReadPreferenceFieldName); + + bool secondaryOk = !secondaryOkField.eoo(); + + boost::optional<ReadPreferenceSetting> readPreference; + BSONElement rpElem; + auto readPrefExtractStatus = bsonExtractTypedField(metadata, + kReadPreferenceFieldName, + mongo::Object, + &rpElem); + + if (readPrefExtractStatus == ErrorCodes::NoSuchKey) { + // Do nothing, it's valid to have no ReadPreference + } + else if (!readPrefExtractStatus.isOK()) { + return readPrefExtractStatus; + } + else { + // We have a read preference in the metadata object. + auto parsedRps = ReadPreferenceSetting::fromBSON(rpElem.Obj()); + if (!parsedRps.isOK()) { + return parsedRps.getStatus(); + } + readPreference.emplace(std::move(parsedRps.getValue())); + } + + return ServerSelectionMetadata(secondaryOk, std::move(readPreference)); + } + + Status ServerSelectionMetadata::writeToMetadata(const ServerSelectionMetadata& ss, + BSONObjBuilder* metadataBob) { + if (ss.isSecondaryOk()) { + metadataBob->append(kSecondaryOkFieldName, 1); + } + + if (ss.getReadPreference()) { + metadataBob->append(kReadPreferenceFieldName, ss.getReadPreference()->toBSON()); + } + + return Status::OK(); + } + + Status ServerSelectionMetadata::downconvert(const BSONObj& command, + const BSONObj& metadata, + BSONObjBuilder* legacyCommand, + int* legacyQueryFlags) { + + BSONElement secondaryOkElem = metadata.getField(kSecondaryOkFieldName); + BSONElement readPrefElem = metadata.getField(kReadPreferenceFieldName); + + if (!secondaryOkElem.eoo()) { + *legacyQueryFlags |= mongo::QueryOption_SlaveOk; + } + else { + *legacyQueryFlags &= ~mongo::QueryOption_SlaveOk; + } + + if (!readPrefElem.eoo()) { + // Use 'query' to wrap query, then append read preference. + + // NOTE(amidvidy): Oddly, the _isSecondaryQuery implementation in dbclient_rs does + // not unwrap the query properly - it only checks for 'query', and not + // '$query'. We should probably standardize on one - drivers use '$query', + // and the shell uses 'query'. See SERVER-18705 for details. + + // TODO: this may need to use the $queryOptions hack on mongos. + legacyCommand->append(kQueryWrapper, command); + legacyCommand->append(readPrefElem); + } + else { + legacyCommand->appendElements(command); + } + + return Status::OK(); + } + + Status ServerSelectionMetadata::upconvert(const BSONObj& legacyCommand, + const int legacyQueryFlags, + BSONObjBuilder* commandBob, + BSONObjBuilder* metadataBob) { + + // The secondaryOK option is equivalent to the slaveOk bit being set on legacy commands. + if (legacyQueryFlags & QueryOption_SlaveOk) { + metadataBob->append(kSecondaryOkFieldName, 1); + } + + // First we need to check if we have a wrapped command. That is, a command of the form + // {'$query': { 'commandName': 1, ...}, '$someOption': 5, ....}. Curiously, the field name + // of the wrapped query can be either '$query', or 'query'. + BSONObj maybeUnwrapped; + bool wasWrapped; + std::tie(wasWrapped, maybeUnwrapped) = unwrapCommand(legacyCommand); + + if (wasWrapped) { + // Check if legacyCommand has an invalid $maxTimeMS option. + // TODO: Move this check elsewhere when we handle upconverting/downconverting maxTimeMS. + if (legacyCommand.hasField("$maxTimeMS")) { + return Status(ErrorCodes::InvalidOptions, + "cannot use $maxTimeMS query option with " + "commands; use maxTimeMS command option " + "instead"); + } + + // If the command was wrapped, we can write out the upconverted command now, as there + // is nothing else we need to remove from it. + commandBob->appendElements(maybeUnwrapped); + + return extractWrappedReadPreference(legacyCommand, metadataBob); + } + + // If the command was not wrapped, we need to check for a readPreference sent by mongos + // on the $queryOptions field of the command. If it is set, we remove it from the + // upconverted command, so we need to pass the command builder along. + return extractUnwrappedReadPreference(maybeUnwrapped, commandBob, metadataBob); + } + + bool ServerSelectionMetadata::isSecondaryOk() const { + return _secondaryOk; + } + + const boost::optional<ReadPreferenceSetting>& + ServerSelectionMetadata::getReadPreference() const { + return _readPreference; + } + +#if defined(_MSC_VER) && _MSC_VER < 1900 + ServerSelectionMetadata::ServerSelectionMetadata(ServerSelectionMetadata&& ssm) + : _secondaryOk(ssm._secondaryOk) + , _readPreference(std::move(ssm._readPreference)) + {} + + ServerSelectionMetadata& ServerSelectionMetadata::operator=(ServerSelectionMetadata&& ssm) { + _secondaryOk = ssm._secondaryOk; + _readPreference = std::move(ssm._readPreference); + return *this; + } +#endif + +} // rpc +} // mongo diff --git a/src/mongo/rpc/metadata/server_selection_metadata.h b/src/mongo/rpc/metadata/server_selection_metadata.h new file mode 100644 index 00000000000..93ba352f317 --- /dev/null +++ b/src/mongo/rpc/metadata/server_selection_metadata.h @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2015 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 <boost/optional.hpp> + +#include "mongo/base/disallow_copying.h" +#include "mongo/client/read_preference.h" +#include "mongo/db/operation_context.h" + +namespace mongo { + class BSONObj; + class BSONObjBuilder; + class Status; + template <typename T> class StatusWith; + +namespace rpc { + + /** + * This class comprises the request metadata fields that concern server selection, that is, + * the conditions on which servers can execute this operation. + */ + class ServerSelectionMetadata { + MONGO_DISALLOW_COPYING(ServerSelectionMetadata); + public: + static const OperationContext::Decoration<ServerSelectionMetadata> get; + + // TODO: Remove when StatusWith supports default-constructible types (SERVER-18007). + ServerSelectionMetadata() = default; + +#if defined(_MSC_VER) && _MSC_VER < 1900 + ServerSelectionMetadata(ServerSelectionMetadata&&); + + ServerSelectionMetadata& operator=(ServerSelectionMetadata&&); +#else + ServerSelectionMetadata(ServerSelectionMetadata&&) = default; + + ServerSelectionMetadata& operator=(ServerSelectionMetadata&&) = default; +#endif + + /** + * Loads ServerSelectionMetadata from a metadata object and stores it on this + * OperationContext. + */ + static StatusWith<ServerSelectionMetadata> readFromMetadata(const BSONObj& metadataObj); + + /** + * Writes this operation's ServerSelectionMetadata to a metadata object. + */ + static Status writeToMetadata(const ServerSelectionMetadata& ss, + BSONObjBuilder* metadataBob); + + /** + * Rewrites the ServerSelectionMetadata from the legacy OP_QUERY format to the metadata + * object format. In particular, if secondaryOk is set, this will set QueryOption_SlaveOk + * on the legacyQueryFlags. If a readPreference is set, the legacy command will be wrapped + * in a 'query' element and a top-level $readPreference field will be set on the command. + */ + static Status downconvert(const BSONObj& command, + const BSONObj& metadata, + BSONObjBuilder* legacyCommand, + int* legacyQueryFlags); + + /** + * Rewrites the ServerSelectionMetadata from the legacy OP_QUERY format to the metadata + * object format. + */ + static Status upconvert(const BSONObj& legacyCommand, + const int legacyQueryFlags, + BSONObjBuilder* commandBob, + BSONObjBuilder* metadataBob); + /** + * Returns true if this operation has been explicitly overridden to run on a secondary. + * This replaces previous usage of QueryOption_SlaveOk. + */ + bool isSecondaryOk() const; + + /** + * Returns the ReadPreference associated with this operation. See + * mongo/client/read_preference.h for further details. + */ + const boost::optional<ReadPreferenceSetting>& getReadPreference() const; + + private: + ServerSelectionMetadata(bool secondaryOk, + boost::optional<ReadPreferenceSetting> readPreference); + + bool _secondaryOk{false}; + boost::optional<ReadPreferenceSetting> _readPreference{}; + }; + +} // namespace rpc +} // namespace mongo diff --git a/src/mongo/rpc/metadata/server_selection_metadata_test.cpp b/src/mongo/rpc/metadata/server_selection_metadata_test.cpp new file mode 100644 index 00000000000..a87b246359a --- /dev/null +++ b/src/mongo/rpc/metadata/server_selection_metadata_test.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2015 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 <utility> + +#include "mongo/base/status.h" +#include "mongo/client/dbclientinterface.h" +#include "mongo/client/read_preference.h" +#include "mongo/db/jsobj.h" +#include "mongo/rpc/metadata/server_selection_metadata.h" +#include "mongo/unittest/unittest.h" + +namespace { + using namespace mongo; + using namespace mongo::rpc; + using mongo::unittest::assertGet; + + ServerSelectionMetadata checkParse(const BSONObj& metadata) { + return assertGet(ServerSelectionMetadata::readFromMetadata(metadata)); + } + + TEST(ServerSelectionMetadata, ReadFromMetadata) { + { + // Empty object - should work just fine. + auto ss = checkParse(BSONObj()); + ASSERT_FALSE(ss.isSecondaryOk()); + ASSERT_FALSE(ss.getReadPreference().is_initialized()); + } + { + // Set secondaryOk but not readPreference. + auto ss = checkParse(BSON("$secondaryOk" << 1)); + ASSERT_TRUE(ss.isSecondaryOk()); + ASSERT_FALSE(ss.getReadPreference().is_initialized()); + } + { + // Set readPreference but not secondaryOk. + auto ss = checkParse(BSON("$readPreference" << + BSON("mode" << "primary"))); + ASSERT_FALSE(ss.isSecondaryOk()); + ASSERT_TRUE(ss.getReadPreference().is_initialized()); + ASSERT_TRUE(ss.getReadPreference()->pref == ReadPreference::PrimaryOnly); + } + { + // Set both. + auto ss = checkParse(BSON("$secondaryOk" << 1 << + "$readPreference" << + BSON("mode" << "secondaryPreferred"))); + ASSERT_TRUE(ss.isSecondaryOk()); + ASSERT_TRUE(ss.getReadPreference()->pref == ReadPreference::SecondaryPreferred); + } + } + + void checkUpconvert(const BSONObj& legacyCommand, + const int legacyQueryFlags, + const BSONObj& upconvertedCommand, + const BSONObj& upconvertedMetadata) { + BSONObjBuilder upconvertedCommandBob; + BSONObjBuilder upconvertedMetadataBob; + auto convertStatus = ServerSelectionMetadata::upconvert(legacyCommand, + legacyQueryFlags, + &upconvertedCommandBob, + &upconvertedMetadataBob); + ASSERT_OK(convertStatus); + // We don't care about the order of the fields in the metadata object + const auto sorted = [](const BSONObj& obj) { + BSONObjIteratorSorted iter(obj); + BSONObjBuilder bob; + while (iter.more()) { + bob.append(iter.next()); + } + return bob.obj(); + }; + + ASSERT_EQ(upconvertedCommand, upconvertedCommandBob.done()); + ASSERT_EQ(sorted(upconvertedMetadata), sorted(upconvertedMetadataBob.done())); + } + + TEST(ServerSelectionMetadata, UpconvertValidMetadata) { + // Wrapped in $query, with readPref and slaveOk bit set. + checkUpconvert(BSON("$query" << BSON("ping" << 1) << + "$readPreference" << BSON("mode" << "secondary")), + mongo::QueryOption_SlaveOk, + BSON("ping" << 1), + BSON("$secondaryOk" << 1 << + "$readPreference" << BSON("mode" << "secondary"))); + + // Wrapped in 'query', with readPref. + checkUpconvert(BSON("query" << BSON("pong" << 1 << "foo" << "bar") << + "$readPreference" << BSON("mode" << "primary" << + "tags" << BSON("dc" << "ny"))), + 0, + BSON("pong" << 1 << "foo" << "bar"), + BSON("$readPreference" << BSON("mode" << "primary" << + "tags" << BSON("dc" << "ny")))); + // Unwrapped, no readPref, no slaveOk + checkUpconvert(BSON("ping" << 1), + 0, + BSON("ping" << 1), + BSONObj()); + + // Readpref wrapped in $queryOptions + checkUpconvert(BSON("pang" << "pong" << + "$queryOptions" << + BSON("$readPreference" << BSON("mode" << "nearest" << + "tags" << BSON("rack" << "city")))), + 0, + BSON("pang" << "pong"), + BSON("$readPreference" << BSON("mode" << "nearest" << + "tags" << BSON("rack" << "city")))); + } + + void checkUpconvertFails(const BSONObj& legacyCommand, ErrorCodes::Error error) { + BSONObjBuilder upconvertedCommandBob; + BSONObjBuilder upconvertedMetadataBob; + auto upconvertStatus = ServerSelectionMetadata::upconvert(legacyCommand, + 0, + &upconvertedCommandBob, + &upconvertedMetadataBob); + ASSERT_NOT_OK(upconvertStatus); + ASSERT_EQ(upconvertStatus.code(), error); + } + + TEST(ServerSelectionMetadata, UpconvertInvalidMetadata) { + // $readPreference not an object. + checkUpconvertFails(BSON("$query" << BSON("pang" << "pong") << + "$readPreference" << 2), + ErrorCodes::TypeMismatch); + + // has $maxTimeMS option + checkUpconvertFails(BSON("query" << BSON("foo" << "bar") << + "$maxTimeMS" << 200), + ErrorCodes::InvalidOptions); + checkUpconvertFails(BSON("$query" << BSON("foo" << "bar") << + "$maxTimeMS" << 200), + ErrorCodes::InvalidOptions); + + // has $queryOptions field, but invalid $readPreference + checkUpconvertFails(BSON("ping" << "pong" << + "$queryOptions" << BSON("$readPreference" << 1.2)), + ErrorCodes::TypeMismatch); + + // has $queryOptions field, but no $readPreference + checkUpconvertFails(BSON("ping" << "pong" << + "$queryOptions" << BSONObj()), + ErrorCodes::NoSuchKey); + } + +} |