thywill
Version:
A Node.js clustered framework for single page web applications based on asynchronous messaging.
346 lines (313 loc) • 13 kB
JavaScript
/**
* @fileOverview
* InMemoryClientTracker class definition.
*/
var util = require('util');
var Thywill = require('thywill');
var Client = Thywill.getBaseClass('Client');
//-----------------------------------------------------------
// Class Definition
//-----------------------------------------------------------
/**
* @class
* A ClientTracker implementation that stores all data in memory. Every cluster
* member process keeps an up to date record of client connections to all
* cluster processes.
*
* Obviously this doesn't scale as well as other implementations to large
* numbers of cluster member processes - there is a lot of cross-talk needed
* between cluster members to keep the data updated in all of them. It is best
* used when your applications must make very frequent requests to ClientTracker
* methods.
*
* @see ClientTracker
*/
function InMemoryClientTracker() {
InMemoryClientTracker.super_.call(this);
}
util.inherits(InMemoryClientTracker, Thywill.getBaseClass('ClientTracker'));
var p = InMemoryClientTracker.prototype;
//-----------------------------------------------------------
// Methods: initialization.
//-----------------------------------------------------------
/**
* @see Component#_configure
*/
p._configure = function (thywill, config, callback) {
var self = this;
this.thywill = thywill;
this.config = config;
// -----------------------------------------------------------------
// Data structures for keeping track of who is online.
// -----------------------------------------------------------------
this.connections = {};
this.thywill.cluster.getClusterMemberIds().forEach(function (clusterMemberId, index, array) {
self.connections[clusterMemberId] = {
connections: {},
sessions: {}
};
});
// -----------------------------------------------------------------
// React to local connections and disconnections.
// -----------------------------------------------------------------
var clientInterface = this.thywill.clientInterface;
var localClusterMemberId = this.thywill.cluster.getLocalClusterMemberId();
clientInterface.on(clientInterface.events.CONNECTION, function (client) {
self._reactToConnectionTo(localClusterMemberId, client);
});
clientInterface.on(clientInterface.events.DISCONNECTION, function (client) {
self._reactToDisconnectionFrom(localClusterMemberId, client);
});
// -----------------------------------------------------------------
// Cluster configuration: communication between processes.
// -----------------------------------------------------------------
// Task names used internally by this clientTracker implementation.
this.clusterTask = {
// Delivery of all connected client data for a specific process.
connectionData: 'thywill:clientTracker:connectedData',
// Request for all connected client data for a specific process.
connectionDataRequest: 'thywill:clientTracker:connectedDataRequest',
// Client connection notice from another cluster member.
connectionTo: 'thywill:clientTracker:connectionTo',
// Client disconnection notice from another cluster member.
disconnectionFrom: 'thywill:clientTracker:disconnectionFrom'
};
// A cluster member fails and goes down. Emit the connection data, then clear it.
this.thywill.cluster.on(this.thywill.cluster.eventNames.CLUSTER_MEMBER_DOWN, function (data) {
var connections = self.connections[data.clusterMemberId].connections;
var sessions = self.connections[data.clusterMemberId].sessions;
self.emit(self.events.CLUSTER_MEMBER_DOWN, data.clusterMemberId, {
connections: connections,
sessions: sessions
});
self._clearConnectionDataForClusterMember(data.clusterMemberId);
});
// A cluster member comes back up. Either it was down, or the heartbeat
// mechanism messed up and it was up all along. Make sure we have an up to
// date set of connection data either way.
this.thywill.cluster.on(this.thywill.cluster.eventNames.CLUSTER_MEMBER_UP, function (data) {
self.thywill.cluster.sendTo(data.clusterMemberId, self.clusterTask.connectionDataRequest, {});
});
// Connection notice from another cluster member.
this.thywill.cluster.on(this.clusterTask.connectionTo, function (data) {
var client = new Client(data.client);
self._reactToConnectionTo(data.clusterMemberId, client);
});
// Disconnection notice from another cluster member.
this.thywill.cluster.on(this.clusterTask.disconnectionFrom, function (data) {
var client = new Client(data.client);
self._reactToDisconnectionFrom(data.clusterMemberId, client);
});
// Delivery of connection data from another server.
this.thywill.cluster.on(this.clusterTask.connectionData, function (data) {
self.thywill.log.debug('InMemoryClientTracker: delivery of connection data from: ' + data.clusterMemberId);
self._updateAllConnectionDataForClusterMember(data.clusterMemberId, data.connections);
});
// Request for connection data from another server.
this.thywill.cluster.on(this.clusterTask.connectionDataRequest, function (data) {
self.thywill.log.debug('InMemoryClientTracker: request for connection data from: ' + data.clusterMemberId);
self.thywill.cluster.sendTo(data.clusterMemberId, self.clusterTask.connectionData, {
connections: self.connections[localClusterMemberId]
});
});
// -----------------------------------------------------------------
// Final steps of configuration.
// -----------------------------------------------------------------
// When Thywill launches, after this._setup() is done:
this.thywill.on('thywill.ready', function () {
// Request an update on who is connected from all other cluster members, so
// as to bring data up to date in the case where a cluster member falls over.
self.thywill.cluster.sendToOthers(self.clusterTask.connectionDataRequest);
});
// Set ready status.
this.readyCallback = callback;
this._announceReady(this.NO_ERRORS);
};
//-----------------------------------------------------------
// Methods: other.
//-----------------------------------------------------------
/**
* @see ClientTracker#clientIsConnected
*/
p.clientIsConnected = function (client, callback) {
var self = this;
var connectionId = this._clientOrConnectionIdToConnectionId(client);
var connected = Object.keys(this.connections).some(function (clusterMemberId, index, array) {
return self.connections[clusterMemberId].connections[connectionId];
});
callback (this.NO_ERRORS, connected);
};
/**
* @see ClientTracker#clientIsConnectedLocally
*/
p.clientIsConnectedLocally = function (client, callback) {
var connectionId = this._clientOrConnectionIdToConnectionId(client);
var clusterMemberId = this.thywill.cluster.getLocalClusterMemberId();
var connected = (this.connections[clusterMemberId].connections[connectionId] !== undefined);
callback (this.NO_ERRORS, connected);
};
/**
* @see ClientTracker#sessionIsConnected
*/
p.clientSessionIsConnected = function (client, callback) {
var self = this;
var sessionId = this._clientOrSessionIdToSessionId(client);
var connected = Object.keys(this.connections).some(function (clusterMemberId, index, array) {
return self.connections[clusterMemberId].sessions[sessionId];
});
callback (this.NO_ERRORS, connected);
};
/**
* @see ClientTracker#connectionIdsForSession
*/
p.connectionIdsForSession = function (sessionId, callback) {
var self = this;
var connectionIds = [];
var connected = Object.keys(this.connections).forEach(function (clusterMemberId, index, array) {
var ids = self.connections[clusterMemberId].sessions[sessionId];
if (ids) {
connectionIds = connectionIds.concat(ids);
}
});
callback (this.NO_ERRORS, connectionIds);
};
/**
* @see ClientTracker#getConnectionData
*/
p.getConnectionData = function (callback) {
callback(this.NO_ERRORS, this.connections);
};
/**
* Update the local connection data to note a connection. Emit an event.
*
* @param {string} clusterMemberId
* The cluster member where the connection occurred.
* @param {string} connectionId
* Unique ID of the connection.
* @param {string} sessionId
* Unique ID of the session associated with this connection - one session
* might have multiple concurrent connections.
* @param {string} [session]
* Session data - only provided for local connections.
*/
p._reactToConnectionTo = function(clusterMemberId, client) {
var connectionId = client.connectionId;
var sessionId = client.sessionId;
// Update the local records.
var data = this.connections[clusterMemberId];
data.connections[connectionId] = Date.now();
if (!data.sessions[sessionId]) {
data.sessions[sessionId] = [connectionId];
} else {
var sessionConnections = data.sessions[sessionId];
if (sessionConnections.indexOf(connectionId) === -1) {
sessionConnections.push(connectionId);
}
}
// Emit tracking events and notify other cluster members, depending on
// whether this is a connection to the local process or not.
if (this.thywill.cluster.getLocalClusterMemberId() === clusterMemberId) {
this.thywill.cluster.sendToOthers(this.clusterTask.connectionTo, {
client: client.toData()
});
this.emit(this.events.CONNECTION, client);
}
this.emit(this.events.CONNECTION_TO, clusterMemberId, client);
};
/**
* Update the local connection data to note a disconnection. Emit an event.
*
* @param {string} clusterMemberId
* The cluster member where the disconnection occurred.
* @param {string} connectionId
* Unique ID of the connection.
* @param {string} sessionId
* Unique ID of the session associated with this connection - one session
* might have multiple concurrent connections.
*/
p._reactToDisconnectionFrom = function(clusterMemberId, client) {
var connectionId = client.connectionId;
var sessionId = client.sessionId;
// Update the local records.
var data = this.connections[clusterMemberId];
delete data.connections[connectionId];
if (data.sessions[sessionId]) {
data.sessions[sessionId] = data.sessions[sessionId].filter(function (element, index, array) {
return (element !== connectionId);
});
if (data.sessions[sessionId].length === 0) {
delete data.sessions[sessionId];
}
}
// Emit tracking events and notify other cluster members, depending on
// whether this is a connection to the local process or not.
if (this.thywill.cluster.getLocalClusterMemberId() === clusterMemberId) {
this.thywill.cluster.sendToOthers(this.clusterTask.disconnectionFrom, {
client: client.toData()
});
this.emit(this.events.DISCONNECTION, client);
}
this.emit(this.events.DISCONNECTION_FROM, clusterMemberId, client);
};
/**
* Given all the connection data for a specific cluster member process, make it
* the local copy of record for that cluster member process.
*
* On startup, processes request all the data from other processes, so as to
* restore the local copy after a crash and restart situation.
*
* @param {string} clusterMemberId
* The cluster member whose data it is.
* @param {object} data
* The connection data for this cluster member.
*/
p._updateAllConnectionDataForClusterMember = function (clusterMemberId, data) {
this.connections[clusterMemberId] = data;
};
/**
* This is called when another cluster member fails, to clear out its
* connection data since all of the connected clients will now be
* disconnected.
*
* @param {string} clusterMemberId
* The cluster member whose data it is.
*/
p._clearConnectionDataForClusterMember = function (clusterMemberId) {
// Important that we replace the object at the top, rather than the
// sessions and connections properties, as the original connections and
// sessions items will be passed on and used to update applications.
this.connections[clusterMemberId] = {
connections: {},
sessions: {}
};
};
/**
* Return a connection ID if given a Client instance or a connection ID.
*
* @param {Client|string} clientOrConnectionId
* Client instance or connection ID.
*/
p._clientOrConnectionIdToConnectionId = function (clientOrConnectionId) {
if (clientOrConnectionId instanceof Client) {
return clientOrConnectionId.getSessionId();
} else {
return clientOrConnectionId;
}
};
/**
* Return a session ID if given a Client instance or a session Id.
*
* @param {Client|string} clientOrSessionId
* Client instance or session ID.
*/
p._clientOrSessionIdToSessionId = function (clientOrSessionId) {
if (clientOrSessionId instanceof Client) {
return clientOrSessionId.getSessionId();
} else {
return clientOrSessionId;
}
};
//-----------------------------------------------------------
// Exports - Class Constructor
//-----------------------------------------------------------
module.exports = InMemoryClientTracker;