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

Incorrect $listCatalog behavior in presence of a concurrent collection rename in v7.0

    • Type: Icon: Bug Bug
    • Resolution: Fixed
    • Priority: Icon: Major - P3 Major - P3
    • 7.1.1, 7.2.0-rc0, 7.0.4, 6.0.13
    • Affects Version/s: 6.0.0, 7.0.0
    • Component/s: None
    • Labels:
      None
    • Catalog and Routing
    • Fully Compatible
    • ALL
    • v7.1, v7.0, v6.0, v5.0, v4.4
    • Hide

      To reproduce outside of Mongosync:

      • Launch and connect to v7.0 replica set (I used mlaunch)
      m 7.0.0
      mlaunch --replicaset --nodes 3
      mongosh mongodb://localhost:27017,localhost:27018,localhost:27019

      - You will need 2 mongosh for this repro.

      • Create collection and index
      use test
      db.createCollection("coll")
      db.coll.createIndex({x:1})
      • On mongosh 1, run the aggregation pipeline to retrieve the index in an infinite loop (replace collectionUUID with actual value)
      stages1 = [{ "$group": Unknown macro: { "_id"}} }, { "$unwind":{ "path": "$catalogEntry" }}, { "$unwind":{ "path": "$catalogEntry.md.indexes" }}, { "$unset": "catalogEntry.md.indexes.spec.ns" }, { "$match":{ "catalogEntry.md.indexes.ready": true }}, { "$set": { "catalogEntry.md.indexes.spec.sparse": { "$cond": { "if":Unknown macro: { "$eq"}, "then": "$$REMOVE", "else": { "$toBool": "$catalogEntry.md.indexes.spec.sparse" } } }, "catalogEntry.md.indexes.spec.expireAfterSeconds": { "$cond": { "if":Unknown macro: { "$eq"}, "then": "$$REMOVE", "else": { "$toInt": "$catalogEntry.md.indexes.spec.expireAfterSeconds" } } }, "catalogEntry.md.indexes.spec.bits": { "$cond": { "if":Unknown macro: { "$eq"}, "then": "$$REMOVE", "else": { "$toInt": "$catalogEntry.md.indexes.spec.bits" } } } } }, { "$group": { "_id": "$catalogEntry.md.indexes.spec.name", "shards":{ "$push": "$catalogEntry.shard" }, "specs": { "$push": { "$objectToArray": { "$ifNull": [ "$catalogEntry.md.indexes.spec", {}] } } }, "allShards": { "$first": "$allShards" } } }]
      
      stages2 = { "$project": { "numShards":{ "$size": "$allShards" }, "missingFromShards": { "$setDifference": [ "$allShards", "$shards"] }, "spec": { "$arrayToObject":{ "$first": "$specs" }}, "inconsistentOptions": { "$setDifference": [ { "$reduce": { "input": "$specs", "initialValue":{ "$arrayElemAt": [ "$specs", 0] }, "in": { "$setUnion": [ "$$value", "$$this"] } } }, { "$reduce": { "input": "$specs", "initialValue":{ "$arrayElemAt": [ "$specs", 0] }, "in": { "$setIntersection": [ "$$value", "$$this"] } } }] } } }
      
      res = db.runCommand({"aggregate": "coll", "collectionUUID": new UUID("85c8ab7b-d107-4768-913e-19f0b107eb2a"), "readConcern": {"level": "majority", "afterClusterTime": Timestamp({ t: 1693905823, i: 20 })}, cursor: {}, pipeline: [\{"$listCatalog": {}}].concat(stages1).concat([\{"$match": {_id: "x_1"} }]).concat([stages2])}).cursor.firstBatch.length
      
      while (res) { res = db.runCommand({"aggregate": "coll", "collectionUUID": new UUID("85c8ab7b-d107-4768-913e-19f0b107eb2a"), "readConcern": {"level": "majority", "afterClusterTime": Timestamp({ t: 1693905823, i: 20 })}, cursor: {}, pipeline: [\{"$listCatalog": {}}].concat(stages1).concat([\{"$match": {_id: "x_1"} }]).concat([stages2])}).cursor.firstBatch.length }
      

       - On mongosh 2, rename the collection

      use test
      db.coll.renameCollection("coll2")

      The infinite loop in mongosh 1 will exit. In v6.0.8, the infinite loop exits with a CollectionUUIDMismatch error as expected, but v.7.0.0, the infinite loop exits because the server returns an empty list of index.

       

      Show
      To reproduce outside of Mongosync: Launch and connect to v7.0 replica set (I used mlaunch) m 7.0.0 mlaunch --replicaset --nodes 3 mongosh mongodb: //localhost:27017,localhost:27018,localhost:27019 - You will need 2 mongosh for this repro. Create collection and index use test db.createCollection( "coll" ) db.coll.createIndex({x:1}) On mongosh 1, run the aggregation pipeline to retrieve the index in an infinite loop (replace collectionUUID with actual value) stages1 = [{ "$group" : Unknown macro: { "_id" }} }, { "$unwind" :{ "path" : "$catalogEntry" }}, { "$unwind" :{ "path" : "$catalogEntry.md.indexes" }}, { "$unset" : "catalogEntry.md.indexes.spec.ns" }, { "$match" :{ "catalogEntry.md.indexes.ready" : true }}, { "$set" : { "catalogEntry.md.indexes.spec.sparse" : { "$cond" : { " if " :Unknown macro: { "$eq" }, "then" : "$$REMOVE" , " else " : { "$toBool" : "$catalogEntry.md.indexes.spec.sparse" } } }, "catalogEntry.md.indexes.spec.expireAfterSeconds" : { "$cond" : { " if " :Unknown macro: { "$eq" }, "then" : "$$REMOVE" , " else " : { "$toInt" : "$catalogEntry.md.indexes.spec.expireAfterSeconds" } } }, "catalogEntry.md.indexes.spec.bits" : { "$cond" : { " if " :Unknown macro: { "$eq" }, "then" : "$$REMOVE" , " else " : { "$toInt" : "$catalogEntry.md.indexes.spec.bits" } } } } }, { "$group" : { "_id" : "$catalogEntry.md.indexes.spec.name" , "shards" :{ "$push" : "$catalogEntry.shard" }, "specs" : { "$push" : { "$objectToArray" : { "$ifNull" : [ "$catalogEntry.md.indexes.spec" , {}] } } }, "allShards" : { "$first" : "$allShards" } } }] stages2 = { "$project" : { "numShards" :{ "$size" : "$allShards" }, "missingFromShards" : { "$setDifference" : [ "$allShards" , "$shards" ] }, "spec" : { "$arrayToObject" :{ "$first" : "$specs" }}, "inconsistentOptions" : { "$setDifference" : [ { "$reduce" : { "input" : "$specs" , "initialValue" :{ "$arrayElemAt" : [ "$specs" , 0] }, "in" : { "$setUnion" : [ "$$value" , "$$ this " ] } } }, { "$reduce" : { "input" : "$specs" , "initialValue" :{ "$arrayElemAt" : [ "$specs" , 0] }, "in" : { "$setIntersection" : [ "$$value" , "$$ this " ] } } }] } } } res = db.runCommand({ "aggregate" : "coll" , "collectionUUID" : new UUID( "85c8ab7b-d107-4768-913e-19f0b107eb2a" ), "readConcern" : { "level" : "majority" , "afterClusterTime" : Timestamp({ t: 1693905823, i: 20 })}, cursor: {}, pipeline: [\{ "$listCatalog" : {}}].concat(stages1).concat([\{ "$match" : {_id: "x_1" } }]).concat([stages2])}).cursor.firstBatch.length while (res) { res = db.runCommand({ "aggregate" : "coll" , "collectionUUID" : new UUID( "85c8ab7b-d107-4768-913e-19f0b107eb2a" ), "readConcern" : { "level" : "majority" , "afterClusterTime" : Timestamp({ t: 1693905823, i: 20 })}, cursor: {}, pipeline: [\{ "$listCatalog" : {}}].concat(stages1).concat([\{ "$match" : {_id: "x_1" } }]).concat([stages2])}).cursor.firstBatch.length }  - On mongosh 2, rename the collection use test db.coll.renameCollection( "coll2" ) The infinite loop in mongosh 1 will exit. In v6.0.8, the infinite loop exits with a CollectionUUIDMismatch error as expected, but v.7.0.0, the infinite loop exits because the server returns an empty list of index.  
    • Execution EMEA Team 2023-10-16, Execution EMEA Team 2023-10-30

      Mongosync uses $listCatalog to list indexes belonging to collections as follows:

      stages1 = [{ "$group": { "_id": null, "catalogEntry": { "$push": "$$ROOT" }, "allShards": { "$addToSet": "$shard" } } }, { "$unwind": { "path": "$catalogEntry" } }, { "$unwind": { "path": "$catalogEntry.md.indexes" } }, { "$unset": "catalogEntry.md.indexes.spec.ns" }, { "$match": { "catalogEntry.md.indexes.ready": true } }, { "$set": { "catalogEntry.md.indexes.spec.sparse": { "$cond": { "if": { "$eq": ["missing", { "$type": "$catalogEntry.md.indexes.spec.sparse" }] }, "then": "$$REMOVE", "else": { "$toBool": "$catalogEntry.md.indexes.spec.sparse" } } }, "catalogEntry.md.indexes.spec.expireAfterSeconds": { "$cond": { "if": { "$eq": ["missing", { "$type": "$catalogEntry.md.indexes.spec.expireAfterSeconds" }] }, "then": "$$REMOVE", "else": { "$toInt": "$catalogEntry.md.indexes.spec.expireAfterSeconds" } } }, "catalogEntry.md.indexes.spec.bits": { "$cond": { "if": { "$eq": ["missing", { "$type": "$catalogEntry.md.indexes.spec.bits" }] }, "then": "$$REMOVE", "else": { "$toInt": "$catalogEntry.md.indexes.spec.bits" } } } } }, { "$group": { "_id": "$catalogEntry.md.indexes.spec.name", "shards": { "$push": "$catalogEntry.shard" }, "specs": { "$push": { "$objectToArray": { "$ifNull": [ "$catalogEntry.md.indexes.spec", {}] } } }, "allShards": { "$first": "$allShards" } } }]
      stages2 = { "$project": { "numShards": { "$size": "$allShards" }, "missingFromShards": { "$setDifference": [ "$allShards", "$shards"] }, "spec": { "$arrayToObject": { "$first": "$specs" } }, "inconsistentOptions": { "$setDifference": [ { "$reduce": { "input": "$specs", "initialValue": { "$arrayElemAt": [ "$specs", 0] }, "in": { "$setUnion": [ "$$value", "$$this"] } } }, { "$reduce": { "input": "$specs", "initialValue": { "$arrayElemAt": [ "$specs", 0] }, "in": { "$setIntersection": [ "$$value", "$$this"] } } }] } } }res = 
      db.runCommand({"aggregate": "coll", "collectionUUID": new UUID("85c8ab7b-d107-4768-913e-19f0b107eb2a"), "readConcern": {"level": "majority", "afterClusterTime": Timestamp({ t: 1693905823, i: 20 })}, cursor: {}, pipeline: [{"$listCatalog": {}}].concat(stages1).concat([{"$match": {_id: "x_1"} }]).concat([stages2])})

      If the collection gets renamed while the aggregation pipeline is running, Mongosync expects to get a CollectionUUIDMismatch error for the server. That is the behavior for v6.0.8.

      In v7.0, the result of the aggregation pipeline is actually empty, which causes Mongosync to incorrectly believe that there is no index on the collection.

            Assignee:
            jordi.olivares-provencio@mongodb.com Jordi Olivares Provencio
            Reporter:
            craven.huynh@mongodb.com Craven Huynh
            Votes:
            0 Vote for this issue
            Watchers:
            9 Start watching this issue

              Created:
              Updated:
              Resolved: