summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArun Banala <arun.banala@10gen.com>2019-11-26 15:17:39 +0000
committerevergreen <evergreen@mongodb.com>2019-11-26 15:17:39 +0000
commit272c89db8935802eb43535382960dd7fe24326d9 (patch)
tree9099045e2202c285a50819497d73903c35b5bbac
parent51fd301db7a7a253b24b12ab669a4ec123bbfc70 (diff)
downloadmongo-272c89db8935802eb43535382960dd7fe24326d9.tar.gz
SERVER-43914 Permit sharding commands to accept compound hashed shard key specs
-rw-r--r--jstests/sharding/compound_hashed_shard_key_presplitting.js106
-rw-r--r--jstests/sharding/compound_hashed_shard_key_sharding_cmds.js165
-rw-r--r--jstests/sharding/compound_hashed_shard_key_zoning.js276
-rw-r--r--jstests/sharding/refine_collection_shard_key_basic.js27
-rw-r--r--jstests/sharding/shard_collection_basic.js6
-rw-r--r--src/mongo/db/s/config/configsvr_update_zone_key_range_command.cpp10
-rw-r--r--src/mongo/db/s/config/initial_split_policy.cpp19
-rw-r--r--src/mongo/db/s/shardsvr_shard_collection.cpp19
-rw-r--r--src/mongo/db/s/split_chunk.cpp25
-rw-r--r--src/mongo/s/shard_key_pattern.cpp41
-rw-r--r--src/mongo/s/shard_key_pattern.h16
-rw-r--r--src/mongo/s/shard_key_pattern_test.cpp24
12 files changed, 691 insertions, 43 deletions
diff --git a/jstests/sharding/compound_hashed_shard_key_presplitting.js b/jstests/sharding/compound_hashed_shard_key_presplitting.js
new file mode 100644
index 00000000000..ef1b249bd1a
--- /dev/null
+++ b/jstests/sharding/compound_hashed_shard_key_presplitting.js
@@ -0,0 +1,106 @@
+/**
+ * Tests the pre-splitting behaviour of compound hashed shard key, for both the case where the
+ * prefix field is hashed, and where the hashed field is not the prefix.
+ *
+ * @tags: [requires_fcv_44]
+ */
+(function() {
+'use strict';
+const st = new ShardingTest({name: jsTestName(), shards: 2});
+const dbname = "test";
+const mongos = st.s0;
+const db = st.getDB(dbname);
+db.adminCommand({enablesharding: dbname});
+st.ensurePrimaryShard('test', st.shard1.shardName);
+
+/**
+ * Test that 'shardCollection' command works when there is existing data in collection and does not
+ * do pre-splitting.
+ */
+[{a: "hashed", rangeField1: 1, rangeField2: 1}, {rangeField1: 1, a: "hashed", rangeField2: 1}]
+ .forEach(function(shardKey) {
+ db.collWithData.drop();
+ db.collWithData.insert({a: 1});
+ db.collWithData.createIndex(shardKey);
+
+ // Verify that command fails when 'numInitialChunks' is specified.
+ assert.commandFailedWithCode(db.adminCommand({
+ shardcollection: db.collWithData.getFullName(),
+ key: shardKey,
+ numInitialChunks: 500
+ }),
+ ErrorCodes.InvalidOptions);
+
+ assert.commandWorked(
+ db.adminCommand({shardcollection: db.collWithData.getFullName(), key: shardKey}));
+ assert.eq(st.config.chunks.count({ns: db.collWithData.getFullName()}),
+ 1,
+ "sharding non-empty collection should not pre-split");
+ });
+
+/**
+ * Validates that the chunks ranges have all the shard key fields and each shard has expected number
+ * of chunks.
+ */
+function checkValidChunks(coll, shardKey, expectedChunksOnShard0, expectedChunksOnShard1) {
+ const chunks = st.config.chunks.find({"ns": coll.getFullName()}).toArray();
+ let shardCountsMap = {[st.shard0.shardName]: 0, [st.shard1.shardName]: 0};
+ for (let chunk of chunks) {
+ shardCountsMap[chunk.shard]++;
+
+ const assertHasAllShardKeyFields = function(obj) {
+ assert.eq(Object.keys(shardKey).length, Object.keys(obj).length, tojson(obj));
+ for (let key in obj) {
+ assert(key in shardKey, tojson(obj));
+ }
+ };
+ assertHasAllShardKeyFields(chunk.min);
+ assertHasAllShardKeyFields(chunk.max);
+ }
+ assert.eq(expectedChunksOnShard0,
+ shardCountsMap[st.shard0.shardName],
+ 'Count mismatch on shard0: ' + tojson(chunks));
+ assert.eq(expectedChunksOnShard1,
+ shardCountsMap[st.shard1.shardName],
+ 'Count mismatch on shard1: ' + tojson(chunks));
+}
+
+function runTest(shardKey) {
+ // Supported: Hashed sharding + numInitialChunks + empty collection.
+ // Expected: Even chunk distribution.
+ assert.commandWorked(db.hashedCollEmpty.createIndex(shardKey));
+ let coll = db.hashedCollEmpty;
+ assert.commandWorked(mongos.adminCommand(
+ {shardCollection: coll.getFullName(), key: shardKey, numInitialChunks: 6}));
+ checkValidChunks(coll, shardKey, 3, 3);
+
+ // Supported: Hashed sharding + numInitialChunks + non-existent collection.
+ // Expected: Even chunk distribution.
+ coll = db.hashedCollNonExistent;
+ assert.commandWorked(mongos.adminCommand(
+ {shardCollection: coll.getFullName(), key: shardKey, numInitialChunks: 6}));
+ checkValidChunks(coll, shardKey, 3, 3);
+
+ // Default pre-splitting.
+ coll = db.hashedDefaultPreSplit;
+ assert.commandWorked(mongos.adminCommand({shardCollection: coll.getFullName(), key: shardKey}));
+ checkValidChunks(coll, shardKey, 2, 2);
+}
+
+/**
+ * Hashed field is a prefix.
+ */
+runTest({aKey: "hashed", rangeField1: 1, rangeField2: 1});
+
+/**
+ * When hashed field is not prefix.
+ * TODO SERVER-43917: Add tests when pre-splitting is enabled for non-prefixes.
+ */
+db.coll.drop();
+let shardKey = {rangeField1: 1, a: "hashed", rangeField2: 1};
+assert.commandFailedWithCode(
+ db.adminCommand({shardcollection: db.coll.getFullName(), key: shardKey, numInitialChunks: 500}),
+ ErrorCodes.InvalidOptions);
+
+st.stop();
+})(); \ No newline at end of file
diff --git a/jstests/sharding/compound_hashed_shard_key_sharding_cmds.js b/jstests/sharding/compound_hashed_shard_key_sharding_cmds.js
new file mode 100644
index 00000000000..f379f882d6c
--- /dev/null
+++ b/jstests/sharding/compound_hashed_shard_key_sharding_cmds.js
@@ -0,0 +1,165 @@
+/**
+ * Perform tests for moveChunk and splitChunk commands when the shard key is compound hashed.
+ *
+ * @tags: [requires_fcv_44]
+ */
+(function() {
+'use strict';
+
+const st = new ShardingTest({shards: 2, other: {chunkSize: 1}});
+const configDB = st.s0.getDB('config');
+assert.commandWorked(configDB.adminCommand({enableSharding: 'test'}));
+const shard0 = st.shard0.shardName;
+const shard1 = st.shard1.shardName;
+st.ensurePrimaryShard('test', shard0);
+const testDBOnPrimary = st.rs0.getPrimary().getDB('test');
+
+function verifyChunkSplitIntoTwo(namespace, chunk) {
+ assert.eq(0, configDB.chunks.count({ns: namespace, min: chunk.min, max: chunk.max}));
+ assert.eq(1, configDB.chunks.count({ns: namespace, min: chunk.min}));
+ assert.eq(1, configDB.chunks.count({ns: namespace, max: chunk.max}));
+}
+
+const nonHashedFieldValue = 111;
+const hashedFieldValue = convertShardKeyToHashed(nonHashedFieldValue);
+
+/**
+ * Returns an object which has all the shard key fields. The hashed field will have the value
+ * provided for 'valueForHashedField'. We are doing this because, the 'bounds' and 'middle'
+ * parameters of splitChunk/moveChunk commands expect a hashed value for the hashed field, where as
+ * 'find' expect a non-hashed value.
+ */
+function buildObjWithAllShardKeyFields(shardKey, valueForHashedField) {
+ let splitObj = {};
+ for (let key in shardKey) {
+ if (shardKey[key] === "hashed") {
+ splitObj[key] = valueForHashedField;
+ } else {
+ splitObj[key] = 1;
+ }
+ }
+ return splitObj;
+}
+
+function testSplit(shardKey, collName) {
+ const namespace = testDBOnPrimary[collName].getFullName();
+ assert.commandWorked(configDB.adminCommand({shardCollection: namespace, key: shardKey}));
+
+ // Insert data since both 'find' and 'bounds' based split requires the chunk to contain some
+ // documents.
+ const bulk = st.s.getDB("test")[collName].initializeUnorderedBulkOp();
+ for (let x = -1200; x < 1200; x++) {
+ bulk.insert({x: x, y: x, z: x});
+ }
+ assert.commandWorked(bulk.execute());
+
+ // Attempt to split on a value that is not the shard key.
+ assert.commandFailed(configDB.adminCommand({split: namespace, middle: {someField: 100}}));
+ assert.commandFailed(configDB.adminCommand({split: namespace, find: {someField: 100}}));
+ assert.commandFailed(configDB.adminCommand(
+ {split: namespace, bounds: [{someField: MinKey}, {someField: MaxKey}]}));
+
+ let totalChunksBefore = configDB.chunks.count({ns: namespace});
+ const lowestChunk = configDB.chunks.find({ns: namespace}).sort({min: 1}).limit(1).next();
+ assert(lowestChunk);
+ // Split the chunk based on 'bounds' and verify total chunks increased by one.
+ assert.commandWorked(
+ configDB.adminCommand({split: namespace, bounds: [lowestChunk.min, lowestChunk.max]}));
+ assert.eq(++totalChunksBefore, configDB.chunks.count({ns: namespace}));
+
+ // Verify that a single chunk with the previous bounds no longer exists but split into two.
+ verifyChunkSplitIntoTwo(namespace, lowestChunk);
+
+ // Cannot split if 'min' and 'max' doesn't correspond to the same chunk.
+ assert.commandFailed(
+ configDB.adminCommand({split: namespace, bounds: [lowestChunk.min, lowestChunk.max]}));
+
+ const splitObjWithHashedValue = buildObjWithAllShardKeyFields(shardKey, hashedFieldValue);
+
+ // Find the chunk to which 'splitObjWithHashedValue' belongs to.
+ let chunkToBeSplit = configDB.chunks.findOne(
+ {ns: namespace, min: {$lte: splitObjWithHashedValue}, max: {$gt: splitObjWithHashedValue}});
+ assert(chunkToBeSplit);
+
+ // Split the 'chunkToBeSplit' using 'find'. Note that the object specified for 'find' is not a
+ // split point.
+ const splitObj = buildObjWithAllShardKeyFields(shardKey, nonHashedFieldValue);
+ assert.commandWorked(configDB.adminCommand({split: namespace, find: splitObj}));
+ assert.eq(++totalChunksBefore, configDB.chunks.count({ns: namespace}));
+
+ // Verify that a single chunk with the previous bounds no longer exists but split into two.
+ verifyChunkSplitIntoTwo(namespace, chunkToBeSplit);
+ assert.eq(0, configDB.chunks.count({ns: namespace, min: splitObjWithHashedValue}));
+
+ // Get the new chunk in which 'splitObj' belongs.
+ chunkToBeSplit = configDB.chunks.findOne(
+ {ns: namespace, min: {$lte: splitObjWithHashedValue}, max: {$gt: splitObjWithHashedValue}});
+
+ // Use 'splitObj' as the middle point.
+ assert.commandWorked(
+ configDB.adminCommand({split: namespace, middle: splitObjWithHashedValue}));
+ assert.eq(++totalChunksBefore, configDB.chunks.count({ns: namespace}));
+ verifyChunkSplitIntoTwo(namespace, chunkToBeSplit);
+
+ // Cannot split on existing chunk boundary with 'middle'.
+ assert.commandFailed(configDB.adminCommand({split: namespace, middle: chunkToBeSplit.min}));
+
+ st.s.getDB("test")[collName].drop();
+}
+
+testSplit({x: "hashed", y: 1, z: 1}, "compound_hashed_prefix");
+testSplit({_id: "hashed", y: 1, z: 1}, "compound_hashed_prefix_id");
+testSplit({x: 1, y: "hashed", z: 1}, "compound_nonhashed_prefix");
+testSplit({x: 1, _id: "hashed", z: 1}, "compound_nonhashed_prefix_id");
+
+function testMoveChunk(shardKey) {
+ const ns = 'test.fooHashed';
+ assert.commandWorked(st.s0.adminCommand({shardCollection: ns, key: shardKey}));
+
+ // Fetch a chunk from 'shard0'.
+ const aChunk = configDB.chunks.findOne({ns: ns, shard: shard0});
+ assert(aChunk);
+
+ // Error if either of the bounds is not a valid shard key.
+ assert.commandFailedWithCode(
+ st.s0.adminCommand({moveChunk: ns, bounds: [NaN, aChunk.max], to: shard1}), 10065);
+ assert.commandFailedWithCode(
+ st.s0.adminCommand({moveChunk: ns, bounds: [aChunk.min, NaN], to: shard1}), 10065);
+
+ assert.commandWorked(
+ st.s0.adminCommand({moveChunk: ns, bounds: [aChunk.min, aChunk.max], to: shard1}));
+
+ assert.eq(0, configDB.chunks.count({_id: aChunk._id, shard: shard0}));
+ assert.eq(1, configDB.chunks.count({_id: aChunk._id, shard: shard1}));
+
+ // Fail if 'find' doesn't have full shard key.
+ assert.commandFailed(st.s0.adminCommand({moveChunk: ns, find: {someField: 0}, to: shard1}));
+
+ // Find the chunk to which 'moveObjWithHashedValue' belongs to.
+ const moveObjWithHashedValue = buildObjWithAllShardKeyFields(shardKey, hashedFieldValue);
+ const chunk = st.config.chunks.findOne(
+ {ns: ns, min: {$lte: moveObjWithHashedValue}, max: {$gt: moveObjWithHashedValue}});
+ assert(chunk);
+
+ // Verify that 'moveChunk' with 'find' works with pre-hashed value.
+ const otherShard = (chunk.shard === shard1) ? shard0 : shard1;
+ const moveObj = buildObjWithAllShardKeyFields(shardKey, nonHashedFieldValue);
+ assert.commandWorked(st.s.adminCommand({moveChunk: ns, find: moveObj, to: otherShard}));
+ assert.eq(st.config.chunks.count({ns: ns, min: chunk.min, shard: otherShard}), 1);
+
+ // Fail if 'find' and 'bounds' are both set.
+ assert.commandFailed(st.s0.adminCommand({
+ moveChunk: ns,
+ find: moveObjWithHashedValue,
+ bounds: [aChunk.min, aChunk.max],
+ to: shard1
+ }));
+
+ st.s.getDB("test").fooHashed.drop();
+}
+
+testMoveChunk({_id: "hashed", b: 1, c: 1});
+testMoveChunk({_id: 1, "b.c.d": "hashed", c: 1});
+
+st.stop();
+})();
diff --git a/jstests/sharding/compound_hashed_shard_key_zoning.js b/jstests/sharding/compound_hashed_shard_key_zoning.js
new file mode 100644
index 00000000000..c2efbb77d56
--- /dev/null
+++ b/jstests/sharding/compound_hashed_shard_key_zoning.js
@@ -0,0 +1,276 @@
+/**
+ * Test that updateZoneKeyRange command works correctly in combination with shardCollection command.
+ * In this test we verify the behaviour of:
+ * - Creating zones after sharding the collection.
+ * - Creating zones before sharding the collection.
+ * - Creating zones in collection which has data and then sharding the collection.
+ *
+ * @tags: [requires_fcv_44]
+ */
+(function() {
+'use strict';
+
+const st = new ShardingTest({shards: 3});
+const kDbName = 'test';
+const kCollName = 'foo';
+const ns = kDbName + '.' + kCollName;
+const zoneName = 'zoneName';
+const mongos = st.s0;
+const testDB = mongos.getDB(kDbName);
+const configDB = mongos.getDB('config');
+const shardName = st.shard0.shardName;
+assert.commandWorked(mongos.adminCommand({enableSharding: kDbName}));
+assert.commandWorked(st.s.adminCommand({addShardToZone: shardName, zone: 'zoneName'}));
+
+function fillMissingShardKeyFields(shardKey, doc, value) {
+ for (let key in shardKey) {
+ if (!(key in doc)) {
+ doc[key] = value ? value : {"$minKey": 1};
+ }
+ }
+ return doc;
+}
+/**
+ * Test that 'updateZoneKeyRange' works correctly by verifying 'tags' collection, after sharding the
+ * collection.
+ */
+function testZoningAfterSharding(namespace, shardKey) {
+ assert.commandWorked(st.s.adminCommand({shardCollection: namespace, key: shardKey}));
+
+ // Testing basic assign.
+ assert.commandWorked(st.s.adminCommand(
+ {updateZoneKeyRange: namespace, min: {x: 0}, max: {x: 10}, zone: 'zoneName'}));
+
+ let tagDoc = configDB.tags.findOne();
+
+ assert.eq(namespace, tagDoc.ns);
+ assert.eq(fillMissingShardKeyFields(shardKey, {x: 0}), tagDoc.min);
+ assert.eq(fillMissingShardKeyFields(shardKey, {x: 10}), tagDoc.max);
+ assert.eq('zoneName', tagDoc.tag);
+
+ // Cannot assign overlapping ranges
+ assert.commandFailedWithCode(
+ st.s.adminCommand(
+ {updateZoneKeyRange: namespace, min: {x: -10}, max: {x: 20}, zone: 'zoneName'}),
+ ErrorCodes.RangeOverlapConflict);
+
+ // Cannot have non-shard key fields in tag range.
+ assert.commandFailedWithCode(st.s.adminCommand({
+ updateZoneKeyRange: namespace,
+ min: {newField: -10},
+ max: {newField: 20},
+ zone: 'zoneName'
+ }),
+ ErrorCodes.ShardKeyNotFound);
+
+ tagDoc = configDB.tags.findOne();
+ assert.eq(namespace, tagDoc.ns);
+ assert.eq(fillMissingShardKeyFields(shardKey, {x: 0}), tagDoc.min);
+ assert.eq(fillMissingShardKeyFields(shardKey, {x: 10}), tagDoc.max);
+ assert.eq('zoneName', tagDoc.tag);
+
+ // Testing basic remove.
+ const res = assert.commandWorked(st.s.adminCommand({
+ updateZoneKeyRange: namespace,
+ min: fillMissingShardKeyFields(shardKey, {x: 0}, MinKey),
+ max: fillMissingShardKeyFields(shardKey, {x: 10}, MinKey),
+ zone: null
+ }));
+ assert.eq(null, configDB.tags.findOne());
+}
+
+testZoningAfterSharding("test.compound_hashed", {x: 1, y: "hashed", z: 1});
+testZoningAfterSharding("test.compound_hashed_prefix", {x: "hashed", y: 1, z: 1});
+
+/**
+ * Test that shardCollection correctly validates shard key against existing zones.
+ */
+function testZoningBeforeSharding({shardKey, zoneRange, failCode}) {
+ assert.commandWorked(testDB.foo.createIndex(shardKey));
+ assert.commandWorked(st.s.adminCommand({addShardToZone: shardName, zone: zoneName}));
+
+ // Update zone range and verify that the 'tags' collection is updated appropriately.
+ assert.commandWorked(st.s.adminCommand(
+ {updateZoneKeyRange: ns, min: zoneRange[0], max: zoneRange[1], zone: zoneName}));
+ assert.eq(1, configDB.tags.count({ns: ns, min: zoneRange[0], max: zoneRange[1]}));
+
+ if (failCode) {
+ assert.commandFailedWithCode(mongos.adminCommand({shardCollection: ns, key: shardKey}),
+ failCode);
+ } else {
+ assert.commandWorked(mongos.adminCommand({shardCollection: ns, key: shardKey}));
+ }
+ assert.commandWorked(testDB.runCommand({drop: kCollName}));
+}
+
+// Fails when hashed field is not number long in 'zoneRange'.
+testZoningBeforeSharding(
+ {shardKey: {x: "hashed"}, zoneRange: [{x: -5}, {x: 5}], failCode: ErrorCodes.InvalidOptions});
+testZoningBeforeSharding({
+ shardKey: {x: "hashed"},
+ zoneRange: [{x: NumberLong(-5)}, {x: 5}],
+ failCode: ErrorCodes.InvalidOptions
+});
+testZoningBeforeSharding({
+ shardKey: {x: "hashed"},
+ zoneRange: [{x: -5}, {x: NumberLong(5)}],
+ failCode: ErrorCodes.InvalidOptions
+});
+testZoningBeforeSharding(
+ {shardKey: {x: "hashed"}, zoneRange: [{x: NumberLong(-5)}, {x: NumberLong(5)}]});
+testZoningBeforeSharding({
+ shardKey: {x: "hashed", y: 1},
+ zoneRange: [{x: NumberLong(-5), y: MinKey}, {x: NumberLong(5), y: MinKey}]
+});
+testZoningBeforeSharding({
+ shardKey: {x: 1, y: "hashed"},
+ zoneRange: [{x: 1, y: NumberLong(-5)}, {x: 2, y: NumberLong(5)}]
+});
+testZoningBeforeSharding({
+ shardKey: {x: 1, y: "hashed"},
+ zoneRange: [{x: 1, y: NumberLong(-5)}, {x: 2, y: 5}],
+ failCode: ErrorCodes.InvalidOptions
+});
+
+// Fails when 'zoneRange' doesn't have a shard key field.
+testZoningBeforeSharding({
+ shardKey: {x: 1, y: "hashed", z: 1},
+ zoneRange: [{x: 1, y: NumberLong(-5)}, {x: 2, y: NumberLong(5)}],
+ failCode: ErrorCodes.InvalidOptions
+});
+
+// Works when shard key field is defined as 'MinKey'.
+testZoningBeforeSharding({
+ shardKey: {x: 1, y: "hashed", z: 1},
+ zoneRange: [{x: 1, y: NumberLong(-5), z: MinKey}, {x: 2, y: NumberLong(5), z: MinKey}],
+});
+testZoningBeforeSharding(
+ {shardKey: {x: 1, y: "hashed"}, zoneRange: [{x: "DUB", y: MinKey}, {x: "NYC", y: MinKey}]});
+
+assert.commandWorked(st.s.adminCommand({removeShardFromZone: shardName, zone: zoneName}));
+
+/**
+ * Test that shardCollection uses existing zone ranges to split chunks.
+ */
+function testChunkSplits({collectionExists, shardKey, zoneRanges, expectedNumChunks}) {
+ const shards = configDB.shards.find().toArray();
+ if (collectionExists) {
+ assert.commandWorked(testDB.foo.createIndex(shardKey));
+ }
+
+ // Create a new zone and assign each zone to the shards using round-robin. Then update each of
+ // the zone's range to the range specified in 'zoneRanges'.
+ for (let i = 0; i < zoneRanges.length; i++) {
+ assert.commandWorked(
+ st.s.adminCommand({addShardToZone: shards[i % shards.length]._id, zone: zoneName + i}));
+ assert.commandWorked(st.s.adminCommand({
+ updateZoneKeyRange: ns,
+ min: zoneRanges[i][0],
+ max: zoneRanges[i][1],
+ zone: zoneName + i
+ }));
+ }
+ assert.eq(configDB.tags.count({ns: ns}), zoneRanges.length);
+ assert.eq(0,
+ configDB.chunks.count({ns: ns}),
+ "expect to see no chunk documents for the collection before shardCollection is run");
+
+ // Shard the collection and validate the resulting chunks.
+ assert.commandWorked(mongos.adminCommand({shardCollection: ns, key: shardKey}));
+ const chunkDocs = configDB.chunks.find({ns: ns}).toArray();
+ assert.eq(chunkDocs.length, expectedNumChunks, chunkDocs);
+
+ // Verify that each of the chunks corresponding to zones are in the right shard.
+ for (let i = 0; i < zoneRanges.length; i++) {
+ assert.eq(1,
+ configDB.chunks.count({
+ ns: ns,
+ min: zoneRanges[i][0],
+ max: zoneRanges[i][1],
+ shard: shards[i % shards.length]._id
+ }),
+ chunkDocs);
+ }
+ assert.commandWorked(testDB.runCommand({drop: kCollName}));
+}
+
+// When shard key is compound hashed with range prefix.
+testChunkSplits({
+ shardKey: {x: 1, y: "hashed"},
+ zoneRanges: [
+ [{x: 0, y: MinKey}, {x: 5, y: MinKey}],
+ [{x: 10, y: MinKey}, {x: 15, y: MinKey}],
+ [{x: 20, y: MinKey}, {x: 25, y: MinKey}],
+ [{x: 30, y: MinKey}, {x: 35, y: MinKey}],
+ ],
+ expectedNumChunks: 9 // 4 zones + 2 boundaries + 3 gap chunks.
+});
+testChunkSplits({
+ shardKey: {x: 1, y: "hashed", z: 1},
+ zoneRanges: [
+ [{x: 0, y: NumberLong(0), z: MinKey}, {x: 5, y: NumberLong(0), z: MinKey}],
+ [{x: 10, y: NumberLong(0), z: MinKey}, {x: 15, y: NumberLong(0), z: MinKey}],
+ [{x: 20, y: NumberLong(0), z: MinKey}, {x: 25, y: NumberLong(0), z: MinKey}],
+ [{x: 30, y: NumberLong(0), z: MinKey}, {x: 35, y: NumberLong(0), z: MinKey}],
+ ],
+ expectedNumChunks: 9 // 4 zones + 2 boundaries + 3 gap chunks.
+});
+
+// When shard key is compound hashed with hashed prefix.
+testChunkSplits({
+ collectionExists: true,
+ shardKey: {x: "hashed", y: 1},
+ zoneRanges: [
+ [{x: NumberLong(0), y: MinKey}, {x: NumberLong(10), y: MinKey}],
+ [{x: NumberLong(10), y: MinKey}, {x: NumberLong(20), y: MinKey}],
+ [{x: NumberLong(20), y: MinKey}, {x: NumberLong(30), y: MinKey}],
+ [{x: NumberLong(30), y: MinKey}, {x: NumberLong(40), y: MinKey}],
+ [{x: NumberLong(40), y: MinKey}, {x: NumberLong(50), y: MinKey}],
+ ],
+ expectedNumChunks: 7 // 5 zones + 2 boundaries.
+});
+
+/**
+ * Tests that a non-empty collection associated with zones can be sharded.
+ */
+function testNonemptyZonedCollection() {
+ const shardKey = {x: 1, y: "hashed"};
+ const shards = configDB.shards.find().toArray();
+ const testColl = testDB.getCollection(kCollName);
+ const ranges = [
+ {min: {x: 0, y: MinKey}, max: {x: 10, y: MaxKey}},
+ {min: {x: 10, y: MaxKey}, max: {x: 20, y: MinKey}},
+ {min: {x: 20, y: MinKey}, max: {x: 40, y: MaxKey}}
+ ];
+
+ for (let i = 0; i < 40; i++) {
+ assert.commandWorked(testColl.insert({x: 1, y: Math.random()}));
+ }
+
+ assert.commandWorked(testColl.createIndex(shardKey));
+
+ for (let i = 0; i < shards.length; i++) {
+ assert.commandWorked(
+ mongos.adminCommand({addShardToZone: shards[i]._id, zone: zoneName + i}));
+ assert.commandWorked(mongos.adminCommand(
+ {updateZoneKeyRange: ns, min: ranges[i].min, max: ranges[i].max, zone: zoneName + i}));
+ }
+
+ assert.commandWorked(mongos.adminCommand({shardCollection: ns, key: shardKey}));
+
+ // Check that there is initially 1 chunk.
+ assert.eq(1, configDB.chunks.count({ns: ns}));
+
+ st.startBalancer();
+
+ // Check that the chunks were moved properly.
+ assert.soon(
+ () => configDB.chunks.count({ns: ns}) === 5, 'balancer never ran', 5 * 60 * 1000, 1000);
+
+ assert.commandWorked(testDB.runCommand({drop: kCollName}));
+}
+
+testNonemptyZonedCollection();
+
+st.stop();
+})();
diff --git a/jstests/sharding/refine_collection_shard_key_basic.js b/jstests/sharding/refine_collection_shard_key_basic.js
index 368c6cbf0e5..f7f0a7b15ff 100644
--- a/jstests/sharding/refine_collection_shard_key_basic.js
+++ b/jstests/sharding/refine_collection_shard_key_basic.js
@@ -285,7 +285,7 @@ assert.commandFailedWithCode(
assert.commandFailedWithCode(
mongos.adminCommand({refineCollectionShardKey: kNsName, key: {_id: -1}}), ErrorCodes.BadValue);
assert.commandFailedWithCode(
- mongos.adminCommand({refineCollectionShardKey: kNsName, key: {_id: 1, aKey: 'hashed'}}),
+ mongos.adminCommand({refineCollectionShardKey: kNsName, key: {_id: 'hashed', aKey: 'hashed'}}),
ErrorCodes.BadValue);
assert.commandFailedWithCode(
mongos.adminCommand({refineCollectionShardKey: kNsName, key: {aKey: 'hahashed'}}),
@@ -303,6 +303,9 @@ assert.commandWorked(mongos.adminCommand({refineCollectionShardKey: kNsName, key
dropAndReshardColl({aKey: 'hashed'});
assert.commandWorked(
mongos.adminCommand({refineCollectionShardKey: kNsName, key: {aKey: 'hashed'}}));
+dropAndReshardColl({_id: 1, aKey: 'hashed'});
+assert.commandWorked(
+ mongos.adminCommand({refineCollectionShardKey: kNsName, key: {_id: 1, aKey: 'hashed'}}));
assert.commandWorked(mongos.getDB(kDbName).dropDatabase());
@@ -421,6 +424,24 @@ assert.commandWorked(mongos.getCollection(kNsName).insert({_id: 12345}));
assert.commandWorked(
mongos.adminCommand({refineCollectionShardKey: kNsName, key: {_id: 1, aKey: 1}}));
+// Should work because an index with missing or incomplete shard key entries exists for new shard
+// key {_id: "hashed", aKey: 1} and these entries are treated as null values.
+dropAndReshardColl({_id: "hashed"});
+assert.commandWorked(mongos.getCollection(kNsName).createIndex({_id: "hashed", aKey: 1}));
+assert.commandWorked(mongos.getCollection(kNsName).insert({_id: 12345}));
+
+assert.commandWorked(
+ mongos.adminCommand({refineCollectionShardKey: kNsName, key: {_id: "hashed", aKey: 1}}));
+
+// Should work because an index with missing or incomplete shard key entries exists for new shard
+// key {_id: 1, aKey: "hashed"} and these entries are treated as null values.
+dropAndReshardColl({_id: 1});
+assert.commandWorked(mongos.getCollection(kNsName).createIndex({_id: 1, aKey: "hashed"}));
+assert.commandWorked(mongos.getCollection(kNsName).insert({_id: 12345}));
+
+assert.commandWorked(
+ mongos.adminCommand({refineCollectionShardKey: kNsName, key: {_id: 1, aKey: "hashed"}}));
+
// Should fail because new shard key {aKey: 1} is not a prefix of current shard key {_id: 1,
// aKey: 1}.
dropAndReshardColl({_id: 1, aKey: 1});
@@ -465,7 +486,7 @@ oldEpoch = mongos.getCollection(kConfigCollections).findOne({_id: kNsName}).last
assert.commandWorked(
mongos.adminCommand({refineCollectionShardKey: kNsName, key: {_id: 1, aKey: 1}}));
validateConfigCollections({_id: 1, aKey: 1}, oldEpoch);
-validateConfigChangelog(3);
+validateConfigChangelog(5);
// Should work because a 'useful' index exists for new shard key {a: 1, b.c: 1}. NOTE: We are
// explicitly verifying that refineCollectionShardKey works with a dotted field.
@@ -476,7 +497,7 @@ oldEpoch = mongos.getCollection(kConfigCollections).findOne({_id: kNsName}).last
assert.commandWorked(
mongos.adminCommand({refineCollectionShardKey: kNsName, key: {a: 1, 'b.c': 1}}));
validateConfigCollections({a: 1, 'b.c': 1}, oldEpoch);
-validateConfigChangelog(4);
+validateConfigChangelog(6);
assert.commandWorked(mongos.getDB(kDbName).dropDatabase());
diff --git a/jstests/sharding/shard_collection_basic.js b/jstests/sharding/shard_collection_basic.js
index aa9be219496..445dab8a2e7 100644
--- a/jstests/sharding/shard_collection_basic.js
+++ b/jstests/sharding/shard_collection_basic.js
@@ -143,9 +143,13 @@ testAndClenaupWithKeyNoIndexFailed({x: 1, y: 1});
assert.commandWorked(mongos.getDB(kDbName).foo.insert({x: 1, y: 1}));
testAndClenaupWithKeyOK({x: 1, y: 1});
-testAndClenaupWithKeyNoIndexFailed({x: 'hashed', y: 1});
+// Multiple hashed fields are not allowed.
+testAndClenaupWithKeyNoIndexFailed({x: 'hashed', a: 1, y: 'hashed'});
testAndClenaupWithKeyNoIndexFailed({x: 'hashed', y: 'hashed'});
+// Negative numbers are not allowed.
+testAndClenaupWithKeyNoIndexFailed({x: 'hashed', a: -1});
+
// Shard by a key component.
testAndClenaupWithKeyOK({'z.x': 1});
testAndClenaupWithKeyOK({'z.x': 'hashed'});
diff --git a/src/mongo/db/s/config/configsvr_update_zone_key_range_command.cpp b/src/mongo/db/s/config/configsvr_update_zone_key_range_command.cpp
index 02d82cafa52..ebab9d7a4d6 100644
--- a/src/mongo/db/s/config/configsvr_update_zone_key_range_command.cpp
+++ b/src/mongo/db/s/config/configsvr_update_zone_key_range_command.cpp
@@ -106,17 +106,15 @@ public:
auto parsedRequest =
uassertStatusOK(UpdateZoneKeyRangeRequest::parseFromConfigCommand(cmdObj));
- std::string zoneName;
- if (!parsedRequest.isRemove()) {
- zoneName = parsedRequest.getZoneName();
- }
-
if (parsedRequest.isRemove()) {
uassertStatusOK(ShardingCatalogManager::get(opCtx)->removeKeyRangeFromZone(
opCtx, parsedRequest.getNS(), parsedRequest.getRange()));
} else {
uassertStatusOK(ShardingCatalogManager::get(opCtx)->assignKeyRangeToZone(
- opCtx, parsedRequest.getNS(), parsedRequest.getRange(), zoneName));
+ opCtx,
+ parsedRequest.getNS(),
+ parsedRequest.getRange(),
+ parsedRequest.getZoneName()));
}
return true;
diff --git a/src/mongo/db/s/config/initial_split_policy.cpp b/src/mongo/db/s/config/initial_split_policy.cpp
index fe10ee8c6bf..cab4a2ca212 100644
--- a/src/mongo/db/s/config/initial_split_policy.cpp
+++ b/src/mongo/db/s/config/initial_split_policy.cpp
@@ -108,10 +108,12 @@ void InitialSplitPolicy::calculateHashedSplitPointsForEmptyCollection(
int numInitialChunks,
std::vector<BSONObj>* initialSplitPoints,
std::vector<BSONObj>* finalSplitPoints) {
- if (!shardKeyPattern.isHashedPattern() || !isEmpty) {
+ if (!shardKeyPattern.isHashedPattern() || !shardKeyPattern.hasHashedPrefix() || !isEmpty) {
+ // TODO SERVER-43917: Fix the error message when pre-splitting is enabled for non-hashed
+ // prefixes.
uassert(ErrorCodes::InvalidOptions,
- str::stream() << "numInitialChunks is not supported when the collection is not "
- << (!shardKeyPattern.isHashedPattern() ? "hashed" : "empty"),
+ str::stream() << "numInitialChunks is only supported when the collection is empty "
+ "and has a hashed field as shard key prefix",
!numInitialChunks);
return;
}
@@ -137,16 +139,21 @@ void InitialSplitPolicy::calculateHashedSplitPointsForEmptyCollection(
const auto proposedKey(shardKeyPattern.getKeyPattern().toBSON());
+ auto buildSplitPoint = [&](long long value) {
+ return shardKeyPattern.getKeyPattern().extendRangeBound(
+ BSON(proposedKey.firstElementFieldName() << value), false);
+ };
+
if (numInitialChunks % 2 == 0) {
- finalSplitPoints->push_back(BSON(proposedKey.firstElementFieldName() << current));
+ finalSplitPoints->push_back(buildSplitPoint(current));
current += intervalSize;
} else {
current += intervalSize / 2;
}
for (int i = 0; i < (numInitialChunks - 1) / 2; i++) {
- finalSplitPoints->push_back(BSON(proposedKey.firstElementFieldName() << current));
- finalSplitPoints->push_back(BSON(proposedKey.firstElementFieldName() << -current));
+ finalSplitPoints->push_back(buildSplitPoint(current));
+ finalSplitPoints->push_back(buildSplitPoint(-current));
current += intervalSize;
}
diff --git a/src/mongo/db/s/shardsvr_shard_collection.cpp b/src/mongo/db/s/shardsvr_shard_collection.cpp
index 448f3d1b451..fe1258f5a5a 100644
--- a/src/mongo/db/s/shardsvr_shard_collection.cpp
+++ b/src/mongo/db/s/shardsvr_shard_collection.cpp
@@ -348,15 +348,16 @@ void validateShardKeyAgainstExistingZones(OperationContext* opCtx,
<< tag.getMinKey() << " -->> " << tag.getMaxKey(),
match);
- if (ShardKeyPattern::isHashedPatternEl(proposedKeyElement) &&
- (tagMinKeyElement.type() != NumberLong || tagMaxKeyElement.type() != NumberLong)) {
- uasserted(ErrorCodes::InvalidOptions,
- str::stream() << "cannot do hash sharding with the proposed key "
- << proposedKey.toString() << " because there exists a zone "
- << tag.getMinKey() << " -->> " << tag.getMaxKey()
- << " whose boundaries are not "
- "of type NumberLong");
- }
+ // If the field is hashed, make sure that the min and max values are of supported type.
+ uassert(
+ ErrorCodes::InvalidOptions,
+ str::stream() << "cannot do hash sharding with the proposed key "
+ << proposedKey.toString() << " because there exists a zone "
+ << tag.getMinKey() << " -->> " << tag.getMaxKey()
+ << " whose boundaries are not of type NumberLong, MinKey or MaxKey",
+ !ShardKeyPattern::isHashedPatternEl(proposedKeyElement) ||
+ (ShardKeyPattern::isValidHashedValue(tagMinKeyElement) &&
+ ShardKeyPattern::isValidHashedValue(tagMaxKeyElement)));
}
}
}
diff --git a/src/mongo/db/s/split_chunk.cpp b/src/mongo/db/s/split_chunk.cpp
index ec8d3e9b530..5230be82496 100644
--- a/src/mongo/db/s/split_chunk.cpp
+++ b/src/mongo/db/s/split_chunk.cpp
@@ -147,21 +147,18 @@ StatusWith<boost::optional<ChunkRange>> splitChunk(OperationContext* opCtx,
<< " to split chunk " << chunkRange.toString());
}
- // If the shard key is hashed, then we must make sure that the split points are of type
- // NumberLong.
- if (KeyPattern::isHashedKeyPattern(keyPatternObj)) {
+ // 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);
+ if (hashedField) {
for (BSONObj splitKey : splitKeys) {
- BSONObjIterator it(splitKey);
- while (it.more()) {
- BSONElement splitKeyElement = it.next();
- if (splitKeyElement.type() != NumberLong) {
- return {ErrorCodes::CannotSplit,
- str::stream()
- << "splitChunk cannot split chunk " << chunkRange.toString()
- << ", split point " << splitKeyElement.toString()
- << " must be of type "
- "NumberLong for hashed shard key patterns"};
- }
+ auto hashedSplitElement = splitKey[hashedField.fieldName()];
+ if (!ShardKeyPattern::isValidHashedValue(hashedSplitElement)) {
+ return {ErrorCodes::CannotSplit,
+ str::stream() << "splitChunk cannot split chunk " << chunkRange.toString()
+ << ", split point " << hashedSplitElement.toString()
+ << "Value of type '" << hashedSplitElement.type()
+ << "' is not allowed for hashed fields"};
}
}
}
diff --git a/src/mongo/s/shard_key_pattern.cpp b/src/mongo/s/shard_key_pattern.cpp
index 5e1773ce391..1a6093825ca 100644
--- a/src/mongo/s/shard_key_pattern.cpp
+++ b/src/mongo/s/shard_key_pattern.cpp
@@ -66,6 +66,7 @@ std::vector<std::unique_ptr<FieldRef>> parseShardKeyPattern(const BSONObj& keyPa
std::vector<std::unique_ptr<FieldRef>> parsedPaths;
+ auto numHashedFields = 0;
for (const auto& patternEl : keyPattern) {
auto newFieldRef(std::make_unique<FieldRef>(patternEl.fieldNameStringData()));
@@ -88,7 +89,9 @@ std::vector<std::unique_ptr<FieldRef>> parseShardKeyPattern(const BSONObj& keyPa
!newFieldRef->getPart(i).empty());
}
- // Numeric and ascending (1.0), or "hashed" and single field
+ // Numeric and ascending (1.0), or "hashed" with exactly hashed field.
+ auto isHashedPattern = ShardKeyPattern::isHashedPatternEl(patternEl);
+ numHashedFields += isHashedPattern ? 1 : 0;
uassert(ErrorCodes::BadValue,
str::stream()
<< "Shard key " << keyPattern.toString()
@@ -96,7 +99,7 @@ std::vector<std::unique_ptr<FieldRef>> parseShardKeyPattern(const BSONObj& keyPa
<< " or multiple numerical fields set to a value of 1. Failed to parse field "
<< patternEl.fieldNameStringData(),
(patternEl.isNumber() && patternEl.numberInt() == 1) ||
- (keyPattern.nFields() == 1 && ShardKeyPattern::isHashedPatternEl(patternEl)));
+ (isHashedPattern && numHashedFields == 1));
parsedPaths.emplace_back(std::move(newFieldRef));
}
@@ -173,10 +176,19 @@ Status ShardKeyPattern::checkShardKeyIsValidForMetadataStorage(const BSONObj& sh
return Status::OK();
}
+BSONElement ShardKeyPattern::extractHashedField(BSONObj keyPattern) {
+ for (auto&& element : keyPattern) {
+ if (isHashedPatternEl(element)) {
+ return element;
+ }
+ }
+ return BSONElement();
+}
ShardKeyPattern::ShardKeyPattern(const BSONObj& keyPattern)
: _keyPattern(keyPattern),
_keyPatternPaths(parseShardKeyPattern(keyPattern)),
- _hasId(keyPattern.hasField("_id"_sd)) {}
+ _hasId(keyPattern.hasField("_id"_sd)),
+ _hashedField(extractHashedField(keyPattern)) {}
ShardKeyPattern::ShardKeyPattern(const KeyPattern& keyPattern)
: ShardKeyPattern(keyPattern.toBSON()) {}
@@ -186,9 +198,30 @@ bool ShardKeyPattern::isHashedPatternEl(const BSONElement& el) {
}
bool ShardKeyPattern::isHashedPattern() const {
+ return !_hashedField.eoo();
+}
+
+bool ShardKeyPattern::isValidHashedValue(const BSONElement& el) {
+ switch (el.type()) {
+ case MinKey:
+ case MaxKey:
+ case NumberLong:
+ return true;
+ default:
+ return false;
+ }
+ MONGO_UNREACHABLE;
+}
+
+
+bool ShardKeyPattern::hasHashedPrefix() const {
return isHashedPatternEl(_keyPattern.toBSON().firstElement());
}
+BSONElement ShardKeyPattern::getHashedField() const {
+ return _hashedField;
+}
+
const KeyPattern& ShardKeyPattern::getKeyPattern() const {
return _keyPattern;
}
@@ -345,7 +378,7 @@ BSONObj ShardKeyPattern::extractShardKeyFromQuery(const CanonicalQuery& query) c
if (!isValidShardKeyElementForStorage(equalEl))
return BSONObj();
- if (isHashedPattern()) {
+ if (_hashedField && _hashedField.fieldNameStringData() == patternPath.dottedField()) {
keyBuilder.append(
patternPath.dottedField(),
BSONElementHasher::hash64(equalEl, BSONElementHasher::DEFAULT_HASH_SEED));
diff --git a/src/mongo/s/shard_key_pattern.h b/src/mongo/s/shard_key_pattern.h
index 91eee10080a..c5baaabc407 100644
--- a/src/mongo/s/shard_key_pattern.h
+++ b/src/mongo/s/shard_key_pattern.h
@@ -88,8 +88,23 @@ public:
*/
static bool isHashedPatternEl(const BSONElement& el);
+ /**
+ * Returns the BSONElement pointing to the hashed field. Returns empty BSONElement if not found.
+ */
+ static BSONElement extractHashedField(BSONObj keyPattern);
+
+ /**
+ * Check if the given BSONElement is of type 'MinKey', 'MaxKey' or 'NumberLong', which are the
+ * only acceptable values for hashed fields.
+ */
+ static bool isValidHashedValue(const BSONElement& el);
+
bool isHashedPattern() const;
+ bool hasHashedPrefix() const;
+
+ BSONElement getHashedField() const;
+
const KeyPattern& getKeyPattern() const;
const std::vector<std::unique_ptr<FieldRef>>& getKeyPatternFields() const;
@@ -255,6 +270,7 @@ private:
std::vector<std::unique_ptr<FieldRef>> _keyPatternPaths;
bool _hasId;
+ BSONElement _hashedField;
};
} // namespace mongo
diff --git a/src/mongo/s/shard_key_pattern_test.cpp b/src/mongo/s/shard_key_pattern_test.cpp
index fd51f371f22..a1196e36e17 100644
--- a/src/mongo/s/shard_key_pattern_test.cpp
+++ b/src/mongo/s/shard_key_pattern_test.cpp
@@ -433,6 +433,19 @@ TEST(ShardKeyPattern, ExtractQueryShardKeyHashed) {
ASSERT_BSONOBJ_EQ(queryKey(pattern, BSON("a" << BSON("c" << value))), BSONObj());
ASSERT_BSONOBJ_EQ(queryKey(pattern, BSON("a" << BSON("b" << BSON_ARRAY(value)))), BSONObj());
ASSERT_BSONOBJ_EQ(queryKey(pattern, BSON("a" << BSON_ARRAY(BSON("b" << value)))), BSONObj());
+
+ pattern = ShardKeyPattern(BSON("a.b"
+ << "hashed"
+ << "c.d" << 1));
+
+ ASSERT_BSONOBJ_EQ(queryKey(pattern, BSON("a.b" << value << "c.d" << value)),
+ BSON("a.b" << hashValue << "c.d" << value));
+ ASSERT_BSONOBJ_EQ(
+ queryKey(pattern, fromjson("{a : {b: '12345', p : 1}, c : {d : '12345', q: 2}}")),
+ BSON("a.b" << hashValue << "c.d" << value));
+
+ ASSERT_BSONOBJ_EQ(queryKey(pattern, BSON("a.b" << value)), BSONObj());
+ ASSERT_BSONOBJ_EQ(queryKey(pattern, fromjson("{'a.b': [10], 'c.d': 1}")), BSONObj());
}
static bool indexComp(const ShardKeyPattern& pattern, const BSONObj& indexPattern) {
@@ -513,5 +526,16 @@ TEST(ShardKeyPattern, UniqueIndexCompatibleHashed) {
ASSERT(!indexComp(pattern, BSON("c" << -1 << "a.b" << 1)));
}
+TEST(ShardKeyPattern, IsHashedPattern) {
+ ASSERT(ShardKeyPattern(BSON("a.b"
+ << "hashed"))
+ .isHashedPattern());
+ ASSERT(ShardKeyPattern(BSON("a.b" << 1 << "c"
+ << "hashed"
+ << "d" << 1))
+ .isHashedPattern());
+ ASSERT(!ShardKeyPattern(BSON("a.b" << 1 << "d" << 1)).isHashedPattern());
+}
+
} // namespace
} // namespace mongo