diff options
author | William Schultz <william.schultz@mongodb.com> | 2018-04-14 08:17:36 -0400 |
---|---|---|
committer | William Schultz <william.schultz@mongodb.com> | 2018-04-14 08:17:36 -0400 |
commit | 6eafc0790bb3602551127fa831ea859a989c384f (patch) | |
tree | 8e715f633eb2dfca26b56c592fa3665104e21512 | |
parent | 4095f8a2effeedbd17de4792af839b20e1a8e8d5 (diff) | |
download | mongo-6eafc0790bb3602551127fa831ea859a989c384f.tar.gz |
SERVER-33580 Restrict multi-statement transactions to replica set primaries
-rw-r--r-- | jstests/replsets/transactions_prohibited_on_secondaries.js | 82 | ||||
-rw-r--r-- | src/mongo/db/SConscript | 2 | ||||
-rw-r--r-- | src/mongo/db/session.cpp | 11 | ||||
-rw-r--r-- | src/mongo/db/session_test.cpp | 64 |
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); |