mirror of https://gitee.com/bigwinds/arangodb
533 lines
17 KiB
JavaScript
533 lines
17 KiB
JavaScript
/* global ArangoServerState, ArangoClusterInfo, REPLICATION_SYNCHRONIZE_FINALIZE */
|
|
'use strict';
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief JavaScript cluster functionality
|
|
// /
|
|
// / @file
|
|
// /
|
|
// / DISCLAIMER
|
|
// /
|
|
// / Copyright 2012 triagens GmbH, Cologne, Germany
|
|
// /
|
|
// / Licensed under the Apache License, Version 2.0 (the "License")
|
|
// / you may not use this file except in compliance with the License.
|
|
// / You may obtain a copy of the License at
|
|
// /
|
|
// / http://www.apache.org/licenses/LICENSE-2.0
|
|
// /
|
|
// / Unless required by applicable law or agreed to in writing, software
|
|
// / distributed under the License is distributed on an "AS IS" BASIS,
|
|
// / WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// / See the License for the specific language governing permissions and
|
|
// / limitations under the License.
|
|
// /
|
|
// / Copyright holder is triAGENS GmbH, Cologne, Germany
|
|
// /
|
|
// / @author Jan Steemann
|
|
// / @author Copyright 2012, triAGENS GmbH, Cologne, Germany
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
var console = require('console');
|
|
var arangodb = require('@arangodb');
|
|
var ArangoError = arangodb.ArangoError;
|
|
var errors = require("internal").errors;
|
|
var wait = require('internal').wait;
|
|
var isEnterprise = require('internal').isEnterprise();
|
|
var _ = require('lodash');
|
|
|
|
var endpointToURL = function (endpoint) {
|
|
if (endpoint.substr(0, 6) === 'ssl://') {
|
|
return 'https://' + endpoint.substr(6);
|
|
}
|
|
var pos = endpoint.indexOf('://');
|
|
if (pos === -1) {
|
|
return 'http://' + endpoint;
|
|
}
|
|
return 'http' + endpoint.substr(pos);
|
|
};
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief throw an ArangoError
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
var raiseError = function (code, msg) {
|
|
var err = new ArangoError();
|
|
err.errorNum = code;
|
|
err.errorMessage = msg;
|
|
|
|
throw err;
|
|
};
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief retrieve a list of shards for a collection
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
var shardList = function (dbName, collectionName) {
|
|
let ci = global.ArangoClusterInfo.getCollectionInfo(dbName, collectionName);
|
|
|
|
if (ci === undefined || typeof ci !== 'object') {
|
|
throw "unable to determine shard list for '" + dbName + '/' + collectionName + "'";
|
|
}
|
|
|
|
let shards = [];
|
|
for (let shard in ci.shards) {
|
|
if (ci.shards.hasOwnProperty(shard)) {
|
|
shards.push(shard);
|
|
}
|
|
}
|
|
|
|
if (shards.length === 0 && isEnterprise) {
|
|
if (isEnterprise) {
|
|
return require('@arangodb/clusterEE').getSmartShards(dbName, collectionName, ci);
|
|
} else {
|
|
raiseError(arangodb.errors.ERROR_ARANGO_DATA_SOURCE_NOT_FOUND.code,
|
|
arangodb.errors.ERROR_ARANGO_DATA_SOURCE_NOT_FOUND.message);
|
|
}
|
|
}
|
|
|
|
return shards;
|
|
};
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief wait for a distributed response
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
var waitForDistributedResponse = function (data, numberOfRequests, ignoreHttpErrors) {
|
|
var received = [];
|
|
try {
|
|
while (received.length < numberOfRequests) {
|
|
var result = global.ArangoClusterComm.wait(data);
|
|
var status = result.status;
|
|
|
|
if (status === 'ERROR') {
|
|
raiseError(arangodb.errors.ERROR_INTERNAL.code,
|
|
'received an error from a DB server: ' + JSON.stringify(result));
|
|
} else if (status === 'TIMEOUT') {
|
|
raiseError(arangodb.errors.ERROR_CLUSTER_TIMEOUT.code,
|
|
arangodb.errors.ERROR_CLUSTER_TIMEOUT.message);
|
|
} else if (status === 'DROPPED') {
|
|
raiseError(arangodb.errors.ERROR_INTERNAL.code,
|
|
'the operation was dropped');
|
|
} else if (status === 'RECEIVED') {
|
|
received.push(result);
|
|
|
|
if (result.headers && result.headers.hasOwnProperty('x-arango-response-code')) {
|
|
var code = parseInt(result.headers['x-arango-response-code'].substr(0, 3), 10);
|
|
result.statusCode = code;
|
|
|
|
if (code >= 400 && !ignoreHttpErrors) {
|
|
var body;
|
|
|
|
try {
|
|
body = JSON.parse(result.body);
|
|
} catch (err) {
|
|
raiseError(arangodb.errors.ERROR_INTERNAL.code,
|
|
'error parsing JSON received from a DB server: ' + err.message);
|
|
}
|
|
|
|
raiseError(body.errorNum,
|
|
body.errorMessage);
|
|
}
|
|
}
|
|
} else {
|
|
// something else... wait without GC
|
|
require('internal').wait(0.1, false);
|
|
}
|
|
}
|
|
} finally {
|
|
global.ArangoClusterComm.drop(data);
|
|
}
|
|
return received;
|
|
};
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief whether or not clustering is enabled
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
var isCluster = function () {
|
|
var role = global.ArangoServerState.role();
|
|
|
|
return (role !== undefined && role !== 'SINGLE' && role !== 'AGENT');
|
|
};
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief whether or not we are a coordinator
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
var isCoordinator = function () {
|
|
return global.ArangoServerState.isCoordinator();
|
|
};
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief role
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
var role = function () {
|
|
var role = global.ArangoServerState.role();
|
|
|
|
if (role !== 'SINGLE') {
|
|
return role;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief status
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
var status = function () {
|
|
if (!isCluster() || !global.ArangoServerState.initialized()) {
|
|
return undefined;
|
|
}
|
|
|
|
return global.ArangoServerState.status();
|
|
};
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief coordinatorId
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
var coordinatorId = function () {
|
|
if (!isCoordinator()) {
|
|
console.topic('heartbeat=error', 'not a coordinator');
|
|
}
|
|
return global.ArangoServerState.id();
|
|
};
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief shard distribution
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
function format (x) {
|
|
var r = {};
|
|
var keys = Object.keys(x);
|
|
for (var i = 0; i < keys.length; ++i) {
|
|
var y = x[keys[i]];
|
|
r[keys[i]] = { leader: y[0], followers: y.slice(1) };
|
|
}
|
|
return r;
|
|
}
|
|
|
|
function shardDistribution () {
|
|
return {
|
|
results: require('internal').getShardDistribution()
|
|
};
|
|
}
|
|
|
|
function collectionShardDistribution (name) {
|
|
return {
|
|
results: require('internal').getCollectionShardDistribution(name)
|
|
};
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief move shard
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
function moveShard (info) {
|
|
var isLeader;
|
|
var collInfo;
|
|
try {
|
|
// First translate server names from short names to long names:
|
|
var servers = global.ArangoClusterInfo.getDBServers();
|
|
for (let i = 0; i < servers.length; i++) {
|
|
if (servers[i].serverId !== info.fromServer) {
|
|
if (servers[i].serverName === info.fromServer) {
|
|
info.fromServer = servers[i].serverId;
|
|
}
|
|
}
|
|
if (servers[i].serverId !== info.toServer) {
|
|
if (servers[i].serverName === info.toServer) {
|
|
info.toServer = servers[i].serverId;
|
|
}
|
|
}
|
|
}
|
|
collInfo = global.ArangoClusterInfo.getCollectionInfo(info.database,
|
|
info.collection);
|
|
if (collInfo.distributeShardsLike !== undefined) {
|
|
return {error:true, errorMessage:'MoveShard only allowed for collections which have distributeShardsLike unset.'};
|
|
}
|
|
var shards = collInfo.shards;
|
|
var shard = shards[info.shard];
|
|
var pos = shard.indexOf(info.fromServer);
|
|
if (pos === -1) {
|
|
throw 'Banana';
|
|
} else if (pos === 0) {
|
|
isLeader = true;
|
|
} else {
|
|
isLeader = false;
|
|
}
|
|
} catch (e2) {
|
|
return {error:true, errorMessage:'Combination of database, collection, shard and fromServer does not make sense.'};
|
|
}
|
|
|
|
var id;
|
|
try {
|
|
id = global.ArangoClusterInfo.uniqid();
|
|
var todo = { 'type': 'moveShard',
|
|
'database': info.database,
|
|
'collection': collInfo.id,
|
|
'shard': info.shard,
|
|
'fromServer': info.fromServer,
|
|
'toServer': info.toServer,
|
|
'jobId': id,
|
|
'timeCreated': (new Date()).toISOString(),
|
|
'creator': ArangoServerState.id(),
|
|
'isLeader': isLeader,
|
|
'remainsFollower': isLeader};
|
|
global.ArangoAgency.set('Target/ToDo/' + id, todo);
|
|
} catch (e1) {
|
|
return {error: true, errorMessage: 'Cannot write to agency.'};
|
|
}
|
|
|
|
return {error: false, id: id};
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief rebalance shards
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
function rebalanceShards () {
|
|
var dbServers = global.ArangoClusterInfo.getDBServers();
|
|
var dbTab = {};
|
|
var i, j, k, l;
|
|
for (i = 0; i < dbServers.length; ++i) {
|
|
dbTab[dbServers[i].serverId] = [];
|
|
}
|
|
var shardMap = {};
|
|
|
|
// First count and collect:
|
|
var db = require('internal').db;
|
|
|
|
var colls = db._collections();
|
|
for (j = 0; j < colls.length; ++j) {
|
|
var collName = colls[j].name();
|
|
if (collName.substr(0, 1) === '_') {
|
|
continue;
|
|
}
|
|
var collInfo = global.ArangoClusterInfo.getCollectionInfo(
|
|
db._name(), collName);
|
|
if (collInfo.distributeShardsLike === undefined) {
|
|
// Only consider those collections that do not follow another one
|
|
// w.r.t. their shard distribution.
|
|
var shardNames = Object.keys(collInfo.shards);
|
|
for (k = 0; k < shardNames.length; k++) {
|
|
var shardName = shardNames[k];
|
|
shardMap[shardName] = {
|
|
database: db._name(), collection: collName,
|
|
servers: collInfo.shards[shardName], weight: 1 };
|
|
dbTab[collInfo.shards[shardName][0]].push(
|
|
{ shard: shardName, leader: true,
|
|
weight: shardMap[shardName].weight });
|
|
for (l = 1; l < collInfo.shards[shardName].length; ++l) {
|
|
dbTab[collInfo.shards[shardName][l]].push(
|
|
{ shard: shardName, leader: false,
|
|
weight: shardMap[shardName].weight });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.topic('heartbeat=debug', "Rebalancing shards");
|
|
console.topic('heartbeat=debug', shardMap);
|
|
console.topic('heartbeat=debug', dbTab);
|
|
|
|
// Compute total weight for each DBServer:
|
|
var totalWeight = [];
|
|
for (i = 0; i < dbServers.length; ++i) {
|
|
totalWeight.push({'server': dbServers[i].serverId,
|
|
'weight': _.reduce(dbTab[dbServers[i].serverId],
|
|
(sum, x) => sum + x.weight, 0)});
|
|
}
|
|
totalWeight = _.sortBy(totalWeight, x => x.weight);
|
|
|
|
var shardList = Object.keys(shardMap);
|
|
var countMoved = 0;
|
|
|
|
for (i = 0; i < shardList.length; i++) {
|
|
var last = totalWeight.length - 1;
|
|
var fullest = totalWeight[last].server;
|
|
var emptiest = totalWeight[0].server;
|
|
var weightDiff = totalWeight[last].weight - totalWeight[0].weight;
|
|
if (weightDiff < 1.0) {
|
|
console.topic('heartbeat=info', 'rebalanceShards: cluster is balanced');
|
|
return true;
|
|
}
|
|
var shard = shardList[i];
|
|
console.topic('heartbeat=info', 'rebalanceShards: considering shard', shard,
|
|
'totalWeight=', totalWeight);
|
|
if (shardMap[shard].servers.indexOf(fullest) >= 0 &&
|
|
shardMap[shard].servers.indexOf(emptiest) === -1 &&
|
|
shardMap[shard].weight < 0.9 * weightDiff) {
|
|
var shardInfo = shardMap[shard];
|
|
var todo = { database: shardInfo.database,
|
|
collection: shardInfo.collection,
|
|
shard: shard,
|
|
fromServer: fullest,
|
|
toServer: emptiest };
|
|
var res = moveShard(todo);
|
|
if (!res.error) {
|
|
console.topic('heartbeat=debug', 'rebalanceShards: moveShard(', todo, ')');
|
|
totalWeight[last].weight -= shardInfo.weight;
|
|
totalWeight[0].weight += shardInfo.weight;
|
|
totalWeight = _.sortBy(totalWeight, x => x.weight);
|
|
countMoved += 1;
|
|
if (countMoved >= 10) {
|
|
break;
|
|
}
|
|
} else {
|
|
console.topic('heartbeat=error', 'rebalanceShards: moveShard(', todo, ') produced:',
|
|
res.errorMessage);
|
|
}
|
|
}
|
|
}
|
|
if (countMoved === 0) {
|
|
console.topic('heartbeat=info', 'rebalanceShards: no sensible moves found');
|
|
return true;
|
|
}
|
|
console.topic('heartbeat=info', 'rebalanceShards: scheduled', countMoved, ' shard moves.');
|
|
return true;
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief supervision state
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
function supervisionState () {
|
|
try {
|
|
var result = global.ArangoAgency.get('Target');
|
|
result = result.arango.Target;
|
|
var proj = { ToDo: result.ToDo, Pending: result.Pending,
|
|
Failed: result.Failed, Finished: result.Finished,
|
|
error: false };
|
|
return proj;
|
|
} catch (err) {
|
|
return { error: true, errorMsg: 'could not read /Target in agency',
|
|
exception: err };
|
|
}
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
// / @brief wait for synchronous replication to settle
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
function checkForSyncReplOneCollection (dbName, collName) {
|
|
try {
|
|
let cinfo = global.ArangoClusterInfo.getCollectionInfo(dbName, collName);
|
|
let shards = Object.keys(cinfo.shards);
|
|
let ccinfo = shards.map(function (s) {
|
|
return global.ArangoClusterInfo.getCollectionInfoCurrent(dbName,
|
|
collName, s).servers;
|
|
});
|
|
console.topic('heartbeat=debug', 'checkForSyncReplOneCollection:', dbName, collName, shards,
|
|
cinfo.shards, ccinfo);
|
|
let ok = true;
|
|
for (let i = 0; i < shards.length; ++i) {
|
|
if (cinfo.shards[shards[i]].length !== ccinfo[i].length) {
|
|
ok = false;
|
|
}
|
|
}
|
|
if (ok) {
|
|
console.topic('heartbeat=debug', 'checkForSyncReplOneCollection: OK:', dbName, collName,
|
|
shards);
|
|
return true;
|
|
}
|
|
console.topic('heartbeat=debug', 'checkForSyncReplOneCollection: not yet:', dbName, collName,
|
|
shards);
|
|
return false;
|
|
} catch (err) {
|
|
console.topic('heartbeat=error', 'checkForSyncReplOneCollection: exception:', dbName, collName,
|
|
JSON.stringify(err));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function waitForSyncRepl (dbName, collList) {
|
|
if (!isCoordinator()) {
|
|
return true;
|
|
}
|
|
let n = collList.length;
|
|
let count = 30 * n; // wait for up to 30 * collList.length seconds
|
|
// usually, this is much faster, but under load
|
|
// when many unittests run, things may take longer
|
|
let ok = [...Array(n)].map(v => false);
|
|
while (--count > 0) {
|
|
let allOk = true;
|
|
for (var i = 0; i < n; ++i) {
|
|
if (!ok[i]) {
|
|
ok[i] = checkForSyncReplOneCollection(dbName, collList[i].name());
|
|
allOk = allOk && ok[i];
|
|
}
|
|
}
|
|
if (allOk) {
|
|
return true;
|
|
}
|
|
require('internal').wait(1, false);
|
|
}
|
|
console.topic('heartbeat=warn', 'waitForSyncRepl: timeout:', dbName, collList);
|
|
return false;
|
|
}
|
|
|
|
function endpoints() {
|
|
try {
|
|
let coords = global.ArangoClusterInfo.getCoordinators();
|
|
let endpoints = coords.map(c => global.ArangoClusterInfo.getServerEndpoint(c));
|
|
return { "endpoints": endpoints.map(function(e) {
|
|
return {"endpoint": e};
|
|
}) };
|
|
} catch (err) {
|
|
return { error: true, exception: err };
|
|
}
|
|
}
|
|
|
|
function queryAgencyJob(id) {
|
|
let job = null;
|
|
let states = ["Finished", "Pending", "Failed", "ToDo"];
|
|
for (let s of states) {
|
|
try {
|
|
job = global.ArangoAgency.get('Target/' + s + '/' + id);
|
|
job = job.arango.Target[s];
|
|
if (Object.keys(job).length !== 0 && job.hasOwnProperty(id)) {
|
|
return {error: false, id, status: s, job: job[id]};
|
|
}
|
|
} catch (err) {
|
|
}
|
|
}
|
|
return {error: true, errorMsg: "Did not find job.", id, job: null};
|
|
}
|
|
|
|
function getLocalInfo () {
|
|
var db = require('internal').db;
|
|
var ret = { result: {}};
|
|
db._collections().forEach(
|
|
function(col) {
|
|
if (col.name().charAt(0) !== '_') {
|
|
ret.result[col.name()] = col.properties();
|
|
ret.result[col.name()].indexes = [];
|
|
col.getIndexes().forEach(function(i) {
|
|
ret.result[col.name()].indexes.push(i);
|
|
});
|
|
}
|
|
});
|
|
return ret;
|
|
}
|
|
|
|
exports.coordinatorId = coordinatorId;
|
|
exports.isCluster = isCluster;
|
|
exports.isCoordinator = isCoordinator;
|
|
exports.role = role;
|
|
exports.shardList = shardList;
|
|
exports.status = status;
|
|
exports.wait = waitForDistributedResponse;
|
|
exports.endpointToURL = endpointToURL;
|
|
exports.shardDistribution = shardDistribution;
|
|
exports.collectionShardDistribution = collectionShardDistribution;
|
|
exports.rebalanceShards = rebalanceShards;
|
|
exports.moveShard = moveShard;
|
|
exports.supervisionState = supervisionState;
|
|
exports.waitForSyncRepl = waitForSyncRepl;
|
|
exports.endpoints = endpoints;
|
|
exports.queryAgencyJob = queryAgencyJob;
|
|
exports.getLocalInfo = getLocalInfo;
|