summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWilliam Schultz <william.schultz@mongodb.com>2019-08-14 13:35:07 +0000
committerevergreen <evergreen@mongodb.com>2019-08-14 13:35:07 +0000
commit69d0dd1dc4fb1f78d21c47aa5dd82aa9077b69eb (patch)
tree7bbd54b3139853c50223c2783845592efe9ca375
parent05d641c6cc47257783252e04160e4ef895a34f52 (diff)
downloadmongo-69d0dd1dc4fb1f78d21c47aa5dd82aa9077b69eb.tar.gz
SERVER-41959 Ensure that we gracefully reject all forms of transaction oplog entries in the applyOps command
-rw-r--r--jstests/replsets/transaction_ops_disallowed_in_applyOps.js111
-rw-r--r--src/mongo/db/repl/apply_ops.cpp1
-rw-r--r--src/mongo/db/repl/apply_ops.idl5
-rw-r--r--src/mongo/db/repl/oplog.cpp4
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);
}}},