From e6aee55a2491414e1c093715d61999f7269bc47e Mon Sep 17 00:00:00 2001 From: Josef Ahmad Date: Fri, 30 Sep 2022 14:24:43 +0000 Subject: SERVER-69847 Handle repl rollback of global indexes --- .../shardsvr_global_index_crud_bulk.js | 15 +- jstests/noPassthrough/shardsvr_global_index_ddl.js | 17 +- jstests/replsets/global_index_ddl_rollback.js | 56 --- jstests/replsets/global_index_rollback.js | 537 +++++++++++++++++++++ .../shardsvr_global_index_crud_rollback.js | 87 ---- src/mongo/db/auth/auth_op_observer.h | 3 +- src/mongo/db/free_mon/free_mon_op_observer.h | 3 +- src/mongo/db/global_index.cpp | 35 +- src/mongo/db/global_index.h | 11 + src/mongo/db/global_index_test.cpp | 54 ++- src/mongo/db/op_observer/fcv_op_observer.h | 3 +- src/mongo/db/op_observer/op_observer.h | 3 +- src/mongo/db/op_observer/op_observer_impl.cpp | 24 +- src/mongo/db/op_observer/op_observer_impl.h | 3 +- src/mongo/db/op_observer/op_observer_noop.h | 3 +- src/mongo/db/op_observer/op_observer_registry.h | 5 +- .../user_write_block_mode_op_observer.h | 3 +- src/mongo/db/repl/oplog.cpp | 18 +- src/mongo/db/repl/oplog_entry.cpp | 4 +- .../db/repl/primary_only_service_op_observer.h | 3 +- src/mongo/db/repl/rollback_impl.cpp | 55 ++- .../db/repl/tenant_migration_donor_op_observer.h | 3 +- .../repl/tenant_migration_recipient_op_observer.h | 3 +- src/mongo/db/s/config_server_op_observer.h | 3 +- src/mongo/db/s/range_deleter_service_op_observer.h | 3 +- src/mongo/db/s/resharding/resharding_op_observer.h | 3 +- src/mongo/db/s/shard_server_op_observer.h | 3 +- .../db/serverless/shard_split_donor_op_observer.h | 3 +- .../idl/cluster_server_parameter_op_observer.h | 3 +- 29 files changed, 723 insertions(+), 243 deletions(-) delete mode 100644 jstests/replsets/global_index_ddl_rollback.js create mode 100644 jstests/replsets/global_index_rollback.js delete mode 100644 jstests/replsets/shardsvr_global_index_crud_rollback.js diff --git a/jstests/noPassthrough/shardsvr_global_index_crud_bulk.js b/jstests/noPassthrough/shardsvr_global_index_crud_bulk.js index d875a0ac71b..d73cb9c7d7b 100644 --- a/jstests/noPassthrough/shardsvr_global_index_crud_bulk.js +++ b/jstests/noPassthrough/shardsvr_global_index_crud_bulk.js @@ -12,14 +12,11 @@ (function() { "use strict"; -function uuidToString(uuid) { - const [_, uuidString] = uuid.toString().match(/"((?:\\.|[^"\\])*)"/); - return uuidString; -} +load('jstests/libs/uuid_util.js'); function entriesInContainer(primary, uuid) { return primary.getDB("system") - .getCollection("globalIndexes." + uuidToString(uuid)) + .getCollection("globalIndexes." + extractUUIDFromObject(uuid)) .find() .itcount(); } @@ -449,7 +446,7 @@ assert.commandFailedWithCode(adminDB.runCommand({_shardsvrWriteGlobalIndexKeys: // triggering DuplicateKey error on inserting the same index key. assert.eq(1, primary.getDB("system") - .getCollection("globalIndexes." + uuidToString(globalIndexUUID)) + .getCollection("globalIndexes." + extractUUIDFromObject(globalIndexUUID)) .find({_id: {sk0: "first", _id: "first"}}) .itcount()); session.startTransaction(); @@ -462,7 +459,7 @@ assert.commandFailedWithCode(adminDB.runCommand({_shardsvrWriteGlobalIndexKeys: session.abortTransaction(); assert.eq(1, primary.getDB("system") - .getCollection("globalIndexes." + uuidToString(otherGlobalIndexUUID)) + .getCollection("globalIndexes." + extractUUIDFromObject(otherGlobalIndexUUID)) .find({_id: {sk0: "secondOnSecondContainer", _id: "secondOnSecondContainer"}}) .itcount()); session.startTransaction(); @@ -506,12 +503,12 @@ assert.commandFailedWithCode(adminDB.runCommand({_shardsvrWriteGlobalIndexKeys: // triggering DuplicateKey error on inserting the same index key. assert.eq(1, primary.getDB("system") - .getCollection("globalIndexes." + uuidToString(globalIndexUUID)) + .getCollection("globalIndexes." + extractUUIDFromObject(globalIndexUUID)) .find({_id: {sk0: "globalIndexKey", _id: "globalIndexKey"}}) .itcount()); assert.eq(1, primary.getDB("system") - .getCollection("globalIndexes." + uuidToString(globalIndexUUID)) + .getCollection("globalIndexes." + extractUUIDFromObject(globalIndexUUID)) .find({_id: {sk0: "globalIndexKey2", _id: "globalIndexKey"}}) .itcount()); session.startTransaction(); diff --git a/jstests/noPassthrough/shardsvr_global_index_ddl.js b/jstests/noPassthrough/shardsvr_global_index_ddl.js index c47df2a1c8c..39a0205ec25 100644 --- a/jstests/noPassthrough/shardsvr_global_index_ddl.js +++ b/jstests/noPassthrough/shardsvr_global_index_ddl.js @@ -11,11 +11,7 @@ "use strict"; load("jstests/libs/collection_drop_recreate.js"); // For assertDropCollection. - -function uuidToString(uuid) { - const [_, uuidString] = uuid.toString().match(/"((?:\\.|[^"\\])*)"/); - return uuidString; -} +load('jstests/libs/uuid_util.js'); function verifyCollectionExists(node, globalIndexUUID, namespace) { const systemDB = node.getDB("system"); @@ -80,6 +76,11 @@ function verifyOplogEntry(node, globalIndexUUID, namespace, commandString, lsid, assert.eq(oplogEntry.op, "c"); assert.eq(oplogEntry.ui, globalIndexUUID); assert.docEq(oplogEntry.o, commandStringNamespaceObj); + if (commandString === "dropGlobalIndex") { + assert.eq(0, oplogEntry.o2.numRecords); + } else { + assert.eq(undefined, oplogEntry.o2); + } // lsid and txnNumber are either both present (retryable writes) or absent. assert((lsid && txnNumber) || (!lsid && !txnNumber)); @@ -96,7 +97,7 @@ function verifyCommandIsRetryableWrite(node, command, oplogCommandString, setup) const lsid = session.getSessionId(); const txnNumber = NumberLong(10); const indexUUID = UUID(); - const globalIndexCollName = "globalIndexes." + uuidToString(indexUUID); + const globalIndexCollName = "globalIndexes." + extractUUIDFromObject(indexUUID); var commandObj = {}; commandObj[command] = indexUUID; @@ -147,7 +148,7 @@ function testCreateGlobalIndex(rst) { const primary = rst.getPrimary(); const adminDB = primary.getDB("admin"); const globalIndexUUID = UUID(); - const globalIndexCollName = "globalIndexes." + uuidToString(globalIndexUUID); + const globalIndexCollName = "globalIndexes." + extractUUIDFromObject(globalIndexUUID); const oplogCommandString = "createGlobalIndex"; verifyMultiDocumentTransactionDisallowed(primary, {_shardsvrCreateGlobalIndex: UUID()}); @@ -173,7 +174,7 @@ function testDropGlobalIndex(rst) { const primary = rst.getPrimary(); const adminDB = primary.getDB("admin"); const globalIndexUUID = UUID(); - const globalIndexCollName = "globalIndexes." + uuidToString(globalIndexUUID); + const globalIndexCollName = "globalIndexes." + extractUUIDFromObject(globalIndexUUID); const oplogCommandString = "dropGlobalIndex"; verifyMultiDocumentTransactionDisallowed(primary, {_shardsvrDropGlobalIndex: UUID()}); diff --git a/jstests/replsets/global_index_ddl_rollback.js b/jstests/replsets/global_index_ddl_rollback.js deleted file mode 100644 index 5243f6d4d3d..00000000000 --- a/jstests/replsets/global_index_ddl_rollback.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Tests that global index container ddl operations can be rolled back. - * - * @tags: [ - * featureFlagGlobalIndexes, - * requires_fcv_62, - * requires_replication, - * ] - */ -(function() { -'use strict'; - -load('jstests/replsets/libs/rollback_test.js'); - -function uuidToNss(uuid) { - const [_, uuidString] = uuid.toString().match(/"((?:\\.|[^"\\])*)"/); - return "globalIndexes." + uuidString; -} - -const rollbackTest = new RollbackTest(jsTestName()); - -const primary = rollbackTest.getPrimary(); -const adminDB = primary.getDB("admin"); -const globalIndexCreateUUID = UUID(); -const globalIndexDropUUID = UUID(); - -// Create a global index container to be dropped. -assert.commandWorked(adminDB.runCommand({_shardsvrCreateGlobalIndex: globalIndexDropUUID})); - -rollbackTest.transitionToRollbackOperations(); - -// Create a global index container to be rolled back. -assert.commandWorked(adminDB.runCommand({_shardsvrCreateGlobalIndex: globalIndexCreateUUID})); -// Drop a global index container, operation should be rolled back. -assert.commandWorked(adminDB.runCommand({_shardsvrDropGlobalIndex: globalIndexDropUUID})); - -// Perform the rollback. -rollbackTest.transitionToSyncSourceOperationsBeforeRollback(); -rollbackTest.transitionToSyncSourceOperationsDuringRollback(); -rollbackTest.transitionToSteadyStateOperations(); - -rollbackTest.getTestFixture().nodes.forEach(function(node) { - const nodeDB = node.getDB("system"); - - // Check globalIndexCreateUUID creation is rolled back and does not exist. - var res = - nodeDB.runCommand({listCollections: 1, filter: {name: uuidToNss(globalIndexCreateUUID)}}); - assert.eq(res.cursor.firstBatch.length, 0); - - // Check globalIndexDropUUID drop is rolled back and still exists. - res = nodeDB.runCommand({listCollections: 1, filter: {name: uuidToNss(globalIndexDropUUID)}}); - assert.eq(res.cursor.firstBatch.length, 1); -}); - -rollbackTest.stop(); -})(); diff --git a/jstests/replsets/global_index_rollback.js b/jstests/replsets/global_index_rollback.js new file mode 100644 index 00000000000..de8a599d9cb --- /dev/null +++ b/jstests/replsets/global_index_rollback.js @@ -0,0 +1,537 @@ +/** + * Tests replication rollback of global index container DDL and CRUD operations. + * Validates the generation of rollback files and efficient restoring of fast-counts. + * + * @tags: [ + * featureFlagGlobalIndexes, + * requires_fcv_62, + * requires_replication, + * ] + */ + +(function() { +'use strict'; + +load('jstests/replsets/libs/rollback_files.js'); +load('jstests/replsets/libs/rollback_test.js'); +load('jstests/libs/uuid_util.js'); + +function uuidToCollName(uuid) { + return "globalIndexes." + extractUUIDFromObject(uuid); +} + +const rollbackTest = new RollbackTest(jsTestName()); + +function rollbackDDLOps() { + const node = rollbackTest.getPrimary(); + const adminDB = node.getDB("admin"); + const globalIndexCreateUUID = UUID(); + const globalIndexDropUUID = UUID(); + jsTestLog("rollbackDDLOps primary=" + node); + + // Create a global index container whose drop won't be majority-committed. + assert.commandWorked(adminDB.runCommand({_shardsvrCreateGlobalIndex: globalIndexDropUUID})); + + rollbackTest.transitionToRollbackOperations(); + + // Create a global index container that's not majority-committed. + assert.commandWorked(adminDB.runCommand({_shardsvrCreateGlobalIndex: globalIndexCreateUUID})); + // Drop a global index container, the operation is not majority-committed. + assert.commandWorked(adminDB.runCommand({_shardsvrDropGlobalIndex: globalIndexDropUUID})); + + // Perform the rollback. + rollbackTest.transitionToSyncSourceOperationsBeforeRollback(); + rollbackTest.transitionToSyncSourceOperationsDuringRollback(); + rollbackTest.transitionToSteadyStateOperations(); + + // Check globalIndexCreateUUID creation is rolled back and does not exist. + var res = node.getDB("system").runCommand( + {listCollections: 1, filter: {name: uuidToCollName(globalIndexCreateUUID)}}); + assert.eq(res.cursor.firstBatch.length, 0); + + // Check globalIndexDropUUID drop is rolled back and still exists. + res = node.getDB("system").runCommand( + {listCollections: 1, filter: {name: uuidToCollName(globalIndexDropUUID)}}); + assert.eq(res.cursor.firstBatch.length, 1); + + // Log calls out that the two commands have been rolled back. + assert(checkLog.checkContainsWithCountJson( + node, + 21656, + { + "oplogEntry": { + "op": "c", + "ns": "system.$cmd", + "ui": {"$uuid": extractUUIDFromObject(globalIndexDropUUID)}, + "o": {"dropGlobalIndex": uuidToCollName(globalIndexDropUUID)}, + "o2": {"numRecords": 0} + } + }, + 1, + null, + true /*isRelaxed*/)); + assert(checkLog.checkContainsWithCountJson( + node, + 21656, + { + "oplogEntry": { + "op": "c", + "ns": "system.$cmd", + "ui": {"$uuid": extractUUIDFromObject(globalIndexCreateUUID)}, + "o": {"createGlobalIndex": uuidToCollName(globalIndexCreateUUID)} + } + }, + 1, + null, + true /*isRelaxed*/)); +} + +// Rollback a single index key insert. +function rollbackSingleKeyInsert(bulk) { + const node = rollbackTest.getPrimary(); + const adminDB = node.getDB("admin"); + const uuid = UUID(); + jsTestLog("rollbackSingleKeyInsert uuid=" + uuid + ", bulk=" + bulk, ", primary=" + node); + + const collName = uuidToCollName(uuid); + assert.commandWorked(adminDB.runCommand({_shardsvrCreateGlobalIndex: uuid})); + assert.eq(0, node.getDB("system").getCollection(collName).find().itcount()); + + const keyMajorityCommitted = {key: {a: 0}, docKey: {sk: 0, _id: 0}}; + const keyToRollback = {key: {a: 1}, docKey: {sk: 1, _id: 1}}; + + // Insert a key, majority-committed. + { + const session = node.startSession(); + session.startTransaction(); + assert.commandWorked(session.getDatabase("system").runCommand( + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyMajorityCommitted))); + session.commitTransaction(); + session.endSession(); + } + assert.eq(1, node.getDB("system").getCollection(collName).find().itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyMajorityCommitted["docKey"]}) + .itcount()); + + // Then insert a key that's not majority-committed. + rollbackTest.transitionToRollbackOperations(); + { + const session = node.startSession(); + session.startTransaction(); + const stmts = [Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyToRollback)]; + if (bulk) { + assert.commandWorked(session.getDatabase("system").runCommand( + {"_shardsvrWriteGlobalIndexKeys": 1, ops: stmts})); + } else { + for (let stmt of stmts) { + assert.commandWorked(session.getDatabase("system").runCommand(stmt)); + } + } + session.commitTransaction(); + session.endSession(); + } + assert.eq(2, node.getDB("system").getCollection(collName).find().itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyMajorityCommitted["docKey"]}) + .itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyToRollback["docKey"]}) + .itcount()); + + // Perform the rollback. + rollbackTest.transitionToSyncSourceOperationsBeforeRollback(); + rollbackTest.transitionToSyncSourceOperationsDuringRollback(); + rollbackTest.transitionToSteadyStateOperations(); + + // Only the majority-committed key is left. + assert.eq(1, node.getDB("system").getCollection(collName).find().itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyMajorityCommitted["docKey"]}) + .itcount()); + assert.eq(0, + node.getDB("system") + .getCollection(collName) + .find({_id: keyToRollback["docKey"]}) + .itcount()); + + // Log calls out that the index key insert has been rolled back. + assert( + checkLog.checkContainsWithCountJson(node, + 6984700, + {"insertGlobalIndexKey": 1, "deleteGlobalIndexKey": 0}, + 1, + null, + true /*isRelaxed*/)); + + // The rollback wrote the rolled-back index key insert to a file. + const replTest = rollbackTest.getTestFixture(); + const expectedEntries = [Object.extend({_id: keyToRollback.docKey}, + {"ik": BinData(0, "KwIE"), "tb": BinData(0, "AQ==")})]; + checkRollbackFiles(replTest.getDbPath(node), "system." + collName, uuid, expectedEntries); +} + +// Rollback a single index key delete. +function rollbackSingleKeyDelete(bulk) { + const node = rollbackTest.getPrimary(); + const adminDB = node.getDB("admin"); + const uuid = UUID(); + jsTestLog("rollbackSingleKeyDelete uuid=" + uuid + ", bulk=" + bulk, ", primary=" + node); + + const collName = uuidToCollName(uuid); + assert.commandWorked(adminDB.runCommand({_shardsvrCreateGlobalIndex: uuid})); + assert.eq(0, node.getDB("system").getCollection(collName).find().itcount()); + + const key = {key: {a: 0}, docKey: {sk: 0, _id: 0}}; + + // Insert a key, majority-committed. + { + const session = node.startSession(); + session.startTransaction(); + assert.commandWorked(session.getDatabase("system").runCommand( + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, key))); + session.commitTransaction(); + session.endSession(); + } + assert.eq(1, node.getDB("system").getCollection(collName).find().itcount()); + assert.eq(1, node.getDB("system").getCollection(collName).find({_id: key["docKey"]}).itcount()); + + // Then delete the key, not majority-committed. + rollbackTest.transitionToRollbackOperations(); + { + const session = node.startSession(); + session.startTransaction(); + const stmts = [Object.extend({"_shardsvrDeleteGlobalIndexKey": uuid}, key)]; + if (bulk) { + assert.commandWorked(session.getDatabase("system").runCommand( + {"_shardsvrWriteGlobalIndexKeys": 1, ops: stmts})); + } else { + for (let stmt of stmts) { + assert.commandWorked(session.getDatabase("system").runCommand(stmt)); + } + } + session.commitTransaction(); + session.endSession(); + } + assert.eq(0, node.getDB("system").getCollection(collName).find().itcount()); + + // Perform the rollback. + rollbackTest.transitionToSyncSourceOperationsBeforeRollback(); + rollbackTest.transitionToSyncSourceOperationsDuringRollback(); + rollbackTest.transitionToSteadyStateOperations(); + + // The key is still present, as its delete wasn't majority-committed. + assert.eq(1, node.getDB("system").getCollection(collName).find().itcount()); + assert.eq(1, node.getDB("system").getCollection(collName).find({_id: key["docKey"]}).itcount()); + + // Log calls out that the index key delete has been rolled back. + assert( + checkLog.checkContainsWithCountJson(node, + 6984700, + {"insertGlobalIndexKey": 0, "deleteGlobalIndexKey": 1}, + 1, + null, + true /*isRelaxed*/)); +} + +function rollbackOneKeyInsertTwoKeyDeletes(bulk) { + const node = rollbackTest.getPrimary(); + const adminDB = node.getDB("admin"); + const uuid = UUID(); + jsTestLog("rollbackOneKeyInsertTwoKeyDeletes uuid=" + uuid + ", bulk=" + bulk, + ", primary=" + node); + + const collName = uuidToCollName(uuid); + assert.commandWorked(adminDB.runCommand({_shardsvrCreateGlobalIndex: uuid})); + assert.eq(0, node.getDB("system").getCollection(collName).find().itcount()); + + const keyMajorityCommitted = {key: {a: 0}, docKey: {sk: 0, _id: 0}}; + const keyToRollback0 = {key: {a: 1}, docKey: {sk: 1, _id: 1}}; + const keyToRollback1 = {key: {a: 2}, docKey: {sk: 2, _id: 2}}; + + // Insert a key, majority-committed. + { + const session = node.startSession(); + session.startTransaction(); + assert.commandWorked(session.getDatabase("system").runCommand( + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyMajorityCommitted))); + session.commitTransaction(); + session.endSession(); + } + assert.eq(1, node.getDB("system").getCollection(collName).find().itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyMajorityCommitted["docKey"]}) + .itcount()); + + // Then delete the key and insert two more keys. All these writes are not majority-committed. + rollbackTest.transitionToRollbackOperations(); + { + const session = node.startSession(); + session.startTransaction(); + const stmts = [ + Object.extend({"_shardsvrDeleteGlobalIndexKey": uuid}, keyMajorityCommitted), + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyToRollback0), + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyToRollback1) + ]; + if (bulk) { + assert.commandWorked(session.getDatabase("system").runCommand( + {"_shardsvrWriteGlobalIndexKeys": 1, ops: stmts})); + } else { + for (let stmt of stmts) { + assert.commandWorked(session.getDatabase("system").runCommand(stmt)); + } + } + session.commitTransaction(); + session.endSession(); + } + assert.eq(2, node.getDB("system").getCollection(collName).find().itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyToRollback0["docKey"]}) + .itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyToRollback1["docKey"]}) + .itcount()); + + // Perform the rollback. + rollbackTest.transitionToSyncSourceOperationsBeforeRollback(); + rollbackTest.transitionToSyncSourceOperationsDuringRollback(); + rollbackTest.transitionToSteadyStateOperations(); + + // The only key that's present is the majority-committed one. + assert.eq(1, node.getDB("system").getCollection(collName).find().itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyMajorityCommitted["docKey"]}) + .itcount()); + + // Log calls out that two index key inserts and one key delete have been rolled back. + assert( + checkLog.checkContainsWithCountJson(node, + 6984700, + {"insertGlobalIndexKey": 2, "deleteGlobalIndexKey": 1}, + 1, + null, + true /*isRelaxed*/)); + + // The rollback wrote the two rolled-back index key inserts to a file. + const replTest = rollbackTest.getTestFixture(); + const expectedEntries = [ + Object.extend({_id: keyToRollback1.docKey}, + {"ik": BinData(0, "KwQE"), "tb": BinData(0, "AQ==")}), + Object.extend({_id: keyToRollback0.docKey}, + {"ik": BinData(0, "KwIE"), "tb": BinData(0, "AQ==")}), + ]; + checkRollbackFiles(replTest.getDbPath(node), "system." + collName, uuid, expectedEntries); +} + +function rollbackCreateWithCrud(bulk) { + const node = rollbackTest.getPrimary(); + const adminDB = node.getDB("admin"); + const uuid = UUID(); + jsTestLog("rollbackCreateWithCrud uuid=" + uuid + ", bulk=" + bulk, ", primary=" + node); + + const collName = uuidToCollName(uuid); + + const keyToRollback0 = {key: {a: 1}, docKey: {sk: 1, _id: 1}}; + const keyToRollback1 = {key: {a: 2}, docKey: {sk: 2, _id: 2}}; + const keyToRollback2 = {key: {a: 3}, docKey: {sk: 3, _id: 3}}; + + // Create a container and insert keys to it. All these operations are not majority-committed. + rollbackTest.transitionToRollbackOperations(); + { + assert.commandWorked(adminDB.runCommand({_shardsvrCreateGlobalIndex: uuid})); + + const session = node.startSession(); + session.startTransaction(); + const stmts = [ + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyToRollback0), + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyToRollback1), + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyToRollback2), + Object.extend({"_shardsvrDeleteGlobalIndexKey": uuid}, keyToRollback1), + ]; + if (bulk) { + assert.commandWorked(session.getDatabase("system").runCommand( + {"_shardsvrWriteGlobalIndexKeys": 1, ops: stmts})); + } else { + for (let stmt of stmts) { + assert.commandWorked(session.getDatabase("system").runCommand(stmt)); + } + } + session.commitTransaction(); + session.endSession(); + } + assert.eq(2, node.getDB("system").getCollection(collName).find().itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyToRollback0["docKey"]}) + .itcount()); + assert.eq(0, + node.getDB("system") + .getCollection(collName) + .find({_id: keyToRollback1["docKey"]}) + .itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyToRollback2["docKey"]}) + .itcount()); + + // Perform the rollback. + rollbackTest.transitionToSyncSourceOperationsBeforeRollback(); + rollbackTest.transitionToSyncSourceOperationsDuringRollback(); + rollbackTest.transitionToSteadyStateOperations(); + + // The global index container doesn't exist. + const container = + node.getDB("system").runCommand({listCollections: 1, filter: {name: collName}}); + assert.eq(container.cursor.firstBatch.length, 0); + + // Log calls out that three index key inserts and one key delete have been rolled back. + assert( + checkLog.checkContainsWithCountJson(node, + 6984700, + {"insertGlobalIndexKey": 3, "deleteGlobalIndexKey": 1}, + 1, + null, + true /*isRelaxed*/)); + + // The rollback wrote the two rolled-back index key inserts to a file. + const replTest = rollbackTest.getTestFixture(); + const expectedEntries = [ + Object.extend({_id: keyToRollback2.docKey}, + {"ik": BinData(0, "KwYE"), "tb": BinData(0, "AQ==")}), + Object.extend({_id: keyToRollback0.docKey}, + {"ik": BinData(0, "KwIE"), "tb": BinData(0, "AQ==")}), + ]; + checkRollbackFiles(replTest.getDbPath(node), "system." + collName, uuid, expectedEntries); +} + +function rollbackDropWithCrud(bulk) { + const node = rollbackTest.getPrimary(); + const adminDB = node.getDB("admin"); + const uuid = UUID(); + jsTestLog("rollbackDropWithCrud uuid=" + uuid + ", bulk=" + bulk, ", primary=" + node); + + const collName = uuidToCollName(uuid); + + assert.commandWorked(adminDB.runCommand({_shardsvrCreateGlobalIndex: uuid})); + const keyMajorityCommitted = {key: {a: 0}, docKey: {sk: 0, _id: 0}}; + + // Insert a key that will be majority-committed. + { + const session = node.startSession(); + session.startTransaction(); + assert.commandWorked(session.getDatabase("system").runCommand( + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyMajorityCommitted))); + session.commitTransaction(); + session.endSession(); + } + assert.eq(1, node.getDB("system").getCollection(collName).find().itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyMajorityCommitted["docKey"]}) + .itcount()); + + const keyToRollback0 = {key: {a: 1}, docKey: {sk: 1, _id: 1}}; + const keyToRollback1 = {key: {a: 2}, docKey: {sk: 2, _id: 2}}; + const keyToRollback2 = {key: {a: 3}, docKey: {sk: 3, _id: 3}}; + + // Write to the container and drop it. All these operations are not majority-committed. + rollbackTest.transitionToRollbackOperations(); + { + assert.commandWorked(adminDB.runCommand({_shardsvrCreateGlobalIndex: uuid})); + + const session = node.startSession(); + session.startTransaction(); + const stmts = [ + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyToRollback1), + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyToRollback2), + Object.extend({"_shardsvrDeleteGlobalIndexKey": uuid}, keyToRollback1), + Object.extend({"_shardsvrInsertGlobalIndexKey": uuid}, keyToRollback0), + Object.extend({"_shardsvrDeleteGlobalIndexKey": uuid}, keyToRollback2), + ]; + if (bulk) { + assert.commandWorked(session.getDatabase("system").runCommand( + {"_shardsvrWriteGlobalIndexKeys": 1, ops: stmts})); + } else { + for (let stmt of stmts) { + assert.commandWorked(session.getDatabase("system").runCommand(stmt)); + } + } + session.commitTransaction(); + session.endSession(); + assert.commandWorked(adminDB.runCommand({_shardsvrDropGlobalIndex: uuid})); + } + // The global index container doesn't exist. + const containerBeforeRollback = + node.getDB("system").runCommand({listCollections: 1, filter: {name: collName}}); + assert.eq(containerBeforeRollback.cursor.firstBatch.length, 0); + + // Perform the rollback. + rollbackTest.transitionToSyncSourceOperationsBeforeRollback(); + rollbackTest.transitionToSyncSourceOperationsDuringRollback(); + rollbackTest.transitionToSteadyStateOperations(); + + // The global index exists, with the single majority-committed key. + const containerAfterRollback = + node.getDB("system").runCommand({listCollections: 1, filter: {name: collName}}); + assert.eq(containerAfterRollback.cursor.firstBatch.length, 1); + + assert.eq(1, node.getDB("system").getCollection(collName).find().itcount()); + assert.eq(1, + node.getDB("system") + .getCollection(collName) + .find({_id: keyMajorityCommitted["docKey"]}) + .itcount()); + + // Log calls out that three index key inserts and two key deletes have been rolled back. + assert( + checkLog.checkContainsWithCountJson(node, + 6984700, + {"insertGlobalIndexKey": 3, "deleteGlobalIndexKey": 2}, + 1, + null, + true /*isRelaxed*/)); + + // We've reset the original fast count rather than doing an expensive collection scan. + assert(checkLog.checkContainsWithCountJson(node, 21602, undefined, 0)); + + // Log states that we're not going to write a rollback file for a collection whose drop was + // rolled back. + assert(checkLog.checkContainsWithCountJson( + node, + 21608, + {"uuid": {"uuid": {"$uuid": extractUUIDFromObject(uuid)}}}, + 1, + null, + true /*isRelaxed*/)); +} + +rollbackDDLOps(); +for (let bulk of [false, true]) { + rollbackSingleKeyInsert(bulk); + rollbackSingleKeyDelete(bulk); + rollbackOneKeyInsertTwoKeyDeletes(bulk); + rollbackCreateWithCrud(bulk); + rollbackDropWithCrud(bulk); +} + +rollbackTest.stop(); +})(); diff --git a/jstests/replsets/shardsvr_global_index_crud_rollback.js b/jstests/replsets/shardsvr_global_index_crud_rollback.js deleted file mode 100644 index 7e4349c8945..00000000000 --- a/jstests/replsets/shardsvr_global_index_crud_rollback.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Tests that global index key insert and delete are properly rolled back. - * - * @tags: [ - * featureFlagGlobalIndexes, - * requires_fcv_62, - * requires_replication, - * ] - */ -(function() { -'use strict'; - -load('jstests/replsets/libs/rollback_test.js'); - -const rollbackTest = new RollbackTest(jsTestName()); - -const primary = rollbackTest.getPrimary(); -const adminDB = primary.getDB("admin"); -const globalIndexUUID = UUID(); -const [_, uuidString] = globalIndexUUID.toString().match(/"((?:\\.|[^"\\])*)"/); -const collName = "globalIndexes." + uuidString; - -assert.commandWorked(adminDB.runCommand({_shardsvrCreateGlobalIndex: globalIndexUUID})); - -// We start on a clean slate: the global index container is empty. -assert.eq(0, primary.getDB("system").getCollection(collName).find().itcount()); - -const docKeyToInsert = { - sk: 1, - _id: 1 -}; -const docKeyToDelete = { - sk: 1, - _id: 5 -}; -// Add key to delete during rollback ops phase. -{ - const session = primary.startSession(); - session.startTransaction(); - assert.commandWorked(session.getDatabase("system").runCommand( - {"_shardsvrInsertGlobalIndexKey": globalIndexUUID, key: {a: 5}, docKey: docKeyToDelete})); - session.commitTransaction(); - session.endSession(); -} - -rollbackTest.transitionToRollbackOperations(); - -// Insert an index key to be rolled back. -{ - const session = primary.startSession(); - session.startTransaction(); - assert.commandWorked(session.getDatabase("system").runCommand( - {"_shardsvrInsertGlobalIndexKey": globalIndexUUID, key: {a: 1}, docKey: docKeyToInsert})); - assert.commandWorked(session.getDatabase("system").runCommand( - {"_shardsvrDeleteGlobalIndexKey": globalIndexUUID, key: {a: 5}, docKey: docKeyToDelete})); - session.commitTransaction(); - session.endSession(); -} - -// The inserted index key is present on the primary. -assert.eq(1, primary.getDB("system").getCollection(collName).find({_id: docKeyToInsert}).itcount()); -// The deleted index key is not present on the primary. -assert.eq(0, primary.getDB("system").getCollection(collName).find({_id: docKeyToDelete}).itcount()); - -// Perform the rollback. -rollbackTest.transitionToSyncSourceOperationsBeforeRollback(); -rollbackTest.transitionToSyncSourceOperationsDuringRollback(); -rollbackTest.transitionToSteadyStateOperations(); - -// Verify both global index key insert and delete have been rolled back. The container has exactly -// one entry, and is the one inserted before transitionToRollbackOperations. -rollbackTest.getTestFixture().nodes.forEach(function(node) { - const nodeDB = node.getDB("system"); - const found = nodeDB.getCollection(collName).find(); - const elArr = found.toArray(); - assert.eq(1, elArr.length); - assert.eq(elArr[0]["_id"], docKeyToDelete); -}); - -// TODO (SERVER-69847): fast count is not updated properly for global index CRUD ops after rollback. -// Current test implementation makes fastcount valid due to rolling back both a delete and an -// insert. After fixing fast count, we should make this test fail if fast count is not working -// properly. -// TODO (SERVER-69847): add a rollback test for _shardsvrWriteGlobalIndexKeys too. - -rollbackTest.stop(); -})(); diff --git a/src/mongo/db/auth/auth_op_observer.h b/src/mongo/db/auth/auth_op_observer.h index c2ee0a02ad8..4b4186db6af 100644 --- a/src/mongo/db/auth/auth_op_observer.h +++ b/src/mongo/db/auth/auth_op_observer.h @@ -56,7 +56,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/free_mon/free_mon_op_observer.h b/src/mongo/db/free_mon/free_mon_op_observer.h index 4cd70414b94..60f3d4ed5b7 100644 --- a/src/mongo/db/free_mon/free_mon_op_observer.h +++ b/src/mongo/db/free_mon/free_mon_op_observer.h @@ -56,7 +56,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/global_index.cpp b/src/mongo/db/global_index.cpp index 13852a11eda..ed702f76aac 100644 --- a/src/mongo/db/global_index.cpp +++ b/src/mongo/db/global_index.cpp @@ -52,8 +52,6 @@ namespace mongo::global_index { namespace { // Anonymous namespace for private functions. -constexpr StringData kIndexKeyIndexName = "ik_1"_sd; - // Build an index entry to insert. BSONObj buildIndexEntry(const BSONObj& key, const BSONObj& docKey) { // Generate the KeyString representation of the index key. @@ -72,12 +70,14 @@ BSONObj buildIndexEntry(const BSONObj& key, const BSONObj& docKey) { // - 'tb': the index key's TypeBits. Only present if non-zero. BSONObjBuilder indexEntryBuilder; - indexEntryBuilder.append("_id", docKey); + indexEntryBuilder.append(kContainerIndexDocKeyFieldName, docKey); indexEntryBuilder.append( - "ik", BSONBinData(ks.getBuffer(), ks.getSize(), BinDataType::BinDataGeneral)); + kContainerIndexKeyFieldName, + BSONBinData(ks.getBuffer(), ks.getSize(), BinDataType::BinDataGeneral)); if (!indexTB.isAllZeros()) { indexEntryBuilder.append( - "tb", BSONBinData(indexTB.getBuffer(), indexTB.getSize(), BinDataType::BinDataGeneral)); + kContainerIndexKeyTypeBitsFieldName, + BSONBinData(indexTB.getBuffer(), indexTB.getSize(), BinDataType::BinDataGeneral)); } return indexEntryBuilder.obj(); } @@ -120,8 +120,9 @@ void createContainer(OperationContext* opCtx, const UUID& indexUUID) { // Create the container. return writeConflictRetry(opCtx, "createGlobalIndexContainer", nss.ns(), [&]() { - const auto indexKeySpec = BSON("v" << 2 << "name" << kIndexKeyIndexName << "key" - << BSON("ik" << 1) << "unique" << true); + const auto indexKeySpec = + BSON("v" << 2 << "name" << kContainerIndexKeyFieldName.toString() + "_1" + << "key" << BSON(kContainerIndexKeyFieldName << 1) << "unique" << true); WriteUnitOfWork wuow(opCtx); @@ -153,13 +154,15 @@ void createContainer(OperationContext* opCtx, const UUID& indexUUID) { str::stream() << "Collection with UUID " << indexUUID << " already exists but it's not clustered.", autoColl->getCollectionOptions().clusteredIndex); - tassert( - 6789205, - str::stream() << "Collection with UUID " << indexUUID - << " already exists but it's missing a unique index on " - "'ik'.", - autoColl->getIndexCatalog()->findIndexByKeyPatternAndOptions( - opCtx, BSON("ik" << 1), indexKeySpec, IndexCatalog::InclusionPolicy::kReady)); + tassert(6789205, + str::stream() << "Collection with UUID " << indexUUID + << " already exists but it's missing a unique index on " + << kContainerIndexKeyFieldName << ".", + autoColl->getIndexCatalog()->findIndexByKeyPatternAndOptions( + opCtx, + BSON(kContainerIndexKeyFieldName << 1), + indexKeySpec, + IndexCatalog::InclusionPolicy::kReady)); tassert(6789206, str::stream() << "Collection with namespace " << nss.ns() << " already exists but it has inconsistent UUID " @@ -191,6 +194,8 @@ void dropContainer(OperationContext* opCtx, const UUID& indexUUID) { return; } + const auto numKeys = autoColl->numRecords(opCtx); + WriteUnitOfWork wuow(opCtx); { repl::UnreplicatedWritesBlock unreplicatedWrites(opCtx); @@ -198,7 +203,7 @@ void dropContainer(OperationContext* opCtx, const UUID& indexUUID) { } auto opObserver = opCtx->getServiceContext()->getOpObserver(); - opObserver->onDropGlobalIndex(opCtx, nss, indexUUID); + opObserver->onDropGlobalIndex(opCtx, nss, indexUUID, numKeys); wuow.commit(); return; diff --git a/src/mongo/db/global_index.h b/src/mongo/db/global_index.h index 73d82da4a03..b93fb439547 100644 --- a/src/mongo/db/global_index.h +++ b/src/mongo/db/global_index.h @@ -35,6 +35,17 @@ namespace mongo::global_index { +// The container (collection) fields of an index key. The document key is stored as a BSON object. +// The index key is stored in its KeyString representation, hence the need to store TypeBits. +constexpr auto kContainerIndexDocKeyFieldName = "_id"_sd; +constexpr auto kContainerIndexKeyFieldName = "ik"_sd; +constexpr auto kContainerIndexKeyTypeBitsFieldName = "tb"_sd; + +// The oplog entry fields representing an insert or delete of an index key. The index key and the +// document key are BSON objects. +constexpr auto kOplogEntryDocKeyFieldName = "dk"_sd; +constexpr auto kOplogEntryIndexKeyFieldName = kContainerIndexKeyFieldName; + /** * Creates the internal collection implements the global index container with the given UUID on the * shard. Replicates as a 'createGlobalIndex' command. This container-backing collection: diff --git a/src/mongo/db/global_index_test.cpp b/src/mongo/db/global_index_test.cpp index 83cb975c990..1730d272b43 100644 --- a/src/mongo/db/global_index_test.cpp +++ b/src/mongo/db/global_index_test.cpp @@ -98,16 +98,21 @@ void verifyStoredKeyMatchesIndexKey(const BSONObj& key, // 'tb' field stores the BinData(TypeBits(key)). The 'tb' field is not present if there are // no TypeBits. - auto entryIndexKeySize = indexEntry["ik"].size(); - const auto entryIndexKeyBinData = indexEntry["ik"].binData(entryIndexKeySize); + auto entryIndexKeySize = indexEntry[global_index::kContainerIndexKeyFieldName].size(); + const auto entryIndexKeyBinData = + indexEntry[global_index::kContainerIndexKeyFieldName].binData(entryIndexKeySize); - const auto hasTypeBits = indexEntry.hasElement("tb"); + const auto hasTypeBits = + indexEntry.hasElement(global_index::kContainerIndexKeyTypeBitsFieldName); ASSERT_EQ(expectTypeBits, hasTypeBits); auto tb = KeyString::TypeBits(KeyString::Version::V1); if (hasTypeBits) { - auto entryTypeBitsSize = indexEntry["tb"].size(); - auto entryTypeBitsBinData = indexEntry["tb"].binData(entryTypeBitsSize); + auto entryTypeBitsSize = + indexEntry[global_index::kContainerIndexKeyTypeBitsFieldName].size(); + auto entryTypeBitsBinData = + indexEntry[global_index::kContainerIndexKeyTypeBitsFieldName].binData( + entryTypeBitsSize); auto entryTypeBitsReader = BufReader(entryTypeBitsBinData, entryTypeBitsSize); tb = KeyString::TypeBits::fromBuffer(KeyString::Version::V1, &entryTypeBitsReader); ASSERT(!tb.isAllZeros()); @@ -134,12 +139,14 @@ TEST_F(GlobalIndexTest, StorageFormat) { const auto key = BSON("" << "hola"); const auto docKey = BSON("shk0" << 0 << "shk1" << 0 << "_id" << 0); - const auto entryId = BSON("_id" << docKey); + const auto entryId = BSON(global_index::kContainerIndexDocKeyFieldName << docKey); global_index::insertKey(operationContext(), uuid, key, docKey); // Validate that the document key is stored in the index entry's _id field. - StatusWith status = storageInterface()->findById( - operationContext(), NamespaceString::makeGlobalIndexNSS(uuid), entryId["_id"]); + StatusWith status = + storageInterface()->findById(operationContext(), + NamespaceString::makeGlobalIndexNSS(uuid), + entryId[global_index::kContainerIndexDocKeyFieldName]); ASSERT_OK(status.getStatus()); const auto indexEntry = status.getValue(); @@ -153,12 +160,14 @@ TEST_F(GlobalIndexTest, StorageFormat) { << "hola" << "" << 1); const auto docKey = BSON("shk0" << 1 << "shk1" << 1 << "_id" << 1); - const auto entryId = BSON("_id" << docKey); + const auto entryId = BSON(global_index::kContainerIndexDocKeyFieldName << docKey); global_index::insertKey(operationContext(), uuid, key, docKey); // Validate that the document key is stored in the index entry's _id field. - StatusWith status = storageInterface()->findById( - operationContext(), NamespaceString::makeGlobalIndexNSS(uuid), entryId["_id"]); + StatusWith status = + storageInterface()->findById(operationContext(), + NamespaceString::makeGlobalIndexNSS(uuid), + entryId[global_index::kContainerIndexDocKeyFieldName]); ASSERT_OK(status.getStatus()); const auto indexEntry = status.getValue(); @@ -172,12 +181,14 @@ TEST_F(GlobalIndexTest, StorageFormat) { << "hola" << "" << 2LL); const auto docKey = BSON("shk0" << 2 << "shk1" << 2 << "_id" << 2); - const auto entryId = BSON("_id" << docKey); + const auto entryId = BSON(global_index::kContainerIndexDocKeyFieldName << docKey); global_index::insertKey(operationContext(), uuid, key, docKey); // Validate that the document key is stored in the index entry's _id field. - StatusWith status = storageInterface()->findById( - operationContext(), NamespaceString::makeGlobalIndexNSS(uuid), entryId["_id"]); + StatusWith status = + storageInterface()->findById(operationContext(), + NamespaceString::makeGlobalIndexNSS(uuid), + entryId[global_index::kContainerIndexDocKeyFieldName]); ASSERT_OK(status.getStatus()); const auto indexEntry = status.getValue(); @@ -191,12 +202,14 @@ TEST_F(GlobalIndexTest, StorageFormat) { << "hola" << "" << 3.0); const auto docKey = BSON("shk0" << 2 << "shk1" << 3 << "_id" << 3); - const auto entryId = BSON("_id" << docKey); + const auto entryId = BSON(global_index::kContainerIndexDocKeyFieldName << docKey); global_index::insertKey(operationContext(), uuid, key, docKey); // Validate that the document key is stored in the index entry's _id field. - StatusWith status = storageInterface()->findById( - operationContext(), NamespaceString::makeGlobalIndexNSS(uuid), entryId["_id"]); + StatusWith status = + storageInterface()->findById(operationContext(), + NamespaceString::makeGlobalIndexNSS(uuid), + entryId[global_index::kContainerIndexDocKeyFieldName]); ASSERT_OK(status.getStatus()); const auto indexEntry = status.getValue(); @@ -258,7 +271,7 @@ TEST_F(GlobalIndexTest, DeleteKey) { const auto insertAndVerifyDelete = [this](const UUID& uuid, const BSONObj& key, const BSONObj& docKey) { - const auto entryId = BSON("_id" << docKey); + const auto entryId = BSON(global_index::kContainerIndexDocKeyFieldName << docKey); const auto nss = NamespaceString::makeGlobalIndexNSS(uuid); // Inserts already tested in StorageFormat case. @@ -266,7 +279,8 @@ TEST_F(GlobalIndexTest, DeleteKey) { // Delete and validate that the key is not found. global_index::deleteKey(operationContext(), uuid, key, docKey); - ASSERT_NOT_OK(storageInterface()->findById(operationContext(), nss, entryId["_id"])); + ASSERT_NOT_OK(storageInterface()->findById( + operationContext(), nss, entryId[global_index::kContainerIndexDocKeyFieldName])); }; const auto docKey = BSON("shk0" << 0 << "shk1" << 0 << "_id" << 0); @@ -332,7 +346,7 @@ void _assertDocumentsInGlobalIndexById(OperationContext* opCtx, BSONObj obj; for (auto& id : ids) { ASSERT_EQUALS(exec->getNext(&obj, nullptr), PlanExecutor::ADVANCED); - ASSERT_BSONOBJ_EQ(id, obj.getObjectField("_id")); + ASSERT_BSONOBJ_EQ(id, obj.getObjectField(global_index::kContainerIndexDocKeyFieldName)); } ASSERT_EQUALS(exec->getNext(&obj, nullptr), PlanExecutor::IS_EOF); } diff --git a/src/mongo/db/op_observer/fcv_op_observer.h b/src/mongo/db/op_observer/fcv_op_observer.h index d9040c52c44..c7641fd6d9a 100644 --- a/src/mongo/db/op_observer/fcv_op_observer.h +++ b/src/mongo/db/op_observer/fcv_op_observer.h @@ -86,7 +86,8 @@ public: BSONObj indexDoc) final {} void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/op_observer/op_observer.h b/src/mongo/db/op_observer/op_observer.h index a17706c33dd..f8bfd52e235 100644 --- a/src/mongo/db/op_observer/op_observer.h +++ b/src/mongo/db/op_observer/op_observer.h @@ -133,7 +133,8 @@ public: virtual void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) = 0; + const UUID& globalIndexUUID, + long long numKeys) = 0; virtual void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/op_observer/op_observer_impl.cpp b/src/mongo/db/op_observer/op_observer_impl.cpp index 5bbc8e86d1b..d8f128e3c0a 100644 --- a/src/mongo/db/op_observer/op_observer_impl.cpp +++ b/src/mongo/db/op_observer/op_observer_impl.cpp @@ -304,6 +304,7 @@ void logGlobalIndexDDLOperation(OperationContext* opCtx, const NamespaceString& globalIndexNss, const UUID& globalIndexUUID, const StringData commandString, + boost::optional numKeys, OplogWriter* oplogWriter) { invariant(!opCtx->inMultiDocumentTransaction()); @@ -314,6 +315,16 @@ void logGlobalIndexDDLOperation(OperationContext* opCtx, MutableOplogEntry oplogEntry; oplogEntry.setOpType(repl::OpTypeEnum::kCommand); oplogEntry.setObject(builder.done()); + + // On global index drops, persist the number of records into the 'o2' field similar to a + // collection drop. This allows for efficiently restoring the index keys count after rollback + // without forcing a collection scan. + invariant((numKeys && commandString == "dropGlobalIndex") || + (!numKeys && commandString == "createGlobalIndex")); + if (numKeys) { + oplogEntry.setObject2(makeObject2ForDropOrRename(*numKeys)); + } + // The 'ns' field is technically redundant as it can be derived from the uuid, however it's a // required oplog entry field. oplogEntry.setNss(globalIndexNss.getCommandNS()); @@ -347,16 +358,21 @@ void OpObserverImpl::onCreateGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, const UUID& globalIndexUUID) { constexpr StringData commandString = "createGlobalIndex"_sd; - logGlobalIndexDDLOperation( - opCtx, globalIndexNss, globalIndexUUID, commandString, _oplogWriter.get()); + logGlobalIndexDDLOperation(opCtx, + globalIndexNss, + globalIndexUUID, + commandString, + boost::none /* numKeys */, + _oplogWriter.get()); } void OpObserverImpl::onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) { + const UUID& globalIndexUUID, + long long numKeys) { constexpr StringData commandString = "dropGlobalIndex"_sd; logGlobalIndexDDLOperation( - opCtx, globalIndexNss, globalIndexUUID, commandString, _oplogWriter.get()); + opCtx, globalIndexNss, globalIndexUUID, commandString, numKeys, _oplogWriter.get()); } void OpObserverImpl::onCreateIndex(OperationContext* opCtx, diff --git a/src/mongo/db/op_observer/op_observer_impl.h b/src/mongo/db/op_observer/op_observer_impl.h index 5a2b1493d70..bb704238a01 100644 --- a/src/mongo/db/op_observer/op_observer_impl.h +++ b/src/mongo/db/op_observer/op_observer_impl.h @@ -64,7 +64,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final; + const UUID& globalIndexUUID, + long long numKeys) final; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/op_observer/op_observer_noop.h b/src/mongo/db/op_observer/op_observer_noop.h index 66344d8e686..8bea94e5578 100644 --- a/src/mongo/db/op_observer/op_observer_noop.h +++ b/src/mongo/db/op_observer/op_observer_noop.h @@ -46,7 +46,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/op_observer/op_observer_registry.h b/src/mongo/db/op_observer/op_observer_registry.h index 59e4b9eb83e..0ce554bfb9a 100644 --- a/src/mongo/db/op_observer/op_observer_registry.h +++ b/src/mongo/db/op_observer/op_observer_registry.h @@ -76,10 +76,11 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final { + const UUID& globalIndexUUID, + long long numKeys) final { ReservedTimes times{opCtx}; for (auto& o : _observers) - o->onDropGlobalIndex(opCtx, globalIndexNss, globalIndexUUID); + o->onDropGlobalIndex(opCtx, globalIndexNss, globalIndexUUID, numKeys); }; void onCreateIndex(OperationContext* const opCtx, diff --git a/src/mongo/db/op_observer/user_write_block_mode_op_observer.h b/src/mongo/db/op_observer/user_write_block_mode_op_observer.h index 1b03e37a316..40e39378e68 100644 --- a/src/mongo/db/op_observer/user_write_block_mode_op_observer.h +++ b/src/mongo/db/op_observer/user_write_block_mode_op_observer.h @@ -173,7 +173,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; // Index builds committing can be left unchecked since we kill any active index builds before // enabling write blocking. This means any index build which gets to the commit phase while diff --git a/src/mongo/db/repl/oplog.cpp b/src/mongo/db/repl/oplog.cpp index 83866de6c9d..016e9be97c1 100644 --- a/src/mongo/db/repl/oplog.cpp +++ b/src/mongo/db/repl/oplog.cpp @@ -1863,19 +1863,21 @@ Status applyOperation_inlock(OperationContext* opCtx, case OpTypeEnum::kInsertGlobalIndexKey: { invariant(op.getUuid()); - global_index::insertKey(opCtx, - *op.getUuid(), - op.getObject().getObjectField("ik"), - op.getObject().getObjectField("dk")); + global_index::insertKey( + opCtx, + *op.getUuid(), + op.getObject().getObjectField(global_index::kOplogEntryIndexKeyFieldName), + op.getObject().getObjectField(global_index::kOplogEntryDocKeyFieldName)); break; } case OpTypeEnum::kDeleteGlobalIndexKey: { invariant(op.getUuid()); - global_index::deleteKey(opCtx, - *op.getUuid(), - op.getObject().getObjectField("ik"), - op.getObject().getObjectField("dk")); + global_index::deleteKey( + opCtx, + *op.getUuid(), + op.getObject().getObjectField(global_index::kOplogEntryIndexKeyFieldName), + op.getObject().getObjectField(global_index::kOplogEntryDocKeyFieldName)); break; } default: { diff --git a/src/mongo/db/repl/oplog_entry.cpp b/src/mongo/db/repl/oplog_entry.cpp index 219dcb5be1c..8c71135dd72 100644 --- a/src/mongo/db/repl/oplog_entry.cpp +++ b/src/mongo/db/repl/oplog_entry.cpp @@ -32,6 +32,7 @@ #include "mongo/db/repl/oplog_entry.h" +#include "mongo/db/global_index.h" #include "mongo/db/index/index_descriptor.h" #include "mongo/db/namespace_string.h" #include "mongo/db/server_feature_flags_gen.h" @@ -135,7 +136,8 @@ ReplOperation makeGlobalIndexCrudOperation(const NamespaceString& indexNss, // required oplog entry field. op.setNss(indexNss.getCommandNS()); op.setUuid(indexUuid); - op.setObject(BSON("ik" << key << "dk" << docKey)); + op.setObject(BSON(global_index::kOplogEntryIndexKeyFieldName + << key << global_index::kOplogEntryDocKeyFieldName << docKey)); return op; } } // namespace diff --git a/src/mongo/db/repl/primary_only_service_op_observer.h b/src/mongo/db/repl/primary_only_service_op_observer.h index 54707c9ca25..391f612daa9 100644 --- a/src/mongo/db/repl/primary_only_service_op_observer.h +++ b/src/mongo/db/repl/primary_only_service_op_observer.h @@ -58,7 +58,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/repl/rollback_impl.cpp b/src/mongo/db/repl/rollback_impl.cpp index 2fff5e05a99..a1cec2f9309 100644 --- a/src/mongo/db/repl/rollback_impl.cpp +++ b/src/mongo/db/repl/rollback_impl.cpp @@ -44,6 +44,7 @@ #include "mongo/db/db_raii.h" #include "mongo/db/dbdirectclient.h" #include "mongo/db/dbhelpers.h" +#include "mongo/db/global_index.h" #include "mongo/db/index_builds_coordinator.h" #include "mongo/db/logical_time_validator.h" #include "mongo/db/operation_context.h" @@ -91,6 +92,8 @@ RollbackImpl::Listener kNoopListener; constexpr auto kInsertCmdName = "insert"_sd; constexpr auto kUpdateCmdName = "update"_sd; constexpr auto kDeleteCmdName = "delete"_sd; +constexpr auto kInsertGlobalIndexKeyCmdName = "insertGlobalIndexKey"_sd; +constexpr auto kDeleteGlobalIndexKeyCmdName = "deleteGlobalIndexKey"_sd; constexpr auto kNumRecordsFieldName = "numRecords"_sd; constexpr auto kToFieldName = "to"_sd; constexpr auto kDropTargetFieldName = "dropTarget"_sd; @@ -601,13 +604,13 @@ void RollbackImpl::_runPhaseFromAbortToReconstructPreparedTxns( auto it = m.find(key); return (it == m.end()) ? 0 : it->second; }; - LOGV2(21599, - "Rollback reverted {insert} insert operations, {update} update operations and {delete} " - "delete operations.", - "Rollback reverted command counts", + LOGV2(6984700, + "Operations reverted by rollback", "insert"_attr = getCommandCount(kInsertCmdName), "update"_attr = getCommandCount(kUpdateCmdName), - "delete"_attr = getCommandCount(kDeleteCmdName)); + "delete"_attr = getCommandCount(kDeleteCmdName), + "insertGlobalIndexKey"_attr = getCommandCount(kInsertGlobalIndexKeyCmdName), + "deleteGlobalIndexKey"_attr = getCommandCount(kDeleteGlobalIndexKeyCmdName)); // Retryable writes create derived updates to the transactions table which can be coalesced into // one operation, so certain session operations history may be lost after restoring to the @@ -919,12 +922,21 @@ Status RollbackImpl::_processRollbackOp(OperationContext* opCtx, const OplogEntr // Keep track of the _ids of inserted and updated documents, as we may need to write them out to // a rollback file. - if (opType == OpTypeEnum::kInsert || opType == OpTypeEnum::kUpdate) { + if (opType == OpTypeEnum::kInsert || opType == OpTypeEnum::kUpdate || + opType == OpTypeEnum::kInsertGlobalIndexKey) { const auto uuid = oplogEntry.getUuid(); invariant(uuid, str::stream() << "Oplog entry to roll back is unexpectedly missing a UUID: " << redact(oplogEntry.toBSONForLogging())); - const auto idElem = oplogEntry.getIdElement(); + const auto idElem = [&]() { + if (opType == OpTypeEnum::kInsertGlobalIndexKey) { + // As global indexes currently lack support for multi-key, a key can be uniquely + // identified by its document key, which maps the _id field in the global index + // container (collection). + return oplogEntry.getObject()[global_index::kOplogEntryDocKeyFieldName]; + } + return oplogEntry.getIdElement(); + }(); if (!idElem.eoo()) { // We call BSONElement::wrap() on each _id element to create a new BSONObj with an owned // buffer, as the underlying storage may be gone when we access this map to write @@ -935,7 +947,7 @@ Status RollbackImpl::_processRollbackOp(OperationContext* opCtx, const OplogEntr } } - if (opType == OpTypeEnum::kInsert) { + if (opType == OpTypeEnum::kInsert || opType == OpTypeEnum::kInsertGlobalIndexKey) { auto idVal = oplogEntry.getObject().getStringField("_id"); if (serverGlobalParams.clusterRole == ClusterRole::ShardServer && opNss == NamespaceString::kServerConfigurationNamespace && @@ -959,11 +971,12 @@ Status RollbackImpl::_processRollbackOp(OperationContext* opCtx, const OplogEntr // Rolling back an insert must decrement the count by 1. _countDiffs[oplogEntry.getUuid().value()] -= 1; - } else if (opType == OpTypeEnum::kDelete) { + } else if (opType == OpTypeEnum::kDelete || opType == OpTypeEnum::kDeleteGlobalIndexKey) { // Rolling back a delete must increment the count by 1. _countDiffs[oplogEntry.getUuid().value()] += 1; } else if (opType == OpTypeEnum::kCommand) { - if (oplogEntry.getCommandType() == OplogEntry::CommandType::kCreate) { + if (oplogEntry.getCommandType() == OplogEntry::CommandType::kCreate || + oplogEntry.getCommandType() == OplogEntry::CommandType::kCreateGlobalIndex) { // If we roll back a create, then we do not need to change the size of that uuid. _countDiffs.erase(oplogEntry.getUuid().value()); _pendingDrops.erase(oplogEntry.getUuid().value()); @@ -984,13 +997,15 @@ Status RollbackImpl::_processRollbackOp(OperationContext* opCtx, const OplogEntr _pendingDrops.erase(importTargetUUID); _newCounts.erase(importTargetUUID); } - } else if (oplogEntry.getCommandType() == OplogEntry::CommandType::kDrop) { - // If we roll back a collection drop, parse the o2 field for the collection count for - // use later by _findRecordStoreCounts(). - // This will be used to reconcile collection counts in the case where the drop-pending - // collection is managed by the storage engine and is not accessible through the UUID - // catalog. - // Adding a _newCounts entry ensures that the count will be set after the rollback. + } else if (oplogEntry.getCommandType() == OplogEntry::CommandType::kDrop || + oplogEntry.getCommandType() == OplogEntry::CommandType::kDropGlobalIndex) { + // The collection count at collection drop time is op-logged in the 'o2' field. + // In the common case where the drop-pending collection is managed by the storage + // engine, the collection metadata - including the number of records at drop time - + // is not accessible through the catalog. + // Keep track of the record count stored in the 'o2' field via the _newCounts variable. + // This allows for cheaply restoring the collection count post rollback without an + // expensive collection scan. const auto uuid = oplogEntry.getUuid().value(); invariant(_countDiffs.find(uuid) == _countDiffs.end(), str::stream() << "Unexpected existing count diff for " << uuid.toString() @@ -1060,6 +1075,12 @@ Status RollbackImpl::_processRollbackOp(OperationContext* opCtx, const OplogEntr if (opType == OpTypeEnum::kDelete) { ++_observerInfo.rollbackCommandCounts[kDeleteCmdName]; } + if (opType == OpTypeEnum::kInsertGlobalIndexKey) { + ++_observerInfo.rollbackCommandCounts[kInsertGlobalIndexKeyCmdName]; + } + if (opType == OpTypeEnum::kDeleteGlobalIndexKey) { + ++_observerInfo.rollbackCommandCounts[kDeleteGlobalIndexKeyCmdName]; + } return Status::OK(); } diff --git a/src/mongo/db/repl/tenant_migration_donor_op_observer.h b/src/mongo/db/repl/tenant_migration_donor_op_observer.h index 58354e9fae8..842a43926ab 100644 --- a/src/mongo/db/repl/tenant_migration_donor_op_observer.h +++ b/src/mongo/db/repl/tenant_migration_donor_op_observer.h @@ -56,7 +56,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/repl/tenant_migration_recipient_op_observer.h b/src/mongo/db/repl/tenant_migration_recipient_op_observer.h index 3c501df9c72..b62a276b86d 100644 --- a/src/mongo/db/repl/tenant_migration_recipient_op_observer.h +++ b/src/mongo/db/repl/tenant_migration_recipient_op_observer.h @@ -57,7 +57,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/s/config_server_op_observer.h b/src/mongo/db/s/config_server_op_observer.h index a25aa999584..64411e8c2a4 100644 --- a/src/mongo/db/s/config_server_op_observer.h +++ b/src/mongo/db/s/config_server_op_observer.h @@ -57,7 +57,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/s/range_deleter_service_op_observer.h b/src/mongo/db/s/range_deleter_service_op_observer.h index 10a4bfc9e1e..430535ceac5 100644 --- a/src/mongo/db/s/range_deleter_service_op_observer.h +++ b/src/mongo/db/s/range_deleter_service_op_observer.h @@ -88,7 +88,8 @@ private: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/s/resharding/resharding_op_observer.h b/src/mongo/db/s/resharding/resharding_op_observer.h index 6759e335f5e..de3a84ded6b 100644 --- a/src/mongo/db/s/resharding/resharding_op_observer.h +++ b/src/mongo/db/s/resharding/resharding_op_observer.h @@ -72,7 +72,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/s/shard_server_op_observer.h b/src/mongo/db/s/shard_server_op_observer.h index 28c8337e477..3c1db831e7d 100644 --- a/src/mongo/db/s/shard_server_op_observer.h +++ b/src/mongo/db/s/shard_server_op_observer.h @@ -56,7 +56,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/db/serverless/shard_split_donor_op_observer.h b/src/mongo/db/serverless/shard_split_donor_op_observer.h index 3a9b056cce4..fba3471c7dd 100644 --- a/src/mongo/db/serverless/shard_split_donor_op_observer.h +++ b/src/mongo/db/serverless/shard_split_donor_op_observer.h @@ -55,7 +55,8 @@ public: void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, diff --git a/src/mongo/idl/cluster_server_parameter_op_observer.h b/src/mongo/idl/cluster_server_parameter_op_observer.h index 6d83e84658e..0de4a831616 100644 --- a/src/mongo/idl/cluster_server_parameter_op_observer.h +++ b/src/mongo/idl/cluster_server_parameter_op_observer.h @@ -108,7 +108,8 @@ public: BSONObj indexDoc) final {} void onDropGlobalIndex(OperationContext* opCtx, const NamespaceString& globalIndexNss, - const UUID& globalIndexUUID) final{}; + const UUID& globalIndexUUID, + long long numKeys) final{}; void onCreateIndex(OperationContext* opCtx, const NamespaceString& nss, -- cgit v1.2.1