1
0
Fork 0
arangodb/js/server/modules/@arangodb/cluster/agency-communication.js

835 lines
25 KiB
JavaScript

/*jshint unused: false */
/*global ArangoAgency */
////////////////////////////////////////////////////////////////////////////////
/// @brief Agency Communication
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2013 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 Michael Hackstein
/// @author Copyright 2013, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
exports.Communication = function() {
'use strict';
var agency,
AgencyWrapper,
splitServerName,
storeServersInCache,
Target,
mapCollectionIDsToNames,
updateCollectionRouteForName,
updateDatabaseRoutes,
self = this,
_ = require("lodash");
splitServerName = function(route) {
var splits = route.split("/");
return splits[splits.length - 1];
};
mapCollectionIDsToNames = function(list) {
var res = {};
_.each(list, function(v, k) {
var n = splitServerName(k);
if (n === "Lock" || n === "Version") {
return;
}
res[v.name] = n;
});
return res;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Wrapper for Agency
///
/// Creates access to routes via objects instead of route strings.
/// And defines specific operations allowed on these routes.
////////////////////////////////////////////////////////////////////////////////
AgencyWrapper = function() {
var _agency = exports._createAgency();
var stubs = {
get: function(route, recursive) {
return _agency.get(route, recursive);
},
getValue: function(route, name) {
var res = _agency.get(route + "/" + name);
return _.values(res)[0];
},
set: function(route, name, value) {
if (value !== undefined) {
return _agency.set(route + "/" + name, value);
}
return _agency.set(route, name);
},
remove: function(route, name) {
return _agency.remove(route + "/" + name);
},
checkVersion: function(route) {
return false;
},
list: function(route) {
return _.map(_agency.list(route, false, true), splitServerName).sort();
}
};
var addLevel = function(base, name, route, functions) {
var newRoute = base.route;
if (newRoute) {
newRoute += "/";
} else {
newRoute = "";
}
newRoute += route;
var newLevel = {
route: newRoute
};
_.each(functions, function(f) {
newLevel[f] = stubs[f].bind(null, newLevel.route);
});
base[name] = newLevel;
return newLevel;
};
var target = addLevel(this, "target", "Target");
addLevel(target, "dbServers", "DBServers", ["get", "set", "remove", "checkVersion"]);
addLevel(target, "db", "Databases", ["list"]);
addLevel(target, "coordinators", "Coordinators", ["list", "get", "set", "remove", "checkVersion"]);
addLevel(target, "endpoints", "MapIDToEndpoint", ["getValue"]);
var plan = addLevel(this, "plan", "Plan");
addLevel(plan, "dbServers", "DBServers", ["get", "checkVersion"]);
addLevel(plan, "db", "Databases", ["list"]);
addLevel(plan, "coordinators", "Coordinators", ["list", "get", "checkVersion"]);
var current = addLevel(this, "current", "Current");
addLevel(current, "dbServers", "DBServers", ["get", "checkVersion"]);
addLevel(current, "db", "Databases", ["list"]);
addLevel(current, "coordinators", "Coordinators", ["list", "get", "checkVersion"]);
addLevel(current, "registered", "ServersRegistered", ["get", "checkVersion"]);
var sync = addLevel(this, "sync", "Sync");
addLevel(sync, "beat", "ServerStates", ["get"]);
addLevel(sync, "interval", "HeartbeatIntervalMs", ["get"]);
this.addLevel = addLevel;
};
agency = new AgencyWrapper();
updateDatabaseRoutes = function(base, writeAccess) {
var list = self.plan.Databases().getList();
_.each(_.keys(base), function(k) {
if (k !== "route" && k !== "list") {
delete base[k];
}
});
var oldRoute = base.route;
base.route = base.route.replace("Databases", "Collections");
_.each(list, function(d) {
agency.addLevel(base, d, d, ["get", "checkVersion"]);
});
base.route = oldRoute;
};
updateCollectionRouteForName = function(route, db, name, writeAccess) {
var list = self.plan.Databases().select(db).getCollectionObjects();
var cId = null;
_.each(list, function(v, k) {
if (v.name === name) {
cId = splitServerName(k);
}
});
var acts = ["get"];
if (writeAccess) {
acts.push("set");
}
agency.addLevel(route, name, cId, acts);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Stores database servers in cache
///
/// Stores a list of database servers into a cache object.
/// It will convert their roles accordingly.
////////////////////////////////////////////////////////////////////////////////
storeServersInCache = function(place, servers) {
_.each(servers, function(v, k) {
var pName = splitServerName(k);
place[pName] = place[pName] || {};
place[pName].role = "primary";
if (v !== "none") {
place[v] = {
role: "secondary"
};
place[pName] = place[pName] || {};
place[pName].secondary = v;
}
});
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Object for DBServer configuration
///
/// Allows to list all database servers for the given hierarchy level.
/// If write access is granted also options to add
/// and remove servers are allowed.
////////////////////////////////////////////////////////////////////////////////
var DBServersObject = function(route, endpoints, writeAccess) {
var cache = {};
var servers;
var getList = function() {
if (!route.checkVersion()) {
cache = {};
servers = route.get(true);
storeServersInCache(cache, servers);
}
return cache;
};
this.getList = function() {
return getList();
};
this.getEndpoint = function(name) {
return endpoints.getValue(name).split("://")[1];
};
this.getProtocol = function(name) {
return endpoints.getValue(name).split("://")[0]
.replace("tcp", "http").replace("ssl","https");
};
if (writeAccess) {
this.addPrimary = function(name) {
return route.set(name, "none");
};
this.addSecondary = function(name, primaryName) {
return route.set(primaryName, name);
};
this.addPair = function(primaryName, secondaryName) {
return route.set(primaryName, secondaryName);
};
this.removeServer = function(name) {
var res = -1;
_.each(getList(), function(opts, n) {
if (n === name) {
// The removed server is a primary
if (opts.role === "primary") {
res = route.remove(name);
if (!res) {
res = -1;
return;
}
if (opts.secondary !== "none") {
res = route.set(opts.secondary, "none");
}
return;
}
}
if (opts.role === "primary" && opts.secondary === name) {
res = route.set(n, "none");
return;
}
});
return res;
};
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Object for Coordinator configuration
///
/// Allows to list all coordinators for the given hierarchy level.
/// If write access is granted also options to add
/// and remove servers are allowed.
////////////////////////////////////////////////////////////////////////////////
var CoordinatorsObject = function(route, endpoints, writeAccess) {
var cache = {};
var servers;
var getList = function() {
if (!route.checkVersion()) {
cache = {};
servers = route.get(true);
storeServersInCache(cache, servers);
}
return cache;
};
this.getList = function() {
return getList();
//return route.list();
};
this.getEndpoint = function(name) {
return endpoints.getValue(name).split("://")[1];
};
this.getProtocol = function(name) {
return endpoints.getValue(name).split("://")[0]
.replace("tcp", "http").replace("ssl","https");
};
if (writeAccess) {
this.add = function(name) {
return route.set(name, "none");
};
this.remove = function(name) {
return route.remove(name);
};
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Object for Collection configuration
///
/// Allows to collect the information stored about a collection.
/// This includes indices, journalSize etc. and also shards.
/// Also convenience functions for shards are added,
/// allowing to get a mapping server -> shards
/// and a mapping shard -> server.
/// If write access is granted also an option to move a shard is added.
////////////////////////////////////////////////////////////////////////////////
var ColObject = function(route, writeAccess) {
this.info = function() {
var res = route.get();
return _.values(res)[0];
};
this.getShards = function() {
var info = this.info();
if (!info) {
return;
}
return info.shards;
};
this.getShardsByServers = function() {
var list = this.getShards();
var res = {};
_.each(list, function(v, k) {
res[v] = res[v] || {
shards: [],
name: v
};
res[v].shards.push(k);
});
var resList = [];
_.each(res, function(v) { resList.push(v);});
return resList;
};
this.getShardsForServer = function(name) {
var list = this.getShards();
var res = [];
_.each(list, function(v, k) {
if (v === name) {
res.push(k);
}
});
return res;
};
this.getServerForShard = function(name) {
var list = this.getShards();
return list[name];
};
if (writeAccess) {
this.moveShard = function(shard, target) {
var toUpdate = this.info();
toUpdate.shards[shard] = target;
return route.set(toUpdate);
};
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Object for content of a Database
///
/// Allows to collect the information stored about a database.
/// It allos to get a list of collections and to select one of them for
/// further information.
////////////////////////////////////////////////////////////////////////////////
var DBObject = function(route, db, writeAccess) {
var cache;
var getRaw = function() {
if (!cache || !route.checkVersion()) {
cache = route.get(true);
}
return cache;
};
var getList = function() {
return _.keys(mapCollectionIDsToNames(
self.plan.Databases().select(db).getCollectionObjects()
)).sort();
};
this.getCollectionObjects = function() {
return getRaw();
};
this.getCollections = function() {
return getList();
};
this.collection = function(name) {
updateCollectionRouteForName(route, db, name, writeAccess);
var colroute = route[name];
if (!colroute) {
return false;
}
return new ColObject(colroute, writeAccess);
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Object for Database configuration
///
/// Allows to list all added databases.
/// Also allows to select one database for further information.
////////////////////////////////////////////////////////////////////////////////
var DatabasesObject = function(route, writeAccess) {
this.getList = function() {
return route.list();
};
this.select = function(name) {
updateDatabaseRoutes(route, writeAccess);
var subroute = route[name];
if (!subroute) {
return false;
}
return new DBObject(subroute, name, writeAccess);
};
};
// Not yet defined
////////////////////////////////////////////////////////////////////////////////
/// @brief Access point for "Target" level.
///
/// Gives access to all information stored in the target level.
/// This includes DBServers, Databases and Coordinators.
/// Also grants write access.
////////////////////////////////////////////////////////////////////////////////
Target = function() {
var DBServers;
var Databases;
var Coordinators;
this.DBServers = function() {
if (!DBServers) {
DBServers = new DBServersObject(agency.target.dbServers, agency.target.endpoints, true);
}
return DBServers;
};
this.Databases = function() {
if (!Databases) {
Databases = new DatabasesObject(agency.target.db, true);
}
return Databases;
};
this.Coordinators = function() {
if (!Coordinators) {
Coordinators = new CoordinatorsObject(agency.target.coordinators, agency.target.endpoints, true);
}
return Coordinators;
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Access point for "Plan" level.
///
/// Gives access to all information stored in the plan level.
/// This includes DBServers, Databases and Coordinators.
/// Does not grant write access.
////////////////////////////////////////////////////////////////////////////////
var Plan = function () {
var DBServers;
var Databases;
var Coordinators;
this.DBServers = function() {
if (!DBServers) {
DBServers = new DBServersObject(agency.plan.dbServers, agency.target.endpoints);
}
return DBServers;
};
this.Databases = function() {
if (!Databases) {
Databases = new DatabasesObject(agency.plan.db);
}
return Databases;
};
this.Coordinators = function() {
if (!Coordinators) {
Coordinators = new CoordinatorsObject(agency.plan.coordinators, agency.target.endpoints);
}
return Coordinators;
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Access point for "Current" level.
///
/// Gives access to all information stored in the current level.
/// This includes DBServers, Databases and Coordinators.
/// Furthermore this consideres IP addresses of all servers.
/// Does not grant write access.
////////////////////////////////////////////////////////////////////////////////
var Current = function () {
var DBServers;
var Databases;
var Coordinators;
var DBServersObject = function() {
var cache = {};
var servers;
var getList = function() {
if (
!agency.current.dbServers.checkVersion()
|| !agency.current.registered.checkVersion()
) {
cache = {};
servers = agency.current.dbServers.get(true);
storeServersInCache(cache, servers);
var addresses = agency.current.registered.get(true);
_.each(addresses, function(v, k) {
var pName = splitServerName(k);
if (cache[pName]) {
cache[pName].address = v.endpoint.split("://")[1];
cache[pName].protocol = v.endpoint.split("://")[0]
.replace("tcp", "http").replace("ssl","https");
}
});
}
return cache;
};
this.getList = function() {
return getList();
};
};
var CoordinatorsObject = function() {
var cache;
var servers;
this.getList = function() {
if (
!agency.current.coordinators.checkVersion()
|| !agency.current.registered.checkVersion()
) {
cache = {};
servers = agency.current.coordinators.get(true);
_.each(servers, function(v, k) {
var pName = splitServerName(k);
cache[pName] = {};
});
var addresses = agency.current.registered.get(true);
_.each(addresses, function(v, k) {
var pName = splitServerName(k);
if (cache[pName]) {
cache[pName].address = v.endpoint.split("://")[1];
cache[pName].protocol = v.endpoint.split("://")[0]
.replace("tcp", "http").replace("ssl","https");
}
});
}
return cache;
};
};
this.DBServers = function() {
if (!DBServers) {
DBServers = new DBServersObject();
}
return DBServers;
};
this.Databases = function() {
if (!Databases) {
Databases = new DatabasesObject(agency.current.db);
}
return Databases;
};
this.Coordinators = function() {
if (!Coordinators) {
Coordinators = new CoordinatorsObject();
}
return Coordinators;
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Access point for "Sync" level.
///
/// Gives access to all information stored in the sync level.
/// This includes the heartbeat of the servers.
/// Offers convenience functions to get serving primaries,
/// list of primaries and secondaries inSync and out of sync
/// and a list of all inactive servers (servers that do not serve
/// or act as secondary).
/// Furthermore offers a function to check if a server has not send a heartbeat
/// in time.
/// Does not grant write access.
////////////////////////////////////////////////////////////////////////////////
var Sync = function() {
var Heartbeats;
var interval = agency.sync.interval.get();
interval = _.values(interval)[0];
var didBeatInTime = function(time) {
};
var isInSync = function(status) {
return (status === "SERVINGSYNC" || status === "INSYNC");
};
var isOutSync = function(status) {
return (status === "SERVINGASYNC" || status === "SYNCING");
};
var isServing = function(status) {
return (status === "SERVINGASYNC" || status === "SERVINGSYNC");
};
var isInactive = function(status) {
return !isInSync(status) && !isOutSync(status);
};
var HeartbeatsObject = function() {
this.list = function() {
var res = agency.sync.beat.get(true);
_.each(res, function(v, k) {
delete res[k];
res[splitServerName(k)] = v;
});
return res;
};
this.getInactive = function() {
var list = this.list();
var res = [];
_.each(list, function(v, k) {
if (isInactive(v.status)) {
res.push(k);
}
});
return res.sort();
};
this.getServing = function() {
var list = this.list();
var res = [];
_.each(list, function(v, k) {
if (isServing(v.status)) {
res.push(k);
}
});
return res.sort();
};
this.getInSync = function() {
var list = this.list();
var res = [];
_.each(list, function(v, k) {
if (isInSync(v.status)) {
res.push(k);
}
});
return res.sort();
};
this.getOutSync = function() {
var list = this.list();
var res = [];
_.each(list, function(v, k) {
if (isOutSync(v.status)) {
res.push(k);
}
});
return res.sort();
};
this.noBeat = function() {
// Do not use, will only work in highly synced clocks
var lastAccepted = new Date((new Date()).getTime() - (2 * interval));
var res = [];
var list = this.list();
_.each(list, function(v, k) {
if (new Date(v.time) < lastAccepted) {
res.push(k);
}
});
return res.sort();
};
this.didBeat = function() {
return _.keys(this.list());
};
};
this.Heartbeats = function() {
if (!Heartbeats) {
Heartbeats = new HeartbeatsObject();
}
return Heartbeats;
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Access point to visualize differences between levels.
///
/// Gives convenient access to compute discrepancies between levels.
/// It allows to get differences between target and plan.
/// (These require a user to step in, the managers do not have enough
/// resources to reach the target).
/// And the differences between plan and current.
/// (These do not require a user to step in, its the managers duty)
/// Supports differences for DBServers and Coordinators.
/// Databases not yet included.
////////////////////////////////////////////////////////////////////////////////
var Diff = function(target, plan, current) {
var DiffObject = function(supRoute, infRoute, supName) {
var infName;
switch (supName) {
case "target":
infName = "plan";
break;
case "plan":
infName = "current";
break;
default:
throw "Sorry please give a correct superior name";
}
var difference = function(superior, inferior, getEndpoint, getProtocol) {
var diff = {
missing: [],
difference: {}
};
var comp;
// diff of plan
if (_.isArray(superior)) {
comp = inferior;
if (!_.isArray(inferior)) {
// Current stores ips no array
comp = _.keys(inferior);
}
_.each(superior, function(v) {
if (!_.contains(comp, v)) {
var toAdd = {
name: v,
address: getEndpoint(v),
protocol: getProtocol(v)
};
diff.missing.push(toAdd);
}
});
return diff;
}
// diff of current
_.each(superior, function(v, k) {
if (!inferior.hasOwnProperty(k)) {
var toAdd = {
name: k,
role: v.role,
address: getEndpoint(k),
protocol: getProtocol(k)
};
diff.missing.push(toAdd);
return;
}
var compTo = _.extend({}, inferior[k]);
delete compTo.address;
if (JSON.stringify(v) !== JSON.stringify(compTo)) {
diff.difference[k] = {};
diff.difference[k][supName] = v;
diff.difference[k][infName] = inferior[k];
}
});
return diff;
};
this.DBServers = function() {
return difference(supRoute.DBServers().getList(),
infRoute.DBServers().getList(),
supRoute.DBServers().getEndpoint,
supRoute.DBServers().getProtocol);
};
this.Coordinators = function() {
return difference(supRoute.Coordinators().getList(),
infRoute.Coordinators().getList(),
supRoute.Coordinators().getEndpoint,
supRoute.Coordinators().getProtocol);
};
};
this.plan = new DiffObject(target, plan, "target");
this.current = new DiffObject(plan, current, "plan");
};
this.target = new Target();
this.plan = new Plan();
this.current = new Current();
this.sync = new Sync();
this.diff = new Diff(this.target, this.plan, this.current);
this.addPrimary = this.target.DBServers().addPrimary;
this.addSecondary = this.target.DBServers().addSecondary;
this.addCoordinator = this.target.Coordinators().add;
this.addPair = this.target.DBServers().addPair;
this.removeServer = function(name) {
var res = this.target.DBServers().removeServer(name);
if (res === -1) {
return this.target.Coordinators().remove(name);
}
return res;
};
};
////////////////////////////////////////////////////////////////////////////////
/// @start Docu Block JSF_agency-communication_agency
/// @brief A wrapper around the Agency initialization
///
/// @FUN{_createAgency()}
///
/// This returns a singleton instance for the agency or creates it.
///
/// *Examples*
///
/// @code
/// agency = communication._createAgency();
/// @endcode
/// @end Docu Block
////////////////////////////////////////////////////////////////////////////////
exports._createAgency = function() {
'use strict';
var agency;
if (agency) {
return agency;
}
agency = ArangoAgency;
return agency;
};