summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTess Avitabile <tess.avitabile@mongodb.com>2020-06-02 09:26:36 -0400
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-06-05 17:42:00 +0000
commitc1bc1b6d6b7b7d216b8243a609f1c7231045e5be (patch)
tree05a182991f3f661cd10256462f0c7fb9444b7c29
parent779f9e866b798cc8b6de7ebe286b516d26f3d84b (diff)
downloadmongo-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.yml1
-rw-r--r--etc/backports_required_for_multiversion_tests.yml2
-rw-r--r--jstests/replsets/invalidate_sessions_on_stepdown.js67
-rw-r--r--src/mongo/db/kill_sessions_local.cpp20
-rw-r--r--src/mongo/db/kill_sessions_local.h6
-rw-r--r--src/mongo/db/repl/replication_coordinator_impl.cpp2
-rw-r--r--src/mongo/db/repl/replication_coordinator_impl_heartbeat.cpp2
-rw-r--r--src/mongo/db/transaction_participant.cpp3
-rw-r--r--src/mongo/db/transaction_validation.cpp4
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},