summaryrefslogtreecommitdiff
path: root/src/mongo
diff options
context:
space:
mode:
authorADAM David Alan Martin <adam.martin@10gen.com>2019-05-17 15:41:29 -0400
committerADAM David Alan Martin <adam.martin@10gen.com>2019-05-17 15:41:29 -0400
commit6784f6568cc45fe25510e2d2393be57daffb5411 (patch)
tree1b3e93ce250ec2fbded3caea297e802b575c111f /src/mongo
parent88efdbf5b5c3b2c30b5b971a9adcaaa0a9f51797 (diff)
downloadmongo-6784f6568cc45fe25510e2d2393be57daffb5411.tar.gz
SERVER-40156 Replica sets support Split Horizons
Replica sets can now respond to `isMaster` requests with different hostnames and ports, if contacted via alternate names using TLS. The `horizons` field in replica set member configurations can be used to control which `HostAndPort` to reply with for which "horizon view" of a replica set.
Diffstat (limited to 'src/mongo')
-rw-r--r--src/mongo/db/auth/authorization_manager_test.cpp14
-rw-r--r--src/mongo/db/client.h5
-rw-r--r--src/mongo/db/repl/SConscript26
-rw-r--r--src/mongo/db/repl/member_config.cpp111
-rw-r--r--src/mongo/db/repl/member_config.h48
-rw-r--r--src/mongo/db/repl/member_config_test.cpp985
-rw-r--r--src/mongo/db/repl/repl_set_config.cpp96
-rw-r--r--src/mongo/db/repl/repl_set_config.h4
-rw-r--r--src/mongo/db/repl/repl_set_config_test.cpp143
-rw-r--r--src/mongo/db/repl/replication_coordinator.h4
-rw-r--r--src/mongo/db/repl/replication_coordinator_impl.cpp5
-rw-r--r--src/mongo/db/repl/replication_coordinator_impl.h3
-rw-r--r--src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp10
-rw-r--r--src/mongo/db/repl/replication_coordinator_impl_test.cpp20
-rw-r--r--src/mongo/db/repl/replication_coordinator_mock.cpp3
-rw-r--r--src/mongo/db/repl/replication_coordinator_mock.h3
-rw-r--r--src/mongo/db/repl/replication_coordinator_test_fixture.cpp5
-rw-r--r--src/mongo/db/repl/replication_info.cpp23
-rw-r--r--src/mongo/db/repl/split_horizon.cpp240
-rw-r--r--src/mongo/db/repl/split_horizon.h128
-rw-r--r--src/mongo/db/repl/split_horizon_test.cpp469
-rw-r--r--src/mongo/db/repl/topology_coordinator.cpp39
-rw-r--r--src/mongo/db/repl/topology_coordinator.h4
-rw-r--r--src/mongo/embedded/replication_coordinator_embedded.cpp3
-rw-r--r--src/mongo/embedded/replication_coordinator_embedded.h3
-rw-r--r--src/mongo/transport/session.h4
-rw-r--r--src/mongo/transport/session_asio.h32
-rw-r--r--src/mongo/util/net/sock.h2
-rw-r--r--src/mongo/util/net/ssl_manager.h9
-rw-r--r--src/mongo/util/net/ssl_manager_apple.cpp51
-rw-r--r--src/mongo/util/net/ssl_manager_openssl.cpp32
-rw-r--r--src/mongo/util/net/ssl_manager_windows.cpp27
-rw-r--r--src/mongo/util/net/ssl_types.cpp4
-rw-r--r--src/mongo/util/net/ssl_types.h12
34 files changed, 1968 insertions, 599 deletions
diff --git a/src/mongo/db/auth/authorization_manager_test.cpp b/src/mongo/db/auth/authorization_manager_test.cpp
index cddb514048c..f51437f697a 100644
--- a/src/mongo/db/auth/authorization_manager_test.cpp
+++ b/src/mongo/db/auth/authorization_manager_test.cpp
@@ -179,9 +179,10 @@ TEST_F(AuthorizationManagerTest, testAcquireV2User) {
#ifdef MONGO_CONFIG_SSL
TEST_F(AuthorizationManagerTest, testLocalX509Authorization) {
- setX509PeerInfo(
- session,
- SSLPeerInfo(buildX509Name(), {RoleName("read", "test"), RoleName("readWrite", "test")}));
+ setX509PeerInfo(session,
+ SSLPeerInfo(buildX509Name(),
+ boost::none,
+ {RoleName("read", "test"), RoleName("readWrite", "test")}));
auto swu = authzManager->acquireUser(opCtx.get(), UserName("CN=mongodb.com", "$external"));
ASSERT_OK(swu.getStatus());
@@ -206,9 +207,10 @@ TEST_F(AuthorizationManagerTest, testLocalX509Authorization) {
#endif
TEST_F(AuthorizationManagerTest, testLocalX509AuthorizationInvalidUser) {
- setX509PeerInfo(
- session,
- SSLPeerInfo(buildX509Name(), {RoleName("read", "test"), RoleName("write", "test")}));
+ setX509PeerInfo(session,
+ SSLPeerInfo(buildX509Name(),
+ boost::none,
+ {RoleName("read", "test"), RoleName("write", "test")}));
ASSERT_NOT_OK(
authzManager->acquireUser(opCtx.get(), UserName("CN=10gen.com", "$external")).getStatus());
diff --git a/src/mongo/db/client.h b/src/mongo/db/client.h
index c1b540ac11b..99ff609d730 100644
--- a/src/mongo/db/client.h
+++ b/src/mongo/db/client.h
@@ -39,7 +39,6 @@
#include <boost/optional.hpp>
-#include "mongo/db/client.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/service_context.h"
#include "mongo/platform/random.h"
@@ -124,6 +123,10 @@ public:
return _session;
}
+ boost::optional<std::string> getSniNameForSession() const {
+ return _session ? _session->getSniName() : boost::none;
+ }
+
transport::SessionHandle session() && {
return std::move(_session);
}
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);
diff --git a/src/mongo/embedded/replication_coordinator_embedded.cpp b/src/mongo/embedded/replication_coordinator_embedded.cpp
index 87b04673039..ccdf18229aa 100644
--- a/src/mongo/embedded/replication_coordinator_embedded.cpp
+++ b/src/mongo/embedded/replication_coordinator_embedded.cpp
@@ -304,7 +304,8 @@ Status ReplicationCoordinatorEmbedded::processReplSetGetStatus(BSONObjBuilder*,
UASSERT_NOT_IMPLEMENTED;
}
-void ReplicationCoordinatorEmbedded::fillIsMasterForReplSet(IsMasterResponse*) {
+void ReplicationCoordinatorEmbedded::fillIsMasterForReplSet(IsMasterResponse*,
+ const SplitHorizon::Parameters&) {
UASSERT_NOT_IMPLEMENTED;
}
diff --git a/src/mongo/embedded/replication_coordinator_embedded.h b/src/mongo/embedded/replication_coordinator_embedded.h
index 9fe6f6ae999..c68a7e8b7af 100644
--- a/src/mongo/embedded/replication_coordinator_embedded.h
+++ b/src/mongo/embedded/replication_coordinator_embedded.h
@@ -167,7 +167,8 @@ public:
Status processReplSetGetStatus(BSONObjBuilder*, ReplSetGetStatusResponseStyle) override;
- void fillIsMasterForReplSet(repl::IsMasterResponse*) override;
+ void fillIsMasterForReplSet(repl::IsMasterResponse*,
+ const repl::SplitHorizon::Parameters& horizon) override;
void appendSlaveInfoData(BSONObjBuilder*) override;
diff --git a/src/mongo/transport/session.h b/src/mongo/transport/session.h
index b2a9b2ba1a6..8924b1a9356 100644
--- a/src/mongo/transport/session.h
+++ b/src/mongo/transport/session.h
@@ -145,6 +145,10 @@ public:
virtual const HostAndPort& remote() const = 0;
virtual const HostAndPort& local() const = 0;
+ virtual boost::optional<std::string> getSniName() const {
+ return boost::none;
+ }
+
/**
* Atomically set all of the session tags specified in the 'tagsToSet' bit field. If the
* 'kPending' tag is set, indicating that no tags have yet been specified for the session, this
diff --git a/src/mongo/transport/session_asio.h b/src/mongo/transport/session_asio.h
index f2113dc29e5..43d307069bf 100644
--- a/src/mongo/transport/session_asio.h
+++ b/src/mongo/transport/session_asio.h
@@ -236,12 +236,10 @@ protected:
return doHandshake().then([this, target] {
_ranHandshake = true;
- auto swPeerInfo = uassertStatusOK(getSSLManager()->parseAndValidatePeerCertificate(
- _sslSocket->native_handle(), target.host(), target));
+ SSLPeerInfo::forSession(shared_from_this()) =
+ uassertStatusOK(getSSLManager()->parseAndValidatePeerCertificate(
+ _sslSocket->native_handle(), target.host(), target));
- if (swPeerInfo) {
- SSLPeerInfo::forSession(shared_from_this()) = std::move(*swPeerInfo);
- }
});
}
@@ -510,6 +508,10 @@ private:
return boost::none;
}
+
+ boost::optional<std::string> getSniName() const override {
+ return SSLPeerInfo::forSession(shared_from_this()).sniName;
+ }
#endif
template <typename Stream, typename ConstBufferSequence>
@@ -614,24 +616,8 @@ private:
auto& sslPeerInfo = SSLPeerInfo::forSession(shared_from_this());
if (sslPeerInfo.subjectName.empty()) {
- auto swPeerInfo = getSSLManager()->parseAndValidatePeerCertificate(
- _sslSocket->native_handle(), "", _remote);
-
- // The value of swPeerInfo is a bit complicated:
- //
- // If !swPeerInfo.isOK(), then there was an error doing the SSL
- // handshake and we should reject the connection.
- //
- // If !sslPeerInfo.getValue(), then the SSL handshake was successful,
- // but the peer didn't provide a SSL certificate, and we do not require
- // one. sslPeerInfo should be empty.
- //
- // Otherwise the SSL handshake was successful and the peer did provide
- // a certificate that is valid, and we should store that info on the
- // session's SSLPeerInfo decoration.
- if (auto optPeerInfo = uassertStatusOK(swPeerInfo)) {
- sslPeerInfo = *optPeerInfo;
- }
+ sslPeerInfo = uassertStatusOK(getSSLManager()->parseAndValidatePeerCertificate(
+ _sslSocket->native_handle(), "", _remote));
}
return true;
});
diff --git a/src/mongo/util/net/sock.h b/src/mongo/util/net/sock.h
index 8079ab00acb..561cd1cd9e4 100644
--- a/src/mongo/util/net/sock.h
+++ b/src/mongo/util/net/sock.h
@@ -78,7 +78,7 @@ typedef int SOCKET;
#endif // _WIN32
/**
- * thin wrapped around file descriptor and system calls
+ * thin wrapper around file descriptor and system calls
* todo: ssl
*/
class Socket {
diff --git a/src/mongo/util/net/ssl_manager.h b/src/mongo/util/net/ssl_manager.h
index de77d2951ae..4a58c388a66 100644
--- a/src/mongo/util/net/ssl_manager.h
+++ b/src/mongo/util/net/ssl_manager.h
@@ -237,12 +237,13 @@ public:
/**
* Fetches a peer certificate and validates it if it exists. If validation fails, but weak
- * validation is enabled, boost::none will be returned. If validation fails, and invalid
+ * validation is enabled, the `subjectName` will be empty. If validation fails, and invalid
* certificates are not allowed, a non-OK status will be returned. If validation is successful,
- * an engaged optional containing the certificate's subject name, and any roles acquired by
- * X509 authorization will be returned.
+ * the `subjectName` will contain the certificate's subject name, and any roles acquired by
+ * X509 authorization will be returned in `roles`.
+ * Further, the SNI Name will be captured into the `sniName` value, when available.
*/
- virtual StatusWith<boost::optional<SSLPeerInfo>> parseAndValidatePeerCertificate(
+ virtual StatusWith<SSLPeerInfo> parseAndValidatePeerCertificate(
SSLConnectionType ssl,
const std::string& remoteHost,
const HostAndPort& hostForLogging) = 0;
diff --git a/src/mongo/util/net/ssl_manager_apple.cpp b/src/mongo/util/net/ssl_manager_apple.cpp
index c8ad459d88a..9ba948100e9 100644
--- a/src/mongo/util/net/ssl_manager_apple.cpp
+++ b/src/mongo/util/net/ssl_manager_apple.cpp
@@ -1062,6 +1062,22 @@ std::string explainTrustFailure(::SecTrustRef trust, ::SecTrustResultType result
return ret("No trust failure reason available");
}
+boost::optional<std::string> getRawSNIServerName(::SSLContextRef _ssl) {
+ size_t len = 0;
+ auto status = ::SSLCopyRequestedPeerNameLength(_ssl, &len);
+ if (status != ::errSecSuccess) {
+ return boost::none;
+ }
+ std::string ret;
+ ret.resize(len + 1);
+ status = ::SSLCopyRequestedPeerName(_ssl, &ret[0], &len);
+ if (status != ::errSecSuccess) {
+ return boost::none;
+ }
+ ret.resize(len);
+ return ret;
+}
+
} // namespace
/////////////////////////////////////////////////////////////////////////////
@@ -1112,19 +1128,7 @@ public:
}
std::string getSNIServerName() const final {
- size_t len = 0;
- auto status = ::SSLCopyRequestedPeerNameLength(_ssl.get(), &len);
- if (status != ::errSecSuccess) {
- return "";
- }
- std::string ret;
- ret.resize(len + 1);
- status = ::SSLCopyRequestedPeerName(_ssl.get(), &ret[0], &len);
- if (status != ::errSecSuccess) {
- return "";
- }
- ret.resize(len);
- return ret;
+ return getRawSNIServerName(get()).value_or("");
}
::SSLContextRef get() const {
@@ -1210,7 +1214,7 @@ public:
const std::string& remoteHost,
const HostAndPort& hostForLogging) final;
- StatusWith<boost::optional<SSLPeerInfo>> parseAndValidatePeerCertificate(
+ StatusWith<SSLPeerInfo> parseAndValidatePeerCertificate(
::SSLContextRef conn,
const std::string& remoteHost,
const HostAndPort& hostForLogging) final;
@@ -1411,7 +1415,7 @@ SSLPeerInfo SSLManagerApple::parseAndValidatePeerCertificateDeprecated(
if (!swPeerSubjectName.isOK()) {
throwSocketError(SocketErrorKind::CONNECT_ERROR, swPeerSubjectName.getStatus().reason());
}
- return swPeerSubjectName.getValue().get_value_or(SSLPeerInfo());
+ return swPeerSubjectName.getValue();
}
StatusWith<TLSVersion> mapTLSVersion(SSLContextRef ssl) {
@@ -1432,8 +1436,9 @@ StatusWith<TLSVersion> mapTLSVersion(SSLContextRef ssl) {
}
-StatusWith<boost::optional<SSLPeerInfo>> SSLManagerApple::parseAndValidatePeerCertificate(
+StatusWith<SSLPeerInfo> SSLManagerApple::parseAndValidatePeerCertificate(
::SSLContextRef ssl, const std::string& remoteHost, const HostAndPort& hostForLogging) {
+ auto sniName = getRawSNIServerName(ssl);
// Record TLS version stats
auto tlsVersionStatus = mapTLSVersion(ssl);
@@ -1451,15 +1456,14 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerApple::parseAndValidatePeerCe
* so that the validation path runs anyway.
*/
if (!_sslConfiguration.hasCA && isSSLServer) {
- return {boost::none};
+ return SSLPeerInfo(sniName);
}
- const auto badCert = [](StringData msg,
- bool warn = false) -> StatusWith<boost::optional<SSLPeerInfo>> {
+ const auto badCert = [&](StringData msg, bool warn = false) -> StatusWith<SSLPeerInfo> {
constexpr StringData prefix = "SSL peer certificate validation failed: "_sd;
if (warn) {
warning() << prefix << msg;
- return {boost::none};
+ return SSLPeerInfo(sniName);
} else {
std::string m = str::stream() << prefix << msg << "; connection rejected";
error() << m;
@@ -1472,7 +1476,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerApple::parseAndValidatePeerCe
CFUniquePtr<::SecTrustRef> cftrust(trust);
if ((status != ::errSecSuccess) || (!cftrust)) {
if (_weakValidation && _suppressNoCertificateWarning) {
- return {boost::none};
+ return SSLPeerInfo(sniName);
} else {
if (status == ::errSecSuccess) {
return badCert(str::stream() << "no SSL certificate provided by peer: "
@@ -1563,8 +1567,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerApple::parseAndValidatePeerCe
if (!swPeerCertificateRoles.isOK()) {
return swPeerCertificateRoles.getStatus();
}
- return boost::make_optional(
- SSLPeerInfo(peerSubjectName, std::move(swPeerCertificateRoles.getValue())));
+ return SSLPeerInfo(peerSubjectName, sniName, std::move(swPeerCertificateRoles.getValue()));
}
// If this is an SSL client context (on a MongoDB server or client)
@@ -1633,7 +1636,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerApple::parseAndValidatePeerCe
}
}
- return boost::make_optional(SSLPeerInfo(peerSubjectName, stdx::unordered_set<RoleName>()));
+ return SSLPeerInfo(peerSubjectName);
}
int SSLManagerApple::SSL_read(SSLConnectionInterface* conn, void* buf, int num) {
diff --git a/src/mongo/util/net/ssl_manager_openssl.cpp b/src/mongo/util/net/ssl_manager_openssl.cpp
index a73c0180cc6..56c01f3460c 100644
--- a/src/mongo/util/net/ssl_manager_openssl.cpp
+++ b/src/mongo/util/net/ssl_manager_openssl.cpp
@@ -359,6 +359,14 @@ private:
std::vector<std::unique_ptr<stdx::recursive_mutex>> SSLThreadInfo::_mutex;
SSLThreadInfo::ThreadIDManager SSLThreadInfo::_idManager;
+boost::optional<std::string> getRawSNIServerName(const SSL* const ssl) {
+ const char* name = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+ if (!name) {
+ return boost::none;
+ }
+ return std::string(name);
+}
+
class SSLConnectionOpenSSL : public SSLConnectionInterface {
public:
SSL* ssl;
@@ -371,11 +379,7 @@ public:
~SSLConnectionOpenSSL();
std::string getSNIServerName() const final {
- const char* name = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
- if (!name)
- return "";
-
- return name;
+ return getRawSNIServerName(ssl).value_or("");
}
};
@@ -408,7 +412,7 @@ public:
const std::string& remoteHost,
const HostAndPort& hostForLogging) final;
- StatusWith<boost::optional<SSLPeerInfo>> parseAndValidatePeerCertificate(
+ StatusWith<SSLPeerInfo> parseAndValidatePeerCertificate(
SSL* conn, const std::string& remoteHost, const HostAndPort& hostForLogging) final;
const SSLConfiguration& getSSLConfiguration() const final {
@@ -1472,8 +1476,9 @@ StatusWith<TLSVersion> mapTLSVersion(SSL* conn) {
}
}
-StatusWith<boost::optional<SSLPeerInfo>> SSLManagerOpenSSL::parseAndValidatePeerCertificate(
+StatusWith<SSLPeerInfo> SSLManagerOpenSSL::parseAndValidatePeerCertificate(
SSL* conn, const std::string& remoteHost, const HostAndPort& hostForLogging) {
+ auto sniName = getRawSNIServerName(conn);
auto tlsVersionStatus = mapTLSVersion(conn);
if (!tlsVersionStatus.isOK()) {
@@ -1483,7 +1488,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerOpenSSL::parseAndValidatePeer
recordTLSVersion(tlsVersionStatus.getValue(), hostForLogging);
if (!_sslConfiguration.hasCA && isSSLServer)
- return {boost::none};
+ return SSLPeerInfo(std::move(sniName));
X509* peerCert = SSL_get_peer_certificate(conn);
@@ -1493,7 +1498,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerOpenSSL::parseAndValidatePeer
if (!_suppressNoCertificateWarning) {
warning() << "no SSL certificate provided by peer";
}
- return {boost::none};
+ return SSLPeerInfo(std::move(sniName));
} else {
auto msg = "no SSL certificate provided by peer; connection rejected";
error() << msg;
@@ -1508,7 +1513,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerOpenSSL::parseAndValidatePeer
if (_allowInvalidCertificates) {
warning() << "SSL peer certificate validation failed: "
<< X509_verify_cert_error_string(result);
- return {boost::none};
+ return SSLPeerInfo(std::move(sniName));
} else {
str::stream msg;
msg << "SSL peer certificate validation failed: "
@@ -1535,8 +1540,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerOpenSSL::parseAndValidatePeer
// If this is an SSL client context (on a MongoDB server or client)
// perform hostname validation of the remote server
if (remoteHost.empty()) {
- return boost::make_optional(
- SSLPeerInfo(peerSubject, std::move(swPeerCertificateRoles.getValue())));
+ return SSLPeerInfo(peerSubject, sniName, std::move(swPeerCertificateRoles.getValue()));
}
// This is to standardize the IPAddress format for comparison.
@@ -1636,7 +1640,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerOpenSSL::parseAndValidatePeer
}
}
- return boost::make_optional(SSLPeerInfo(peerSubject, stdx::unordered_set<RoleName>()));
+ return SSLPeerInfo(peerSubject);
}
@@ -1651,7 +1655,7 @@ SSLPeerInfo SSLManagerOpenSSL::parseAndValidatePeerCertificateDeprecated(
if (!swPeerSubjectName.isOK()) {
throwSocketError(SocketErrorKind::CONNECT_ERROR, swPeerSubjectName.getStatus().reason());
}
- return swPeerSubjectName.getValue().get_value_or(SSLPeerInfo());
+ return swPeerSubjectName.getValue();
}
StatusWith<stdx::unordered_set<RoleName>> SSLManagerOpenSSL::_parsePeerRoles(X509* peerCert) const {
diff --git a/src/mongo/util/net/ssl_manager_windows.cpp b/src/mongo/util/net/ssl_manager_windows.cpp
index 71c394da34b..79610013d7f 100644
--- a/src/mongo/util/net/ssl_manager_windows.cpp
+++ b/src/mongo/util/net/ssl_manager_windows.cpp
@@ -235,6 +235,12 @@ StatusWith<stdx::unordered_set<RoleName>> parsePeerRoles(PCCERT_CONTEXT cert) {
reinterpret_cast<char*>(extension->Value.pbData) + extension->Value.cbData));
}
+// TODO(SERVER-41045): If SNI functionality is needed on Windows, this is where one would implement
+// it.
+boost::optional<std::string> getSNIServerName_impl() {
+ return boost::none;
+}
+
/**
* Manage state for a SSL Connection. Used by the Socket class.
*/
@@ -251,8 +257,7 @@ public:
~SSLConnectionWindows();
std::string getSNIServerName() const final {
- // TODO
- return "";
+ return getSNIServerName_impl().value_or("");
};
};
@@ -277,7 +282,7 @@ public:
const std::string& remoteHost,
const HostAndPort& hostForLogging) final;
- StatusWith<boost::optional<SSLPeerInfo>> parseAndValidatePeerCertificate(
+ StatusWith<SSLPeerInfo> parseAndValidatePeerCertificate(
PCtxtHandle ssl, const std::string& remoteHost, const HostAndPort& hostForLogging) final;
@@ -1516,7 +1521,7 @@ SSLPeerInfo SSLManagerWindows::parseAndValidatePeerCertificateDeprecated(
throwSocketError(SocketErrorKind::CONNECT_ERROR, swPeerSubjectName.getStatus().reason());
}
- return swPeerSubjectName.getValue().get_value_or(SSLPeerInfo());
+ return swPeerSubjectName.getValue();
}
// Get a list of subject alternative names to assist the user in diagnosing certificate verification
@@ -1747,8 +1752,9 @@ StatusWith<TLSVersion> mapTLSVersion(PCtxtHandle ssl) {
}
}
-StatusWith<boost::optional<SSLPeerInfo>> SSLManagerWindows::parseAndValidatePeerCertificate(
+StatusWith<SSLPeerInfo> SSLManagerWindows::parseAndValidatePeerCertificate(
PCtxtHandle ssl, const std::string& remoteHost, const HostAndPort& hostForLogging) {
+ auto sniName = getSNIServerName_impl();
PCCERT_CONTEXT cert;
auto tlsVersionStatus = mapTLSVersion(ssl);
@@ -1759,7 +1765,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerWindows::parseAndValidatePeer
recordTLSVersion(tlsVersionStatus.getValue(), hostForLogging);
if (!_sslConfiguration.hasCA && isSSLServer)
- return {boost::none};
+ return SSLPeerInfo(sniName);
SECURITY_STATUS ss = QueryContextAttributes(ssl, SECPKG_ATTR_REMOTE_CERT_CONTEXT, &cert);
@@ -1769,7 +1775,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerWindows::parseAndValidatePeer
if (!_suppressNoCertificateWarning) {
warning() << "no SSL certificate provided by peer";
}
- return {boost::none};
+ return SSLPeerInfo(sniName);
} else {
auto msg = "no SSL certificate provided by peer; connection rejected";
error() << msg;
@@ -1811,7 +1817,7 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerWindows::parseAndValidatePeer
}
if (peerSubjectName.empty()) {
- return {boost::none};
+ return SSLPeerInfo(sniName);
}
LOG(2) << "Accepted TLS connection from peer: " << peerSubjectName;
@@ -1828,10 +1834,9 @@ StatusWith<boost::optional<SSLPeerInfo>> SSLManagerWindows::parseAndValidatePeer
return swPeerCertificateRoles.getStatus();
}
- return boost::make_optional(
- SSLPeerInfo(peerSubjectName, std::move(swPeerCertificateRoles.getValue())));
+ return SSLPeerInfo(peerSubjectName, sniName, std::move(swPeerCertificateRoles.getValue()));
} else {
- return boost::make_optional(SSLPeerInfo(peerSubjectName, stdx::unordered_set<RoleName>()));
+ return SSLPeerInfo(peerSubjectName);
}
}
diff --git a/src/mongo/util/net/ssl_types.cpp b/src/mongo/util/net/ssl_types.cpp
index 1e1af066a29..cc2f7e063ea 100644
--- a/src/mongo/util/net/ssl_types.cpp
+++ b/src/mongo/util/net/ssl_types.cpp
@@ -46,6 +46,10 @@ SSLPeerInfo& SSLPeerInfo::forSession(const transport::SessionHandle& session) {
return peerInfoForSession(session.get());
}
+const SSLPeerInfo& SSLPeerInfo::forSession(const transport::ConstSessionHandle& session) {
+ return peerInfoForSession(session.get());
+}
+
const SSLParams& getSSLGlobalParams() {
return sslGlobalParams;
}
diff --git a/src/mongo/util/net/ssl_types.h b/src/mongo/util/net/ssl_types.h
index cd2175df49e..238e8393e5a 100644
--- a/src/mongo/util/net/ssl_types.h
+++ b/src/mongo/util/net/ssl_types.h
@@ -115,14 +115,22 @@ inline bool operator<(const SSLX509Name::Entry& lhs, const SSLX509Name::Entry& r
* outside of the networking stack.
*/
struct SSLPeerInfo {
- SSLPeerInfo(SSLX509Name subjectName, stdx::unordered_set<RoleName> roles)
- : subjectName(std::move(subjectName)), roles(std::move(roles)) {}
+ explicit SSLPeerInfo(SSLX509Name subjectName,
+ boost::optional<std::string> sniName = {},
+ stdx::unordered_set<RoleName> roles = {})
+ : subjectName(std::move(subjectName)),
+ sniName(std::move(sniName)),
+ roles(std::move(roles)) {}
SSLPeerInfo() = default;
+ explicit SSLPeerInfo(boost::optional<std::string> sniName) : sniName(std::move(sniName)) {}
+
SSLX509Name subjectName;
+ boost::optional<std::string> sniName;
stdx::unordered_set<RoleName> roles;
static SSLPeerInfo& forSession(const transport::SessionHandle& session);
+ static const SSLPeerInfo& forSession(const transport::ConstSessionHandle& session);
};
} // namespace mongo