diff options
author | Siyuan Zhou <siyuan.zhou@mongodb.com> | 2020-07-13 03:58:12 -0400 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2020-11-03 23:06:07 +0000 |
commit | b907dabad7724cce00695d74cdd0823756b24d05 (patch) | |
tree | aa852dbfdafa8997fcb74b80d52248d182822367 | |
parent | 3a62d9a1f02e5d7e83285712584b97d941882f70 (diff) | |
download | mongo-b907dabad7724cce00695d74cdd0823756b24d05.tar.gz |
SERVER-50154 Add declarative mock network framework for unit testing.
-rw-r--r-- | src/mongo/db/repl/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/repl/initial_syncer_test.cpp | 97 | ||||
-rw-r--r-- | src/mongo/db/repl/mock_fixture.cpp | 135 | ||||
-rw-r--r-- | src/mongo/db/repl/mock_fixture.h | 155 | ||||
-rw-r--r-- | src/mongo/db/repl/replication_coordinator_impl_elect_v1_test.cpp | 74 | ||||
-rw-r--r-- | src/mongo/executor/network_interface_mock.cpp | 12 | ||||
-rw-r--r-- | src/mongo/executor/network_interface_mock.h | 6 |
7 files changed, 480 insertions, 0 deletions
diff --git a/src/mongo/db/repl/SConscript b/src/mongo/db/repl/SConscript index 10f68f9bad4..b03690d1ba3 100644 --- a/src/mongo/db/repl/SConscript +++ b/src/mongo/db/repl/SConscript @@ -785,6 +785,7 @@ env.Library( env.Library( target='replmocks', source=[ + 'mock_fixture.cpp', 'replication_consistency_markers_mock.cpp', 'replication_coordinator_external_state_mock.cpp', 'replication_coordinator_mock.cpp', diff --git a/src/mongo/db/repl/initial_syncer_test.cpp b/src/mongo/db/repl/initial_syncer_test.cpp index fd33e1d9899..077f346373f 100644 --- a/src/mongo/db/repl/initial_syncer_test.cpp +++ b/src/mongo/db/repl/initial_syncer_test.cpp @@ -46,6 +46,7 @@ #include "mongo/db/repl/data_replicator_external_state_mock.h" #include "mongo/db/repl/initial_syncer.h" #include "mongo/db/repl/member_state.h" +#include "mongo/db/repl/mock_fixture.h" #include "mongo/db/repl/oplog_entry.h" #include "mongo/db/repl/oplog_fetcher.h" #include "mongo/db/repl/oplog_fetcher_mock.h" @@ -106,6 +107,7 @@ namespace { using namespace mongo; using namespace mongo::repl; +using namespace mongo::test::mock; using executor::NetworkInterfaceMock; using executor::RemoteCommandRequest; @@ -1713,6 +1715,42 @@ TEST_F(InitialSyncerTest, InitialSyncerPassesThroughFCVFetcherScheduleError) { assertFCVRequest(request); } +// This is to demonstrate the unit testing mock framework. The logic is the same as the following +// test. +TEST_F(InitialSyncerTest, InitialSyncerPassesThroughFCVFetcherCallbackError_Mock) { + auto initialSyncer = &getInitialSyncer(); + auto opCtx = makeOpCtx(); + + _syncSourceSelector->setChooseNewSyncSourceResult_forTest(HostAndPort("localhost", 12345)); + + MockNetwork mock(getNet()); + + // Set up default behavior. + mock.expect("replSetGetRBID", makeRollbackCheckerResponse(1)); + + mock.expect([](auto& request) { return request["find"].str() == "oplog.rs"; }, + makeCursorResponse(0LL, _options.localOplogNS, {makeOplogEntryObj(1)})); + + mock.expect( + [](auto& request) { return request["find"].str() == "transactions"; }, + makeCursorResponse(0LL, NamespaceString::kSessionTransactionsTableNamespace, {}, true)); + + // This is what we want to test. + mock.expect([](auto& request) { return request["find"].str() == "system.version"; }, + RemoteCommandResponse(ErrorCodes::OperationFailed, + "find command failed at sync source")) + .times(1); + + // Start the real work. + ASSERT_OK(initialSyncer->startup(opCtx.get(), maxAttempts)); + + // Run mock. + mock.runUntilExpectationsSatisfied(); + + initialSyncer->join(); + ASSERT_EQUALS(ErrorCodes::OperationFailed, _lastApplied); +} + TEST_F(InitialSyncerTest, InitialSyncerPassesThroughFCVFetcherCallbackError) { auto initialSyncer = &getInitialSyncer(); auto opCtx = makeOpCtx(); @@ -1943,6 +1981,65 @@ TEST_F(InitialSyncerTest, InitialSyncerSucceedsWhenFCVFetcherReturnsOldVersion) ASSERT_EQUALS(ErrorCodes::CallbackCanceled, _lastApplied); } +// This is to demonstrate the unit testing mock framework. The logic is the same as the following +// test. +TEST_F( + InitialSyncerTest, + InitialSyncerPassesThroughOplogFetcherRestartsBasedOnInitialSyncFetcherRestartDecision_Mock) { + auto initialSyncer = &getInitialSyncer(); + auto opCtx = makeOpCtx(); + + _syncSourceSelector->setChooseNewSyncSourceResult_forTest(HostAndPort("localhost", 12345)); + + const std::uint32_t initialSyncMaxAttempts = 2U; + + auto lastOp = makeOplogEntry(2); + + MockNetwork mock(getNet()); + + // Set up default behavior. + mock.expect("replSetGetRBID", makeRollbackCheckerResponse(1)); + + mock.expect([](auto& request) { return request["find"].str() == "oplog.rs"; }, + makeCursorResponse(0LL, _options.localOplogNS, {makeOplogEntryObj(1)})) + .times(2); + + mock.expect([](auto& request) { return request["find"].str() == "transactions"; }, + makeCursorResponse(0LL, NamespaceString::kSessionTransactionsTableNamespace, {})); + + // This is what we want to test. + FeatureCompatibilityVersionDocument fcvDoc; + // (Generic FCV reference): This FCV reference should exist across LTS binary versions. + fcvDoc.setVersion(ServerGlobalParams::FeatureCompatibility::kLastLTS); + mock.expect([](auto& request) { return request["find"].str() == "system.version"; }, + makeCursorResponse( + 0LL, NamespaceString::kServerConfigurationNamespace, {fcvDoc.toBSON()})) + .times(1); + + FailPointEnableBlock skipReconstructPreparedTransactions("skipReconstructPreparedTransactions"); + FailPointEnableBlock skipRecoverTenantMigrationAccessBlockers( + "skipRecoverTenantMigrationAccessBlockers"); + + // Start the real work. + ASSERT_OK(initialSyncer->startup(opCtx.get(), initialSyncMaxAttempts)); + + mock.runUntilExpectationsSatisfied(); + + // Simulate response to OplogFetcher so it has enough operations to reach end timestamp. + getOplogFetcher()->receiveBatch(1LL, {makeOplogEntryObj(1), lastOp.toBSON()}); + // Simulate a network error response that restarts the OplogFetcher. + getOplogFetcher()->simulateResponseError(Status(ErrorCodes::NetworkTimeout, "network error")); + + mock.expect([](auto& request) { return request["find"].str() == "oplog.rs"; }, + makeCursorResponse(0LL, _options.localOplogNS, {lastOp.toBSON()})) + .times(1); + + mock.runUntilExpectationsSatisfied(); + + initialSyncer->join(); + ASSERT_OK(_lastApplied.getStatus()); +} + TEST_F(InitialSyncerTest, InitialSyncerPassesThroughOplogFetcherRestartsBasedOnInitialSyncFetcherRestartDecision) { auto initialSyncer = &getInitialSyncer(); diff --git a/src/mongo/db/repl/mock_fixture.cpp b/src/mongo/db/repl/mock_fixture.cpp new file mode 100644 index 00000000000..2d4156fdca9 --- /dev/null +++ b/src/mongo/db/repl/mock_fixture.cpp @@ -0,0 +1,135 @@ +/** + * Copyright (C) 2020-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_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest + +#include "mongo/platform/basic.h" + +#include "mongo/db/repl/mock_fixture.h" + +#include "mongo/executor/network_interface_mock.h" +#include "mongo/logv2/log.h" + +namespace mongo { +namespace test { +namespace mock { + +bool MockNetwork::_allExpectationsSatisfied() const { + return std::all_of(_expectations.begin(), _expectations.end(), [](const Expectation& exp) { + return exp.isDefault() || exp.isSatisfied(); + }); +} + +void MockNetwork::_runUntilIdle() { + executor::NetworkInterfaceMock::InNetworkGuard guard(_net); + do { + // The main responsibility of the mock network is to host incoming requests and scheduled + // responses. Additionally, the mock network interface is a de facto lock-step scheduler. + // + // The executor thread and the mock/test thread run in turn. The test thread + // (1) triggers the tested behavior, e.g. by simulating a command; + // (2) responds to network requests; + // (3) advances the mock clock; and + // (4) handles some network operations implicitly (explained below). + // The executor runs the asynchronous jobs which may schedule network requests. + // + // The executor thread gets the first turn, then each of them yields at the end of their + // turns by enabling and signaling the other. The executor thread yields by calling + // waitForWork() on the mock network; the test thread yields by calling + // runReadyNetworkOperations(). + // + // runReadyNetworkOperations() also first checks for expired scheduled works (e.g. request + // timeout) and executes the expired works. This behavior is the item (4) mentioned above. + // + // After yielding to the executor thread, it's possible that new expired scheduled works + // were added by the executor. That's why we need to double check if there's any ready + // network operations before deciding the network is idle. + // + // External threads may make things more complex. For example, they can schedule new + // requests right after we thought the network was idle. However, that's always the case + // with or without the mock framework. + _net->runReadyNetworkOperations(); + if (_net->hasReadyRequests()) { + // Peek the next request. + auto noi = _net->getFrontOfUnscheduledQueue(); + auto request = noi->getRequest().cmdObj; + + // We ignore the next request if it's not expected. + auto exp = std::find_if(_expectations.begin(), _expectations.end(), [&](auto& exp) { + return !exp.isSatisfied() && exp.match(request); + }); + + if (exp != _expectations.end()) { + // Consume the next request and execute the action. + noi = _net->getNextReadyRequest(); + auto response = exp->run(request); + LOGV2_DEBUG(5015401, + 1, + "mock reply ", + "request"_attr = request, + "response"_attr = response); + _net->scheduleResponse(noi, _net->now(), response); + + // Continue handling network operations and process requests. + continue; + } + } + + // The executor is idle since we just ran it. Check hasReadyNetworkOperations so that no + // scheduled work is waiting for the network thread. + } while (_net->hasReadyNetworkOperations()); +} + +void MockNetwork::runUntilExpectationsSatisfied() { + // If there exist extra threads beside the executor and the mock/test thread, when the + // network is idle, the extra threads may be running and will schedule new requests. As a + // result, the current best practice is to busy-loop to prepare for that. + while (!_allExpectationsSatisfied()) { + _runUntilIdle(); + } +} + +void MockNetwork::runUntil(Date_t target) { + while (_net->now() < target) { + LOGV2_DEBUG( + 5015402, 1, "mock advances time", "from"_attr = _net->now(), "to"_attr = target); + { + executor::NetworkInterfaceMock::InNetworkGuard guard(_net); + // Even if we cannot reach target time, we are still making progress in the loop. + _net->runUntil(target); + } + // Run until idle. + _runUntilIdle(); + } + LOGV2_DEBUG(5015403, 1, "mock reached time", "target"_attr = target); +} + +} // namespace mock +} // namespace test +} // namespace mongo diff --git a/src/mongo/db/repl/mock_fixture.h b/src/mongo/db/repl/mock_fixture.h new file mode 100644 index 00000000000..c0da17ae456 --- /dev/null +++ b/src/mongo/db/repl/mock_fixture.h @@ -0,0 +1,155 @@ +/** + * Copyright (C) 2020-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 <limits> + +#include "mongo/executor/network_interface_mock.h" +#include "mongo/stdx/thread.h" + +namespace mongo { + +class BSONObj; +using executor::RemoteCommandResponse; + +namespace test { +namespace mock { + +// MockNetwork wraps the NetworkInterfaceMock to provide a declarative approach +// to specify expected behaviors on the network and to hide the interaction with +// the NetworkInterfaceMock. +class MockNetwork { +public: + using MatcherFunc = std::function<bool(const BSONObj&)>; + using ActionFunc = std::function<RemoteCommandResponse(const BSONObj&)>; + + class Matcher { + public: + Matcher(const char* cmdName) : Matcher(std::string(cmdName)) {} + Matcher(const std::string& cmdName) { + _matcherFunc = [=](const BSONObj& request) { + return request.firstElementFieldNameStringData() == cmdName; + }; + } + + Matcher(MatcherFunc matcherFunc) : _matcherFunc(std::move(matcherFunc)) {} + + bool operator()(const BSONObj& request) { + return _matcherFunc(request); + } + + private: + MatcherFunc _matcherFunc; + }; + + class Action { + public: + Action(ActionFunc func) : _actionFunc(std::move(func)){}; + + Action(const BSONObj& response) { + _actionFunc = [=](const BSONObj& request) { + return RemoteCommandResponse(response, Milliseconds(0)); + }; + } + + Action(const RemoteCommandResponse& commandResponse) { + _actionFunc = [=](const BSONObj& request) { return commandResponse; }; + } + + RemoteCommandResponse operator()(const BSONObj& request) { + return _actionFunc(request); + } + + private: + ActionFunc _actionFunc; + }; + + class Expectation { + public: + Expectation(Matcher matcher, Action action) + : _matcher(std::move(matcher)), _action(std::move(action)) {} + + Expectation& times(int t) { + _allowedTimes = t; + return *this; + } + + bool match(const BSONObj& request) { + return _matcher(request); + } + + RemoteCommandResponse run(const BSONObj& request) { + if (!isDefault()) { + _allowedTimes--; + } + return _action(request); + } + + bool isDefault() const { + return _allowedTimes == std::numeric_limits<int>::max(); + } + + bool isSatisfied() const { + return _allowedTimes == 0; + } + + private: + Matcher _matcher; + Action _action; + int _allowedTimes = std::numeric_limits<int>::max(); + }; + + explicit MockNetwork(executor::NetworkInterfaceMock* net) : _net(net) {} + + // Accept anything that Matcher's and Action's constructors allow. + template <typename MatcherType> + Expectation& expect(MatcherType&& matcher, Action action) { + _expectations.emplace_back(Matcher(std::forward<MatcherType>(matcher)), std::move(action)); + return _expectations.back(); + } + + // Advance time to the target. Run network operations and process requests along the way. + void runUntil(Date_t targetTime); + + // Run until both the executor and the network are idle and all expectations are satisfied. + // Otherwise, it hangs forever. + void runUntilExpectationsSatisfied(); + +private: + void _runUntilIdle(); + bool _allExpectationsSatisfied() const; + + std::vector<Expectation> _expectations; + executor::NetworkInterfaceMock* _net; +}; + +} // namespace mock +} // namespace test +} // namespace mongo 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 360cfb72bb8..66c87ba8eac 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 @@ -35,6 +35,7 @@ #include "mongo/db/jsobj.h" #include "mongo/db/operation_context_noop.h" #include "mongo/db/repl/is_master_response.h" +#include "mongo/db/repl/mock_fixture.h" #include "mongo/db/repl/repl_set_config.h" #include "mongo/db/repl/repl_set_heartbeat_args_v1.h" #include "mongo/db/repl/repl_set_heartbeat_response.h" @@ -56,6 +57,8 @@ namespace mongo { namespace repl { namespace { +using namespace mongo::test::mock; + using executor::NetworkInterfaceMock; using executor::RemoteCommandRequest; using executor::RemoteCommandResponse; @@ -339,6 +342,77 @@ TEST_F(ReplCoordTest, ElectionSucceedsWhenMaxSevenNodesVoteYea) { ASSERT_EQUALS(1, countTextFormatLogLinesContaining("Election succeeded")); } +// This is to demonstrate the unit testing mock framework. The logic is the same as the following +// test. +TEST_F(ReplCoordTest, ElectionFailsWhenInsufficientVotesAreReceivedDuringDryRun_Mock) { + startCapturingLogMessages(); + BSONObj configObj = BSON("_id" + << "mySet" + << "version" << 1 << "members" + << BSON_ARRAY(BSON("_id" << 1 << "host" + << "node1:12345") + << BSON("_id" << 2 << "host" + << "node2:12345") + << BSON("_id" << 3 << "host" + << "node3:12345")) + << "protocolVersion" << 1); + assertStartSuccess(configObj, HostAndPort("node1", 12345)); + ReplSetConfig config = assertMakeRSConfig(configObj); + + OperationContextNoop opCtx; + OpTime time1(Timestamp(100, 1), 0); + replCoordSetMyLastAppliedOpTime(time1, Date_t() + Seconds(time1.getSecs())); + replCoordSetMyLastDurableOpTime(time1, Date_t() + Seconds(time1.getSecs())); + ASSERT_OK(getReplCoord()->setFollowerMode(MemberState::RS_SECONDARY)); + + simulateEnoughHeartbeatsForAllNodesUp(); + + // Check that the node's election candidate metrics are unset before it becomes primary. + ASSERT_BSONOBJ_EQ( + BSONObj(), ReplicationMetrics::get(getServiceContext()).getElectionCandidateMetricsBSON()); + + auto electionTimeoutWhen = getReplCoord()->getElectionTimeout_forTest(); + ASSERT_NOT_EQUALS(Date_t(), electionTimeoutWhen); + LOGV2(2145401, + "Election timeout scheduled at {electionTimeoutWhen} (simulator time)", + "electionTimeoutWhen"_attr = electionTimeoutWhen); + + MockNetwork mock(getNet()); + + // Heartbeat default behavior. + OpTime lastApplied(Timestamp(100, 1), 0); + ReplSetHeartbeatResponse hbResp; + auto rsConfig = getReplCoord()->getReplicaSetConfig_forTest(); + hbResp.setSetName(rsConfig.getReplSetName()); + hbResp.setState(MemberState::RS_SECONDARY); + hbResp.setConfigVersion(rsConfig.getConfigVersion()); + hbResp.setConfigTerm(rsConfig.getConfigTerm()); + hbResp.setAppliedOpTimeAndWallTime({lastApplied, Date_t() + Seconds(lastApplied.getSecs())}); + hbResp.setDurableOpTimeAndWallTime({lastApplied, Date_t() + Seconds(lastApplied.getSecs())}); + + mock.expect("replSetHeartbeat", hbResp.toBSON()); + + mock.expect("replSetRequestVotes", + BSON("ok" << 1 << "term" << 0 << "voteGranted" << false << "reason" + << "don't like him much")) + .times(2); + + // Trigger election. + mock.runUntil(electionTimeoutWhen); + + mock.runUntilExpectationsSatisfied(); + + stopCapturingLogMessages(); + ASSERT_EQUALS(1, + countTextFormatLogLinesContaining( + "Not running for primary, we received insufficient votes")); + + // Check that the node's election candidate metrics have been cleared, since it lost the dry-run + // election and will not become primary. + ASSERT_BSONOBJ_EQ( + BSONObj(), ReplicationMetrics::get(getServiceContext()).getElectionCandidateMetricsBSON()); +} + TEST_F(ReplCoordTest, ElectionFailsWhenInsufficientVotesAreReceivedDuringDryRun) { startCapturingLogMessages(); BSONObj configObj = BSON("_id" diff --git a/src/mongo/executor/network_interface_mock.cpp b/src/mongo/executor/network_interface_mock.cpp index 513dcf78a63..ae0677f5ec3 100644 --- a/src/mongo/executor/network_interface_mock.cpp +++ b/src/mongo/executor/network_interface_mock.cpp @@ -617,6 +617,18 @@ void NetworkInterfaceMock::_runReadyNetworkOperations_inlock(stdx::unique_lock<s _waitingToRunMask &= ~kNetworkThread; } +bool NetworkInterfaceMock::hasReadyNetworkOperations() { + stdx::lock_guard<stdx::mutex> lk(_mutex); + invariant(_currentlyRunning == kNetworkThread); + if (!_alarms.empty() && _now_inlock() >= _alarms.top().when) { + return true; + } + if (!_scheduled.empty() && _scheduled.front().getResponseDate() <= _now_inlock()) { + return true; + } + return false; +} + void NetworkInterfaceMock::_waitForWork_inlock(stdx::unique_lock<stdx::mutex>* lk) { if (_waitingToRunMask & kExecutorThread) { _waitingToRunMask &= ~kExecutorThread; diff --git a/src/mongo/executor/network_interface_mock.h b/src/mongo/executor/network_interface_mock.h index 911917523fc..fdc8b5b753c 100644 --- a/src/mongo/executor/network_interface_mock.h +++ b/src/mongo/executor/network_interface_mock.h @@ -287,6 +287,12 @@ public: const std::vector<NetworkOperationList*>& queuesToCheck, const TaskExecutor::ResponseStatus& response); + /** + * Returns true if there is no scheduled work (i.e. alarms and scheduled responses) for the + * network thread to process. + */ + bool hasReadyNetworkOperations(); + private: /** * Information describing a scheduled alarm. |