summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVesselina Ratcheva <vesselina.ratcheva@10gen.com>2019-05-13 17:33:07 -0400
committerVesselina Ratcheva <vesselina.ratcheva@10gen.com>2019-05-20 18:38:44 -0400
commitcb7eff01f675d854d7484bd68ce298a8817d2af2 (patch)
tree27e2ccaf24b526867adf7a254f5342898af1ba53
parentc8e0cc640ba6045ab3f6bd5f192d7c533f45e0f1 (diff)
downloadmongo-cb7eff01f675d854d7484bd68ce298a8817d2af2.tar.gz
SERVER-41100 Update lastWriteOpTime for prepareTransaction on secondaries
-rw-r--r--jstests/replsets/prepare_failover_rollback_commit.js68
-rw-r--r--src/mongo/db/transaction_participant.cpp11
2 files changed, 79 insertions, 0 deletions
diff --git a/jstests/replsets/prepare_failover_rollback_commit.js b/jstests/replsets/prepare_failover_rollback_commit.js
new file mode 100644
index 00000000000..658f20c1d32
--- /dev/null
+++ b/jstests/replsets/prepare_failover_rollback_commit.js
@@ -0,0 +1,68 @@
+/**
+ * This tests that preparing a transaction successfully will update the lastWriteOpTime on
+ * secondaries so that the corresponding commitTransaction oplog entry has a correct prevOpTime.
+ * The test exercises a failover right after a prepare, so that we have to commit the transaction
+ * while talking to a node that was in secondary state when it was prepared. We commit that
+ * transaction, then roll back that commit entry, the success of which depends on the prevOpTime
+ * being set properly.
+ *
+ * @tags: [uses_transactions, uses_prepare_transaction]
+ */
+(function() {
+ "use strict";
+ load("jstests/replsets/libs/rollback_test.js");
+ load("jstests/core/txns/libs/prepare_helpers.js");
+
+ const dbName = "test";
+ const collName = "prepare_failover_rollback_commit";
+
+ const rollbackTest =
+ new RollbackTest(collName, undefined, true /* expectPreparedTxnsDuringRollback */);
+
+ let primary = rollbackTest.getPrimary();
+ const testDB = primary.getDB(dbName);
+ const testColl = testDB.getCollection(collName);
+
+ // First create the collection for all.
+ assert.commandWorked(testColl.insert({"a": "baseDoc"}));
+
+ const session = primary.startSession();
+ const sessionDB = session.getDatabase(dbName);
+ const sessionColl = sessionDB.getCollection(collName);
+
+ session.startTransaction();
+ assert.commandWorked(sessionColl.insert({"b": "transactionDoc"}));
+
+ // Prepare a transaction. This will be replicated to the secondary.
+ const prepareTimestamp = PrepareHelpers.prepareTransaction(session);
+
+ // Do a failover first, without rolling back any of the data from this test. We want the
+ // current secondary to become primary and inherit the prepared transaction.
+ rollbackTest.transitionToRollbackOperations();
+ rollbackTest.transitionToSyncSourceOperationsBeforeRollback();
+ rollbackTest.transitionToSyncSourceOperationsDuringRollback();
+ rollbackTest.transitionToSteadyStateOperations();
+
+ // Now set up a rollback scenario for that new primary.
+ rollbackTest.transitionToRollbackOperations();
+
+ // Create a proxy session to reuse the session state of the old primary.
+ primary = rollbackTest.getPrimary();
+ const newSession1 = new _DelegatingDriverSession(primary, session);
+
+ // Commit the transaction on this primary. We expect the commit to roll back.
+ assert.commandWorked(PrepareHelpers.commitTransaction(newSession1, prepareTimestamp));
+
+ rollbackTest.transitionToSyncSourceOperationsBeforeRollback();
+ rollbackTest.transitionToSyncSourceOperationsDuringRollback();
+ rollbackTest.transitionToSteadyStateOperations();
+
+ // Create a proxy session to reuse the session state of the old primary.
+ primary = rollbackTest.getPrimary();
+ const newSession2 = new _DelegatingDriverSession(primary, session);
+
+ // Commit the transaction for all to conclude the test.
+ assert.commandWorked(PrepareHelpers.commitTransaction(newSession2, prepareTimestamp));
+
+ rollbackTest.stop();
+})();
diff --git a/src/mongo/db/transaction_participant.cpp b/src/mongo/db/transaction_participant.cpp
index 0260a0509f5..8a19b66dfad 100644
--- a/src/mongo/db/transaction_participant.cpp
+++ b/src/mongo/db/transaction_participant.cpp
@@ -1103,6 +1103,13 @@ Timestamp TransactionParticipant::Participant::prepareTransaction(
const auto ticks = opCtx->getServiceContext()->getTickSource()->getTicks();
stdx::lock_guard<Client> lk(*opCtx->getClient());
o(lk).transactionMetricsObserver.onPrepare(ServerTransactionsMetrics::get(opCtx), ticks);
+
+ // Ensure the lastWriteOpTime is set. This is needed so that we can correctly assign the
+ // prevOpTime for commit and abort oplog entries if a failover happens after the prepare.
+ // This value is updated in _registerCacheUpdateOnCommit, but only on primaries. We
+ // update the lastWriteOpTime here so that it is also available to secondaries. We can
+ // count on it to persist since we never invalidate prepared transactions.
+ o(lk).lastWriteOpTime = prepareOplogSlot;
}
if (MONGO_FAIL_POINT(hangAfterSettingPrepareStartTime)) {
@@ -1297,6 +1304,10 @@ void TransactionParticipant::Participant::commitPreparedTransaction(
invariant(commitOplogEntryOpTime);
}
+ // We must have a lastWriteOpTime set, as that will be used for the prevOpTime on the oplog
+ // entry.
+ invariant(!o().lastWriteOpTime.isNull());
+
// If commitOplogEntryOpTime is a nullopt, then we grab the OpTime from the commitOplogSlot
// which will only be set if we are primary. Otherwise, the commitOplogEntryOpTime must have
// been passed in during secondary oplog application.