• Type: Bug
    • Resolution: Unresolved
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Component/s: None
    • Query Optimization
    • ALL
    • Hide
      import {configureFailPoint} from "jstests/libs/fail_point_util.js";
      
      const conn = MongoRunner.runMongod({});
      const testDb = conn.getDB(jsTestName());
      const coll = testDb.coll;
      const control = testDb.control;
      
      const filter = {$expr: {$eq: ["$a", 5]}};
      
      const docs = [];
      for (let i = 0; i < 100; i++) {
          docs.push({a: 5, b: i, c: i});
      }
      assert.commandWorked(coll.insert(docs));
      
      assert.commandWorked(coll.createIndex({a: 1}));
      assert.commandWorked(coll.createIndex({a: 1, b: 1}));
      assert.commandWorked(coll.createIndex({a: 1, c: 1}));
      
      // Two concurrent finds multiplan against the pre-flip, non-multikey {a: 1} index and pause
      // inside the multiplanner. Both plan correctly (point bounds on a non-multikey index) and return
      // all 100 documents; the second one's cache write promotes the stale entry to active.
      const fp = configureFailPoint(conn, "sleepWhileMultiplanning", {ms: 8000});
      const shells = [];
      for (let i = 0; i < 2; i++) {
          shells.push(startParallelShell(
              `
              const testDb = db.getSiblingDB("${testDb.getName()}");
              assert.commandWorked(testDb.control.insert({_id: ${i}}));
              const n = testDb.coll.find(${tojson(filter)}).itcount();
              assert.eq(100, n, "multiplanning find returned incomplete results");
              `,
              conn.port));
      }
      
      // Wait until both finds are inside the multiplanner sleep.
      assert.soon(() => control.countDocuments({}) === 2, "parallel shells did not start");
      sleep(2000);
      
      // Flip {a: 1} to multikey while the finds are paused. This clears the plan cache inline in the
      // write unit of work, but both finds have already snapshotted the non-multikey IndexEntry and
      // will write their cache entries after the clear — a stale entry that nothing re-screens.
      assert.commandWorked(coll.insert({a: [1, 2, 3]}));
      fp.off();
      shells.forEach((shell) => shell());
      
      // The plan cache now holds an active stale entry tagged to the pre-flip index. Replaying it
      // builds an unfiltered index scan with empty bounds on the now-multikey index, so reads return
      // nothing.
      assert.eq(100, coll.find(filter).hint({$natural: 1}).itcount());
      
      // Assertion fails
      assert.eq(100, coll.find(filter).itcount(), "find returned incomplete results from the plan cache");
      
      MongoRunner.stopMongod(conn);
      
      Show
      import {configureFailPoint} from "jstests/libs/fail_point_util.js" ; const conn = MongoRunner.runMongod({}); const testDb = conn.getDB(jsTestName()); const coll = testDb.coll; const control = testDb.control; const filter = {$expr: {$eq: [ "$a" , 5]}}; const docs = []; for (let i = 0; i < 100; i++) { docs.push({a: 5, b: i, c: i}); } assert .commandWorked(coll.insert(docs)); assert .commandWorked(coll.createIndex({a: 1})); assert .commandWorked(coll.createIndex({a: 1, b: 1})); assert .commandWorked(coll.createIndex({a: 1, c: 1})); // Two concurrent finds multiplan against the pre-flip, non-multikey {a: 1} index and pause // inside the multiplanner. Both plan correctly (point bounds on a non-multikey index) and return // all 100 documents; the second one's cache write promotes the stale entry to active. const fp = configureFailPoint(conn, "sleepWhileMultiplanning" , {ms: 8000}); const shells = []; for (let i = 0; i < 2; i++) { shells.push(startParallelShell( ` const testDb = db.getSiblingDB( "${testDb.getName()}" ); assert .commandWorked(testDb.control.insert({_id: ${i}})); const n = testDb.coll.find(${tojson(filter)}).itcount(); assert .eq(100, n, "multiplanning find returned incomplete results" ); `, conn.port)); } // Wait until both finds are inside the multiplanner sleep. assert .soon(() => control.countDocuments({}) === 2, "parallel shells did not start" ); sleep(2000); // Flip {a: 1} to multikey while the finds are paused. This clears the plan cache inline in the // write unit of work, but both finds have already snapshotted the non-multikey IndexEntry and // will write their cache entries after the clear — a stale entry that nothing re-screens. assert .commandWorked(coll.insert({a: [1, 2, 3]})); fp.off(); shells.forEach((shell) => shell()); // The plan cache now holds an active stale entry tagged to the pre-flip index. Replaying it // builds an unfiltered index scan with empty bounds on the now-multikey index, so reads return // nothing. assert .eq(100, coll.find(filter).hint({$natural: 1}).itcount()); // Assertion fails assert .eq(100, coll.find(filter).itcount(), "find returned incomplete results from the plan cache" ); MongoRunner.stopMongod(conn);
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      If an index becomes multikey during the planning of a query which has a stale snapshot of the non-multikey index, that query can activate its plan in the plan cache. At least in the repro attached, this results in missing/incorrect query results since we applied optimizations based on the non-multikeyness.

      One idea for the fix is to encode multikeyness of the index(es) when creating and inserting the plan cache entry. If multikeyness changes, then future queries of the same shape will not hit this cache entry. The LRU mechanism will handle evicting this dead entry.

            Assignee:
            Unassigned
            Reporter:
            Militsa Sotirova
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated: