121 lines
4.8 KiB
JavaScript
121 lines
4.8 KiB
JavaScript
/**
|
|
* Test that a node crashes if it tries to roll back a 'prepare' oplog entry using refetch-based
|
|
* rollback. The tests mimics the standard PSA rollback setup by using a PSS replica set where the
|
|
* last node effectively acts as an arbiter without formally being one (this is necessary because
|
|
* we disallow the 'prepareTransaction' command in sets with arbiters).
|
|
*
|
|
* @tags: [uses_transactions, uses_prepare_transaction]
|
|
*/
|
|
|
|
(function() {
|
|
"use strict";
|
|
load("jstests/libs/write_concern_util.js");
|
|
load("jstests/core/txns/libs/prepare_helpers.js");
|
|
|
|
const dbName = "test";
|
|
const collName = "rollback_via_refetch_prepare";
|
|
|
|
const rst = new ReplSetTest({
|
|
name: collName,
|
|
nodes: 3,
|
|
useBridge: true,
|
|
nodeOptions: {setParameter: "forceRollbackViaRefetch=true"}
|
|
});
|
|
let nodes = rst.startSet();
|
|
|
|
let config = rst.getReplSetConfig();
|
|
config.members[2].priority = 0;
|
|
rst.initiate(config);
|
|
|
|
const rollbackNode = nodes[0];
|
|
const syncSource = nodes[1];
|
|
const tiebreakerNode = nodes[2];
|
|
|
|
// Wait for primary to be up and ready.
|
|
let primary = rst.getPrimary();
|
|
assert.eq(rollbackNode, primary);
|
|
|
|
// Also wait for the secondaries.
|
|
rst.awaitSecondaryNodes();
|
|
|
|
// Create the collection we're using beforehand.
|
|
assert.commandWorked(
|
|
primary.getDB(dbName).runCommand({create: collName, writeConcern: {w: 3}}));
|
|
|
|
// Stop replication on the tiebreaker node, but keep it connected to the primary.
|
|
stopServerReplication(tiebreakerNode);
|
|
|
|
// Partition out the future primary so it doesn't receive the oplog entry.
|
|
syncSource.disconnect(rollbackNode);
|
|
syncSource.disconnect(tiebreakerNode);
|
|
|
|
// Issue a 'prepareTransaction' command just to the current primary.
|
|
const session = primary.getDB(dbName).getMongo().startSession({causalConsistency: false});
|
|
const sessionDB = session.getDatabase(dbName);
|
|
const sessionColl = sessionDB.getCollection(collName);
|
|
session.startTransaction();
|
|
assert.commandWorked(sessionColl.insert({"prepare": "entry"}));
|
|
const result = assert.commandWorked(
|
|
session.getDatabase('admin').adminCommand({prepareTransaction: 1, writeConcern: {w: 1}}));
|
|
assert(result.prepareTimestamp,
|
|
"prepareTransaction did not return a 'prepareTimestamp': " + tojson(result));
|
|
|
|
// TODO SERVER-38161: Remove this line once we have the functionality to step down without
|
|
// having to commit or abort our prepared transactions. This 'commitTransaction' is currently
|
|
// required to be able to proceed with the test. However, this makes the node crash on the
|
|
// commit entry before getting to the prepare entry, and the goal of this test is to verify
|
|
// it crashes on the prepare entry instead.
|
|
PrepareHelpers.commitTransactionAfterPrepareTS(session, result.prepareTimestamp);
|
|
|
|
// Shift the partition to only isolate the rollback node (current primary) instead.
|
|
rollbackNode.disconnect(tiebreakerNode);
|
|
|
|
// Issue an explicit stepdown command to save us some of the wait.
|
|
try {
|
|
rollbackNode.adminCommand({replSetStepDown: 60, force: true});
|
|
} catch (e) {
|
|
// Stepdown may fail if the node has already started stepping down. We might also
|
|
// get an exception when the node closes the connection. Both are acceptable.
|
|
print('Caught exception from replSetStepDown: ' + e);
|
|
}
|
|
|
|
// Wait for the old primary to finish stepping down and become a secondary.
|
|
rst.waitForState(rollbackNode, ReplSetTest.State.SECONDARY);
|
|
|
|
// Let the sync source step up in its place by giving it the third node's vote.
|
|
syncSource.reconnect(tiebreakerNode);
|
|
primary = rst.getPrimary();
|
|
assert.eq(syncSource, primary);
|
|
|
|
// Make sure the sync source has something new on it. We let the tiebreaker node replicate
|
|
// that for some added test robustness.
|
|
restartServerReplication(tiebreakerNode);
|
|
assert.writeOK(
|
|
syncSource.getDB(dbName)["syncSourceOnly"].insert({e: 1, writeConcern: {w: "majority"}}));
|
|
|
|
clearRawMongoProgramOutput();
|
|
|
|
// Reconnect the old primary to the rest of the set. We expect it to go into rollback.
|
|
rollbackNode.reconnect(syncSource);
|
|
rollbackNode.reconnect(tiebreakerNode);
|
|
|
|
// Verify that it crashed because it failed to roll back.
|
|
assert.soon(function() {
|
|
try {
|
|
rollbackNode.getDB("local").runCommand({ping: 1});
|
|
} catch (e) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}, "Node did not fassert", ReplSetTest.kDefaultTimeoutMS);
|
|
|
|
rst.stop(rst.getNodeId(rollbackNode), undefined, {allowedExitCode: MongoRunner.EXIT_ABRUPT});
|
|
|
|
var msg = RegExp("Can't roll back this command yet: ");
|
|
assert.soon(function() {
|
|
return rawMongoProgramOutput().match(msg);
|
|
}, "Node did not fail to roll back entry.");
|
|
|
|
rst.stopSet();
|
|
}());
|