Uploaded image for project: 'Core Server'
  1. Core Server
  2. SERVER-96465

Change stream updateLookup is not returning documents when reading before reshardCollection even happened

    • Type: Icon: Bug Bug
    • Resolution: Unresolved
    • Priority: Icon: Major - P3 Major - P3
    • None
    • Affects Version/s: 8.1.0-rc0, 8.0.4, 7.0.16
    • Component/s: None
    • None
    • Query Execution
    • ALL

      While working on SERVER-95976 I discovered a bug that in case of the following events, updateLookup will return an empty document:

      • create a collection A (UUID1)
      • insert a document D1
      • perform an update on D1
      • reshard the collection A (UUID2)

      When reading the update event, we will not receive the fullDocument. The reason for that is _getCollectionDefaultCollator() call that occurs during creating of the ExpCtx for the updateLookup call (https://github.com/10gen/mongo/blob/v8.0/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.cpp#L620). In order to read the collation from the collection, it tries to acquire a lock on it by passing the UUID, however, reshardCollection command changes the UUID (UUID2), which causes the lock acquisition to fail with an NamespaceNotFound exception.

      In didn't run the test on older versions, but the code didn't seem to be modified recently and therefore should be reproducible on older versions

      Sample code:

      import {
          assertCreateCollection,
          assertDropCollection
      } from "jstests/libs/collection_drop_recreate.js";
      import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
      import {ChangeStreamTest} from "jstests/libs/query/change_stream_util.js";
      
      function assertUpdateLookupSemantcis(scenario, changeStreamOptions) {
          jsTest.log(`Running change stream with update lookup for ${scenario} case with ${tojson(changeStreamOptions)}`);    
          const collNameA = "collA";
          const collNameB = "collB";
          assertCreateCollection(db, collNameA);
          let cst = new ChangeStreamTest(db);
          let cursor = cst.startWatchingChanges({
              pipeline: [{$changeStream: {...changeStreamOptions, fullDocument: "updateLookup"}}],
              collection: collNameA
          });
          // Insert 'doc' into 'collA' and ensure it is seen in the change stream.
          const doc = {_id: 0, a: 1};
          assert.commandWorked(db.getCollection(collNameA).insert(doc));
          let expected = {
              documentKey: {_id: doc._id},
              fullDocument: doc,
              ns: {db: "test", coll: collNameA},
              operationType: "insert",
          };
          cst.assertNextChangesEqual({cursor: cursor, expectedChanges: [expected]});    
          // Update the 'doc' in order to generate the update event.
          assert.commandWorked(db.getCollection(collNameA).update({_id: doc._id}, {$inc: {a: 1}}));
          const updatedDocInCollA = {...doc, a: 2};
          // In case where a change stream is opened with the 'checkUUIDOnUpdateLookup' flag set to true,
          // no 'fullDocument' should be returned to the user as the collection on which 'updateLookup'
          // has been performed has a different UUID from the collection on which change stream has been
          // opened. In case of the flag being set to false or not present return the latest document on
          // the collection with the same name.
          let expectedFullDocument;
          if (scenario === 'rename') {
              // Rename collection collA -> collB.
              assert.commandWorked(db.getCollection(collNameA).renameCollection(collNameB));        
              // Create new collection with the old name, "collA", (yet UUID will be different) and insert
              // document with the same id.
              assertCreateCollection(db, collNameA);
              const newDocInNewCollA = {
                  ...doc,
                  b: "extra field in the new document in the new collection"
              };
              assert.commandWorked(db.getCollection(collNameA).insert(newDocInNewCollA));
              expectedFullDocument =
                  changeStreamOptions.checkUUIDOnUpdateLookup ? null : newDocInNewCollA;
          } else if (scenario === 'reshard') {
              // Reshard the collection in order to generate the new collection with the same name, but
              // different UUID.
              assert.commandWorked(db.adminCommand({
                  reshardCollection: db.getCollection(collNameA).getFullName(),
                  key: {_id: 1},
                  numInitialChunks: 2
              }));        
              expectedFullDocument =
                  changeStreamOptions.checkUUIDOnUpdateLookup ? null : updatedDocInCollA;
          }    expected = {
              documentKey: {_id: doc._id},
              fullDocument: expectedFullDocument,
              ns: {db: "test", coll: collNameA},
              operationType: "update",
          };
          cst.assertNextChangesEqual({cursor: cursor, expectedChanges: [expected]});    // Cleanup.
          cst.cleanUp();
          assertDropCollection(db, collNameA);
          assertDropCollection(db, collNameB);
      }
      
      if (FixtureHelpers.isMongos(db)) {
          assertUpdateLookupSemantcis('reshard', {});
          assertUpdateLookupSemantcis('reshard', {checkUUIDOnUpdateLookup: false});
          assertUpdateLookupSemantcis('reshard', {checkUUIDOnUpdateLookup: true});
      } 

            Assignee:
            Unassigned Unassigned
            Reporter:
            denis.grebennicov@mongodb.com Denis Grebennicov
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated: