Implemented connection to cluster using gossip seeds

This commit is contained in:
Nicolas Dextraze
2016-10-15 15:41:25 -07:00
parent 4ea996781f
commit dd1302f641
10 changed files with 351 additions and 303 deletions

View File

@ -44,6 +44,7 @@ module.exports.UserCredentials = require('./systemData/userCredentials');
module.exports.EventData = EventData;
module.exports.PersistentSubscriptionSettings = require('./persistentSubscriptionSettings');
module.exports.SystemConsumerStrategies = require('./systemConsumerStrategies');
module.exports.GossipSeed = require('./gossipSeed');
// Exporting errors
module.exports.WrongExpectedVersionError = require('./errors/wrongExpectedVersionError');
module.exports.StreamDeletedError = require('./errors/streamDeletedError');

View File

@ -0,0 +1,240 @@
var http = require('http');
var util = require('util');
var GossipSeed = require('../gossipSeed');
function NodeEndPoints(tcpEndPoint, secureTcpEndPoint) {
if (tcpEndPoint === null && secureTcpEndPoint === null) throw new Error('Both endpoints are null.');
Object.defineProperties(this, {
tcpEndPoint: {
enumerable: true,
value: tcpEndPoint
},
secureTcpEndPoint: {
enumerable: true,
value: secureTcpEndPoint
}
});
}
function ClusterDnsEndPointDiscoverer(log, clusterDns, maxDiscoverAttempts, managerExternalHttpPort, gossipSeeds, gossipTimeout) {
if (!clusterDns && (!gossipSeeds || gossipSeeds.length === 0)) throw new Error('Both clusterDns and gossipSeeds are null/empty.');
this._log = log;
this._clusterDns = clusterDns;
this._maxDiscoverAttempts = maxDiscoverAttempts;
this._managerExternalHttpPort = managerExternalHttpPort;
this._gossipSeeds = gossipSeeds;
this._gossipTimeout = gossipTimeout;
this._oldGossip = null;
}
ClusterDnsEndPointDiscoverer.prototype.discover = function(failedTcpEndPoint) {
var attempt = 1;
var self = this;
function discover(resolve, reject) {
self._discoverEndPoint(failedTcpEndPoint)
.then(function (endPoints) {
if (!endPoints)
self._log.info(util.format("Discovering attempt %d/%d failed: no candidate found.", attempt, self._maxDiscoverAttempts));
return endPoints;
})
.catch(function (exc) {
self._log.info(util.format("Discovering attempt %d/%d failed with error: %s.", attempt, self._maxDiscoverAttempts, exc));
})
.then(function (endPoints) {
if (endPoints)
return resolve(endPoints);
if (attempt++ === self._maxDiscoverAttempts)
return reject(new Error('Failed to discover candidate in ' + self._maxDiscoverAttempts + ' attempts.'));
setTimeout(discover, 500, resolve, reject);
});
}
return new Promise(function (resolve, reject) {
discover(resolve, reject);
});
};
/**
* Discover Cluster endpoints
* @param {Object} failedTcpEndPoint
* @returns {Promise.<NodeEndPoints>}
* @private
*/
ClusterDnsEndPointDiscoverer.prototype._discoverEndPoint = function (failedTcpEndPoint) {
try {
var gossipCandidates = this._oldGossip
? this._getGossipCandidatesFromOldGossip(this._oldGossip, failedTcpEndPoint)
: this._getGossipCandidatesFromDns();
var self = this;
var promise = Promise.resolve();
var j = 0;
for (var i = 0; i < gossipCandidates.length; i++) {
promise = promise.then(function (endPoints) {
if (endPoints) return endPoints;
return self._tryGetGossipFrom(gossipCandidates[j++])
.then(function (gossip) {
if (gossip === null || gossip.members === null || gossip.members.length === 0)
return;
var bestNode = self._tryDetermineBestNode(gossip.members);
if (bestNode !== null) {
self._oldGossip = gossip.members;
return bestNode;
}
});
});
}
return promise;
} catch (e) {
return Promise.reject(e);
}
};
ClusterDnsEndPointDiscoverer.prototype._getGossipCandidatesFromOldGossip = function (oldGossip, failedTcpEndPoint) {
if (failedTcpEndPoint === null) return oldGossip;
var gossipCandidates = oldGossip.filter(function(x) {
//TODO: failedTcpEndpoint.host might not be an ip
return (x.externalTcpPort !== failedTcpEndPoint.port && x.externalTcpIp !== failedTcpEndPoint.host);
});
return this._arrangeGossipCandidates(gossipCandidates);
};
ClusterDnsEndPointDiscoverer.prototype._arrangeGossipCandidates = function (members) {
var result = new Array(members.length);
var i = -1;
var j = members.length;
for (var k = 0; k < members.length; ++k)
{
if (members[k].state === 'Manager')
result[--j] = new GossipSeed({host: members[k].externalHttpIp, port: members[k].externalHttpPort});
else
result[++i] = new GossipSeed({host: members[k].externalHttpIp, port: members[k].externalHttpPort});
}
this._randomShuffle(result, 0, i); // shuffle nodes
this._randomShuffle(result, j, members.length - 1); // shuffle managers
return result;
};
ClusterDnsEndPointDiscoverer.prototype._getGossipCandidatesFromDns = function () {
var endpoints = [];
if(this._gossipSeeds !== null && this._gossipSeeds.length > 0)
{
endpoints = this._gossipSeeds;
}
else
{
//TODO: dns resolve
throw new Error('Not implemented.');
//endpoints = ResolveDns(_clusterDns).Select(x => new GossipSeed(new IPEndPoint(x, _managerExternalHttpPort))).ToArray();
}
this._randomShuffle(endpoints, 0, endpoints.length-1);
return endpoints;
};
ClusterDnsEndPointDiscoverer.prototype._tryGetGossipFrom = function (endPoint) {
var options = {
hostname: endPoint.endPoint.hostname,
port: endPoint.endPoint.port,
path: '/gossip?format=json'
};
if (endPoint.hostHeader) {
options.headers = {'Host': endPoint.hostHeader};
}
var self = this;
return new Promise(function (resolve, reject) {
try {
http
.request(options, function (res) {
var result = '';
if (res.statusCode !== 200) {
self._log.info('Trying to get gossip from', endPoint, 'failed with status code:', res.statusCode);
resolve();
return;
}
res.on('data', function (chunk) {
result += chunk.toString();
});
res.on('end', function () {
try {
result = JSON.parse(result);
} catch (e) {
return resolve();
}
resolve(result);
});
})
.setTimeout(self._gossipTimeout, function () {
self._log.info('Trying to get gossip from', endPoint, 'timed out.');
resolve();
})
.on('error', function (e) {
self._log.info('Trying to get gossip from', endPoint, 'failed with error:', e);
resolve();
})
.end();
} catch(e) {
reject(e);
}
});
};
const VNodeStates = {
'Initializing': 0,
'Unknown': 1,
'PreReplica': 2,
'CatchingUp': 3,
'Clone': 4,
'Slave': 5,
'PreMaster': 6,
'Master': 7,
'Manager': 8,
'ShuttingDown': 9,
'Shutdown': 10
};
ClusterDnsEndPointDiscoverer.prototype._tryDetermineBestNode = function (members) {
var notAllowedStates = [
'Manager',
'ShuttingDown',
'Shutdown'
];
var node = members
.filter(function (x) {
return (x.isAlive && notAllowedStates.indexOf(x.state) === -1);
})
.sort(function (a, b) {
return VNodeStates[b.state] - VNodeStates[a.state];
})[0];
if (!node)
{
//_log.Info("Unable to locate suitable node. Gossip info:\n{0}.", string.Join("\n", members.Select(x => x.ToString())));
return null;
}
var normTcp = {host: node.externalTcpIp, port: node.externalTcpPort};
var secTcp = node.externalSecureTcpPort > 0
? {host: externalTcpIp, port: node.externalSecureTcpPort}
: null;
this._log.info(util.format("Discovering: found best choice [%j,%j] (%s).", normTcp, secTcp == null ? "n/a" : secTcp, node.state));
return new NodeEndPoints(normTcp, secTcp);
};
function rndNext(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
}
ClusterDnsEndPointDiscoverer.prototype._randomShuffle = function (arr, i, j) {
if (i >= j)
return;
for (var k = i; k <= j; ++k)
{
var index = rndNext(k, j + 1);
var tmp = arr[index];
arr[index] = arr[k];
arr[k] = tmp;
}
};
module.exports = ClusterDnsEndPointDiscoverer;

View File

@ -624,7 +624,7 @@ EventStoreConnectionLogicHandler.prototype._timerTick = function() {
};
EventStoreConnectionLogicHandler.prototype._manageHeartbeats = function() {
if (this._connection == null) throw new Error();
if (this._connection == null) return;
var timeout = this._heartbeatInfo.isIntervalStage ? this._settings.heartbeatInterval : this._settings.heartbeatTimeout;
if (Date.now() - this._heartbeatInfo.timeStamp < timeout)

View File

@ -1,6 +1,8 @@
var EventStoreNodeConnection = require('./eventStoreNodeConnection');
var StaticEndpointDiscoverer = require('./core/staticEndpointDiscoverer');
var ClusterDnsEndPointDiscoverer = require('./core/clusterDnsEndPointDiscoverer');
var NoopLogger = require('./common/log/noopLogger');
var ensure = require('./common/utils/ensure');
var defaultConnectionSettings = {
log: new NoopLogger(),
@ -25,7 +27,13 @@ var defaultConnectionSettings = {
failOnNoServerResponse: false,
heartbeatInterval: 750,
heartbeatTimeout: 1500,
clientConnectionTimeout: 1000
clientConnectionTimeout: 1000,
// Cluster Settings
clusterDns: '',
maxDiscoverAttemps: 10,
externalGossipPort: 0,
gossipTimeout: 1000
};
@ -40,24 +48,63 @@ function merge(a,b) {
return c;
}
function createFromTcpEndpoint(settings, tcpEndpoint, connectionName) {
if (!tcpEndpoint.port || !tcpEndpoint.hostname) throw new TypeError('endPoint object must have hostname and port properties.');
var mergedSettings = merge(defaultConnectionSettings, settings || {});
var endpointDiscoverer = new StaticEndpointDiscoverer(tcpEndpoint, settings.useSslConnection);
return new EventStoreNodeConnection(mergedSettings, null, endpointDiscoverer, connectionName || null);
}
function createFromStringEndpoint(settings, endPoint, connectionName) {
var m = endPoint.match(/^(tcp|discover):\/\/([^:]):?(\d+)?$/);
if (!m) throw new Error('endPoint string must be tcp://hostname[:port] or discover://dns[:port]');
var scheme = m[1];
var hostname = m[2];
var port = m[3] ? parseInt(m[3]) : 1113;
if (scheme === 'tcp') {
var tcpEndpoint = {
hostname: hostname,
port: port
};
return createFromTcpEndpoint(settings, tcpEndpoint, connectionName);
}
if (scheme === 'discover') {
throw new Error('Not implemented.');
}
throw new Error('Invalid scheme for endPoint: ' + scheme);
}
function createFromGossipSeeds(connectionSettings, gossipSeeds, connectionName) {
ensure.notNull(connectionSettings, "connectionSettings");
ensure.notNull(gossipSeeds, "gossipSeeds");
var mergedSettings = merge(defaultConnectionSettings, connectionSettings || {});
var clusterSettings = {
clusterDns: '',
gossipSeeds: gossipSeeds,
externalGossipPort: 0,
maxDiscoverAttempts: mergedSettings.maxDiscoverAttempts,
gossipTimeout: mergedSettings.gossipTimeout
};
var endPointDiscoverer = new ClusterDnsEndPointDiscoverer(connectionSettings.log,
clusterSettings.clusterDns,
clusterSettings.maxDiscoverAttempts,
clusterSettings.externalGossipPort,
clusterSettings.gossipSeeds,
clusterSettings.gossipTimeout
);
return new EventStoreNodeConnection(mergedSettings, clusterSettings, endPointDiscoverer, connectionName);
}
/**
* Create an EventStore connection
* @param {object} settings
* @param {string|object} endPoint
* @param {string|object|array} endPointOrGossipSeeds
* @param {string} [connectionName]
* @returns {EventStoreNodeConnection}
*/
module.exports.create = function(settings, endPoint, connectionName) {
if (typeof endPoint === 'object') {
var mergedSettings = merge(defaultConnectionSettings, settings || {});
var endpointDiscoverer = new StaticEndpointDiscoverer(endPoint, settings.useSslConnection);
return new EventStoreNodeConnection(mergedSettings, endpointDiscoverer, connectionName || null);
}
if (typeof endPoint === 'string') {
//TODO: tcpEndpoint represented as tcp://hostname:port
//TODO: cluster discovery via dns represented as discover://dns:?port
throw new Error('Not implemented.');
}
//TODO: cluster discovery via gossip seeds in settings
throw new Error('Not implemented.');
module.exports.create = function(settings, endPointOrGossipSeeds, connectionName) {
if (Array.isArray(endPointOrGossipSeeds)) return createFromGossipSeeds(settings, endPointOrGossipSeeds, connectionName);
if (typeof endPointOrGossipSeeds === 'object') return createFromTcpEndpoint(settings, endPointOrGossipSeeds, connectionName);
if (typeof endPointOrGossipSeeds === 'string') return createFromStringEndpoint(settings, endPointOrGossipSeeds, connectionName);
throw new TypeError('endPointOrGossipSeeds must be an object, a string or an array.');
};

View File

@ -34,14 +34,16 @@ const MaxReadSize = 4096;
/**
* @param settings
* @param clusterSettings
* @param endpointDiscoverer
* @param connectionName
* @constructor
* @property {string} connectionName
*/
function EventStoreNodeConnection(settings, endpointDiscoverer, connectionName) {
function EventStoreNodeConnection(settings, clusterSettings, endpointDiscoverer, connectionName) {
this._connectionName = connectionName || ['ES-', uuid.v4()].join('');
this._settings = settings;
this._clusterSettings = clusterSettings;
this._endpointDiscoverer = endpointDiscoverer;
this._handler = new EventStoreConnectionLogicHandler(this, settings);

13
src/gossipSeed.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = function GossipSeed(endPoint, hostName) {
if (typeof endPoint !== 'object' || !endPoint.hostname || !endPoint.port) throw new TypeError('endPoint must be have hostname and port properties.');
Object.defineProperties(this, {
endPoint: {
enumerable: true,
value: endPoint
},
hostName: {
enumerable: true,
value: hostName
}
});
};