mongo/jstests/sharding/refine_collection_shard_key...

337 lines
14 KiB
JavaScript

//
// Basic tests for refineCollectionShardKey.
//
import {configureFailPoint} from "jstests/libs/fail_point_util.js";
import {ReplSetTest} from "jstests/libs/replsettest.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";
import {findChunksUtil} from "jstests/sharding/libs/find_chunks_util.js";
import {
dropAndReshardColl,
integrationTests,
shardKeyValidationTests,
simpleValidationTests,
uniquePropertyTests,
} from "jstests/sharding/libs/refine_collection_shard_key_common.js";
import {flushRoutersAndRefreshShardMetadata} from "jstests/sharding/libs/sharded_transactions_helpers.js";
const st = new ShardingTest({
mongos: 2,
shards: 2,
rs: {nodes: 3},
configOptions: {setParameter: {maxTransactionLockRequestTimeoutMillis: ReplSetTest.kDefaultTimeoutMS}},
});
const mongos = st.s0;
const staleMongos = st.s1;
const primaryShard = st.shard0.shardName;
const secondaryShard = st.shard1.shardName;
const kDbName = "db";
const kCollName = "foo";
const kNsName = kDbName + "." + kCollName;
const kUnrelatedName = kDbName + ".bar";
const kConfigCollections = "config.collections";
const kConfigTags = "config.tags";
function dropAndRecreateColl(keyDoc) {
assert.commandWorked(mongos.getDB(kDbName).runCommand({drop: kCollName}));
assert.commandWorked(mongos.getCollection(kNsName).insert(keyDoc));
}
// 1. Assume oldKeyDoc = {a: 1, b: 1} when validating operations before
// 'refineCollectionShardKey'.
// 2. Assume newKeyDoc = {a: 1, b: 1, c: 1, d: 1} when validating operations after
// 'refineCollectionShardKey'.
function setupCRUDBeforeRefine() {
const session = mongos.startSession({retryWrites: true});
const sessionDB = session.getDatabase(kDbName);
// The documents below will be read after refineCollectionShardKey to verify data integrity.
assert.commandWorked(sessionDB.getCollection(kCollName).insert({a: 5, b: 5, c: 5, d: 5}));
assert.commandWorked(sessionDB.getCollection(kCollName).insert({a: 10, b: 10, c: 10, d: 10}));
}
function validateCRUDAfterRefine() {
// Force a refresh on the mongos and each shard because refineCollectionShardKey only triggers
// best-effort shard refreshes.
flushRoutersAndRefreshShardMetadata(st, {ns: kNsName});
const session = mongos.startSession({retryWrites: true});
const sessionDB = session.getDatabase(kDbName);
// Verify that documents inserted before refineCollectionShardKey have not been corrupted.
assert.eq([{a: 5, b: 5, c: 5, d: 5}], sessionDB.getCollection(kCollName).find({a: 5}, {_id: 0}).toArray());
assert.eq([{a: 10, b: 10, c: 10, d: 10}], sessionDB.getCollection(kCollName).find({a: 10}, {_id: 0}).toArray());
// A write with the incomplete shard key is treated as if the missing values are null.
assert.commandWorked(sessionDB.getCollection(kCollName).insert({a: 1, b: 1}));
assert.commandWorked(sessionDB.getCollection(kCollName).insert({a: -1, b: -1}));
assert.neq(null, sessionDB.getCollection(kCollName).findOne({a: 1, b: 1, c: null, d: null}));
assert.neq(null, sessionDB.getCollection(kCollName).findOne({a: -1, b: -1, c: null, d: null}));
// Full shard key writes work properly.
assert.commandWorked(sessionDB.getCollection(kCollName).insert({a: 1, b: 1, c: 1, d: 1}));
assert.commandWorked(sessionDB.getCollection(kCollName).insert({a: -1, b: -1, c: -1, d: -1}));
// This enables the feature allows writes to omit the shard key in their queries.
assert.commandWorked(sessionDB.getCollection(kCollName).update({a: 1, b: 1, c: 1}, {$set: {x: 2}}));
assert.commandWorked(sessionDB.getCollection(kCollName).update({a: 1, b: 1, c: 1, d: 1}, {$set: {b: 2}}));
assert.commandWorked(sessionDB.getCollection(kCollName).update({a: -1, b: -1, c: -1, d: -1}, {$set: {b: 4}}));
assert.eq(2, sessionDB.getCollection(kCollName).findOne({c: 1}).x);
assert.eq(2, sessionDB.getCollection(kCollName).findOne({c: 1}).b);
assert.eq(4, sessionDB.getCollection(kCollName).findOne({c: -1}).b);
// Versioned reads against secondaries should work as expected.
mongos.setReadPref("secondary");
assert.eq(2, sessionDB.getCollection(kCollName).findOne({c: 1}).x);
assert.eq(2, sessionDB.getCollection(kCollName).findOne({c: 1}).b);
assert.eq(4, sessionDB.getCollection(kCollName).findOne({c: -1}).b);
mongos.setReadPref(null);
assert.commandWorked(sessionDB.getCollection(kCollName).remove({a: 1, b: 1}, true));
assert.commandWorked(sessionDB.getCollection(kCollName).remove({a: -1, b: -1}, true));
assert.commandWorked(sessionDB.getCollection(kCollName).remove({a: 1, b: 2, c: 1, d: 1}, true));
assert.commandWorked(sessionDB.getCollection(kCollName).remove({a: -1, b: 4, c: -1, d: -1}, true));
assert.commandWorked(sessionDB.getCollection(kCollName).remove({a: 5, b: 5, c: 5, d: 5}, true));
assert.commandWorked(sessionDB.getCollection(kCollName).remove({a: 10, b: 10, c: 10, d: 10}, true));
assert.commandWorked(sessionDB.getCollection(kCollName).remove({a: 1, b: 1, c: null, d: null}, true));
assert.commandWorked(sessionDB.getCollection(kCollName).remove({a: -1, b: -1, c: null, d: null}, true));
assert.eq(null, sessionDB.getCollection(kCollName).findOne());
}
function validateUnrelatedCollAfterRefine(oldCollArr, oldChunkArr, oldTagsArr) {
const collArr = mongos.getCollection(kConfigCollections).find({_id: kUnrelatedName}).toArray();
assert.eq(1, collArr.length);
assert.sameMembers(oldCollArr, collArr);
const chunkArr = findChunksUtil.findChunksByNs(mongos.getDB("config"), kUnrelatedName).toArray();
assert.eq(3, chunkArr.length);
assert.sameMembers(oldChunkArr, chunkArr);
const tagsArr = mongos.getCollection(kConfigTags).find({ns: kUnrelatedName}).toArray();
assert.eq(3, tagsArr.length);
assert.sameMembers(oldTagsArr, tagsArr);
}
const oldKeyDoc = {
a: 1,
b: 1,
};
const newKeyDoc = {
a: 1,
b: 1,
c: 1,
d: 1,
};
simpleValidationTests(mongos, kDbName);
shardKeyValidationTests(mongos, kDbName);
uniquePropertyTests(mongos, kDbName);
jsTestLog("********** NAMESPACE VALIDATION TESTS **********");
assert.commandWorked(mongos.adminCommand({shardCollection: kNsName, key: {_id: 1}}));
// Configure failpoint 'hangRefineCollectionShardKeyAfterRefresh' on staleMongos and run
// refineCollectionShardKey against this mongos in a parallel thread.
let hangAfterRefreshFailPoint = configureFailPoint(staleMongos, "hangRefineCollectionShardKeyAfterRefresh");
const awaitShellToTriggerNamespaceNotSharded = startParallelShell(() => {
assert.commandFailedWithCode(
db.adminCommand({refineCollectionShardKey: "db.foo", key: {_id: 1, aKey: 1}}),
ErrorCodes.NamespaceNotSharded,
);
}, staleMongos.port);
hangAfterRefreshFailPoint.wait();
// Drop and re-create namespace 'db.foo' without staleMongos refreshing its metadata.
dropAndRecreateColl({aKey: 1});
// Should fail because namespace 'db.foo' is not sharded.
hangAfterRefreshFailPoint.off();
awaitShellToTriggerNamespaceNotSharded();
assert.commandWorked(mongos.getDB(kDbName).dropDatabase());
jsTestLog("********** INTEGRATION TESTS **********");
integrationTests(mongos, kDbName, primaryShard, secondaryShard);
assert.commandWorked(mongos.adminCommand({shardCollection: kNsName, key: oldKeyDoc}));
assert.commandWorked(mongos.getCollection(kNsName).createIndex(newKeyDoc));
// CRUD operations before and after refineCollectionShardKey should work as expected.
setupCRUDBeforeRefine();
assert.commandWorked(mongos.adminCommand({refineCollectionShardKey: kNsName, key: newKeyDoc}));
validateCRUDAfterRefine();
// Create an unrelated namespace 'db.bar' with 3 chunks and 3 tags to verify that it isn't
// corrupted after refineCollectionShardKey.
dropAndReshardColl(mongos, kDbName, kCollName, oldKeyDoc);
assert.commandWorked(mongos.getCollection(kNsName).createIndex(newKeyDoc));
assert.commandWorked(mongos.adminCommand({shardCollection: kUnrelatedName, key: oldKeyDoc}));
assert.commandWorked(mongos.adminCommand({split: kUnrelatedName, middle: {a: 0, b: 0}}));
assert.commandWorked(mongos.adminCommand({split: kUnrelatedName, middle: {a: 5, b: 5}}));
assert.commandWorked(mongos.adminCommand({addShardToZone: primaryShard, zone: "unrelated_1"}));
assert.commandWorked(mongos.adminCommand({addShardToZone: primaryShard, zone: "unrelated_2"}));
assert.commandWorked(mongos.adminCommand({addShardToZone: primaryShard, zone: "unrelated_3"}));
assert.commandWorked(
mongos.adminCommand({
updateZoneKeyRange: kUnrelatedName,
min: {a: MinKey, b: MinKey},
max: {a: 0, b: 0},
zone: "unrelated_1",
}),
);
assert.commandWorked(
mongos.adminCommand({
updateZoneKeyRange: kUnrelatedName,
min: {a: 0, b: 0},
max: {a: 5, b: 5},
zone: "unrelated_2",
}),
);
assert.commandWorked(
mongos.adminCommand({
updateZoneKeyRange: kUnrelatedName,
min: {a: 5, b: 5},
max: {a: MaxKey, b: MaxKey},
zone: "unrelated_3",
}),
);
const oldCollArr = mongos.getCollection(kConfigCollections).find({_id: kUnrelatedName}).toArray();
const oldChunkArr = findChunksUtil.findChunksByNs(mongos.getDB("config"), kUnrelatedName).toArray();
const oldTagsArr = mongos.getCollection(kConfigTags).find({ns: kUnrelatedName}).toArray();
assert.eq(1, oldCollArr.length);
assert.eq(3, oldChunkArr.length);
assert.eq(3, oldTagsArr.length);
assert.commandWorked(mongos.adminCommand({refineCollectionShardKey: kNsName, key: newKeyDoc}));
validateUnrelatedCollAfterRefine(oldCollArr, oldChunkArr, oldTagsArr);
// Assumes the given arrays are sorted by the max field.
function compareMinAndMaxFields(shardedArr, refinedArr) {
assert(shardedArr.length && refinedArr.length, tojson(shardedArr) + ", " + tojson(refinedArr));
assert.eq(shardedArr.length, refinedArr.length, tojson(shardedArr) + ", " + tojson(refinedArr));
const shardedMinAndMax = shardedArr.map((obj) => {
return {min: obj.min, max: obj.max};
});
const refinedMinAndMax = refinedArr.map((obj) => {
return {min: obj.min, max: obj.max};
});
assert.eq(shardedMinAndMax, refinedMinAndMax);
}
// Verifies the min and max fields are the same for the chunks and tags in the given collections.
function compareBoundaries(conn, shardedNs, refinedNs) {
// Compare chunks.
const shardedChunks = findChunksUtil.findChunksByNs(conn.getDB("config"), shardedNs).sort({max: 1}).toArray();
const refinedChunks = findChunksUtil.findChunksByNs(conn.getDB("config"), refinedNs).sort({max: 1}).toArray();
compareMinAndMaxFields(shardedChunks, refinedChunks);
// Compare tags.
const shardedTags = conn.getDB("config").tags.find({ns: shardedNs}).sort({max: 1}).toArray();
const refinedTags = conn.getDB("config").tags.find({ns: refinedNs}).sort({max: 1}).toArray();
compareMinAndMaxFields(shardedTags, refinedTags);
}
//
// Verify the chunk and tag boundaries are the same for a collection sharded to a certain shard key
// and a collection refined to that same shard key.
//
// For a shard key without nested fields.
(() => {
const dbName = "compareDB";
const shardedNs = dbName + ".shardedColl";
const refinedNs = dbName + ".refinedColl";
assert.commandWorked(st.s.adminCommand({enableSharding: dbName, primaryShard: st.shard0.shardName}));
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard0.shardName, zone: "zone_1"}));
assert.commandWorked(st.s.adminCommand({shardCollection: shardedNs, key: {a: 1, b: 1, c: 1}}));
assert.commandWorked(st.s.adminCommand({split: shardedNs, middle: {a: 0, b: MinKey, c: MinKey}}));
assert.commandWorked(
st.s.adminCommand({
updateZoneKeyRange: shardedNs,
min: {a: MinKey, b: MinKey, c: MinKey},
max: {a: 0, b: MinKey, c: MinKey},
zone: "zone_1",
}),
);
assert.commandWorked(
st.s.adminCommand({
updateZoneKeyRange: shardedNs,
min: {a: 10, b: MinKey, c: MinKey},
max: {a: MaxKey, b: MaxKey, c: MaxKey},
zone: "zone_1",
}),
);
assert.commandWorked(st.s.adminCommand({shardCollection: refinedNs, key: {a: 1}}));
assert.commandWorked(st.s.adminCommand({split: refinedNs, middle: {a: 0}}));
assert.commandWorked(
st.s.adminCommand({updateZoneKeyRange: refinedNs, min: {a: MinKey}, max: {a: 0}, zone: "zone_1"}),
);
assert.commandWorked(
st.s.adminCommand({updateZoneKeyRange: refinedNs, min: {a: 10}, max: {a: MaxKey}, zone: "zone_1"}),
);
assert.commandWorked(st.s.getCollection(refinedNs).createIndex({a: 1, b: 1, c: 1}));
assert.commandWorked(st.s.adminCommand({refineCollectionShardKey: refinedNs, key: {a: 1, b: 1, c: 1}}));
compareBoundaries(st.s, shardedNs, refinedNs);
})();
// For a shard key with nested fields .
(() => {
const dbName = "compareDBNested";
const shardedNs = dbName + ".shardedColl";
const refinedNs = dbName + ".refinedColl";
assert.commandWorked(st.s.adminCommand({enableSharding: dbName, primaryShard: st.shard0.shardName}));
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard0.shardName, zone: "zone_1"}));
assert.commandWorked(st.s.adminCommand({shardCollection: shardedNs, key: {"a.b": 1, "c.d.e": 1, f: 1}}));
assert.commandWorked(st.s.adminCommand({split: shardedNs, middle: {"a.b": 0, "c.d.e": MinKey, f: MinKey}}));
assert.commandWorked(
st.s.adminCommand({
updateZoneKeyRange: shardedNs,
min: {"a.b": MinKey, "c.d.e": MinKey, f: MinKey},
max: {"a.b": 0, "c.d.e": MinKey, f: MinKey},
zone: "zone_1",
}),
);
assert.commandWorked(
st.s.adminCommand({
updateZoneKeyRange: shardedNs,
min: {"a.b": 10, "c.d.e": MinKey, f: MinKey},
max: {"a.b": MaxKey, "c.d.e": MaxKey, f: MaxKey},
zone: "zone_1",
}),
);
assert.commandWorked(st.s.adminCommand({shardCollection: refinedNs, key: {"a.b": 1}}));
assert.commandWorked(st.s.adminCommand({split: refinedNs, middle: {"a.b": 0}}));
assert.commandWorked(
st.s.adminCommand({updateZoneKeyRange: refinedNs, min: {"a.b": MinKey}, max: {"a.b": 0}, zone: "zone_1"}),
);
assert.commandWorked(
st.s.adminCommand({updateZoneKeyRange: refinedNs, min: {"a.b": 10}, max: {"a.b": MaxKey}, zone: "zone_1"}),
);
assert.commandWorked(st.s.getCollection(refinedNs).createIndex({"a.b": 1, "c.d.e": 1, f: 1}));
assert.commandWorked(st.s.adminCommand({refineCollectionShardKey: refinedNs, key: {"a.b": 1, "c.d.e": 1, f: 1}}));
compareBoundaries(st.s, shardedNs, refinedNs);
})();
st.stop();