diff options
Diffstat (limited to 'src/mongo/db/repl')
21 files changed, 1858 insertions, 511 deletions
diff --git a/src/mongo/db/repl/SConscript b/src/mongo/db/repl/SConscript index bcbc313c18c..a9531a25cac 100644 --- a/src/mongo/db/repl/SConscript +++ b/src/mongo/db/repl/SConscript @@ -848,6 +848,29 @@ env.CppUnitTest( ], ) +env.Library( + target='split_horizon', + source=[ + 'split_horizon.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/db/concurrency/lock_manager', + '$BUILD_DIR/mongo/util/net/network', + ], +) + +env.CppUnitTest( + target='split_horizon_test', + source=[ + 'split_horizon_test.cpp', + ], + LIBDEPS=[ + 'split_horizon', + ], +) + + env.Library('topology_coordinator', [ 'heartbeat_response_action.cpp', @@ -867,6 +890,7 @@ env.Library('topology_coordinator', LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/db/catalog/commit_quorum_options', '$BUILD_DIR/mongo/idl/server_parameter', + 'split_horizon', ]) env.CppUnitTest('repl_set_heartbeat_response_test', @@ -1110,6 +1134,7 @@ env.Library('replica_set_messages', ], LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/idl/server_parameter', + 'split_horizon', ]) env.CppUnitTest('repl_set_config_test', @@ -1678,6 +1703,7 @@ env.Library( '$BUILD_DIR/mongo/db/commands/server_status', '$BUILD_DIR/mongo/db/stats/counters', '$BUILD_DIR/mongo/transport/message_compressor', + 'split_horizon', ], ) diff --git a/src/mongo/db/repl/member_config.cpp b/src/mongo/db/repl/member_config.cpp index 29907879a2e..6d5265a4b7f 100644 --- a/src/mongo/db/repl/member_config.cpp +++ b/src/mongo/db/repl/member_config.cpp @@ -50,6 +50,7 @@ const std::string MemberConfig::kSlaveDelayFieldName = "slaveDelay"; const std::string MemberConfig::kArbiterOnlyFieldName = "arbiterOnly"; const std::string MemberConfig::kBuildIndexesFieldName = "buildIndexes"; const std::string MemberConfig::kTagsFieldName = "tags"; +const std::string MemberConfig::kHorizonsFieldName = "horizons"; const std::string MemberConfig::kInternalVoterTagName = "$voter"; const std::string MemberConfig::kInternalElectableTagName = "$electable"; const std::string MemberConfig::kInternalAllTagName = "$all"; @@ -63,7 +64,8 @@ const std::string kLegalMemberConfigFieldNames[] = {MemberConfig::kIdFieldName, MemberConfig::kSlaveDelayFieldName, MemberConfig::kArbiterOnlyFieldName, MemberConfig::kBuildIndexesFieldName, - MemberConfig::kTagsFieldName}; + MemberConfig::kTagsFieldName, + MemberConfig::kHorizonsFieldName}; const int kVotesFieldDefault = 1; const double kPriorityFieldDefault = 1.0; @@ -76,40 +78,34 @@ const Seconds kMaxSlaveDelay(3600 * 24 * 366); } // namespace -Status MemberConfig::initialize(const BSONObj& mcfg, ReplSetTagConfig* tagConfig) { - Status status = bsonCheckOnlyHasFields( - "replica set member configuration", mcfg, kLegalMemberConfigFieldNames); - if (!status.isOK()) - return status; +MemberConfig::MemberConfig(const BSONObj& mcfg, ReplSetTagConfig* tagConfig) { + uassertStatusOK(bsonCheckOnlyHasFields( + "replica set member configuration", mcfg, kLegalMemberConfigFieldNames)); // // Parse _id field. // BSONElement idElement = mcfg[kIdFieldName]; - if (idElement.eoo()) { - return Status(ErrorCodes::NoSuchKey, str::stream() << kIdFieldName << " field is missing"); - } - if (!idElement.isNumber()) { - return Status(ErrorCodes::TypeMismatch, - str::stream() << kIdFieldName << " field has non-numeric type " - << typeName(idElement.type())); - } + if (idElement.eoo()) + uasserted(ErrorCodes::NoSuchKey, str::stream() << kIdFieldName << " field is missing"); + + if (!idElement.isNumber()) + uasserted(ErrorCodes::TypeMismatch, + str::stream() << kIdFieldName << " field has non-numeric type " + << typeName(idElement.type())); _id = idElement.numberInt(); // // Parse h field. // std::string hostAndPortString; - status = bsonExtractStringField(mcfg, kHostFieldName, &hostAndPortString); - if (!status.isOK()) - return status; + uassertStatusOK(bsonExtractStringField(mcfg, kHostFieldName, &hostAndPortString)); boost::trim(hostAndPortString); - status = _host.initialize(hostAndPortString); - if (!status.isOK()) - return status; - if (!_host.hasPort()) { + HostAndPort host; + uassertStatusOK(host.initialize(hostAndPortString)); + if (!host.hasPort()) { // make port explicit even if default. - _host = HostAndPort(_host.host(), _host.port()); + host = HostAndPort(host.host(), host.port()); } // @@ -121,18 +117,16 @@ Status MemberConfig::initialize(const BSONObj& mcfg, ReplSetTagConfig* tagConfig } else if (votesElement.isNumber()) { _votes = votesElement.numberInt(); } else { - return Status(ErrorCodes::TypeMismatch, - str::stream() << kVotesFieldName << " field value has non-numeric type " - << typeName(votesElement.type())); + uasserted(ErrorCodes::TypeMismatch, + str::stream() << kVotesFieldName << " field value has non-numeric type " + << typeName(votesElement.type())); } // // Parse arbiterOnly field. // - status = bsonExtractBooleanFieldWithDefault( - mcfg, kArbiterOnlyFieldName, kArbiterOnlyFieldDefault, &_arbiterOnly); - if (!status.isOK()) - return status; + uassertStatusOK(bsonExtractBooleanFieldWithDefault( + mcfg, kArbiterOnlyFieldName, kArbiterOnlyFieldDefault, &_arbiterOnly)); // // Parse priority field. @@ -144,9 +138,9 @@ Status MemberConfig::initialize(const BSONObj& mcfg, ReplSetTagConfig* tagConfig } else if (priorityElement.isNumber()) { _priority = priorityElement.numberDouble(); } else { - return Status(ErrorCodes::TypeMismatch, - str::stream() << kPriorityFieldName << " field has non-numeric type " - << typeName(priorityElement.type())); + uasserted(ErrorCodes::TypeMismatch, + str::stream() << kPriorityFieldName << " field has non-numeric type " + << typeName(priorityElement.type())); } // @@ -158,47 +152,52 @@ Status MemberConfig::initialize(const BSONObj& mcfg, ReplSetTagConfig* tagConfig } else if (slaveDelayElement.isNumber()) { _slaveDelay = Seconds(slaveDelayElement.numberInt()); } else { - return Status(ErrorCodes::TypeMismatch, - str::stream() << kSlaveDelayFieldName << " field value has non-numeric type " - << typeName(slaveDelayElement.type())); + uasserted(ErrorCodes::TypeMismatch, + str::stream() << kSlaveDelayFieldName << " field value has non-numeric type " + << typeName(slaveDelayElement.type())); } // // Parse hidden field. // - status = - bsonExtractBooleanFieldWithDefault(mcfg, kHiddenFieldName, kHiddenFieldDefault, &_hidden); - if (!status.isOK()) - return status; + uassertStatusOK( + bsonExtractBooleanFieldWithDefault(mcfg, kHiddenFieldName, kHiddenFieldDefault, &_hidden)); // // Parse buildIndexes field. // - status = bsonExtractBooleanFieldWithDefault( - mcfg, kBuildIndexesFieldName, kBuildIndexesFieldDefault, &_buildIndexes); - if (!status.isOK()) - return status; + uassertStatusOK(bsonExtractBooleanFieldWithDefault( + mcfg, kBuildIndexesFieldName, kBuildIndexesFieldDefault, &_buildIndexes)); // // Parse "tags" field. // - _tags.clear(); - BSONElement tagsElement; - status = bsonExtractTypedField(mcfg, kTagsFieldName, Object, &tagsElement); - if (status.isOK()) { + try { + BSONElement tagsElement; + uassertStatusOK(bsonExtractTypedField(mcfg, kTagsFieldName, Object, &tagsElement)); for (auto&& tag : tagsElement.Obj()) { if (tag.type() != String) { - return Status(ErrorCodes::TypeMismatch, - str::stream() << "tags." << tag.fieldName() - << " field has non-string value of type " - << typeName(tag.type())); + uasserted(ErrorCodes::TypeMismatch, + str::stream() << "tags." << tag.fieldName() + << " field has non-string value of type " + << typeName(tag.type())); } _tags.push_back(tagConfig->makeTag(tag.fieldNameStringData(), tag.valueStringData())); } - } else if (ErrorCodes::NoSuchKey != status) { - return status; + } catch (const ExceptionFor<ErrorCodes::NoSuchKey>&) { + // No such key is okay in this case, everything else is a problem. } + const auto horizonsElement = [&]() -> boost::optional<BSONElement> { + const BSONElement result = mcfg[kHorizonsFieldName]; + if (result.eoo()) { + return boost::none; + } + return result; + }(); + + _splitHorizon = SplitHorizon(host, horizonsElement); + // // Add internal tags based on other member properties. // @@ -218,8 +217,6 @@ Status MemberConfig::initialize(const BSONObj& mcfg, ReplSetTagConfig* tagConfig if (!_arbiterOnly) { _tags.push_back(tagConfig->makeTag(kInternalAllTagName, id)); } - - return Status::OK(); } Status MemberConfig::validate() const { @@ -286,7 +283,7 @@ bool MemberConfig::hasTags(const ReplSetTagConfig& tagConfig) const { BSONObj MemberConfig::toBSON(const ReplSetTagConfig& tagConfig) const { BSONObjBuilder configBuilder; configBuilder.append("_id", _id); - configBuilder.append("host", _host.toString()); + configBuilder.append("host", _host().toString()); configBuilder.append("arbiterOnly", _arbiterOnly); configBuilder.append("buildIndexes", _buildIndexes); configBuilder.append("hidden", _hidden); @@ -303,6 +300,8 @@ BSONObj MemberConfig::toBSON(const ReplSetTagConfig& tagConfig) const { } tags.done(); + _splitHorizon.toBSON(configBuilder); + configBuilder.append("slaveDelay", durationCount<Seconds>(_slaveDelay)); configBuilder.append("votes", getNumVotes()); return configBuilder.obj(); diff --git a/src/mongo/db/repl/member_config.h b/src/mongo/db/repl/member_config.h index cd019ea326c..f4cbd7d9609 100644 --- a/src/mongo/db/repl/member_config.h +++ b/src/mongo/db/repl/member_config.h @@ -34,7 +34,9 @@ #include "mongo/base/status.h" #include "mongo/db/repl/repl_set_tag.h" +#include "mongo/db/repl/split_horizon.h" #include "mongo/util/net/hostandport.h" +#include "mongo/util/string_map.h" #include "mongo/util/time_support.h" namespace mongo { @@ -59,26 +61,20 @@ public: static const std::string kArbiterOnlyFieldName; static const std::string kBuildIndexesFieldName; static const std::string kTagsFieldName; + static const std::string kHorizonsFieldName; static const std::string kInternalVoterTagName; static const std::string kInternalElectableTagName; static const std::string kInternalAllTagName; /** - * Default constructor, produces a MemberConfig in an undefined state. - * Must successfully call initialze() before calling validate() or the - * accessors. - */ - MemberConfig() : _slaveDelay(0) {} - - /** - * Initializes this MemberConfig from the contents of "mcfg". + * Construct a MemberConfig from the contents of "mcfg". * * If "mcfg" describes any tags, builds ReplSetTags for this * configuration using "tagConfig" as the tag's namespace. This may * have the effect of altering "tagConfig" when "mcfg" describes a * tag not previously added to "tagConfig". */ - Status initialize(const BSONObj& mcfg, ReplSetTagConfig* tagConfig); + MemberConfig(const BSONObj& mcfg, ReplSetTagConfig* tagConfig); /** * Performs basic consistency checks on the member configuration. @@ -96,8 +92,31 @@ public: * Gets the canonical name of this member, by which other members and clients * will contact it. */ - const HostAndPort& getHostAndPort() const { - return _host; + const HostAndPort& getHostAndPort(StringData horizon = SplitHorizon::kDefaultHorizon) const { + return _splitHorizon.getHostAndPort(horizon); + } + + /** + * Gets the mapping of horizon names to `HostAndPort` for this replica set member. + */ + const auto& getHorizonMappings() const { + return _splitHorizon.getForwardMappings(); + } + + /** + * Gets the mapping of host names (not `HostAndPort`) to horizon names for this replica set + * member. + */ + const auto& getHorizonReverseHostMappings() const { + return _splitHorizon.getReverseHostMappings(); + } + + /** + * Gets the horizon name for which the parameters (captured during the first `isMaster`) + * correspond. + */ + StringData determineHorizon(const SplitHorizon::Parameters& params) const { + return _splitHorizon.determineHorizon(params); } /** @@ -191,8 +210,11 @@ public: BSONObj toBSON(const ReplSetTagConfig& tagConfig) const; private: + const HostAndPort& _host() const { + return getHostAndPort(SplitHorizon::kDefaultHorizon); + } + int _id; - HostAndPort _host; double _priority; // 0 means can never be primary int _votes; // Can this member vote? Only 0 and 1 are valid. Default 1. bool _arbiterOnly; @@ -200,6 +222,8 @@ private: bool _hidden; // if set, don't advertise to drivers in isMaster. bool _buildIndexes; // if false, do not create any non-_id indexes std::vector<ReplSetTag> _tags; // tagging for data center, rack, etc. + + SplitHorizon _splitHorizon; }; } // namespace repl diff --git a/src/mongo/db/repl/member_config_test.cpp b/src/mongo/db/repl/member_config_test.cpp index ac8f0215058..c7a498288bf 100644 --- a/src/mongo/db/repl/member_config_test.cpp +++ b/src/mongo/db/repl/member_config_test.cpp @@ -41,10 +41,9 @@ namespace { TEST(MemberConfig, ParseMinimalMemberConfigAndCheckDefaults) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "localhost:12345"), - &tagConfig)); + MemberConfig mc(BSON("_id" << 0 << "host" + << "localhost:12345"), + &tagConfig); ASSERT_EQUALS(0, mc.getId()); ASSERT_EQUALS(HostAndPort("localhost", 12345), mc.getHostAndPort()); ASSERT_EQUALS(1.0, mc.getPriority()); @@ -59,227 +58,242 @@ TEST(MemberConfig, ParseMinimalMemberConfigAndCheckDefaults) { TEST(MemberConfig, ParseFailsWithIllegalFieldName) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_EQUALS(ErrorCodes::BadValue, - mc.initialize(BSON("_id" << 0 << "host" - << "localhost" - << "frim" - << 1), - &tagConfig)); + ASSERT_THROWS(MemberConfig(BSON("_id" << 0 << "host" + << "localhost" + << "frim" + << 1), + &tagConfig), + ExceptionFor<ErrorCodes::BadValue>); } TEST(MemberConfig, ParseFailsWithMissingIdField) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_EQUALS(ErrorCodes::NoSuchKey, - mc.initialize(BSON("host" - << "localhost:12345"), - &tagConfig)); + ASSERT_THROWS(MemberConfig(BSON("host" + << "localhost:12345"), + &tagConfig), + ExceptionFor<ErrorCodes::NoSuchKey>); } TEST(MemberConfig, ParseFailsWithBadIdField) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_EQUALS(ErrorCodes::NoSuchKey, - mc.initialize(BSON("host" - << "localhost:12345"), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::TypeMismatch, - mc.initialize(BSON("_id" - << "0" - << "host" - << "localhost:12345"), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::TypeMismatch, - mc.initialize(BSON("_id" << Date_t() << "host" - << "localhost:12345"), - &tagConfig)); + ASSERT_THROWS(MemberConfig(BSON("host" + << "localhost:12345"), + &tagConfig), + ExceptionFor<ErrorCodes::NoSuchKey>); + ASSERT_THROWS(MemberConfig(BSON("_id" + << "0" + << "host" + << "localhost:12345"), + &tagConfig), + ExceptionFor<ErrorCodes::TypeMismatch>); + ASSERT_THROWS(MemberConfig(BSON("_id" << Date_t() << "host" + << "localhost:12345"), + &tagConfig), + ExceptionFor<ErrorCodes::TypeMismatch>); } TEST(MemberConfig, ParseFailsWithMissingHostField) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_EQUALS(ErrorCodes::NoSuchKey, mc.initialize(BSON("_id" << 0), &tagConfig)); + ASSERT_THROWS(MemberConfig(BSON("_id" << 0), &tagConfig), ExceptionFor<ErrorCodes::NoSuchKey>); } TEST(MemberConfig, ParseFailsWithBadHostField) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_EQUALS(ErrorCodes::TypeMismatch, - mc.initialize(BSON("_id" << 0 << "host" << 0), &tagConfig)); - ASSERT_EQUALS(ErrorCodes::FailedToParse, - mc.initialize(BSON("_id" << 0 << "host" - << ""), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::FailedToParse, - mc.initialize(BSON("_id" << 0 << "host" - << "myhost:zabc"), - &tagConfig)); + ASSERT_THROWS(MemberConfig(BSON("_id" << 0 << "host" << 0), &tagConfig), + ExceptionFor<ErrorCodes::TypeMismatch>); + ASSERT_THROWS(MemberConfig(BSON("_id" << 0 << "host" + << ""), + &tagConfig), + ExceptionFor<ErrorCodes::FailedToParse>); + ASSERT_THROWS(MemberConfig(BSON("_id" << 0 << "host" + << "myhost:zabc"), + &tagConfig), + ExceptionFor<ErrorCodes::FailedToParse>); } TEST(MemberConfig, ParseArbiterOnly) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "arbiterOnly" - << 1.0), - &tagConfig)); - ASSERT_TRUE(mc.isArbiter()); - ASSERT_EQUALS(0.0, mc.getPriority()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "arbiterOnly" - << false), - &tagConfig)); - ASSERT_TRUE(!mc.isArbiter()); - ASSERT_EQUALS(1.0, mc.getPriority()); + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "arbiterOnly" + << 1.0), + &tagConfig); + ASSERT_TRUE(mc.isArbiter()); + ASSERT_EQUALS(0.0, mc.getPriority()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "arbiterOnly" + << false), + &tagConfig); + ASSERT_TRUE(!mc.isArbiter()); + ASSERT_EQUALS(1.0, mc.getPriority()); + } } TEST(MemberConfig, ParseHidden) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "hidden" - << 1.0), - &tagConfig)); - ASSERT_TRUE(mc.isHidden()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "hidden" - << false), - &tagConfig)); - ASSERT_TRUE(!mc.isHidden()); - ASSERT_EQUALS(ErrorCodes::TypeMismatch, - mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "hidden" - << "1.0"), - &tagConfig)); + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "hidden" + << 1.0), + &tagConfig); + ASSERT_TRUE(mc.isHidden()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "hidden" + << false), + &tagConfig); + ASSERT_TRUE(!mc.isHidden()); + } + ASSERT_THROWS(MemberConfig(BSON("_id" << 0 << "host" + << "h" + << "hidden" + << "1.0"), + &tagConfig), + ExceptionFor<ErrorCodes::TypeMismatch>); } TEST(MemberConfig, ParseBuildIndexes) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "buildIndexes" - << 1.0), - &tagConfig)); - ASSERT_TRUE(mc.shouldBuildIndexes()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "buildIndexes" - << false), - &tagConfig)); - ASSERT_TRUE(!mc.shouldBuildIndexes()); + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "buildIndexes" + << 1.0), + &tagConfig); + ASSERT_TRUE(mc.shouldBuildIndexes()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "buildIndexes" + << false), + &tagConfig); + ASSERT_TRUE(!mc.shouldBuildIndexes()); + } } TEST(MemberConfig, ParseVotes) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 1.0), - &tagConfig)); - ASSERT_TRUE(mc.isVoter()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 0 - << "priority" - << 0), - &tagConfig)); - ASSERT_FALSE(mc.isVoter()); - - // For backwards compatibility, truncate 1.X to 1, and 0.X to 0 (and -0.X to 0). - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 1.5), - &tagConfig)); - ASSERT_TRUE(mc.isVoter()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 0.5), - &tagConfig)); - ASSERT_FALSE(mc.isVoter()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << -0.5), - &tagConfig)); - ASSERT_FALSE(mc.isVoter()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 2), - &tagConfig)); - - ASSERT_EQUALS(ErrorCodes::TypeMismatch, - mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << Date_t::fromMillisSinceEpoch(2)), - &tagConfig)); + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 1.0), + &tagConfig); + ASSERT_TRUE(mc.isVoter()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 0 + << "priority" + << 0), + &tagConfig); + ASSERT_FALSE(mc.isVoter()); + } + { + // For backwards compatibility, truncate 1.X to 1, and 0.X to 0 (and -0.X to 0). + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 1.5), + &tagConfig); + ASSERT_TRUE(mc.isVoter()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 0.5), + &tagConfig); + ASSERT_FALSE(mc.isVoter()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << -0.5), + &tagConfig); + ASSERT_FALSE(mc.isVoter()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 2), + &tagConfig); + } + ASSERT_THROWS(MemberConfig(BSON("_id" << 0 << "host" + << "h" + << "votes" + << Date_t::fromMillisSinceEpoch(2)), + &tagConfig), + ExceptionFor<ErrorCodes::TypeMismatch>); } TEST(MemberConfig, ParsePriority) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 1), - &tagConfig)); - ASSERT_EQUALS(1.0, mc.getPriority()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 0), - &tagConfig)); - ASSERT_EQUALS(0.0, mc.getPriority()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 100.8), - &tagConfig)); - ASSERT_EQUALS(100.8, mc.getPriority()); - - ASSERT_EQUALS(ErrorCodes::TypeMismatch, - mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << Date_t::fromMillisSinceEpoch(2)), - &tagConfig)); + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 1), + &tagConfig); + ASSERT_EQUALS(1.0, mc.getPriority()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 0), + &tagConfig); + ASSERT_EQUALS(0.0, mc.getPriority()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 100.8), + &tagConfig); + ASSERT_EQUALS(100.8, mc.getPriority()); + } + ASSERT_THROWS(MemberConfig(BSON("_id" << 0 << "host" + << "h" + << "priority" + << Date_t::fromMillisSinceEpoch(2)), + &tagConfig), + ExceptionFor<ErrorCodes::TypeMismatch>); } TEST(MemberConfig, ParseSlaveDelay) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "slaveDelay" - << 100), - &tagConfig)); + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "slaveDelay" + << 100), + &tagConfig); ASSERT_EQUALS(Seconds(100), mc.getSlaveDelay()); } TEST(MemberConfig, ParseTags) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "tags" - << BSON("k1" - << "v1" - << "k2" - << "v2")), - &tagConfig)); + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "tags" + << BSON("k1" + << "v1" + << "k2" + << "v2")), + &tagConfig); ASSERT_EQUALS(5U, mc.getNumTags()); ASSERT_EQUALS(5, std::distance(mc.tagsBegin(), mc.tagsEnd())); ASSERT_EQUALS(1, std::count(mc.tagsBegin(), mc.tagsEnd(), tagConfig.findTag("k1", "v1"))); @@ -290,247 +304,444 @@ TEST(MemberConfig, ParseTags) { ASSERT_EQUALS(1, std::count(mc.tagsBegin(), mc.tagsEnd(), tagConfig.findTag("$all", "0"))); } -TEST(MemberConfig, ValidateFailsWithIdOutOfRange) { +TEST(MemberConfig, ParseHorizonFields) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << -1 << "host" - << "localhost:12345"), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); - ASSERT_OK(mc.initialize(BSON("_id" << 256 << "host" - << "localhost:12345"), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "horizons" + << BSON("alpha" + << "a.host:43" + << "beta" + << "b.host:256")), + &tagConfig); + + ASSERT_EQUALS(std::size_t{1}, mc.getHorizonMappings().count("alpha")); + ASSERT_EQUALS(std::size_t{1}, mc.getHorizonMappings().count("beta")); + ASSERT_EQUALS(std::size_t{1}, mc.getHorizonMappings().count("__default")); + + ASSERT_EQUALS("alpha", mc.getHorizonReverseHostMappings().find("a.host")->second); + ASSERT_EQUALS("beta", mc.getHorizonReverseHostMappings().find("b.host")->second); + ASSERT_EQUALS("__default", mc.getHorizonReverseHostMappings().find("h")->second); + + ASSERT_EQUALS(HostAndPort("a.host", 43), mc.getHorizonMappings().find("alpha")->second); + ASSERT_EQUALS(HostAndPort("b.host", 256), mc.getHorizonMappings().find("beta")->second); + ASSERT_EQUALS(HostAndPort("h"), mc.getHorizonMappings().find("__default")->second); + + ASSERT_EQUALS(mc.getHorizonMappings().size(), std::size_t{3}); } -TEST(MemberConfig, ValidateVotes) { +TEST(MemberConfig, DuplicateHorizonNames) { ReplSetTagConfig tagConfig; - MemberConfig mc; + try { + MemberConfig(BSON("_id" << 0 << "host" + << "h" + << "horizons" + << BSON("goofyRepeatedHorizonName" + << "a.host:43" + << "goofyRepeatedHorizonName" + + << "b.host:256")), + &tagConfig); + ASSERT_TRUE(false); // Should not succeed. + } catch (const ExceptionFor<ErrorCodes::BadValue>& ex) { + const Status& s = ex.toStatus(); + ASSERT_NOT_EQUALS(s.reason().find("goofyRepeatedHorizonName"), std::string::npos); + ASSERT_NOT_EQUALS(s.reason().find("Duplicate horizon name found"), std::string::npos); + } + try { + MemberConfig(BSON("_id" << 0 << "host" + << "h" + << "horizons" + << BSON("someUniqueHorizonName" + << "a.host:43" + << SplitHorizon::kDefaultHorizon + << "b.host:256")), + &tagConfig); + ASSERT_TRUE(false); // Should not succeed. + } catch (const ExceptionFor<ErrorCodes::BadValue>& ex) { + const Status& s = ex.toStatus(); + ASSERT_NOT_EQUALS(s.reason().find(std::string(SplitHorizon::kDefaultHorizon)), + std::string::npos); + ASSERT_NOT_EQUALS(s.reason().find("reserved for internal"), std::string::npos); + } +} - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 1.0), - &tagConfig)); - ASSERT_OK(mc.validate()); - ASSERT_TRUE(mc.isVoter()); +TEST(MemberConfig, DuplicateHorizonHostAndPort) { + ReplSetTagConfig tagConfig; + // Repeated `HostAndPort` within the horizon definition. + try { + MemberConfig(BSON("_id" << 0 << "host" + << "uniquehostname.example.com:42" + << "horizons" + << BSON("alpha" + << "duplicatedhostname.example.com:42" + << "beta" + << "duplicatedhostname.example.com:42")), + &tagConfig); + ASSERT_TRUE(false); // Should not succeed. + } catch (const ExceptionFor<ErrorCodes::BadValue>& ex) { + const Status& s = ex.toStatus(); + ASSERT_EQUALS(s.reason().find("uniquehostname.example.com"), std::string::npos); + ASSERT_NOT_EQUALS(s.reason().find("duplicatedhostname.example.com"), std::string::npos) + << "Failed to find duplicated host name in message: " << s.reason(); + } + + // Repeated `HostAndPort` across the host and members. + try { + MemberConfig(BSON("_id" << 0 << "host" + << "duplicatedhostname.example.com:42" + << "horizons" + << BSON("alpha" + << "uniquehostname.example.com:42" + << "beta" + << "duplicatedhostname.example.com:42")), + &tagConfig); + ASSERT_TRUE(false); // Should not succeed. + } catch (const ExceptionFor<ErrorCodes::BadValue>& ex) { + const Status& s = ex.toStatus(); + ASSERT_EQUALS(s.reason().find("uniquehostname.example.com"), std::string::npos); + ASSERT_NOT_EQUALS(s.reason().find("duplicatedhostname.example.com"), std::string::npos); + } + + // Repeated hostname across host and horizons, with different ports should fail. + try { + MemberConfig(BSON("_id" << 0 << "host" + << "duplicatedhostname.example.com:42" + << "horizons" + << BSON("alpha" + << "uniquehostname.example.com:43" + << "beta" + << "duplicatedhostname.example.com:43")), + &tagConfig); + ASSERT_TRUE(false); // Should not succeed. + } catch (const ExceptionFor<ErrorCodes::BadValue>& ex) { + const Status& s = ex.toStatus(); + ASSERT_EQUALS(s.reason().find("uniquehostname.example.com"), std::string::npos); + ASSERT_NOT_EQUALS(s.reason().find("duplicatedhostname.example.com"), std::string::npos); + } + + // Repeated hostname within the horizons, with different ports should fail. + try { + MemberConfig(BSON("_id" << 0 << "host" + << "uniquehostname.example.com:42" + << "horizons" + << BSON("alpha" + << "duplicatedhostname.example.com:42" + << "beta" + << "duplicatedhostname.example.com:43")), + &tagConfig); + ASSERT_TRUE(false); // Should not succeed. + } catch (const ExceptionFor<ErrorCodes::BadValue>& ex) { + const Status& s = ex.toStatus(); + ASSERT_EQUALS(s.reason().find("uniquehostname.example.com"), std::string::npos); + ASSERT_NOT_EQUALS(s.reason().find("duplicatedhostname.example.com"), std::string::npos); + } +} - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 0 - << "priority" - << 0), - &tagConfig)); - ASSERT_OK(mc.validate()); - ASSERT_FALSE(mc.isVoter()); - - // For backwards compatibility, truncate 1.X to 1, and 0.X to 0 (and -0.X to 0). - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 1.5), - &tagConfig)); - ASSERT_OK(mc.validate()); - ASSERT_TRUE(mc.isVoter()); +TEST(MemberConfig, HorizonFieldsWithNoneInSpec) { + ReplSetTagConfig tagConfig; + MemberConfig mc(BSON("_id" << 0 << "host" + << "h"), + &tagConfig); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 0.5 - << "priority" - << 0), - &tagConfig)); - ASSERT_OK(mc.validate()); - ASSERT_FALSE(mc.isVoter()); - - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << -0.5 - << "priority" - << 0), - &tagConfig)); - ASSERT_OK(mc.validate()); - ASSERT_FALSE(mc.isVoter()); - - // Invalid values - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 2), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + ASSERT_EQUALS(std::size_t{1}, mc.getHorizonMappings().count("__default")); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << -1), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + ASSERT_EQUALS(HostAndPort("h"), mc.getHorizonMappings().find("__default")->second); + + ASSERT_EQUALS(mc.getHorizonMappings().size(), std::size_t{1}); +} + +TEST(MemberConfig, HorizonFieldWithEmptyStringIsRejected) { + ReplSetTagConfig tagConfig; + try { + MemberConfig(BSON("_id" << 0 << "host" + << "h" + << "horizons" + << BSON("" + << "example.com:42")), + &tagConfig); + ASSERT_TRUE(false); // Never should get here + } catch (const ExceptionFor<ErrorCodes::BadValue>& ex) { + ASSERT_NOT_EQUALS(ex.toStatus().reason().find("Horizons cannot have empty names"), + std::string::npos); + } +} + +TEST(MemberConfig, ValidateFailsWithIdOutOfRange) { + ReplSetTagConfig tagConfig; + { + MemberConfig mc(BSON("_id" << -1 << "host" + << "localhost:12345"), + &tagConfig); + ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 256 << "host" + << "localhost:12345"), + &tagConfig); + ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + } +} + +TEST(MemberConfig, ValidateVotes) { + ReplSetTagConfig tagConfig; + + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 1.0), + &tagConfig); + ASSERT_OK(mc.validate()); + ASSERT_TRUE(mc.isVoter()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 0 + << "priority" + << 0), + &tagConfig); + ASSERT_OK(mc.validate()); + ASSERT_FALSE(mc.isVoter()); + } + { + // For backwards compatibility, truncate 1.X to 1, and 0.X to 0 (and -0.X to 0). + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 1.5), + &tagConfig); + ASSERT_OK(mc.validate()); + ASSERT_TRUE(mc.isVoter()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 0.5 + << "priority" + << 0), + &tagConfig); + ASSERT_OK(mc.validate()); + ASSERT_FALSE(mc.isVoter()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << -0.5 + << "priority" + << 0), + &tagConfig); + ASSERT_OK(mc.validate()); + ASSERT_FALSE(mc.isVoter()); + } + { + // Invalid values + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 2), + &tagConfig); + ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << -1), + &tagConfig); + ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + } } TEST(MemberConfig, ValidatePriorityRanges) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 0), - &tagConfig)); - ASSERT_OK(mc.validate()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 1000), - &tagConfig)); - ASSERT_OK(mc.validate()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << -1), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 1001), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 0), + &tagConfig); + ASSERT_OK(mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 1000), + &tagConfig); + ASSERT_OK(mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << -1), + &tagConfig); + ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 1001), + &tagConfig); + ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + } } TEST(MemberConfig, ValidateSlaveDelays) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 0 - << "slaveDelay" - << 0), - &tagConfig)); - ASSERT_OK(mc.validate()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 0 - << "slaveDelay" - << 3600 * 10), - &tagConfig)); - ASSERT_OK(mc.validate()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 0 - << "slaveDelay" - << -1), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 0 - << "slaveDelay" - << 3600 * 24 * 400), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 0 + << "slaveDelay" + << 0), + &tagConfig); + ASSERT_OK(mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 0 + << "slaveDelay" + << 3600 * 10), + &tagConfig); + ASSERT_OK(mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 0 + << "slaveDelay" + << -1), + &tagConfig); + ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 0 + << "slaveDelay" + << 3600 * 24 * 400), + &tagConfig); + ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + } } TEST(MemberConfig, ValidatePriorityAndSlaveDelayRelationship) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 1 - << "slaveDelay" - << 60), - &tagConfig)); + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 1 + << "slaveDelay" + << 60), + &tagConfig); ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); } TEST(MemberConfig, ValidatePriorityAndHiddenRelationship) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 1 - << "hidden" - << true), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 1 - << "hidden" - << false), - &tagConfig)); - ASSERT_OK(mc.validate()); + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 1 + << "hidden" + << true), + &tagConfig); + ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 1 + << "hidden" + << false), + &tagConfig); + ASSERT_OK(mc.validate()); + } } TEST(MemberConfig, ValidatePriorityAndBuildIndexesRelationship) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 1 - << "buildIndexes" - << false), - &tagConfig)); - - ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "priority" - << 1 - << "buildIndexes" - << true), - &tagConfig)); - ASSERT_OK(mc.validate()); + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 1 + << "buildIndexes" + << false), + &tagConfig); + + ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "priority" + << 1 + << "buildIndexes" + << true), + &tagConfig); + ASSERT_OK(mc.validate()); + } } TEST(MemberConfig, ValidateArbiterVotesRelationship) { ReplSetTagConfig tagConfig; - MemberConfig mc; - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 1 - << "arbiterOnly" - << true), - &tagConfig)); - ASSERT_OK(mc.validate()); - - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 0 - << "priority" - << 0 - << "arbiterOnly" - << false), - &tagConfig)); - ASSERT_OK(mc.validate()); - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 1 - << "arbiterOnly" - << false), - &tagConfig)); - ASSERT_OK(mc.validate()); - - ASSERT_OK(mc.initialize(BSON("_id" << 0 << "host" - << "h" - << "votes" - << 0 - << "arbiterOnly" - << true), - &tagConfig)); - ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 1 + << "arbiterOnly" + << true), + &tagConfig); + ASSERT_OK(mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 0 + << "priority" + << 0 + << "arbiterOnly" + << false), + &tagConfig); + ASSERT_OK(mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 1 + << "arbiterOnly" + << false), + &tagConfig); + ASSERT_OK(mc.validate()); + } + { + MemberConfig mc(BSON("_id" << 0 << "host" + << "h" + << "votes" + << 0 + << "arbiterOnly" + << true), + &tagConfig); + ASSERT_EQUALS(ErrorCodes::BadValue, mc.validate()); + } } } // namespace diff --git a/src/mongo/db/repl/repl_set_config.cpp b/src/mongo/db/repl/repl_set_config.cpp index 6b67b047733..ced480e9d04 100644 --- a/src/mongo/db/repl/repl_set_config.cpp +++ b/src/mongo/db/repl/repl_set_config.cpp @@ -142,12 +142,14 @@ Status ReplSetConfig::_initialize(const BSONObj& cfg, bool forInitiate, OID defa << " to be Object, but found " << typeName(memberElement.type())); } - _members.resize(_members.size() + 1); const auto& memberBSON = memberElement.Obj(); - status = _members.back().initialize(memberBSON, &_tagConfig); - if (!status.isOK()) - return Status(ErrorCodes::InvalidReplicaSetConfig, - str::stream() << status.toString() << " for member:" << memberBSON); + try { + _members.emplace_back(memberBSON, &_tagConfig); + } catch (const DBException& ex) { + return Status( + ErrorCodes::InvalidReplicaSetConfig, + str::stream() << ex.toStatus().toString() << " for member:" << memberBSON); + } } // @@ -439,11 +441,53 @@ Status ReplSetConfig::validate() const { size_t voterCount = 0; size_t arbiterCount = 0; size_t electableCount = 0; + + auto extractHorizonMembers = [](const auto& replMember) { + std::vector<std::string> rv; + std::transform(begin(replMember.getHorizonMappings()), + end(replMember.getHorizonMappings()), + back_inserter(rv), + [](auto&& mapping) { return mapping.first; }); + std::sort(begin(rv), end(rv)); + return rv; + }; + + const auto expectedHorizonNameMapping = extractHorizonMembers(_members[0]); + + stdx::unordered_set<std::string> nonUniversalHorizons; + std::map<HostAndPort, int> horizonHostNameCounts; for (size_t i = 0; i < _members.size(); ++i) { const MemberConfig& memberI = _members[i]; Status status = memberI.validate(); if (!status.isOK()) return status; + + // Check the replica set configuration for errors in horizon specification: + // * Check that all members have the same set of horizon names + // * Check that no hostname:port appears more than once for any member + // * Check that all hostname:port endpoints are unique for all members + const auto seenHorizonNameMapping = extractHorizonMembers(memberI); + + if (expectedHorizonNameMapping != seenHorizonNameMapping) { + // Collect a list of horizons only seen on one side of the pair of horizon maps + // considered. Names that are only on one side are non-universal, and should be + // reported -- the same set of horizon names must exist across all replica set members. + // We collect the list while parsing over ALL members, this way we can report all + // horizons which are not universally listed in the replica set configuration in a + // single error message. + std::set_symmetric_difference( + begin(expectedHorizonNameMapping), + end(expectedHorizonNameMapping), + begin(seenHorizonNameMapping), + end(seenHorizonNameMapping), + inserter(nonUniversalHorizons, end(nonUniversalHorizons))); + } else { + // Because "__default" always lives in the mappings, we don't have to get it separately + for (const auto& mapping : memberI.getHorizonMappings()) { + ++horizonHostNameCounts[mapping.second]; + } + } + if (memberI.getHostAndPort().isLocalHost()) { ++localhostCount; } @@ -501,6 +545,48 @@ Status ReplSetConfig::validate() const { } } + // If we found horizons that weren't universally present, list all non-universally present + // horizons for this replica set. + if (!nonUniversalHorizons.empty()) { + const auto missingHorizonList = [&] { + std::string rv; + for (const auto& horizonName : nonUniversalHorizons) { + rv += " " + horizonName + ","; + } + rv.pop_back(); + return rv; + }(); + return Status(ErrorCodes::BadValue, + "Saw a replica set member with a different horizon mapping than the " + "others. The following horizons were not universally present:" + + missingHorizonList); + } + + const auto nonUniqueHostNameList = [&] { + std::vector<HostAndPort> rv; + for (const auto& host : horizonHostNameCounts) { + if (host.second > 1) + rv.push_back(host.first); + } + return rv; + }(); + + if (!nonUniqueHostNameList.empty()) { + const auto nonUniqueHostNames = [&] { + std::string rv; + for (const auto& hostName : nonUniqueHostNameList) { + rv += " " + hostName.toString() + ","; + } + rv.pop_back(); + return rv; + }(); + return Status(ErrorCodes::BadValue, + "The following hostnames are not unique across all horizons and host " + "specifications in the replica set:" + + nonUniqueHostNames); + } + + if (localhostCount != 0 && localhostCount != _members.size()) { return Status( ErrorCodes::BadValue, diff --git a/src/mongo/db/repl/repl_set_config.h b/src/mongo/db/repl/repl_set_config.h index c3de46ea033..53e5e3a0f33 100644 --- a/src/mongo/db/repl/repl_set_config.h +++ b/src/mongo/db/repl/repl_set_config.h @@ -160,6 +160,10 @@ public: return _members.end(); } + const std::vector<MemberConfig>& members() const { + return _members; + } + /** * Access a MemberConfig element by index. */ diff --git a/src/mongo/db/repl/repl_set_config_test.cpp b/src/mongo/db/repl/repl_set_config_test.cpp index 14295fc86fd..5a7ba98157b 100644 --- a/src/mongo/db/repl/repl_set_config_test.cpp +++ b/src/mongo/db/repl/repl_set_config_test.cpp @@ -1310,7 +1310,8 @@ bool operator==(const MemberConfig& a, const MemberConfig& b) { a.getPriority() == b.getPriority() && a.getSlaveDelay() == b.getSlaveDelay() && a.isVoter() == b.isVoter() && a.isArbiter() == b.isArbiter() && a.isHidden() == b.isHidden() && a.shouldBuildIndexes() == b.shouldBuildIndexes() && - a.getNumTags() == b.getNumTags(); + a.getNumTags() == b.getNumTags() && a.getHorizonMappings() == b.getHorizonMappings() && + a.getHorizonReverseHostMappings() == b.getHorizonReverseHostMappings(); } bool operator==(const ReplSetConfig& a, const ReplSetConfig& b) { @@ -1387,6 +1388,29 @@ TEST(ReplSetConfig, toBSONRoundTripAbility) { ASSERT_TRUE(configA == configB); } +TEST(ReplSetConfig, toBSONRoundTripAbilityWithHorizon) { + ReplSetConfig configA; + ReplSetConfig configB; + ASSERT_OK(configA.initialize(BSON( + "_id" + << "rs0" + << "version" + << 1 + << "protocolVersion" + << 1 + << "members" + << BSON_ARRAY(BSON("_id" << 0 << "host" + << "localhost:12345" + << "horizons" + << BSON("horizon" + << "example.com:42"))) + << "settings" + << BSON("heartbeatIntervalMillis" << 5000 << "heartbeatTimeoutSecs" << 20 << "replicaSetId" + << OID::gen())))); + ASSERT_OK(configB.initialize(configA.toBSON())); + ASSERT_TRUE(configA == configB); +} + TEST(ReplSetConfig, toBSONRoundTripAbilityLarge) { ReplSetConfig configA; ReplSetConfig configB; @@ -1872,6 +1896,123 @@ TEST(ReplSetConfig, ConfirmDefaultValuesOfAndAbilityToSetWriteConcernMajorityJou ASSERT_TRUE(config.toBSON().hasField("writeConcernMajorityJournalDefault")); } +TEST(ReplSetConfig, HorizonConsistency) { + ReplSetConfig config; + ASSERT_OK(config.initialize(BSON("_id" + << "rs0" + << "protocolVersion" + << 1 + << "version" + << 1 + << "members" + << BSON_ARRAY(BSON("_id" << 0 << "host" + << "localhost:12345" + << "horizons" + << BSON("alpha" + << "a.host:42" + << "beta" + << "a.host2:43" + << "gamma" + << "a.host3:44")) + << BSON("_id" << 1 << "host" + << "localhost:23456" + << "horizons" + << BSON("alpha" + << "b.host:42" + << "gamma" + << "b.host3:44")) + << BSON("_id" << 2 << "host" + << "localhost:34567" + << "horizons" + << BSON("alpha" + << "c.host:42" + << "beta" + << "c.host1:42" + << "gamma" + << "c.host2:43" + << "delta" + + << "c.host3:44"))) + << "writeConcernMajorityJournalDefault" + << false))); + + Status status = config.validate(); + ASSERT_NOT_OK(status); + ASSERT_EQUALS(status.reason().find("alpha"), std::string::npos); + ASSERT_EQUALS(status.reason().find("gamma"), std::string::npos); + + ASSERT_NOT_EQUALS(status.reason().find("beta"), std::string::npos); + ASSERT_NOT_EQUALS(status.reason().find("delta"), std::string::npos); + + // Within-member duplicates are detected by a different piece of code, first, + // in the member-config code path. + status = config.initialize(BSON("_id" + << "rs0" + << "protocolVersion" + << 1 + << "version" + << 1 + << "members" + << BSON_ARRAY(BSON("_id" << 0 << "host" + << "same1" + << "horizons" + << BSON("alpha" + << "a.host:44" + << "beta" + << "a.host2:44" + << "gamma" + << "a.host3:44" + << "delta" + << "a.host4:45")) + << BSON("_id" << 1 << "host" + << "localhost:1" + << "horizons" + << BSON("alpha" + << "same1" + << "beta" + << "b.host2:44" + << "gamma" + << "b.host3:44" + << "delta" + << "b.host4:44")) + << BSON("_id" << 2 << "host" + << "localhost:2" + << "horizons" + << BSON("alpha" + << "c.host1:44" + << "beta" + << "c.host2:44" + << "gamma" + << "c.host3:44" + << "delta" + << "same2")) + << BSON("_id" << 3 << "host" + << "localhost:3" + << "horizons" + << BSON("alpha" + << "same2" + << "beta" + << "d.host2:44" + << "gamma" + << "d.host3:44" + << "delta" + << "d.host4:44"))) + << "writeConcernMajorityJournalDefault" + << false)); + ASSERT_OK(status) << " failing status was: " << status.reason(); + + status = config.validate(); + ASSERT_NOT_OK(status); + ASSERT_EQUALS(status.reason().find("a.host"), std::string::npos); + ASSERT_EQUALS(status.reason().find("b.host"), std::string::npos); + ASSERT_EQUALS(status.reason().find("c.host"), std::string::npos); + ASSERT_EQUALS(status.reason().find("d.host"), std::string::npos); + ASSERT_EQUALS(status.reason().find("localhost"), std::string::npos); + + ASSERT_NOT_EQUALS(status.reason().find("same1"), std::string::npos); + ASSERT_NOT_EQUALS(status.reason().find("same2"), std::string::npos); +} + TEST(ReplSetConfig, ReplSetId) { // Uninitialized configuration has no ID. ASSERT_FALSE(ReplSetConfig().hasReplicaSetId()); diff --git a/src/mongo/db/repl/replication_coordinator.h b/src/mongo/db/repl/replication_coordinator.h index 1e8f00b83a0..b9eee2a78a9 100644 --- a/src/mongo/db/repl/replication_coordinator.h +++ b/src/mongo/db/repl/replication_coordinator.h @@ -39,6 +39,7 @@ #include "mongo/db/repl/member_data.h" #include "mongo/db/repl/member_state.h" #include "mongo/db/repl/repl_settings.h" +#include "mongo/db/repl/split_horizon.h" #include "mongo/db/repl/sync_source_selector.h" #include "mongo/util/net/hostandport.h" #include "mongo/util/time_support.h" @@ -594,7 +595,8 @@ public: * Handles an incoming isMaster command for a replica set node. Should not be * called on a standalone node. */ - virtual void fillIsMasterForReplSet(IsMasterResponse* result) = 0; + virtual void fillIsMasterForReplSet(IsMasterResponse* result, + const SplitHorizon::Parameters& horizonParams) = 0; /** * Adds to "result" a description of the slaveInfo data structure used to map RIDs to their diff --git a/src/mongo/db/repl/replication_coordinator_impl.cpp b/src/mongo/db/repl/replication_coordinator_impl.cpp index ccf2d91f8af..143594ec56a 100644 --- a/src/mongo/db/repl/replication_coordinator_impl.cpp +++ b/src/mongo/db/repl/replication_coordinator_impl.cpp @@ -2333,11 +2333,12 @@ Status ReplicationCoordinatorImpl::processReplSetGetStatus( return result; } -void ReplicationCoordinatorImpl::fillIsMasterForReplSet(IsMasterResponse* response) { +void ReplicationCoordinatorImpl::fillIsMasterForReplSet( + IsMasterResponse* response, const SplitHorizon::Parameters& horizonParams) { invariant(getSettings().usingReplSets()); stdx::lock_guard<stdx::mutex> lk(_mutex); - _topCoord->fillIsMasterForReplSet(response); + _topCoord->fillIsMasterForReplSet(response, horizonParams); OpTime lastOpTime = _getMyLastAppliedOpTime_inlock(); response->setLastWrite(lastOpTime, lastOpTime.getTimestamp().getSecs()); diff --git a/src/mongo/db/repl/replication_coordinator_impl.h b/src/mongo/db/repl/replication_coordinator_impl.h index 7e003562029..147bd9adf5b 100644 --- a/src/mongo/db/repl/replication_coordinator_impl.h +++ b/src/mongo/db/repl/replication_coordinator_impl.h @@ -211,7 +211,8 @@ public: virtual Status processReplSetGetStatus(BSONObjBuilder* result, ReplSetGetStatusResponseStyle responseStyle) override; - virtual void fillIsMasterForReplSet(IsMasterResponse* result) override; + virtual void fillIsMasterForReplSet(IsMasterResponse* result, + const SplitHorizon::Parameters& horizonParams) override; virtual void appendSlaveInfoData(BSONObjBuilder* result) override; diff --git a/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp b/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp index 56de352ac68..3c088e5d2cb 100644 --- a/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp +++ b/src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp @@ -172,11 +172,11 @@ TEST_F(ReplCoordTest, ElectionSucceedsWhenNodeIsTheOnlyElectableNode) { // Since we're still in drain mode, expect that we report ismaster: false, issecondary:true. IsMasterResponse imResponse; - getReplCoord()->fillIsMasterForReplSet(&imResponse); + getReplCoord()->fillIsMasterForReplSet(&imResponse, {}); ASSERT_FALSE(imResponse.isMaster()) << imResponse.toBSON().toString(); ASSERT_TRUE(imResponse.isSecondary()) << imResponse.toBSON().toString(); signalDrainComplete(&opCtx); - getReplCoord()->fillIsMasterForReplSet(&imResponse); + getReplCoord()->fillIsMasterForReplSet(&imResponse, {}); ASSERT_TRUE(imResponse.isMaster()) << imResponse.toBSON().toString(); ASSERT_FALSE(imResponse.isSecondary()) << imResponse.toBSON().toString(); } @@ -234,11 +234,11 @@ TEST_F(ReplCoordTest, ElectionSucceedsWhenNodeIsTheOnlyNode) { // Since we're still in drain mode, expect that we report ismaster: false, issecondary:true. IsMasterResponse imResponse; - getReplCoord()->fillIsMasterForReplSet(&imResponse); + getReplCoord()->fillIsMasterForReplSet(&imResponse, {}); ASSERT_FALSE(imResponse.isMaster()) << imResponse.toBSON().toString(); ASSERT_TRUE(imResponse.isSecondary()) << imResponse.toBSON().toString(); signalDrainComplete(&opCtx); - getReplCoord()->fillIsMasterForReplSet(&imResponse); + getReplCoord()->fillIsMasterForReplSet(&imResponse, {}); ASSERT_TRUE(imResponse.isMaster()) << imResponse.toBSON().toString(); ASSERT_FALSE(imResponse.isSecondary()) << imResponse.toBSON().toString(); } @@ -2230,7 +2230,7 @@ protected: simulateSuccessfulV1Voting(); IsMasterResponse imResponse; - getReplCoord()->fillIsMasterForReplSet(&imResponse); + getReplCoord()->fillIsMasterForReplSet(&imResponse, {}); ASSERT_FALSE(imResponse.isMaster()) << imResponse.toBSON().toString(); ASSERT_TRUE(imResponse.isSecondary()) << imResponse.toBSON().toString(); diff --git a/src/mongo/db/repl/replication_coordinator_impl_test.cpp b/src/mongo/db/repl/replication_coordinator_impl_test.cpp index 6db3053a42a..821c2d71202 100644 --- a/src/mongo/db/repl/replication_coordinator_impl_test.cpp +++ b/src/mongo/db/repl/replication_coordinator_impl_test.cpp @@ -149,7 +149,7 @@ TEST_F(ReplCoordTest, IsMasterIsFalseDuringStepdown) { // Test that "ismaster" is immediately false, although "secondary" is not yet true. IsMasterResponse response; - replCoord->fillIsMasterForReplSet(&response); + replCoord->fillIsMasterForReplSet(&response, {}); ASSERT_TRUE(response.isConfigSet()); BSONObj responseObj = response.toBSON(); ASSERT_FALSE(responseObj["ismaster"].Bool()); @@ -2594,7 +2594,7 @@ TEST_F(StepDownTest, InterruptingStepDownCommandRestoresWriteAvailability) { // We should not indicate that we are master, nor that we are secondary. IsMasterResponse response; - getReplCoord()->fillIsMasterForReplSet(&response); + getReplCoord()->fillIsMasterForReplSet(&response, {}); ASSERT_FALSE(response.isMaster()); ASSERT_FALSE(response.isSecondary()); @@ -2609,7 +2609,7 @@ TEST_F(StepDownTest, InterruptingStepDownCommandRestoresWriteAvailability) { ASSERT_TRUE(getReplCoord()->getMemberState().primary()); // We should now report that we are master. - getReplCoord()->fillIsMasterForReplSet(&response); + getReplCoord()->fillIsMasterForReplSet(&response, {}); ASSERT_TRUE(response.isMaster()); ASSERT_FALSE(response.isSecondary()); @@ -2646,7 +2646,7 @@ TEST_F(StepDownTest, InterruptingAfterUnconditionalStepdownDoesNotRestoreWriteAv // We should not indicate that we are master, nor that we are secondary. IsMasterResponse response; - getReplCoord()->fillIsMasterForReplSet(&response); + getReplCoord()->fillIsMasterForReplSet(&response, {}); ASSERT_FALSE(response.isMaster()); ASSERT_FALSE(response.isSecondary()); @@ -2671,7 +2671,7 @@ TEST_F(StepDownTest, InterruptingAfterUnconditionalStepdownDoesNotRestoreWriteAv ASSERT_TRUE(getReplCoord()->getMemberState().secondary()); // We should still be indicating that we are not master. - getReplCoord()->fillIsMasterForReplSet(&response); + getReplCoord()->fillIsMasterForReplSet(&response, {}); ASSERT_FALSE(response.isMaster()); // This is the important check, that we didn't accidentally step back up when aborting the @@ -3154,7 +3154,7 @@ TEST_F(ReplCoordTest, IsMasterResponseMentionsLackOfReplicaSetConfig) { start(); IsMasterResponse response; - getReplCoord()->fillIsMasterForReplSet(&response); + getReplCoord()->fillIsMasterForReplSet(&response, {}); ASSERT_FALSE(response.isConfigSet()); BSONObj responseObj = response.toBSON(); ASSERT_FALSE(responseObj["ismaster"].Bool()); @@ -3195,7 +3195,7 @@ TEST_F(ReplCoordTest, IsMaster) { replCoordSetMyLastAppliedOpTime(opTime, Date_t::min() + Seconds(100)); IsMasterResponse response; - getReplCoord()->fillIsMasterForReplSet(&response); + getReplCoord()->fillIsMasterForReplSet(&response, {}); ASSERT_EQUALS("mySet", response.getReplSetName()); ASSERT_EQUALS(2, response.getReplSetVersion()); @@ -3259,7 +3259,7 @@ TEST_F(ReplCoordTest, IsMasterWithCommittedSnapshot) { ASSERT_EQUALS(majorityOpTime, getReplCoord()->getCurrentCommittedSnapshotOpTime()); IsMasterResponse response; - getReplCoord()->fillIsMasterForReplSet(&response); + getReplCoord()->fillIsMasterForReplSet(&response, {}); ASSERT_EQUALS(opTime, response.getLastWriteOpTime()); ASSERT_EQUALS(lastWriteDate, response.getLastWriteDate()); @@ -3282,7 +3282,7 @@ TEST_F(ReplCoordTest, IsMasterInShutdown) { runSingleNodeElection(opCtx.get()); IsMasterResponse responseBeforeShutdown; - getReplCoord()->fillIsMasterForReplSet(&responseBeforeShutdown); + getReplCoord()->fillIsMasterForReplSet(&responseBeforeShutdown, {}); ASSERT_TRUE(responseBeforeShutdown.isMaster()); ASSERT_FALSE(responseBeforeShutdown.isSecondary()); @@ -3290,7 +3290,7 @@ TEST_F(ReplCoordTest, IsMasterInShutdown) { // Must not report ourselves as master while we're in shutdown. IsMasterResponse responseAfterShutdown; - getReplCoord()->fillIsMasterForReplSet(&responseAfterShutdown); + getReplCoord()->fillIsMasterForReplSet(&responseAfterShutdown, {}); ASSERT_FALSE(responseAfterShutdown.isMaster()); ASSERT_FALSE(responseBeforeShutdown.isSecondary()); } diff --git a/src/mongo/db/repl/replication_coordinator_mock.cpp b/src/mongo/db/repl/replication_coordinator_mock.cpp index 40df9529262..dbdd745e7e4 100644 --- a/src/mongo/db/repl/replication_coordinator_mock.cpp +++ b/src/mongo/db/repl/replication_coordinator_mock.cpp @@ -340,7 +340,8 @@ Status ReplicationCoordinatorMock::processReplSetGetStatus(BSONObjBuilder*, return Status::OK(); } -void ReplicationCoordinatorMock::fillIsMasterForReplSet(IsMasterResponse* result) { +void ReplicationCoordinatorMock::fillIsMasterForReplSet(IsMasterResponse* result, + const SplitHorizon::Parameters&) { result->setReplSetVersion(_getConfigReturnValue.getConfigVersion()); result->setIsMaster(true); result->setIsSecondary(false); diff --git a/src/mongo/db/repl/replication_coordinator_mock.h b/src/mongo/db/repl/replication_coordinator_mock.h index 6317399c25c..f841a9c5664 100644 --- a/src/mongo/db/repl/replication_coordinator_mock.h +++ b/src/mongo/db/repl/replication_coordinator_mock.h @@ -179,7 +179,8 @@ public: virtual Status processReplSetGetStatus(BSONObjBuilder*, ReplSetGetStatusResponseStyle); - virtual void fillIsMasterForReplSet(IsMasterResponse* result); + void fillIsMasterForReplSet(IsMasterResponse* result, + const SplitHorizon::Parameters& horizon) override; virtual void appendSlaveInfoData(BSONObjBuilder* result); diff --git a/src/mongo/db/repl/replication_coordinator_test_fixture.cpp b/src/mongo/db/repl/replication_coordinator_test_fixture.cpp index 43f65feb274..8324d84232f 100644 --- a/src/mongo/db/repl/replication_coordinator_test_fixture.cpp +++ b/src/mongo/db/repl/replication_coordinator_test_fixture.cpp @@ -366,10 +366,11 @@ void ReplCoordTest::simulateSuccessfulV1ElectionWithoutExitingDrainMode(Date_t e ASSERT(replCoord->getMemberState().primary()) << replCoord->getMemberState().toString(); IsMasterResponse imResponse; - replCoord->fillIsMasterForReplSet(&imResponse); + replCoord->fillIsMasterForReplSet(&imResponse, {}); ASSERT_FALSE(imResponse.isMaster()) << imResponse.toBSON().toString(); ASSERT_TRUE(imResponse.isSecondary()) << imResponse.toBSON().toString(); } + void ReplCoordTest::simulateSuccessfulV1ElectionAt(Date_t electionTime) { simulateSuccessfulV1ElectionWithoutExitingDrainMode(electionTime); ReplicationCoordinatorImpl* replCoord = getReplCoord(); @@ -380,7 +381,7 @@ void ReplCoordTest::simulateSuccessfulV1ElectionAt(Date_t electionTime) { } ASSERT(replCoord->getApplierState() == ReplicationCoordinator::ApplierState::Stopped); IsMasterResponse imResponse; - replCoord->fillIsMasterForReplSet(&imResponse); + replCoord->fillIsMasterForReplSet(&imResponse, {}); ASSERT_TRUE(imResponse.isMaster()) << imResponse.toBSON().toString(); ASSERT_FALSE(imResponse.isSecondary()) << imResponse.toBSON().toString(); diff --git a/src/mongo/db/repl/replication_info.cpp b/src/mongo/db/repl/replication_info.cpp index 0b32d093332..855356c8c9e 100644 --- a/src/mongo/db/repl/replication_info.cpp +++ b/src/mongo/db/repl/replication_info.cpp @@ -68,12 +68,13 @@ using std::string; using std::stringstream; namespace repl { - +namespace { void appendReplicationInfo(OperationContext* opCtx, BSONObjBuilder& result, int level) { ReplicationCoordinator* replCoord = ReplicationCoordinator::get(opCtx); if (replCoord->getSettings().usingReplSets()) { + const auto& horizonParams = SplitHorizon::getParameters(opCtx->getClient()); IsMasterResponse isMasterResponse; - replCoord->fillIsMasterForReplSet(&isMasterResponse); + replCoord->fillIsMasterForReplSet(&isMasterResponse, horizonParams); result.appendElements(isMasterResponse.toBSON()); if (level) { replCoord->appendSlaveInfoData(&result); @@ -148,8 +149,6 @@ void appendReplicationInfo(OperationContext* opCtx, BSONObjBuilder& result, int } } -namespace { - class ReplicationInfoServerStatus : public ServerStatusSection { public: ReplicationInfoServerStatus() : ServerStatusSection("repl") {} @@ -255,6 +254,7 @@ public: auto& clientMetadataIsMasterState = ClientMetadataIsMasterState::get(opCtx->getClient()); bool seenIsMaster = clientMetadataIsMasterState.hasSeenIsMaster(); + if (!seenIsMaster) { clientMetadataIsMasterState.setSeenIsMaster(); } @@ -266,16 +266,19 @@ public: "The client metadata document may only be sent in the first isMaster"); } - auto swParseClientMetadata = ClientMetadata::parse(element); + auto parsedClientMetadata = uassertStatusOK(ClientMetadata::parse(element)); - uassertStatusOK(swParseClientMetadata.getStatus()); + invariant(parsedClientMetadata); - invariant(swParseClientMetadata.getValue()); + parsedClientMetadata->logClientMetadata(opCtx->getClient()); - swParseClientMetadata.getValue().get().logClientMetadata(opCtx->getClient()); + clientMetadataIsMasterState.setClientMetadata(opCtx->getClient(), + std::move(parsedClientMetadata)); + } - clientMetadataIsMasterState.setClientMetadata( - opCtx->getClient(), std::move(swParseClientMetadata.getValue())); + if (!seenIsMaster) { + auto sniName = opCtx->getClient()->getSniNameForSession(); + SplitHorizon::setParameters(opCtx->getClient(), std::move(sniName)); } // Parse the optional 'internalClient' field. This is provided by incoming connections from diff --git a/src/mongo/db/repl/split_horizon.cpp b/src/mongo/db/repl/split_horizon.cpp new file mode 100644 index 00000000000..10a942c632d --- /dev/null +++ b/src/mongo/db/repl/split_horizon.cpp @@ -0,0 +1,240 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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. + */ + +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kReplication + +#include "mongo/platform/basic.h" + +#include "mongo/db/repl/split_horizon.h" + +#include <utility> + +#include "mongo/bson/util/bson_extract.h" +#include "mongo/db/client.h" +#include "mongo/util/log.h" + +using namespace std::literals::string_literals; + +using std::begin; +using std::end; + +namespace mongo { +namespace repl { +namespace { +const auto getSplitHorizonParameters = Client::declareDecoration<SplitHorizon::Parameters>(); + +using AllMappings = SplitHorizon::AllMappings; + +// The reverse mappings for a forward mapping are used to fully initialize a `SplitHorizon` +// instance. +AllMappings computeReverseMappings(SplitHorizon::ForwardMapping forwardMapping) { + using ForwardMapping = SplitHorizon::ForwardMapping; + using ReverseHostOnlyMapping = SplitHorizon::ReverseHostOnlyMapping; + + // Build the reverse mapping (from host-only to horizon names) from the forward mapping table. + ReverseHostOnlyMapping reverseHostMapping; + + // Default horizon case is special -- it always has to exist, and needs to be set before + // entering the loop, to correctly handle ambiguous host-only cases within that horizon. + reverseHostMapping.emplace(forwardMapping[SplitHorizon::kDefaultHorizon].host(), + std::string{SplitHorizon::kDefaultHorizon}); + for (const auto& entry : forwardMapping) { + reverseHostMapping[entry.second.host()] = entry.first; + } + + // Check for duplicate host-and-port entries. + if (forwardMapping.size() != reverseHostMapping.size()) { + const auto horizonMember = [&] { + std::vector<std::string> rv; + std::transform(begin(forwardMapping), + end(forwardMapping), + back_inserter(rv), + [](const auto& entry) { return entry.second.host(); }); + std::sort(begin(rv), end(rv)); + return rv; + }(); + + auto duplicate = std::adjacent_find(begin(horizonMember), end(horizonMember)); + invariant(duplicate != end(horizonMember)); + + uasserted(ErrorCodes::BadValue, "Duplicate horizon member found \""s + *duplicate + "\"."); + } + + + return {std::move(forwardMapping), std::move(reverseHostMapping)}; +} + +SplitHorizon::ForwardMapping computeForwardMappings( + const HostAndPort& host, const boost::optional<BSONElement>& horizonsElement) { + SplitHorizon::ForwardMapping forwardMapping; + + if (horizonsElement) { + using MapMember = std::pair<std::string, HostAndPort>; + + if (horizonsElement->eoo()) { + uasserted(ErrorCodes::BadValue, + str::stream() << "The horizons field cannot be empty, if present."); + } + + if (horizonsElement->type() != Object) { + uasserted(ErrorCodes::TypeMismatch, + str::stream() << "The horizons field must be an object"); + } + + const auto& horizonsObject = horizonsElement->Obj(); + + if (horizonsObject.isEmpty()) { + uasserted(ErrorCodes::BadValue, + str::stream() << "The horizons field cannot be empty, if present."); + } + + // Process all of the BSON description of horizons into a linear list. + auto convert = [](auto&& horizonObj) -> MapMember { + const StringData horizonName = horizonObj.fieldName(); + + if (horizonObj.type() != String) { + uasserted(ErrorCodes::TypeMismatch, + str::stream() << "horizons." << horizonName + << " field has non-string value of type " + << typeName(horizonObj.type())); + } else if (horizonName == SplitHorizon::kDefaultHorizon) { + uasserted(ErrorCodes::BadValue, + "Horizon name \"" + SplitHorizon::kDefaultHorizon + + "\" is reserved for internal mongodb usage"); + } else if (horizonName == "") { + uasserted(ErrorCodes::BadValue, "Horizons cannot have empty names"); + } + + return {horizonName.toString(), HostAndPort{horizonObj.valueStringData()}}; + }; + + const auto horizonEntries = [&] { + std::vector<MapMember> rv; + std::transform( + begin(horizonsObject), end(horizonsObject), inserter(rv, end(rv)), convert); + return rv; + }(); + + + // Dump the linear list into the forward mapping. + forwardMapping.insert(begin(horizonEntries), end(horizonEntries)); + + // Check for duplicate horizon names and reserved names, which would be if the horizon + // linear list size disagrees with the size of the mapping. + if (horizonEntries.size() != forwardMapping.size()) { + // If the map has a different amount than a linear list of the bson converted, then it + // had better be a lesser amount, indicating duplicates. A greater amount should be + // impossible. + invariant(horizonEntries.size() > forwardMapping.size()); + + // Find which one is duplicated. + const auto horizonNames = [&] { + std::vector<std::string> rv; + std::transform(begin(horizonEntries), + end(horizonEntries), + back_inserter(rv), + [](const auto& entry) { return entry.first; }); + std::sort(begin(rv), end(rv)); + return rv; + }(); + + const auto duplicate = std::adjacent_find(begin(horizonNames), end(horizonNames)); + + // Report our duplicate if found. + if (duplicate != end(horizonNames)) { + uasserted(ErrorCodes::BadValue, + "Duplicate horizon name found \""s + *duplicate + "\"."); + } + } + } + + // Finally add the default mapping, regardless of whether we processed a configuration. + const bool successInDefaultPlacement = + forwardMapping.emplace(SplitHorizon::kDefaultHorizon, host).second; + // And that insertion BETTER succeed -- it mightn't if there's a bug in the configuration + // processing logic. + invariant(successInDefaultPlacement); + + return forwardMapping; +} +} // namespace + +void SplitHorizon::setParameters(Client* const client, boost::optional<std::string> sniName) { + stdx::lock_guard<Client> lk(*client); + getSplitHorizonParameters(*client) = Parameters{std::move(sniName)}; +} + +auto SplitHorizon::getParameters(const Client* const client) -> Parameters { + return getSplitHorizonParameters(*client); +} + +StringData SplitHorizon::determineHorizon(const SplitHorizon::Parameters& horizonParameters) const { + if (horizonParameters.sniName) { + const auto sniName = *horizonParameters.sniName; + const auto found = _reverseHostMapping.find(sniName); + if (found != end(_reverseHostMapping)) { + return found->second; + } + } + return kDefaultHorizon; +} + +void SplitHorizon::toBSON(BSONObjBuilder& configBuilder) const { + invariant(!_forwardMapping.empty()); + invariant(_forwardMapping.count(SplitHorizon::kDefaultHorizon)); + + // `forwardMapping` should always contain the "__default" horizon, so we need to emit the + // horizon repl specification only when there are OTHER horizons besides it. If there's only + // one horizon, it's gotta be "__default", so we do nothing. + if (_forwardMapping.size() == 1) + return; + + BSONObjBuilder horizonsBson(configBuilder.subobjStart("horizons")); + for (const auto& horizon : _forwardMapping) { + // The "__default" horizon should never be emitted in the horizon table. + if (horizon.first == SplitHorizon::kDefaultHorizon) + continue; + horizonsBson.append(horizon.first, horizon.second.toString()); + } +} + +// A split horizon built from a known forward mapping table should just need to construct the +// reverse mappings. +SplitHorizon::SplitHorizon(ForwardMapping mapping) + : SplitHorizon(computeReverseMappings(std::move(mapping))) {} + +// A split horizon constructed from the BSON configuration and the host specifier for this member +// needs to compute the forward mapping table. In turn that will be used to compute the reverse +// mapping table. +SplitHorizon::SplitHorizon(const HostAndPort& host, + const boost::optional<BSONElement>& horizonsElement) + : SplitHorizon(computeForwardMappings(host, horizonsElement)) {} + +} // namespace repl +} // namespace mongo diff --git a/src/mongo/db/repl/split_horizon.h b/src/mongo/db/repl/split_horizon.h new file mode 100644 index 00000000000..8728a6a173e --- /dev/null +++ b/src/mongo/db/repl/split_horizon.h @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <map> +#include <string> + +#include <boost/optional.hpp> + +#include "mongo/base/string_data.h" +#include "mongo/db/client.h" +#include "mongo/db/repl/repl_set_tag.h" +#include "mongo/util/net/hostandport.h" + +namespace mongo { +namespace repl { + +/** + * Every Replica Set member has several views under which it can respond. The Split Horizon class + * represents the unification of all of those views. For example, a member might be reachable under + * "internal.example.com:27017" and "external.example.com:25000". The replica set needs to be able + * to respond, as a group, with the correct view, when `isMaster` requests come in. Each member of + * the replica set has its own `SplitHorizon` class to manage the mapping between server names and + * horizon names. `SplitHorizon` models a single member's view across all horizons, not views for + * all of the members. + */ +class SplitHorizon { +public: + static constexpr auto kDefaultHorizon = "__default"_sd; + + using ForwardMapping = StringMap<HostAndPort>; + using ReverseHostOnlyMapping = std::map<std::string, std::string>; + + using AllMappings = + std::tuple<SplitHorizon::ForwardMapping, SplitHorizon::ReverseHostOnlyMapping>; + + struct Parameters { + boost::optional<std::string> sniName; + + Parameters() = default; + explicit Parameters(boost::optional<std::string> initialSniName) + : sniName(std::move(initialSniName)) {} + }; + + /** + * Set the split horizon connection parameters, for use by future `isMaster` commands. + */ + static void setParameters(Client* client, boost::optional<std::string> sniName); + + /** + * Get the client's SplitHorizonParameters object. + */ + static Parameters getParameters(const Client*); + + explicit SplitHorizon() = default; + explicit SplitHorizon(const HostAndPort& host, + const boost::optional<BSONElement>& horizonsElement); + + // This constructor is for testing and internal use only + explicit SplitHorizon(ForwardMapping forward); + + /** + * Gets the horizon name for which the parameters (captured during the first `isMaster`) + * correspond. + */ + StringData determineHorizon(const Parameters& horizonParameters) const; + + const HostAndPort& getHostAndPort(StringData horizon) const { + invariant(!_forwardMapping.empty()); + invariant(!horizon.empty()); + auto found = _forwardMapping.find(horizon); + if (found == end(_forwardMapping)) + uasserted(ErrorCodes::NoSuchKey, str::stream() << "No horizon named " << horizon); + return found->second; + } + + const auto& getForwardMappings() const { + return _forwardMapping; + } + + const auto& getReverseHostMappings() const { + return _reverseHostMapping; + } + + void toBSON(BSONObjBuilder& configBuilder) const; + +private: + // Unified Constructor -- All other constructors delegate to this one. + explicit SplitHorizon(AllMappings mappings) + : _forwardMapping(std::move(std::get<0>(mappings))), + _reverseHostMapping(std::move(std::get<1>(mappings))) {} + + // Maps each horizon name to a network address for this replica set member + ForwardMapping _forwardMapping; + + // Maps each hostname which this replica set member has to a horizon name under which that + // address applies + ReverseHostOnlyMapping _reverseHostMapping; +}; +} // namespace repl +} // namespace mongo diff --git a/src/mongo/db/repl/split_horizon_test.cpp b/src/mongo/db/repl/split_horizon_test.cpp new file mode 100644 index 00000000000..0a3a655ccaf --- /dev/null +++ b/src/mongo/db/repl/split_horizon_test.cpp @@ -0,0 +1,469 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/repl/split_horizon.h" + +#include <algorithm> +#include <boost/optional.hpp> +#include <iterator> + +#include "mongo/stdx/utility.h" +#include "mongo/unittest/unittest.h" + +using namespace std::literals::string_literals; + +namespace mongo { +namespace repl { +namespace { +static const std::string defaultHost = "default.dns.name.example.com"; +static const std::string defaultPort = "4242"; +static const std::string defaultHostAndPort = defaultHost + ":" + defaultPort; + +static const std::string matchingHost = "matching.dns.name.example.com"; +static const std::string matchingPort = "4243"; +static const std::string matchingHostAndPort = matchingHost + ":" + matchingPort; + + +static const std::string nonmatchingHost = "nonmatching.dns.name.example.com"; +static const std::string nonmatchingPort = "4244"; +static const std::string nonmatchingHostAndPort = nonmatchingHost + ":" + nonmatchingPort; + +static const std::string altPort = ":666"; + +using MappingType = std::map<std::string, std::string>; + +SplitHorizon::ForwardMapping populateForwardMap(const MappingType& mapping) { + SplitHorizon::ForwardMapping forwardMapping; + forwardMapping.emplace(SplitHorizon::kDefaultHorizon, defaultHostAndPort); + + using ForwardMappingValueType = decltype(forwardMapping)::value_type; + using ElementType = MappingType::value_type; + auto createForwardMapping = [](const ElementType& element) { + return ForwardMappingValueType{element.first, HostAndPort(element.second)}; + }; + std::transform(begin(mapping), + end(mapping), + inserter(forwardMapping, end(forwardMapping)), + createForwardMapping); + + return forwardMapping; +} + +TEST(SplitHorizonTesting, determineHorizon) { + struct Input { + SplitHorizon::ForwardMapping forwardMapping; // Will get "__default" added to it. + SplitHorizon::Parameters horizonParameters; + + Input(const MappingType& mapping, boost::optional<std::string> sniName) + : forwardMapping(populateForwardMap(mapping)), horizonParameters(std::move(sniName)) {} + }; + + struct { + Input input; + + std::string expected; + } tests[] = { + // No parameters and no horizon views configured. + {{{}, boost::none}, "__default"}, + {{{}, defaultHost}, "__default"}, + + // No SNI -> no match + {{{{"unusedHorizon", "badmatch:00001"}}, boost::none}, "__default"}, + + // Unmatching SNI -> no match + {{{{"unusedHorizon", "badmatch:00001"}}, nonmatchingHost}, "__default"}, + + // Matching SNI -> match + {{{{"targetHorizon", matchingHostAndPort}}, matchingHost}, "targetHorizon"}, + }; + + for (const auto& test : tests) { + const auto& expected = test.expected; + const auto& input = test.input; + + const std::string witness = + SplitHorizon(input.forwardMapping).determineHorizon(input.horizonParameters).toString(); + ASSERT_EQUALS(witness, expected); + } + + const Input failingCtorCases[] = { + // Matching SNI, different port, collision -> fails + {{{"targetHorizon", matchingHost + altPort}, {"badHorizon", matchingHostAndPort}}, + matchingHost}, + + // Default horizon ambiguous case is a failure + {{{"targetHorizon", defaultHost + altPort}, {"badHorizon", nonmatchingHostAndPort}}, + defaultHost}, + }; + + for (const auto& input : failingCtorCases) { + ASSERT_THROWS(SplitHorizon(input.forwardMapping), ExceptionFor<ErrorCodes::BadValue>); + } +} + +TEST(SplitHorizonTesting, basicConstruction) { + struct Input { + SplitHorizon::ForwardMapping forwardMapping; // Will get "__default" added to it. + + Input(const MappingType& mapping) : forwardMapping(populateForwardMap(mapping)) {} + }; + + const struct { + Input input; + ErrorCodes::Error expectedErrorCode; + std::vector<std::string> expectedErrorMessageFragments; + std::vector<std::string> absentErrorMessageFragments; + } tests[] = { + // Empty case (the `Input` type constructs the expected "__default" member.) + {{{}}, ErrorCodes::OK, {}, {}}, + + // A single horizon case, with no conflicts. + {{{{"extraHorizon", "example.com:42"}}}, ErrorCodes::OK, {}, {}}, + + // Two horizons with no conflicts + {{{{"extraHorizon", "example.com:42"}, {"extraHorizon2", "extra.example.com:42"}}}, + ErrorCodes::OK, + {}, + {}}, + + // Three horizons with no conflicts + {{{{"extraHorizon", "example.com:42"}, + {"extraHorizon2", "extra.example.com:42"}, + {"extraHorizon3", "extra3.example.com" + altPort}}}, + ErrorCodes::OK, + {}, + {}}, + + // Two horizons, with the same host and port + {{{{"horizon1", "same.example.com:42"}, {"horizon2", "same.example.com:42"}}}, + ErrorCodes::BadValue, + {"Duplicate horizon member found", "same.example.com"}, + {}}, + + // Two horizons, with the same host and different port + {{{{"horizon1", "same.example.com:42"}, {"horizon2", "same.example.com:43"}}}, + ErrorCodes::BadValue, + {"Duplicate horizon member found", "same.example.com"}, + {}}, + + // Three horizons, with two of them having the same host and port (checking that + // the distinct horizon isn't reported in the error message. + {{{{"horizon1", "same.example.com:42"}, + {"horizon2", "different.example.com:42"}, + {"horizon3", "same.example.com:42"}}}, + ErrorCodes::BadValue, + {"Duplicate horizon member found", "same.example.com"}, + {"different.example.com"}}, + }; + + for (const auto& test : tests) { + const auto& input = test.input; + const auto& expectedErrorCode = test.expectedErrorCode; + const auto horizonOpt = [&]() -> boost::optional<SplitHorizon> { + try { + return SplitHorizon(input.forwardMapping); + } catch (const DBException& ex) { + ASSERT_NOT_EQUALS(expectedErrorCode, ErrorCodes::OK); + ASSERT_EQUALS(ex.toStatus().code(), expectedErrorCode); + for (const auto& fragment : test.expectedErrorMessageFragments) { + ASSERT_NOT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos); + } + for (const auto& fragment : test.absentErrorMessageFragments) { + ASSERT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos); + } + return boost::none; + } + }(); + + if (!horizonOpt) + continue; + ASSERT_EQUALS(expectedErrorCode, ErrorCodes::OK); + + const auto& horizon = *horizonOpt; + + for (const auto& element : input.forwardMapping) { + { + const auto found = horizon.getForwardMappings().find(element.first); + ASSERT_TRUE(found != end(horizon.getForwardMappings())); + ASSERT_EQUALS(HostAndPort(element.second).toString(), found->second.toString()); + } + + { + const auto found = + horizon.getReverseHostMappings().find(HostAndPort(element.second).host()); + ASSERT_TRUE(found != end(horizon.getReverseHostMappings())); + ASSERT_EQUALS(element.first, found->second); + } + } + ASSERT_EQUALS(input.forwardMapping.size(), horizon.getForwardMappings().size()); + ASSERT_EQUALS(input.forwardMapping.size(), horizon.getReverseHostMappings().size()); + } +} + +TEST(SplitHorizonTesting, BSONConstruction) { + // The none-case can be tested outside ot the table, to help keep the table ctors + // easier. + { + const SplitHorizon horizon(HostAndPort(matchingHostAndPort), boost::none); + + { + const auto forwardFound = horizon.getForwardMappings().find("__default"); + ASSERT_TRUE(forwardFound != end(horizon.getForwardMappings())); + ASSERT_EQUALS(forwardFound->second, HostAndPort(matchingHostAndPort)); + ASSERT_EQUALS(horizon.getForwardMappings().size(), std::size_t{1}); + } + + { + const auto reverseFound = horizon.getReverseHostMappings().find(matchingHost); + ASSERT_TRUE(reverseFound != end(horizon.getReverseHostMappings())); + ASSERT_EQUALS(reverseFound->second, "__default"); + + ASSERT_EQUALS(horizon.getReverseHostMappings().size(), std::size_t{1}); + } + } + + const struct { + BSONObj bsonContents; + std::string host; + std::vector<std::pair<std::string, std::string>> expectedMapping; // bidirectional + ErrorCodes::Error expectedErrorCode; + std::vector<std::string> expectedErrorMessageFragments; + std::vector<std::string> absentErrorMessageFragments; + } tests[] = { + // Empty bson object + {BSONObj(), + defaultHostAndPort, + {}, + ErrorCodes::BadValue, + {"horizons field cannot be empty, if present"}, + {"example.com"}}, + + // One simple horizon case. + {BSON("horizon" << matchingHostAndPort), + defaultHostAndPort, + {{"__default", defaultHostAndPort}, {"horizon", matchingHostAndPort}}, + ErrorCodes::OK, + {}, + {}}, + + // Two simple horizons case + {BSON("horizon" << matchingHostAndPort << "horizon2" << nonmatchingHostAndPort), + defaultHostAndPort, + {{"__default", defaultHostAndPort}, + {"horizon", matchingHostAndPort}, + {"horizon2", nonmatchingHostAndPort}}, + ErrorCodes::OK, + {}, + {}}, + + // Three horizons, two having duplicate names + { + BSON("duplicateHorizon" + << "horizon1.example.com:42" + << "duplicateHorizon" + << "horizon2.example.com:42" + << "uniqueHorizon" + << "horizon3.example.com:42"), + defaultHostAndPort, + {}, + ErrorCodes::BadValue, + {"Duplicate horizon name found", "duplicateHorizon"}, + {"uniqueHorizon", "__default"}}, + + // Two horizons with duplicate host and ports. + {BSON("horizonWithDuplicateHost1" << matchingHostAndPort << "horizonWithDuplicateHost2" + << matchingHostAndPort + << "uniqueHorizon" + << nonmatchingHost), + defaultHostAndPort, + {}, + ErrorCodes::BadValue, + {"Duplicate horizon member found", matchingHost}, + {"uniqueHorizon", nonmatchingHost, defaultHost}}, + }; + + for (const auto& test : tests) { + const auto testNumber = &test - tests; + const BSONObj bson = BSON("horizons" << test.bsonContents); + const auto& expectedErrorCode = test.expectedErrorCode; + + const auto horizonOpt = [&]() -> boost::optional<SplitHorizon> { + const auto host = HostAndPort(test.host); + const auto& bsonElement = bson.firstElement(); + try { + return SplitHorizon(host, bsonElement); + } catch (const DBException& ex) { + ASSERT_NOT_EQUALS(expectedErrorCode, ErrorCodes::OK) + << "Failing on test case # " << (&test - tests) + << " with unexpected failure: " << ex.toStatus().reason(); + ASSERT_EQUALS(ex.toStatus().code(), expectedErrorCode) + << "Failing status code comparison on test case " << testNumber + << " reason: " << ex.toStatus().reason(); + for (const auto& fragment : test.expectedErrorMessageFragments) { + ASSERT_NOT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos) + << "Wanted to see the text fragment \"" << fragment + << "\" in the message: \"" << ex.toStatus().reason() << "\""; + } + for (const auto& fragment : test.absentErrorMessageFragments) { + ASSERT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos); + } + return boost::none; + } + }(); + + if (!horizonOpt) + continue; + ASSERT_EQUALS(expectedErrorCode, ErrorCodes::OK); + + const auto& horizon = *horizonOpt; + + for (const auto& element : test.expectedMapping) { + { + const auto found = horizon.getForwardMappings().find(element.first); + ASSERT_TRUE(found != end(horizon.getForwardMappings())); + ASSERT_EQUALS(HostAndPort(element.second).toString(), found->second.toString()); + } + + { + const auto found = + horizon.getReverseHostMappings().find(HostAndPort(element.second).host()); + ASSERT_TRUE(found != end(horizon.getReverseHostMappings())) + << "Failed test # " << testNumber + << " because we didn't find a reverse mapping for the host " << element.first; + ASSERT_EQUALS(element.first, found->second) << "on test " << testNumber; + } + } + + ASSERT_EQUALS(test.expectedMapping.size(), horizon.getForwardMappings().size()); + ASSERT_EQUALS(test.expectedMapping.size(), horizon.getReverseHostMappings().size()); + } +} + +TEST(SplitHorizonTesting, toBSON) { + struct Input { + SplitHorizon::ForwardMapping forwardMapping; // Will get "__default" added to it. + + Input(const MappingType& mapping) : forwardMapping(populateForwardMap(mapping)) {} + }; + + const Input tests[] = { + {{}}, + {{{"horizon1", "horizon1.example.com:42"}}}, + {{{"horizon1", "horizon1.example.com:42"}, {"horizon2", "horizon2.example.com:42"}}}, + {{{"horizon1", "horizon1.example.com:42"}, + {"horizon2", "horizon2.example.com:42"}, + {"horizon3", "horizon3.example.com:99"}}}, + }; + for (const auto& test : tests) { + const auto testNumber = &test - tests; + const auto& input = test; + const auto& expectedKeys = [&] { + auto rv = input.forwardMapping; + rv.erase(SplitHorizon::kDefaultHorizon); + return rv; + }(); + + const SplitHorizon horizon(input.forwardMapping); + + const BSONObj output = [&] { + BSONObjBuilder outputBuilder; + horizon.toBSON(outputBuilder); + return outputBuilder.obj(); + }(); + + if (expectedKeys.empty()) { + ASSERT_TRUE(output["horizons"].eoo()); + continue; + } + + ASSERT_FALSE(output["horizons"].eoo()); + + for (const auto& horizons : output) { + ASSERT_TRUE(horizons.fieldNameStringData() == "horizons") + << "Test #" << testNumber << " Failing finding a horizons element"; + } + + const auto& horizonsElement = output["horizons"]; + ASSERT_EQUALS(horizonsElement.type(), Object); + const auto& horizons = horizonsElement.Obj(); + + std::vector<std::string> visitedHorizons; + + for (const auto& element : horizons) { + ASSERT_TRUE(expectedKeys.count(element.fieldNameStringData().toString())) + << "Test #" << testNumber << " Failing because we found a horizon with the name " + << element.fieldNameStringData() << " which shouldn't exist."; + + ASSERT_TRUE( + expectedKeys.find(element.fieldNameStringData().toString())->second.toString() == + element.valueStringData()) + << "Test #" << testNumber << " failing because horizon " + << element.fieldNameStringData() << " had an incorrect HostAndPort"; + visitedHorizons.push_back(element.fieldNameStringData().toString()); + } + + ASSERT_EQUALS(visitedHorizons.size(), expectedKeys.size()); + } +} + +TEST(SplitHorizonTesting, BSONRoundTrip) { + struct Input { + SplitHorizon::ForwardMapping forwardMapping; // Will get "__default" added to it. + + Input(const MappingType& mapping) : forwardMapping(populateForwardMap(mapping)) {} + }; + + const Input tests[] = { + {{{"horizon1", "horizon1.example.com:42"}}}, + {{{"horizon1", "horizon1.example.com:42"}, {"horizon2", "horizon2.example.com:42"}}}, + }; + for (const auto& input : tests) { + const auto testNumber = &input - tests; + + const SplitHorizon horizon(input.forwardMapping); + + const BSONObj bson = [&] { + BSONObjBuilder outputBuilder; + horizon.toBSON(outputBuilder); + return outputBuilder.obj(); + }(); + + const SplitHorizon witness(HostAndPort(defaultHostAndPort), bson["horizons"]); + + ASSERT_TRUE(horizon.getForwardMappings() == witness.getForwardMappings()) + << "Test #" << testNumber << " Failed on bson round trip with forward map"; + ASSERT_TRUE(horizon.getReverseHostMappings() == witness.getReverseHostMappings()) + << "Test #" << testNumber << " Failed on bson round trip with reverse map"; + } +} +} // namespace +} // namespace repl +} // namespace mongo diff --git a/src/mongo/db/repl/topology_coordinator.cpp b/src/mongo/db/repl/topology_coordinator.cpp index 233ed4ee970..87ac940523d 100644 --- a/src/mongo/db/repl/topology_coordinator.cpp +++ b/src/mongo/db/repl/topology_coordinator.cpp @@ -1721,34 +1721,41 @@ void TopologyCoordinator::fillMemberData(BSONObjBuilder* result) { } } -void TopologyCoordinator::fillIsMasterForReplSet(IsMasterResponse* response) { +void TopologyCoordinator::fillIsMasterForReplSet(IsMasterResponse* const response, + const SplitHorizon::Parameters& horizonParams) { const MemberState myState = getMemberState(); if (!_rsConfig.isInitialized()) { response->markAsNoConfig(); return; } - for (ReplSetConfig::MemberIterator it = _rsConfig.membersBegin(); it != _rsConfig.membersEnd(); - ++it) { - if (it->isHidden() || it->getSlaveDelay() > Seconds{0}) { + response->setReplSetName(_rsConfig.getReplSetName()); + if (myState.removed()) { + response->markAsNoConfig(); + return; + } + + invariant(!_rsConfig.members().empty()); + + const auto& self = _rsConfig.members()[_selfIndex]; + + const auto horizon = self.determineHorizon(horizonParams); + + for (const auto& member : _rsConfig.members()) { + if (member.isHidden() || member.getSlaveDelay() > Seconds{0}) { continue; } + auto hostView = member.getHostAndPort(horizon); - if (it->isElectable()) { - response->addHost(it->getHostAndPort()); - } else if (it->isArbiter()) { - response->addArbiter(it->getHostAndPort()); + if (member.isElectable()) { + response->addHost(std::move(hostView)); + } else if (member.isArbiter()) { + response->addArbiter(std::move(hostView)); } else { - response->addPassive(it->getHostAndPort()); + response->addPassive(std::move(hostView)); } } - response->setReplSetName(_rsConfig.getReplSetName()); - if (myState.removed()) { - response->markAsNoConfig(); - return; - } - response->setReplSetVersion(_rsConfig.getConfigVersion()); // "ismaster" is false if we are not primary. If we're stepping down, we're waiting for the // Replication State Transition Lock before we can change to secondary, but we should report @@ -1758,7 +1765,7 @@ void TopologyCoordinator::fillIsMasterForReplSet(IsMasterResponse* response) { const MemberConfig* curPrimary = _currentPrimaryMember(); if (curPrimary) { - response->setPrimary(curPrimary->getHostAndPort()); + response->setPrimary(curPrimary->getHostAndPort(horizon)); } const MemberConfig& selfConfig = _rsConfig.getMemberAt(_selfIndex); diff --git a/src/mongo/db/repl/topology_coordinator.h b/src/mongo/db/repl/topology_coordinator.h index 742f98abd0c..9c4a674fdab 100644 --- a/src/mongo/db/repl/topology_coordinator.h +++ b/src/mongo/db/repl/topology_coordinator.h @@ -35,6 +35,7 @@ #include "mongo/db/repl/last_vote.h" #include "mongo/db/repl/repl_set_heartbeat_response.h" #include "mongo/db/repl/replication_coordinator.h" +#include "mongo/db/repl/split_horizon.h" #include "mongo/db/repl/update_position_args.h" #include "mongo/db/server_options.h" #include "mongo/stdx/functional.h" @@ -321,7 +322,8 @@ public: // Produce a reply to an ismaster request. It is only valid to call this if we are a // replset. Drivers interpret the isMaster fields according to the Server Discovery and // Monitoring Spec, see the "Parsing an isMaster response" section. - void fillIsMasterForReplSet(IsMasterResponse* response); + void fillIsMasterForReplSet(IsMasterResponse* response, + const SplitHorizon::Parameters& horizonParams); // Produce member data for the serverStatus command and diagnostic logging. void fillMemberData(BSONObjBuilder* result); |