diff options
-rw-r--r-- | jstests/noPassthrough/txn_index_catalog_changes.js | 80 | ||||
-rw-r--r-- | src/mongo/db/index/index_access_method.cpp | 18 |
2 files changed, 98 insertions, 0 deletions
diff --git a/jstests/noPassthrough/txn_index_catalog_changes.js b/jstests/noPassthrough/txn_index_catalog_changes.js new file mode 100644 index 00000000000..b29a8129a8b --- /dev/null +++ b/jstests/noPassthrough/txn_index_catalog_changes.js @@ -0,0 +1,80 @@ +/** + * Verifies that a multi-document transaction aborts with WriteConflictError if an index build has + * committed since the transaction's read snapshot. + * + * @tags: [ + * requires_replication, + * ] + */ +(function() { +'use strict'; + +const replTest = new ReplSetTest({nodes: 2}); +replTest.startSet(); +replTest.initiate(); + +const primary = replTest.getPrimary(); +const db = primary.getDB('test'); + +// Transaction inserting an index key. +{ + assert.commandWorked(db['c'].insertOne({_id: 0, num: 0})); + + const s0 = db.getMongo().startSession(); + s0.startTransaction(); + assert.commandWorked(s0.getDatabase('test')['c'].deleteOne({_id: 0})); + s0.commitTransaction(); + + const clusterTime = s0.getClusterTime().clusterTime; + + assert.commandWorked(db['c'].createIndex({num: 1})); + + // Start a transaction whose snapshot predates the completion of the index build, and which + // reserves an oplog entry after the index build commits. + try { + const s1 = db.getMongo().startSession(); + s1.startTransaction({readConcern: {level: "snapshot", atClusterTime: clusterTime}}); + s1.getDatabase('test').c.insertOne({_id: 1, num: 1}); + + // Transaction should have failed. + assert(0); + } catch (e) { + assert(e.hasOwnProperty("errorLabels"), tojson(e)); + assert.contains("TransientTransactionError", e.errorLabels, tojson(e)); + assert.eq(e["code"], ErrorCodes.WriteConflict, tojson(e)); + } +} + +db.c.drop(); + +// Transaction deleting an index key. +{ + assert.commandWorked(db.createCollection('c')); + + const s0 = db.getMongo().startSession(); + s0.startTransaction(); + assert.commandWorked(s0.getDatabase('test')['c'].insertOne({_id: 0, num: 0})); + s0.commitTransaction(); + + const clusterTime = s0.getClusterTime().clusterTime; + + assert.commandWorked(db['c'].createIndex({num: 1})); + + // Start a transaction whose snapshot predates the completion of the index build, and which + // reserves an oplog entry after the index build commits. + try { + const s1 = db.getMongo().startSession(); + s1.startTransaction({readConcern: {level: "snapshot", atClusterTime: clusterTime}}); + s1.getDatabase('test').c.deleteOne({_id: 0}); + + // Transaction should have failed. + assert(0); + } catch (e) { + assert(e.hasOwnProperty("errorLabels"), tojson(e)); + assert.contains("TransientTransactionError", e.errorLabels, tojson(e)); + assert.eq(e["code"], ErrorCodes.WriteConflict, tojson(e)); + } +} + +replTest.stopSet(); +})(); diff --git a/src/mongo/db/index/index_access_method.cpp b/src/mongo/db/index/index_access_method.cpp index 63148efb8cf..48621af7a33 100644 --- a/src/mongo/db/index/index_access_method.cpp +++ b/src/mongo/db/index/index_access_method.cpp @@ -1144,6 +1144,16 @@ Status SortedDataIndexAccessMethod::_indexKeysOrWriteToSideTable( *keysInsertedOut += inserted; } } else { + // Ensure that our snapshot is compatible with the index's minimum visibile snapshot. + const auto minVisibleTimestamp = _indexCatalogEntry->getMinimumVisibleSnapshot(); + const auto readTimestamp = + opCtx->recoveryUnit()->getPointInTimeReadTimestamp(opCtx).value_or( + opCtx->recoveryUnit()->getCatalogConflictingTimestamp()); + if (minVisibleTimestamp && !readTimestamp.isNull() && + readTimestamp < *minVisibleTimestamp) { + throw WriteConflictException(); + } + int64_t numInserted = 0; status = insertKeysAndUpdateMultikeyPaths( opCtx, @@ -1206,6 +1216,14 @@ void SortedDataIndexAccessMethod::_unindexKeysOrWriteToSideTable( options.dupsAllowed = options.dupsAllowed || !_indexCatalogEntry->isReady(opCtx) || (checkRecordId == CheckRecordId::On); + // Ensure that our snapshot is compatible with the index's minimum visibile snapshot. + const auto minVisibleTimestamp = _indexCatalogEntry->getMinimumVisibleSnapshot(); + const auto readTimestamp = opCtx->recoveryUnit()->getPointInTimeReadTimestamp(opCtx).value_or( + opCtx->recoveryUnit()->getCatalogConflictingTimestamp()); + if (minVisibleTimestamp && !readTimestamp.isNull() && readTimestamp < *minVisibleTimestamp) { + throw WriteConflictException(); + } + int64_t removed = 0; Status status = removeKeys(opCtx, keys, options, &removed); |