diff options
-rw-r--r-- | jstests/replsets/transaction_ops_disallowed_in_applyOps.js | 111 | ||||
-rw-r--r-- | src/mongo/db/repl/apply_ops.cpp | 1 | ||||
-rw-r--r-- | src/mongo/db/repl/apply_ops.idl | 5 | ||||
-rw-r--r-- | src/mongo/db/repl/oplog.cpp | 4 |
4 files changed, 117 insertions, 4 deletions
diff --git a/jstests/replsets/transaction_ops_disallowed_in_applyOps.js b/jstests/replsets/transaction_ops_disallowed_in_applyOps.js new file mode 100644 index 00000000000..cfa5ef1f710 --- /dev/null +++ b/jstests/replsets/transaction_ops_disallowed_in_applyOps.js @@ -0,0 +1,111 @@ +/** + * Test that transaction oplog entries are not accepted by the 'applyOps' command. + * + * In 4.2, there are no MongoDB backup services that rely on applyOps based mechanisms, and any + * other external tools that use applyOps should be converting transactional oplog entries to a + * non-transactional format before running them through applyOps. + * + * @tags: [uses_transactions, uses_prepare_transaction] + */ +(function() { +"use strict"; + +load('jstests/core/txns/libs/prepare_helpers.js'); + +const dbName = "test"; +const collName = "coll"; + +const rst = new ReplSetTest({ + name: collName, + nodes: 1, + // Make it easy to generate multiple oplog entries per transaction. + nodeOptions: {setParameter: {maxNumberOfTransactionOperationsInSingleOplogEntry: 1}} +}); +rst.startSet(); +rst.initiate(); + +const primary = rst.getPrimary(); + +// Initiate a session on the primary. +const sessionOptions = { + causalConsistency: false +}; +const primarySession = primary.getDB(dbName).getMongo().startSession(sessionOptions); +const primarySessionDb = primarySession.getDatabase(dbName); +const primarySessionColl = primarySessionDb[collName]; + +// Create a collection. +assert.commandWorked(primarySessionColl.insert({})); + +// +// Run transactions of different varieties and record the oplog entries they generate, so that we +// can later try to apply them via the 'applyOps' command. +// + +let oplog = primary.getDB("local")["oplog.rs"]; +let sessionId = primarySession.getSessionId().id; + +// Run an unprepared transaction that commits. +primarySession.startTransaction(); +assert.commandWorked(primarySessionColl.insert({x: 1})); +assert.commandWorked(primarySessionColl.insert({x: 2})); +assert.commandWorked(primarySession.commitTransaction_forTesting()); + +let txnNum = primarySession.getTxnNumber_forTesting(); +let unpreparedTxnOps = oplog.find({"lsid.id": sessionId, txnNumber: txnNum}).toArray(); +assert.eq(unpreparedTxnOps.length, 2, "unexpected op count: " + tojson(unpreparedTxnOps)); + +// Run a prepared transaction that commits. +primarySession.startTransaction(); +assert.commandWorked(primarySessionColl.insert({x: 1})); +assert.commandWorked(primarySessionColl.insert({x: 2})); +let prepareTs = PrepareHelpers.prepareTransaction(primarySession); +PrepareHelpers.commitTransaction(primarySession, prepareTs); + +txnNum = primarySession.getTxnNumber_forTesting(); +let preparedAndCommittedTxnOps = oplog.find({"lsid.id": sessionId, txnNumber: txnNum}).toArray(); +assert.eq(preparedAndCommittedTxnOps.length, + 3, + "unexpected op count: " + tojson(preparedAndCommittedTxnOps)); + +// Run a prepared transaction that aborts. +primarySession.startTransaction(); +assert.commandWorked(primarySessionColl.insert({x: 1})); +assert.commandWorked(primarySessionColl.insert({x: 2})); +PrepareHelpers.prepareTransaction(primarySession); +assert.commandWorked(primarySession.abortTransaction_forTesting()); + +txnNum = primarySession.getTxnNumber_forTesting(); +let preparedAndAbortedTxnOps = oplog.find({"lsid.id": sessionId, txnNumber: txnNum}).toArray(); +assert.eq( + preparedAndAbortedTxnOps.length, 3, "unexpected op count: " + tojson(preparedAndAbortedTxnOps)); + +// Clear out any documents that may have been created in the collection. +assert.commandWorked(primarySessionColl.remove({})); + +// +// Now we test running the various transaction ops we captured through the 'applyOps' command. +// + +let op = unpreparedTxnOps[0]; // in-progress op. +jsTestLog("Testing in-progress transaction op: " + tojson(op)); +assert.commandFailedWithCode(primarySessionDb.adminCommand({applyOps: [op]}), 31056); + +op = unpreparedTxnOps[1]; // implicit commit op. +jsTestLog("Testing unprepared implicit commit transaction op: " + tojson(op)); +assert.commandFailedWithCode(primarySessionDb.adminCommand({applyOps: [op]}), 31240); + +op = preparedAndCommittedTxnOps[1]; // implicit prepare op. +jsTestLog("Testing implicit prepare transaction op: " + tojson(op)); +assert.commandFailedWithCode(primarySessionDb.adminCommand({applyOps: [op]}), 51145); + +op = preparedAndCommittedTxnOps[2]; // prepared commit op. +jsTestLog("Testing prepared commit transaction op: " + tojson(op)); +assert.commandFailedWithCode(primarySessionDb.adminCommand({applyOps: [op]}), 50987); + +op = preparedAndAbortedTxnOps[2]; // prepared abort op. +jsTestLog("Testing prepared abort transaction op: " + tojson(op)); +assert.commandFailedWithCode(primarySessionDb.adminCommand({applyOps: [op]}), 50972); + +rst.stopSet(); +}()); diff --git a/src/mongo/db/repl/apply_ops.cpp b/src/mongo/db/repl/apply_ops.cpp index 4572d05c98e..ede0399d161 100644 --- a/src/mongo/db/repl/apply_ops.cpp +++ b/src/mongo/db/repl/apply_ops.cpp @@ -381,6 +381,7 @@ Status applyOps(OperationContext* opCtx, uassert( ErrorCodes::BadValue, "applyOps command can't have 'prepare' field", !info.getPrepare()); uassert(31056, "applyOps command can't have 'partialTxn' field.", !info.getPartialTxn()); + uassert(31240, "applyOps command can't have 'count' field.", !info.getCount()); // There's only one case where we are allowed to take the database lock instead of the global // lock - no preconditions; only CRUD ops; and non-atomic mode. diff --git a/src/mongo/db/repl/apply_ops.idl b/src/mongo/db/repl/apply_ops.idl index f78894145dd..80592642e2b 100644 --- a/src/mongo/db/repl/apply_ops.idl +++ b/src/mongo/db/repl/apply_ops.idl @@ -78,3 +78,8 @@ structs: optional: true description: "Specifies that this applyOps command is part of a multi-statement transaction that has not yet been committed or prepared." + count: + type: safeInt64 + optional: true + description: "The number of operations contained in this multi-oplog-entry + transaction."
\ No newline at end of file diff --git a/src/mongo/db/repl/oplog.cpp b/src/mongo/db/repl/oplog.cpp index d2c4d68411f..5be65dbaa5e 100644 --- a/src/mongo/db/repl/oplog.cpp +++ b/src/mongo/db/repl/oplog.cpp @@ -1053,10 +1053,6 @@ const StringMap<ApplyOpMetadata> kOpsMap = { const OplogEntry& entry, OplogApplication::Mode mode, boost::optional<Timestamp> stableTimestampForRecovery) -> Status { - // The 'applyOps' is either an implicit prepare oplog entry or is an entry that was not - // generated by a transaction. Partial and unprepared commit applyOps should - // have been dispatched before this point. - invariant(!entry.isPartialTransaction()); return entry.shouldPrepare() ? applyPrepareTransaction(opCtx, entry, mode) : applyApplyOpsOplogEntry(opCtx, entry, mode); }}}, |