diff options
author | Tess Avitabile <tess.avitabile@mongodb.com> | 2020-06-02 09:26:36 -0400 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2020-06-05 17:42:00 +0000 |
commit | c1bc1b6d6b7b7d216b8243a609f1c7231045e5be (patch) | |
tree | 05a182991f3f661cd10256462f0c7fb9444b7c29 | |
parent | 779f9e866b798cc8b6de7ebe286b516d26f3d84b (diff) | |
download | mongo-c1bc1b6d6b7b7d216b8243a609f1c7231045e5be.tar.gz |
SERVER-47645 Must invalidate all sessions on stepdown
(cherry picked from commit 5f1a69aaf69bc12124f68e7b489a1437f9cdd575)
-rw-r--r-- | buildscripts/resmokeconfig/suites/replica_sets_multiversion.yml | 1 | ||||
-rw-r--r-- | etc/backports_required_for_multiversion_tests.yml | 2 | ||||
-rw-r--r-- | jstests/replsets/invalidate_sessions_on_stepdown.js | 67 | ||||
-rw-r--r-- | src/mongo/db/kill_sessions_local.cpp | 20 | ||||
-rw-r--r-- | src/mongo/db/kill_sessions_local.h | 6 | ||||
-rw-r--r-- | src/mongo/db/repl/replication_coordinator_impl.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/repl/replication_coordinator_impl_heartbeat.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/transaction_participant.cpp | 3 | ||||
-rw-r--r-- | src/mongo/db/transaction_validation.cpp | 4 |
9 files changed, 105 insertions, 2 deletions
diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multiversion.yml b/buildscripts/resmokeconfig/suites/replica_sets_multiversion.yml index 912c5cf419a..ac409f98c3f 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multiversion.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multiversion.yml @@ -7,6 +7,7 @@ selector: - multiversion_incompatible - requires_fcv_44 exclude_files: + - jstests/replsets/invalidate_sessions_on_stepdown.js executor: config: shell_options: diff --git a/etc/backports_required_for_multiversion_tests.yml b/etc/backports_required_for_multiversion_tests.yml index d9bb6fc1a98..d69c6afdb8c 100644 --- a/etc/backports_required_for_multiversion_tests.yml +++ b/etc/backports_required_for_multiversion_tests.yml @@ -69,6 +69,8 @@ replica_sets_multiversion: test_file: jstests/replsets/force_shutdown_primary.js - ticket: SERVER-47390 test_file: jstests/replsets/disallow_adding_initialized_node1.js +- ticket: SERVER-47645 + test_file: jstests/replsets/invalidate_sessions_on_stepdown.js sharding_multiversion: - ticket: SERVER-38691 diff --git a/jstests/replsets/invalidate_sessions_on_stepdown.js b/jstests/replsets/invalidate_sessions_on_stepdown.js new file mode 100644 index 00000000000..50916865780 --- /dev/null +++ b/jstests/replsets/invalidate_sessions_on_stepdown.js @@ -0,0 +1,67 @@ +/** + * Tests that a txnNumber for a transaction that was aborted in-memory can be reused by a new + * primary. Mongos does this as an optimization. + * @tags: [uses_transactions, uses_prepare_transaction] + */ +(function() { +"use strict"; + +const rst = new ReplSetTest({name: "invalidate_sessions_on_stepdown", nodes: 2}); +rst.startSet(); +rst.initiateWithHighElectionTimeout(); + +const dbName = "test"; +const collName = "coll"; +const node0 = rst.getPrimary(); +const node1 = rst.getSecondary(); +const node0Session = node0.startSession(); +const sessionId = node0Session.getSessionId(); +const node0SessionDB = node0Session.getDatabase(dbName); + +assert.commandWorked(node0SessionDB.coll.insert({a: 1}, {writeConcern: {"w": "majority"}})); + +jsTestLog("Run a transaction with txnNumber 0 on the primary."); +assert.commandWorked(node0SessionDB.runCommand({ + insert: collName, + documents: [{b: 1}], + txnNumber: NumberLong(0), + startTransaction: true, + autocommit: false +})); + +jsTestLog("Step up the secondary. The primary will abort the transaction when it steps down."); +assert.commandWorked(node1.adminCommand({replSetStepUp: 1})); +assert.eq(node1, rst.getPrimary()); + +const node1DB = node1.getDB(dbName); + +jsTestLog("Run a transaction with txnNumber 0 and the same session ID on the new primary."); +assert.commandWorked(node1DB.runCommand({ + insert: collName, + documents: [{c: 1}], + lsid: sessionId, + txnNumber: NumberLong(0), + startTransaction: true, + autocommit: false +})); +let res = assert.commandWorked(node1DB.adminCommand({ + prepareTransaction: 1, + lsid: sessionId, + txnNumber: NumberLong(0), + autocommit: false, + writeConcern: {w: "majority"} +})); +assert.commandWorked(node1DB.adminCommand({ + commitTransaction: 1, + commitTimestamp: res.prepareTimestamp, + lsid: sessionId, + txnNumber: NumberLong(0), + autocommit: false, + writeConcern: {w: "majority"} +})); +assert.eq(2, node0SessionDB.coll.find().itcount()); +assert.eq(0, node0SessionDB.coll.find({b: 1}).itcount()); +assert.eq(1, node0SessionDB.coll.find({c: 1}).itcount()); + +rst.stopSet(); +})(); diff --git a/src/mongo/db/kill_sessions_local.cpp b/src/mongo/db/kill_sessions_local.cpp index d1b57090e54..d1570e2f2e9 100644 --- a/src/mongo/db/kill_sessions_local.cpp +++ b/src/mongo/db/kill_sessions_local.cpp @@ -218,4 +218,24 @@ void yieldLocksForPreparedTransactions(OperationContext* opCtx) { ErrorCodes::InterruptedDueToReplStateChange); } +void invalidateSessionsForStepdown(OperationContext* opCtx) { + // It is illegal to invalidate the sessions if the operation has a session checked out. + invariant(!OperationContextSession::get(opCtx)); + + SessionKiller::Matcher matcherAllSessions( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(opCtx)}); + killSessionsAction(opCtx, + matcherAllSessions, + [](const ObservableSession& session) { + return !TransactionParticipant::get(session).transactionIsPrepared(); + }, + [](OperationContext* killerOpCtx, const SessionToKill& session) { + auto txnParticipant = TransactionParticipant::get(session); + if (!txnParticipant.transactionIsPrepared()) { + txnParticipant.invalidate(killerOpCtx); + } + }, + ErrorCodes::InterruptedDueToReplStateChange); +} + } // namespace mongo diff --git a/src/mongo/db/kill_sessions_local.h b/src/mongo/db/kill_sessions_local.h index 6b4da948e1c..e489589bdce 100644 --- a/src/mongo/db/kill_sessions_local.h +++ b/src/mongo/db/kill_sessions_local.h @@ -71,4 +71,10 @@ void killSessionsAbortAllPreparedTransactions(OperationContext* opCtx); */ void yieldLocksForPreparedTransactions(OperationContext* opCtx); +/** + * Invalidates sessions that do not have prepared transactions, since txnNumbers for transactions + * that were aborted in-memory may be reused on the new primary. + */ +void invalidateSessionsForStepdown(OperationContext* opCtx); + } // namespace mongo diff --git a/src/mongo/db/repl/replication_coordinator_impl.cpp b/src/mongo/db/repl/replication_coordinator_impl.cpp index 44991026173..096a799de37 100644 --- a/src/mongo/db/repl/replication_coordinator_impl.cpp +++ b/src/mongo/db/repl/replication_coordinator_impl.cpp @@ -2652,6 +2652,7 @@ void ReplicationCoordinatorImpl::stepDown(OperationContext* opCtx, lk.unlock(); yieldLocksForPreparedTransactions(opCtx); + invalidateSessionsForStepdown(opCtx); lk.lock(); @@ -3435,6 +3436,7 @@ void ReplicationCoordinatorImpl::_finishReplSetReconfig(OperationContext* opCtx, lk.unlock(); yieldLocksForPreparedTransactions(opCtx); + invalidateSessionsForStepdown(opCtx); lk.lock(); diff --git a/src/mongo/db/repl/replication_coordinator_impl_heartbeat.cpp b/src/mongo/db/repl/replication_coordinator_impl_heartbeat.cpp index 3c75ccf2843..9656e642eb0 100644 --- a/src/mongo/db/repl/replication_coordinator_impl_heartbeat.cpp +++ b/src/mongo/db/repl/replication_coordinator_impl_heartbeat.cpp @@ -464,6 +464,7 @@ void ReplicationCoordinatorImpl::_stepDownFinish( lk.unlock(); yieldLocksForPreparedTransactions(opCtx.get()); + invalidateSessionsForStepdown(opCtx.get()); lk.lock(); @@ -743,6 +744,7 @@ void ReplicationCoordinatorImpl::_heartbeatReconfigFinish( lk.unlock(); yieldLocksForPreparedTransactions(opCtx.get()); + invalidateSessionsForStepdown(opCtx.get()); lk.lock(); diff --git a/src/mongo/db/transaction_participant.cpp b/src/mongo/db/transaction_participant.cpp index 97453881f18..a151d95a7e5 100644 --- a/src/mongo/db/transaction_participant.cpp +++ b/src/mongo/db/transaction_participant.cpp @@ -583,6 +583,9 @@ void TransactionParticipant::Participant::beginOrContinueTransactionUnconditiona if (o().activeTxnNumber != txnNumber) { _beginMultiDocumentTransaction(opCtx, txnNumber); + } else { + invariant(o().txnState.isInSet(TransactionState::kInProgress | TransactionState::kPrepared), + str::stream() << "Current state: " << o().txnState); } // Assume we need to write an abort if we abort this transaction. This method is called only diff --git a/src/mongo/db/transaction_validation.cpp b/src/mongo/db/transaction_validation.cpp index 9342e07d1b9..f2360df18b6 100644 --- a/src/mongo/db/transaction_validation.cpp +++ b/src/mongo/db/transaction_validation.cpp @@ -53,8 +53,8 @@ const StringMap<int> retryableWriteCommands = {{"delete", 1}, {"_recvChunkStart", 1}}; // Commands that can be sent with session info but should not check out a session. -const StringMap<int> skipSessionCheckoutList = {{"coordinateCommitTransaction", 1}, - {"_recvChunkStart", 1}}; +const StringMap<int> skipSessionCheckoutList = { + {"coordinateCommitTransaction", 1}, {"_recvChunkStart", 1}, {"replSetStepDown", 1}}; const StringMap<int> transactionCommands = {{"commitTransaction", 1}, {"coordinateCommitTransaction", 1}, |