ExportXMLWordPrintableJSON

    • Type: Bug
    • Resolution: Gone away
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Component/s: None
    • None
    • Storage Execution
    • ALL
    • Hide

      Cause

      Per-operation deltas are not recorded when the operation is applied. They are recorded by on-commit handlers that src/mongo/db/repl/oplog.cpp registers on the recovery unit (in updateObjectByRid(), deleteObjectByRid() and applyOperation_inlock(), for single and grouped inserts). For a prepared transaction those handlers do not run at prepare; they run when the transaction commits:

      recordSingleInsertDelta()                              oplog.cpp
      <- RecoveryUnit::_executeCommitHandlers()
      <- WriteUnitOfWork::commit()
      <- TransactionParticipant::Participant::commitPreparedTransaction()
      <- _applyCommitTransaction()                            transaction_oplog_application.cpp
      <- applyCommitTransaction()
      

      Suspending recording while the prepared operations are applied therefore does not stop these handlers, because they run later, after recording has resumed. The transaction is counted once by the per-operation handlers and once by the commit entry.

      The per-operation handlers also use each operation's own timestamp, which is the prepare timestamp, while the primary uses the commit timestamp. Without any fix, the standby counts the transaction once, but possibly toward a different checkpoint than the primary when a watermark falls between prepare and commit. 

      Proposed fix

      Register the on-commit recording handlers only if recording is active when the operation is applied:

      • New SizeCountCheckpointValidator::isRecording().
      • New helper onCommitRecordDelta() in oplog.cpp, used at all six registration sites.

      Operations applied while recording is suspended (i.e. a prepared transaction's) are then never recorded, and the commit entry is the only source.

       
      Show
      Cause Per-operation deltas are not recorded when the operation is applied. They are recorded by on-commit handlers that src/mongo/db/repl/oplog.cpp registers on the recovery unit (in updateObjectByRid() , deleteObjectByRid() and applyOperation_inlock() , for single and grouped inserts). For a prepared transaction those handlers do not run at prepare; they run when the transaction commits: recordSingleInsertDelta() oplog.cpp <- RecoveryUnit::_executeCommitHandlers() <- WriteUnitOfWork::commit() <- TransactionParticipant::Participant::commitPreparedTransaction() <- _applyCommitTransaction() transaction_oplog_application.cpp <- applyCommitTransaction() Suspending recording while the prepared operations are applied therefore does not stop these handlers, because they run later, after recording has resumed. The transaction is counted once by the per-operation handlers and once by the commit entry. The per-operation handlers also use each operation's own timestamp, which is the prepare timestamp, while the primary uses the commit timestamp. Without any fix, the standby counts the transaction once, but possibly toward a different checkpoint than the primary when a watermark falls between prepare and commit.  Proposed fix Register the on-commit recording handlers only if recording is active when the operation is applied: New SizeCountCheckpointValidator::isRecording() . New helper onCommitRecordDelta() in oplog.cpp , used at all six registration sites. Operations applied while recording is suspended (i.e. a prepared transaction's) are then never recorded, and the commit entry is the only source.  
    • Storage Execution 2026-11-09
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      Problem

      The fix for the companion hash ticket makes the standby record a prepared transaction's size and count from its commitTransaction entry, as the primary does. With that alone, the standby still counts the transaction's operations as well, so it fasserts with 13626005 at roughly double the primary's size and count:

      standby {size: 36, count: 2}, primary checkpoint has {size: 18, count: 1}
      standby {size: 41943180, count: 4}, primary checkpoint has {size: 20971590, count: 2}
      standby {size: 292716, count: 10}, primary checkpoint has {size: 193138, count: 10}   (updates only change size)
      

      Cause

      Per-operation deltas are not recorded when the operation is applied. They are recorded by on-commit handlers that src/mongo/db/repl/oplog.cpp registers on the recovery unit (in updateObjectByRid(), deleteObjectByRid() and applyOperation_inlock(), for single and grouped inserts). For a prepared transaction those handlers do not run at prepare; they run when the transaction commits:

      recordSingleInsertDelta()                              oplog.cpp
      <- RecoveryUnit::_executeCommitHandlers()
      <- WriteUnitOfWork::commit()
      <- TransactionParticipant::Participant::commitPreparedTransaction()
      <- _applyCommitTransaction()                            transaction_oplog_application.cpp
      <- applyCommitTransaction()
      

      Suspending recording while the prepared operations are applied therefore does not stop these handlers, because they run later, after recording has resumed. The transaction is counted once by the per-operation handlers and once by the commit entry.

      The per-operation handlers also use each operation's own timestamp, which is the prepare timestamp, while the primary uses the commit timestamp. Without any fix, the standby counts the transaction once, but possibly toward a different checkpoint than the primary when a watermark falls between prepare and commit. One run showed {{standby

      {size: 20971590, count: 2, hash: ...}

      , primary checkpoint has

      {size: 0, count: 0, hash: 0}

      }} in commit_and_abort_large_prepared_transactions.js. That fits this explanation but has not been confirmed.

      Evidence

      • Five disagg-only runs with only the companion fix (6aebdb17bec40d0007fad4d9, 6aebdb29bec40d0007fad591, 6aebdb3ddd3c7a0007edff6b, 6aebdb503b2ceb0007152512, 6aebdb6483ce9e0007d0f1b0): 39 to 47 failed tasks per run; 242 size-and-count and 30 size-only 13626005 mismatches in the sampled failed tests, all in prepared-transaction tests.
      • A local run of jstests/core/txns/commit_prepared_transaction.js in disagg_repl_jscore_passthrough with debug logging showed both records for the same transaction: the commit entry's (+29, +1) and the per-operation one at the prepare timestamp. The stack trace above was captured in that run.

      Proposed fix

      Register the on-commit recording handlers only if recording is active when the operation is applied:

      • New SizeCountCheckpointValidator::isRecording().
      • New helper onCommitRecordDelta() in oplog.cpp, used at all six registration sites.

      Operations applied while recording is suspended (i.e. a prepared transaction's) are then never recorded, and the commit entry is the only source. Candidate commit: 6bd8b6e7bff on stephanie.eristoff/SERVER-136298-txn-hash-unknown.

      Validation

      • New unit test CheckpointHashValidationTest.PreparedTransactionCountedOnceAtCommit (oplog_applier_checkpoint_validation_test.cpp) applies a prepare, then the commit and a checkpoint, through the real applier. It passes with the fix and fails without it ({{standby {size: 42, count: 2}

        }}).

      • Five disagg-only runs with both fixes and the fassert on (6aec69c946152d00071ef353, 6aec69dde595410007deb642, 6aec69f098fa580007328ba1, 6aec6a02e595410007deb69b, 6aec6a157a91680007887c0a): zero checkpoint fasserts (one task in the last run had not finished when checked).

      Acceptance criteria

      • Prepared transactions are counted exactly once on the standby, at the commit timestamp.
      • No 13626005 size or count mismatches for prepared transactions in disagg suites with the fassert on.
        
        

            Assignee:
            Cedric Sirianni
            Reporter:
            Stephanie Eristoff
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: