mongo/jstests/replsets/recover_prepared_transactio...

203 lines
8.2 KiB
JavaScript

/**
* 1. Test that rollback can successfully recover an aborted prepared transaction that was
* rolled-back but was in prepare between the stable timestamp and the common point.
* 2. Test that rollback can successfully recover a committed prepared transaction that was
* rolled-back but was in prepare before the stable timestamp.
*
* This test holds back the stable timestamp and starts two prepared transactions
* before transitioning to rollback operations, where the branches of history on
* the rollback node and sync source will diverge. This ensures that we prepare
* the transactions in between the stable timestamp and the common point.
*
* After a rollback of commit/abort, we should correctly reconstruct the two prepared transactions
* and be able to commit/abort them again.
*
* @tags: [
* uses_prepare_transaction,
* uses_transactions,
* ]
*/
import {PrepareHelpers} from "jstests/core/txns/libs/prepare_helpers.js";
import {RollbackTest} from "jstests/replsets/libs/rollback_test.js";
const dbName = "test";
const collName = "recover_prepared_transaction_state_after_rollback";
const rollbackTest = new RollbackTest(dbName);
let primary = rollbackTest.getPrimary();
// Create collection we're using beforehand.
const testDB = primary.getDB(dbName);
const testColl = testDB.getCollection(collName);
assert.commandWorked(testDB.runCommand({create: collName}));
// Start two different sessions on the primary.
let session1 = primary.startSession({causalConsistency: false});
let session2 = primary.startSession({causalConsistency: false});
// Save both session IDs so we can later start sessions with the same IDs and commit or
// abort a prepared transaction on them.
const sessionID1 = session1.getSessionId();
const sessionID2 = session2.getSessionId();
let sessionDB1 = session1.getDatabase(dbName);
const sessionColl1 = sessionDB1.getCollection(collName);
let sessionDB2 = session2.getDatabase(dbName);
const sessionColl2 = sessionDB2.getCollection(collName);
assert.commandWorked(sessionColl1.insert({_id: 1}));
assert.commandWorked(sessionColl1.insert({_id: 2}));
rollbackTest.awaitLastOpCommitted();
// Prepare a transaction on the first session whose commit will be rolled-back.
session1.startTransaction();
assert.commandWorked(sessionColl1.insert({_id: 3}));
assert.commandWorked(sessionColl1.update({_id: 1}, {$set: {a: 1}}));
const prepareTimestamp = PrepareHelpers.prepareTransaction(session1);
// Prevent the stable timestamp from moving beyond the following prepared transactions so
// that when we replay the oplog from the stable timestamp, we correctly recover them.
assert.commandWorked(primary.adminCommand({configureFailPoint: "disableSnapshotting", mode: "alwaysOn"}));
// The following transactions will be prepared before the common point, so they must be in
// prepare after rollback recovery.
// Prepare another transaction on the second session whose abort will be rolled-back.
session2.startTransaction();
assert.commandWorked(sessionColl2.insert({_id: 4}));
assert.commandWorked(sessionColl2.update({_id: 2}, {$set: {b: 2}}));
const prepareTimestamp2 = PrepareHelpers.prepareTransaction(session2, {w: 1});
// Check that we have two transactions in the transactions table.
assert.eq(primary.getDB("config")["transactions"].find().itcount(), 2);
// This characterizes the current behavior of fastcount, which is that the two open transaction
// count toward the value.
assert.eq(testColl.count(), 4);
// The following commit and abort will be rolled back.
rollbackTest.transitionToRollbackOperations();
PrepareHelpers.commitTransaction(session1, prepareTimestamp);
assert.commandWorked(session2.abortTransaction_forTesting());
// The fastcount should be accurate because there are no open transactions.
assert.eq(testColl.count(), 3);
rollbackTest.transitionToSyncSourceOperationsBeforeRollback();
rollbackTest.transitionToSyncSourceOperationsDuringRollback();
try {
rollbackTest.transitionToSteadyStateOperations({skipDataConsistencyChecks: true});
} finally {
assert.commandWorked(primary.adminCommand({configureFailPoint: "disableSnapshotting", mode: "off"}));
}
// Make sure there are two transactions in the transactions table after rollback recovery.
assert.eq(primary.getDB("config")["transactions"].find().itcount(), 2);
// Make sure we can only see the first write and cannot see the writes from the prepared
// transactions or the write that was rolled back.
assert.sameMembers(sessionColl1.find().toArray(), [{_id: 1}, {_id: 2}]);
assert.sameMembers(testColl.find().toArray(), [{_id: 1}, {_id: 2}]);
// This check characterizes the current behavior of fastcount after rollback. It will not be
// correct, but reflects the count at the point where both transactions are not yet committed or
// aborted (because the operations were not majority committed). The count will eventually be
// correct once the commit and abort are retried.
assert.eq(sessionColl1.count(), 4);
assert.eq(testColl.count(), 4);
// Get the correct primary after the topology changes.
primary = rollbackTest.getPrimary();
rollbackTest.awaitReplication();
// Make sure we can successfully commit the first rolled back prepared transaction.
session1 = PrepareHelpers.createSessionWithGivenId(primary, sessionID1, {causalConsistency: false});
sessionDB1 = session1.getDatabase(dbName);
// The next transaction on this session should have a txnNumber of 0. We explicitly set this
// since createSessionWithGivenId does not restore the current txnNumber in the shell.
session1.setTxnNumber_forTesting(0);
const txnNumber1 = session1.getTxnNumber_forTesting();
// Make sure we cannot add any operations to a prepared transaction.
assert.commandFailedWithCode(
sessionDB1.runCommand({
insert: collName,
txnNumber: NumberLong(txnNumber1),
documents: [{_id: 10}],
autocommit: false,
}),
ErrorCodes.PreparedTransactionInProgress,
);
// Make sure that writing to a document that was updated in the first prepared transaction
// causes a write conflict.
assert.commandFailedWithCode(
sessionDB1.runCommand({update: collName, updates: [{q: {_id: 1}, u: {$set: {a: 2}}}], maxTimeMS: 5 * 1000}),
ErrorCodes.MaxTimeMSExpired,
);
const commitTimestamp = Timestamp(prepareTimestamp.getTime(), prepareTimestamp.getInc() + 1);
assert.commandWorked(
sessionDB1.adminCommand({
commitTransaction: 1,
commitTimestamp: commitTimestamp,
txnNumber: NumberLong(txnNumber1),
autocommit: false,
}),
);
// Retry the commitTransaction command after rollback.
assert.commandWorked(
sessionDB1.adminCommand({
commitTransaction: 1,
commitTimestamp: commitTimestamp,
txnNumber: NumberLong(txnNumber1),
autocommit: false,
}),
);
// Make sure we can successfully abort the second recovered prepared transaction.
session2 = PrepareHelpers.createSessionWithGivenId(primary, sessionID2, {causalConsistency: false});
sessionDB2 = session2.getDatabase(dbName);
// The next transaction on this session should have a txnNumber of 0. We explicitly set this
// since createSessionWithGivenId does not restore the current txnNumber in the shell.
session2.setTxnNumber_forTesting(0);
const txnNumber2 = session2.getTxnNumber_forTesting();
// Make sure we cannot add any operations to a prepared transaction.
assert.commandFailedWithCode(
sessionDB2.runCommand({
insert: collName,
txnNumber: NumberLong(txnNumber2),
documents: [{_id: 10}],
autocommit: false,
}),
ErrorCodes.PreparedTransactionInProgress,
);
// Make sure that writing to a document that was updated in the second prepared transaction
// causes a write conflict.
assert.commandFailedWithCode(
sessionDB2.runCommand({update: collName, updates: [{q: {_id: 2}, u: {$set: {b: 3}}}], maxTimeMS: 5 * 1000}),
ErrorCodes.MaxTimeMSExpired,
);
assert.commandWorked(
sessionDB2.adminCommand({
abortTransaction: 1,
txnNumber: NumberLong(txnNumber2),
autocommit: false,
}),
);
rollbackTest.awaitReplication();
// Make sure we can see the result of the committed prepared transaction and cannot see the
// write from the aborted transaction.
assert.sameMembers(testColl.find().toArray(), [{_id: 1, a: 1}, {_id: 2}, {_id: 3}]);
assert.eq(testColl.count(), 3);
rollbackTest.stop();