178 lines
7.6 KiB
JavaScript
178 lines
7.6 KiB
JavaScript
/**
|
|
* The purpose of this test is to verify that simple CRUD operations are rolled back
|
|
* successfully in multiversion replica sets. This test induces communication between
|
|
* the rollback node and the sync source during rollback. This is done in order to
|
|
* exercise rollback via refetch in the case that refetch is necessary.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
load("jstests/replsets/libs/rollback_test.js");
|
|
load("jstests/libs/collection_drop_recreate.js");
|
|
|
|
/**
|
|
* Executes and validates rollback between a pair of nodes with the given versions.
|
|
*
|
|
* @param {string} testName the name of the test being run
|
|
* @param {string} rollbackNodeVersion the desired version for the rollback node
|
|
* @param {string} syncSourceVersion the desired version for the sync source
|
|
*
|
|
*/
|
|
function testMultiversionRollback(testName, rollbackNodeVersion, syncSourceVersion) {
|
|
jsTestLog("Started multiversion rollback test for versions: {rollbackNode: " +
|
|
rollbackNodeVersion + ", syncSource: " + syncSourceVersion + "}.");
|
|
|
|
let dbName = testName;
|
|
|
|
let CommonOps = (node) => {
|
|
// Insert four documents on both nodes.
|
|
assert.commandWorked(node.getDB(dbName)["bothNodesKeep"].insert({a: 1}));
|
|
assert.commandWorked(node.getDB(dbName)["rollbackNodeDeletes"].insert({b: 1}));
|
|
assert.commandWorked(node.getDB(dbName)["rollbackNodeUpdates"].insert({c: 1}));
|
|
assert.commandWorked(node.getDB(dbName)["bothNodesUpdate"].insert({d: 1}));
|
|
};
|
|
|
|
let RollbackOps = (node) => {
|
|
// Perform operations only on the rollback node:
|
|
// 1. Delete a document.
|
|
// 2. Update a document only on this node.
|
|
// 3. Update a document on both nodes.
|
|
// All three documents will be refetched during rollback.
|
|
assert.commandWorked(node.getDB(dbName)["rollbackNodeDeletes"].remove({b: 1}));
|
|
assert.commandWorked(node.getDB(dbName)["rollbackNodeUpdates"].update({c: 1}, {c: 0}));
|
|
assert.commandWorked(node.getDB(dbName)["bothNodesUpdate"].update({d: 1}, {d: 0}));
|
|
};
|
|
|
|
let SyncSourceOps = (node) => {
|
|
// Perform operations only on the sync source:
|
|
// 1. Make a conflicting write on one of the documents the rollback node updates.
|
|
// 2. Insert a new document.
|
|
assert.commandWorked(node.getDB(dbName)["bothNodesUpdate"].update({d: 1}, {d: 2}));
|
|
assert.commandWorked(node.getDB(dbName)["syncSourceInserts"].insert({e: 1}));
|
|
};
|
|
|
|
// Set up replica set.
|
|
let replSet = setupReplicaSet(testName, rollbackNodeVersion, syncSourceVersion);
|
|
|
|
// Set up Rollback Test.
|
|
let rollbackTest = new RollbackTest(testName, replSet);
|
|
|
|
CommonOps(rollbackTest.getPrimary());
|
|
|
|
// Perform operations that will be rolled back.
|
|
let rollbackNode = rollbackTest.transitionToRollbackOperations();
|
|
RollbackOps(rollbackNode);
|
|
|
|
// Perform different operations only on the sync source.
|
|
let syncSource = rollbackTest.transitionToSyncSourceOperationsBeforeRollback();
|
|
SyncSourceOps(syncSource);
|
|
|
|
// Wait for rollback to finish.
|
|
rollbackTest.transitionToSyncSourceOperationsDuringRollback();
|
|
rollbackTest.transitionToSteadyStateOperations();
|
|
|
|
rollbackTest.stop();
|
|
}
|
|
|
|
/**
|
|
* Sets up a multiversion replica set.
|
|
*
|
|
* Note that, regardless of which node in the rollbackNode-syncSource pair requires
|
|
* which version, there is only one possible way to start up such a cluster:
|
|
*
|
|
* 1. Start up the first two nodes with the higher of the two versions.
|
|
* 2. Set the FCV to the lower version in order to be able to include the third node.
|
|
* 3. Bring up the third node and add it to the set, with the lower binary version.
|
|
* 4. This always results in a higher-version primary and a lower-version secondary,
|
|
* so if a test case specifies the lower version on the rollback node (i.e. the
|
|
* opposite setup), this function will force the current primary and secondary
|
|
* to switch roles.
|
|
*
|
|
* This function returns a replica set with the intended rollback node as the primary.
|
|
*
|
|
* @param {string} testName the name of the test being run
|
|
* @param {string} rollbackNodeVersion the desired version for the rollback node
|
|
* @param {string} syncSourceVersion the desired version for the sync source
|
|
*/
|
|
function setupReplicaSet(testName, rollbackNodeVersion, syncSourceVersion) {
|
|
jsTestLog(
|
|
`[${testName}] Beginning cluster setup with versions: {rollbackNode: ${rollbackNodeVersion},
|
|
syncSource: ${syncSourceVersion}}.`);
|
|
|
|
let sortedVersions =
|
|
[rollbackNodeVersion, syncSourceVersion].sort(MongoRunner.compareBinVersions);
|
|
let lowerVersion = MongoRunner.getBinVersionFor(sortedVersions[0]);
|
|
let higherVersion = MongoRunner.getBinVersionFor(sortedVersions[1]);
|
|
|
|
jsTestLog(`[${testName}] Starting up first two nodes with version: ${higherVersion}`);
|
|
var initialNodes = {n1: {binVersion: higherVersion}, n2: {binVersion: higherVersion}};
|
|
|
|
// Start up a two-node cluster first. This cluster contains two data bearing nodes, but the
|
|
// second node will be priority: 0 to ensure that it will never become primary. This, in
|
|
// addition to stopping/restarting server replication should make the node exhibit similar
|
|
// behavior to an arbiter.
|
|
var rst = new ReplSetTest(
|
|
{name: testName, nodes: initialNodes, useBridge: true, settings: {chainingAllowed: false}});
|
|
rst.startSet();
|
|
rst.initiateWithHighElectionTimeout();
|
|
|
|
// Wait for both nodes to be up.
|
|
waitForState(rst.nodes[0], ReplSetTest.State.PRIMARY);
|
|
waitForState(rst.nodes[1], ReplSetTest.State.SECONDARY);
|
|
|
|
const initialPrimary = rst.getPrimary();
|
|
|
|
// Set FCV to accommodate third node.
|
|
jsTestLog(
|
|
`[${testName} - ${initialPrimary.host}] Setting FCV to ${lowerVersion} on the primary.`);
|
|
assert.commandWorked(
|
|
initialPrimary.adminCommand({setFeatureCompatibilityVersion: lowerVersion}));
|
|
|
|
jsTestLog(`[${testName}] Bringing up third node with version ${lowerVersion}`);
|
|
rst.add({binVersion: lowerVersion});
|
|
rst.reInitiate();
|
|
|
|
let config = rst.getReplSetConfigFromNode();
|
|
config.members[1].priority = 0;
|
|
reconfig(rst, config, true);
|
|
|
|
jsTestLog(
|
|
`[${testName} - ${rst.nodes[2].host}] Waiting for the newest node to become a secondary.`);
|
|
rst.awaitSecondaryNodes();
|
|
|
|
let primary = rst.nodes[0];
|
|
let secondary = rst.nodes[2];
|
|
let tiebreakerNode = rst.nodes[1];
|
|
|
|
// Make sure we still have the right node as the primary.
|
|
assert.eq(rst.getPrimary(), primary);
|
|
|
|
// Also make sure the other two nodes are in their expected states.
|
|
assert.eq(ReplSetTest.State.SECONDARY,
|
|
tiebreakerNode.adminCommand({replSetGetStatus: true}).myState);
|
|
assert.eq(ReplSetTest.State.SECONDARY,
|
|
secondary.adminCommand({replSetGetStatus: true}).myState);
|
|
|
|
jsTestLog(`[${testName}] Cluster now running with versions: {primary: ${higherVersion},
|
|
secondary: ${lowerVersion}, tiebreakerNode: ${higherVersion}}.`);
|
|
|
|
// Some test cases require that the primary (future rollback node) is running the lower
|
|
// version, which at this point is always on the secondary, so we elect that node instead.
|
|
if (rollbackNodeVersion === lowerVersion) {
|
|
jsTestLog(
|
|
`[${testName}] Test case requires opposite versions for primary and secondary. Swapping roles.`);
|
|
|
|
// Force the current secondary to become the primary.
|
|
rst.stepUp(secondary);
|
|
|
|
let newPrimary = secondary;
|
|
secondary = primary;
|
|
primary = newPrimary;
|
|
|
|
jsTestLog(`[${testName}] Cluster now running with versions: {primary: ${lowerVersion},
|
|
secondary: ${higherVersion}, tiebreakerNode: ${higherVersion}}.`);
|
|
}
|
|
|
|
return rst;
|
|
}
|