// Tests that the last vote document is stored during elections and that it is loaded and used on // startup. // // The test first runs a few elections and checks that the lastVote document is set correctly // after each one. // // The test then restarts one node as a standalone, changes its last vote doc, and stops the // other node. It then restarts the first node as a replicaset and manually runs // replSetRequestVotes commands against it and checks that its response is correct. // // @tags: [requires_persistence] (function() { "use strict"; load("jstests/replsets/rslib.js"); // For getLatestOp() var name = "last_vote"; var rst = new ReplSetTest({ name: name, nodes: 2, }); rst.startSet(); // Initiate the set with a high election timeout so that there aren't any unplanned elections. rst.initiateWithHighElectionTimeout(); const lastVoteNS = 'local.replset.election'; function getLastVoteDoc(conn) { assert.eq(conn.getCollection(lastVoteNS).find().itcount(), 1, 'last vote should be singleton'); return conn.getCollection(lastVoteNS).findOne(); } function setLastVoteDoc(conn, term, candidate) { var newLastVote = {term: term, candidateIndex: rst.getNodeId(candidate)}; return assert.commandWorked(conn.getCollection(lastVoteNS).update({}, newLastVote)); } function assertNodeHasLastVote(node, term, candidate) { var lastVoteDoc = getLastVoteDoc(node); assert.eq(lastVoteDoc.term, term, node.host + " had wrong last vote term."); assert.eq(lastVoteDoc.candidateIndex, rst.getNodeId(candidate), node.host + " had wrong last vote candidate."); } function assertCurrentTerm(node, term) { var stat = assert.commandWorked(node.adminCommand({replSetGetStatus: 1})); assert.eq(stat.term, term, "Term changed when it should not have"); } jsTestLog("Test that last vote is set on successive elections"); // Run a few successive elections, alternating who becomes primary. var numElections = 3; for (var i = 0; i < numElections; i++) { var primary = rst.getPrimary(); var secondary = rst.getSecondary(); var term = getLatestOp(primary).t; // SERVER-20844 ReplSetTest starts up a single node replica set then reconfigures to the // correct size, so secondaries didn't vote in the first election. if (i > 0) { jsTestLog("Last vote should have term: " + term + " and candidate: " + primary.host + ", index: " + rst.getNodeId(primary)); rst.nodes.forEach(function(node) { assertNodeHasLastVote(node, term, primary); }); } rst.stepUp(secondary); // Make sure a new primary has been established. rst.waitForState(primary, ReplSetTest.State.SECONDARY); rst.waitForState(secondary, ReplSetTest.State.PRIMARY); // Reset election timeout for the old primary. assert.commandWorked(primary.adminCommand({replSetFreeze: 0})); } var term = getLatestOp(rst.getPrimary()).t + 100; jsTestLog("Test that last vote is loaded on startup"); // Ensure that all ops are replicated before stepping up node 1. rst.awaitReplication(); // We cannot reconfig node 0 to have priority 0 if it is currently the primary, // so we make sure node 1 is primary. jsTestLog("Stepping up node 1"); rst.stepUp(rst.nodes[1]); jsTestLog("Reconfiguring cluster to make node 0 unelectable so it stays SECONDARY on restart"); let conf = rst.getReplSetConfigFromNode(); conf.version++; conf.members[0].priority = 0; reconfig(rst, conf); rst.awaitNodesAgreeOnConfigVersion(); jsTestLog("Restarting node 0 as a standalone"); var node0 = rst.restart(0, {noReplSet: true}); // Restart as a standalone node. jsTestLog("Stopping node 1"); rst.stop(1); // Stop node 1 so that node 0 controls the term by itself. jsTestLog("Setting the lastVote on node 0 to term: " + term + " candidate: " + rst.nodes[0].host + ", index: 0"); setLastVoteDoc(node0, term, rst.nodes[0]); jsTestLog("Restarting node 0 in replica set mode"); node0 = rst.restart(0); // Restart in replSet mode again. rst.waitForState(node0, ReplSetTest.State.SECONDARY); assert.soonNoExcept(function() { assertCurrentTerm(node0, term); return true; }); jsTestLog("Manually sending node 0 a dryRun replSetRequestVotes command, " + "expecting failure in old term"); var response = assert.commandWorked(node0.adminCommand({ replSetRequestVotes: 1, setName: name, dryRun: true, term: term - 1, candidateIndex: 1, configVersion: conf.version, lastCommittedOp: getLatestOp(node0) })); assert.eq( response.term, term, "replSetRequestVotes response had the wrong term: " + tojson(response)); assert(!response.voteGranted, "node granted vote in term before last vote doc: " + tojson(response)); assertNodeHasLastVote(node0, term, rst.nodes[0]); assertCurrentTerm(node0, term); jsTestLog("Manually sending node 0 a dryRun replSetRequestVotes command in same term, " + "expecting success but no recording of lastVote"); response = assert.commandWorked(node0.adminCommand({ replSetRequestVotes: 1, setName: name, dryRun: true, term: term, candidateIndex: 1, configVersion: conf.version, lastCommittedOp: getLatestOp(node0) })); assert.eq( response.term, term, "replSetRequestVotes response had the wrong term: " + tojson(response)); assert(response.voteGranted, "node failed to grant dryRun vote in term equal to last vote doc: " + tojson(response)); assert.eq( response.reason, "", "replSetRequestVotes response had the wrong reason: " + tojson(response)); assertNodeHasLastVote(node0, term, rst.nodes[0]); assertCurrentTerm(node0, term); jsTestLog("Manually sending node 0 a replSetRequestVotes command, expecting failure in same term"); response = assert.commandWorked(node0.adminCommand({ replSetRequestVotes: 1, setName: name, dryRun: false, term: term, candidateIndex: 1, configVersion: conf.version, lastCommittedOp: getLatestOp(node0) })); assert.eq( response.term, term, "replSetRequestVotes response had the wrong term: " + tojson(response)); assert(!response.voteGranted, "node granted vote in term of last vote doc: " + tojson(response)); assertNodeHasLastVote(node0, term, rst.nodes[0]); assertCurrentTerm(node0, term); jsTestLog("Manually sending node 0 a replSetRequestVotes command, " + "expecting success with a recording of the new lastVote"); response = assert.commandWorked(node0.adminCommand({ replSetRequestVotes: 1, setName: name, dryRun: false, term: term + 1, candidateIndex: 1, configVersion: conf.version, lastCommittedOp: getLatestOp(node0) })); assert.eq(response.term, term + 1, "replSetRequestVotes response had the wrong term: " + tojson(response)); assert(response.voteGranted, "node failed to grant vote in term greater than last vote doc: " + tojson(response)); assert.eq( response.reason, "", "replSetRequestVotes response had the wrong reason: " + tojson(response)); assertNodeHasLastVote(node0, term + 1, rst.nodes[1]); assertCurrentTerm(node0, term + 1); jsTestLog("Manually sending node 0 a dryRun replSetRequestVotes command in future term, " + "expecting success but no recording of lastVote"); response = assert.commandWorked(node0.adminCommand({ replSetRequestVotes: 1, setName: name, dryRun: true, term: term + 2, candidateIndex: 1, configVersion: conf.version, lastCommittedOp: getLatestOp(node0) })); assert.eq(response.term, term + 2, "replSetRequestVotes response had the wrong term: " + tojson(response)); assert(response.voteGranted, "node failed to grant vote in term greater than last vote doc: " + tojson(response)); assert.eq( response.reason, "", "replSetRequestVotes response had the wrong reason: " + tojson(response)); assertNodeHasLastVote(node0, term + 1, rst.nodes[1]); assertCurrentTerm(node0, term + 2); rst.stopSet(); })();