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

Uncommitted fast count updates leak out to other operations

    • Type: Icon: Bug Bug
    • Resolution: Done
    • Priority: Icon: Major - P3 Major - P3
    • None
    • Affects Version/s: None
    • Component/s: None
    • Catalog and Routing
    • ALL
    • Hide

      Minimal reproducer using only find() to demonstrate fast count updates from uncommitted transactions being leaked

      diff --git a/jstests/noPassthrough/repro.js b/jstests/noPassthrough/repro.js
      new file mode 100644
      index 00000000000..d54c5e98a8e
      --- /dev/null
      +++ b/jstests/noPassthrough/repro.js
      @@ -0,0 +1,42 @@
      +import {configureFailPoint} from "jstests/libs/fail_point_util.js";
      +import {funWithArgs} from "jstests/libs/parallel_shell_helpers.js";
      +import {ReplSetTest} from "jstests/libs/replsettest.js";
      +
      +let rst = new ReplSetTest({
      +    name: jsTestName(),
      +    nodes: 1,
      +});
      +rst.startSet();
      +rst.initiate();
      +let primary = rst.getPrimary();
      +
      +let db = primary.getDB("test");
      +let coll = db.getCollection("test");
      +
      +// Insert some data and check that fast count is correct.
      +for (let i = 0; i < 5; i++) {
      +    assert.commandWorked(coll.insert({_id: i}));
      +}
      +
      +assert(coll.find().count() == 5);
      +assert(coll.find().itcount() == 5);
      +
      +// Begin a transaction, hang after notifying the WiredTigerSizeStorer of a change.
      +let fp = configureFailPoint(primary, "WTHangAfterFastCountUpdate");
      +let awaitInsert =
      +    startParallelShell(funWithArgs(function(dbName, collName) {
      +                           assert.commandWorked(db.getSiblingDB(dbName)[collName].insert({_id: 5}));
      +                       }, db.getName(), coll.getName()), primary.port);
      +fp.wait();
      +
      +assert(coll.find().count() == 6);  // Should be 5 at this point.
      +assert(coll.find().itcount() == 5);
      +
      +// Commit the transaction.
      +fp.off();
      +awaitInsert();
      +
      +assert(coll.find().count() == 6);
      +assert(coll.find().itcount() == 6);
      +
      +rst.stopSet();
      diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp
      index bfa5b99b2ea..b9e0cd05556 100644
      --- a/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp
      +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp
      @@ -216,6 +216,7 @@ MONGO_FAIL_POINT_DEFINE(WTCompactRecordStoreEBUSY);
       MONGO_FAIL_POINT_DEFINE(WTRecordStoreUassertOutOfOrder);
       MONGO_FAIL_POINT_DEFINE(WTWriteConflictException);
       MONGO_FAIL_POINT_DEFINE(WTWriteConflictExceptionForReads);
      +MONGO_FAIL_POINT_DEFINE(WTHangAfterFastCountUpdate);
       
       StatusWith<std::string> WiredTigerRecordStore::parseOptionsField(const BSONObj options) {
           StringBuilder ss;
      @@ -1033,6 +1034,10 @@ Status WiredTigerRecordStore::_insertRecords(OperationContext* opCtx,
           }
           _changeNumRecordsAndDataSize(opCtx, nRecords, totalLength);
       
      +    if (MONGO_unlikely(WTHangAfterFastCountUpdate.shouldFail())) {
      +        WTHangAfterFastCountUpdate.pauseWhileSet(opCtx);
      +    }
      +
           if (_oplog && _oplog->getTruncateMarkers()) {
               // records[nRecords - 1] is the record in the oplog with the highest recordId.
               auto wall = [&] {
      
      
      Show
      Minimal reproducer using only find() to demonstrate fast count updates from uncommitted transactions being leaked diff --git a/jstests/noPassthrough/repro.js b/jstests/noPassthrough/repro.js new file mode 100644 index 00000000000..d54c5e98a8e --- /dev/ null +++ b/jstests/noPassthrough/repro.js @@ -0,0 +1,42 @@ + import {configureFailPoint} from "jstests/libs/fail_point_util.js" ; + import {funWithArgs} from "jstests/libs/parallel_shell_helpers.js" ; + import {ReplSetTest} from "jstests/libs/replsettest.js" ; + +let rst = new ReplSetTest({ + name: jsTestName(), + nodes: 1, +}); +rst.startSet(); +rst.initiate(); +let primary = rst.getPrimary(); + +let db = primary.getDB( "test" ); +let coll = db.getCollection( "test" ); + + // Insert some data and check that fast count is correct. + for (let i = 0; i < 5; i++) { + assert .commandWorked(coll.insert({_id: i})); +} + + assert (coll.find().count() == 5); + assert (coll.find().itcount() == 5); + + // Begin a transaction, hang after notifying the WiredTigerSizeStorer of a change. +let fp = configureFailPoint(primary, "WTHangAfterFastCountUpdate" ); +let awaitInsert = + startParallelShell(funWithArgs(function(dbName, collName) { + assert .commandWorked(db.getSiblingDB(dbName)[collName].insert({_id: 5})); + }, db.getName(), coll.getName()), primary.port); +fp.wait(); + + assert (coll.find().count() == 6); // Should be 5 at this point. + assert (coll.find().itcount() == 5); + + // Commit the transaction. +fp.off(); +awaitInsert(); + + assert (coll.find().count() == 6); + assert (coll.find().itcount() == 6); + +rst.stopSet(); diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp index bfa5b99b2ea..b9e0cd05556 100644 --- a/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp +++ b/src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp @@ -216,6 +216,7 @@ MONGO_FAIL_POINT_DEFINE(WTCompactRecordStoreEBUSY); MONGO_FAIL_POINT_DEFINE(WTRecordStoreUassertOutOfOrder); MONGO_FAIL_POINT_DEFINE(WTWriteConflictException); MONGO_FAIL_POINT_DEFINE(WTWriteConflictExceptionForReads); +MONGO_FAIL_POINT_DEFINE(WTHangAfterFastCountUpdate); StatusWith<std::string> WiredTigerRecordStore::parseOptionsField( const BSONObj options) { StringBuilder ss; @@ -1033,6 +1034,10 @@ Status WiredTigerRecordStore::_insertRecords(OperationContext* opCtx, } _changeNumRecordsAndDataSize(opCtx, nRecords, totalLength); + if (MONGO_unlikely(WTHangAfterFastCountUpdate.shouldFail())) { + WTHangAfterFastCountUpdate.pauseWhileSet(opCtx); + } + if (_oplog && _oplog->getTruncateMarkers()) { // records[nRecords - 1] is the record in the oplog with the highest recordId. auto wall = [&] {
    • CAR Team 2024-09-30

      We've observed this while working on SERVER-87119 to use the collection acquisition logic from the shard role API. I'm not sure why we aren't seeing this with validate today, but what we observed once we swapped the APIs was that validate:

      • Saw the fast count for an uncommitted transaction inserting two docs (ex: fast count 5 docs, itcount 3 docs)
      • Validate fixes the fast count to how many documents it saw (ex: fast count 3 docs, itcount 3 docs)
      • The uncommitted transaction commits two docs (ex: fast count 3 docs, itcount 5 docs)

      So validate made the fast count incorrect by rolling back the change for the uncommitted transaction.

            Assignee:
            jordi.olivares-provencio@mongodb.com Jordi Olivares Provencio
            Reporter:
            gregory.wlodarek@mongodb.com Gregory Wlodarek
            Votes:
            0 Vote for this issue
            Watchers:
            6 Start watching this issue

              Created:
              Updated:
              Resolved: