summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWilliam Schultz <william.schultz@mongodb.com>2018-04-14 08:17:36 -0400
committerWilliam Schultz <william.schultz@mongodb.com>2018-04-14 08:17:36 -0400
commit6eafc0790bb3602551127fa831ea859a989c384f (patch)
tree8e715f633eb2dfca26b56c592fa3665104e21512
parent4095f8a2effeedbd17de4792af839b20e1a8e8d5 (diff)
downloadmongo-6eafc0790bb3602551127fa831ea859a989c384f.tar.gz
SERVER-33580 Restrict multi-statement transactions to replica set primaries
-rw-r--r--jstests/replsets/transactions_prohibited_on_secondaries.js82
-rw-r--r--src/mongo/db/SConscript2
-rw-r--r--src/mongo/db/session.cpp11
-rw-r--r--src/mongo/db/session_test.cpp64
4 files changed, 159 insertions, 0 deletions
diff --git a/jstests/replsets/transactions_prohibited_on_secondaries.js b/jstests/replsets/transactions_prohibited_on_secondaries.js
new file mode 100644
index 00000000000..8ed3f463474
--- /dev/null
+++ b/jstests/replsets/transactions_prohibited_on_secondaries.js
@@ -0,0 +1,82 @@
+/**
+ * Test that transactions are prohibited from running on secondaries.
+ *
+ * @tags: [uses_transactions]
+ */
+(function() {
+ "use strict";
+
+ // In 4.0, we allow read-only transactions on secondaries when test commands are enabled, so we
+ // disable them in this test, to test that transactions on secondaries will be disallowed
+ // for production users.
+ jsTest.setOption('enableTestCommands', false);
+ TestData.authenticationDatabase = "local";
+
+ const dbName = "test";
+ const collName = "transactions_prohibited_on_secondaries";
+
+ // Start up the replica set. We want a stable topology, so make the secondary unelectable.
+ const replTest = new ReplSetTest({name: collName, nodes: 2});
+ replTest.startSet();
+ let config = replTest.getReplSetConfig();
+ config.members[1].priority = 0;
+ replTest.initiate(config);
+
+ const primary = replTest.getPrimary();
+ const secondary = replTest.getSecondary();
+
+ // Set slaveOk=true so that normal read commands would be allowed on the secondary.
+ secondary.setSlaveOk(true);
+
+ // Create a test collection that we can run commands against.
+ assert.commandWorked(primary.getDB(dbName).createCollection(collName));
+ replTest.awaitLastOpCommitted();
+
+ // Initiate a session on the secondary.
+ const sessionOptions = {causalConsistency: false};
+ const session = secondary.getDB(dbName).getMongo().startSession(sessionOptions);
+ const sessionDb = session.getDatabase(dbName);
+ let txnNumber = 0;
+
+ /**
+ * Verify that all given commands are disallowed from starting a transaction on a secondary by
+ * checking that each command fails with the expected error code.
+ */
+ function testCommands(commands, expectedErrorCode) {
+ // Verify secondary transactions are disallowed various command types.
+ for (let i = 0; i < commands.length; i++) {
+ let startTxnArgs = {
+ readConcern: {level: "snapshot"},
+ txnNumber: NumberLong(txnNumber++),
+ stmtId: NumberInt(0),
+ startTransaction: true,
+ autocommit: false,
+ };
+ let cmdObject = Object.assign(commands[i], startTxnArgs);
+ jsTestLog("Trying to start transaction on secondary with command: " +
+ tojson(cmdObject));
+ assert.commandFailedWithCode(sessionDb.runCommand(cmdObject), expectedErrorCode);
+ }
+ }
+
+ // Test read commands that are supported in transactions.
+ let readCommands = [
+ {find: collName},
+ {count: collName},
+ {aggregate: collName, pipeline: [{$project: {_id: 1}}], cursor: {}},
+ {distinct: collName, key: "_id"},
+ ];
+
+ jsTestLog("Testing read commands.");
+ testCommands(readCommands, 50789);
+
+ // Test one write command. Normal write commands should already be
+ // disallowed on secondaries so we don't test them exhaustively here.
+ let writeCommands = [{insert: collName, documents: [{_id: 0}]}];
+
+ jsTestLog("Testing write commands.");
+ testCommands(writeCommands, 50789);
+
+ session.endSession();
+ replTest.stopSet();
+}());
diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript
index 04346773588..049aeb9e702 100644
--- a/src/mongo/db/SConscript
+++ b/src/mongo/db/SConscript
@@ -625,6 +625,7 @@ env.Library(
'repl/oplog_entry',
's/sharding_api_d',
'views/views',
+ '$BUILD_DIR/mongo/db/commands/test_commands_enabled',
],
LIBDEPS_PRIVATE=[
'commands/server_status',
@@ -1781,6 +1782,7 @@ env.CppUnitTest(
LIBDEPS=[
'query_exec',
'$BUILD_DIR/mongo/db/repl/mock_repl_coord_server_fixture',
+ '$BUILD_DIR/mongo/client/read_preference'
],
)
diff --git a/src/mongo/db/session.cpp b/src/mongo/db/session.cpp
index 9f9d13b6a54..a7747ccf996 100644
--- a/src/mongo/db/session.cpp
+++ b/src/mongo/db/session.cpp
@@ -34,6 +34,7 @@
#include "mongo/db/catalog/index_catalog.h"
#include "mongo/db/commands/feature_compatibility_version_documentation.h"
+#include "mongo/db/commands/test_commands_enabled.h"
#include "mongo/db/concurrency/lock_state.h"
#include "mongo/db/concurrency/write_conflict_exception.h"
#include "mongo/db/db_raii.h"
@@ -316,6 +317,16 @@ void Session::beginOrContinueTxn(OperationContext* opCtx,
return;
}
+ // If the command specified a read preference that allows it to run on a secondary, and it is
+ // trying to execute an operation on a multi-statement transaction, then we throw an error.
+ // Transactions are only allowed to be run on a primary.
+ if (!getTestCommandsEnabled()) {
+ uassert(50789,
+ "readPreference=primary is the only allowed readPreference for multi-statement "
+ "transactions.",
+ !(autocommit && ReadPreferenceSetting::get(opCtx).canRunOnSecondary()));
+ }
+
invariant(!opCtx->lockState()->isLocked());
stdx::lock_guard<stdx::mutex> lg(_mutex);
diff --git a/src/mongo/db/session_test.cpp b/src/mongo/db/session_test.cpp
index d78f189901f..fd4a5674139 100644
--- a/src/mongo/db/session_test.cpp
+++ b/src/mongo/db/session_test.cpp
@@ -753,6 +753,70 @@ TEST_F(SessionTest, AutocommitRequiredOnEveryTxnOp) {
session.beginOrContinueTxn(opCtx(), txnNum, false, boost::none);
}
+TEST_F(SessionTest, TransactionsOnlyPermitAllowedReadPreferences) {
+ const auto sessionId = makeLogicalSessionIdForTest();
+ Session session(sessionId);
+ session.refreshFromStorageIfNeeded(opCtx());
+ TxnNumber txnNum = 1;
+
+ //
+ // Multi-statement transaction operations can only be run with 'readPreference=primary'.
+ //
+
+ auto startTxnWithReadPref = [&](ReadPreference readPref,
+ boost::optional<bool> autocommit,
+ boost::optional<bool> startTransaction) {
+ txnNum++;
+ ReadPreferenceSetting::get(opCtx()) = ReadPreferenceSetting(readPref);
+ session.beginOrContinueTxn(opCtx(), txnNum, autocommit, startTransaction);
+ };
+
+ // Shouldn't throw.
+ startTxnWithReadPref(ReadPreference::PrimaryOnly, false, true);
+ ASSERT_TRUE(session.inSnapshotReadOrMultiDocumentTransaction());
+
+ // All unsupported read preferences should throw.
+ ASSERT_THROWS_CODE(startTxnWithReadPref(ReadPreference::PrimaryPreferred, false, true),
+ AssertionException,
+ 50789);
+ ASSERT_THROWS_CODE(startTxnWithReadPref(ReadPreference::SecondaryOnly, false, true),
+ AssertionException,
+ 50789);
+ ASSERT_THROWS_CODE(startTxnWithReadPref(ReadPreference::SecondaryPreferred, false, true),
+ AssertionException,
+ 50789);
+ ASSERT_THROWS_CODE(
+ startTxnWithReadPref(ReadPreference::Nearest, false, true), AssertionException, 50789);
+
+ //
+ // Operations that are not on a multi-statement transaction are allowed to specify any
+ // readPreference.
+ //
+
+ auto activeTxnNum = TxnNumber{-1};
+
+ // None of these should throw. Each should start a transaction with a new, higher, transaction
+ // number.
+ startTxnWithReadPref(ReadPreference::PrimaryOnly, boost::none, boost::none);
+ ASSERT_GT(session.getActiveTxnNumberForTest(), activeTxnNum);
+ activeTxnNum = session.getActiveTxnNumberForTest();
+
+ startTxnWithReadPref(ReadPreference::PrimaryPreferred, boost::none, boost::none);
+ ASSERT_GT(session.getActiveTxnNumberForTest(), activeTxnNum);
+ activeTxnNum = session.getActiveTxnNumberForTest();
+
+ startTxnWithReadPref(ReadPreference::SecondaryOnly, boost::none, boost::none);
+ ASSERT_GT(session.getActiveTxnNumberForTest(), activeTxnNum);
+ activeTxnNum = session.getActiveTxnNumberForTest();
+
+ startTxnWithReadPref(ReadPreference::SecondaryPreferred, boost::none, boost::none);
+ ASSERT_GT(session.getActiveTxnNumberForTest(), activeTxnNum);
+ activeTxnNum = session.getActiveTxnNumberForTest();
+
+ startTxnWithReadPref(ReadPreference::Nearest, boost::none, boost::none);
+ ASSERT_GT(session.getActiveTxnNumberForTest(), activeTxnNum);
+}
+
TEST_F(SessionTest, SameTransactionPreservesStoredStatements) {
const auto sessionId = makeLogicalSessionIdForTest();
Session session(sessionId);