UNPKG

endpointjs

Version:

Endpoint.js enables modules within a web application to discover and use each other, whether that be on the same web page, other browser windows and tabs, iframes, servers and web workers in a reactive way by providing robust discovery, execution and stre

404 lines (369 loc) 13.8 kB
/* * (C) 2016 * Booz Allen Hamilton, All rights reserved * Powered by InnoVision, created by the GIAT * * Endpoint.js was developed at the * National Geospatial-Intelligence Agency (NGA) in collaboration with * Booz Allen Hamilton [http://www.boozallen.com]. The government has * "unlimited rights" and is releasing this software to increase the * impact of government investments by providing developers with the * opportunity to take things in new directions. * * 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. */ /* jshint -W097 */ /* globals __filename */ 'use strict'; var EventEmitter = require('events').EventEmitter, inherits = require('util').inherits, isArray = require('util').isArray, addressTool = require('./address'), uuid = require('node-uuid'), appUtils = require('../util/appUtils'), log = appUtils.getLogger(__filename); inherits(HostAffinity, EventEmitter); module.exports = HostAffinity; /** * HostAffinity will establish a relationship between a chain of nodes. If * any node in the chain goes down, then the subsequent nodes in the chain * will be notified of the break. * @augments EventEmitter * @fires HostAffinity#affinity-add (affinityId) * @fires HostAffinity#affinity-remove (affinityId) * @fires HostAffinity#affinity-error (affinityId) * @param {Router} routerInstance - an instance of the Router class * @param {Configuration} config - system configuration * @constructor */ function HostAffinity(routerInstance, config) { if (!(this instanceof HostAffinity)) { return new HostAffinity(routerInstance, config); } // Call parent constructor EventEmitter.call(this); this.setMaxListeners(0); this._id = config.get('instanceId'); this._maxHostAffinities = config.get('maxHostAffinities'); this._maxHops = config.get('maxHops'); // These are affinities that have been requested via add/add-ack this._trackedHosts = {}; // These are local affinities that were initiated in our Endpoint.js instance. this._localAffinities = {}; // Register with the router to track HostAffinity packets // NOTE: There might be an issue here. If a link switches (link-switch), then // it could be that the remote link detected the link was dead, and killed affinity, // thereby re-connecting. Our side will not detect this however, meaning the remote // instance/facade might be dead but we won't know on this side ... this._routerInstance = routerInstance; this._routerInstance.addHandler('affinity'); this._routerInstance.on('route-unavailable', this._handleRouteLost.bind(this)); this._routerInstance.on('affinity', this._handleAffinityPacket.bind(this)); } /** * This function is used to establish host affinities * @param packet * @param fromUuid * @private */ HostAffinity.prototype._handleAffinityPacket = function(packet, fromUuid) { var fromRoute = this._routerInstance.getRoute(fromUuid); if (!fromRoute) { return; } // If this is an internal route, then trust the 'from' parameter. This // is because the packet could be coming from another border node, not // an adjacent node. if (!fromRoute.external) { fromUuid = packet.from; } switch (packet.type) { case 'add': this._handleAdd(packet, fromUuid); break; case 'remove': this._handleRemove(packet, fromUuid); break; case 'error': this._handleError(packet, fromUuid); break; } }; /** * This function is called when a request to add a tracked affinity id occurs. * It expects one affinity id in the 'packet.id' field, which is NOT an array. * @param packet - the originating protocol packet * @param fromUuid - who sent us the protocol packet * @private */ HostAffinity.prototype._handleAdd = function(packet, fromUuid) { if (!packet.id || !isArray(packet.path) || !appUtils.isUuid(packet.id)) { return; } // Valid? var address = addressTool(packet.path, true); if (!address.isValid(this._maxHops)) { return; } // Determine who we're going to send the affinity record to. It will // be the next record in the path. var toPath = address.getPathVector(); // Remove any additional references to me due to path-vector algorithm var toUuid = this._id; while (toUuid == this._id && toPath.length > 0) { toUuid = toPath[0]; toPath.shift(); } if (toUuid == this._id) { toUuid = 'local'; } var addedFrom, addedTo; // Add tracked record for 'from' as long as from is not local addedFrom = this._addTrackedRecord(fromUuid, toUuid, packet.id, true); // Add tracked record for 'to' as long as to is not local, and as long as we know about // the host we're sending to. if (addedFrom) { if (toUuid == 'local' || this._routerInstance.getRoute(toUuid)) { addedTo = this._addTrackedRecord(toUuid, fromUuid, packet.id, false); } } if (!addedFrom || !addedTo) { if (addedFrom) { // Cleanup and send error or forward to next host. this._removeTrackedRecord(fromUuid, packet.id); } // Reply with error. this._sendProtocolMessage(fromUuid, 'error', packet.id, 'local'); } else { this._sendProtocolMessage(toUuid, 'add', packet.id, fromUuid, toPath); } }; /** * This function is called when a request to remove a tracked affinity id occurs. * @param packet - the originating protocol packet * @param fromUuid - who sent us the protocol packet * @private */ HostAffinity.prototype._handleRemove = function(packet, fromUuid) { if (!isArray(packet.id)) { return; } var notificationHosts = {}; packet.id.forEach(function(affinityId) { var concernedHost = this._removeTrackedRecord(fromUuid, affinityId); if (concernedHost) { notificationHosts[concernedHost] = notificationHosts[concernedHost] || []; notificationHosts[concernedHost].push(affinityId); this._removeTrackedRecord(concernedHost, affinityId); } }, this); // Send out all notifications for (var host in notificationHosts) { // Forward the packet log.log(log.DEBUG3, 'Sending affinity (remove) message to host %s with %s items', host, notificationHosts[host].length); this._sendProtocolMessage(host, 'remove', notificationHosts[host], 'local'); } }; /** * Occurs when we can't establish the affinity. (Usually maxAffinities is reached) * @param packet * @param fromUuid * @private */ HostAffinity.prototype._handleError = function(packet, fromUuid) { if (!packet.id || isArray(packet.id)) { return; } var concernedHost = this._removeTrackedRecord(fromUuid, packet.id); if (concernedHost) { this._removeTrackedRecord(concernedHost, packet.id); this._sendProtocolMessage(concernedHost, 'error', packet.id, 'local'); } }; /** * This occurs when a route is lost on our local routing table. This can include * external hosts. If we have a tracked host in our affinities for that id, then * we will send a notification that the affinity has been lost * @param toId - the host that dropped * @private */ HostAffinity.prototype._handleRouteLost = function(toId) { var hostInfo = this._trackedHosts[toId]; if (hostInfo) { log.log(log.DEBUG2, 'Detected %s is lost, removing affinities', toId); // Simulate a remove from the host for all affinities this._handleRemove( { id: Object.keys(hostInfo._affinities) }, toId ); } }; /** * This function will add a tracked record for the given host, if necessary * @param toId * @param affinityId * @param owned * @returns {Boolean} true if the record was added * @private */ HostAffinity.prototype._addTrackedRecord = function(toId, concernedId, affinityId, owned) { var toIdInfo = this._trackedHosts[toId]; if (!toIdInfo) { toIdInfo = this._trackedHosts[toId] = { _affinities: {}, _ownedCount: 0, _totalCount: 0 }; } // Ensure that we don't exceed, otherwise send a remove message back to the sender. // But only if this is an external link. if (owned && appUtils.isExtUuid(toId) && toIdInfo._ownedCount >= this._maxHostAffinities) { log.log(log.WARN, 'Exceeded max affinities for host %s', toId); return false; } if (!toIdInfo._affinities[affinityId]) { toIdInfo._affinities[affinityId] = { _concernedHost: concernedId, _owned: !!owned }; toIdInfo._totalCount += 1; if (owned) { toIdInfo._totalOwned += 1; } log.log(log.DEBUG2, 'Added tracked affinity: [id: %s] [host: %s] [concerned host: %s]', affinityId, toId, concernedId); return true; } else { log.log(log.DEBUG, 'We already know that affinity id %s for %s', affinityId, toId); } return false; }; /** * Remove the given tracked record, and return the concerned host id if it exists. * @param toId * @param affinityId * @returns {String} the concerned host id or null * @private */ HostAffinity.prototype._removeTrackedRecord = function(toId, affinityId) { var toIdInfo = this._trackedHosts[toId]; if (toIdInfo) { var concernedHost = toIdInfo._affinities[affinityId]; if (concernedHost) { log.log(log.DEBUG2, 'Removed tracked affinity: [id: %s] [host: %s] [concerned host: %s]', affinityId, toId, concernedHost._concernedHost); delete toIdInfo._affinities[affinityId]; toIdInfo._totalCount -= 1; if (concernedHost._owned) { toIdInfo._ownedCount -= 1; } return concernedHost._concernedHost; } } if (toIdInfo._totalCount === 0) { delete this._trackedHosts[toId]; } return null; }; /** * Send an affinity packet to the given host * @param toId - host to send the message to * @param type - the type of the message * @param affinityId - list of ids or an id * @param {String} [path] - the path to continue the affinity * @private */ HostAffinity.prototype._sendProtocolMessage = function(toId, type, affinityId, fromUuid, path) { if (toId == 'local') { // Report locally, as long as it's not an error. We don't want to // prevent communication if there are no affinities available var ids = !isArray(affinityId) ? [affinityId] : affinityId; var _this = this; ids.forEach(function(id) { _this.emit(id, type); _this.removeAllListeners(id); }); } else { this._routerInstance.sendPacket( toId, 'affinity', { id: affinityId, from: this._id, type: type, path: path }, fromUuid); } }; /** * Establish a affinity with the given node * @param address */ HostAffinity.prototype.establishHostAffinity = function(address) { var addressHash = address.toString(); // Ensure that the address has no loops and isn't directed at myself. if (addressHash.length === 0 || !address.isValid(this._maxHops)) { throw new Error('invalid address'); } var localAffinity = this._localAffinities[addressHash]; if (!localAffinity) { // Don't route to myself var pathVector = address.getPathVector(); if (pathVector.length == 1 && pathVector[0] == this._id) { return null; } // Create a remote affinity record localAffinity = this._localAffinities[addressHash] = { _id: uuid(), _count: 0 }; this._handleAdd( { path: address.getPathVector(), id: localAffinity._id }, 'local' ); } localAffinity._count += 1; return localAffinity._id; }; /** * Tell the distant node that we're breaking the given host affinity * @param address * @param affinityId */ HostAffinity.prototype.removeHostAffinity = function(address) { var addressHash = address.toString(); if (addressHash.length === 0) { return; } var localAffinity = this._localAffinities[addressHash]; if (localAffinity) { localAffinity._count -= 1; if (localAffinity._count === 0) { delete this._localAffinities[addressHash]; this._handleRemove( { id: [localAffinity._id] }, 'local' ); } } };