summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVesselina Ratcheva <vesselina.ratcheva@10gen.com>2020-02-19 16:10:31 -0500
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-02-27 04:26:40 +0000
commit3230c16349387faaee28efe830573c2e6076789a (patch)
tree3764d1cae4965551012ed6e3a7a52015ac5db665
parent5964e1321dc84b848e5ec8d6fe1dd681cbb98436 (diff)
downloadmongo-3230c16349387faaee28efe830573c2e6076789a.tar.gz
SERVER-17934 Do not report replication progress upstream while in initial sync
-rw-r--r--jstests/replsets/no_progress_updates_during_initial_sync.js143
-rw-r--r--src/mongo/db/repl/topology_coordinator.cpp12
-rw-r--r--src/mongo/db/repl/topology_coordinator_v1_test.cpp35
3 files changed, 188 insertions, 2 deletions
diff --git a/jstests/replsets/no_progress_updates_during_initial_sync.js b/jstests/replsets/no_progress_updates_during_initial_sync.js
new file mode 100644
index 00000000000..bae3ecfc069
--- /dev/null
+++ b/jstests/replsets/no_progress_updates_during_initial_sync.js
@@ -0,0 +1,143 @@
+/**
+ * Test that a node in initial sync does not report replication progress. There are two routes
+ * these kinds of updates take:
+ * - via spanning tree:
+ * initial-syncing nodes should send no replSetUpdatePosition commands upstream at all
+ * - via heartbeats:
+ * these nodes should include null lastApplied and lastDurable optimes in heartbeat responses
+ *
+ * @tags: [requires_fcv_44]
+ */
+(function() {
+"use strict";
+
+load("jstests/libs/write_concern_util.js");
+
+const testName = jsTestName();
+const rst = new ReplSetTest({name: testName, nodes: 1});
+rst.startSet();
+rst.initiate();
+
+const primary = rst.getPrimary();
+const primaryDb = primary.getDB("test");
+assert.commandWorked(primaryDb.test.insert({"starting": "doc"}));
+
+jsTestLog("Adding a new node to the replica set");
+
+const secondary = rst.add({
+ rsConfig: {priority: 0},
+ setParameter: {
+ // Used to guarantee we have something to fetch.
+ 'failpoint.initialSyncHangAfterDataCloning': tojson({mode: 'alwaysOn'}),
+ 'failpoint.initialSyncHangBeforeFinish': tojson({mode: 'alwaysOn'}),
+ 'numInitialSyncAttempts': 1,
+ }
+});
+rst.reInitiate();
+rst.waitForState(secondary, ReplSetTest.State.STARTUP_2);
+
+// Make sure we are through with cloning before inserting more docs on the primary, so that we can
+// guarantee we have to fetch and apply them. We begin fetching inclusively of the primary's
+// lastApplied.
+assert.commandWorked(secondary.adminCommand({
+ waitForFailPoint: "initialSyncHangAfterDataCloning",
+ timesEntered: 1,
+ maxTimeMS: kDefaultWaitForFailPointTimeout
+}));
+
+jsTestLog("Inserting some docs on the primary to advance its lastApplied");
+
+assert.commandWorked(primaryDb.test.insert([{a: 1}, {b: 2}, {c: 3}, {d: 4}, {e: 5}]));
+
+jsTestLog("Resuming initial sync");
+
+assert.commandWorked(
+ secondary.adminCommand({configureFailPoint: "initialSyncHangAfterDataCloning", mode: "off"}));
+
+assert.commandWorked(secondary.adminCommand({
+ waitForFailPoint: "initialSyncHangBeforeFinish",
+ timesEntered: 1,
+ maxTimeMS: kDefaultWaitForFailPointTimeout
+}));
+
+// 1. Make sure the initial syncing node sent no replSetUpdatePosition commands while applying.
+sleep(4 * 1000);
+const numUpdatePosition = assert.commandWorked(secondary.adminCommand({serverStatus: 1}))
+ .metrics.repl.network.replSetUpdatePosition.num;
+assert.eq(0, numUpdatePosition);
+
+const nullOpTime = {
+ "ts": Timestamp(0, 0),
+ "t": NumberLong(-1)
+};
+const nullWallTime = ISODate("1970-01-01T00:00:00Z");
+
+// 2. It also should not participate in the acknowledgement of any writes.
+const writeResW2 = primaryDb.runCommand({
+ insert: "test",
+ documents: [{"writeConcernTwo": "shouldfail"}],
+ writeConcern: {w: 2, wtimeout: 4000}
+});
+checkWriteConcernTimedOut(writeResW2);
+
+// The lastCommitted opTime should not advance on the secondary.
+const opTimesAfterW2 = assert.commandWorked(secondary.adminCommand({replSetGetStatus: 1})).optimes;
+assert.docEq(opTimesAfterW2.lastCommittedOpTime, nullOpTime, () => tojson(opTimesAfterW2));
+assert.eq(nullWallTime, opTimesAfterW2.lastCommittedWallTime, () => tojson(opTimesAfterW2));
+
+const writeResWMaj = primaryDb.runCommand({
+ insert: "test",
+ documents: [{"writeConcernMajority": "shouldfail"}],
+ writeConcern: {w: "majority", wtimeout: 4000}
+});
+checkWriteConcernTimedOut(writeResWMaj);
+
+// The lastCommitted opTime should not advance on the secondary.
+const statusAfterWMaj = assert.commandWorked(secondary.adminCommand({replSetGetStatus: 1}));
+const opTimesAfterWMaj = statusAfterWMaj.optimes;
+assert.docEq(opTimesAfterWMaj.lastCommittedOpTime, nullOpTime, () => tojson(opTimesAfterWMaj));
+assert.eq(nullWallTime, opTimesAfterWMaj.lastCommittedWallTime, () => tojson(opTimesAfterWMaj));
+
+// 3. Make sure that even though the lastApplied and lastDurable have advanced on the secondary...
+const secondaryOpTimes = opTimesAfterWMaj;
+assert.gte(
+ bsonWoCompare(secondaryOpTimes.appliedOpTime, nullOpTime), 0, () => tojson(secondaryOpTimes));
+assert.gte(
+ bsonWoCompare(secondaryOpTimes.durableOpTime, nullOpTime), 0, () => tojson(secondaryOpTimes));
+assert.neq(nullWallTime, secondaryOpTimes.optimeDate, () => tojson(secondaryOpTimes));
+assert.neq(nullWallTime, secondaryOpTimes.optimeDurableDate, () => tojson(secondaryOpTimes));
+
+// ...the primary thinks they're still null as they were null in the heartbeat responses.
+const primaryStatusRes = assert.commandWorked(primary.adminCommand({replSetGetStatus: 1}));
+const secondaryOpTimesAsSeenByPrimary = primaryStatusRes.members[1];
+assert.docEq(secondaryOpTimesAsSeenByPrimary.optime,
+ nullOpTime,
+ () => tojson(secondaryOpTimesAsSeenByPrimary));
+assert.docEq(secondaryOpTimesAsSeenByPrimary.optimeDurable,
+ nullOpTime,
+ () => tojson(secondaryOpTimesAsSeenByPrimary));
+assert.eq(nullWallTime,
+ secondaryOpTimesAsSeenByPrimary.optimeDate,
+ () => tojson(secondaryOpTimesAsSeenByPrimary));
+assert.eq(nullWallTime,
+ secondaryOpTimesAsSeenByPrimary.optimeDurableDate,
+ () => tojson(secondaryOpTimesAsSeenByPrimary));
+
+// 4. Finally, confirm that we did indeed fetch and apply all documents during initial sync.
+assert(statusAfterWMaj.initialSyncStatus,
+ () => "Response should have an 'initialSyncStatus' field: " + tojson(statusAfterWMaj));
+// We should have applied at least 6 documents, not 5, as fetching and applying are inclusive of the
+// sync source's lastApplied.
+assert.gte(statusAfterWMaj.initialSyncStatus.appliedOps, 6);
+
+// Turn off the last failpoint and wait for the node to finish initial sync.
+assert.commandWorked(
+ secondary.adminCommand({configureFailPoint: "initialSyncHangBeforeFinish", mode: "off"}));
+rst.awaitSecondaryNodes();
+
+// The set should now be able to satisfy {w:2} writes.
+assert.commandWorked(
+ primaryDb.runCommand({insert: "test", documents: [{"will": "succeed"}], writeConcern: {w: 2}}));
+
+rst.stopSet();
+})();
diff --git a/src/mongo/db/repl/topology_coordinator.cpp b/src/mongo/db/repl/topology_coordinator.cpp
index 506f28290df..fed13a3e3f4 100644
--- a/src/mongo/db/repl/topology_coordinator.cpp
+++ b/src/mongo/db/repl/topology_coordinator.cpp
@@ -681,8 +681,16 @@ Status TopologyCoordinator::prepareHeartbeatResponseV1(Date_t now,
response->setElectionTime(_electionTime);
}
- const OpTimeAndWallTime lastOpApplied = getMyLastAppliedOpTimeAndWallTime();
- const OpTimeAndWallTime lastOpDurable = getMyLastDurableOpTimeAndWallTime();
+ OpTimeAndWallTime lastOpApplied;
+ OpTimeAndWallTime lastOpDurable;
+
+ // We include null times for lastApplied and lastDurable if we are in STARTUP_2, as we do not
+ // want to report replication progress and be part of write majorities while in initial sync.
+ if (!myState.startup2()) {
+ lastOpApplied = getMyLastAppliedOpTimeAndWallTime();
+ lastOpDurable = getMyLastDurableOpTimeAndWallTime();
+ }
+
response->setAppliedOpTimeAndWallTime(lastOpApplied);
response->setDurableOpTimeAndWallTime(lastOpDurable);
diff --git a/src/mongo/db/repl/topology_coordinator_v1_test.cpp b/src/mongo/db/repl/topology_coordinator_v1_test.cpp
index a8468b3655a..4383b5d748d 100644
--- a/src/mongo/db/repl/topology_coordinator_v1_test.cpp
+++ b/src/mongo/db/repl/topology_coordinator_v1_test.cpp
@@ -2314,6 +2314,41 @@ TEST_F(TopoCoordTest, OmitUninitializedConfigTermFromHeartbeat) {
ASSERT_FALSE(response.toBSON().hasField("configTerm"_sd));
}
+TEST_F(TopoCoordTest, RespondToHeartbeatsWithNullLastAppliedAndLastDurableWhileInInitialSync) {
+ ASSERT_TRUE(TopologyCoordinator::Role::kFollower == getTopoCoord().getRole());
+ ASSERT_EQUALS(MemberState::RS_STARTUP, getTopoCoord().getMemberState().s);
+ updateConfig(BSON("_id"
+ << "rs0"
+ << "version" << 1 << "members"
+ << BSON_ARRAY(BSON("_id" << 0 << "host"
+ << "h0")
+ << BSON("_id" << 1 << "host"
+ << "h1"))),
+ 1);
+
+ ASSERT_TRUE(TopologyCoordinator::Role::kFollower == getTopoCoord().getRole());
+ ASSERT_EQUALS(MemberState::RS_STARTUP2, getTopoCoord().getMemberState().s);
+
+ heartbeatFromMember(
+ HostAndPort("h0"), "rs0", MemberState::RS_SECONDARY, OpTime(Timestamp(3, 0), 0));
+
+ // The lastApplied and lastDurable should be null for any heartbeat responses we send while in
+ // STARTUP_2, even when they are otherwise initialized.
+ OpTime lastOpTime(Timestamp(2, 0), 0);
+ topoCoordSetMyLastAppliedOpTime(lastOpTime, Date_t(), false);
+ topoCoordSetMyLastDurableOpTime(lastOpTime, Date_t(), false);
+
+ ReplSetHeartbeatArgsV1 args;
+ args.setConfigVersion(1);
+ args.setSetName("rs0");
+ args.setSenderId(0);
+ ReplSetHeartbeatResponse response;
+
+ ASSERT_OK(getTopoCoord().prepareHeartbeatResponseV1(now()++, args, "rs0", &response));
+ ASSERT_EQUALS(OpTime(), response.getAppliedOpTime());
+ ASSERT_EQUALS(OpTime(), response.getDurableOpTime());
+}
+
TEST_F(TopoCoordTest, BecomeCandidateWhenBecomingSecondaryInSingleNodeSet) {
ASSERT_TRUE(TopologyCoordinator::Role::kFollower == getTopoCoord().getRole());
ASSERT_EQUALS(MemberState::RS_STARTUP, getTopoCoord().getMemberState().s);