2016-11-16 21:33:31 +01:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Provides wrapper functions that perform exponential backoff and allow for
|
|
|
|
* acceptable errors to be returned from mergeChunks, moveChunk, and splitChunk
|
|
|
|
* commands.
|
|
|
|
*
|
|
|
|
* Also provides functions to help perform assertions about the state of chunks.
|
|
|
|
*
|
|
|
|
* Intended for use by workloads testing sharding (i.e., workloads starting with 'sharded_').
|
|
|
|
*/
|
|
|
|
|
|
|
|
load('jstests/concurrency/fsm_workload_helpers/server_types.js'); // for isMongos & isMongod
|
|
|
|
|
|
|
|
var ChunkHelper = (function() {
|
|
|
|
// exponential backoff
|
|
|
|
function getNextBackoffSleep(curSleep) {
|
|
|
|
const MAX_BACKOFF_SLEEP = 5000; // milliseconds
|
|
|
|
|
|
|
|
curSleep *= 2;
|
|
|
|
return Math.min(curSleep, MAX_BACKOFF_SLEEP);
|
|
|
|
}
|
|
|
|
|
|
|
|
function runCommandWithRetries(db, cmd, acceptableErrorCodes) {
|
|
|
|
const INITIAL_BACKOFF_SLEEP = 500; // milliseconds
|
|
|
|
const MAX_RETRIES = 5;
|
|
|
|
|
|
|
|
var acceptableErrorOccurred = function acceptableErrorOccurred(errorCode,
|
|
|
|
acceptableErrorCodes) {
|
|
|
|
return acceptableErrorCodes.indexOf(errorCode) > -1;
|
|
|
|
};
|
|
|
|
|
|
|
|
var res;
|
|
|
|
var retries = 0;
|
|
|
|
var backoffSleep = INITIAL_BACKOFF_SLEEP;
|
|
|
|
while (retries < MAX_RETRIES) {
|
|
|
|
retries++;
|
|
|
|
res = db.adminCommand(cmd);
|
|
|
|
// If the command worked, exit the loop early.
|
|
|
|
if (res.ok) {
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
// Assert command worked or acceptable error occurred.
|
|
|
|
var msg = tojson({command: cmd, res: res});
|
|
|
|
assertWhenOwnColl(acceptableErrorOccurred(res.code, acceptableErrorCodes), msg);
|
|
|
|
|
|
|
|
// When an acceptable error occurs, sleep and then retry.
|
|
|
|
sleep(backoffSleep);
|
|
|
|
backoffSleep = getNextBackoffSleep(backoffSleep);
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
function splitChunkAtPoint(db, collName, splitPoint) {
|
|
|
|
var cmd = {split: db[collName].getFullName(), middle: {_id: splitPoint}};
|
|
|
|
var acceptableErrorCodes = [ErrorCodes.LockBusy];
|
|
|
|
return runCommandWithRetries(db, cmd, acceptableErrorCodes);
|
|
|
|
}
|
|
|
|
|
|
|
|
function splitChunkWithBounds(db, collName, bounds) {
|
|
|
|
var cmd = {split: db[collName].getFullName(), bounds: bounds};
|
|
|
|
var acceptableErrorCodes = [ErrorCodes.LockBusy];
|
|
|
|
return runCommandWithRetries(db, cmd, acceptableErrorCodes);
|
|
|
|
}
|
|
|
|
|
|
|
|
function moveChunk(db, collName, bounds, toShard, waitForDelete) {
|
|
|
|
var cmd = {
|
|
|
|
moveChunk: db[collName].getFullName(),
|
|
|
|
bounds: bounds,
|
|
|
|
to: toShard,
|
|
|
|
_waitForDelete: waitForDelete
|
|
|
|
};
|
|
|
|
var acceptableErrorCodes =
|
|
|
|
[ErrorCodes.ConflictingOperationInProgress, ErrorCodes.ChunkRangeCleanupPending];
|
|
|
|
return runCommandWithRetries(db, cmd, acceptableErrorCodes);
|
|
|
|
}
|
|
|
|
|
|
|
|
function mergeChunks(db, collName, bounds) {
|
|
|
|
var cmd = {mergeChunks: db[collName].getFullName(), bounds: bounds};
|
|
|
|
var acceptableErrorCodes = [ErrorCodes.LockBusy];
|
|
|
|
return runCommandWithRetries(db, cmd, acceptableErrorCodes);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Take a set of connections to a shard (replica set or standalone mongod),
|
|
|
|
// or a set of connections to the config servers, and return a connection
|
|
|
|
// to any node in the set for which ismaster is true.
|
|
|
|
function getPrimary(connArr) {
|
2019-05-13 19:53:27 +02:00
|
|
|
const kDefaultTimeoutMS = 10 * 60 * 1000; // 10 minutes.
|
2016-11-16 21:33:31 +01:00
|
|
|
assertAlways(Array.isArray(connArr), 'Expected an array but got ' + tojson(connArr));
|
|
|
|
|
2019-05-13 19:53:27 +02:00
|
|
|
let primary = null;
|
|
|
|
assert.soon(() => {
|
|
|
|
for (let conn of connArr) {
|
|
|
|
assert(isMongod(conn.getDB('admin')), tojson(conn) + ' is not to a mongod');
|
|
|
|
let res = conn.adminCommand({isMaster: 1});
|
|
|
|
assertAlways.commandWorked(res);
|
|
|
|
|
|
|
|
if (res.ismaster) {
|
|
|
|
primary = conn;
|
|
|
|
return primary;
|
|
|
|
}
|
2016-11-16 21:33:31 +01:00
|
|
|
}
|
2019-05-13 19:53:27 +02:00
|
|
|
}, 'Finding primary timed out', kDefaultTimeoutMS);
|
|
|
|
|
|
|
|
return primary;
|
2016-11-16 21:33:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Take a set of mongos connections to a sharded cluster and return a
|
|
|
|
// random connection.
|
|
|
|
function getRandomMongos(connArr) {
|
|
|
|
assertAlways(Array.isArray(connArr), 'Expected an array but got ' + tojson(connArr));
|
|
|
|
var conn = connArr[Random.randInt(connArr.length)];
|
|
|
|
assert(isMongos(conn.getDB('admin')), tojson(conn) + ' is not to a mongos');
|
|
|
|
return conn;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Intended for use on mongos connections only.
|
|
|
|
// Return all shards containing documents in [lower, upper).
|
|
|
|
function getShardsForRange(conn, collName, lower, upper) {
|
|
|
|
assert(isMongos(conn.getDB('admin')), tojson(conn) + ' is not to a mongos');
|
|
|
|
var adminDB = conn.getDB('admin');
|
|
|
|
var shardVersion = adminDB.runCommand({getShardVersion: collName, fullMetadata: true});
|
|
|
|
assertAlways.commandWorked(shardVersion);
|
|
|
|
// As noted in SERVER-20768, doing a range query with { $lt : X }, where
|
|
|
|
// X is the _upper bound_ of a chunk, incorrectly targets the shard whose
|
|
|
|
// _lower bound_ is X. Therefore, if upper !== MaxKey, we use a workaround
|
|
|
|
// to ensure that only the shard whose lower bound = X is targeted.
|
|
|
|
var query;
|
|
|
|
if (upper === MaxKey) {
|
|
|
|
query = {$and: [{_id: {$gte: lower}}, {_id: {$lt: upper}}]};
|
|
|
|
} else {
|
|
|
|
query = {$and: [{_id: {$gte: lower}}, {_id: {$lte: upper - 1}}]};
|
|
|
|
}
|
|
|
|
var res = conn.getCollection(collName).find(query).explain();
|
|
|
|
assertAlways.commandWorked(res);
|
|
|
|
assertAlways.gt(
|
|
|
|
res.queryPlanner.winningPlan.shards.length, 0, 'Explain did not have shards key.');
|
|
|
|
|
|
|
|
var shards = res.queryPlanner.winningPlan.shards.map(shard => shard.shardName);
|
|
|
|
return {shards: shards, explain: res, query: query, shardVersion: shardVersion};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the number of docs in [lower, upper) as seen by conn.
|
|
|
|
function getNumDocs(conn, collName, lower, upper) {
|
|
|
|
var coll = conn.getCollection(collName);
|
|
|
|
var query = {$and: [{_id: {$gte: lower}}, {_id: {$lt: upper}}]};
|
|
|
|
return coll.find(query).itcount();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Intended for use on config or mongos connections only.
|
|
|
|
// Get number of chunks containing values in [lower, upper). The upper bound on a chunk is
|
|
|
|
// exclusive, but to capture the chunk we must provide it with less than or equal to 'upper'.
|
2017-10-10 02:37:41 +02:00
|
|
|
function getNumChunks(conn, ns, lower, upper) {
|
2016-11-16 21:33:31 +01:00
|
|
|
assert(isMongos(conn.getDB('admin')) || isMongodConfigsvr(conn.getDB('admin')),
|
|
|
|
tojson(conn) + ' is not to a mongos or a mongod config server');
|
2017-10-10 02:37:41 +02:00
|
|
|
assert(isString(ns) && ns.indexOf('.') !== -1 && !ns.startsWith('.') && !ns.endsWith('.'),
|
|
|
|
ns + ' is not a valid namespace');
|
|
|
|
var query = {'ns': ns, 'min._id': {$gte: lower}, 'max._id': {$lte: upper}};
|
2016-11-16 21:33:31 +01:00
|
|
|
return conn.getDB('config').chunks.find(query).itcount();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Intended for use on config or mongos connections only.
|
|
|
|
// For getting chunks containing values in [lower, upper). The upper bound on a chunk is
|
|
|
|
// exclusive, but to capture the chunk we must provide it with less than or equal to 'upper'.
|
2017-10-10 02:37:41 +02:00
|
|
|
function getChunks(conn, ns, lower, upper) {
|
2016-11-16 21:33:31 +01:00
|
|
|
assert(isMongos(conn.getDB('admin')) || isMongodConfigsvr(conn.getDB('admin')),
|
|
|
|
tojson(conn) + ' is not to a mongos or a mongod config server');
|
2017-10-10 02:37:41 +02:00
|
|
|
assert(isString(ns) && ns.indexOf('.') !== -1 && !ns.startsWith('.') && !ns.endsWith('.'),
|
|
|
|
ns + ' is not a valid namespace');
|
|
|
|
var query = {'ns': ns, 'min._id': {$gte: lower}, 'max._id': {$lte: upper}};
|
2016-11-16 21:33:31 +01:00
|
|
|
return conn.getDB('config').chunks.find(query).sort({'min._id': 1}).toArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Intended for use on config or mongos connections only.
|
|
|
|
// For debug printing chunks containing values in [lower, upper). The upper bound on a chunk is
|
|
|
|
// exclusive, but to capture the chunk we must provide it with less than or equal to 'upper'.
|
|
|
|
function stringifyChunks(conn, lower, upper) {
|
|
|
|
assert(isMongos(conn.getDB('admin')) || isMongodConfigsvr(conn.getDB('admin')),
|
|
|
|
tojson(conn) + ' is not to a mongos or a mongod config server');
|
|
|
|
return getChunks(conn, lower, upper).map(chunk => tojson(chunk)).join('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
splitChunkAtPoint: splitChunkAtPoint,
|
|
|
|
splitChunkWithBounds: splitChunkWithBounds,
|
|
|
|
moveChunk: moveChunk,
|
|
|
|
mergeChunks: mergeChunks,
|
|
|
|
getPrimary: getPrimary,
|
|
|
|
getRandomMongos: getRandomMongos,
|
|
|
|
getShardsForRange: getShardsForRange,
|
|
|
|
getNumDocs: getNumDocs,
|
|
|
|
getNumChunks: getNumChunks,
|
|
|
|
getChunks: getChunks,
|
|
|
|
stringifyChunks: stringifyChunks
|
|
|
|
};
|
|
|
|
})();
|