diff --git a/jstests/auth/internal_command_auth_validation.js b/jstests/auth/internal_command_auth_validation.js index cf31e75521c..7de1beee89e 100644 --- a/jstests/auth/internal_command_auth_validation.js +++ b/jstests/auth/internal_command_auth_validation.js @@ -833,6 +833,18 @@ const internalCommandsMap = { phase: "complete", }, }, + _shardsvrSplitChunk: { + testname: "_shardsvrSplitChunk", + command: { + _shardsvrSplitChunk: "test.x", + keyPattern: {x: 1}, + min: {x: MinKey}, + max: {x: MaxKey}, + splitKeys: [{x: 0}], + from: "shard0000", + epoch: ObjectId(), + }, + }, _shardsvrValidateShardKeyCandidate: { testname: "_shardsvrValidateShardKeyCandidate", command: {_shardsvrValidateShardKeyCandidate: "x.y", key: {a: 1}}, diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js index 4b70325aefa..f6798049d91 100644 --- a/jstests/auth/lib/commands_lib.js +++ b/jstests/auth/lib/commands_lib.js @@ -7330,8 +7330,16 @@ export const authCommandsLib = { ], }, { - testname: "splitChunk", - command: {splitChunk: "test.x"}, + testname: "_shardsvrSplitChunk", + command: { + _shardsvrSplitChunk: "test.x", + keyPattern: {a: 1}, + min: {a: MinKey}, + max: {a: MaxKey}, + splitKeys: [{a: 0}], + from: "shard0000", + epoch: ObjectId(), + }, skipSharded: true, testcases: [ { diff --git a/jstests/concurrency/fsm_workload_helpers/chunks.js b/jstests/concurrency/fsm_workload_helpers/chunks.js index 53cec8fdba8..be54f1c9061 100644 --- a/jstests/concurrency/fsm_workload_helpers/chunks.js +++ b/jstests/concurrency/fsm_workload_helpers/chunks.js @@ -51,7 +51,13 @@ export var ChunkHelper = (function () { function splitChunkAt(db, collName, middle) { let cmd = {split: db[collName].getFullName(), middle: middle}; - return runCommandWithRetries(db, cmd, (res) => res.code === ErrorCodes.LockBusy); + // TODO (SERVER-125033): Accept ConflictingOperationInProgress until multiple split chunk + // commands can instantiate concurrent coordinators. + return runCommandWithRetries( + db, + cmd, + (res) => res.code === ErrorCodes.LockBusy || res.code === ErrorCodes.ConflictingOperationInProgress, + ); } function splitChunkAtPoint(db, collName, splitPoint) { @@ -60,7 +66,13 @@ export var ChunkHelper = (function () { function splitChunkWithBounds(db, collName, bounds) { let cmd = {split: db[collName].getFullName(), bounds: bounds}; - return runCommandWithRetries(db, cmd, (res) => res.code === ErrorCodes.LockBusy); + // TODO (SERVER-125033): Accept ConflictingOperationInProgress until multiple split chunk + // commands can instantiate concurrent coordinators. + return runCommandWithRetries( + db, + cmd, + (res) => res.code === ErrorCodes.LockBusy || res.code === ErrorCodes.ConflictingOperationInProgress, + ); } function moveChunk(db, collName, bounds, toShard, waitForDelete, secondaryThrottle) { diff --git a/jstests/core/catalog/views/views_all_commands.js b/jstests/core/catalog/views/views_all_commands.js index 5d86bcdf887..c65b6b5dbb2 100644 --- a/jstests/core/catalog/views/views_all_commands.js +++ b/jstests/core/catalog/views/views_all_commands.js @@ -222,6 +222,7 @@ let viewsCommandTests = { _shardsvrRunSearchIndexCommand: {skip: isAnInternalCommand}, _shardsvrSetClusterParameter: {skip: isAnInternalCommand}, _shardsvrSetUserWriteBlockMode: {skip: isAnInternalCommand}, + _shardsvrSplitChunk: {skip: isAnInternalCommand}, _shardsvrUpgradeDowngradeViewlessTimeseries: {skip: isAnInternalCommand}, _shardsvrTimeseriesUpgradeDowngradePrepare: {skip: isAnInternalCommand}, _shardsvrTimeseriesUpgradeDowngradeCommit: {skip: isAnInternalCommand}, @@ -763,19 +764,7 @@ let viewsCommandTests = { isAdminCommand: true, }, splitChunk: { - command: { - splitChunk: "test.view", - from: "shard0000", - min: {x: MinKey}, - max: {x: 0}, - keyPattern: {x: 1}, - splitKeys: [{x: -2}, {x: -1}], - shardVersion: {t: Timestamp(1, 2), e: ObjectId(), v: Timestamp(1, 1)}, - }, - skipSharded: true, - expectFailure: true, - expectedErrorCode: ErrorCodes.ShardingStateNotInitialized, - isAdminCommand: true, + skip: isDeprecated, }, splitVector: { command: { diff --git a/jstests/core_sharding/chunk_migration/merge_split_chunks.js b/jstests/core_sharding/chunk_migration/merge_split_chunks.js deleted file mode 100644 index 32af3678116..00000000000 --- a/jstests/core_sharding/chunk_migration/merge_split_chunks.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Tests that merge, split and move chunks via mongos works/doesn't work with different chunk - * configurations - * - * @tags: [ - * requires_getmore, - * assumes_balancer_off, - * does_not_support_stepdowns, - * # This test performs explicit calls to shardCollection - * assumes_unsharded_collection, - * ] - */ - -import {findChunksUtil} from "jstests/sharding/libs/find_chunks_util.js"; - -const dbName = db.getName(); -const admin = db.getSiblingDB("admin"); -const config = db.getSiblingDB("config"); -const collName = jsTestName(); -const ns = dbName + "." + collName; -const coll = db.getCollection(collName); - -const shardNames = db.adminCommand({listShards: 1}).shards.map((shard) => shard._id); - -if (shardNames.length < 2) { - print(jsTestName() + " will not run; at least 2 shards are required."); - quit(); -} - -print(jsTestName() + " is running on " + shardNames.length + " shards."); - -assert.commandWorked(admin.runCommand({enableSharding: dbName})); -assert.commandWorked(admin.runCommand({shardCollection: ns, key: {_id: 1}})); - -// Make sure split is correctly disabled for unsharded collection -jsTest.log("Trying to split an unsharded collection ..."); -const collNameUnsplittable = collName + "_unsplittable"; -const nsUnsplittable = dbName + "." + collNameUnsplittable; -assert.commandWorked(db.runCommand({create: collNameUnsplittable})); -assert.commandFailedWithCode( - admin.runCommand({split: nsUnsplittable, middle: {_id: 0}}), - ErrorCodes.NamespaceNotSharded, -); -jsTest.log("Trying to merge an unsharded collection ..."); -assert.commandFailedWithCode( - admin.runCommand({mergeChunks: nsUnsplittable, bounds: [{_id: 90}, {_id: MaxKey}]}), - ErrorCodes.NamespaceNotSharded, -); -db.getCollection(collNameUnsplittable).drop(); - -// Create ranges MIN->0,0->10,(hole),20->40,40->50,50->90,(hole),100->110,110->MAX on first -// shard -jsTest.log("Creating ranges..."); - -assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 0}})); -assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 10}})); -assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 20}})); -assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 40}})); -assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 50}})); -assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 90}})); -assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 100}})); -assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 110}})); - -assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 10}, to: shardNames[1]})); -assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 90}, to: shardNames[1]})); - -// Insert some data into each of the consolidated ranges -let numDocs = 0; -const bulk = coll.initializeUnorderedBulkOp(); -for (let i = 0; i <= 120; i++) { - bulk.insert({_id: i}); - numDocs++; -} -assert.commandWorked(bulk.execute({w: "majority"})); - -// S0: min->0, 0->10, 20->40, 40->50, 50->90, 100->110, 110->max -// S1: 10->20, 90->100 -assert.eq(9, findChunksUtil.findChunksByNs(config, ns).itcount()); - -jsTest.log("Trying merges that should succeed..."); - -// Make sure merge including the MinKey works -assert.commandWorked(admin.runCommand({mergeChunks: ns, bounds: [{_id: MinKey}, {_id: 10}]})); -assert.eq(8, findChunksUtil.findChunksByNs(config, ns).itcount()); -// S0: min->10, 20->40, 40->50, 50->90, 100->110, 110->max -// S1: 10->20, 90->100 - -// Make sure merging three chunks in the middle works -assert.commandWorked(admin.runCommand({mergeChunks: ns, bounds: [{_id: 20}, {_id: 90}]})); -assert.eq(6, findChunksUtil.findChunksByNs(config, ns).itcount()); -assert.eq(numDocs, coll.find().itcount()); -// S0: min->10, 20->90, 100->110, 110->max -// S1: 10->20, 90->100 - -// Make sure splitting chunks after merging works -assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 55}})); -assert.eq(7, findChunksUtil.findChunksByNs(config, ns).itcount()); -assert.eq(numDocs, coll.find().itcount()); -// S0: min->10, 20->55, 55->90, 100->110, 110->max -// S1: 10->20, 90->100 - -// make sure moving the new chunk works -assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 20}, to: shardNames[1]})); -assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 55}, to: shardNames[1]})); -assert.eq(7, findChunksUtil.findChunksByNs(config, ns).itcount()); -assert.eq(numDocs, coll.find().itcount()); -// S0: min->10, 100->110, 110->max -// S1: 10->20, 20->55, 55->90, 90->100 - -// Make sure merge including the MaxKey works -assert.commandWorked(admin.runCommand({mergeChunks: ns, bounds: [{_id: 100}, {_id: MaxKey}]})); -assert.eq(6, findChunksUtil.findChunksByNs(config, ns).itcount()); -// S0: min->10, 100->max -// S1: 10->20, 20->55, 55->90, 90->100 - -// Make sure merging chunks after a chunk has been moved out of a shard succeeds -assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 110}, to: shardNames[1]})); -assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 10}, to: shardNames[0]})); -assert.eq(numDocs, coll.find().itcount()); -assert.eq(6, findChunksUtil.findChunksByNs(config, ns).itcount()); -// S0: min->10, 10->20 -// S1: 20->55, 55->90, 90->100, 100->max - -assert.commandWorked(admin.runCommand({mergeChunks: ns, bounds: [{_id: 90}, {_id: MaxKey}]})); -assert.commandWorked(admin.runCommand({mergeChunks: ns, bounds: [{_id: 20}, {_id: 90}]})); -assert.eq(numDocs, coll.find().itcount()); -// S0: min->10, 10->20 -// S1: 20->90, 90->max - -assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 15}})); -assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 30}})); -assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 30}, to: shardNames[0]})); -assert.eq(numDocs, coll.find().itcount()); -// S0: min->10, 10->15, 15->20, 30->90 -// S1: 20->30, 90->max - -// range has a hole on shard 0 -assert.commandFailed(admin.runCommand({mergeChunks: ns, bounds: [{_id: MinKey}, {_id: 90}]})); - -assert.eq(6, findChunksUtil.findChunksByNs(config, ns).itcount()); - -coll.drop(); diff --git a/jstests/core_sharding/chunk_migration/BUILD.bazel b/jstests/core_sharding/chunk_operations/BUILD.bazel similarity index 100% rename from jstests/core_sharding/chunk_migration/BUILD.bazel rename to jstests/core_sharding/chunk_operations/BUILD.bazel diff --git a/jstests/core_sharding/chunk_operations/OWNERS.yml b/jstests/core_sharding/chunk_operations/OWNERS.yml new file mode 100644 index 00000000000..93f9a8c320b --- /dev/null +++ b/jstests/core_sharding/chunk_operations/OWNERS.yml @@ -0,0 +1,11 @@ +version: 2.0.0 +filters: + - "*": + approvers: + - 10gen/server-cluster-scalability + - "*merge*": + approvers: + - 10gen/server-catalog-and-routing-ddl + - "*split*": + approvers: + - 10gen/server-catalog-and-routing-ddl diff --git a/jstests/core_sharding/chunk_migration/chunk_operations_preserve_uuid.js b/jstests/core_sharding/chunk_operations/chunk_operations_preserve_uuid.js similarity index 100% rename from jstests/core_sharding/chunk_migration/chunk_operations_preserve_uuid.js rename to jstests/core_sharding/chunk_operations/chunk_operations_preserve_uuid.js diff --git a/jstests/core_sharding/chunk_operations/merge_split_chunks.js b/jstests/core_sharding/chunk_operations/merge_split_chunks.js new file mode 100644 index 00000000000..33dab9c0da0 --- /dev/null +++ b/jstests/core_sharding/chunk_operations/merge_split_chunks.js @@ -0,0 +1,202 @@ +/** + * Tests that merge, split and move chunks via mongos works/doesn't work with different chunk + * configurations. Covers the three split modes (middle, find, bounds), merge across various + * chunk layouts, and move of newly split chunks. + * + * @tags: [ + * requires_getmore, + * assumes_balancer_off, + * does_not_support_stepdowns, + * # This test performs explicit calls to shardCollection + * assumes_unsharded_collection, + * requires_2_or_more_shards, + * ] + */ + +import {after, before, describe, it} from "jstests/libs/mochalite.js"; +import {findChunksUtil} from "jstests/sharding/libs/find_chunks_util.js"; + +describe("merge, split, and move chunks", function () { + const dbName = db.getName(); + const admin = db.getSiblingDB("admin"); + const config = db.getSiblingDB("config"); + const collName = jsTestName(); + const ns = dbName + "." + collName; + const coll = db.getCollection(collName); + const shardNames = db.adminCommand({listShards: 1}).shards.map((shard) => shard._id); + let numDocs; + + before(function () { + assert.commandWorked(admin.runCommand({enableSharding: dbName})); + assert.commandWorked(admin.runCommand({shardCollection: ns, key: {_id: 1}})); + + // Create ranges MIN->0, 0->10, (hole), 20->40, 40->50, 50->90, (hole), + // 100->110, 110->MAX on shard 0, with chunks [10->20) and [90->100) on shard 1. + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 0}})); + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 10}})); + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 20}})); + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 40}})); + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 50}})); + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 90}})); + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 100}})); + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 110}})); + + assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 10}, to: shardNames[1]})); + assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 90}, to: shardNames[1]})); + + numDocs = 0; + const bulk = coll.initializeUnorderedBulkOp(); + for (let i = 0; i <= 120; i++) { + bulk.insert({_id: i}); + numDocs++; + } + assert.commandWorked(bulk.execute({w: "majority"})); + + // S0: min->0, 0->10, 20->40, 40->50, 50->90, 100->110, 110->max + // S1: 10->20, 90->100 + assert.eq(9, findChunksUtil.findChunksByNs(config, ns).itcount()); + }); + + after(function () { + coll.drop(); + }); + + describe("operations on unsharded collections", function () { + it("rejects split on unsharded collection", function () { + const unsplittableName = collName + "_unsplittable"; + const unsplittableNs = dbName + "." + unsplittableName; + assert.commandWorked(db.runCommand({create: unsplittableName})); + assert.commandFailedWithCode( + admin.runCommand({split: unsplittableNs, middle: {_id: 0}}), + ErrorCodes.NamespaceNotSharded, + ); + db.getCollection(unsplittableName).drop(); + }); + + it("rejects merge on unsharded collection", function () { + const unsplittableName = collName + "_unsplittable_merge"; + const unsplittableNs = dbName + "." + unsplittableName; + assert.commandWorked(db.runCommand({create: unsplittableName})); + assert.commandFailedWithCode( + admin.runCommand({mergeChunks: unsplittableNs, bounds: [{_id: 90}, {_id: MaxKey}]}), + ErrorCodes.NamespaceNotSharded, + ); + db.getCollection(unsplittableName).drop(); + }); + }); + + // The tests in this block are sequential: each builds on the chunk layout left by the + // previous test. mochalite runs tests serially within a describe, so this is safe. + describe("chunk merge and split workflows", function () { + it("merges chunks including MinKey", function () { + assert.commandWorked(admin.runCommand({mergeChunks: ns, bounds: [{_id: MinKey}, {_id: 10}]})); + assert.eq(8, findChunksUtil.findChunksByNs(config, ns).itcount()); + // S0: min->10, 20->40, 40->50, 50->90, 100->110, 110->max + // S1: 10->20, 90->100 + }); + + it("merges three adjacent chunks in the middle", function () { + assert.commandWorked(admin.runCommand({mergeChunks: ns, bounds: [{_id: 20}, {_id: 90}]})); + assert.eq(6, findChunksUtil.findChunksByNs(config, ns).itcount()); + assert.eq(numDocs, coll.find().itcount()); + // S0: min->10, 20->90, 100->110, 110->max + // S1: 10->20, 90->100 + }); + + it("splits a merged chunk with middle", function () { + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 55}})); + assert.eq(7, findChunksUtil.findChunksByNs(config, ns).itcount()); + assert.eq(numDocs, coll.find().itcount()); + // S0: min->10, 20->55, 55->90, 100->110, 110->max + // S1: 10->20, 90->100 + }); + + it("moves newly split chunks to another shard", function () { + assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 20}, to: shardNames[1]})); + assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 55}, to: shardNames[1]})); + assert.eq(7, findChunksUtil.findChunksByNs(config, ns).itcount()); + assert.eq(numDocs, coll.find().itcount()); + // S0: min->10, 100->110, 110->max + // S1: 10->20, 20->55, 55->90, 90->100 + }); + + it("merges chunks including MaxKey", function () { + assert.commandWorked(admin.runCommand({mergeChunks: ns, bounds: [{_id: 100}, {_id: MaxKey}]})); + assert.eq(6, findChunksUtil.findChunksByNs(config, ns).itcount()); + // S0: min->10, 100->max + // S1: 10->20, 20->55, 55->90, 90->100 + }); + + it("merges chunks after a chunk has been moved out of a shard", function () { + assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 110}, to: shardNames[1]})); + assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 10}, to: shardNames[0]})); + assert.eq(numDocs, coll.find().itcount()); + assert.eq(6, findChunksUtil.findChunksByNs(config, ns).itcount()); + // S0: min->10, 10->20 + // S1: 20->55, 55->90, 90->100, 100->max + + assert.commandWorked(admin.runCommand({mergeChunks: ns, bounds: [{_id: 90}, {_id: MaxKey}]})); + assert.commandWorked(admin.runCommand({mergeChunks: ns, bounds: [{_id: 20}, {_id: 90}]})); + assert.eq(numDocs, coll.find().itcount()); + // S0: min->10, 10->20 + // S1: 20->90, 90->max + }); + + it("splits after merge then moves across shards", function () { + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 15}})); + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 30}})); + assert.commandWorked(admin.runCommand({moveChunk: ns, find: {_id: 30}, to: shardNames[0]})); + assert.eq(numDocs, coll.find().itcount()); + // S0: min->10, 10->15, 15->20, 30->90 + // S1: 20->30, 90->max + }); + + it("rejects merge across a hole in chunk ranges", function () { + // S0 has chunks min->10, 10->15, 15->20, 30->90 with a hole at [20, 30) + assert.commandFailed(admin.runCommand({mergeChunks: ns, bounds: [{_id: MinKey}, {_id: 90}]})); + assert.eq(6, findChunksUtil.findChunksByNs(config, ns).itcount()); + }); + + // The following tests exercise split modes not covered by the workflow above. + // State at this point: + // S0: min->10, 10->15, 15->20, 30->90 + // S1: 20->30, 90->max + + it("splits using find mode", function () { + // Split the [30, 90) chunk on S0 via find (which uses selectMedianKey / + // splitVector with force:true to auto-select a split point). + const chunksBefore = findChunksUtil.findChunksByNs(config, ns).itcount(); + assert.commandWorked(admin.runCommand({split: ns, find: {_id: 50}})); + assert.gt(findChunksUtil.findChunksByNs(config, ns).itcount(), chunksBefore); + assert.eq(numDocs, coll.find().itcount()); + }); + + it("splits using bounds mode", function () { + // Split the [90, MaxKey) chunk on S1 via bounds (which auto-selects a split + // point using splitVector on the target chunk). + const targetChunk = findChunksUtil.findOneChunkByNs(config, ns, {min: {_id: 90}}); + assert.neq(null, targetChunk); + const chunksBefore = findChunksUtil.findChunksByNs(config, ns).itcount(); + + assert.commandWorked(admin.runCommand({split: ns, bounds: [targetChunk.min, targetChunk.max]})); + assert.gt(findChunksUtil.findChunksByNs(config, ns).itcount(), chunksBefore); + assert.eq(numDocs, coll.find().itcount()); + }); + + it("re-splits at the same point after merge", function () { + // Split [10, 15) at _id: 12, merge the halves back, then split at 12 again. + // Confirms the split/merge/split round-trip works. + const chunksBefore = findChunksUtil.findChunksByNs(config, ns).itcount(); + + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 12}})); + assert.eq(chunksBefore + 1, findChunksUtil.findChunksByNs(config, ns).itcount()); + + assert.commandWorked(admin.runCommand({mergeChunks: ns, bounds: [{_id: 10}, {_id: 15}]})); + assert.eq(chunksBefore, findChunksUtil.findChunksByNs(config, ns).itcount()); + + assert.commandWorked(admin.runCommand({split: ns, middle: {_id: 12}})); + assert.eq(chunksBefore + 1, findChunksUtil.findChunksByNs(config, ns).itcount()); + assert.eq(numDocs, coll.find().itcount()); + }); + }); +}); diff --git a/jstests/core_sharding/chunk_migration/move_chunk.js b/jstests/core_sharding/chunk_operations/move_chunk.js similarity index 100% rename from jstests/core_sharding/chunk_migration/move_chunk.js rename to jstests/core_sharding/chunk_operations/move_chunk.js diff --git a/jstests/core_sharding/chunk_migration/move_chunk_split_merge_timeseries.js b/jstests/core_sharding/chunk_operations/move_chunk_split_merge_timeseries.js similarity index 100% rename from jstests/core_sharding/chunk_migration/move_chunk_split_merge_timeseries.js rename to jstests/core_sharding/chunk_operations/move_chunk_split_merge_timeseries.js diff --git a/jstests/core_sharding/chunk_migration/move_range_timeseries.js b/jstests/core_sharding/chunk_operations/move_range_timeseries.js similarity index 100% rename from jstests/core_sharding/chunk_migration/move_range_timeseries.js rename to jstests/core_sharding/chunk_operations/move_range_timeseries.js diff --git a/jstests/core_sharding/chunk_operations/split_chunk_parameter_validation.js b/jstests/core_sharding/chunk_operations/split_chunk_parameter_validation.js new file mode 100644 index 00000000000..50d0dc2b5d2 --- /dev/null +++ b/jstests/core_sharding/chunk_operations/split_chunk_parameter_validation.js @@ -0,0 +1,131 @@ +/** + * Tests the split command's parameter validation on mongos. Covers mutually exclusive parameter + * combinations, missing required parameters, malformed bounds arrays, and partial shard keys + * on compound-key collections. These validation paths live in cluster_split_cmd.cpp's + * errmsgRun() and are not exercised by the other split tests (basic_split.js, etc.). + * + * @tags: [ + * assumes_balancer_off, + * ] + */ + +import {after, before, describe, it} from "jstests/libs/mochalite.js"; + +describe("split command parameter validation", function () { + const dbName = db.getName(); + const admin = db.getSiblingDB("admin"); + + const simpleCollName = jsTestName() + "_simple"; + const simpleNs = dbName + "." + simpleCollName; + + const compoundCollName = jsTestName() + "_compound"; + const compoundNs = dbName + "." + compoundCollName; + + before(function () { + assert.commandWorked(admin.runCommand({shardCollection: simpleNs, key: {_id: 1}})); + assert.commandWorked(admin.runCommand({shardCollection: compoundNs, key: {x: 1, y: 1}})); + }); + + after(function () { + db.getCollection(simpleCollName).drop(); + db.getCollection(compoundCollName).drop(); + }); + + describe("mutually exclusive parameters", function () { + it("rejects find and bounds together", function () { + assert.commandFailed( + admin.runCommand({ + split: simpleNs, + find: {_id: 0}, + bounds: [{_id: MinKey}, {_id: MaxKey}], + }), + ); + }); + + it("rejects find and middle together", function () { + assert.commandFailed( + admin.runCommand({ + split: simpleNs, + find: {_id: 0}, + middle: {_id: 0}, + }), + ); + }); + + it("rejects bounds and middle together", function () { + assert.commandFailed( + admin.runCommand({ + split: simpleNs, + bounds: [{_id: MinKey}, {_id: MaxKey}], + middle: {_id: 0}, + }), + ); + }); + + it("rejects all three parameters together", function () { + assert.commandFailed( + admin.runCommand({ + split: simpleNs, + find: {_id: 0}, + bounds: [{_id: MinKey}, {_id: MaxKey}], + middle: {_id: 0}, + }), + ); + }); + }); + + describe("missing parameters", function () { + it("rejects split with no find, bounds, or middle", function () { + assert.commandFailed(admin.runCommand({split: simpleNs})); + }); + }); + + describe("incomplete bounds", function () { + it("rejects bounds with only lower bound", function () { + assert.commandFailed( + admin.runCommand({ + split: simpleNs, + bounds: [{_id: MinKey}], + }), + ); + }); + + it("rejects bounds with empty array", function () { + assert.commandFailed( + admin.runCommand({ + split: simpleNs, + bounds: [], + }), + ); + }); + }); + + describe("partial shard key on compound key collection", function () { + it("rejects middle with partial compound shard key", function () { + assert.commandFailed( + admin.runCommand({ + split: compoundNs, + middle: {x: 0}, + }), + ); + }); + + it("rejects find with partial compound shard key", function () { + assert.commandFailed( + admin.runCommand({ + split: compoundNs, + find: {x: 0}, + }), + ); + }); + + it("rejects bounds with partial compound shard key", function () { + assert.commandFailed( + admin.runCommand({ + split: compoundNs, + bounds: [{x: MinKey}, {x: MaxKey}], + }), + ); + }); + }); +}); diff --git a/jstests/libs/write_concern_all_commands.js b/jstests/libs/write_concern_all_commands.js index 66dafecf775..70e10327522 100644 --- a/jstests/libs/write_concern_all_commands.js +++ b/jstests/libs/write_concern_all_commands.js @@ -210,6 +210,7 @@ const wcCommandsTests = { _shardsvrSetAllowMigrations: {skip: "internal command"}, _shardsvrSetClusterParameter: {skip: "internal command"}, _shardsvrSetUserWriteBlockMode: {skip: "internal command"}, + _shardsvrSplitChunk: {skip: "internal command"}, _shardsvrValidateShardKeyCandidate: {skip: "internal command"}, _shardsvrCollMod: {skip: "internal command"}, _shardsvrCollModParticipant: {skip: "internal command"}, @@ -2987,7 +2988,6 @@ const wcCommandsTests = { shutdown: {skip: "does not accept write concern"}, sleep: {skip: "does not accept write concern"}, split: {skip: "does not accept write concern"}, - splitChunk: {skip: "does not accept write concern"}, splitVector: {skip: "internal command"}, stageDebug: {skip: "does not accept write concern"}, startSession: {skip: "does not accept write concern"}, @@ -3436,6 +3436,7 @@ const wcTimeseriesCommandsTests = { _shardsvrSetAllowMigrations: {skip: "internal command"}, _shardsvrSetClusterParameter: {skip: "internal command"}, _shardsvrSetUserWriteBlockMode: {skip: "internal command"}, + _shardsvrSplitChunk: {skip: "internal command"}, _shardsvrValidateShardKeyCandidate: {skip: "internal command"}, _shardsvrCollMod: {skip: "internal command"}, _shardsvrCollModParticipant: {skip: "internal command"}, @@ -4378,7 +4379,6 @@ const wcTimeseriesCommandsTests = { shutdown: {skip: "does not accept write concern"}, sleep: {skip: "does not accept write concern"}, split: {skip: "does not accept write concern"}, - splitChunk: {skip: "does not accept write concern"}, splitVector: {skip: "internal command"}, stageDebug: {skip: "does not accept write concern"}, startSession: {skip: "does not accept write concern"}, diff --git a/jstests/replsets/all_commands_downgrading_to_upgraded.js b/jstests/replsets/all_commands_downgrading_to_upgraded.js index 4ae84fbd1a1..01fbe6aa91b 100644 --- a/jstests/replsets/all_commands_downgrading_to_upgraded.js +++ b/jstests/replsets/all_commands_downgrading_to_upgraded.js @@ -159,6 +159,7 @@ const allCommands = { _shardsvrSetAllowMigrations: {skip: isAnInternalCommand}, _shardsvrSetClusterParameter: {skip: isAnInternalCommand}, _shardsvrSetUserWriteBlockMode: {skip: isAnInternalCommand}, + _shardsvrSplitChunk: {skip: isAnInternalCommand}, _shardsvrValidateShardKeyCandidate: {skip: isAnInternalCommand}, _shardsvrCollMod: {skip: isAnInternalCommand}, _shardsvrCollModParticipant: {skip: isAnInternalCommand}, @@ -1602,7 +1603,6 @@ const allCommands = { isAdminCommand: true, isShardedOnly: true, }, - splitChunk: {skip: isAnInternalCommand}, splitVector: {skip: isAnInternalCommand}, startRecordingTraffic: { skip: "Renamed to startTrafficRecording", diff --git a/jstests/replsets/db_reads_while_recovering_all_commands.js b/jstests/replsets/db_reads_while_recovering_all_commands.js index 4385ea3c735..7e832f6cefa 100644 --- a/jstests/replsets/db_reads_while_recovering_all_commands.js +++ b/jstests/replsets/db_reads_while_recovering_all_commands.js @@ -145,6 +145,7 @@ const allCommands = { _shardsvrSetAllowMigrations: {skip: isPrimaryOnly}, _shardsvrSetClusterParameter: {skip: isAnInternalCommand}, _shardsvrSetUserWriteBlockMode: {skip: isPrimaryOnly}, + _shardsvrSplitChunk: {skip: isPrimaryOnly}, _shardsvrValidateShardKeyCandidate: {skip: isPrimaryOnly}, _shardsvrCoordinateMultiUpdate: {skip: isAnInternalCommand}, _shardsvrCollMod: {skip: isPrimaryOnly}, @@ -448,7 +449,6 @@ const allCommands = { shardingState: {skip: isNotAUserDataRead}, shutdown: {skip: isNotAUserDataRead}, sleep: {skip: isNotAUserDataRead}, - splitChunk: {skip: isPrimaryOnly}, splitVector: {skip: isPrimaryOnly}, startRecordingTraffic: {skip: "Renamed to startTrafficRecording"}, stopRecordingTraffic: {skip: "Renamed to stopTrafficRecording"}, diff --git a/jstests/sharding/all_commands_direct_shard_connection_auth.js b/jstests/sharding/all_commands_direct_shard_connection_auth.js index e97e5fea559..13d8f54d338 100644 --- a/jstests/sharding/all_commands_direct_shard_connection_auth.js +++ b/jstests/sharding/all_commands_direct_shard_connection_auth.js @@ -154,6 +154,7 @@ const allCommands = { _shardsvrSetAllowMigrations: {skip: isAnInternalCommand}, _shardsvrSetClusterParameter: {skip: isAnInternalCommand}, _shardsvrSetUserWriteBlockMode: {skip: isAnInternalCommand}, + _shardsvrSplitChunk: {skip: isAnInternalCommand}, _shardsvrValidateShardKeyCandidate: {skip: isAnInternalCommand}, _shardsvrCollMod: {skip: isAnInternalCommand}, _shardsvrCollModParticipant: {skip: isAnInternalCommand}, @@ -1188,7 +1189,6 @@ const allCommands = { shutdown: {skip: "requires changes to shards"}, sleep: {skip: isAnInternalCommand}, split: {skip: requiresMongoS}, - splitChunk: {skip: isAnInternalCommand}, splitVector: {skip: isAnInternalCommand}, startRecordingTraffic: {skip: "Renamed to startTrafficRecording"}, stopRecordingTraffic: {skip: "Renamed to stopTrafficRecording"}, diff --git a/jstests/sharding/database_versioning_all_commands.js b/jstests/sharding/database_versioning_all_commands.js index 5b74185d710..9680e31e07b 100644 --- a/jstests/sharding/database_versioning_all_commands.js +++ b/jstests/sharding/database_versioning_all_commands.js @@ -2,6 +2,10 @@ * Specifies for each command whether it is expected to send a databaseVersion, and verifies that * the commands match the specification. * + * Each command is executed against two different scenarios: after movePrimary, and after + * dropDatabase + recreate on a different primary shard; to verify that the command behaves correctly + * when run with a stale dbVersion. + * * Each command must have exactly one corresponding test defined. Each defined test case must * correspond to an existing command. The allowable fields for the test cases are as follows: * @@ -912,7 +916,19 @@ const allTestCases = { shardCollection: {skip: "does not forward command to primary shard"}, shardDrainingStatus: {skip: "not on a user database"}, shutdown: {skip: "does not forward command to primary shard"}, - split: {skip: "does not forward command to primary shard"}, + split: { + run: { + sendsDbVersion: false, + runsAgainstAdminDb: true, + command: function (dbName, collName) { + return { + split: dbName + "." + collName, + middle: {_id: 0}, + }; + }, + expectedFailureCode: ErrorCodes.NamespaceNotSharded, + }, + }, splitVector: {skip: "does not forward command to primary shard"}, getTrafficRecordingStatus: {skip: "executes locally on targeted node"}, startRecordingTraffic: {skip: "Renamed to startTrafficRecording"}, @@ -1213,6 +1229,23 @@ const allTestCases = { _shardsvrSetAllowMigrations: {skip: "TODO"}, _shardsvrSetClusterParameter: {skip: "TODO"}, _shardsvrSetUserWriteBlockMode: {skip: "TODO"}, + _shardsvrSplitChunk: { + run: { + runsAgainstAdminDb: true, + command: function (dbName, collName) { + return { + _shardsvrSplitChunk: dbName + "." + collName, + keyPattern: {_id: 1}, + min: {_id: MinKey}, + max: {_id: MaxKey}, + splitKeys: [{_id: 0}], + from: "shard0", + epoch: ObjectId(), + }; + }, + expectedFailureCode: ErrorCodes.StaleConfig, + }, + }, _shardsvrUpgradeDowngradeViewlessTimeseries: {skip: "internal command"}, _shardsvrTimeseriesUpgradeDowngradePrepare: {skip: "internal command"}, _shardsvrTimeseriesUpgradeDowngradeCommit: {skip: "internal command"}, @@ -1397,7 +1430,7 @@ const allTestCases = { shardingState: {skip: "TODO"}, shutdown: {skip: "TODO"}, sleep: {skip: "TODO"}, - splitChunk: {skip: "TODO"}, + splitChunk: {skip: "is deprecated", conditional: true}, splitVector: {skip: "TODO"}, startSession: {skip: "TODO"}, startTrafficRecording: {skip: "TODO"}, diff --git a/jstests/sharding/libs/last_lts_mongod_commands.js b/jstests/sharding/libs/last_lts_mongod_commands.js index ea0d85507f1..6ab29c15ea6 100644 --- a/jstests/sharding/libs/last_lts_mongod_commands.js +++ b/jstests/sharding/libs/last_lts_mongod_commands.js @@ -83,4 +83,5 @@ export const commandsAddedToMongodSinceLastLTS = [ "_shardsvrCommitCreateCollectionMetadata", "_internalClearCollectionShardingMetadata", "_shardsvrCommitDropCollectionMetadata", + "_shardsvrSplitChunk", ]; diff --git a/jstests/sharding/libs/mongos_api_params_util.js b/jstests/sharding/libs/mongos_api_params_util.js index 3adac72b0b9..8b065ad5eaf 100644 --- a/jstests/sharding/libs/mongos_api_params_util.js +++ b/jstests/sharding/libs/mongos_api_params_util.js @@ -1665,8 +1665,9 @@ export let MongosAPIParametersUtil = (function () { commandName: "split", run: { inAPIVersion1: false, - configServerCommandName: "_configsvrCommitChunkSplit", - shardCommandName: "splitChunk", + // TODO (SERVER-108802): Re-enable this test case after the api version is propagated to the config server. + // configServerCommandName: "_configsvrCommitChunkSplit", + shardCommandName: "_shardsvrSplitChunk", runsAgainstAdminDb: true, permittedInTxn: false, requiresShardedCollection: true, diff --git a/jstests/sharding/read_write_concern_defaults_application.js b/jstests/sharding/read_write_concern_defaults_application.js index 9d30a845f65..c7556f077cf 100644 --- a/jstests/sharding/read_write_concern_defaults_application.js +++ b/jstests/sharding/read_write_concern_defaults_application.js @@ -212,6 +212,7 @@ let testCases = { _shardsvrSetAllowMigrations: {skip: "internal command"}, _shardsvrSetClusterParameter: {skip: "internal command"}, _shardsvrSetUserWriteBlockMode: {skip: "internal command"}, + _shardsvrSplitChunk: {skip: "internal command"}, _shardsvrValidateShardKeyCandidate: {skip: "internal command"}, _shardsvrCollMod: {skip: "internal command"}, _shardsvrCollModParticipant: {skip: "internal command"}, diff --git a/jstests/sharding/safe_secondary_reads_drop_recreate.js b/jstests/sharding/safe_secondary_reads_drop_recreate.js index e1421c39016..131f98ef7a5 100644 --- a/jstests/sharding/safe_secondary_reads_drop_recreate.js +++ b/jstests/sharding/safe_secondary_reads_drop_recreate.js @@ -97,6 +97,7 @@ let testCases = { _shardsvrMovePrimaryEnterCriticalSection: {skip: "primary only"}, _shardsvrMovePrimaryExitCriticalSection: {skip: "primary only"}, _shardsvrMoveRange: {skip: "primary only"}, + _shardsvrSplitChunk: {skip: "primary only"}, _flushShardRegistry: {skip: "internal command"}, _recvChunkAbort: {skip: "primary only"}, _recvChunkCommit: {skip: "primary only"}, @@ -391,7 +392,6 @@ let testCases = { shutdown: {skip: "does not return user data"}, sleep: {skip: "does not return user data"}, split: {skip: "primary only"}, - splitChunk: {skip: "primary only"}, splitVector: {skip: "primary only"}, startRecordingTraffic: {skip: "Renamed to startTrafficRecording"}, stopRecordingTraffic: {skip: "Renamed to stopTrafficRecording"}, diff --git a/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js b/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js index 7e31ad10533..3d29d1e6d05 100644 --- a/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js +++ b/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js @@ -113,6 +113,7 @@ let testCases = { _shardsvrMovePrimaryEnterCriticalSection: {skip: "primary only"}, _shardsvrMovePrimaryExitCriticalSection: {skip: "primary only"}, _shardsvrMoveRange: {skip: "primary only"}, + _shardsvrSplitChunk: {skip: "primary only"}, _flushShardRegistry: {skip: "internal command"}, _recvChunkAbort: {skip: "primary only"}, _recvChunkCommit: {skip: "primary only"}, @@ -492,7 +493,6 @@ let testCases = { shutdown: {skip: "does not return user data"}, sleep: {skip: "does not return user data"}, split: {skip: "primary only"}, - splitChunk: {skip: "primary only"}, splitVector: {skip: "primary only"}, startRecordingTraffic: {skip: "Renamed to startTrafficRecording"}, stopRecordingTraffic: {skip: "Renamed to stopTrafficRecording"}, diff --git a/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js b/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js index 876aa7336d9..97c7995c175 100644 --- a/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js +++ b/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js @@ -104,6 +104,7 @@ let testCases = { _shardsvrMovePrimaryEnterCriticalSection: {skip: "primary only"}, _shardsvrMovePrimaryExitCriticalSection: {skip: "primary only"}, _shardsvrMoveRange: {skip: "primary only"}, + _shardsvrSplitChunk: {skip: "primary only"}, _flushShardRegistry: {skip: "internal command"}, _recvChunkAbort: {skip: "primary only"}, _recvChunkCommit: {skip: "primary only"}, @@ -407,7 +408,6 @@ let testCases = { shutdown: {skip: "does not return user data"}, sleep: {skip: "does not return user data"}, split: {skip: "primary only"}, - splitChunk: {skip: "primary only"}, splitVector: {skip: "primary only"}, startRecordingTraffic: {skip: "Renamed to startTrafficRecording"}, stopRecordingTraffic: {skip: "Renamed to stopTrafficRecording"}, diff --git a/src/mongo/db/global_catalog/ddl/BUILD.bazel b/src/mongo/db/global_catalog/ddl/BUILD.bazel index aff90154862..1c0de2e6749 100644 --- a/src/mongo/db/global_catalog/ddl/BUILD.bazel +++ b/src/mongo/db/global_catalog/ddl/BUILD.bazel @@ -590,6 +590,16 @@ idl_generator( ], ) +idl_generator( + name = "split_chunk_coordinator_document_gen", + src = "split_chunk_coordinator_document.idl", + deps = [ + "//src/mongo/db:basic_types_gen", + "//src/mongo/db/global_catalog/ddl:sharded_ddl_commands_gen", + "//src/mongo/db/global_catalog/ddl:sharding_coordinator_gen", + ], +) + idl_generator( name = "test_chunk_operation_sharding_coordinator_document_gen", src = "test_chunk_operation_sharding_coordinator_document.idl", diff --git a/src/mongo/db/global_catalog/ddl/chunk_operation_sharding_coordinator_test.cpp b/src/mongo/db/global_catalog/ddl/chunk_operation_sharding_coordinator_test.cpp index b31f22673f2..198b7f3880a 100644 --- a/src/mongo/db/global_catalog/ddl/chunk_operation_sharding_coordinator_test.cpp +++ b/src/mongo/db/global_catalog/ddl/chunk_operation_sharding_coordinator_test.cpp @@ -31,6 +31,7 @@ #include "mongo/db/global_catalog/ddl/merge_chunks_coordinator.h" #include "mongo/db/global_catalog/ddl/sharding_coordinator_external_state_for_test.h" +#include "mongo/db/global_catalog/ddl/split_chunk_coordinator.h" #include "mongo/db/global_catalog/ddl/test_chunk_operation_sharding_coordinator_document_gen.h" #include "mongo/db/repl/primary_only_service_test_fixture.h" #include "mongo/db/shard_role/lock_manager/locker.h" @@ -121,6 +122,26 @@ protected: return doc; } + SplitChunkCoordinatorDocument makeSplitChunkCoordinatorDoc(std::vector splitKeys, + OID epoch) { + SplitChunkCoordinatorDocument doc; + ShardsvrSplitChunkRequest req; + req.setKeyPattern(BSON("x" << 1)); + req.setMin(BSON("x" << 0)); + req.setMax(BSON("x" << 100)); + req.setSplitKeys(std::move(splitKeys)); + req.setFrom("shard0000"); + req.setEpoch(epoch); + ShardingCoordinatorMetadata metadata{ + {NamespaceString::createNamespaceString_forTest("test.split"), + CoordinatorTypeEnum::kSplitChunk}}; + ForwardableOperationMetadata forwardableOpMetadata(_opCtx); + metadata.setForwardableOpMetadata(forwardableOpMetadata); + doc.setShardingCoordinatorMetadata(std::move(metadata)); + doc.setShardsvrSplitChunkRequest(req); + return doc; + } + class TestChunkOperationShardingCoordinator : public ChunkOperationShardingCoordinator { public: @@ -192,6 +213,22 @@ TEST_F(ChunkOperationShardingCoordinatorTest, MergeChunksCheckIfOptionsConflictS ->interrupt({ErrorCodes::Interrupted, "Test cleanup"}); } +TEST_F(ChunkOperationShardingCoordinatorTest, SplitChunkCheckIfOptionsConflictSameParams) { + auto epoch = OID::gen(); + std::vector splitKeys = {BSON("x" << 50)}; + auto coordinatorDoc = makeSplitChunkCoordinatorDoc(splitKeys, epoch); + + auto coordinator = std::make_shared( + static_cast(_service), coordinatorDoc.toBSON()); + + // Same parameters — should not throw. + ASSERT_DOES_NOT_THROW(coordinator->checkIfOptionsConflict(coordinatorDoc.toBSON())); + + // Satisfy destructor invariants by resolving internal promises. + static_cast(coordinator.get()) + ->interrupt({ErrorCodes::Interrupted, "Test cleanup"}); +} + TEST_F(ChunkOperationShardingCoordinatorTest, MergeChunksCheckIfOptionsConflictDifferentBounds) { auto epoch = OID::gen(); std::vector bounds = {BSON("a" << 1), BSON("a" << 10)}; @@ -212,6 +249,26 @@ TEST_F(ChunkOperationShardingCoordinatorTest, MergeChunksCheckIfOptionsConflictD ->interrupt({ErrorCodes::Interrupted, "Test cleanup"}); } +TEST_F(ChunkOperationShardingCoordinatorTest, SplitChunkCheckIfOptionsConflictDifferentSplitKeys) { + auto epoch = OID::gen(); + std::vector splitKeys = {BSON("x" << 50)}; + auto coordinatorDoc = makeSplitChunkCoordinatorDoc(splitKeys, epoch); + + auto coordinator = std::make_shared( + static_cast(_service), coordinatorDoc.toBSON()); + + // Different split keys — should throw. + std::vector differentSplitKeys = {BSON("x" << 60)}; + auto otherDoc = makeSplitChunkCoordinatorDoc(differentSplitKeys, epoch); + + ASSERT_THROWS_CODE(coordinator->checkIfOptionsConflict(otherDoc.toBSON()), + DBException, + ErrorCodes::ConflictingOperationInProgress); + + static_cast(coordinator.get()) + ->interrupt({ErrorCodes::Interrupted, "Test cleanup"}); +} + TEST_F(ChunkOperationShardingCoordinatorTest, MergeChunksCheckIfOptionsConflictDifferentEpoch) { auto epoch = OID::gen(); std::vector bounds = {BSON("a" << 1), BSON("a" << 10)}; @@ -232,4 +289,24 @@ TEST_F(ChunkOperationShardingCoordinatorTest, MergeChunksCheckIfOptionsConflictD ->interrupt({ErrorCodes::Interrupted, "Test cleanup"}); } +TEST_F(ChunkOperationShardingCoordinatorTest, SplitChunkAppendCommandInfoIncludesRequestFields) { + auto epoch = OID::gen(); + std::vector splitKeys = {BSON("x" << 50), BSON("x" << 75)}; + auto coordinatorDoc = makeSplitChunkCoordinatorDoc(splitKeys, epoch); + + auto coordinator = std::make_shared( + static_cast(_service), coordinatorDoc.toBSON()); + + BSONObjBuilder cmdInfoBuilder; + coordinator->appendCommandInfo(&cmdInfoBuilder); + auto cmdInfo = cmdInfoBuilder.obj(); + ASSERT_BSONOBJ_EQ(cmdInfo.getObjectField("min"), BSON("x" << 0)); + ASSERT_BSONOBJ_EQ(cmdInfo.getObjectField("max"), BSON("x" << 100)); + ASSERT_EQ(cmdInfo.getStringField("from"), "shard0000"); + ASSERT_EQ(cmdInfo.getField("splitKeys").Array().size(), 2u); + + static_cast(coordinator.get()) + ->interrupt({ErrorCodes::Interrupted, "Test cleanup"}); +} + } // namespace mongo diff --git a/src/mongo/db/global_catalog/ddl/shard_util.cpp b/src/mongo/db/global_catalog/ddl/shard_util.cpp index 1e0f9a1169e..16072e2032b 100644 --- a/src/mongo/db/global_catalog/ddl/shard_util.cpp +++ b/src/mongo/db/global_catalog/ddl/shard_util.cpp @@ -41,6 +41,7 @@ #include "mongo/bson/simple_bsonobj_comparator.h" #include "mongo/bson/util/bson_extract.h" #include "mongo/client/read_preference.h" +#include "mongo/db/global_catalog/ddl/sharded_ddl_commands_gen.h" #include "mongo/db/global_catalog/shard_key_pattern.h" #include "mongo/db/namespace_string.h" #include "mongo/db/namespace_string_util.h" @@ -182,18 +183,17 @@ Status splitChunkAtMultiplePoints(OperationContext* opCtx, return {ErrorCodes::CannotSplit, msg}; } - BSONObjBuilder cmd; - cmd.append("splitChunk", - NamespaceStringUtil::serialize(nss, SerializationContext::stateDefault())); - cmd.append("from", shardId.toString()); - cmd.append("keyPattern", shardKeyPattern.toBSON()); - cmd.append("epoch", epoch); - cmd.append("timestamp", timestamp); + ShardsvrSplitChunk req(nss); + req.setDbName(DatabaseName::kAdmin); + req.setKeyPattern(shardKeyPattern.toBSON()); + req.setMin(chunkRange.getMin()); + req.setMax(chunkRange.getMax()); + req.setSplitKeys({splitPointsBeginIt, splitPointsEndIt}); + req.setFrom(shardId.toString()); + req.setEpoch(epoch); + req.setTimestamp(timestamp); - chunkRange.serialize(&cmd); - cmd.append("splitKeys", splitPointsBeginIt, splitPointsEndIt); - - BSONObj cmdObj = cmd.obj(); + BSONObj cmdObj = req.toBSON(); Status status{ErrorCodes::InternalError, "Uninitialized value"}; BSONObj cmdResponse; diff --git a/src/mongo/db/global_catalog/ddl/sharded_ddl_commands.idl b/src/mongo/db/global_catalog/ddl/sharded_ddl_commands.idl index 8cad3683b03..b084a10eff6 100644 --- a/src/mongo/db/global_catalog/ddl/sharded_ddl_commands.idl +++ b/src/mongo/db/global_catalog/ddl/sharded_ddl_commands.idl @@ -500,6 +500,33 @@ structs: type: safeInt64 description: "Maximum number of documents in the capped collection" + ShardsvrSplitChunkRequest: + description: "_shardsvrSplitChunk command parameters" + strict: false + fields: + keyPattern: + type: object_owned + description: "The shard key pattern for the collection." + min: + type: object_owned + description: "The min bound of the chunk range to split." + max: + type: object_owned + description: "The max bound of the chunk range to split." + splitKeys: + type: array + description: "The split points for the chunk." + from: + type: string + description: "The shard name." + epoch: + type: objectid + description: "The expected collection epoch." + timestamp: + type: timestamp + optional: true + description: "The expected collection timestamp." + commands: _shardsvrCreateCollection: command_name: _shardsvrCreateCollection @@ -896,6 +923,18 @@ commands: chained_structs: ShardsvrConvertToCappedRequest: ShardsvrConvertToCappedRequest + _shardsvrSplitChunk: + command_name: _shardsvrSplitChunk + command_alias: splitChunk + cpp_name: ShardsvrSplitChunk + description: "The internal split command for a shard." + strict: false + namespace: type + type: namespacestring + api_version: "" + chained_structs: + ShardsvrSplitChunkRequest: ShardsvrSplitChunkRequest + # TODO (SERVER-116499): Remove this command once 9.0 becomes last LTS. _shardsvrUpgradeDowngradeViewlessTimeseries: command_name: _shardsvrUpgradeDowngradeViewlessTimeseries diff --git a/src/mongo/db/global_catalog/ddl/sharding_coordinator.idl b/src/mongo/db/global_catalog/ddl/sharding_coordinator.idl index 624a6591520..c61eb8e8943 100644 --- a/src/mongo/db/global_catalog/ddl/sharding_coordinator.idl +++ b/src/mongo/db/global_catalog/ddl/sharding_coordinator.idl @@ -68,6 +68,7 @@ enums: # TODO (SERVER-116499): Remove this coordinator type once 9.0 becomes last LTS. kTimeseriesUpgradeDowngrade: "upgradeDowngradeViewlessTimeseries" kMergeChunks: "mergeChunks" + kSplitChunk: "splitChunk" kTestCoordinator: "testCoordinator" # TODO (SERVER-98118): Remove this enum once v9.0 become last-lts. diff --git a/src/mongo/db/global_catalog/ddl/sharding_coordinator_service.cpp b/src/mongo/db/global_catalog/ddl/sharding_coordinator_service.cpp index 6695674da1b..2a20ba06b51 100644 --- a/src/mongo/db/global_catalog/ddl/sharding_coordinator_service.cpp +++ b/src/mongo/db/global_catalog/ddl/sharding_coordinator_service.cpp @@ -56,6 +56,7 @@ #include "mongo/db/global_catalog/ddl/rename_collection_coordinator.h" #include "mongo/db/global_catalog/ddl/set_allow_migrations_coordinator.h" #include "mongo/db/global_catalog/ddl/sharding_coordinator.h" +#include "mongo/db/global_catalog/ddl/split_chunk_coordinator.h" #include "mongo/db/global_catalog/ddl/timeseries_upgrade_downgrade_coordinator.h" #include "mongo/db/global_catalog/ddl/untrack_unsplittable_collection_coordinator.h" #include "mongo/db/pipeline/aggregate_command_gen.h" @@ -133,6 +134,7 @@ constexpr std::pair}, {CoordinatorTypeEnum::kMergeChunks, typedInstance}, + {CoordinatorTypeEnum::kSplitChunk, typedInstance}, {CoordinatorTypeEnum::kTestCoordinator, noInstance}, }; diff --git a/src/mongo/db/global_catalog/ddl/shardsvr_split_chunk_command.cpp b/src/mongo/db/global_catalog/ddl/shardsvr_split_chunk_command.cpp index 3f56e2c304f..8327d595aa5 100644 --- a/src/mongo/db/global_catalog/ddl/shardsvr_split_chunk_command.cpp +++ b/src/mongo/db/global_catalog/ddl/shardsvr_split_chunk_command.cpp @@ -27,44 +27,28 @@ * it in the license file. */ - -#include "mongo/base/error_codes.h" -#include "mongo/base/status.h" -#include "mongo/base/string_data.h" -#include "mongo/bson/bsonelement.h" -#include "mongo/bson/bsonmisc.h" -#include "mongo/bson/bsonobj.h" -#include "mongo/bson/bsonobjbuilder.h" -#include "mongo/bson/bsontypes.h" -#include "mongo/bson/oid.h" -#include "mongo/bson/timestamp.h" -#include "mongo/bson/util/bson_extract.h" #include "mongo/db/auth/action_type.h" #include "mongo/db/auth/authorization_session.h" #include "mongo/db/auth/resource_pattern.h" #include "mongo/db/commands.h" -#include "mongo/db/database_name.h" -#include "mongo/db/database_name_util.h" +#include "mongo/db/commands/feature_compatibility_version.h" +#include "mongo/db/global_catalog/ddl/sharded_ddl_commands_gen.h" #include "mongo/db/global_catalog/ddl/split_chunk.h" +#include "mongo/db/global_catalog/ddl/split_chunk_coordinator.h" #include "mongo/db/global_catalog/type_chunk.h" -#include "mongo/db/namespace_string.h" -#include "mongo/db/namespace_string_util.h" -#include "mongo/db/operation_context.h" +#include "mongo/db/s/active_migrations_registry.h" #include "mongo/db/s/chunk_operation_precondition_checks.h" #include "mongo/db/service_context.h" -#include "mongo/db/shard_role/shard_catalog/operation_sharding_state.h" #include "mongo/db/shard_role/shard_catalog/shard_filtering_metadata_refresh.h" -#include "mongo/db/tenant_id.h" +#include "mongo/db/sharding_environment/sharding_feature_flags_gen.h" +#include "mongo/db/sharding_environment/sharding_runtime_d_params_gen.h" #include "mongo/db/topology/sharding_state.h" -#include "mongo/db/versioning_protocol/chunk_version.h" +#include "mongo/db/version_context.h" #include "mongo/logv2/log.h" -#include "mongo/util/assert_util.h" #include -#include #include -#include #include #include @@ -74,19 +58,81 @@ namespace mongo { namespace { -class SplitChunkCommand : public ErrmsgCommandDeprecated { +/** + * Attempts to execute the split through the sharding coordinator service, retrying while a + * conflicting coordinator is already running for the same namespace. Returns true if the split + * completed via the coordinator; returns false if the caller should fall back to the legacy + * config-server path (because the authoritative metadata feature flag is disabled). Throws + * ConflictingOperationInProgress if the configured retry budget is exhausted. + */ +bool tryRunSplitChunkCoordinator(OperationContext* opCtx, + const NamespaceString& nss, + const ShardsvrSplitChunk& req) { + // If a conflicting split coordinator is already running for this namespace, + // wait for it to complete and retry. + // TODO (SERVER-125033): Remove the retry-loop once this task gets done. + const int maxConflictRetries = shardsvrSplitChunkMaxConflictRetries.load(); + Status lastConflictStatus = Status::OK(); + for (int retries = 0; retries < maxConflictRetries; ++retries) { + boost::optional optFixedFcvRegion{boost::in_place_init, opCtx}; + + if (!feature_flags::gShardAuthoritativeCollMetadata.isEnabled( + VersionContext::getDecoration(opCtx), + optFixedFcvRegion.get()->acquireFCVSnapshot())) { + return false; + } + + auto coordinatorDoc = SplitChunkCoordinatorDocument(); + coordinatorDoc.setShardsvrSplitChunkRequest(req.getShardsvrSplitChunkRequest()); + coordinatorDoc.setShardingCoordinatorMetadata({{nss, CoordinatorTypeEnum::kSplitChunk}}); + + // Defer option conflict checking to the explicit checkIfOptionsConflict + // call below, allowing the retry loop to handle ConflictingOperationInProgress. + auto service = ShardingCoordinatorService::getService(opCtx); + auto coordinator = checked_pointer_cast(service->getOrCreateInstance( + opCtx, coordinatorDoc.toBSON(), *optFixedFcvRegion, false /*checkOptions*/)); + + try { + coordinator->checkIfOptionsConflict(coordinatorDoc.toBSON()); + } catch (const ExceptionFor& ex) { + LOGV2_DEBUG(12117801, + 1, + "Split chunk coordinator already running, waiting for completion", + "namespace"_attr = nss, + "error"_attr = ex); + lastConflictStatus = ex.toStatus(); + optFixedFcvRegion.reset(); + coordinator->getCompletionFuture().getNoThrow(opCtx).ignore(); + continue; + } + + optFixedFcvRegion.reset(); + coordinator->getCompletionFuture().get(opCtx); + return true; + } + + uasserted(ErrorCodes::ConflictingOperationInProgress, + str::stream() << "Failed to execute split chunk for namespace " + << nss.toStringForErrorMsg() << " after " << maxConflictRetries + << " retries due to conflicting operations. Last conflict: " + << lastConflictStatus.reason()); +} + +class ShardsvrSplitChunkCommand final : public TypedCommand { public: - SplitChunkCommand() : ErrmsgCommandDeprecated("splitChunk", "_shardsvrSplitChunk") {} + using Request = ShardsvrSplitChunk; + + ShardsvrSplitChunkCommand() : TypedCommand(Request::kCommandName, Request::kCommandAlias) {} std::string help() const override { return "internal command usage only\n" "example:\n" - " { splitChunk:\"db.foo\" , keyPattern: {a:1} , min : {a:100} , max: {a:200} { " - "splitKeys : [ {a:150} , ... ]}"; + " { _shardsvrSplitChunk: \"db.foo\", keyPattern: {a:1}, min: {a:100}," + " max: {a:200}, splitKeys: [{a:150}] }"; } - bool supportsWriteConcern(const BSONObj& cmd) const override { - return false; + bool skipApiVersionCheck() const override { + return true; } AllowedOnSecondary secondaryAllowed(ServiceContext*) const override { @@ -97,107 +143,70 @@ public: return true; } - Status checkAuthForOperation(OperationContext* opCtx, - const DatabaseName& dbName, - const BSONObj&) const override { - if (!AuthorizationSession::get(opCtx->getClient()) - ->isAuthorizedForActionsOnResource( - ResourcePattern::forClusterResource(dbName.tenantId()), - ActionType::internal)) { - return Status(ErrorCodes::Unauthorized, "Unauthorized"); - } - return Status::OK(); - } + class Invocation final : public InvocationBase { + public: + using InvocationBase::InvocationBase; - NamespaceString parseNs(const DatabaseName& dbName, const BSONObj& cmdObj) const override { - return NamespaceStringUtil::deserialize(dbName.tenantId(), - CommandHelpers::parseNsFullyQualified(cmdObj), - SerializationContext::stateDefault()); - } + void typedRun(OperationContext* opCtx) { + ShardingState::get(opCtx)->assertCanAcceptShardedCommands(); - bool errmsgRun(OperationContext* opCtx, - const DatabaseName& dbName, - const BSONObj& cmdObj, - std::string& errmsg, - BSONObjBuilder& result) override { - ShardingState::get(opCtx)->assertCanAcceptShardedCommands(); + const auto& nss = ns(); + const auto& req = request(); - const NamespaceString nss(parseNs(dbName, cmdObj)); - - // Check whether parameters passed to splitChunk are sound - BSONObj keyPatternObj; - { - BSONElement keyPatternElem; - auto keyPatternStatus = - bsonExtractTypedField(cmdObj, "keyPattern", BSONType::object, &keyPatternElem); - - if (!keyPatternStatus.isOK()) { - errmsg = "need to specify the key pattern the collection is sharded over"; - return false; - } - keyPatternObj = keyPatternElem.Obj(); - } - - auto chunkRange = ChunkRange::fromBSON(cmdObj); - - std::string shardName; - auto parseShardNameStatus = bsonExtractStringField(cmdObj, "from", &shardName); - uassertStatusOK(parseShardNameStatus); - - LOGV2(22104, "Received splitChunk request", "request"_attr = redact(cmdObj)); - - std::vector splitKeys; - { - BSONElement splitKeysElem; - auto splitKeysElemStatus = - bsonExtractTypedField(cmdObj, "splitKeys", BSONType::array, &splitKeysElem); - - if (!splitKeysElemStatus.isOK()) { - errmsg = "need to provide the split points to chunk over"; - return false; + if (tryRunSplitChunkCoordinator(opCtx, nss, req)) { + return; } - BSONObjIterator it(splitKeysElem.Obj()); - while (it.more()) { - splitKeys.push_back(it.next().Obj().getOwned()); + // Legacy path: precondition checks + splitChunk(). + auto chunkRange = ChunkRange(req.getMin(), req.getMax()); + uassertStatusOK(ChunkRange::validate(chunkRange)); + + { + uassertStatusOK( + FilteringMetadataCache::get(opCtx)->onCollectionPlacementVersionMismatch( + opCtx, nss, boost::none)); + const auto metadata = + checkCollectionIdentity(opCtx, nss, req.getEpoch(), req.getTimestamp()); + checkShardKeyPattern(opCtx, nss, metadata, chunkRange); + checkChunkMatchesRange(opCtx, nss, metadata, chunkRange); } + + auto scopedChunk = + uassertStatusOK(ActiveMigrationsRegistry::get(opCtx).registerSplitOrMergeChunk( + opCtx, nss, chunkRange)); + + uassertStatusOK(splitChunk( + opCtx, + nss, + req.getKeyPattern(), + chunkRange, + std::vector(req.getSplitKeys().begin(), req.getSplitKeys().end()), + std::string{req.getFrom()}, + req.getEpoch(), + req.getTimestamp(), + scopedChunk)); } - OID expectedCollectionEpoch; - uassertStatusOK(bsonExtractOIDField(cmdObj, "epoch", &expectedCollectionEpoch)); - - boost::optional expectedCollectionTimestamp; - if (cmdObj["timestamp"]) { - expectedCollectionTimestamp.emplace(); - uassertStatusOK(bsonExtractTimestampField( - cmdObj, "timestamp", expectedCollectionTimestamp.get_ptr())); + private: + NamespaceString ns() const override { + return request().getCommandParameter(); } - // Check that the preconditions for split chunk are met and throw StaleShardVersion - // otherwise. - { - uassertStatusOK( - FilteringMetadataCache::get(opCtx)->onCollectionPlacementVersionMismatch( - opCtx, nss, boost::none)); - const auto metadata = checkCollectionIdentity( - opCtx, nss, expectedCollectionEpoch, expectedCollectionTimestamp); - checkShardKeyPattern(opCtx, nss, metadata, chunkRange); - checkChunkMatchesRange(opCtx, nss, metadata, chunkRange); + bool supportsWriteConcern() const override { + return false; } - uassertStatusOK(splitChunk(opCtx, - nss, - keyPatternObj, - chunkRange, - std::move(splitKeys), - shardName, - expectedCollectionEpoch, - expectedCollectionTimestamp)); - - return true; - } + void doCheckAuthorization(OperationContext* opCtx) const override { + uassert(ErrorCodes::Unauthorized, + "Unauthorized", + AuthorizationSession::get(opCtx->getClient()) + ->isAuthorizedForActionsOnResource( + ResourcePattern::forClusterResource(request().getDbName().tenantId()), + ActionType::internal)); + } + }; }; -MONGO_REGISTER_COMMAND(SplitChunkCommand).forShard(); +MONGO_REGISTER_COMMAND(ShardsvrSplitChunkCommand).forShard(); } // namespace } // namespace mongo diff --git a/src/mongo/db/global_catalog/ddl/split_chunk.cpp b/src/mongo/db/global_catalog/ddl/split_chunk.cpp index 2383f14d677..18ab01090ca 100644 --- a/src/mongo/db/global_catalog/ddl/split_chunk.cpp +++ b/src/mongo/db/global_catalog/ddl/split_chunk.cpp @@ -157,10 +157,8 @@ Status splitChunk(OperationContext* opCtx, std::vector&& splitPoints, const std::string& shardName, const OID& expectedCollectionEpoch, - const boost::optional& expectedCollectionTimestamp) { - auto scopedSplitOrMergeChunk(uassertStatusOK( - ActiveMigrationsRegistry::get(opCtx).registerSplitOrMergeChunk(opCtx, nss, chunkRange))); - + const boost::optional& expectedCollectionTimestamp, + const ScopedSplitMergeChunk&) { // If the shard key is hashed, then we must make sure that the split points are of supported // data types. const auto hashedField = ShardKeyPattern::extractHashedField(keyPatternObj); diff --git a/src/mongo/db/global_catalog/ddl/split_chunk.h b/src/mongo/db/global_catalog/ddl/split_chunk.h index 7bdc08d7cff..4ea10a518bf 100644 --- a/src/mongo/db/global_catalog/ddl/split_chunk.h +++ b/src/mongo/db/global_catalog/ddl/split_chunk.h @@ -47,12 +47,17 @@ class BSONObj; class ChunkRange; class NamespaceString; class OperationContext; +class ScopedSplitMergeChunk; /** * Attempts to split a chunk with the specified parameters. If the split fails, then the StatusWith * object returned will contain a Status with an ErrorCode regarding the cause of failure. If the * split succeeds, then the StatusWith object returned will contain Status::Ok(). * Will update the shard's filtering metadata. + * + * The caller must hold the ActiveMigrationsRegistry split/merge lock for this chunk range and pass + * it as scopedSplitMergeChunk to prove exclusive ownership. The lock must remain live for the + * duration of the call. */ Status splitChunk(OperationContext* opCtx, const NamespaceString& nss, @@ -61,6 +66,7 @@ Status splitChunk(OperationContext* opCtx, std::vector&& splitPoints, const std::string& shardName, const OID& expectedCollectionEpoch, - const boost::optional& expectedCollectionTimestamp); + const boost::optional& expectedCollectionTimestamp, + const ScopedSplitMergeChunk& scopedSplitMergeChunk); } // namespace mongo diff --git a/src/mongo/db/global_catalog/ddl/split_chunk_coordinator.cpp b/src/mongo/db/global_catalog/ddl/split_chunk_coordinator.cpp new file mode 100644 index 00000000000..ec1ba6dab5e --- /dev/null +++ b/src/mongo/db/global_catalog/ddl/split_chunk_coordinator.cpp @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/global_catalog/ddl/split_chunk_coordinator.h" + +#include "mongo/bson/simple_bsonobj_comparator.h" +#include "mongo/db/global_catalog/ddl/split_chunk.h" +#include "mongo/db/s/chunk_operation_precondition_checks.h" +#include "mongo/db/shard_role/shard_catalog/shard_filtering_metadata_refresh.h" +#include "mongo/logv2/log.h" +#include "mongo/util/future_util.h" +#include "mongo/util/str.h" + +#include +#include +#include + +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kSharding + +namespace mongo { + +SplitChunkCoordinator::SplitChunkCoordinator(ShardingCoordinatorService* service, + const BSONObj& initialStateDoc) + : ChunkOperationShardingCoordinator(service, "SplitChunkCoordinator", initialStateDoc), + _request(_doc.getShardsvrSplitChunkRequest()) {} + +void SplitChunkCoordinator::checkIfOptionsConflict(const BSONObj& doc) const { + const auto otherDoc = SplitChunkCoordinatorDocument::parse( + doc, IDLParserContext("SplitChunkCoordinatorDocument")); + + const auto& selfReq = _request.toBSON(); + const auto& otherReq = otherDoc.getShardsvrSplitChunkRequest().toBSON(); + + uassert(ErrorCodes::ConflictingOperationInProgress, + str::stream() << "Another split chunk operation for namespace " + << nss().toStringForErrorMsg() + << " is being executed with different parameters: " << redact(selfReq) + << " vs " << redact(otherReq), + SimpleBSONObjComparator::kInstance.evaluate(selfReq == otherReq)); +} + +void SplitChunkCoordinator::appendCommandInfo(BSONObjBuilder* cmdInfoBuilder) const { + cmdInfoBuilder->appendElements(_request.toBSON()); +} + +bool SplitChunkCoordinator::isInCriticalSection(Phase phase) const { + return false; +} + +ExecutorFuture SplitChunkCoordinator::_acquireLocksAsync( + OperationContext* opCtx, + std::shared_ptr executor, + const CancellationToken& token) { + return AsyncTry([this, anchor = shared_from_this()] { + auto opCtxHolder = makeOperationContext(/*deprioritizable=*/true); + auto* opCtx = opCtxHolder.get(); + + auto chunkRange = ChunkRange(_request.getMin(), _request.getMax()); + _scopedSplitMergeChunk.emplace( + uassertStatusOK(ActiveMigrationsRegistry::get(opCtx).registerSplitOrMergeChunk( + opCtx, nss(), chunkRange))); + }) + .until([this, anchor = shared_from_this()](Status status) { + if (!status.isOK()) { + LOGV2_WARNING(12117803, + "ActiveMigrationsRegistry lock acquisition attempt failed", + logv2::DynamicAttributes{getCoordinatorLogAttrs(), + "error"_attr = redact(status)}); + } + return !_recoveredFromDisk || status.isOK(); + }) + .withBackoffBetweenIterations(kExponentialBackoff) + .on(**executor, token); +} + +void SplitChunkCoordinator::_releaseLocks(OperationContext* opCtx) { + _scopedSplitMergeChunk.reset(); +} + +ExecutorFuture SplitChunkCoordinator::_runImpl( + std::shared_ptr executor, + const CancellationToken& token) noexcept { + return ExecutorFuture(**executor).then([this, anchor = shared_from_this()] { + auto opCtxHolder = makeOperationContext(/*deprioritizable=*/true); + auto* opCtx = opCtxHolder.get(); + + const auto& keyPatternObj = _request.getKeyPattern(); + auto chunkRange = ChunkRange(_request.getMin(), _request.getMax()); + uassertStatusOK(ChunkRange::validate(chunkRange)); + const auto& splitKeys = _request.getSplitKeys(); + const std::string shardName{_request.getFrom()}; + const auto& expectedCollectionEpoch = _request.getEpoch(); + const auto& expectedCollectionTimestamp = _request.getTimestamp(); + + LOGV2(12117800, + "Running split chunk operation", + logAttrs(nss()), + "range"_attr = chunkRange.toString()); + + uassert(ErrorCodes::InvalidOptions, + "need to provide the split points to chunk over", + !splitKeys.empty()); + + // Verify placement version, collection identity, shard key pattern and chunk range + // are still valid. + { + uassertStatusOK( + FilteringMetadataCache::get(opCtx)->onCollectionPlacementVersionMismatch( + opCtx, nss(), boost::none)); + const auto metadata = checkCollectionIdentity( + opCtx, nss(), expectedCollectionEpoch, expectedCollectionTimestamp); + checkShardKeyPattern(opCtx, nss(), metadata, chunkRange); + checkChunkMatchesRange(opCtx, nss(), metadata, chunkRange); + } + + uassertStatusOK(splitChunk(opCtx, + nss(), + keyPatternObj, + chunkRange, + std::vector(splitKeys.begin(), splitKeys.end()), + shardName, + expectedCollectionEpoch, + expectedCollectionTimestamp, + *_scopedSplitMergeChunk)); + + LOGV2(12117802, + "Completed split chunk operation", + logAttrs(nss()), + "range"_attr = chunkRange.toString()); + }); +} + +} // namespace mongo diff --git a/src/mongo/db/global_catalog/ddl/split_chunk_coordinator.h b/src/mongo/db/global_catalog/ddl/split_chunk_coordinator.h new file mode 100644 index 00000000000..60e9a44cc27 --- /dev/null +++ b/src/mongo/db/global_catalog/ddl/split_chunk_coordinator.h @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2026-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/db/global_catalog/ddl/chunk_operation_sharding_coordinator.h" +#include "mongo/db/global_catalog/ddl/split_chunk_coordinator_document_gen.h" +#include "mongo/db/s/active_migrations_registry.h" + +namespace mongo { + +class SplitChunkCoordinator final + : public ChunkOperationShardingCoordinator { +public: + SplitChunkCoordinator(ShardingCoordinatorService* service, const BSONObj& initialStateDoc); + + void checkIfOptionsConflict(const BSONObj& doc) const final; + + void appendCommandInfo(BSONObjBuilder* cmdInfoBuilder) const override; + +protected: + bool isInCriticalSection(Phase phase) const override; + +private: + ExecutorFuture _acquireLocksAsync(OperationContext* opCtx, + std::shared_ptr executor, + const CancellationToken& token) override; + + void _releaseLocks(OperationContext* opCtx) override; + + ExecutorFuture _runImpl(std::shared_ptr executor, + const CancellationToken& token) noexcept override; + + const ShardsvrSplitChunkRequest _request; + boost::optional _scopedSplitMergeChunk; +}; + +} // namespace mongo diff --git a/src/mongo/db/global_catalog/ddl/split_chunk_coordinator_document.idl b/src/mongo/db/global_catalog/ddl/split_chunk_coordinator_document.idl new file mode 100644 index 00000000000..9285fafaa7f --- /dev/null +++ b/src/mongo/db/global_catalog/ddl/split_chunk_coordinator_document.idl @@ -0,0 +1,58 @@ +# Copyright (C) 2026-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# . +# +# As a special exception, the copyright holders give permission to link the +# code of portions of this program with the OpenSSL library under certain +# conditions as described in each individual source file and distribute +# linked combinations including the program with the OpenSSL library. You +# must comply with the Server Side Public License in all respects for +# all of the code used other than as permitted herein. If you modify file(s) +# with this exception, you may extend this exception to your version of the +# file(s), but you are not obligated to do so. If you do not wish to do so, +# delete this exception statement from your version. If you delete this +# exception statement from all source files in the program, then also delete +# it in the license file. +# + +# This file defines the format of the state document for the split chunk coordinator. + +global: + mod_visibility: private + cpp_namespace: "mongo" + +imports: + - "mongo/db/basic_types.idl" + - "mongo/db/global_catalog/ddl/sharding_coordinator.idl" + - "mongo/db/global_catalog/ddl/sharded_ddl_commands.idl" + +enums: + SplitChunkCoordinatorPhase: + description: "Phases for the split chunk coordinator." + type: string + values: + kUnset: "unset" + +structs: + SplitChunkCoordinatorDocument: + description: "State document for the split chunk coordinator." + generate_comparison_operators: false + strict: false + chained_structs: + ShardingCoordinatorMetadata: ShardingCoordinatorMetadata + ShardsvrSplitChunkRequest: ShardsvrSplitChunkRequest + fields: + phase: + type: SplitChunkCoordinatorPhase + default: kUnset diff --git a/src/mongo/db/s/BUILD.bazel b/src/mongo/db/s/BUILD.bazel index bd2fcb93503..24a5f4ca73a 100644 --- a/src/mongo/db/s/BUILD.bazel +++ b/src/mongo/db/s/BUILD.bazel @@ -660,6 +660,8 @@ mongo_cc_library( "//src/mongo/db/global_catalog/ddl:shardsvr_untrack_unsplittable_collection_command.cpp", "//src/mongo/db/global_catalog/ddl:shardsvr_upgrade_downgrade_viewless_timeseries_command.cpp", "//src/mongo/db/global_catalog/ddl:shardsvr_validate_shard_key_candidate.cpp", + "//src/mongo/db/global_catalog/ddl:split_chunk_coordinator.cpp", + "//src/mongo/db/global_catalog/ddl:split_chunk_coordinator_document_gen", "//src/mongo/db/global_catalog/ddl:timeseries_upgrade_downgrade_coordinator.cpp", "//src/mongo/db/global_catalog/ddl:timeseries_upgrade_downgrade_coordinator_document_gen", "//src/mongo/db/global_catalog/ddl:untrack_unsplittable_collection_coordinator.cpp", diff --git a/src/mongo/db/sharding_environment/sharding_runtime_d_params.idl b/src/mongo/db/sharding_environment/sharding_runtime_d_params.idl index fd7956a4a41..853c1b1fdb5 100644 --- a/src/mongo/db/sharding_environment/sharding_runtime_d_params.idl +++ b/src/mongo/db/sharding_environment/sharding_runtime_d_params.idl @@ -227,3 +227,16 @@ server_parameters: gt: 0 default: 10 redact: false + + # TODO (SERVER-125033): Remove this parameter once this task gets done. + shardsvrSplitChunkMaxConflictRetries: + description: >- + Maximum number of times _shardsvrSplitChunk will retry when a conflicting split + chunk coordinator is already running for the same namespace. + set_at: [startup, runtime] + cpp_vartype: AtomicWord + cpp_varname: shardsvrSplitChunkMaxConflictRetries + validator: + gt: 0 + default: 10 + redact: false