mongo/jstests/sharding/compound_hashed_shard_key_p...

325 lines
13 KiB
JavaScript

/**
* 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.
*/
import {ShardingTest} from "jstests/libs/shardingtest.js";
import {findChunksUtil} from "jstests/sharding/libs/find_chunks_util.js";
const st = new ShardingTest({name: jsTestName(), shards: 3});
const dbname = "test";
const mongos = st.s0;
const db = st.getDB(dbname);
db.adminCommand({enablesharding: dbname, primaryShard: st.shard1.shardName});
const expectedTotalChunkCount = 3;
const expectedChunkPerShardCount = 1;
/**
* 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);
assert.commandWorked(db.adminCommand({shardcollection: db.collWithData.getFullName(), key: shardKey}));
assert.eq(
findChunksUtil.countChunksForNs(st.config, db.collWithData.getFullName()),
1,
"sharding non-empty collection should not pre-split",
);
});
/**
* Helper function to validate that the chunks ranges have all the shard key fields and each shard
* has expected number of chunks.
*/
function checkValidChunks(coll, shardKey, checkChunksPerShardFn) {
const chunks = findChunksUtil.findChunksByNs(st.config, coll.getFullName()).toArray();
let shardCountsMap = {[st.shard0.shardName]: 0, [st.shard1.shardName]: 0, [st.shard2.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);
}
checkChunksPerShardFn(shardCountsMap);
}
//
// Test cases for compound hashed shard keys with hashed prefix.
//
let shardKey = {hashedField: "hashed", rangeField1: 1, rangeField2: 1};
// Supported: Hashed sharding + empty collection.
// Expected: Chunk distribution with a total of 'expectedTotalChunkCount' chunks,
// and 'expectedChunkPerShardCount' chunks placed on each shard.
assert.commandWorked(db.hashedCollEmpty.createIndex(shardKey));
let coll = db.hashedCollEmpty;
assert.commandWorked(mongos.adminCommand({shardCollection: coll.getFullName(), key: shardKey}));
checkValidChunks(coll, shardKey, (shardCountsMap) => {
// Each shard has 'expectedChunkPerShardCount' chunks.
assert(Object.values(shardCountsMap).every((count) => count === expectedChunkPerShardCount));
});
// Supported: Hashed sharding + non-existent collection.
// Expected: Chunk distribution with a total of 'expectedTotalChunkCount' chunks,
// and 'expectedChunkPerShardCount' chunks placed on each shard.
coll = db.hashedCollNonExistent;
assert.commandWorked(mongos.adminCommand({shardCollection: coll.getFullName(), key: shardKey}));
checkValidChunks(coll, shardKey, (shardCountsMap) => {
const totalChunks = Object.values(shardCountsMap).reduce((accumulator, v) => accumulator + v);
assert.eq(expectedTotalChunkCount, totalChunks, "Unexpected total amount of chunks");
// Each shard has 'expectedChunkPerShardCount' chunks.
assert(Object.values(shardCountsMap).every((count) => count == expectedChunkPerShardCount));
});
// Default pre-splitting assigns one chunk per shard.
coll = db.hashedDefaultPreSplit;
assert.commandWorked(mongos.adminCommand({shardCollection: coll.getFullName(), key: shardKey}));
checkValidChunks(coll, shardKey, (shardCountsMap) => {
assert.gte(
shardCountsMap[st.shard0.shardName],
expectedChunkPerShardCount,
"Unexpected amount of chunks on " + st.shard0.shardName,
);
assert.gte(
shardCountsMap[st.shard1.shardName],
expectedChunkPerShardCount,
"Unexpected amount of chunks on " + st.shard1.shardName,
);
assert.gte(
shardCountsMap[st.shard2.shardName],
expectedChunkPerShardCount,
"Unexpected amount of chunks on " + st.shard2.shardName,
);
});
db.hashedPrefixColl.drop();
// 'presplitHashedZones' cannot be passed without setting up zones.
assert.commandFailedWithCode(
db.adminCommand({
shardcollection: db.hashedPrefixColl.getFullName(),
key: shardKey,
presplitHashedZones: true,
}),
31387,
);
// Verify that 'shardCollection' command will fail if the zones are set up incorrectly.
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard2.shardName, zone: "hashedPrefix"}));
assert.commandWorked(
st.s.adminCommand({
updateZoneKeyRange: db.hashedPrefixColl.getFullName(),
min: {hashedField: MinKey, rangeField1: MinKey, rangeField2: MinKey},
max: {
hashedField: MaxKey,
rangeField1: MaxKey,
rangeField2: MinKey,
}, // All fields should be MaxKey for a valid zone.
zone: "hashedPrefix",
}),
);
assert.commandFailedWithCode(
db.adminCommand({
shardcollection: db.hashedPrefixColl.getFullName(),
key: shardKey,
presplitHashedZones: true,
}),
31412,
);
// Verify that 'shardCollection' command will pre-split chunks if a single zone is set up ranging
// from MinKey to MaxKey and 'presplitHashedZones' flag is set.
db.hashedPrefixColl.drop();
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard1.shardName, zone: "hashedPrefix"}));
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard2.shardName, zone: "hashedPrefix"}));
assert.commandWorked(
st.s.adminCommand({
updateZoneKeyRange: db.hashedPrefixColl.getFullName(),
min: {hashedField: MinKey, rangeField1: MinKey, rangeField2: MinKey},
max: {hashedField: MaxKey, rangeField1: MaxKey, rangeField2: MaxKey},
zone: "hashedPrefix",
}),
);
assert.commandWorked(
db.adminCommand({
shardcollection: db.hashedPrefixColl.getFullName(),
key: shardKey,
presplitHashedZones: true,
}),
);
// By default, we create one chunk per shard for each shard that contains at least one zone.
checkValidChunks(db.hashedPrefixColl, shardKey, (shardCountsMap) => {
assert.eq(0, shardCountsMap[st.shard0.shardName], "Unexpected amount of chunks on shard0");
assert.eq(expectedChunkPerShardCount, shardCountsMap[st.shard1.shardName], "Unexpected amount of chunks on shard1");
assert.eq(expectedChunkPerShardCount, shardCountsMap[st.shard2.shardName], "Unexpected amount of chunks on shard2");
});
//
// Test cases for compound hashed shard keys with non-hashed prefix.
//
/**
* Helper function to create two non-overlapping interweaving tag ranges for each of the specified
* zones.
*/
function createZoneRanges(coll, zoneNames) {
let offsetFirstTag = 0,
offsetSecondTag = 0;
// Create zone ranges such that first zone has tag ranges [a, b) and [A, B). Second zone has tag
// ranges [c, d) and [C, D). Third zone has tag ranges [e, f) and [E, f) so on.
for (let zoneName of zoneNames) {
assert.commandWorked(
st.s.adminCommand({
updateZoneKeyRange: coll.getFullName(),
min: {
rangeField1: String.fromCharCode(97 + offsetFirstTag++),
hashedField: MinKey,
rangeField2: MinKey,
},
max: {
rangeField1: String.fromCharCode(97 + offsetFirstTag++),
hashedField: MinKey,
rangeField2: MinKey,
},
zone: zoneName,
}),
);
assert.commandWorked(
st.s.adminCommand({
updateZoneKeyRange: coll.getFullName(),
min: {
rangeField1: String.fromCharCode(65 + offsetSecondTag++),
hashedField: MinKey,
rangeField2: MinKey,
},
max: {
rangeField1: String.fromCharCode(65 + offsetSecondTag++),
hashedField: MinKey,
rangeField2: MinKey,
},
zone: zoneName,
}),
);
}
}
/**
* Helper function to set up two zones named 'nonHashedPrefix1' and 'nonHashedPrefix2' on 'shard0'.
*/
function setUpTwoZonesOnShard0(coll) {
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard0.shardName, zone: "nonHashedPrefix1"}));
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard0.shardName, zone: "nonHashedPrefix2"}));
assert.commandWorked(st.s.adminCommand({removeShardFromZone: st.shard1.shardName, zone: "nonHashedPrefix1"}));
assert.commandWorked(st.s.adminCommand({removeShardFromZone: st.shard1.shardName, zone: "nonHashedPrefix2"}));
assert.commandWorked(st.s.adminCommand({removeShardFromZone: st.shard2.shardName, zone: "nonHashedPrefix2"}));
createZoneRanges(coll, ["nonHashedPrefix1", "nonHashedPrefix2"]);
}
/**
* Helper function to set up two zones such that 'nonHashedPrefix1' zone is assigned to shard0 and
* shard1. 'nonHashedPrefix2' is assinged to shard1 and shard2.
*/
function setUpTwoZones(coll) {
assert.commandWorked(st.s.adminCommand({removeShardFromZone: st.shard0.shardName, zone: "nonHashedPrefix2"}));
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard0.shardName, zone: "nonHashedPrefix1"}));
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard1.shardName, zone: "nonHashedPrefix1"}));
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard1.shardName, zone: "nonHashedPrefix2"}));
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard2.shardName, zone: "nonHashedPrefix2"}));
createZoneRanges(coll, ["nonHashedPrefix1", "nonHashedPrefix2"]);
}
shardKey = {
rangeField1: 1,
hashedField: "hashed",
rangeField2: 1,
};
db.coll.drop();
setUpTwoZonesOnShard0(db.coll);
db.coll.drop();
// 'presplitHashedZones' cannot be passed without setting up zones.
assert.commandFailedWithCode(
db.adminCommand({shardcollection: db.coll.getFullName(), key: shardKey, presplitHashedZones: true}),
31387,
);
// Verify that shardCollection command will fail if the zones are set up incorrectly.
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard0.shardName, zone: "nonHashedPrefix1"}));
assert.commandWorked(
st.s.adminCommand({
updateZoneKeyRange: db.coll.getFullName(),
min: {rangeField1: "A", hashedField: MinKey, rangeField2: MinKey},
max: {rangeField1: "A", hashedField: MaxKey, rangeField2: MinKey},
zone: "nonHashedPrefix1",
}),
);
assert.commandFailedWithCode(
db.adminCommand({shardcollection: db.coll.getFullName(), key: shardKey, presplitHashedZones: true}),
31390,
);
// Verify that 'presplitHashedZones' works correctly when zones are set up.
db.coll.drop();
setUpTwoZones(db.coll);
assert.commandWorked(
db.adminCommand({shardcollection: db.coll.getFullName(), key: shardKey, presplitHashedZones: true}),
);
// Verify that 'presplitHashedZones' uses the default initial value of 1 chunk per tag on each
// shard. Setup two zones such that shard1 hosts both zones, and shard0/2 host one zone each.
db.coll.drop();
setUpTwoZones(db.coll);
assert.commandWorked(
db.adminCommand({shardcollection: db.coll.getFullName(), key: shardKey, presplitHashedZones: true}),
);
// The chunk distribution from zones should be [2, 2+2 (two zones), 2]. The 5 gap chunks should be
// distributed among three shards.
checkValidChunks(db.coll, shardKey, (shardCountsMap) => {
const totalChunks = Object.values(shardCountsMap).reduce((accumulator, v) => accumulator + v);
assert.eq(13, totalChunks, "Unexpected total amount of chunks");
assert.gte(shardCountsMap[st.shard0.shardName], 3, "Unexpected amount of chunks on shard0");
assert.gte(shardCountsMap[st.shard1.shardName], 5, "Unexpected amount of chunks on shard1");
assert.gte(shardCountsMap[st.shard2.shardName], 3, "Unexpected amount of chunks on shard2");
});
// Verify that 'presplitHashedZones' uses the default initial value of 1 chunk per tag on each
// shard. Setup two zones on shard0.
db.coll.drop();
setUpTwoZonesOnShard0(db.coll);
assert.commandWorked(
db.adminCommand({shardcollection: db.coll.getFullName(), key: shardKey, presplitHashedZones: true}),
);
// Since only Shard0 has chunks, we create on chunk per tag on shard0. The 5 gap chunks should be
// distributed among three shards.
checkValidChunks(db.coll, shardKey, (shardCountsMap) => {
const totalChunks = Object.values(shardCountsMap).reduce((accumulator, v) => accumulator + v);
assert.eq(9, totalChunks, "Unexpected total amount of chunks");
assert.gte(shardCountsMap[st.shard0.shardName], 5, "Unexpected amount of chunks on shard0");
assert.gte(shardCountsMap[st.shard1.shardName], 1, "Unexpected amount of chunks on shard1");
assert.gte(shardCountsMap[st.shard2.shardName], 1, "Unexpected amount of chunks on shard2");
});
st.stop();