/**
* Copyright (C) 2012-2015 MongoDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects for
* all of the code used other than as permitted herein. If you modify file(s)
* with this exception, you may extend this exception to your version of the
* file(s), but you are not obligated to do so. If you do not wish to do so,
* delete this exception statement from your version. If you delete this
* exception statement from all source files in the program, then also delete
* it in the license file.
*/
#include "mongo/platform/basic.h"
#include "mongo/client/replica_set_monitor.h"
#include "mongo/client/replica_set_monitor_internal.h"
#include "mongo/unittest/unittest.h"
namespace {
using namespace mongo;
using std::set;
// Pull nested types to top-level scope
typedef ReplicaSetMonitor::IsMasterReply IsMasterReply;
typedef ReplicaSetMonitor::SetState SetState;
typedef ReplicaSetMonitor::SetStatePtr SetStatePtr;
typedef ReplicaSetMonitor::Refresher Refresher;
typedef Refresher::NextStep NextStep;
typedef SetState::Node Node;
typedef SetState::Nodes Nodes;
std::vector basicSeedsBuilder() {
std::vector out;
out.push_back(HostAndPort("a"));
out.push_back(HostAndPort("b"));
out.push_back(HostAndPort("c"));
return out;
}
const std::vector basicSeeds = basicSeedsBuilder();
const std::set basicSeedsSet(basicSeeds.begin(), basicSeeds.end());
// NOTE: Unless stated otherwise, all tests assume exclusive access to state belongs to the
// current (only) thread, so they do not lock SetState::mutex before examining state. This is
// NOT something that non-test code should do.
TEST(ReplicaSetMonitor, InitialState) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
ASSERT_EQUALS(state->name, "name");
ASSERT(state->seedNodes == basicSeedsSet);
ASSERT(state->lastSeenMaster.empty());
ASSERT_EQUALS(state->nodes.size(), basicSeeds.size());
for (size_t i = 0; i < basicSeeds.size(); i++) {
Node* node = state->findNode(basicSeeds[i]);
ASSERT(node);
ASSERT_EQUALS(node->host.toString(), basicSeeds[i].toString());
ASSERT(!node->isUp);
ASSERT(!node->isMaster);
ASSERT(node->tags.isEmpty());
}
}
TEST(ReplicaSetMonitor, IsMasterBadParse) {
BSONObj ismaster = BSON("hosts" << BSON_ARRAY("mongo.example:badport"));
IsMasterReply imr(HostAndPort("mongo.example:27017"), -1, ismaster);
ASSERT_EQUALS(imr.ok, false);
}
TEST(ReplicaSetMonitor, IsMasterReplyRSNotInitiated) {
BSONObj ismaster = BSON(
"ismaster" << false
<< "secondary" << false
<< "info" << "can't get local.system.replset config from self or any seed (EMPTYCONFIG)"
<< "isreplicaset" << true
<< "maxBsonObjectSize" << 16777216
<< "maxMessageSizeBytes" << 48000000
<< "maxWriteBatchSize" << 1000
<< "localTime" << mongo::jsTime()
<< "maxWireVersion" << 2
<< "minWireVersion" << 0
<< "ok" << 1
);
IsMasterReply imr(HostAndPort(), -1, ismaster);
ASSERT_EQUALS(imr.ok, true);
ASSERT_EQUALS(imr.setName, "");
ASSERT_EQUALS(imr.hidden, false);
ASSERT_EQUALS(imr.secondary, false);
ASSERT_EQUALS(imr.isMaster, false);
ASSERT(imr.primary.empty());
ASSERT(imr.normalHosts.empty());
ASSERT(imr.tags.isEmpty());
}
TEST(ReplicaSetMonitor, IsMasterReplyRSPrimary) {
BSONObj ismaster = BSON(
"setName" << "test"
<< "setVersion" << 1
<< "ismaster" << true
<< "secondary" << false
<< "hosts" << BSON_ARRAY("mongo.example:3000")
<< "primary" << "mongo.example:3000"
<< "me" << "mongo.example:3000"
<< "maxBsonObjectSize" << 16777216
<< "maxMessageSizeBytes" << 48000000
<< "maxWriteBatchSize" << 1000
<< "localTime" << mongo::jsTime()
<< "maxWireVersion" << 2
<< "minWireVersion" << 0
<< "ok" << 1
);
IsMasterReply imr(HostAndPort("mongo.example:3000"), -1, ismaster);
ASSERT_EQUALS(imr.ok, true);
ASSERT_EQUALS(imr.host.toString(), HostAndPort("mongo.example:3000").toString());
ASSERT_EQUALS(imr.setName, "test");
ASSERT_EQUALS(imr.hidden, false);
ASSERT_EQUALS(imr.secondary, false);
ASSERT_EQUALS(imr.isMaster, true);
ASSERT_EQUALS(imr.primary.toString(), HostAndPort("mongo.example:3000").toString());
ASSERT(imr.normalHosts.count(HostAndPort("mongo.example:3000")));
ASSERT(imr.tags.isEmpty());
}
TEST(ReplicaSetMonitor, IsMasterReplyPassiveSecondary) {
BSONObj ismaster = BSON(
"setName" << "test"
<< "setVersion" << 1
<< "ismaster" << false
<< "secondary" << true
<< "hosts" << BSON_ARRAY("mongo.example:3000")
<< "passives" << BSON_ARRAY("mongo.example:3001")
<< "primary" << "mongo.example:3000"
<< "passive" << true
<< "me" << "mongo.example:3001"
<< "maxBsonObjectSize" << 16777216
<< "maxMessageSizeBytes" << 48000000
<< "maxWriteBatchSize" << 1000
<< "localTime" << mongo::jsTime()
<< "maxWireVersion" << 2
<< "minWireVersion" << 0
<< "ok" << 1
);
IsMasterReply imr(HostAndPort("mongo.example:3001"), -1, ismaster);
ASSERT_EQUALS(imr.ok, true);
ASSERT_EQUALS(imr.host.toString(), HostAndPort("mongo.example:3001").toString());
ASSERT_EQUALS(imr.setName, "test");
ASSERT_EQUALS(imr.hidden, false);
ASSERT_EQUALS(imr.secondary, true);
ASSERT_EQUALS(imr.isMaster, false);
ASSERT_EQUALS(imr.primary.toString(), HostAndPort("mongo.example:3000").toString());
ASSERT(imr.normalHosts.count(HostAndPort("mongo.example:3000")));
ASSERT(imr.normalHosts.count(HostAndPort("mongo.example:3001")));
ASSERT(imr.tags.isEmpty());
}
TEST(ReplicaSetMonitor, IsMasterReplyHiddenSecondary) {
BSONObj ismaster = BSON(
"setName" << "test"
<< "setVersion" << 1
<< "ismaster" << false
<< "secondary" << true
<< "hosts" << BSON_ARRAY("mongo.example:3000")
<< "primary" << "mongo.example:3000"
<< "passive" << true
<< "hidden" << true
<< "me" << "mongo.example:3001"
<< "maxBsonObjectSize" << 16777216
<< "maxMessageSizeBytes" << 48000000
<< "maxWriteBatchSize" << 1000
<< "localTime" << mongo::jsTime()
<< "maxWireVersion" << 2
<< "minWireVersion" << 0
<< "ok" << 1
);
IsMasterReply imr(HostAndPort("mongo.example:3001"), -1, ismaster);
ASSERT_EQUALS(imr.ok, true);
ASSERT_EQUALS(imr.host.toString(), HostAndPort("mongo.example:3001").toString());
ASSERT_EQUALS(imr.setName, "test");
ASSERT_EQUALS(imr.hidden, true);
ASSERT_EQUALS(imr.secondary, true);
ASSERT_EQUALS(imr.isMaster, false);
ASSERT_EQUALS(imr.primary.toString(), HostAndPort("mongo.example:3000").toString());
ASSERT(imr.normalHosts.count(HostAndPort("mongo.example:3000")));
ASSERT(imr.tags.isEmpty());
}
TEST(ReplicaSetMonitor, IsMasterSecondaryWithTags) {
BSONObj ismaster = BSON(
"setName" << "test"
<< "setVersion" << 1
<< "ismaster" << false
<< "secondary" << true
<< "hosts" << BSON_ARRAY("mongo.example:3000" << "mongo.example:3001")
<< "primary" << "mongo.example:3000"
<< "me" << "mongo.example:3001"
<< "maxBsonObjectSize" << 16777216
<< "maxMessageSizeBytes" << 48000000
<< "maxWriteBatchSize" << 1000
<< "localTime" << mongo::jsTime()
<< "maxWireVersion" << 2
<< "minWireVersion" << 0
<< "tags" << BSON("dc" << "nyc" << "use" << "production")
<< "ok" << 1
);
IsMasterReply imr(HostAndPort("mongo.example:3001"), -1, ismaster);
ASSERT_EQUALS(imr.ok, true);
ASSERT_EQUALS(imr.host.toString(), HostAndPort("mongo.example:3001").toString());
ASSERT_EQUALS(imr.setName, "test");
ASSERT_EQUALS(imr.hidden, false);
ASSERT_EQUALS(imr.secondary, true);
ASSERT_EQUALS(imr.isMaster, false);
ASSERT_EQUALS(imr.primary.toString(), HostAndPort("mongo.example:3000").toString());
ASSERT(imr.normalHosts.count(HostAndPort("mongo.example:3000")));
ASSERT(imr.normalHosts.count(HostAndPort("mongo.example:3001")));
ASSERT(imr.tags.hasElement("dc"));
ASSERT(imr.tags.hasElement("use"));
ASSERT_EQUALS(imr.tags["dc"].str(), "nyc");
ASSERT_EQUALS(imr.tags["use"].str(), "production");
}
TEST(ReplicaSetMonitor, CheckAllSeedsSerial) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
set seen;
for (size_t i = 0; i < basicSeeds.size(); i++) {
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
// mock a reply
bool primary = ns.host.host() == "a";
refresher.receivedIsMaster(ns.host, -1, BSON(
"setName" << "name"
<< "ismaster" << primary
<< "secondary" << !primary
<< "hosts" << BSON_ARRAY("a" << "b" << "c")
<< "ok" << true
));
}
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::DONE);
ASSERT(ns.host.empty());
// validate final state
ASSERT_EQUALS(state->nodes.size(), basicSeeds.size());
for (size_t i = 0; i < basicSeeds.size(); i++) {
Node* node = state->findNode(basicSeeds[i]);
ASSERT(node);
ASSERT_EQUALS(node->host.toString(), basicSeeds[i].toString());
ASSERT(node->isUp);
ASSERT_EQUALS(node->isMaster, node->host.host() == "a");
ASSERT(node->tags.isEmpty());
}
}
TEST(ReplicaSetMonitor, CheckAllSeedsParallel) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
set seen;
// get all hosts to contact first
for (size_t i = 0; i < basicSeeds.size(); i++) {
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
}
// mock all replies
for (size_t i = 0; i < basicSeeds.size(); i++) {
// All hosts to talk to are already dispatched, but no reply has been received
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::WAIT);
ASSERT(ns.host.empty());
bool primary = i == 0;
refresher.receivedIsMaster(basicSeeds[i], -1, BSON(
"setName" << "name"
<< "ismaster" << primary
<< "secondary" << !primary
<< "hosts" << BSON_ARRAY("a" << "b" << "c")
<< "ok" << true
));
}
// Now all hosts have returned data
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::DONE);
ASSERT(ns.host.empty());
// validate final state
ASSERT_EQUALS(state->nodes.size(), basicSeeds.size());
for (size_t i = 0; i < basicSeeds.size(); i++) {
Node* node = state->findNode(basicSeeds[i]);
ASSERT(node);
ASSERT_EQUALS(node->host.toString(), basicSeeds[i].toString());
ASSERT(node->isUp);
ASSERT_EQUALS(node->isMaster, i == 0);
ASSERT(node->tags.isEmpty());
}
}
TEST(ReplicaSetMonitor, NoMasterInitAllUp) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
set seen;
for (size_t i = 0; i < basicSeeds.size(); i++) {
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
// mock a reply
refresher.receivedIsMaster(ns.host, -1, BSON(
"setName" << "name"
<< "ismaster" << false
<< "secondary" << true
<< "hosts" << BSON_ARRAY("a" << "b" << "c")
<< "ok" << true
));
}
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::DONE);
ASSERT(ns.host.empty());
// validate final state
ASSERT_EQUALS(state->nodes.size(), basicSeeds.size());
for (size_t i = 0; i < basicSeeds.size(); i++) {
Node* node = state->findNode(basicSeeds[i]);
ASSERT(node);
ASSERT_EQUALS(node->host.toString(), basicSeeds[i].toString());
ASSERT(node->isUp);
ASSERT_EQUALS(node->isMaster, false);
ASSERT(node->tags.isEmpty());
}
}
TEST(ReplicaSetMonitor, MasterNotInSeeds_NoPrimaryInIsMaster) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
set seen;
for (size_t i = 0; i < basicSeeds.size(); i++) {
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
// mock a reply
refresher.receivedIsMaster(ns.host,
-1,
BSON("setName" << "name" <<
"ismaster" << false <<
"secondary" << true <<
"hosts" << BSON_ARRAY("a" << "b" << "c" << "d") <<
"ok" << true));
}
// Only look at "d" after exhausting all other hosts
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT_EQUALS(ns.host.host(), "d");
refresher.receivedIsMaster(ns.host, -1, BSON(
"setName" << "name"
<< "ismaster" << true
<< "secondary" << false
<< "hosts" << BSON_ARRAY("a" << "b" << "c" << "d")
<< "ok" << true
));
ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::DONE);
ASSERT(ns.host.empty());
// validate final state
ASSERT_EQUALS(state->nodes.size(), basicSeeds.size() + 1);
for (size_t i = 0; i < basicSeeds.size(); i++) {
Node* node = state->findNode(basicSeeds[i]);
ASSERT(node);
ASSERT_EQUALS(node->host.toString(), basicSeeds[i].toString());
ASSERT(node->isUp);
ASSERT_EQUALS(node->isMaster, false);
ASSERT(node->tags.isEmpty());
}
Node* node = state->findNode(HostAndPort("d"));
ASSERT(node);
ASSERT_EQUALS(node->host.host(), "d");
ASSERT(node->isUp);
ASSERT_EQUALS(node->isMaster, true);
ASSERT(node->tags.isEmpty());
}
TEST(ReplicaSetMonitor, MasterNotInSeeds_PrimaryInIsMaster) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
set seen;
for (size_t i = 0; i < basicSeeds.size() + 1; i++) {
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
if (i == 1) { // d should be the second host we contact since we are told it is primary
ASSERT_EQUALS(ns.host.host(), "d");
}
else {
ASSERT(basicSeedsSet.count(ns.host));
}
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
// mock a reply
bool primary = ns.host.host() == "d";
refresher.receivedIsMaster(ns.host,
-1,
BSON("setName" << "name" <<
"ismaster" << primary <<
"secondary" << !primary <<
"primary" << "d" <<
"hosts" << BSON_ARRAY("a" << "b" << "c" << "d") <<
"ok" << true));
}
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::DONE);
ASSERT(ns.host.empty());
// validate final state
ASSERT_EQUALS(state->nodes.size(), basicSeeds.size() + 1);
for (size_t i = 0; i < basicSeeds.size(); i++) {
Node* node = state->findNode(basicSeeds[i]);
ASSERT(node);
ASSERT_EQUALS(node->host.toString(), basicSeeds[i].toString());
ASSERT(node->isUp);
ASSERT_EQUALS(node->isMaster, false);
ASSERT(node->tags.isEmpty());
}
Node* node = state->findNode(HostAndPort("d"));
ASSERT(node);
ASSERT_EQUALS(node->host.host(), "d");
ASSERT(node->isUp);
ASSERT_EQUALS(node->isMaster, true);
ASSERT(node->tags.isEmpty());
}
// Make sure we can use slaves we find even if we can't find a primary
TEST(ReplicaSetMonitor, SlavesUsableEvenIfNoMaster) {
std::set seeds;
seeds.insert(HostAndPort("a"));
SetStatePtr state = std::make_shared("name", seeds);
Refresher refresher(state);
const ReadPreferenceSetting secondary(ReadPreference::SecondaryOnly, TagSet());
// Mock a reply from the only host we know about and have it claim to not be master or know
// about any other hosts. This leaves the scan with no more hosts to scan, but all hosts are
// still marked as down since we never contacted a master. The next call to
// Refresher::getNextStep will apply all unconfimedReplies and return DONE.
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT_EQUALS(ns.host.host(), "a");
refresher.receivedIsMaster(ns.host, -1, BSON(
"setName" << "name"
<< "ismaster" << false
<< "secondary" << true
<< "hosts" << BSON_ARRAY("a")
<< "ok" << true
));
// Check intended conditions for entry to refreshUntilMatches.
ASSERT(state->currentScan->hostsToScan.empty());
ASSERT(state->currentScan->waitingFor.empty());
ASSERT(state->currentScan->possibleNodes == state->currentScan->triedHosts);
ASSERT(state->getMatchingHost(secondary).empty());
// This calls getNextStep after not finding a matching host. We want to ensure that it checks
// again after being told that there are no more hosts to contact.
ASSERT(!refresher.refreshUntilMatches(secondary).empty());
// Future calls should be able to return directly from the cached data.
ASSERT(!state->getMatchingHost(secondary).empty());
}
// Test multiple nodes that claim to be master (we use a last-wins policy)
TEST(ReplicaSetMonitor, MultipleMasterLastNodeWins) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
set seen;
// get all hosts to contact first
for (size_t i = 0; i != basicSeeds.size(); ++i) {
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
}
const ReadPreferenceSetting primaryOnly(ReadPreference::PrimaryOnly, TagSet());
// mock all replies
for (size_t i = 0; i != basicSeeds.size(); ++i) {
// All hosts to talk to are already dispatched, but no reply has been received
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::WAIT);
ASSERT(ns.host.empty());
refresher.receivedIsMaster(basicSeeds[i], -1, BSON(
"setName" << "name"
<< "ismaster" << true
<< "secondary" << false
<< "hosts" << BSON_ARRAY("a" << "b" << "c")
<< "ok" << true
));
// Ensure the set primary is the host we just got a reply from
HostAndPort currentPrimary = state->getMatchingHost(primaryOnly);
ASSERT_EQUALS(currentPrimary.host(), basicSeeds[i].host());
ASSERT_EQUALS(state->nodes.size(), basicSeeds.size());
// Check the state of each individual node
for (size_t j = 0; j != basicSeeds.size(); ++j) {
Node* node = state->findNode(basicSeeds[j]);
ASSERT(node);
ASSERT_EQUALS(node->host.toString(), basicSeeds[j].toString());
ASSERT_EQUALS(node->isUp, j <= i);
ASSERT_EQUALS(node->isMaster, j == i);
ASSERT(node->tags.isEmpty());
}
}
// Now all hosts have returned data
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::DONE);
ASSERT(ns.host.empty());
}
// Test nodes disagree about who is in the set, master is source of truth
TEST(ReplicaSetMonitor, MasterIsSourceOfTruth) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
BSONArray primaryHosts = BSON_ARRAY("a" << "b" << "d");
BSONArray secondaryHosts = BSON_ARRAY("a" << "b" << "c");
// mock all replies
NextStep ns = refresher.getNextStep();
while (ns.step == NextStep::CONTACT_HOST) {
bool primary = ns.host.host() == "a";
refresher.receivedIsMaster(ns.host, -1, BSON(
"setName" << "name"
<< "ismaster" << primary
<< "secondary" << !primary
<< "hosts" << (primary ? primaryHosts : secondaryHosts)
<< "ok" << true
));
ns = refresher.getNextStep();
}
// Ensure that we have heard from all hosts and scan is done
ASSERT_EQUALS(ns.step, NextStep::DONE);
// Ensure that d is in the set but c is not
ASSERT(state->findNode(HostAndPort("d")));
ASSERT(!state->findNode(HostAndPort("c")));
}
// Test multiple master nodes that disagree about set membership
TEST(ReplicaSetMonitor, MultipleMastersDisagree) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
BSONArray hostsForSeed[3];
hostsForSeed[0] = BSON_ARRAY("a" << "b" << "c" << "d");
hostsForSeed[1] = BSON_ARRAY("a" << "b" << "c" << "e");
hostsForSeed[2] = hostsForSeed[0];
set seen;
for (size_t i = 0; i != basicSeeds.size(); ++i) {
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
}
const ReadPreferenceSetting primaryOnly(ReadPreference::PrimaryOnly, TagSet());
// mock all replies
for (size_t i = 0; i != basicSeeds.size(); ++i) {
refresher.receivedIsMaster(basicSeeds[i], -1, BSON(
"setName" << "name"
<< "ismaster" << true
<< "secondary" << false
<< "hosts" << hostsForSeed[i % 2]
<< "ok" << true
));
// Ensure the primary is the host we just got a reply from
HostAndPort currentPrimary = state->getMatchingHost(primaryOnly);
ASSERT_EQUALS(currentPrimary.host(), basicSeeds[i].host());
// Ensure each primary discovered becomes source of truth
if (i == 1) {
// "b" thinks node "e" is a member but "d" is not
ASSERT(state->findNode(HostAndPort("e")));
ASSERT(!state->findNode(HostAndPort("d")));
}
else {
// "a" and "c" think node "d" is a member but "e" is not
ASSERT(state->findNode(HostAndPort("d")));
ASSERT(!state->findNode(HostAndPort("e")));
}
}
// next step should be to contact "d"
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT_EQUALS(ns.host.host(), "d");
seen.insert(ns.host);
// reply from "d"
refresher.receivedIsMaster(HostAndPort("d"), -1, BSON(
"setName" << "name"
<< "ismaster" << false
<< "secondary" << true
<< "hosts" << hostsForSeed[0]
<< "ok" << true
));
// scan should be complete
ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::DONE);
ASSERT(ns.host.empty());
// Validate final state (only "c" should be master and "d" was added)
ASSERT_EQUALS(state->nodes.size(), basicSeeds.size() + 1);
std::vector nodes = state->nodes;
for (std::vector::const_iterator it = nodes.begin(); it != nodes.end(); ++it) {
const Node& node = *it;
ASSERT(node.isUp);
ASSERT_EQUALS(node.isMaster, node.host.host() == "c");
ASSERT(seen.count(node.host));
}
}
// Ensure getMatchingHost returns hosts even if scan is ongoing
TEST(ReplicaSetMonitor, GetMatchingDuringScan) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
const ReadPreferenceSetting primaryOnly(ReadPreference::PrimaryOnly, TagSet());
const ReadPreferenceSetting secondaryOnly(ReadPreference::SecondaryOnly, TagSet());
for (std::vector::const_iterator it = basicSeeds.begin(); it != basicSeeds.end();
++it) {
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(state->getMatchingHost(primaryOnly).empty());
ASSERT(state->getMatchingHost(secondaryOnly).empty());
}
// mock replies and validate set state as replies come back
for (size_t i = 0; i != basicSeeds.size(); ++i) {
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::WAIT);
ASSERT(ns.host.empty());
bool primary = (i == 1);
refresher.receivedIsMaster(basicSeeds[i], -1, BSON(
"setName" << "name"
<< "ismaster" << primary
<< "secondary" << !primary
<< "hosts" << BSON_ARRAY("a" << "b" << "c")
<< "ok" << true
));
bool hasPrimary = !(state->getMatchingHost(primaryOnly).empty());
bool hasSecondary = !(state->getMatchingHost(secondaryOnly).empty());
// secondary node has not been confirmed by primary until i == 1
if (i >= 1) {
ASSERT(hasPrimary);
ASSERT(hasSecondary);
}
else {
ASSERT(!hasPrimary);
ASSERT(!hasSecondary);
}
}
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::DONE);
ASSERT(ns.host.empty());
}
// Ensure nothing breaks when out-of-band failedHost is called during scan
TEST(ReplicaSetMonitor, OutOfBandFailedHost) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
ReplicaSetMonitorPtr rsm = std::make_shared(state);
Refresher refresher = rsm->startOrContinueRefresh();
for (size_t i = 0; i != basicSeeds.size(); ++i) {
NextStep ns = refresher.getNextStep();
}
for (size_t i = 0; i != basicSeeds.size(); ++i) {
bool primary = (i == 0);
refresher.receivedIsMaster(basicSeeds[i], -1, BSON(
"setName" << "name"
<< "ismaster" << primary
<< "secondary" << !primary
<< "hosts" << BSON_ARRAY("a" << "b" << "c")
<< "ok" << true
));
if (i >= 1) {
HostAndPort a("a");
rsm->failedHost(a);
Node* node = state->findNode(a);
ASSERT(node);
ASSERT(!node->isUp);
ASSERT(!node->isMaster);
}
else {
Node* node = state->findNode(HostAndPort("a"));
ASSERT(node);
ASSERT(node->isUp);
ASSERT(node->isMaster);
}
}
}
// Newly elected primary with electionId >= maximum electionId seen by the Refresher
TEST(ReplicaSetMonitorTests, NewPrimaryWithMaxElectionId) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
set seen;
// get all hosts to contact first
for (size_t i = 0; i != basicSeeds.size(); ++i) {
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
}
const ReadPreferenceSetting primaryOnly(ReadPreference::PrimaryOnly, TagSet());
// mock all replies
for (size_t i = 0; i != basicSeeds.size(); ++i) {
// All hosts to talk to are already dispatched, but no reply has been received
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::WAIT);
ASSERT(ns.host.empty());
refresher.receivedIsMaster(basicSeeds[i],
-1,
BSON("setName" << "name"
<< "ismaster" << true
<< "secondary" << false
<< "hosts" << BSON_ARRAY("a" << "b" << "c")
<< "electionId" << OID::gen()
<< "ok" << true));
// Ensure the set primary is the host we just got a reply from
HostAndPort currentPrimary = state->getMatchingHost(primaryOnly);
ASSERT_EQUALS(currentPrimary.host(), basicSeeds[i].host());
ASSERT_EQUALS(state->nodes.size(), basicSeeds.size());
// Check the state of each individual node
for (size_t j = 0; j != basicSeeds.size(); ++j) {
Node* node = state->findNode(basicSeeds[j]);
ASSERT(node);
ASSERT_EQUALS(node->host.toString(), basicSeeds[j].toString());
ASSERT_EQUALS(node->isUp, j <= i);
ASSERT_EQUALS(node->isMaster, j == i);
ASSERT(node->tags.isEmpty());
}
}
// Now all hosts have returned data
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::DONE);
ASSERT(ns.host.empty());
}
// Ignore electionId of secondaries
TEST(ReplicaSetMonitorTests, IgnoreElectionIdFromSecondaries) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
set seen;
const OID primaryElectionId = OID::gen();
// mock all replies
for (size_t i = 0; i != basicSeeds.size(); ++i) {
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
// mock a reply
const bool primary = ns.host.host() == "a";
refresher.receivedIsMaster(ns.host,
-1,
BSON("setName" << "name"
<< "ismaster" << primary
<< "secondary" << !primary
<< "electionId" << (primary ?
primaryElectionId : OID::gen())
<< "hosts" << BSON_ARRAY("a" << "b" << "c")
<< "ok" << true));
}
// check that the SetState's maxElectionId == primary's electionId
ASSERT_EQUALS(state->maxElectionId, primaryElectionId);
// Now all hosts have returned data
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::DONE);
ASSERT(ns.host.empty());
}
// Stale Primary with obsolete electionId
TEST(ReplicaSetMonitorTests, StalePrimaryWithObsoleteElectionId) {
SetStatePtr state = std::make_shared("name", basicSeedsSet);
Refresher refresher(state);
const OID firstElectionId = OID::gen();
const OID secondElectionId = OID::gen();
set seen;
// contact first host claiming to be primary with greater electionId
{
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
refresher.receivedIsMaster(ns.host,
-1,
BSON("setName" << "name"
<< "ismaster" << true
<< "secondary" << false
<< "electionId" << secondElectionId
<< "hosts" << BSON_ARRAY("a" << "b" << "c")
<< "ok" << true));
Node* node = state->findNode(ns.host);
ASSERT(node);
ASSERT_TRUE(node->isMaster);
ASSERT_EQUALS(state->maxElectionId, secondElectionId);
}
// contact second host claiming to be primary with smaller electionId
{
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
refresher.receivedIsMaster(ns.host,
-1,
BSON("setName" << "name"
<< "ismaster" << true
<< "secondary" << false
<< "electionId" << firstElectionId
<< "hosts" << BSON_ARRAY("a" << "b" << "c")
<< "ok" << true));
Node* node = state->findNode(ns.host);
ASSERT(node);
// The SetState shouldn't see this host as master
ASSERT_FALSE(node->isMaster);
// the max electionId should remain the same
ASSERT_EQUALS(state->maxElectionId, secondElectionId);
}
// third host is a secondary
{
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::CONTACT_HOST);
ASSERT(basicSeedsSet.count(ns.host));
ASSERT(!seen.count(ns.host));
seen.insert(ns.host);
refresher.receivedIsMaster(ns.host,
-1,
BSON("setName" << "name"
<< "ismaster" << false
<< "secondary" << true
<< "hosts" << BSON_ARRAY("a" << "b" << "c")
<< "ok" << true));
Node* node = state->findNode(ns.host);
ASSERT(node);
ASSERT_FALSE(node->isMaster);
// the max electionId should remain the same
ASSERT_EQUALS(state->maxElectionId, secondElectionId);
}
// Now all hosts have returned data
NextStep ns = refresher.getNextStep();
ASSERT_EQUALS(ns.step, NextStep::DONE);
ASSERT(ns.host.empty());
}
} // namespace