summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuoxin Xu <ruoxin.xu@mongodb.com>2020-03-05 23:46:22 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-03-25 13:30:47 +0000
commitcaa29e31ae529b23c4dabb6878de58e2a7f0eda9 (patch)
treeea9a6047bc1d0922642833d56af5ed6555a754f0
parent4c4e1d0055ccfd3ca8f137d834a7b194a0961160 (diff)
downloadmongo-caa29e31ae529b23c4dabb6878de58e2a7f0eda9.tar.gz
SERVER-45987 Ensure apply op entries for deletes in multi-document transactions only include the document key
(cherry picked from commit ed0c0365b4baf8939d39edb766c8ddbbc33187d3)
-rw-r--r--jstests/change_streams/delete_in_txn_produces_correct_doc_key.js122
-rw-r--r--src/mongo/db/op_observer_impl.cpp3
2 files changed, 123 insertions, 2 deletions
diff --git a/jstests/change_streams/delete_in_txn_produces_correct_doc_key.js b/jstests/change_streams/delete_in_txn_produces_correct_doc_key.js
new file mode 100644
index 00000000000..89f6faa6f54
--- /dev/null
+++ b/jstests/change_streams/delete_in_txn_produces_correct_doc_key.js
@@ -0,0 +1,122 @@
+// Tests that 'delete' events within a multi-document transaction do not include the full document
+// in the 'documentKey' field. Exercises the fix for SERVER-45987.
+// @tags: [
+// uses_transactions,
+// assumes_against_mongod_not_mongos
+// ]
+
+(function() {
+ "use strict";
+
+ load("jstests/libs/collection_drop_recreate.js"); // For assert[Drop|Create]Collection.
+ load("jstests/libs/change_stream_util.js"); // For ChangeStreamTest.
+
+ const collName = "delete_in_txn_produces_correct_doc_key";
+
+ /**
+ * Test function which is used to test three types of delete-related commands, deleteOne(),
+ * deleteMany() and findAndModify(). Ensure only documentKey instead of a full document
+ * will be logged on entries for deletes in multi-document transactions, and also ensure that
+ * we can resume the change stream from these delete events.
+ */
+ function testDeleteInMultiDocTxn({collName, deleteCommand, expectedChanges}) {
+ // Initialize the collection.
+ const coll = assertDropAndRecreateCollection(db, collName);
+
+ assert.commandWorked(coll.insertMany([
+ {_id: 1, a: 0, fullDoc: "It's a full document!"},
+ {_id: 2, a: 0},
+ {_id: 3, a: 0},
+ {_id: 4, a: 1}
+ ]));
+
+ // Open a change stream on the test collection.
+ const cst = new ChangeStreamTest(db);
+ let cursor = cst.startWatchingChanges({
+ pipeline: [{$changeStream: {}}, {$project: {"lsid": 0, "txnNumber": 0}}],
+ collection: coll
+ });
+
+ // Start a transaction in which to perform the delete operation(s).
+ const session = db.getMongo().startSession();
+ const sessionDb = session.getDatabase(db.getName());
+ const sessionColl = sessionDb[collName];
+ session.startTransaction();
+
+ // Run the given 'deleteCommand' function to perform the delete(s).
+ deleteCommand(sessionColl);
+
+ // Commit the transaction so that the events become visible to the change stream.
+ assert.commandWorked(session.commitTransaction_forTesting());
+
+ // Verify that the stream returns the expected sequence of changes.
+ const changes =
+ cst.assertNextChangesEqual({cursor: cursor, expectedChanges: expectedChanges});
+
+ // Test the change stream can be resumed after a delete event from within the transaction.
+ cursor = cst.startWatchingChanges({
+ pipeline: [{$changeStream: {resumeAfter: changes[changes.length - 1]._id}}],
+ collection: coll
+ });
+ assert.commandWorked(coll.insert({_id: 5}));
+ assert.docEq(cst.getOneChange(cursor).documentKey, {_id: 5});
+
+ cst.cleanUp();
+ }
+
+ jsTestLog("Testing deleteOne() in a transaction.");
+ testDeleteInMultiDocTxn({
+ collName: collName,
+ deleteCommand: function(sessionColl) {
+ assert.commandWorked(sessionColl.deleteOne({_id: 1}));
+ },
+ expectedChanges: [
+ {
+ documentKey: {_id: 1},
+ ns: {db: db.getName(), coll: collName},
+ operationType: "delete",
+ },
+ ],
+ });
+
+ jsTestLog("Testing deleteMany() in a transaction.");
+ testDeleteInMultiDocTxn({
+ collName: collName,
+ deleteCommand: function(sessionColl) {
+ assert.commandWorked(sessionColl.deleteMany({a: 0, _id: {$gt: 0}}));
+ },
+ expectedChanges: [
+ {
+ documentKey: {_id: 1},
+ ns: {db: db.getName(), coll: collName},
+ operationType: "delete",
+ },
+ {
+ documentKey: {_id: 2},
+ ns: {db: db.getName(), coll: collName},
+ operationType: "delete",
+ },
+ {
+ documentKey: {_id: 3},
+ ns: {db: db.getName(), coll: collName},
+ operationType: "delete",
+ },
+ ],
+ });
+
+ // Note that findAndModify will delete only the *first* matching document.
+ jsTestLog("Testing findAndModify() in a transaction.");
+ testDeleteInMultiDocTxn({
+ collName: collName,
+ deleteCommand: function(sessionColl) {
+ sessionColl.findAndModify({query: {a: 0}, sort: {_id: 1}, remove: true});
+ },
+ expectedChanges: [
+ {
+ documentKey: {_id: 1},
+ ns: {db: db.getName(), coll: collName},
+ operationType: "delete",
+ },
+ ],
+ });
+}());
diff --git a/src/mongo/db/op_observer_impl.cpp b/src/mongo/db/op_observer_impl.cpp
index 62a6d9699cf..23e0f7427a4 100644
--- a/src/mongo/db/op_observer_impl.cpp
+++ b/src/mongo/db/op_observer_impl.cpp
@@ -518,8 +518,7 @@ void OpObserverImpl::onDelete(OperationContext* opCtx,
session->inActiveOrKilledMultiDocumentTransaction();
OpTimeBundle opTime;
if (inMultiDocumentTransaction) {
- auto operation =
- OplogEntry::makeDeleteOperation(nss, uuid, deletedDoc ? deletedDoc.get() : documentKey);
+ auto operation = OplogEntry::makeDeleteOperation(nss, uuid, documentKey);
session->addTransactionOperation(opCtx, operation);
} else {
opTime = replLogDelete(opCtx, nss, uuid, session, stmtId, fromMigrate, deletedDoc);