Files
mongo/jstests/replsets/libs/rollback_test.js
2017-10-13 10:20:35 -04:00

193 lines
7.5 KiB
JavaScript

/**
* Wrapper around ReplSetTest for testing rollback behavior. It allows the caller to easily
* transition between stages of a rollback without having to configure the replset explicitly.
*
* This library exposes the following 3 sequential stages of rollback:
* 1. RollbackTest starts in kSteadyStateOps: the replica set is in steady state replication.
* Operations applied will be replicated.
* 2. kRollbackOps: operations applied during this phase will not be replicated and eventually be
* rolled back.
* 3. kSyncSourceOps: operations applied during this stage will not be replicated initially,
* causing the data-bearing nodes to diverge. The data will be replicated after other
* nodes are rolled back.
* 4. kSteadyStateOps: (back to stage 1) with the option of waiting for the rollback to finish.
*
* Please refer to the various `transition*` functions for more information on the behavior
* of each stage.
*/
load("jstests/replsets/rslib.js");
function RollbackTest(name = "RollbackTest") {
const State = {
kStopped: "stopped",
kRollbackOps: "doing operations that will be rolled back",
kSyncSourceOps: "doing operations on sync source",
kSteadyStateOps: "doing operations during steady state replication",
};
const AcceptableTransitions = {
[State.kStopped]: [],
[State.kRollbackOps]: [State.kSyncSourceOps],
[State.kSyncSourceOps]: [State.kSteadyStateOps],
[State.kSteadyStateOps]: [State.kStopped, State.kRollbackOps],
};
const rst = new ReplSetTest({name, nodes: 3, useBridge: true});
let curPrimary;
let curSecondary;
let arbiter;
let curState;
// Do more complicated instantiation in this init() function. This reduces the amount of code
// run at the class level (i.e. outside of any functions) and improves readability.
(function init() {
rst.startSet();
const nodes = rst.nodeList();
rst.initiate({
_id: name,
members: [
{_id: 0, host: nodes[0]},
{_id: 1, host: nodes[1]},
{_id: 2, host: nodes[2], arbiterOnly: true}
]
});
// The primary is always the first node after calling ReplSetTest.initiate();
curPrimary = rst.nodes[0];
curSecondary = rst.nodes[1];
arbiter = rst.nodes[2];
// Wait for the primary to be ready.
const actualPrimary = rst.getPrimary();
assert.eq(actualPrimary, curPrimary, 'The ReplSetTest.node[0] is not the primary');
curState = State.kSteadyStateOps;
})();
/**
* return whether the cluster can transition from the current State to `newState`.
* @private
*/
function transitionIfAllowed(newState) {
if (AcceptableTransitions[curState].includes(newState)) {
jsTestLog(`[${name}] Transitioning to: "${newState}"`);
curState = newState;
} else {
// Transitioning to a disallowed State is likely a bug in the code, so we throw an
// error here instead of silently failing.
throw new Error(`Can't transition to State "${newState}" from State "${curState}"`);
}
}
/**
* Transition from a rollback state to a steady state. Operations applied in this phase will
* be replicated to all nodes and should not be rolled back.
*
* @param waitForRollback specify whether to wait for rollback to finish before returning.
*/
this.transitionToSteadyStateOperations = function(
{waitForRollback: waitForRollback = true} = {}) {
transitionIfAllowed(State.kSteadyStateOps);
const originalRBID = assert.commandWorked(curSecondary.adminCommand("replSetGetRBID")).rbid;
// Connect the secondary to everything else so it'll go into rollback.
curSecondary.reconnect([curPrimary, arbiter]);
if (waitForRollback) {
jsTestLog(`[${name}] Waiting for rollback to complete`);
assert.soonNoExcept(() => {
// Command can fail when sync source is being cleared.
const rbid = assert.commandWorked(curSecondary.adminCommand("replSetGetRBID")).rbid;
return rbid === originalRBID + 1;
}, 'Timed out waiting for RBID to increment');
rst.awaitSecondaryNodes();
rst.awaitReplication();
jsTestLog(`[${name}] Rollback completed`);
}
return curPrimary;
};
/**
* Transition to the first stage of rollback testing, where we isolate the current primary so
* its operations will eventually be rolled back.
*/
this.transitionToRollbackOperations = function() {
transitionIfAllowed(State.kRollbackOps);
// Ensure previous operations are replicated. The current secondary will be used as the sync
// source later on, so it must be up-to-date to prevent any previous operations from being
// rolled back.
rst.awaitReplication();
// Disconnect the current primary from the secondary so operations on it will eventually be
// rolled back. But do not disconnect it from the arbiter so it can stay as the primary.
curPrimary.disconnect([curSecondary]);
return curPrimary;
};
/**
* Transition to the second stage of rollback testing, where we isolate the old primary and
* elect the old secondary as the new primary. Then, operations can be performed on the new
* primary so that that optimes diverge and previous operations on the old primary will be
* rolled back.
*/
this.transitionToSyncSourceOperations = function() {
transitionIfAllowed(State.kSyncSourceOps);
// Isolate the current primary from everything so it steps down.
curPrimary.disconnect([curSecondary, arbiter]);
// Wait for the primary to step down.
waitForState(curPrimary, ReplSetTest.State.SECONDARY);
// Reconnect the secondary to the arbiter so it can be elected.
curSecondary.reconnect([arbiter]);
// Wait for the old secondary to become the new primary, which will be used as the sync
// source later on when the old primary rolls back.
const newPrimary = rst.getPrimary();
// As a sanity check, ensure the new primary is the old secondary. The opposite scenario
// should never be possible with 2 electable nodes and the sequence of operations thus far.
assert.eq(newPrimary, curSecondary, "Did not elect a new node as primary");
// Add a sleep and a dummy write to ensure the new primary has an optime greater than
// the last optime on the node that will undergo rollback. This greater optime ensures that
// the new primary is eligible to become a sync source in pv0.
sleep(1000);
var dbName = "ensureEligiblePV0";
assert.writeOK(newPrimary.getDB(dbName).testColl.insert({id: 0}));
// The old primary is the new secondary; the old secondary just got elected as the new
// primary, so we update the topology to reflect this change.
curSecondary = curPrimary;
curPrimary = newPrimary;
return curPrimary;
};
this.stop = function() {
transitionIfAllowed(State.kStopped);
const name = rst.name;
rst.checkOplogs(name);
rst.checkReplicatedDataHashes(name);
return rst.stopSet();
};
this.getPrimary = function() {
return curPrimary;
};
this.getSecondary = function() {
return curSecondary;
};
}