diff --git a/conf/config.yml b/conf/config.yml index 51cb441f3..e08465a54 100644 --- a/conf/config.yml +++ b/conf/config.yml @@ -113,4 +113,10 @@ clusterKeepAliveInterval: 5000 # The interval at which all nodes within a cluster registry are checked for timeouts clusterActiveCheckInterval: 1000 # The longest duration since the last status message was received until the remote node is declared inactive -clusterNodeInactiveTimeout: 6000 \ No newline at end of file +clusterNodeInactiveTimeout: 6000 +# The amount of time to wait for a provider to acknowledge or reject a listen request +listenResponseTimeout: 500 +# The amount of time a lock can be reserved for before it is force released +lockTimeout: 1000 +# The amount of time a lock request waits for before it defaults to false +lockRequestTimeout: 1000 \ No newline at end of file diff --git a/package.json b/package.json index 4e27b07b3..7af00bc51 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "test": "test" }, "scripts": { - "coverage": "istanbul cover node_modules/jasmine/bin/jasmine.js JASMINE_CONFIG_PATH=jasmine.json", + "coverage": "istanbul cover node_modules/jasmine/bin/jasmine.js JASMINE_CONFIG_PATH=jasmine.json -x **/pid-helper.js", "watch": "node node_modules/watch/cli.js \"npm test\" ./src ./test", "reporter": "node jasmine-runner", "test": "jasmine JASMINE_CONFIG_PATH=jasmine.json", @@ -26,27 +26,26 @@ }, "dependencies": { "adm-zip": "^0.4.7", - "colors": "1.0.3", + "colors": "^1.0.3", "commander": "^2.9.0", "engine.io": "1.6.11", "glob": "^7.0.5", "js-yaml": "^3.6.1", "mkdirp": "^0.5.1", - "needle": "^1.0.0" + "needle": "^1.1.0" }, "devDependencies": { "async": "^0.2.9", - "coveralls": "^2.11.9", + "coveralls": "^2.11.12", "engine.io-client": "^1.6.11", - "grunt": "^1.0.1 ", - "grunt-release": "^0.14.0", - "istanbul": "^0.4.3", + "grunt": "^1.0.1", + "istanbul": "^0.4.4", "jasmine": "^2.4.1", "jasmine-spec-reporter": "^2.5.0", "n0p3": "^1.0.2", "nexe": "^1.1.2", "proxyquire": "1.7.10", - "watch": "^0.19.1" + "watch": "^0.19.2" }, "author": "deepstreamHub GmbH", "license": "AGPL-3.0", diff --git a/src/cluster/cluster-registry.js b/src/cluster/cluster-registry.js index 175e2fe39..31963e64f 100644 --- a/src/cluster/cluster-registry.js +++ b/src/cluster/cluster-registry.js @@ -13,7 +13,7 @@ const EventEmitter = require( 'events' ).EventEmitter; * @emits add * @emits remove */ -module.exports = class ClusterRegistry extends EventEmitter{ +module.exports = class ClusterRegistry extends EventEmitter { /** * Creates the class, initialises all intervals and publishes the @@ -61,7 +61,11 @@ module.exports = class ClusterRegistry extends EventEmitter{ action: C.ACTIONS.REMOVE, data:[ this._options.serverName ] }); - this._options.messageConnector.unsubscribe( C.TOPIC.CLUSTER, this._onMessageFn ); + + // TODO: If a message connector doesn't close this is required to avoid an error + // being thrown during shutdown + //this._options.messageConnector.unsubscribe( C.TOPIC.CLUSTER, this._onMessageFn ); + process.removeListener( 'beforeExit', this._leaveClusterFn ); process.removeListener( 'exit', this._leaveClusterFn ); clearInterval( this._publishInterval ); @@ -80,6 +84,18 @@ module.exports = class ClusterRegistry extends EventEmitter{ return Object.keys( this._nodes ); } + /** + * Returns true if this node is the cluster leader + * @return {Boolean} [description] + */ + isLeader() { + return this._options.serverName === this.getCurrentLeader(); + } + + /** + * Returns the name of the current leader + * @return {String} + */ getCurrentLeader() { var maxScore = 0, serverName, diff --git a/src/cluster/cluster-unique-state-provider.js b/src/cluster/cluster-unique-state-provider.js index 94c26d543..f0298df06 100644 --- a/src/cluster/cluster-unique-state-provider.js +++ b/src/cluster/cluster-unique-state-provider.js @@ -9,54 +9,99 @@ SUPPORTED_ACTIONS[ C.ACTIONS.LOCK_RESPONSE ] = true; SUPPORTED_ACTIONS[ C.ACTIONS.LOCK_REQUEST ] = true; SUPPORTED_ACTIONS[ C.ACTIONS.LOCK_RELEASE ] = true; - /** - * Uses options - * uniqueTimeout - * leaderResponseTimeout + * The unique registry is responsible for maintaing a single source of truth + * within the cluster, used mainly for issuing cluster wide locks when an operation + * that stretches over multiple nodes are required. * - * Uses topics - * C.TOPIC.LEADER - * C.TOPIC.LEADER_PRIVATE_ + * For example, distributed listening requires a leader to drive the nodes in sequence, + * so issuing a lock prevents multiple nodes from assuming the lead. * - * Uses action - * C.ACTIONS.LEADER_REQUEST - * C.ACTIONS.LEADER_VOTE */ -module.exports = class UniqueRegistry{ +module.exports = class UniqueRegistry { + + /** + * The unique registry is a singleton and is only created once + * within deepstream.io. It is passed via + * via the options object. + * + * @param {Object} options The options deepstream was created with + * @param {ClusterRegistry} clusterRegistry The cluster registry, used to get the cluster leader + * + * @constructor + */ constructor( options, clusterRegistry ) { this._options = options; this._clusterRegistry = clusterRegistry; this._locks = {}; - this._lockTimeouts = {}; + this._timeouts = {}; this._responseEventEmitter = new EventEmitter(); this._onPrivateMessageFn = this._onPrivateMessage.bind( this ); this._localTopic = this._getPrivateTopic( this._options.serverName ); this._options.messageConnector.subscribe( this._localTopic, this._onPrivateMessageFn ); } + /** + * Requests a lock, if the leader ( whether local or distributed ) has the lock availble + * it will invoke the callback with true, otherwise false. + * + * @param {String} name the lock name that is desired + * @param {Function} callback the callback to be told if the lock has been reserved succesfully + * + * @public + * @returns {void} + */ get( name, callback ) { var leaderServerName = this._clusterRegistry.getCurrentLeader(); if( this._options.serverName === leaderServerName ) { callback( this._getLock( name ) ); - } else { - //TODO start timeout + } + else if( !this._timeouts[ name ] ) { this._getRemoteLock( name, leaderServerName, callback ); } + else { + callback( false ); + } } + /** + * Release a lock, allowing other resources to request it again + * + * @param {String} name the lock name that is desired + * + * @public + * @returns {void} + */ release( name ) { var leaderServerName = this._clusterRegistry.getCurrentLeader(); if( this._options.serverName === leaderServerName ) { this._releaseLock( name ); - } else { + } + else { this._releaseRemoteLock( name, leaderServerName ); } } + /** + * Called when the current node is not the leader, issuing a lock request + * via the message bus + * + * @param {String} name The lock name + * @param {String} leaderServerName The leader of the cluster + * @param {Function} callback The callback to invoke once a response + * from the server is retrieved + * @private + * @returns {void} + */ _getRemoteLock( name, leaderServerName, callback ) { + + this._timeouts[ name ] = utils.setTimeout( + this._onLockRequestTimeout.bind( this, name ), + this._options.lockRequestTimeout + ); + this._responseEventEmitter.once( name, callback ); var remoteTopic = this._getPrivateTopic( leaderServerName ); @@ -71,6 +116,15 @@ module.exports = class UniqueRegistry{ }); } + /** + * Notifies a remote leader keeping a lock that said lock is no longer required + * + * @param {String} name The lock name + * @param {String} leaderServerName The leader of the cluster + * + * @private + * @returns {void} + */ _releaseRemoteLock( name, leaderServerName ) { var remoteTopic = this._getPrivateTopic( leaderServerName ); @@ -83,6 +137,16 @@ module.exports = class UniqueRegistry{ }); } + /** + * Called when a message is recieved on the message bus. + * This could mean the leader responded to a request or that you're currently + * the leader and recieved a request. + * + * @param {Object} message Object from message bus + * + * @private + * @returns {void} + */ _onPrivateMessage( message ) { if( !SUPPORTED_ACTIONS[ message.action ] ) { this._options.logger.log( C.LOG_LEVEL.WARN, C.EVENT.UNKNOWN_ACTION, message.action ); @@ -99,11 +163,18 @@ module.exports = class UniqueRegistry{ return; } - //LOCK_REQUEST & LOCK_RELEASE require this node to be the leader - if( this._isLeader() === false ) { - // TODO log unsolicited message - //TODO - What happens if the remote node believes we are the leader - //but we don't agree? + if( this._clusterRegistry.isLeader() === false ) { + var remoteServerName = 'unknown-server' + if( message.data[ 0 ].responseTopic ) { + remoteServerName = message.data[ 0 ].responseTopic.replace( C.TOPIC.LEADER_PRIVATE, ''); + } + + this._options.logger.log( + C.LOG_LEVEL.WARN, + C.EVENT.INVALID_LEADER_REQUEST, + `server ${remoteServerName} assumes this node '${this._options.serverName}' is the leader` + ); + return; } @@ -115,9 +186,17 @@ module.exports = class UniqueRegistry{ } } + /** + * Called when a remote lock request is received + * + * @param {Object} data messageData + * + * @private + * @returns {void} + */ _handleRemoteLockRequest( data ) { this._options.messageConnector.publish( data.responseTopic, { - topic: responseTopic, + topic: data.responseTopic, action: C.ACTIONS.LOCK_RESPONSE, data: [{ name: data.name, @@ -126,48 +205,108 @@ module.exports = class UniqueRegistry{ }); } + /** + * Called when a remote lock response is received + * + * @param {Object} data messageData + * + * @private + * @returns {void} + */ _handleRemoteLockResponse( data ) { + clearTimeout( this._timeouts[ data.name ] ); + delete this._timeouts[ data.name ]; this._responseEventEmitter.emit( data.name, data.result ); } + /** + * Called when a remote node notifies the cluster that a lock has been removeded + * + * @param {Object} data messageData + * + * @private + * @returns {void} + */ _handleRemoteLockRelease( data ) { - + delete this._locks[ data.name ]; } + /** + * Generates a private topic to allow routing requests directly + * to this node + * + * @param {String} serverName The server of this server + * + * @private + * @returns {String} privateTopic + */ _getPrivateTopic( serverName ) { return C.TOPIC.LEADER_PRIVATE + serverName; } - _isLeader() { - return this._options.serverName === this._clusterRegistry.getCurrentLeader(); - } - - _getLock( name, callback ) { - this._lockTimeouts[ name ] = utils.setTimeout( - this._onLockTimeout.bind( this, name ), - this._options.lockTimeout - ); - + /** + * Returns true if reserving lock was possible otherwise returns false + * + * @param {String} name Name of lock + * + * @private + * @return {boolean} + */ + _getLock( name ) { if( this._locks[ name ] === true ) { return false; } else { + this._timeouts[ name ] = utils.setTimeout( + this._onLockTimeout.bind( this, name ), + this._options.lockTimeout + ); this._locks[ name ] = true; return true; } } + /** + * Called when a lock is no longer required and can be released. This is triggered either by + * a timeout if a remote release message wasn't received in time or when release was called locally. + * + * Important note: Anyone can release a lock. It is assumed that the cluster is trusted + * so maintaining who has the lock is not required. This may need to change going forward. + * + * @param {String} name Lock name + * + * @private + * @returns {void} + */ _releaseLock( name ) { - clearTimeout( this._lockTimeouts[ name ] ); + clearTimeout( this._timeouts[ name ] ); + delete this._timeouts[ name ]; delete this._locks[ name ]; } + /** + * Called when a timeout occurs on a lock that has been reserved for too long + * + * @param {String} name The lock name + * + * @private + * @returns {void} + */ _onLockTimeout( name ) { this._releaseLock( name ); - this._options.logger.log( C.LOG_LEVEL.WARN, C.EVENT.TIMEOUT, 'lock ' + name + ' released due to timeout' ); + this._options.logger.log( C.LOG_LEVEL.WARN, C.EVENT.TIMEOUT, `lock ${name} released due to timeout` ); } + /** + * Called when a remote request has timed out, resulting in notifying the client that + * the lock wasn't able to be reserved + * + * @param {String} name The lock name + * + * @private + * @returns {void} + */ _onLockRequestTimeout( name ) { this._handleRemoteLockResponse({ name: name, result: false }); - this._options.logger.log( C.LOG_LEVEL.WARN, C.EVENT.TIMEOUT, 'request for lock ' + name + ' timed out' ); + this._options.logger.log( C.LOG_LEVEL.WARN, C.EVENT.TIMEOUT, `request for lock ${name} timed out` ); } } diff --git a/src/utils/distributed-state-registry.js b/src/cluster/distributed-state-registry.js similarity index 92% rename from src/utils/distributed-state-registry.js rename to src/cluster/distributed-state-registry.js index 30465177b..e7d035b65 100644 --- a/src/utils/distributed-state-registry.js +++ b/src/cluster/distributed-state-registry.js @@ -18,11 +18,8 @@ DATA_LENGTH[ C.EVENT.DISTRIBUTED_STATE_REMOVE ] = 3; * an 'add' event is emitted. Whenever its removed by the last node within the cluster, * a 'remove' event is emitted. * - * @todo Currently, this class expects to be notified from the outside when a node leaves the cluster - * (removeAll call). It might make sense to implement cluster presence/hardbeats on a global level or - * have this class explicitly listen to a CLUSTER_STATUS topic. - Let's discuss next week - * * @extends {EventEmitter} + * * @event 'add' emitted whenever an entry is added for the first time * @event 'remove' emitted whenever an entry is removed by the last node * @@ -43,9 +40,23 @@ module.exports = class DistributedStateRegistry extends EventEmitter{ this._topic = topic; this._options = options; this._options.messageConnector.subscribe( topic, this._processIncomingMessage.bind( this ) ); + this._options.clusterRegistry.on( 'remove', this.removeAll.bind( this ) ); this._data = {}; this._reconciliationTimeouts = {}; this._fullStateSent = false; + this._requestFullState( C.ALL ); + } + + /** + * Checks if a given entry exists within the registry + * + * @param {String} name the name of the entry + * + * @public + * @returns {Boolean} exists + */ + has( name ) { + return !!this._data[ name ]; } /** @@ -97,6 +108,20 @@ module.exports = class DistributedStateRegistry extends EventEmitter{ } } + /** + * Returns all the servers that hold a given state + * + * @public + * @returns {Object} entries + */ + getAllServers( name ) { + if( this._data[ name ] ) { + return this._data[ name ].nodes; + } else { + return {}; + } + } + /** * Returns all currently registered entries * @@ -125,7 +150,7 @@ module.exports = class DistributedStateRegistry extends EventEmitter{ return; } - this._data[ name ].nodes[ serverName ] = false; + delete this._data[ name ].nodes[ serverName ]; for( serverName in this._data[ name ].nodes ) { if( this._data[ name ].nodes[ serverName ] === true ) { @@ -150,6 +175,7 @@ module.exports = class DistributedStateRegistry extends EventEmitter{ * @returns {void} */ _add( name, serverName ) { + if( !this._data[ name ] ) { this._data[ name ] = { nodes: {}, @@ -245,6 +271,8 @@ module.exports = class DistributedStateRegistry extends EventEmitter{ * another message from the remote server arrives before the reconciliation request * is send, it will be cancelled. * + * TODO: Shouldn't this remove the timeout first? + * * @param {String} serverName the name of the remote server for which the checkSum should be calculated * @param {Number} remoteCheckSum The checksum the remote server has calculated for all its local entries * @@ -254,7 +282,7 @@ module.exports = class DistributedStateRegistry extends EventEmitter{ _verifyCheckSum( serverName, remoteCheckSum ) { if( this._getCheckSumTotal( serverName ) !== remoteCheckSum ) { this._reconciliationTimeouts[ serverName ] = setTimeout( - this._reconcile.bind( this, serverName ), + this._requestFullState.bind( this, serverName ), this._options.stateReconciliationTimeout ); } else if( this._reconciliationTimeouts[ serverName ] ) { @@ -274,7 +302,7 @@ module.exports = class DistributedStateRegistry extends EventEmitter{ * @private * @returns {void} */ - _reconcile( serverName ) { + _requestFullState( serverName ) { this._options.messageConnector.publish( this._topic, { topic: this._topic, action: C.EVENT.DISTRIBUTED_STATE_REQUEST_FULL_STATE, @@ -377,7 +405,7 @@ module.exports = class DistributedStateRegistry extends EventEmitter{ } else if( message.action === C.EVENT.DISTRIBUTED_STATE_REQUEST_FULL_STATE ) { - if( message.data[ 0 ] === this._options.serverName && this._fullStateSent === false ) { + if( message.data[ 0 ] === C.ALL || ( message.data[ 0 ] === this._options.serverName && this._fullStateSent === false ) ) { this._sendFullState(); } } diff --git a/src/constants/constants.js b/src/constants/constants.js index 05932122c..6de93be22 100644 --- a/src/constants/constants.js +++ b/src/constants/constants.js @@ -2,6 +2,7 @@ exports.MESSAGE_SEPERATOR = String.fromCharCode( 30 ); // ASCII Record Seperator exports.MESSAGE_PART_SEPERATOR = String.fromCharCode( 31 ); // ASCII Unit Separator 1F exports.SOURCE_MESSAGE_CONNECTOR = 'SOURCE_MESSAGE_CONNECTOR'; +exports.ALL = 'ALL'; exports.LOG_LEVEL = {}; exports.LOG_LEVEL.DEBUG = 0; @@ -69,6 +70,7 @@ exports.EVENT.DISTRIBUTED_STATE_REQUEST_FULL_STATE = 'DISTRIBUTED_STATE_REQUEST_ exports.EVENT.DISTRIBUTED_STATE_FULL_STATE = 'DISTRIBUTED_STATE_FULL_STATE'; exports.EVENT.CLUSTER_JOIN = 'CLUSTER_JOIN'; exports.EVENT.CLUSTER_LEAVE = 'CLUSTER_LEAVE'; +exports.EVENT.INVALID_LEADER_REQUEST = 'INVALID_LEADER_REQUEST'; exports.EVENT.TIMEOUT = 'TIMEOUT'; exports.TOPIC = {}; @@ -84,6 +86,12 @@ exports.TOPIC.LEADER = 'L'; exports.TOPIC.LEADER_PRIVATE = 'LP_'; exports.TOPIC.PRIVATE = 'PRIVATE/'; +exports.TOPIC.LISTEN = 'LI'; +exports.TOPIC.PUBLISHED_SUBSCRIPTIONS = 'PS'; +exports.TOPIC.LISTEN_PATTERNS = 'LIP'; +exports.TOPIC.SUBSCRIPTIONS = 'SUB'; + + exports.ACTIONS = {}; exports.ACTIONS.ACK = 'A'; exports.ACTIONS.READ = 'R'; @@ -120,6 +128,12 @@ exports.ACTIONS.LOCK_REQUEST = 'LRQ'; exports.ACTIONS.LOCK_RESPONSE = 'LRP'; exports.ACTIONS.LOCK_RELEASE = 'LRL'; +exports.ACTIONS.LEADER_REQUEST = 'LR'; +exports.ACTIONS.LEADER_VOTE = 'LV'; +exports.ACTIONS.LOCK_REQUEST = 'LRQ'; +exports.ACTIONS.LOCK_RESPONSE = 'LRP'; +exports.ACTIONS.LOCK_RELEASE = 'LRL'; + //WebRtc exports.ACTIONS.WEBRTC_REGISTER_CALLEE = 'RC'; exports.ACTIONS.WEBRTC_UNREGISTER_CALLEE = 'URC'; diff --git a/src/deepstream.io.js b/src/deepstream.io.js index bf7914a88..8f79b09c0 100644 --- a/src/deepstream.io.js +++ b/src/deepstream.io.js @@ -19,6 +19,7 @@ var ConnectionEndpoint = require( './message/connection-endpoint' ), WebRtcHandler = require( './webrtc/webrtc-handler' ), DependencyInitialiser = require( './utils/dependency-initialiser' ), ClusterRegistry = require( './cluster/cluster-registry' ), + UniqueRegistry = require( './cluster/cluster-unique-state-provider' ), C = require( './constants/constants' ), pkg = require( '../package.json' ); @@ -202,7 +203,7 @@ Deepstream.prototype.stop = function() { } utils.combineEvents( closables, 'close', this._onStopped.bind( this ) ); - this._clusterRegistry.leaveCluster(); + this._options.clusterRegistry.leaveCluster(); this._connectionEndpoint.close(); }; @@ -291,7 +292,10 @@ Deepstream.prototype._init = function() { this._messageProcessor = new MessageProcessor( this._options ); this._messageDistributor = new MessageDistributor( this._options ); this._connectionEndpoint.onMessage = this._messageProcessor.process.bind( this._messageProcessor ); - this._clusterRegistry = new ClusterRegistry( this._options, this._connectionEndpoint ); + + this._options.clusterRegistry = new ClusterRegistry( this._options, this._connectionEndpoint ); + this._options.uniqueRegistry = new UniqueRegistry( this._options, this._options.clusterRegistry ); + this._eventHandler = new EventHandler( this._options ); this._messageDistributor.registerForTopic( C.TOPIC.EVENT, this._eventHandler.handle.bind( this._eventHandler ) ); diff --git a/src/default-options.js b/src/default-options.js index 5d81e0b9c..8fdcf516f 100644 --- a/src/default-options.js +++ b/src/default-options.js @@ -83,7 +83,10 @@ exports.get = function() { stateReconciliationTimeout: 500, clusterKeepAliveInterval: 5000, clusterActiveCheckInterval: 1000, - clusterNodeInactiveTimeout: 6000 + clusterNodeInactiveTimeout: 6000, + listenResponseTimeout: 500, + lockTimeout: 1000, + lockRequestTimeout: 1000 }; return options; diff --git a/src/event/event-handler.js b/src/event/event-handler.js index ef4dfe5c9..6ea992c8c 100644 --- a/src/event/event-handler.js +++ b/src/event/event-handler.js @@ -122,7 +122,7 @@ EventHandler.prototype._triggerEvent = function( messageSource, message ) { } if( this._options.dataTransforms && this._options.dataTransforms.has( C.TOPIC.EVENT, C.ACTIONS.EVENT ) ) { - var receivers = this._subscriptionRegistry.getSubscribers( message.data[ 0 ] ); + var receivers = this._subscriptionRegistry.getLocalSubscribers( message.data[ 0 ] ); if( receivers ) { receivers.forEach( this._sendTransformedMessage.bind( this, message, messageSource ) ); diff --git a/src/listen/listener-registry.js b/src/listen/listener-registry.js index 921828d3f..c26c009cd 100644 --- a/src/listen/listener-registry.js +++ b/src/listen/listener-registry.js @@ -2,11 +2,14 @@ var C = require( '../constants/constants' ), SubscriptionRegistry = require( '../utils/subscription-registry' ), + DistributedStateRegistry = require( '../cluster/distributed-state-registry' ), TimeoutRegistry = require( './listener-timeout-registry' ), + ListenerUtils = require( './listener-utils' ), messageParser = require( '../message/message-parser' ), messageBuilder = require( '../message/message-builder' ); class ListenerRegistry { + /** * Deepstream.io allows clients to register as listeners for subscriptions. * This allows for the creation of 'active' data-providers, @@ -36,14 +39,32 @@ class ListenerRegistry { this._topic = topic; this._options = options; this._clientRegistry = clientRegistry; - this._providerRegistry = new SubscriptionRegistry( options, this._topic ); - this._providerRegistry.setAction( 'subscribe', C.ACTIONS.LISTEN ); - this._providerRegistry.setAction( 'unsubscribe', C.ACTIONS.UNLISTEN ); + + this._uniqueStateProvider = this._options.uniqueRegistry; + + this._listenerUtils = new ListenerUtils( topic, options, clientRegistry ); + this._patterns = {}; - this._providedRecords = {}; - this._listenInProgress = {}; + this._localListenInProgress = {}; this._listenerTimeoutRegistery = new TimeoutRegistry( topic, options ); - this._reconcilePatternsBound = this._reconcilePatterns.bind( this ); + + this._providerRegistry = new SubscriptionRegistry( options, this._topic, `${topic}_${C.TOPIC.LISTEN_PATTERNS}` ); + this._providerRegistry.setAction( 'subscribe', C.ACTIONS.LISTEN ); + this._providerRegistry.setAction( 'unsubscribe', C.ACTIONS.UNLISTEN ); + this._providerRegistry.setSubscriptionListener( { + onSubscriptionRemoved: this._removePattern.bind( this ), + onSubscriptionMade: this._addPattern.bind( this ) + } ); + + this._locallyProvidedRecords = {}; + this._clusterProvidedRecords = new DistributedStateRegistry( `${topic}_${C.TOPIC.PUBLISHED_SUBSCRIPTIONS}`, options ); + this._clusterProvidedRecords.on( 'add', this._onRecordStartProvided.bind( this ) ); + this._clusterProvidedRecords.on( 'remove', this._onRecordStopProvided.bind( this ) ); + + this._leadListen = {}; + this._leadingListen = {}; + this._listenTopic = this._listenerUtils.getMessageBusTopic( this._options.serverName, this._topic ); + this._options.messageConnector.subscribe( this._listenTopic, this._onIncomingMessage.bind( this ) ); } /** @@ -53,12 +74,13 @@ class ListenerRegistry { * @returns {boolean} */ hasActiveProvider( susbcriptionName ) { - return !!this._providedRecords[ susbcriptionName ]; + return this._clusterProvidedRecords.has( susbcriptionName ); } /** - * The main entry point toe the handle class. + * The main entry point to the handle class. * Called on any of the following actions: + * * 1) C.ACTIONS.LISTEN * 2) C.ACTIONS.UNLISTEN * 3) C.ACTIONS.LISTEN_ACCEPT @@ -74,26 +96,55 @@ class ListenerRegistry { handle( socketWrapper, message ) { const pattern = message.data[ 0 ]; const subscriptionName = message.data[ 1 ]; - if (message.action === C.ACTIONS.LISTEN_SNAPSHOT ) { - this._sendSnapshot( socketWrapper, message ); - } else if (message.action === C.ACTIONS.LISTEN ) { + if (message.action === C.ACTIONS.LISTEN ) { this._addListener( socketWrapper, message ); } else if (message.action === C.ACTIONS.UNLISTEN ) { this._removeListener( socketWrapper, message ); } else if( this._listenerTimeoutRegistery.isALateResponder( socketWrapper, message ) ) { this._listenerTimeoutRegistery.handle( socketWrapper, message ); - } else if( this._listenInProgress[ subscriptionName ] ) { - this._processResponseForListInProgress( socketWrapper, subscriptionName, message ); + } else if( this._localListenInProgress[ subscriptionName ] ) { + this._processResponseForListenInProgress( socketWrapper, subscriptionName, message ); } else { - this._onMsgDataError( socketWrapper, message.raw, C.EVENT.INVALID_MESSAGE ); + this._listenerUtils.onMsgDataError( socketWrapper, message.raw, C.EVENT.INVALID_MESSAGE ); } } /** - * Process an accept or reject for a listen that is currently in progress - * which hasn't timed out yet. - */ - _processResponseForListInProgress( socketWrapper, subscriptionName, message ) { + * Handle messages that arrive via the message bus + * + * This can either be messages by the leader indicating that this + * node is responsible for starting a local discovery phase + * or from a resulting node with an ACK to allow the leader + * to move on and release its lock + * + * @param {Object} message The received message + * + * @private + * @returns {void} + */ + _onIncomingMessage( message ) { + if( this._options.serverName === message.data[ 0 ] ) { + if( message.action === C.ACTIONS.LISTEN ) { + this._leadListen[ message.data[ 1 ] ] = message.data[ 2 ]; + this._startLocalDiscoveryStage( message.data[ 1 ] ); + } else if( message.action === C.ACTIONS.ACK ) { + this._nextDiscoveryStage( message.data[ 1 ] ); + } + } + } + + /** + * Process an accept or reject for a listen that is currently in progress + * and hasn't timed out yet. + * + * @param {SocketWrapper} socketWrapper The socket endpoint of the listener + * @param {String} subscriptionName The name of the subscription that a listen is in process for + * @param {Object} message Deepstream message object + * + * @private + * @returns {void} + */ + _processResponseForListenInProgress( socketWrapper, subscriptionName, message ) { if (message.action === C.ACTIONS.LISTEN_ACCEPT ) { this._accept( socketWrapper, message ); this._listenerTimeoutRegistery.rejectLateResponderThatAccepted( subscriptionName ); @@ -109,36 +160,6 @@ class ListenerRegistry { } } - /** - * Send a snapshot of all the names that match the provided pattern - * - * @param {SocketWrapper} socketWrapper the socket that send the request - * @param {Object} message parsed and validated message - * - * @private - * @returns {void} - */ - _sendSnapshot( socketWrapper, message ) { - const matchingNames = []; - const pattern = this._getPattern( socketWrapper, message ); - const existingSubscriptions = this._clientRegistry.getNames(); - const regExp = this._validatePattern( socketWrapper, pattern ); - var subscriptionName; - - if( !regExp ) { - return; - } - - for( var i = 0; i < existingSubscriptions.length; i++ ) { - subscriptionName = existingSubscriptions[ i ]; - if( subscriptionName.match( regExp ) ) { - matchingNames.push( subscriptionName ); - } - } - - socketWrapper.send( messageBuilder.getMsg( this._topic, C.ACTIONS.SUBSCRIPTIONS_FOR_PATTERN_FOUND, [ pattern, matchingNames ] ) ); - } - /** * Called by the record subscription registry whenever the subscription count decrements. * Part of the subscriptionListener interface. @@ -148,60 +169,51 @@ class ListenerRegistry { * @public * @returns {void} */ - onSubscriptionMade( name, socketWrapper, count ) { - if( this.hasActiveProvider( name ) && this._topic === C.TOPIC.RECORD ) { - socketWrapper.send( messageBuilder.getMsg( - this._topic, C.ACTIONS.SUBSCRIPTION_HAS_PROVIDER, [ name, C.TYPES.TRUE ] - ) ); + onSubscriptionMade( subscriptionName, socketWrapper, localCount ) { + if( this.hasActiveProvider( subscriptionName ) ) { + this._listenerUtils.sendHasProviderUpdateToSingleSubscriber( true, socketWrapper, subscriptionName ); return; } - if( count > 1 ) { + if( localCount > 1 ) { return; } - this._createListenMap( name ); - this._triggerNextProvider( name ); + this._startDiscoveryStage( subscriptionName ); } /** * Called by the record subscription registry whenever the subscription count increments. * Part of the subscriptionListener interface. * - * @param {String} name + * @param {String} subscriptionName * * @public * @returns {void} */ - onSubscriptionRemoved( subscriptionName, socketWrapper, count ) { - const provider = this._providedRecords[ subscriptionName ]; + onSubscriptionRemoved( subscriptionName, socketWrapper, localCount, remoteCount ) { + const provider = this._locallyProvidedRecords[ subscriptionName ]; if( !provider ) { return; } - if( count > 1 ) { + if( localCount > 1 || remoteCount > 0 ) { return; } // provider discarded, but there is still another active subscriber - if( count === 1 && provider.socketWrapper === socketWrapper) { + if( localCount === 1 && provider.socketWrapper === socketWrapper) { return; } // provider isn't a subscriber, meaning we should wait for 0 - if( count === 1 && this._clientRegistry.getSubscribers().indexOf( provider.socketWrapper ) === -1 ) { + if( localCount === 1 && this._clientRegistry.getLocalSubscribers().indexOf( provider.socketWrapper ) === -1 ) { return; } - // stop providing - this._sendHasProviderUpdate( false, subscriptionName ); - provider.socketWrapper.send( - messageBuilder.getMsg( - this._topic, C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_REMOVED, [ provider.pattern, subscriptionName ] - ) - ); - delete this._providedRecords[ subscriptionName ]; + this._listenerUtils.sendSubscriptionForPatternRemoved( provider, subscriptionName ); + this._removeActiveListener( subscriptionName, provider ); } /** @@ -213,18 +225,17 @@ class ListenerRegistry { _accept( socketWrapper, message ) { const subscriptionName = message.data[ 1 ]; - delete this._listenInProgress[ subscriptionName ]; - this._listenerTimeoutRegistery.clearTimeout( subscriptionName ); - this._providedRecords[ subscriptionName ] = { + this._locallyProvidedRecords[ subscriptionName ] = { socketWrapper: socketWrapper, pattern: message.data[ 0 ], closeListener: this._removeListener.bind( this, socketWrapper, message ) } - socketWrapper.socket.once( 'close', this._providedRecords[ subscriptionName ].closeListener ); + socketWrapper.socket.once( 'close', this._locallyProvidedRecords[ subscriptionName ].closeListener ); + this._clusterProvidedRecords.add( subscriptionName ); - this._sendHasProviderUpdate( true, subscriptionName ); + this._stopLocalDiscoveryStage( subscriptionName ); } /** @@ -237,44 +248,22 @@ class ListenerRegistry { * @returns {void} */ _addListener( socketWrapper, message ) { - const pattern = this._getPattern( socketWrapper, message ); - const regExp = this._validatePattern( socketWrapper, pattern ); + const pattern = this._listenerUtils.getPattern( socketWrapper, message ); + const regExp = this._listenerUtils.validatePattern( socketWrapper, pattern ); if( !regExp ) { return; } - const providers = this._providerRegistry.getSubscribers( pattern ); + const providers = this._providerRegistry.getLocalSubscribers( pattern ); const notInSubscriptionRegistry = !providers || providers.indexOf( socketWrapper ) === -1; if( notInSubscriptionRegistry ) { this._providerRegistry.subscribe( pattern, socketWrapper ); - this._addUniqueCloseListener( socketWrapper, this._reconcilePatternsBound ); - } - - // Create pattern entry (if it doesn't exist already) - if( !this._patterns[ pattern ] ) { - this._patterns[ pattern ] = regExp; } this._reconcileSubscriptionsToPatterns( regExp, pattern, socketWrapper ); } - _addUniqueCloseListener(socketWrapper, eventHandler) { - var eventName = 'close'; - var socketListeners = socketWrapper.socket.listeners( eventName ); - var listenerFound = false; - for( var i=0; i { + + if( success ) { + if( this.hasActiveProvider( subscriptionName ) ) { + this._uniqueStateProvider.release( this._listenerUtils.getUniqueLockName( subscriptionName ) ); + return; + } + this._leadingListen[ subscriptionName ] = remoteListenArray; + this._startLocalDiscoveryStage( subscriptionName ); + } + } ); } /** - * Create a has provider update message + * called when a subscription has been provided to clear down the discovery stage, or when an ack has + * been recieved via the message bus + * + * @param {String} subscriptionName check if the subscription has a provider yet, if not trigger + * the next request via the message bus * * @private - * @returns {Message} + * @returns {void} */ - _createHasProviderMessage(hasProvider, topic, subscriptionName) { - return messageBuilder.getMsg( - topic, - C.ACTIONS.SUBSCRIPTION_HAS_PROVIDER, - [subscriptionName, (hasProvider ? C.TYPES.TRUE : C.TYPES.FALSE)] - ); + _nextDiscoveryStage( subscriptionName ) { + if( this.hasActiveProvider( subscriptionName ) || this._leadingListen[ subscriptionName ].length === 0 ) { + delete this._leadingListen[ subscriptionName ]; + this._uniqueStateProvider.release( this._listenerUtils.getUniqueLockName( subscriptionName ) ); + } else { + const nextServerName = this._leadingListen[ subscriptionName ].shift(); + this._listenerUtils.sendRemoteDiscoveryStart( nextServerName, subscriptionName ); + } } /** - * Create a map of all the listeners that patterns match the subscriptionName + * Start discovery phase once a lock is obtained from the leader within + * the cluster + * + * @param {String} subscriptionName the subscription name * * @private * @returns {void} */ - _createListenMap( subscriptionName ) { - const providers = []; - for( var pattern in this._patterns ) { - if( this._patterns[ pattern ].test( subscriptionName ) ) { - var providersForPattern = this._providerRegistry.getSubscribers( pattern ); - for( var i = 0; providersForPattern && i < providersForPattern.length; i++ ) { - providers.push( { - pattern: pattern, - socketWrapper: providersForPattern[ i ] - }); - } - } + _startLocalDiscoveryStage( subscriptionName ) { + const localListenMap = this._listenerUtils.createLocalListenMap( this._patterns, this._providerRegistry, subscriptionName ); + + if( localListenMap.length > 0 ) { + this._localListenInProgress[ subscriptionName ] = localListenMap; + this._triggerNextProvider( subscriptionName ); + } + } + + /** + * Finalises a local listener discovery stage + * + * @param {String} subscriptionName the subscription a listener is searched for + * + * @private + * @returns {void} + */ + _stopLocalDiscoveryStage( subscriptionName ) { + delete this._localListenInProgress[ subscriptionName ]; + + if( this._leadingListen[ subscriptionName ] ) { + this._nextDiscoveryStage( subscriptionName ); + } else { + this._listenerUtils.sendRemoteDiscoveryStop( this._leadListen[ subscriptionName ], subscriptionName ); + delete this._leadListen[ subscriptionName ]; } - this._listenInProgress[ subscriptionName ] = providers; } /** * Trigger the next provider in the map of providers capable of publishing * data to the specific subscriptionName * + * @param {String} subscriptionName the subscription a listener is searched for + * * @private * @returns {void} */ _triggerNextProvider( subscriptionName ) { - if( !this._listenInProgress[ subscriptionName ] ) { + const listenInProgress = this._localListenInProgress[ subscriptionName ]; + + if( typeof listenInProgress === 'undefined' ) { return; } - //TODO: Needs tests - if( this._listenInProgress[ subscriptionName ].length === 0 ) { - delete this._listenInProgress[ subscriptionName ]; + if( listenInProgress.length === 0 ) { + this._stopLocalDiscoveryStage( subscriptionName ); return; } - const provider = this._listenInProgress[ subscriptionName ].shift(); + const provider = listenInProgress.shift(); this._listenerTimeoutRegistery.addTimeout( subscriptionName, provider, this._triggerNextProvider.bind( this ) ); - provider.socketWrapper.send( messageBuilder.getMsg( - this._topic, C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, [ provider.pattern, subscriptionName ] - ) - ); + this._listenerUtils.sendSubscriptionForPatternFound( provider, subscriptionName ); } /** - * Extracts the subscription pattern from the message and notifies the sender - * if something went wrong + * Triggered when a subscription is being provided by a node in the cluster * - * @param {SocketWrapper} socketWrapper - * @param {Object} message + * @param {String} subscriptionName the subscription a listener is searched for * * @private * @returns {void} */ - _getPattern( socketWrapper, message ) { - if( message.data.length > 2 ) { - this._onMsgDataError( socketWrapper, message.raw ); - return null; + _onRecordStartProvided( subscriptionName ) { + this._listenerUtils.sendHasProviderUpdate( true, subscriptionName ); + if( this._leadingListen[ subscriptionName ] ) { + // Clears down discovery if was provided. TODO: Refactor + this._nextDiscoveryStage( subscriptionName ); } - - var pattern = message.data[ 0 ]; - - if( typeof pattern !== 'string' ) { - this._onMsgDataError( socketWrapper, pattern ); - return null; - } - - return pattern; } /** - * Validates that the pattern is not empty and is a valid regular expression + * Triggered when a subscription is stopped being provided by a node in the cluster * - * @param {SocketWrapper} socketWrapper - * @param {String} pattern + * @param {String} subscriptionName the subscription a listener is searched for * * @private - * @returns {RegExp} + * @returns {void} */ - _validatePattern( socketWrapper, pattern ) { - if( !pattern ) { - return; - } - - try{ - return new RegExp( pattern ); - } catch( e ) { - this._onMsgDataError( socketWrapper, e.toString() ); - return; + _onRecordStopProvided( subscriptionName ) { + this._listenerUtils.sendHasProviderUpdate( false, subscriptionName ); + if( !this.hasActiveProvider( subscriptionName ) && this._clientRegistry.hasName( subscriptionName ) ) { + this._startDiscoveryStage( subscriptionName ); } } /** - * Processes errors for invalid messages + * Compiles a regular expression from an incoming pattern * - * @param {SocketWrapper} socketWrapper - * @param {String} errorMsg - * @param {Event} [errorEvent] Default to C.EVENT.INVALID_MESSAGE_DATA + * @param {String} pattern the raw pattern + * @param {SocketWrapper} socketWrapper connection to the client that provided the pattern + * @param {Number} count the amount of times this pattern is present * * @private * @returns {void} */ - _onMsgDataError( socketWrapper, errorMsg, errorEvent ) { - errorEvent = errorEvent || C.EVENT.INVALID_MESSAGE_DATA; - socketWrapper.sendError( this._topic, errorEvent, errorMsg ); - this._options.logger.log( C.LOG_LEVEL.ERROR, errorEvent, errorMsg ); + _addPattern( pattern, socketWrapper, count ) { + if( count === 1 ) { + this._patterns[ pattern ] = new RegExp( pattern ); + } } /** - * Clean-up for pattern subscriptions. If a connection is lost or a listener removes - * this makes sure that the internal pattern array stays in sync with the subscription - * registry + * Deletes the pattern regex when removed + * + * @param {String} pattern the raw pattern + * @param {SocketWrapper} socketWrapper connection to the client that provided the pattern + * @param {Number} count the amount of times this pattern is present * * @private * @returns {void} */ - _reconcilePatterns() { - for( var pattern in this._patterns ) { - if( !this._providerRegistry.hasSubscribers( pattern ) ) { - delete this._patterns[ pattern ]; - } + _removePattern( pattern, socketWrapper, count ) { + if( socketWrapper ) { + this._listenerTimeoutRegistery.removeProvider( socketWrapper ); + this._listenerUtils.removeListenerFromInProgress( this._localListenInProgress, pattern, socketWrapper ); + this._removeListenerIfActive( pattern, socketWrapper ); + } + + if( count === 0 ) { + delete this._patterns[ pattern ]; } } } - -module.exports = ListenerRegistry; +module.exports = ListenerRegistry; \ No newline at end of file diff --git a/src/listen/listener-timeout-registry.js b/src/listen/listener-timeout-registry.js index 4c84564cf..44e1ee384 100644 --- a/src/listen/listener-timeout-registry.js +++ b/src/listen/listener-timeout-registry.js @@ -4,6 +4,7 @@ const messageBuilder = require( '../message/message-builder' ); const C = require( '../constants/constants' ); class ListenerTimeoutRegistry { + /** * The ListenerTimeoutRegistry is responsible for keeping track of listeners that have * been asked whether they want to provide a certain subscription, but have not yet @@ -11,6 +12,8 @@ class ListenerTimeoutRegistry { * * @param {Topic} type * @param {Map} options + * + * @constructor */ constructor( type, options ) { this._type = type; @@ -26,6 +29,12 @@ class ListenerTimeoutRegistry { * * 1) If reject, remove from map * 2) If accept, store as an accepted and reject all following accepts + * + * @param {SocketWrapper} socketWrapper + * @param {Object} message deepstream message + * + * @private + * @returns {void} */ handle( socketWrapper, message ) { const pattern = message.data[ 0 ]; @@ -49,9 +58,13 @@ class ListenerTimeoutRegistry { } } -/** + /** * Clear cache once discovery phase is complete - * @private + * + * @param {String} subscriptionName the subscription that needs to be removed + * + * @public + * @returns {void} */ clear( subscriptionName ) { delete this._timeoutMap[ subscriptionName ]; @@ -61,23 +74,42 @@ class ListenerTimeoutRegistry { /** * Called whenever a provider closes to ensure cleanup + * + * @param {SocketWrapper} socketWrapper the now closed connection endpoint + * * @private + * @returns {void} */ removeProvider( socketWrapper ) { - // TODO + for( var acceptedProvider in this._acceptedProvider ) { + if( this._acceptedProvider[ acceptedProvider ].socketWrapper === socketWrapper ) { + delete this._acceptedProvider[ acceptedProvider ]; + } + } + for( var subscriptionName in this._timeoutMap ) { + if( this._timeoutMap[ subscriptionName ] ) { + this.clearTimeout( subscriptionName ); + } + } } - /** - * Start a timeout for a provider. If it doesn't respond within - * it trigger the callback to process with the next listener. - * If the provider comes back after the timeout with a reject, - * ignore it, if it comes back with an accept and no other listener - * accepted yet, wait for the next listener response or timeout, - * if a reject just remove, if an accept and another provider already - * accepted, then send it an immediate SUBSCRIPTION_REMOVED message. - * - * @public - */ + * Starts a timeout for a provider. The following cases can apply + * + * Provider accepts within the timeout: We stop here + * Provider rejects within the timeout: We ask the next provider + * Provider doesn't respond within the timeout: We ask the next provider + * + * Provider accepts after the timeout: + * If no other provider accepted yet, we'll wait for the current request to end and stop here + * If another provider has accepted already, we'll immediatly send a SUBSCRIPTION_REMOVED message + * + * @param {String} subscriptionName [description] + * @param {[type]} provider [description] TODO!!! + * @param {Function} callback [description] + * + * @public + * @returns {void} + */ addTimeout( subscriptionName, provider, callback ) { var timeoutId = setTimeout( () => { if (this._timedoutProviders[ subscriptionName ] == null ) { @@ -85,7 +117,7 @@ class ListenerTimeoutRegistry { } this._timedoutProviders[ subscriptionName ].push( provider ); callback( subscriptionName ); - }, 20 ); // TODO: Configurable + }, this._options.listenResponseTimeout ); this._timeoutMap[ subscriptionName ] = timeoutId; } @@ -97,6 +129,7 @@ class ListenerTimeoutRegistry { */ clearTimeout( subscriptionName ) { clearTimeout( this._timeoutMap[ subscriptionName ] ); + delete this._timeoutMap[ subscriptionName ]; } /** diff --git a/src/listen/listener-utils.js b/src/listen/listener-utils.js new file mode 100644 index 000000000..9ffd62d10 --- /dev/null +++ b/src/listen/listener-utils.js @@ -0,0 +1,274 @@ +'use strict'; + +var C = require( '../constants/constants' ), + messageParser = require( '../message/message-parser' ), + messageBuilder = require( '../message/message-builder' ), + utils = require( '../utils/utils' ); + +class ListenerUtils { + + /** + * Construct a class with utils that can be called in from the main + * listener registery, but that do not actually require state. + * @param {Topic} topic the topic used to create the listen registery + * @param {Object} options the options the server was initialised with + * @param {SubscriptionRegistery} clientRegistry the client registry passed into the listen registry + */ + constructor( topic, options, clientRegistry ) { + this._uniqueLockName = `${topic}_LISTEN_LOCK`; + this._topic = topic; + this._options = options; + this._clientRegistry = clientRegistry; + } + + /** + * Remove provider from listen in progress map if it unlistens during + * discovery stage + * + * @param {Object} listensCurrentlyInProgress the listeners currently in progress + * @param {String} pattern the pattern that has been unlistened to + * @param {SocketWrapper} socketWrapper the socket wrapper of the provider that unlistened + */ + removeListenerFromInProgress( listensCurrentlyInProgress, pattern, socketWrapper ) { + var subscriptionName, i, listenInProgress; + for( subscriptionName in listensCurrentlyInProgress ) { + listenInProgress = listensCurrentlyInProgress[ subscriptionName ]; + for( i=0; i< listenInProgress.length; i++) { + if( + listenInProgress[i].socketWrapper === socketWrapper && + listenInProgress[i].pattern === pattern + ) { + listenInProgress.splice( i, 1 ); + } + } + } + } + + /** + * Sends a has provider update to a single subcriber + * @param {Boolean} hasProvider send T or F so provided status + * @param {SocketWrapper} socketWrapper the socket wrapper to send to, if it doesn't exist then don't do anything + * @param {String} subscriptionName The subscription name which provided status changed + */ + sendHasProviderUpdateToSingleSubscriber( hasProvider, socketWrapper, subscriptionName ) { + if( socketWrapper && this._topic === C.TOPIC.RECORD ) { + socketWrapper.send( this._createHasProviderMessage( hasProvider, this._topic, subscriptionName ) ); + } + } + + /** + * Sends a has provider update to all subcribers + * @param {Boolean} hasProvider send T or F so provided status + * @param {String} subscriptionName The subscription name which provided status changed + */ + sendHasProviderUpdate( hasProvider, subscriptionName ) { + if( this._topic !== C.TOPIC.RECORD ) { + return + } + this._clientRegistry.sendToSubscribers( subscriptionName, this._createHasProviderMessage( hasProvider, this._topic, subscriptionName ) ); + } + + /** + * Sent by the listen leader, and is used to inform the next server in the cluster to + * start a local discovery + * + * @param {String} serverName the name of the server to notify + * @param {String} subscriptionName the subscription to find a provider for + */ + sendRemoteDiscoveryStart( serverName, subscriptionName ) { + const messageTopic = this.getMessageBusTopic( serverName, this._topic ); + this._options.messageConnector.publish( messageTopic, { + topic: messageTopic, + action: C.ACTIONS.LISTEN, + data:[ serverName, subscriptionName, this._options.serverName ] + }); + } + + /** + * Sent by the listen follower, and is used to inform the leader that it has + * complete its local discovery start + * + * @param {String} listenLeaderServerName the name of the listen leader + * @param {String} subscriptionName the subscription to that has just finished + */ + sendRemoteDiscoveryStop( listenLeaderServerName, subscriptionName ) { + const messageTopic = this.getMessageBusTopic( listenLeaderServerName, this._topic ); + this._options.messageConnector.publish( messageTopic, { + topic: messageTopic, + action: C.ACTIONS.ACK, + data:[ listenLeaderServerName, subscriptionName ] + }); + } + + /** + * Send a subscription found to a provider + * + * @param {{patten:String, socketWrapper:SocketWrapper}} provider An object containing an provider that can provide the subscription + * @param {String} subscriptionName the subscription to find a provider for + */ + sendSubscriptionForPatternFound( provider, subscriptionName ) { + provider.socketWrapper.send( messageBuilder.getMsg( + this._topic, C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_FOUND, [ provider.pattern, subscriptionName ] + ) + ); + } + + /** + * Send a subscription removed to a provider + * + * @param {{patten:String, socketWrapper:SocketWrapper}} provider An object containing the provider that is currently the active provider + * @param {String} subscriptionName the subscription to stop providing + */ + sendSubscriptionForPatternRemoved( provider, subscriptionName ) { + provider.socketWrapper.send( + messageBuilder.getMsg( + this._topic, C.ACTIONS.SUBSCRIPTION_FOR_PATTERN_REMOVED, [ provider.pattern, subscriptionName ] + ) + ); + } + + /** + * Create a map of all the listeners that patterns match the subscriptionName locally + * @param {Object} patterns All patterns currently on this deepstream node + * @param {SusbcriptionRegistery} providerRegistry All the providers currently registered + * @param {String} subscriptionName the subscription to find a provider for + * @return {Array} An array of all the providers that can provide the subscription + */ + createRemoteListenArray( patterns, providerRegistry, subscriptionName ) { + var isMatch, pattern, providersForPattern, i; + const providerPatterns = providerRegistry.getNames(); + var servers = {}; + + for( var i=0; i 2 ) { + this.onMsgDataError( socketWrapper, message.raw ); + return null; + } + + var pattern = message.data[ 0 ]; + + if( typeof pattern !== 'string' ) { + this.onMsgDataError( socketWrapper, pattern ); + return null; + } + + return pattern; + } + + /** + * Validates that the pattern is not empty and is a valid regular expression + * + * @param {SocketWrapper} socketWrapper + * @param {String} pattern + * + * @returns {RegExp} + */ + validatePattern( socketWrapper, pattern ) { + if( !pattern ) { + return false; + } + + try{ + return new RegExp( pattern ); + } catch( e ) { + this.onMsgDataError( socketWrapper, e.toString() ); + return false ; + } + } + + /** + * Processes errors for invalid messages + * + * @param {SocketWrapper} socketWrapper + * @param {String} errorMsg + * @param {Event} [errorEvent] Default to C.EVENT.INVALID_MESSAGE_DATA + */ + onMsgDataError( socketWrapper, errorMsg, errorEvent ) { + errorEvent = errorEvent || C.EVENT.INVALID_MESSAGE_DATA; + socketWrapper.sendError( this._topic, errorEvent, errorMsg ); + // TODO: This isn't a CRITICAL error, would we say its an info + this._options.logger.log( C.LOG_LEVEL.ERROR, errorEvent, errorMsg ); + } + + /** + * Get the unique topic to use for the message bus + * @param {String} serverName the name of the server + * @param {Topic} topic + * @return {String} + */ + getMessageBusTopic( serverName, topic ) { + return C.TOPIC.LEADER_PRIVATE + serverName + topic + C.ACTIONS.LISTEN; + } + + /** + * Returns the unique lock when leading a listen discovery phase + * + * @param {String} subscriptionName the subscription to find a provider for + * + * @return {String} + */ + getUniqueLockName( subscriptionName ) { + return `${this._uniqueLockName}_${subscriptionName}`; + } + + /** + * Create a has provider update message + * + * @returns {Message} + */ + _createHasProviderMessage(hasProvider, topic, subscriptionName) { + return messageBuilder.getMsg( + topic, + C.ACTIONS.SUBSCRIPTION_HAS_PROVIDER, + [subscriptionName, (hasProvider ? C.TYPES.TRUE : C.TYPES.FALSE)] + ); + } +} + +module.exports = ListenerUtils; diff --git a/src/record/record-handler.js b/src/record/record-handler.js index 2d3759781..a20d00868 100644 --- a/src/record/record-handler.js +++ b/src/record/record-handler.js @@ -361,7 +361,7 @@ RecordHandler.prototype._$broadcastUpdate = function( name, message, originalSen * @returns {void} */ RecordHandler.prototype._broadcastTransformedUpdate = function( transformUpdate, transformPatch, name, message, originalSender ) { - var receiver = this._subscriptionRegistry.getSubscribers( name ) || [], + var receiver = this._subscriptionRegistry.getLocalSubscribers( name ) || [], metaData = { recordName: name, version: parseInt( message.data[ 1 ], 10 ) @@ -501,7 +501,7 @@ RecordHandler.prototype._delete = function( socketWrapper, message ) { * @returns {void} */ RecordHandler.prototype._onDeleted = function( name, message, originalSender ) { - var subscribers = this._subscriptionRegistry.getSubscribers( name ); + var subscribers = this._subscriptionRegistry.getLocalSubscribers( name ); var i; this._$broadcastUpdate( name, message, originalSender ); diff --git a/src/rpc/rpc-handler.js b/src/rpc/rpc-handler.js index 4aab71894..802a9f7d7 100644 --- a/src/rpc/rpc-handler.js +++ b/src/rpc/rpc-handler.js @@ -95,8 +95,8 @@ RpcHandler.prototype.getAlternativeProvider = function( rpcName, usedProviders, /* * Look within the local providers for one that hasn't been used yet */ - if( this._subscriptionRegistry.hasSubscribers( rpcName ) ) { - localProviders = this._subscriptionRegistry.getSubscribers( rpcName ); + if( this._subscriptionRegistry.hasLocalSubscribers( rpcName ) ) { + localProviders = this._subscriptionRegistry.getLocalSubscribers( rpcName ); for( i = 0; i < localProviders.length; i++ ) { if( usedProviders.indexOf( localProviders[ i ] ) === -1 ) { @@ -197,8 +197,8 @@ RpcHandler.prototype._makeRpc = function( socketWrapper, message ) { makeRemoteRpcFn, provider; - if( this._subscriptionRegistry.hasSubscribers( rpcName ) ) { - provider = this._subscriptionRegistry.getRandomSubscriber( rpcName ); + if( this._subscriptionRegistry.hasLocalSubscribers( rpcName ) ) { + provider = this._subscriptionRegistry.getRandomLocalSubscriber( rpcName ); new Rpc( this, socketWrapper, provider, this._options, message ); } else { makeRemoteRpcFn = this._makeRemoteRpc.bind( this, socketWrapper, message ); @@ -302,7 +302,7 @@ RpcHandler.prototype._onPrivateMessage = function( msg ) { */ RpcHandler.prototype._respondToProviderQuery = function( msg ) { var rpcName = msg.data[ 0 ], - providers = this._subscriptionRegistry.getSubscribers( rpcName ), + providers = this._subscriptionRegistry.getLocalSubscribers( rpcName ), queryResponse; if( !providers ) { diff --git a/src/utils/subscription-registry.js b/src/utils/subscription-registry.js index 686774139..a6b4c0521 100644 --- a/src/utils/subscription-registry.js +++ b/src/utils/subscription-registry.js @@ -1,268 +1,346 @@ -var C = require( '../constants/constants' ); - -/** - * A generic mechanism to handle subscriptions from sockets to topics. - * A bit like an event-hub, only that it registers SocketWrappers rather - * than functions - * - * @constructor - * - * @param {Object} options deepstream options - * @param {String} topic one of C.TOPIC - * @param {[SubscriptionListener]} subscriptionListener Optional. A class exposing a onSubscriptionMade - * and onSubscriptionRemoved method. - */ -var SubscriptionRegistry = function( options, topic, subscriptionListener ) { - this._subscriptions = {}; - this._options = options; - this._topic = topic; - this._subscriptionListener = subscriptionListener; - this._unsubscribeAllFunctions = []; - this._constants = { - MULTIPLE_SUBSCRIPTIONS: C.EVENT.MULTIPLE_SUBSCRIPTIONS, - SUBSCRIBE: C.ACTIONS.SUBSCRIBE, - UNSUBSCRIBE: C.ACTIONS.UNSUBSCRIBE, - NOT_SUBSCRIBED: C.EVENT.NOT_SUBSCRIBED - }; -}; - -/** -* This method allows you to customise the SubscriptionRegistry so that it can send custom events and ack messages back. -* For example, when using the C.ACTIONS.LISTEN, you would override SUBSCRIBE with C.ACTIONS.SUBSCRIBE and UNSUBSCRIBE with UNSUBSCRIBE -* -* @param {string} name The name of the the variable to override. This can be either MULTIPLE_SUBSCRIPTIONS, SUBSCRIBE, UNSUBSCRIBE, NOT_SUBSCRIBED -* @param {string} value The value to override with. -*/ -SubscriptionRegistry.prototype.setAction = function( name, value ) { - this._constants[ name.toUpperCase() ] = value; -}; - -/** - * Sends a message string to all subscribers - * - * @param {String} name the name/topic the subscriber was previously registered for - * @param {String} msgString the message as string - * @param {[SocketWrapper]} sender an optional socketWrapper that shouldn't receive the message - * - * @public - * @returns {void} - */ -SubscriptionRegistry.prototype.sendToSubscribers = function( name, msgString, sender ) { - if( this._subscriptions[ name ] === undefined ) { - return; - } +'use strict'; + +const C = require( '../constants/constants' ); +const DistributedStateRegistry = require( '../cluster/distributed-state-registry' ); - var i, l = this._subscriptions[ name ].length; +class SubscriptionRegistry { - for( i = 0; i < l; i++ ) { - if( this._subscriptions[ name ] && - this._subscriptions[ name ][ i ] && - this._subscriptions[ name ][ i ] !== sender - ) { - this._subscriptions[ name ][ i ].send( msgString ); + /** + * A generic mechanism to handle subscriptions from sockets to topics. + * A bit like an event-hub, only that it registers SocketWrappers rather + * than functions + * + * @constructor + * + * @param {Object} options deepstream options + * @param {String} topic one of C.TOPIC + * @param {[String]} clusterTopic A unique cluster topic, if not created uses format: topic_SUBSCRIPTIONS + */ + constructor( options, topic, clusterTopic ) { + this._subscriptions = {} + this._options = options; + this._topic = topic; + this._subscriptionListener = null; + this._unsubscribeAllFunctions = []; + this._constants = { + MULTIPLE_SUBSCRIPTIONS: C.EVENT.MULTIPLE_SUBSCRIPTIONS, + SUBSCRIBE: C.ACTIONS.SUBSCRIBE, + UNSUBSCRIBE: C.ACTIONS.UNSUBSCRIBE, + NOT_SUBSCRIBED: C.EVENT.NOT_SUBSCRIBED } - } -}; - -/** - * Adds a SocketWrapper as a subscriber to a topic - * - * @param {String} name - * @param {SocketWrapper} socketWrapper - * - * @public - * @returns {void} - */ -SubscriptionRegistry.prototype.subscribe = function( name, socketWrapper ) { - if( this._subscriptions[ name ] === undefined ) { - this._subscriptions[ name ] = []; + this._clusterSubscriptions = new DistributedStateRegistry( clusterTopic || `${topic}_${C.TOPIC.SUBSCRIPTIONS}`, options ); + this._clusterSubscriptions.on( 'add', this._onClusterSubscriptionAdded.bind( this ) ); + this._clusterSubscriptions.on( 'remove', this._onClusterSubscriptionRemoved.bind( this ) ); } - if( this._subscriptions[ name ].indexOf( socketWrapper ) !== -1 ) { - var msg = 'repeat supscription to "' + name + '" by ' + socketWrapper.user; - this._options.logger.log( C.LOG_LEVEL.WARN, this._constants.MULTIPLE_SUBSCRIPTIONS, msg ); - socketWrapper.sendError( this._topic, this._constants.MULTIPLE_SUBSCRIPTIONS, name ); - return; + /** + * Return all the servers that have this subscription. + * + * @param {String} subscriptionName the subscriptionName to look for + * + * @public + * @return {Array} An array of all the servernames with this subscription + */ + getAllServers( subscriptionName ) { + return this._clusterSubscriptions.getAllServers( subscriptionName ); } - if( !this.isSubscriber( socketWrapper ) ) { - var unsubscribeAllFn = this.unsubscribeAll.bind( this, socketWrapper ); - this._unsubscribeAllFunctions.push({ - socketWrapper: socketWrapper, - fn: unsubscribeAllFn - }); - socketWrapper.socket.once( 'close', unsubscribeAllFn ); + /** + * Returns a list of all the topic this registry + * currently has subscribers for + * + * @public + * @returns {Array} names + */ + getNames() { + return this._clusterSubscriptions.getAll(); } - this._subscriptions[ name ].push( socketWrapper ); + /** + * Returns true if the subscription exists somewhere + * in the cluster + * + * @public + * @returns {Array} names + */ + hasName( subscriptionName ) { + return this._clusterSubscriptions.getAll().indexOf( subscriptionName ) !== -1; + } - if( this._subscriptionListener ) { - this._subscriptionListener.onSubscriptionMade( name, socketWrapper, this._subscriptions[ name ].length ); + /** + * This method allows you to customise the SubscriptionRegistry so that it can send custom events and ack messages back. + * For example, when using the C.ACTIONS.LISTEN, you would override SUBSCRIBE with C.ACTIONS.SUBSCRIBE and UNSUBSCRIBE with UNSUBSCRIBE + * + * @param {string} name The name of the the variable to override. This can be either MULTIPLE_SUBSCRIPTIONS, SUBSCRIBE, UNSUBSCRIBE, NOT_SUBSCRIBED + * @param {string} value The value to override with. + * + * @public + * @returns {void} + */ + setAction( name, value ) { + this._constants[ name.toUpperCase() ] = value; } - var logMsg = 'for ' + this._topic + ':' + name + ' by ' + socketWrapper.user; - this._options.logger.log( C.LOG_LEVEL.DEBUG, this._constants.SUBSCRIBE, logMsg ); - socketWrapper.sendMessage( this._topic, C.ACTIONS.ACK, [ this._constants.SUBSCRIBE, name ] ); -}; - -/** - * Removes a SocketWrapper from the list of subscriptions for a topic - * - * @param {String} name - * @param {SocketWrapper} socketWrapper - * @param {Boolean} silent supresses logs and unsubscribe ACK messages - * - * @public - * @returns {void} - */ -SubscriptionRegistry.prototype.unsubscribe = function( name, socketWrapper, silent ) { - var msg, i; - - for( i = 0; i < this._unsubscribeAllFunctions.length; i++ ) { - if( this._unsubscribeAllFunctions[ i ].socketWrapper === socketWrapper ) { - socketWrapper.socket.removeListener( 'close', this._unsubscribeAllFunctions[ i ].fn ); - this._unsubscribeAllFunctions.splice( i, 1 ); - break; + /** + * Sends a message string to all subscribers + * + * @param {String} name the name/topic the subscriber was previously registered for + * @param {String} msgString the message as string + * @param {[SocketWrapper]} sender an optional socketWrapper that shouldn't receive the message + * + * @public + * @returns {void} + */ + sendToSubscribers( name, msgString, sender ) { + if( this._subscriptions[ name ] === undefined ) { + return; + } + + var i, l = this._subscriptions[ name ].length; + + for( i = 0; i < l; i++ ) { + if( this._subscriptions[ name ] && + this._subscriptions[ name ][ i ] && + this._subscriptions[ name ][ i ] !== sender + ) { + this._subscriptions[ name ][ i ].send( msgString ); + } } } - if( this._subscriptions[ name ] === undefined || - this._subscriptions[ name ].indexOf( socketWrapper ) === -1 ) { - msg = socketWrapper.user + ' is not subscribed to ' + name; - this._options.logger.log( C.LOG_LEVEL.WARN, this._constants.NOT_SUBSCRIBED, msg ); - socketWrapper.sendError( this._topic, this._constants.NOT_SUBSCRIBED, name ); - return; + /** + * Adds a SocketWrapper as a subscriber to a topic + * + * @param {String} name + * @param {SocketWrapper} socketWrapper + * + * @public + * @returns {void} + */ + subscribe( name, socketWrapper ) { + if( this._subscriptions[ name ] === undefined ) { + this._subscriptions[ name ] = []; + } + + if( this._subscriptions[ name ].indexOf( socketWrapper ) !== -1 ) { + var msg = 'repeat supscription to "' + name + '" by ' + socketWrapper.user; + this._options.logger.log( C.LOG_LEVEL.WARN, this._constants.MULTIPLE_SUBSCRIPTIONS, msg ); + socketWrapper.sendError( this._topic, this._constants.MULTIPLE_SUBSCRIPTIONS, name ); + return; + } + + if( !this.isLocalSubscriber( socketWrapper ) ) { + var unsubscribeAllFn = this.unsubscribeAll.bind( this, socketWrapper ); + this._unsubscribeAllFunctions.push({ + socketWrapper: socketWrapper, + fn: unsubscribeAllFn + }); + socketWrapper.socket.once( 'close', unsubscribeAllFn ); + } + + this._subscriptions[ name ].push( socketWrapper ); + + if( this._subscriptionListener ) { + this._subscriptionListener.onSubscriptionMade( + name, + socketWrapper, + this._subscriptions[ name ].length + ); + } + + this._clusterSubscriptions.add( name ); + + var logMsg = 'for ' + this._topic + ':' + name + ' by ' + socketWrapper.user; + this._options.logger.log( C.LOG_LEVEL.DEBUG, this._constants.SUBSCRIBE, logMsg ); + socketWrapper.sendMessage( this._topic, C.ACTIONS.ACK, [ this._constants.SUBSCRIBE, name ] ); } - if( this._subscriptions[ name ].length === 1 ) { - delete this._subscriptions[ name ]; - } else { - this._subscriptions[ name ].splice( this._subscriptions[ name ].indexOf( socketWrapper ), 1 ); + /** + * Removes a SocketWrapper from the list of subscriptions for a topic + * + * @param {String} name + * @param {SocketWrapper} socketWrapper + * @param {Boolean} silent supresses logs and unsubscribe ACK messages + * + * @public + * @returns {void} + */ + unsubscribe( name, socketWrapper, silent ) { + var msg, i; + + for( i = 0; i < this._unsubscribeAllFunctions.length; i++ ) { + if( this._unsubscribeAllFunctions[ i ].socketWrapper === socketWrapper ) { + socketWrapper.socket.removeListener( 'close', this._unsubscribeAllFunctions[ i ].fn ); + this._unsubscribeAllFunctions.splice( i, 1 ); + break; + } + } + + if( this._subscriptions[ name ] === undefined || + this._subscriptions[ name ].indexOf( socketWrapper ) === -1 ) { + msg = socketWrapper.user + ' is not subscribed to ' + name; + this._options.logger.log( C.LOG_LEVEL.WARN, this._constants.NOT_SUBSCRIBED, msg ); + socketWrapper.sendError( this._topic, this._constants.NOT_SUBSCRIBED, name ); + return; + } + + if( this._subscriptions[ name ].length === 1 ) { + this._clusterSubscriptions.remove( name ); + delete this._subscriptions[ name ]; + } else { + this._subscriptions[ name ].splice( this._subscriptions[ name ].indexOf( socketWrapper ), 1 ); + } + + if( this._subscriptionListener ) { + //TODO: OPTIMISE!! + const allServerNames = Object.keys( this._clusterSubscriptions.getAllServers( name ) ); + if( allServerNames.indexOf( this._options.serverName ) > -1 ) { + allServerNames.splice( allServerNames.indexOf( this._options.serverName ), 1 ); + } + this._subscriptionListener.onSubscriptionRemoved( + name, + socketWrapper, + this._subscriptions[ name ] ? this._subscriptions[ name ].length : 0, + allServerNames.length + ); + } + + if( !silent ) { + var logMsg = 'for ' + this._topic + ':' + name + ' by ' + socketWrapper.user; + this._options.logger.log( C.LOG_LEVEL.DEBUG, this._constants.UNSUBSCRIBE, logMsg ); + socketWrapper.sendMessage( this._topic, C.ACTIONS.ACK, [ this._constants.UNSUBSCRIBE, name ] ); + } } - if( this._subscriptionListener ) { - this._subscriptionListener.onSubscriptionRemoved( name, socketWrapper, (this._subscriptions[ name ] || [] ).length ); + /** + * Removes the SocketWrapper from all subscriptions. This is also called + * when the socket closes + * + * @param {SocketWrapper} socketWrapper + * + * @public + * @returns {void} + */ + unsubscribeAll( socketWrapper ) { + var name, + index; + + for( name in this._subscriptions ) { + index = this._subscriptions[ name ].indexOf( socketWrapper ); + + if( index !== -1 ) { + this.unsubscribe( name, socketWrapper ); + } + } } - if( !silent ) { - var logMsg = 'for ' + this._topic + ':' + name + ' by ' + socketWrapper.user; - this._options.logger.log( C.LOG_LEVEL.DEBUG, this._constants.UNSUBSCRIBE, logMsg ); - socketWrapper.sendMessage( this._topic, C.ACTIONS.ACK, [ this._constants.UNSUBSCRIBE, name ] ); + /** + * Returns true if socketWrapper is subscribed to any of the events in + * this registry. This is useful to bind events on close only once + * + * @param {SocketWrapper} socketWrapper + * + * @public + * @returns {Boolean} isLocalSubscriber + */ + isLocalSubscriber( socketWrapper ) { + for( var name in this._subscriptions ) { + if( this._subscriptions[ name ].indexOf( socketWrapper ) !== -1 ) { + return true; + } + } + + return false; } -}; - -/** - * Removes the SocketWrapper from all subscriptions. This is also called - * when the socket closes - * - * @param {SocketWrapper} socketWrapper - * - * @public - * @returns {void} - */ -SubscriptionRegistry.prototype.unsubscribeAll = function( socketWrapper ) { - var name, - index; - - for( name in this._subscriptions ) { - index = this._subscriptions[ name ].indexOf( socketWrapper ); - - if( index !== -1 ) { - this.unsubscribe( name, socketWrapper ); + + /** + * Returns an array of SocketWrappers that are subscribed + * to or null if there are no subscribers + * + * @param {String} name + * + * @public + * @returns {Array} SocketWrapper[] + */ + getLocalSubscribers( name ) { + if( this.hasLocalSubscribers( name ) ) { + return this._subscriptions[ name ].slice(); + } else { + return null; } } -}; - -/** - * Returns true if socketWrapper is subscribed to any of the events in - * this registry. This is useful to bind events on close only once - * - * @param {SocketWrapper} socketWrapper - * - * @public - * @returns {Boolean} isSubscriber - */ -SubscriptionRegistry.prototype.isSubscriber = function( socketWrapper ) { - for( var name in this._subscriptions ) { - if( this._subscriptions[ name ].indexOf( socketWrapper ) !== -1 ) { - return true; + + /** + * Returns a random SocketWrapper out of the array + * of SocketWrappers that are subscribed to + * + * @param {String} name + * + * @public + * @returns {SocketWrapper} + */ + getRandomLocalSubscriber( name ) { + var subscribers = this.getLocalSubscribers( name ); + + if( subscribers ) { + return subscribers[ Math.floor( Math.random() * subscribers.length ) ]; + } else { + return null; } } - return false; -}; - -/** - * Returns an array of SocketWrappers that are subscribed - * to or null if there are no subscribers - * - * @param {String} name - * - * @public - * @returns {Array} SocketWrapper[] - */ -SubscriptionRegistry.prototype.getSubscribers = function( name ) { - if( this.hasSubscribers( name ) ) { - return this._subscriptions[ name ].slice(); - } else { - return null; + /** + * Returns true if there are SocketWrappers that + * are subscribed to or false if there + * aren't any subscribers + * + * @param {String} name + * + * @public + * @returns {Boolean} hasLocalSubscribers + */ + hasLocalSubscribers( name ) { + return !!this._subscriptions[ name ] && this._subscriptions[ name ].length !== 0; } -}; - -/** - * Returns a random SocketWrapper out of the array - * of SocketWrappers that are subscribed to - * - * @param {String} name - * - * @public - * @returns {SocketWrapper} - */ -SubscriptionRegistry.prototype.getRandomSubscriber = function( name ) { - var subscribers = this.getSubscribers( name ); - - if( subscribers ) { - return subscribers[ Math.floor( Math.random() * subscribers.length ) ]; - } else { - return null; + + /** + * Allows to set a subscriptionListener after the class had been instantiated + * + * @param {SubscriptionListener} subscriptionListener - a class exposing a onSubscriptionMade and onSubscriptionRemoved method + * + * @public + * @returns {void} + */ + setSubscriptionListener( subscriptionListener ) { + this._subscriptionListener = subscriptionListener; } -}; - -/** - * Returns true if there are SocketWrappers that - * are subscribed to or false if there - * aren't any subscribers - * - * @param {String} name - * - * @public - * @returns {Boolean} hasSubscribers - */ -SubscriptionRegistry.prototype.hasSubscribers = function( name ) { - return !!this._subscriptions[ name ] && this._subscriptions[ name ].length !== 0; -}; - -/** - * Returns a list of all the topic this registry - * currently has subscribers for - * - * @public - * @returns {Array} names - */ -SubscriptionRegistry.prototype.getNames = function() { - return Object.keys( this._subscriptions ); -}; - -/** - * Allows to set a subscriptionListener after the class had been instantiated - * - * @param {SubscriptionListener} subscriptionListener - a class exposing a onSubscriptionMade and onSubscriptionRemoved method - * - * @public - * @returns {void} - */ -SubscriptionRegistry.prototype.setSubscriptionListener = function( subscriptionListener ) { - this._subscriptionListener = subscriptionListener; -}; + + /** + * Called when a subscription has been added to the cluster + * This can be invoked locally or remotely, so we check if it + * is a local invocation and ignore it if so in favour of the + * call done from subscribe + * @param {String} name the name that was added + */ + _onClusterSubscriptionAdded( name ) { + if( this._subscriptionListener && !this._subscriptions[ name ] ) { + this._subscriptionListener.onSubscriptionMade( name, null, 1 ); + } + } + + /** + * Called when a subscription has been removed from the cluster + * This can be invoked locally or remotely, so we check if it + * is a local invocation and ignore it if so in favour of the + * call done from unsubscribe + * @param {String} name the name that was removed + */ + _onClusterSubscriptionRemoved( name ) { + if( this._subscriptionListener && !this._subscriptions[ name ] ) { + this._subscriptionListener.onSubscriptionRemoved( name, null, 0, 0 ); + } + } + +} module.exports = SubscriptionRegistry; diff --git a/src/webrtc/webrtc-handler.js b/src/webrtc/webrtc-handler.js index f5e19e81f..e04b69d99 100644 --- a/src/webrtc/webrtc-handler.js +++ b/src/webrtc/webrtc-handler.js @@ -41,7 +41,9 @@ var C = require( '../constants/constants' ), */ var WebRtcHandler = function( options ) { this._options = options; - this._calleeRegistry = new SubscriptionRegistry( this._options, C.TOPIC.WEBRTC, this ); + this._calleeRegistry = new SubscriptionRegistry( this._options, C.TOPIC.WEBRTC ); + this._calleeRegistry.setSubscriptionListener( this ); + this._calleeListenerRegistry = new SubscriptionRegistry( this._options, C.TOPIC.WEBRTC ); this._callInitiatiorRegistry = new SubscriptionRegistry( this._options, C.TOPIC.WEBRTC ); }; @@ -169,19 +171,19 @@ WebRtcHandler.prototype._forwardMessage = function( socketWrapper, message ) { data = message.data[ 2 ]; // Response - if( this._callInitiatiorRegistry.hasSubscribers( receiverName ) ){ + if( this._callInitiatiorRegistry.hasLocalSubscribers( receiverName ) ){ this._callInitiatiorRegistry.sendToSubscribers( receiverName, message.raw ); } // Request else { - if( !this._calleeRegistry.hasSubscribers( receiverName ) ) { + if( !this._calleeRegistry.hasLocalSubscribers( receiverName ) ) { this._options.logger.log( C.LOG_LEVEL.WARN, C.EVENT.UNKNOWN_CALLEE, receiverName ); socketWrapper.sendError( C.TOPIC.WEBRTC, C.EVENT.UNKNOWN_CALLEE, receiverName ); return; } - if( !this._callInitiatiorRegistry.hasSubscribers( senderName ) ) { + if( !this._callInitiatiorRegistry.hasLocalSubscribers( senderName ) ) { this._callInitiatiorRegistry.subscribe( senderName, socketWrapper ); } @@ -202,14 +204,14 @@ WebRtcHandler.prototype._forwardMessage = function( socketWrapper, message ) { WebRtcHandler.prototype._clearInitiator = function( socketWrapper, message ) { var subscribers; var subscriberId; - if( this._callInitiatiorRegistry.hasSubscribers( message.data[ 0 ] ) ) { + if( this._callInitiatiorRegistry.hasLocalSubscribers( message.data[ 0 ] ) ) { subscriberId = message.data[ 0 ]; - } else if( this._callInitiatiorRegistry.hasSubscribers( message.data[ 1 ] ) ) { + } else if( this._callInitiatiorRegistry.hasLocalSubscribers( message.data[ 1 ] ) ) { subscriberId = message.data[ 1 ]; } if( subscriberId ) { - subscribers = this._callInitiatiorRegistry.getSubscribers( subscriberId ); + subscribers = this._callInitiatiorRegistry.getLocalSubscribers( subscriberId ); this._callInitiatiorRegistry.unsubscribe( subscriberId, subscribers[ 0 ] ); } }; @@ -233,7 +235,7 @@ WebRtcHandler.prototype._checkIsAlive = function( socketWrapper, message ) { return; } - isAlive = this._callInitiatiorRegistry.hasSubscribers( remoteId ) || this._calleeRegistry.hasSubscribers( remoteId ); + isAlive = this._callInitiatiorRegistry.hasLocalSubscribers( remoteId ) || this._calleeRegistry.hasLocalSubscribers( remoteId ); socketWrapper.sendMessage( C.TOPIC.WEBRTC, C.ACTIONS.WEBRTC_IS_ALIVE, [ remoteId, isAlive ] ); }; diff --git a/test/cluster/cluster-registry-clusterSpec.js b/test/cluster/cluster-registry-clusterSpec.js index 3309f3cd6..0708ac62c 100644 --- a/test/cluster/cluster-registry-clusterSpec.js +++ b/test/cluster/cluster-registry-clusterSpec.js @@ -31,7 +31,6 @@ describe( 'distributed-state-registry adds and removes names', function(){ return result; }; - var a,b,c; it( 'creates three registries', function( done ){ diff --git a/test/cluster/cluster-registrySpec.js b/test/cluster/cluster-registrySpec.js index 057c7a005..1e606c7f9 100644 --- a/test/cluster/cluster-registrySpec.js +++ b/test/cluster/cluster-registrySpec.js @@ -5,8 +5,13 @@ var connectionEndpointMock = { getBrowserConnectionCount: function() { return 8; }, getTcpConnectionCount: function() { return 7; } }; +var EventEmitter = require( 'events' ).EventEmitter; + +var realProcess; +var emitter; describe( 'distributed-state-registry adds and removes names', function(){ + var clusterRegistry; var addSpy = jasmine.createSpy( 'add' ); @@ -152,7 +157,27 @@ describe( 'distributed-state-registry adds and removes names', function(){ }, 500 ); }); - it( 'sends a remove message when the process ends', function(){ + var expectedLength; + it( 'publishes leave message when closing down', function(){ + expectedLength = options.messageConnector.publishedMessages.length + 1; + + clusterRegistry.leaveCluster(); + + expect( options.messageConnector.lastPublishedMessage ).toEqual( + { topic: 'CL', action: 'RM', data: [ 'server-name-a' ] } + ); + expect( options.messageConnector.publishedMessages.length ).toBe( expectedLength ); + }); + + it( 'doesn\'t publish leave message when trying to leave twice', function(){ + clusterRegistry.leaveCluster(); + + expect( options.messageConnector.publishedMessages.length ).toBe( expectedLength ); + }); + + // we can't simulate an exit via the process since it is triggered + // to test harness + xit( 'sends a remove message when the process ends', function(){ options.messageConnector.reset(); process.emit( 'exit' ); expect( options.messageConnector.lastPublishedMessage ).toEqual(({ diff --git a/test/cluster/cluster-unique-state-providerClusterSpec.js b/test/cluster/cluster-unique-state-providerClusterSpec.js new file mode 100644 index 000000000..7d2157830 --- /dev/null +++ b/test/cluster/cluster-unique-state-providerClusterSpec.js @@ -0,0 +1,81 @@ +var ClusterUniqueStateProvider = require( '../../src/cluster/cluster-unique-state-provider' ); +var C = require( '../../src/constants/constants' ); +var LocalMessageConnector = new (require( '../mocks/local-message-connector' ))(); +var ClusterRegistry = require( '../../src/cluster/cluster-registry' ); + +function createServer( serverName, clusterScore ) { + var connectionEndpoint = { + getBrowserConnectionCount: () => {}, + getTcpConnectionCount: () => {} + }; + + var options = { + serverName: serverName, + messageConnector: LocalMessageConnector, + logger: { log: jasmine.createSpy( 'log' ) }, + lockTimeout: 10, + lockRequestTimeout: 5 + }; + + var result = {}; + result.options = options; + result.clusterRegistry = new ClusterRegistry( options, connectionEndpoint ); + result.clusterRegistry._leaderScore = clusterScore; + result.uniqueStateProvider = new ClusterUniqueStateProvider( options, result.clusterRegistry ); + result.spy = jasmine.createSpy( serverName ); + + return result; +} + +describe( 'unique state provider handles cluster locks', function(){ + + + var a, + b, + c; + + beforeEach( ( done ) => { + a = createServer( 'servername-a', 1 ); + b = createServer( 'servername-b', 2 ); + c = createServer( 'servername-c', 3 ); + + // Done for nodes to find each other out + setTimeout( done, 50 ); + }); + + afterEach( () => { + a.clusterRegistry.leaveCluster(); + b.clusterRegistry.leaveCluster(); + c.clusterRegistry.leaveCluster(); + } ); + + it( 'provider A gets a lock, provider B can\'t get one', function(){ + a.uniqueStateProvider.get( 'a', a.spy ); + b.uniqueStateProvider.get( 'a', b.spy ); + + expect( a.spy ).toHaveBeenCalledWith( true ); + expect( b.spy ).toHaveBeenCalledWith( false ); + }); + + it( 'provider A gets a lock, releases it, then provider B can get one', function(){ + a.uniqueStateProvider.get( 'a', a.spy ); + a.uniqueStateProvider.release( 'a' ); + b.uniqueStateProvider.get( 'a', b.spy ); + + expect( a.spy ).toHaveBeenCalledWith( true ); + expect( b.spy ).toHaveBeenCalledWith( true ); + }); + + it( 'provider A gets a lock and doesn\'t release it in time', function( done ){ + a.uniqueStateProvider.get( 'a', a.spy ); + + setTimeout( function() { + b.uniqueStateProvider.get( 'a', b.spy ); + + expect( a.spy ).toHaveBeenCalledWith( true ); + expect( b.spy ).toHaveBeenCalledWith( true ); + + done(); + }, a.options.lockTimeout + 10 ); + }); +}); \ No newline at end of file diff --git a/test/cluster/cluster-unique-state-providerSpec.js b/test/cluster/cluster-unique-state-providerSpec.js index c17d99f2f..cd40e03b3 100644 --- a/test/cluster/cluster-unique-state-providerSpec.js +++ b/test/cluster/cluster-unique-state-providerSpec.js @@ -3,110 +3,166 @@ var C = require( '../../src/constants/constants' ); var MessageConnectorMock = require( '../mocks/message-connector-mock' ); var ClusterRegistryMock = require( '../mocks/cluster-registry-mock' ); +describe( 'unique state provider', function(){ + describe( 'handles local locks', function(){ + var options, uniqueStateProvider; + + beforeAll( () => { + options = { + serverName: 'server-name-a', + messageConnector: new MessageConnectorMock(), + logger: { log: jasmine.createSpy( 'log' ) }, + lockTimeout: 100, + lockRequestTimeout: 50 + }; + + var clusterRegistryMock = new ClusterRegistryMock( options ); + uniqueStateProvider = new ClusterUniqueStateProvider( options, clusterRegistryMock ); + } ); + + it( 'subscribes to the lock on message bus', function(){ + expect( options.messageConnector.lastSubscribedTopic ).toBe( 'LP_server-name-a' ); + }); -describe( 'unique state provider handles local locks', function(){ - var uniqueStateProvider; + it( 'is the leader and returns a local lock', function( done ){ + uniqueStateProvider.get( 'lock-a', function( success ){ + expect( success ).toBe( true ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + done(); + }); + }); - var options = { - serverName: 'server-name-a', - messageConnector: new MessageConnectorMock(), - logger: { log: jasmine.createSpy( 'log' ) }, - lockTimeout: 100, - lockRequestTimeout: 50 - }; + it( 'has kept the local lock', function( done ){ + uniqueStateProvider.get( 'lock-a', function( success ){ + expect( success ).toBe( false ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + done(); + }); + }); - var clusterRegistryMock = new ClusterRegistryMock(); + it( 'releases the local lock', function( done ){ + uniqueStateProvider.release( 'lock-a') + uniqueStateProvider.get( 'lock-a', function( success ){ + expect( success ).toBe( true ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + done(); + }); + }); + }); + describe( 'handles remote locks', function(){ + var options, uniqueStateProvider, lockCallbackA; + + beforeAll( () => { + options = { + serverName: 'server-name-a', + messageConnector: new MessageConnectorMock(), + logger: { log: jasmine.createSpy( 'log' ) }, + lockTimeout: 10, + lockRequestTimeout: 10 + }; + + var clusterRegistryMock = new ClusterRegistryMock( options ); + clusterRegistryMock.currentLeader = 'server-name-b'; + uniqueStateProvider = new ClusterUniqueStateProvider( options, clusterRegistryMock ); + lockCallbackA = jasmine.createSpy( 'lock-callback-a' ); + } ); + + beforeEach( () => { + lockCallbackA.calls.reset(); + options.logger.log.calls.reset(); + } ); + + it( 'queries for a remote lock', function(){ + uniqueStateProvider.get( 'lock-a', lockCallbackA ); + expect( lockCallbackA ).not.toHaveBeenCalled(); + expect( options.messageConnector.lastPublishedMessage ).toEqual({ + topic: 'LP_server-name-b', + action: C.ACTIONS.LOCK_REQUEST, + data: [{ + name: 'lock-a', + responseTopic: 'LP_server-name-a' + }] + }); + }); - it( 'creates the provider', function(){ - uniqueStateProvider = new ClusterUniqueStateProvider( options, clusterRegistryMock ); - expect( typeof uniqueStateProvider.get ).toBe( 'function' ); - expect( options.messageConnector.lastSubscribedTopic ).toBe( 'LP_server-name-a' ); - }); + it( 'returns a positive response for lock-a', function(){ + expect( lockCallbackA ).not.toHaveBeenCalled(); + options.messageConnector.simulateIncomingMessage({ + topic: 'LP_server-name-a', + action: C.ACTIONS.LOCK_RESPONSE, + data: [{ + name: 'lock-a', + result: true + }] + }); + expect( options.logger.log ).not.toHaveBeenCalled(); + expect( lockCallbackA ).toHaveBeenCalledWith( true ); + }); - it( 'is the leader and returns a local lock', function( done ){ - uniqueStateProvider.get( 'lock-a', function( success ){ - expect( success ).toBe( true ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - done(); + it( 'releases the remote lock', function(){ + options.messageConnector.reset(); + uniqueStateProvider.release( 'lock-a' ); + expect( options.messageConnector.lastPublishedMessage ).toEqual({ + topic: 'LP_server-name-b', + action: C.ACTIONS.LOCK_RELEASE, + data:[{ + name: 'lock-a' + }] + }); }); - }); - it( 'has kept the local lock', function( done ){ - uniqueStateProvider.get( 'lock-a', function( success ){ - expect( success ).toBe( false ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - done(); + it( 'returns false if lock request times out', function( done ){ + uniqueStateProvider.get( 'lock-a', lockCallbackA ); + setTimeout( () => { + expect( lockCallbackA ).toHaveBeenCalledWith( false ); + done(); + }, 20 ); }); - }); - it( 'releases the local lock', function( done ){ - uniqueStateProvider.release( 'lock-a') - uniqueStateProvider.get( 'lock-a', function( success ){ - expect( success ).toBe( true ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - done(); + it( 'returns false to second lock call before response', function(){ + uniqueStateProvider.get( 'lock-a', () => {} ); + uniqueStateProvider.get( 'lock-a', lockCallbackA ); + expect( lockCallbackA ).toHaveBeenCalledWith( false ); }); - }); -}); - -describe( 'unique state provider handles remove locks', function(){ - var uniqueStateProvider; - - var options = { - serverName: 'server-name-a', - messageConnector: new MessageConnectorMock(), - logger: { log: jasmine.createSpy( 'log' ) }, - lockTimeout: 100, - lockRequestTimeout: 50 - }; - - var clusterRegistryMock = new ClusterRegistryMock(); - clusterRegistryMock.currentLeader = 'server-name-b'; - var lockCallbackA = jasmine.createSpy( 'lock-callback-a' ); - - it( 'creates the provider', function(){ - uniqueStateProvider = new ClusterUniqueStateProvider( options, clusterRegistryMock ); - expect( typeof uniqueStateProvider.get ).toBe( 'function' ); - expect( options.messageConnector.lastSubscribedTopic ).toBe( 'LP_server-name-a' ); - }); - it( 'queries for a remote lock', function(){ - uniqueStateProvider.get( 'lock-a', lockCallbackA ); - expect( lockCallbackA ).not.toHaveBeenCalled(); - expect( options.messageConnector.lastPublishedMessage ).toEqual({ - topic: 'LP_server-name-b', - action: C.ACTIONS.LOCK_REQUEST, - data: [{ - name: 'lock-a', - responseTopic: 'LP_server-name-a' - }] + it( 'logs a warning on an unsupported action', function(){ + expect( lockCallbackA ).not.toHaveBeenCalled(); + options.messageConnector.simulateIncomingMessage({ + topic: 'LP_server-name-a', + action: C.ACTIONS.SUBSCRIBE, + data: [{ + name: 'lock-a', + result: true + }] + }); + expect( options.logger.log ).toHaveBeenCalled(); + expect( options.logger.log ).toHaveBeenCalledWith( C.LOG_LEVEL.WARN, C.EVENT.UNKNOWN_ACTION, C.ACTIONS.SUBSCRIBE ); }); - }); - it( 'returns a positive response for lock-a', function(){ - expect( lockCallbackA ).not.toHaveBeenCalled(); - options.messageConnector.simulateIncomingMessage({ - topic: 'LP_server-name-a', - action: C.ACTIONS.LOCK_RESPONSE, - data: [{ - name: 'lock-a', - result: true - }] + it( 'logs a warning on an unsupported action', function(){ + expect( lockCallbackA ).not.toHaveBeenCalled(); + options.messageConnector.simulateIncomingMessage({ + topic: 'LP_server-name-a', + action: C.ACTIONS.LOCK_RELEASE, + data: [] + }); + expect( options.logger.log ).toHaveBeenCalled(); + expect( options.logger.log ).toHaveBeenCalledWith( C.LOG_LEVEL.WARN, C.EVENT.INVALID_MESSAGE_DATA, [] ); }); - expect( options.logger.log ).not.toHaveBeenCalled(); - expect( lockCallbackA ).toHaveBeenCalledWith( true ); - }); - it( 'releases the remote lock', function(){ - options.messageConnector.reset(); - uniqueStateProvider.release( 'lock-a' ); - expect( options.messageConnector.lastPublishedMessage ).toEqual({ - topic: 'LP_server-name-b', - action: C.ACTIONS.LOCK_RELEASE, - data:[{ - name: 'lock-a' - }] + it( 'recieves a ', function(){ + expect( lockCallbackA ).not.toHaveBeenCalled(); + options.messageConnector.simulateIncomingMessage({ + topic: 'LP_server-name-a', + action: C.ACTIONS.LOCK_REQUEST, + data: [ { + name: 'lock-a', + responseTopic: 'LP_server-name-a' + } ] + }); + expect( options.logger.log ).toHaveBeenCalled(); + expect( options.logger.log ).toHaveBeenCalledWith( C.LOG_LEVEL.WARN, C.EVENT.INVALID_LEADER_REQUEST, 'server server-name-a assumes this node \'server-name-a\' is the leader' ); }); }); }); \ No newline at end of file diff --git a/test/cluster/distributed-state-registry-clusterSpec.js b/test/cluster/distributed-state-registry-clusterSpec.js new file mode 100644 index 000000000..1b7b15c8c --- /dev/null +++ b/test/cluster/distributed-state-registry-clusterSpec.js @@ -0,0 +1,230 @@ +var DistributedStateRegistry = require( '../../src/cluster/distributed-state-registry' ); +var LocalMessageConnector = require( '../mocks/local-message-connector' ); +var clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(); + +var createRegistry = function ( serverName, messageConnector ) { + var options = { + clusterRegistry: clusterRegistryMock, + serverName: serverName, + stateReconciliationTimeout: 10, + messageConnector: messageConnector + }; + var result = { + options: options, + addCallback: jasmine.createSpy( 'add' ), + removeCallback: jasmine.createSpy( 'remove' ), + registry: new DistributedStateRegistry( 'TEST_TOPIC', options ) + } + + result.registry.on( 'add', result.addCallback ); + result.registry.on( 'remove', result.removeCallback ); + + return result; +}; + +describe( 'distributed-state-registry cluster', function(){ + describe( 'adds and removes names', function(){ + var messageConnector = new LocalMessageConnector(); + var registryA; + var registryB; + var registryC; + + it( 'creates the registries', function(){ + registryA = createRegistry( 'server-name-a', messageConnector ); + registryB = createRegistry( 'server-name-b', messageConnector ); + registryC = createRegistry( 'server-name-c', messageConnector ); + expect( messageConnector.subscribedTopics.length ).toBe( 3 ); + }); + + it( 'adds an entry to registry a', function(){ + registryA.registry.add( 'test-entry-a' ); + expect( registryA.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); + expect( registryB.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); + expect( registryC.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); + expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a' ]); + expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a' ]); + expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a' ]); + }); + + it( 'adds an entry to registry b', function(){ + registryB.registry.add( 'test-entry-b' ); + expect( registryA.addCallback ).toHaveBeenCalledWith( 'test-entry-b' ); + expect( registryB.addCallback ).toHaveBeenCalledWith( 'test-entry-b' ); + expect( registryC.addCallback ).toHaveBeenCalledWith( 'test-entry-b' ); + expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); + expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); + expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); + }); + + it( 'adds the same entry to registry c', function(){ + registryA.addCallback.calls.reset(); + registryB.addCallback.calls.reset(); + registryC.addCallback.calls.reset(); + registryC.registry.add( 'test-entry-b' ); + expect( registryA.addCallback ).not.toHaveBeenCalled(); + expect( registryB.addCallback ).not.toHaveBeenCalled(); + expect( registryC.addCallback ).not.toHaveBeenCalled(); + expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); + expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); + expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); + }); + + it( 'removes a single node entry from registry a', function(){ + registryA.registry.remove( 'test-entry-a' ); + expect( registryA.removeCallback ).toHaveBeenCalledWith( 'test-entry-a' ); + expect( registryB.removeCallback ).toHaveBeenCalledWith( 'test-entry-a' ); + expect( registryC.removeCallback ).toHaveBeenCalledWith( 'test-entry-a' ); + expect( registryA.registry.getAll() ).toEqual([ 'test-entry-b' ]); + expect( registryB.registry.getAll() ).toEqual([ 'test-entry-b' ]); + expect( registryC.registry.getAll() ).toEqual([ 'test-entry-b' ]); + }); + + it( 'removes a multi node entry from registry b', function(){ + registryA.removeCallback.calls.reset(); + registryB.removeCallback.calls.reset(); + registryC.removeCallback.calls.reset(); + registryB.registry.remove( 'test-entry-b' ); + expect( registryA.removeCallback ).not.toHaveBeenCalled(); + expect( registryB.removeCallback ).not.toHaveBeenCalled(); + expect( registryC.removeCallback ).not.toHaveBeenCalled(); + expect( registryA.registry.getAll() ).toEqual([ 'test-entry-b' ]); + expect( registryB.registry.getAll() ).toEqual([ 'test-entry-b' ]); + expect( registryC.registry.getAll() ).toEqual([ 'test-entry-b' ]); + }); + + it( 'removes a multi node entry from registry c', function(){ + registryA.removeCallback.calls.reset(); + registryB.removeCallback.calls.reset(); + registryC.removeCallback.calls.reset(); + registryC.registry.remove( 'test-entry-b' ); + expect( registryA.removeCallback ).toHaveBeenCalledWith( 'test-entry-b' ); + expect( registryB.removeCallback ).toHaveBeenCalledWith( 'test-entry-b' ); + expect( registryC.removeCallback ).toHaveBeenCalledWith( 'test-entry-b' ); + expect( registryA.registry.getAll() ).toEqual([]); + expect( registryB.registry.getAll() ).toEqual([]); + expect( registryC.registry.getAll() ).toEqual([]); + }); + + it( 'gets all servernames with a subscription', function(){ + registryA.removeCallback.calls.reset(); + registryB.removeCallback.calls.reset(); + registryC.removeCallback.calls.reset(); + + registryB.registry.add( 'test-entry-same' ); + registryC.registry.add( 'test-entry-same' ); + + expect( registryA.registry.getAllServers( 'test-entry-same' ) ).toEqual( { 'server-name-b': true, 'server-name-c': true } ); + + registryB.registry.remove( 'test-entry-same' ); + registryC.registry.remove( 'test-entry-same' ); + + expect( registryA.registry.getAllServers( 'test-entry-same' ) ).toEqual( {} ); + }); + + it( 'removes an entire server', function(){ + registryA.removeCallback.calls.reset(); + registryB.removeCallback.calls.reset(); + registryC.removeCallback.calls.reset(); + + registryB.registry.add( 'test-entry-b-1' ); + registryB.registry.add( 'test-entry-b-2' ); + registryC.registry.add( 'test-entry-c' ); + + expect( registryA.registry.getAll() ).toEqual([ 'test-entry-b-1', 'test-entry-b-2', 'test-entry-c' ]); + + registryA.options.clusterRegistry.emit( 'remove', 'server-name-b' ); + + expect( registryA.registry.getAll() ).toEqual([ 'test-entry-c' ]); + }); + }); + + describe( 'reconciles state', function(){ + var messageConnector = new LocalMessageConnector(); + var registryA; + var registryB; + var registryC; + + it( 'creates the registries', function(){ + registryA = createRegistry( 'server-name-a', messageConnector ); + registryB = createRegistry( 'server-name-b', messageConnector ); + registryC = createRegistry( 'server-name-c', messageConnector ); + expect( messageConnector.subscribedTopics.length ).toBe( 3 ); + }); + + it( 'adds an entry to registry a', function(){ + registryA.registry.add( 'test-entry-a' ); + expect( registryA.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); + expect( registryB.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); + expect( registryC.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); + expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a' ]); + expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a' ]); + expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a' ]); + }); + + it( 'adds an entry to registry b, but drops the message resulting in a compromised state', function(){ + registryA.addCallback.calls.reset(); + registryB.addCallback.calls.reset(); + registryC.addCallback.calls.reset(); + messageConnector.dropNextMessage = true; + registryB.registry.add( 'test-entry-f' ); + expect( registryA.addCallback ).not.toHaveBeenCalled(); + expect( registryB.addCallback ).toHaveBeenCalledWith( 'test-entry-f' ); + expect( registryC.addCallback ).not.toHaveBeenCalled(); + expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a' ]); + expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-f' ]); + expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a' ]); + }); + + it( 'adds another entry to registry b, the other registries detect the compromised state', function( done ){ + registryA.addCallback.calls.reset(); + registryB.addCallback.calls.reset(); + registryC.addCallback.calls.reset(); + registryB.registry.add( 'test-entry-g' ); + expect( messageConnector.messages.length ).toBe( 11 ); + expect( registryA.addCallback ).toHaveBeenCalledWith( 'test-entry-g' ); + expect( registryB.addCallback ).toHaveBeenCalledWith( 'test-entry-g' ); + expect( registryC.addCallback ).toHaveBeenCalledWith( 'test-entry-g' ); + expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-g' ]); + expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-f', 'test-entry-g' ]); + expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-g' ]); + setTimeout( done, 50 ); + }); + + it( 'has reconciled the compromised state', function(){ + expect( messageConnector.messages.length ).toBe( 14 ); + + // registry A asks for state + expect( messageConnector.messages[ 0 ].data.action ).toBe( 'DISTRIBUTED_STATE_REQUEST_FULL_STATE' ); + expect( messageConnector.messages[ 1 ].data.action ).toBe( 'DISTRIBUTED_STATE_FULL_STATE' ); + + // registry B asks for state + expect( messageConnector.messages[ 2 ].data.action ).toBe( 'DISTRIBUTED_STATE_REQUEST_FULL_STATE' ); + expect( messageConnector.messages[ 3 ].data.action ).toBe( 'DISTRIBUTED_STATE_FULL_STATE' ); + expect( messageConnector.messages[ 4 ].data.action ).toBe( 'DISTRIBUTED_STATE_FULL_STATE' ); + + // registry C asks for state + expect( messageConnector.messages[ 5 ].data.action ).toBe( 'DISTRIBUTED_STATE_REQUEST_FULL_STATE' ); + expect( messageConnector.messages[ 6 ].data.action ).toBe( 'DISTRIBUTED_STATE_FULL_STATE' ); + expect( messageConnector.messages[ 7 ].data.action ).toBe( 'DISTRIBUTED_STATE_FULL_STATE' ); + expect( messageConnector.messages[ 8 ].data.action ).toBe( 'DISTRIBUTED_STATE_FULL_STATE' ); + + // add 'test-entry-a' + expect( messageConnector.messages[ 9 ].data.action ).toBe( 'DISTRIBUTED_STATE_ADD' ); + // add 'test-entry-g', 'test-entry-f' has been dropped + expect( messageConnector.messages[ 10 ].data.action ).toBe( 'DISTRIBUTED_STATE_ADD' ); + // full state request from either A or C arrives + expect( messageConnector.messages[ 11 ].data.action ).toBe( 'DISTRIBUTED_STATE_REQUEST_FULL_STATE' ); + // B response immediatly with full state + expect( messageConnector.messages[ 12 ].data.action ).toBe( 'DISTRIBUTED_STATE_FULL_STATE' ); + // full state request from the other registry (either A or C) arrives, but is ignored as fulls state has already + // been send within stateReconciliationTimeout + expect( messageConnector.messages[ 13 ].data.action ).toBe( 'DISTRIBUTED_STATE_REQUEST_FULL_STATE' ); + + expect( registryA.addCallback ).toHaveBeenCalledWith( 'test-entry-f' ); + expect( registryC.addCallback ).toHaveBeenCalledWith( 'test-entry-f' ); + expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-g', 'test-entry-f' ]); + expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-f', 'test-entry-g' ]); + expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-g', 'test-entry-f' ]); + }); + }); +}); \ No newline at end of file diff --git a/test/cluster/distributed-state-registrySpec.js b/test/cluster/distributed-state-registrySpec.js new file mode 100644 index 000000000..cd389b7f9 --- /dev/null +++ b/test/cluster/distributed-state-registrySpec.js @@ -0,0 +1,318 @@ +var DistributedStateRegistry = require( '../../src/cluster/distributed-state-registry' ); +var MessageConnectorMock = require( '../mocks/message-connector-mock' ); +var clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(); + +describe( 'distributed-state-registry local', function(){ + describe( 'adds and removes names', function(){ + var registry; + + var options = { + clusterRegistry: clusterRegistryMock, + serverName: 'server-name-a', + stateReconciliationTimeout: 10, + messageConnector: new MessageConnectorMock() + }; + + it( 'creates the registry', function(){ + registry = new DistributedStateRegistry( 'TEST_TOPIC', options ); + expect( typeof registry.add ).toBe( 'function' ); + }); + + it( 'adds a new local name', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'add', callback ); + registry.add( 'test-name-a' ); + + expect( options.messageConnector.lastPublishedTopic ).toBe( 'TEST_TOPIC' ); + expect( options.messageConnector.lastPublishedMessage ).toEqual({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_ADD', + data: [ 'test-name-a', 'server-name-a', 2467841850 ] + }); + + expect( callback ).toHaveBeenCalledWith( 'test-name-a' ); + expect( registry.getAll() ).toEqual([ 'test-name-a' ]); + }); + + it( 'adds another new local name', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'add', callback ); + registry.add( 'test-name-b' ); + + expect( options.messageConnector.lastPublishedTopic ).toBe( 'TEST_TOPIC' ); + expect( options.messageConnector.lastPublishedMessage ).toEqual({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_ADD', + data: [ 'test-name-b', 'server-name-a', 4935683701 ] + }); + + expect( callback ).toHaveBeenCalledWith( 'test-name-b' ); + expect( registry.getAll() ).toEqual([ 'test-name-a', 'test-name-b' ]); + }); + + it( 'adds an existing local name', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + + registry.once( 'add', callback ); + registry.add( 'test-name-b' ); + + expect( options.messageConnector.lastPublishedTopic ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + expect( callback ).not.toHaveBeenCalled(); + expect( registry.getAll() ).toEqual([ 'test-name-a', 'test-name-b' ]); + }); + + it( 'adds a new remote name', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'add', callback ); + + options.messageConnector.simulateIncomingMessage({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_ADD', + data: [ 'test-name-c', 'server-name-b', 2467841852 ] + }); + + expect( options.messageConnector.lastPublishedTopic ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + expect( callback ).toHaveBeenCalledWith( 'test-name-c' ); + expect( registry.getAll() ).toEqual([ 'test-name-a', 'test-name-b', 'test-name-c' ]); + }); + + it( 'adds another new remote name', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'add', callback ); + + options.messageConnector.simulateIncomingMessage({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_ADD', + data: [ 'test-name-d', 'server-name-b', 4935683705 ] + }); + + expect( options.messageConnector.lastPublishedTopic ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + expect( callback ).toHaveBeenCalledWith( 'test-name-d' ); + expect( registry.getAll() ).toEqual([ 'test-name-a', 'test-name-b', 'test-name-c', 'test-name-d' ]); + }); + + it( 'adds an existing remote name', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'add', callback ); + + options.messageConnector.simulateIncomingMessage({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_ADD', + data: [ 'test-name-c', 'server-name-c', 2467841852 ] + }); + + expect( options.messageConnector.lastPublishedTopic ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + expect( callback ).not.toHaveBeenCalled(); + expect( registry.getAll() ).toEqual([ 'test-name-a', 'test-name-b', 'test-name-c', 'test-name-d' ]); + expect( registry.has( 'test-name-a' ) ).toBe( true ); + }); + + it( 'removes a name that exists once locally', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'remove', callback ); + + registry.remove( 'test-name-a' ); + + expect( options.messageConnector.lastPublishedTopic ).toBe( 'TEST_TOPIC' ); + expect( options.messageConnector.lastPublishedMessage ).toEqual({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_REMOVE', + data: [ 'test-name-a', 'server-name-a', 2467841851 ] + }); + expect( callback ).toHaveBeenCalledWith( 'test-name-a' ); + expect( registry.getAll() ).toEqual([ 'test-name-b', 'test-name-c', 'test-name-d' ]); + expect( registry.has( 'test-name-a' ) ).toBe( false ); + }); + + it( 'removes a remote name that exists once', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'remove', callback ); + + options.messageConnector.simulateIncomingMessage({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_REMOVE', + data: [ 'test-name-d', 'server-name-b', 2467841852 ] + }); + + expect( options.messageConnector.lastPublishedTopic ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + expect( callback ).toHaveBeenCalledWith( 'test-name-d' ); + expect( registry.getAll() ).toEqual([ 'test-name-b', 'test-name-c' ]); + }); + + it( 'doesnt remove a remote name that exists for another node', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'remove', callback ); + + options.messageConnector.simulateIncomingMessage({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_REMOVE', + data: [ 'test-name-c', 'server-name-b', 0 ] + }); + + expect( options.messageConnector.lastPublishedTopic ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + expect( callback ).not.toHaveBeenCalled(); + expect( registry.getAll() ).toEqual([ 'test-name-b', 'test-name-c' ]); + }); + + it( 'removes a remote name once the last node is removed', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'remove', callback ); + + options.messageConnector.simulateIncomingMessage({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_REMOVE', + data: [ 'test-name-c', 'server-name-c', 0 ] + }); + + expect( options.messageConnector.lastPublishedTopic ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + expect( callback ).toHaveBeenCalledWith( 'test-name-c' ); + expect( registry.getAll() ).toEqual([ 'test-name-b' ]); + }); + + it( 'ensures that no reconciliation messages where pending', function( done ){ + options.messageConnector.reset(); + setTimeout(function(){ + expect( options.messageConnector.lastPublishedTopic ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + done(); + }, 50 ); + }); + }); + + describe( 'reconciles states', function(){ + var registry; + + var options = { + clusterRegistry: clusterRegistryMock, + serverName: 'server-name-a', + stateReconciliationTimeout: 10, + logger: { log: function(){ console.log( arguments ); }}, + messageConnector: new MessageConnectorMock() + }; + + it( 'creates the registry', function(){ + registry = new DistributedStateRegistry( 'TEST_TOPIC', options ); + expect( typeof registry.add ).toBe( 'function' ); + }); + + it( 'adds a remote name with invalid checksum', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'add', callback ); + + options.messageConnector.simulateIncomingMessage({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_ADD', + data: [ 'test-name-z', 'server-name-b', 666 ] // should be 2467841875 + }); + + expect( options.messageConnector.lastPublishedTopic ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + expect( callback ).toHaveBeenCalledWith( 'test-name-z' ); + expect( registry.getAll() ).toEqual([ 'test-name-z' ]); + }); + + it( 'adds a remote name with invalid checksum', function( done ){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'add', callback ); + + options.messageConnector.simulateIncomingMessage({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_ADD', + data: [ 'test-name-c', 'server-name-b', 666 ] // should be 1054 + }); + + expect( options.messageConnector.lastPublishedTopic ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + expect( callback ).toHaveBeenCalledWith( 'test-name-c' ); + + setTimeout(function(){ + expect( options.messageConnector.lastPublishedTopic ).toBe( 'TEST_TOPIC' ); + expect( options.messageConnector.lastPublishedMessage ).toEqual({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_REQUEST_FULL_STATE', + data: [ 'server-name-b' ] + }); + expect( registry.getAll() ).toEqual([ 'test-name-z', 'test-name-c' ]); + + done(); + }, 30 ); + }); + + it( 'receives a full state update', function(){ + options.messageConnector.reset(); + var callback = jasmine.createSpy( 'callback' ); + registry.once( 'add', callback ); + + options.messageConnector.simulateIncomingMessage({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_FULL_STATE', + data: [ 'server-name-b', [ 'test-name-x', 'test-name-c' ] ] + }); + + expect( options.messageConnector.lastPublishedTopic ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + expect( callback ).toHaveBeenCalledWith( 'test-name-x' ); + expect( registry.getAll() ).toEqual([ 'test-name-c', 'test-name-x' ]); + }); + }); + + describe( 'invalid messages', function(){ + var registry; + + var options = { + clusterRegistry: clusterRegistryMock, + serverName: 'server-name-a', + stateReconciliationTimeout: 10, + logger: { log: jasmine.createSpy( 'log' ) }, + messageConnector: new MessageConnectorMock() + }; + + it( 'creates the registry', function(){ + registry = new DistributedStateRegistry( 'TEST_TOPIC', options ); + expect( typeof registry.add ).toBe( 'function' ); + }); + + it( 'recieves an invalid length', function(){ + options.messageConnector.reset(); + + options.messageConnector.simulateIncomingMessage({ + topic: 'TEST_TOPIC', + action: 'DISTRIBUTED_STATE_FULL_STATE', + data: [ 'server-name-b' ] + }); + + expect( options.logger.log ).toHaveBeenCalledWith( 2, 'INVALID_MESSAGE_DATA', [ 'server-name-b' ] ); + }); + + it( 'recieves an unknown action', function(){ + options.messageConnector.reset(); + + options.messageConnector.simulateIncomingMessage({ + topic: 'TEST_TOPIC', + action: 'ACTION_X', + data: [] + }); + + expect( options.logger.log ).toHaveBeenCalledWith( 2, 'UNKNOWN_ACTION', 'ACTION_X' ); + }); + }); +}); \ No newline at end of file diff --git a/test/event/event-data-transformSpec.js b/test/event/event-data-transformSpec.js index 9d831db29..867d61698 100644 --- a/test/event/event-data-transformSpec.js +++ b/test/event/event-data-transformSpec.js @@ -6,22 +6,28 @@ var EventHandler = require( '../../src/event/event-handler' ), messageConnectorMock = new (require( '../mocks/message-connector-mock' ))(), _msg = require( '../test-helper/test-helper' ).msg, LoggerMock = require( '../mocks/logger-mock' ), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(), createEventHandler = function( dataTransformSettigns ) { var result = { subscriber: [] }, subscriber, i; result.eventHandler = new EventHandler({ + clusterRegistry: clusterRegistryMock, messageConnector: messageConnectorMock, dataTransforms: new DataTransforms( dataTransformSettigns ), - logger: new LoggerMock() + logger: new LoggerMock(), + uniqueRegistry: { + get: function() {}, + release: function() {} + } }); for( i = 0; i < 3; i++ ) { subscriber = new SocketWrapper( new SocketMock() ); subscriber.user = 'socket_' + i; - result.eventHandler.handle( subscriber, { - topic: C.TOPIC.EVENT, + result.eventHandler.handle( subscriber, { + topic: C.TOPIC.EVENT, action: C.ACTIONS.SUBSCRIBE, raw: 'rawMessageString', data: [ 'someEvent' ] @@ -37,9 +43,9 @@ describe( 'event handler data transforms', function(){ it( 'distributes events directly if no transform is specified', function(){ var obj = createEventHandler([]); - - obj.eventHandler.handle( obj.subscriber[ 0 ], { - topic: C.TOPIC.EVENT, + + obj.eventHandler.handle( obj.subscriber[ 0 ], { + topic: C.TOPIC.EVENT, action: C.ACTIONS.EVENT, raw: 'rawMessageString', data: [ 'someEvent', 'O{"value":"A"}' ] @@ -64,8 +70,8 @@ describe( 'event handler data transforms', function(){ var obj = createEventHandler([ setting ]); - obj.eventHandler.handle( obj.subscriber[ 0 ], { - topic: C.TOPIC.EVENT, + obj.eventHandler.handle( obj.subscriber[ 0 ], { + topic: C.TOPIC.EVENT, action: C.ACTIONS.EVENT, raw: 'rawMessageString', data: [ 'someEvent', 'O{"value":"A"}' ] @@ -96,8 +102,8 @@ describe( 'event handler data transforms', function(){ var obj = createEventHandler([ setting ]); - obj.eventHandler.handle( obj.subscriber[ 0 ], { - topic: C.TOPIC.EVENT, + obj.eventHandler.handle( obj.subscriber[ 0 ], { + topic: C.TOPIC.EVENT, action: C.ACTIONS.EVENT, raw: 'rawMessageString', data: [ 'someEvent', 'O{"value":"C"}' ] diff --git a/test/event/event-handlerSpec.js b/test/event/event-handlerSpec.js index d118c040d..ea2f55d65 100644 --- a/test/event/event-handlerSpec.js +++ b/test/event/event-handlerSpec.js @@ -1,75 +1,87 @@ var EventHandler = require( '../../src/event/event-handler' ), - SocketWrapper = require( '../../src/message/socket-wrapper' ), - C = require( '../../src/constants/constants' ), - _msg = require( '../test-helper/test-helper' ).msg, - SocketMock = require( '../mocks/socket-mock' ), - messageConnectorMock = new (require( '../mocks/message-connector-mock' ))(), - LoggerMock = require( '../mocks/logger-mock' ), - options = { messageConnector: messageConnectorMock, logger: new LoggerMock() }, - eventHandler = new EventHandler( options ), - subscriptionsMessage = { - topic: C.TOPIC.EVENT, - action: C.ACTIONS.SUBSCRIBE, - raw: 'rawMessageString', - data: [ 'someEvent' ] + SocketWrapper = require( '../../src/message/socket-wrapper' ), + C = require( '../../src/constants/constants' ), + _msg = require( '../test-helper/test-helper' ).msg, + SocketMock = require( '../mocks/socket-mock' ), + messageConnectorMock = new (require( '../mocks/message-connector-mock' ))(), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(), + UniqueRegistry = require( '../../src/cluster/cluster-unique-state-provider' ), + LoggerMock = require( '../mocks/logger-mock' ), + options = { + clusterRegistry: clusterRegistryMock, + serverName: 'server-name-a', + stateReconciliationTimeout: 10, + messageConnector: messageConnectorMock, + logger: new LoggerMock(), + uniqueRegistry: { + get: function() {}, + release: function() {} + } + }, + subscriptionsMessage = { + topic: C.TOPIC.EVENT, + action: C.ACTIONS.SUBSCRIBE, + raw: 'rawMessageString', + data: [ 'someEvent' ] }, eventMessage = { topic: C.TOPIC.EVENT, action: C.ACTIONS.EVENT, raw: 'rawMessageString', data: [ 'someEvent' ] - }; + }, + eventHandler = new EventHandler( options ); describe( 'the eventHandler routes events correctly', function(){ it( 'sends an error for invalid subscription messages', function(){ - var socketWrapper = new SocketWrapper( new SocketMock(), {} ), - invalidMessage = { - topic: C.TOPIC.EVENT, - action: C.ACTIONS.SUBSCRIBE, - raw: 'rawMessageString' - }; - - eventHandler.handle( socketWrapper, invalidMessage ); - expect( socketWrapper.socket.lastSendMessage ).toBe( _msg( 'E|E|INVALID_MESSAGE_DATA|rawMessageString+' ) ); + var socketWrapper = new SocketWrapper( new SocketMock(), {} ), + invalidMessage = { + topic: C.TOPIC.EVENT, + action: C.ACTIONS.SUBSCRIBE, + raw: 'rawMessageString' + }; + + eventHandler.handle( socketWrapper, invalidMessage ); + expect( socketWrapper.socket.lastSendMessage ).toBe( _msg( 'E|E|INVALID_MESSAGE_DATA|rawMessageString+' ) ); }); it( 'sends an error for subscription messages without an event name', function(){ - var socketWrapper = new SocketWrapper( new SocketMock(), {} ), - invalidMessage = { - topic: C.TOPIC.EVENT, - action: C.ACTIONS.SUBSCRIBE, - raw: 'rawMessageString', - data: [] - }; - - eventHandler.handle( socketWrapper, invalidMessage ); - expect( socketWrapper.socket.lastSendMessage ).toBe( _msg( 'E|E|INVALID_MESSAGE_DATA|rawMessageString+' ) ); + var socketWrapper = new SocketWrapper( new SocketMock(), {} ), + invalidMessage = { + topic: C.TOPIC.EVENT, + action: C.ACTIONS.SUBSCRIBE, + raw: 'rawMessageString', + data: [] + }; + + eventHandler.handle( socketWrapper, invalidMessage ); + expect( socketWrapper.socket.lastSendMessage ).toBe( _msg( 'E|E|INVALID_MESSAGE_DATA|rawMessageString+' ) ); }); it( 'sends an error for subscription messages with an invalid action', function(){ - var socketWrapper = new SocketWrapper( new SocketMock(), {} ), - invalidMessage = { - topic: C.TOPIC.EVENT, - action: 'giberrish', - raw: 'rawMessageString', - data: [] - }; - - eventHandler.handle( socketWrapper, invalidMessage ); - expect( socketWrapper.socket.lastSendMessage ).toBe( _msg( 'E|E|UNKNOWN_ACTION|unknown action giberrish+' ) ); + var socketWrapper = new SocketWrapper( new SocketMock(), {} ), + invalidMessage = { + topic: C.TOPIC.EVENT, + action: 'giberrish', + raw: 'rawMessageString', + data: [] + }; + + eventHandler.handle( socketWrapper, invalidMessage ); + expect( socketWrapper.socket.lastSendMessage ).toBe( _msg( 'E|E|UNKNOWN_ACTION|unknown action giberrish+' ) ); }); it( 'subscribes to events', function(){ - var socketWrapper = new SocketWrapper( new SocketMock(), {} ); - expect( socketWrapper.socket.lastSendMessage ).toBe( null ); - eventHandler.handle( socketWrapper, subscriptionsMessage ); - expect( socketWrapper.socket.lastSendMessage ).toBe( _msg( 'E|A|S|someEvent+' ) ); + var socketWrapper = new SocketWrapper( new SocketMock(), {} ); + expect( socketWrapper.socket.lastSendMessage ).toBe( null ); + eventHandler.handle( socketWrapper, subscriptionsMessage ); + expect( socketWrapper.socket.lastSendMessage ).toBe( _msg( 'E|A|S|someEvent+' ) ); }); it( 'triggers events', function(){ - var socketA = new SocketWrapper( new SocketMock(), {} ), - socketB = new SocketWrapper( new SocketMock(), {} ); + var socketA = new SocketWrapper( new SocketMock(), {} ), + socketB = new SocketWrapper( new SocketMock(), {} ); eventHandler.handle( socketA, subscriptionsMessage ); eventHandler.handle( socketB, subscriptionsMessage ); @@ -123,9 +135,9 @@ describe( 'the eventHandler routes events correctly', function(){ }); it( 'unsubscribes', function(){ - var socketA = new SocketWrapper( new SocketMock(), {} ), - socketB = new SocketWrapper( new SocketMock(), {} ), - socketC = new SocketWrapper( new SocketMock(), {} ); + var socketA = new SocketWrapper( new SocketMock(), {} ), + socketB = new SocketWrapper( new SocketMock(), {} ), + socketC = new SocketWrapper( new SocketMock(), {} ); eventHandler.handle( socketA, subscriptionsMessage ); eventHandler.handle( socketB, subscriptionsMessage ); diff --git a/test/event/event-listeningSpec.js b/test/event/event-listeningSpec.js index b4ba658fa..be433db50 100644 --- a/test/event/event-listeningSpec.js +++ b/test/event/event-listeningSpec.js @@ -4,15 +4,24 @@ var EventHandler = require( '../../src/event/event-handler' ), SocketMock = require( '../mocks/socket-mock' ), SocketWrapper = require( '../../src/message/socket-wrapper' ), LoggerMock = require( '../mocks/logger-mock' ), - noopMessageConnector = require( '../../src/default-plugins/noop-message-connector' ); + noopMessageConnector = require( '../../src/default-plugins/noop-message-connector' ), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(), + options = { + clusterRegistry: clusterRegistryMock, + serverName: 'server-name-a', + stateReconciliationTimeout: 10, + messageConnector: noopMessageConnector, + logger: new LoggerMock(), + uniqueRegistry: { + get: function( name, callback ) { callback( true ); }, + release: function() {} + } + }, + eventHandler, + subscribingClient = new SocketWrapper( new SocketMock(), {} ), + listeningClient = new SocketWrapper( new SocketMock(), {} ); describe( 'event handler handles messages', function(){ - var eventHandler, - subscribingClient = new SocketWrapper( new SocketMock(), {} ), - listeningClient = new SocketWrapper( new SocketMock(), {} ), - options = { - logger: new LoggerMock() - }; it( 'creates the event handler', function(){ eventHandler = new EventHandler( options ); @@ -56,17 +65,6 @@ describe( 'event handler handles messages', function(){ expect( listeningClient.socket.lastSendMessage ).toBe( msg( 'E|SP|event\/.*|event/C+' ) ); }); - it( 'returns a snapshot of the all event that match the pattern', function(){ - eventHandler.handle( subscribingClient, { - raw: msg( 'E|LSN|user\/*' ), - topic: 'E', - action: 'LSN', - data: [ 'event\/*' ] - }); - - expect( subscribingClient.socket.lastSendMessage ).toBe( msg( 'E|SF|event/*|["event/A","event/B","event/C"]+' )); - }); - it( 'doesn\'t send messages for subsequent subscriptions', function(){ expect( listeningClient.socket.sendMessages.length ).toBe( 4 ); eventHandler.handle( subscribingClient, { diff --git a/test/listen/listener-register-local-timeoutsSpec.js b/test/listen/listener-register-local-timeoutsSpec.js index 70c6580bc..dfe37102e 100644 --- a/test/listen/listener-register-local-timeoutsSpec.js +++ b/test/listen/listener-register-local-timeoutsSpec.js @@ -34,7 +34,7 @@ describe( 'listener-registry-local-timeouts', function() { // 9 tu.providerRecievedNoNewMessages( 2 ) done() - }, 25) + }, 40) }); it( 'provider 1 times out, but then it accepts but will be ignored because provider 2 accepts as well', function(done) { @@ -57,7 +57,7 @@ describe( 'listener-registry-local-timeouts', function() { tu.providerGetsSubscriptionRemoved( 1, 'a/.*', 'a/1' ) tu.providerRecievedNoNewMessages( 2 ) done() - }, 25) + }, 40) }); it( 'provider 1 times out, but then it accept and will be used because provider 2 rejects', function(done) { @@ -77,7 +77,7 @@ describe( 'listener-registry-local-timeouts', function() { // 11. subscription doesnt have active provider tu.subscriptionHasActiveProvider( 'a/1', true ) done() - }, 25) + }, 40) }); it( 'provider 1 and 2 times out and 3 rejects, 1 rejects and 2 accepts later and 2 wins', function(done) { @@ -106,12 +106,42 @@ describe( 'listener-registry-local-timeouts', function() { // 14. First provider is not sent anything tu.providerRecievedNoNewMessages( 1 ) done() - }, 25) - }, 25) + }, 40) + }, 40) + }); + + it( '1 rejects and 2 accepts later and dies and 3 wins', function(done) { + // 5. provider 3 does listen a/[1] + tu.providerListensTo( 3, 'a/[1]' ) + // 6. Timeout occurs + setTimeout(function() { + // 7. Provider 2 gets subscription found + tu.providerGetsSubscriptionFound( 2, 'a/[0-9]', 'a/1' ) + tu.providerRecievedNoNewMessages( 1 ) + tu.providerRecievedNoNewMessages( 3 ) + // 8. Timeout occurs + setTimeout(function() { + // 9. Provider 3 gets subscription found + tu.providerGetsSubscriptionFound( 3, 'a/[1]', 'a/1' ) + tu.providerRecievedNoNewMessages( 1 ) + tu.providerRecievedNoNewMessages( 2 ) + // 10. provider 1 responds with ACCEPT + tu.providerRejects( 1, 'a/.*', 'a/1' ) + // 11. provider 2 responds with ACCEPT + tu.providerAcceptsButIsntAcknowledged( 2, 'a/[0-9]', 'a/1' ) + // 12. provider 2 dies + tu.providerLosesItsConnection( 2 ) + // 13. provider 3 responds with reject + tu.providerRejects( 3, 'a/[1]', 'a/1' ) + // 14. send publishing=true to the clients + tu.publishUpdateIsNotSentToSubscribers() + done() + }, 40) + }, 40) }); // TODO: One of those magical timeouts that randomly fail other tests - xit( 'provider 1 and 2 times out and 3 rejects, 1 and 2 accepts later and 1 wins', function(done) { + it( 'provider 1 and 2 times out and 3 rejects, 1 and 2 accepts later and 1 wins', function(done) { // 5. provider 3 does listen a/[1] tu.providerListensTo( 3, 'a/[1]' ) // 6. Timeout occurs @@ -137,7 +167,7 @@ describe( 'listener-registry-local-timeouts', function() { // 14. First provider is not sent anything tu.providerRecievedNoNewMessages( 1 ) done() - }, 25) - }, 25) + }, 40) + }, 40) }); }); \ No newline at end of file diff --git a/test/listen/listener-registry-remote-load-balancingSpec.js b/test/listen/listener-registry-remote-load-balancingSpec.js new file mode 100644 index 000000000..0c062fb96 --- /dev/null +++ b/test/listen/listener-registry-remote-load-balancingSpec.js @@ -0,0 +1,38 @@ +/* global describe, expect, it, jasmine */ +var ListenerTestUtils = require( './listener-test-utils' ); +var tu; + +xdescribe( 'listener-registry-remote-load-balancing', function() { + beforeEach(function() { + tu = new ListenerTestUtils(); + }); + + describe( 'when a listen leader', function(){ + + it( 'asks other nodes to publish', function() { + // 1. provider does listen a/.* + tu.providerListensTo( 1, 'a/.*' ) + // 2. remote provider does listen a/.* + tu.remoteProviderListensTo( 'server-b', 'a/.*' ) + // 2. remote provider does listen a/.* + tu.remoteProviderListensTo( 'server-c', 'a/[0-9]' ) + // 2. clients 1 request a/1 + tu.clientSubscribesTo( 1, 'a/1' ) + // 2. provider requests lock + tu.lockRequested( 'R_LISTEN_LOCK_a/1' ); + // 3. local provider gets a SP + tu.providerGetsSubscriptionFound( 1, 'a/.*', 'a/1' ) + // 4. local provider rejects + tu.providerRejects( 1, 'a/.*', 'a/1' ) + // 5. remote deepstream is asked + tu.remoteProviderNotifiedToStartDiscovery( 'server-b', 'a/1' ); + // 5. remote deepstream provides ack + tu.remoteProviderRespondsWithAck( 'server-b', 'a/1' ); + // 5. remote deepstream is asked + tu.remoteProviderNotifiedToStartDiscovery( 'server-c', 'a/1' ); + // 6. remote published match added + tu.remoteActiveProvidedRecordRecieved( 'server-c', 'a/1' ) + // notify has providers + }); + }); +}); \ No newline at end of file diff --git a/test/listen/listener-registrySpec.js b/test/listen/listener-registrySpec.js index 037876f11..ca48ecd24 100644 --- a/test/listen/listener-registrySpec.js +++ b/test/listen/listener-registrySpec.js @@ -4,21 +4,24 @@ var ListenerRegistry = require('../../src/listen/listener-registry'), SocketMock = require('../mocks/socket-mock'), SocketWrapper = require('../../src/message/socket-wrapper'), LoggerMock = require('../mocks/logger-mock'), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(), noopMessageConnector = require('../../src/default-plugins/noop-message-connector'); +var listenerRegistry, + options = { + clusterRegistry: clusterRegistryMock, + messageConnector: noopMessageConnector, + logger: { + log: jasmine.createSpy( 'logger' ) + } + }, + recordSubscriptionRegistryMock = { + getNames: function() { + return ['car/Mercedes', 'car/Abarth']; + } + }; describe('listener-registry errors', function() { - var listenerRegistry, - options = { - logger: { - log: jasmine.createSpy( 'logger' ) - } - }, - recordSubscriptionRegistryMock = { - getNames: function() { - return ['car/Mercedes', 'car/Abarth']; - } - }; beforeEach(function() { listeningSocket = new SocketWrapper(new SocketMock(), options); @@ -58,15 +61,4 @@ describe('listener-registry errors', function() { expect(options.logger.log).toHaveBeenCalledWith(3, 'INVALID_MESSAGE_DATA', 'SyntaxError: Invalid regular expression: /us(/: Unterminated group'); expect(socketWrapper.socket.lastSendMessage).toBe(msg('R|E|INVALID_MESSAGE_DATA|SyntaxError: Invalid regular expression: /us(/: Unterminated group+')); }); - - it('requests a snapshot with an invalid regexp', function() { - var socketWrapper = new SocketWrapper(new SocketMock()); - listenerRegistry.handle(socketWrapper, { - topic: 'R', - action: 'LSN', - data: ['xs('] - }); - expect(options.logger.log).toHaveBeenCalledWith(3, 'INVALID_MESSAGE_DATA', 'SyntaxError: Invalid regular expression: /xs(/: Unterminated group'); - expect(socketWrapper.socket.lastSendMessage).toBe(msg('R|E|INVALID_MESSAGE_DATA|SyntaxError: Invalid regular expression: /xs(/: Unterminated group+')); - }); }); \ No newline at end of file diff --git a/test/listen/listener-test-utils.js b/test/listen/listener-test-utils.js index d408e2acd..64fb6b099 100644 --- a/test/listen/listener-test-utils.js +++ b/test/listen/listener-test-utils.js @@ -5,7 +5,9 @@ var ListenerRegistry = require( '../../src/listen/listener-registry' ), SocketMock = require( '../mocks/socket-mock' ), SocketWrapper = require( '../../src/message/socket-wrapper' ), LoggerMock = require( '../mocks/logger-mock' ), - noopMessageConnector = require( '../../src/default-plugins/noop-message-connector' ), + LocalMessageConnector = require( '../mocks/local-message-connector' ), + ClusterRegistry = require( '../../src/cluster/cluster-registry' ), + UniqueRegistry = require( '../../src/cluster/cluster-unique-state-provider' ), C = require( '../../src/constants/constants' ); var topic, @@ -15,8 +17,7 @@ var topic, providers, clients, listenerRegistry, - options = { logger: new LoggerMock() }, - clientRegistry = null, + clientRegistry, messageHistory; class ListenerTestUtils { @@ -27,15 +28,34 @@ class ListenerTestUtils { subscribedTopics = []; clientRegistry = { + hasName: function( subscriptionName ) { + return subscribedTopics.indexOf( subscriptionName ); + }, getNames: function() { return subscribedTopics; }, - getSubscribers: function() { + getLocalSubscribers: function() { return subscribers; }, sendToSubscribers: sendToSubscribersMock }; + // TODO Mock process insead + process.setMaxListeners( 0 ); + + var options = { + serverName: 'server-name-a', + stateReconciliationTimeout: 10, + messageConnector: new LocalMessageConnector(), + logger: new LoggerMock(), + listenResponseTimeout: 30 + }; + options.clusterRegistry = new ClusterRegistry( options, { + getBrowserConnectionCount: function() {}, + getTcpConnectionCount: function() {} + } ); + options.uniqueRegistry = new UniqueRegistry( options, options.clusterRegistry ); + clients = [ null, // to make tests start from 1 new SocketWrapper( new SocketMock(), options ), @@ -204,7 +224,11 @@ class ListenerTestUtils { publishUpdateSentToSubscribers( subscription, state ) { var msgString = msg( `${topic}|${C.ACTIONS.SUBSCRIPTION_HAS_PROVIDER}|${subscription}|${state ? C.TYPES.TRUE : C.TYPES.FALSE}+` ) - expect( sendToSubscribersMock.calls.mostRecent().args ).toEqual( [ subscription, msgString ] ) + if( sendToSubscribersMock.calls.mostRecent() ) { + expect( sendToSubscribersMock.calls.mostRecent().args ).toEqual( [ subscription, msgString ] ) + } else { + expect( "Send to subscribers never called" ).toEqual( 0 ); + } } subscriptionHasActiveProvider( subscription, value ) { diff --git a/test/mocks/cluster-registry-mock.js b/test/mocks/cluster-registry-mock.js index fd3502039..206676720 100644 --- a/test/mocks/cluster-registry-mock.js +++ b/test/mocks/cluster-registry-mock.js @@ -3,10 +3,11 @@ const EventEmitter = require( 'events' ).EventEmitter; module.exports = class ClusterRegistryMock extends EventEmitter{ - constructor() { + constructor( options ) { super(); this.all = null; this.currentLeader = null; + this.options = options; this.reset(); } @@ -19,6 +20,10 @@ module.exports = class ClusterRegistryMock extends EventEmitter{ return this.all; } + isLeader() { + return this.currentLeader === this.options.serverName; + } + getCurrentLeader() { return this.currentLeader; } diff --git a/test/record/record-data-transformSpec.js b/test/record/record-data-transformSpec.js index adf3ffa1e..86e2f5c9f 100644 --- a/test/record/record-data-transformSpec.js +++ b/test/record/record-data-transformSpec.js @@ -6,17 +6,23 @@ var RecordHandler = require( '../../src/record/record-handler' ), SocketWrapper = require( '../../src/message/socket-wrapper' ), DataTransforms = require( '../../src/message/data-transforms' ), LoggerMock = require( '../mocks/logger-mock' ), + ClusterRegistryMock = require( '../mocks/cluster-registry-mock' ), noopMessageConnector = require( '../../src/default-plugins/noop-message-connector' ); function createRecordHandler( dataTransformSettings ) { var recordHandler, clients = [], options = { + clusterRegistry: new ClusterRegistryMock(), cache: new StorageMock(), storage: new StorageMock(), logger: new LoggerMock(), messageConnector: noopMessageConnector, - permissionHandler: { canPerformAction: function( a, b, c ){ c( null, true ); }} + permissionHandler: { canPerformAction: function( a, b, c ){ c( null, true ); }}, + uniqueRegistry: { + get: function() {}, + release: function() {} + } }; if( dataTransformSettings ) { diff --git a/test/record/record-handler-permissionSpec.js b/test/record/record-handler-permissionSpec.js index d1e25d3b4..3a2707ca7 100644 --- a/test/record/record-handler-permissionSpec.js +++ b/test/record/record-handler-permissionSpec.js @@ -5,7 +5,8 @@ var RecordHandler = require( '../../src/record/record-handler' ), SocketMock = require( '../mocks/socket-mock' ), SocketWrapper = require( '../../src/message/socket-wrapper' ), LoggerMock = require( '../mocks/logger-mock' ), - noopMessageConnector = require( '../../src/default-plugins/noop-message-connector' ); + noopMessageConnector = require( '../../src/default-plugins/noop-message-connector' ), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(); var permissionHandler = { nextResult: true, @@ -22,12 +23,17 @@ describe( 'record handler handles messages', function(){ clientA = new SocketWrapper( new SocketMock(), {} ), clientB = new SocketWrapper( new SocketMock(), {} ), options = { + clusterRegistry: clusterRegistryMock, cache: new StorageMock(), storage: new StorageMock(), storageExclusion: new RegExp( 'no-storage'), logger: new LoggerMock(), messageConnector: noopMessageConnector, - permissionHandler: permissionHandler + permissionHandler: permissionHandler, + uniqueRegistry: { + get: function( name, callback ) { callback( true ); }, + release: function() {} + } }; it( 'creates the record handler', function(){ diff --git a/test/record/record-handlerSpec.js b/test/record/record-handlerSpec.js index 430f26b65..5efb0c0eb 100644 --- a/test/record/record-handlerSpec.js +++ b/test/record/record-handlerSpec.js @@ -5,19 +5,25 @@ var RecordHandler = require( '../../src/record/record-handler' ), SocketMock = require( '../mocks/socket-mock' ), SocketWrapper = require( '../../src/message/socket-wrapper' ), LoggerMock = require( '../mocks/logger-mock' ), - noopMessageConnector = require( '../../src/default-plugins/noop-message-connector' ); + noopMessageConnector = require( '../../src/default-plugins/noop-message-connector' ), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(); describe( 'record handler handles messages', function(){ var recordHandler, clientA = new SocketWrapper( new SocketMock(), {} ), clientB = new SocketWrapper( new SocketMock(), {} ), options = { + clusterRegistry: clusterRegistryMock, cache: new StorageMock(), storage: new StorageMock(), storageExclusion: new RegExp( 'no-storage'), logger: new LoggerMock(), messageConnector: noopMessageConnector, - permissionHandler: { canPerformAction: function( a, b, c ){ c( null, true ); }} + permissionHandler: { canPerformAction: function( a, b, c ){ c( null, true ); }}, + uniqueRegistry: { + get: function() {}, + release: function() {} + } }; it( 'creates the record handler', function(){ @@ -418,12 +424,17 @@ describe( 'record handler handles messages', function(){ clientA = new SocketWrapper( new SocketMock(), {} ), clientB = new SocketWrapper( new SocketMock(), {} ), options = { + clusterRegistry: clusterRegistryMock, cache: new StorageMock(), storage: new StorageMock(), storageExclusion: new RegExp( 'no-storage'), logger: new LoggerMock(), messageConnector: noopMessageConnector, - permissionHandler: { canPerformAction: function( a, b, c ){ c( null, true ); }} + permissionHandler: { canPerformAction: function( a, b, c ){ c( null, true ); }}, + uniqueRegistry: { + get: function() {}, + release: function() {} + } }; options.cache.nextGetWillBeSynchronous = true; diff --git a/test/record/record-listeningSpec.js b/test/record/record-listeningSpec.js index 891e82a6c..63146c572 100644 --- a/test/record/record-listeningSpec.js +++ b/test/record/record-listeningSpec.js @@ -5,18 +5,24 @@ var RecordHandler = require( '../../src/record/record-handler' ), SocketMock = require( '../mocks/socket-mock' ), SocketWrapper = require( '../../src/message/socket-wrapper' ), LoggerMock = require( '../mocks/logger-mock' ), - noopMessageConnector = require( '../../src/default-plugins/noop-message-connector' ); + noopMessageConnector = require( '../../src/default-plugins/noop-message-connector' ), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(); describe( 'record handler handles messages', function(){ var recordHandler, - subscribingClient = new SocketWrapper( new SocketMock(), {} ), + subscribingClient = new SocketWrapper( new SocketMock(), {} ), listeningClient = new SocketWrapper( new SocketMock(), {} ), options = { + clusterRegistry: clusterRegistryMock, cache: new StorageMock(), storage: new StorageMock(), logger: new LoggerMock(), messageConnector: noopMessageConnector, - permissionHandler: { canPerformAction: function( a, b, c ){ c( null, true ); }} + permissionHandler: { canPerformAction: function( a, b, c ){ c( null, true ); }}, + uniqueRegistry: { + get: function( name, callback ) { callback( true ); }, + release: function() {} + } }; it( 'creates the record handler', function(){ @@ -61,17 +67,6 @@ describe( 'record handler handles messages', function(){ expect( listeningClient.socket.lastSendMessage ).toBe( msg( 'R|SP|user\/.*|user/C+' ) ); }); - it( 'returns a snapshot of the all records that match the pattern', function(){ - recordHandler.handle( subscribingClient, { - raw: msg( 'R|LSN|user\/*' ), - topic: 'R', - action: 'LSN', - data: [ 'user\/*' ] - }); - - expect( subscribingClient.socket.lastSendMessage ).toBe( msg( 'R|SF|user/*|["user/A","user/B","user/C"]+' )); - }); - it( 'doesn\'t send messages for subsequent subscriptions', function(){ expect( listeningClient.socket.sendMessages.length ).toBe( 4 ); recordHandler.handle( subscribingClient, { diff --git a/test/record/record-message-connectorSpec.js b/test/record/record-message-connectorSpec.js index f2a404e3a..4fc310568 100644 --- a/test/record/record-message-connectorSpec.js +++ b/test/record/record-message-connectorSpec.js @@ -5,17 +5,24 @@ var RecordHandler = require( '../../src/record/record-handler' ), SocketMock = require( '../mocks/socket-mock' ), SocketWrapper = require( '../../src/message/socket-wrapper' ), LoggerMock = require( '../mocks/logger-mock' ), - MessageConnectorMock = require( '../mocks/message-connector-mock.js' ); + MessageConnectorMock = require( '../mocks/message-connector-mock.js' ), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(); describe( 'messages from direct connected clients and messages that come in via message connector co-exist peacefully', function(){ var recordHandler, subscriber = new SocketWrapper( new SocketMock(), {} ), options = { + clusterRegistry: clusterRegistryMock, + serverName: 'a-server-name', cache: new StorageMock(), storage: new StorageMock(), logger: new LoggerMock(), messageConnector: new MessageConnectorMock(), - permissionHandler: { canPerformAction: function( a, b, c ){ c( null, true ); }} + permissionHandler: { canPerformAction: function( a, b, c ){ c( null, true ); }}, + uniqueRegistry: { + get: function( name, callback ) { callback( true ); }, + release: function() {} + } }; it( 'creates the record handler', function(){ @@ -31,7 +38,11 @@ describe( 'messages from direct connected clients and messages that come in via data: [ 'someRecord' ] }); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); + expect( options.messageConnector.lastPublishedMessage ).toEqual( { + topic: 'R_SUB', + action: 'DISTRIBUTED_STATE_ADD', + data: [ 'someRecord', 'a-server-name', -5602883995 ] + } ); expect( subscriber.socket.lastSendMessage ).toBe( msg( 'R|R|someRecord|0|{}+' ) ); }); diff --git a/test/rpc/rpc-handler-remoteSpec.js b/test/rpc/rpc-handler-remoteSpec.js index a213a9155..0e7e2d7ec 100644 --- a/test/rpc/rpc-handler-remoteSpec.js +++ b/test/rpc/rpc-handler-remoteSpec.js @@ -4,9 +4,11 @@ var RpcHandler = require( '../../src/rpc/rpc-handler' ), C = require( '../../src/constants/constants' ), msg = require( '../test-helper/test-helper' ).msg, SocketMock = require( '../mocks/socket-mock' ), - MessageConnectorMock = require( '../mocks/message-connector-mock' ); + MessageConnectorMock = require( '../mocks/message-connector-mock' ), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(); var options = { + clusterRegistry: clusterRegistryMock, messageConnector: new MessageConnectorMock(), logger: { log: jasmine.createSpy( 'log' ) }, serverName: 'thisServer', diff --git a/test/rpc/rpc-handler-reroutingSpec.js b/test/rpc/rpc-handler-reroutingSpec.js index 4e41f7b41..3e7c5a26e 100644 --- a/test/rpc/rpc-handler-reroutingSpec.js +++ b/test/rpc/rpc-handler-reroutingSpec.js @@ -4,9 +4,11 @@ var RpcHandler = require( '../../src/rpc/rpc-handler' ), C = require( '../../src/constants/constants' ), msg = require( '../test-helper/test-helper' ).msg, SocketMock = require( '../mocks/socket-mock' ), - MessageConnectorMock = require( '../mocks/message-connector-mock' ); + MessageConnectorMock = require( '../mocks/message-connector-mock' ), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(); var options = { + clusterRegistry: clusterRegistryMock, messageConnector: new MessageConnectorMock(), logger: { log: jasmine.createSpy( 'log' ) }, serverName: 'thisServer', @@ -102,7 +104,11 @@ describe('rpc handler returns alternative providers for the same rpc', function( action: C.ACTIONS.QUERY, data: [ 'rpcX' ] }); - expect( options.messageConnector.lastPublishedMessage ).toEqual( null ); + expect( options.messageConnector.lastPublishedMessage ).toEqual( { + topic: 'P_SUB', + action: 'DISTRIBUTED_STATE_ADD', + data: [ 'rpcB', 'thisServer', 7013881 ] + } ); }); it( 'receives a provider query for an rpc with providers', function(){ diff --git a/test/rpc/rpc-handlerSpec.js b/test/rpc/rpc-handlerSpec.js index 8f456ad21..859af9840 100644 --- a/test/rpc/rpc-handlerSpec.js +++ b/test/rpc/rpc-handlerSpec.js @@ -5,7 +5,9 @@ var RpcHandler = require( '../../src/rpc/rpc-handler' ), SocketMock = require( '../mocks/socket-mock' ), MessageConnectorMock = require( '../mocks/message-connector-mock' ), LoggerMock = require( '../mocks/logger-mock' ), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(), options = { + clusterRegistry: clusterRegistryMock, messageConnector: new MessageConnectorMock(), logger: new LoggerMock(), serverName: 'thisServer', diff --git a/test/utils/distributed-state-registry-clusterSpec.js b/test/utils/distributed-state-registry-clusterSpec.js deleted file mode 100644 index 207970ad8..000000000 --- a/test/utils/distributed-state-registry-clusterSpec.js +++ /dev/null @@ -1,177 +0,0 @@ -var DistributedStateRegistry = require( '../../src/utils/distributed-state-registry' ); -var LocalMessageConnector = require( '../mocks/local-message-connector' ); - -var createRegistry = function ( serverName, messageConnector ) { - var options = { - serverName: serverName, - stateReconciliationTimeout: 10, - messageConnector: messageConnector - }; - var result = { - addCallback: jasmine.createSpy( 'add' ), - removeCallback: jasmine.createSpy( 'remove' ), - registry: new DistributedStateRegistry( 'TEST_TOPIC', options ) - } - - result.registry.on( 'add', result.addCallback ); - result.registry.on( 'remove', result.removeCallback ); - - return result; -}; - -describe( 'distributed-state-registry adds and removes names in a cluster', function(){ - var messageConnector = new LocalMessageConnector(); - var registryA; - var registryB; - var registryC; - - it( 'creates the registries', function(){ - registryA = createRegistry( 'server-name-a', messageConnector ); - registryB = createRegistry( 'server-name-b', messageConnector ); - registryC = createRegistry( 'server-name-c', messageConnector ); - expect( messageConnector.subscribedTopics.length ).toBe( 3 ); - }); - - it( 'adds an entry to registry a', function(){ - registryA.registry.add( 'test-entry-a' ); - expect( registryA.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); - expect( registryB.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); - expect( registryC.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); - expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a' ]); - expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a' ]); - expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a' ]); - }); - - it( 'adds an entry to registry b', function(){ - registryB.registry.add( 'test-entry-b' ); - expect( registryA.addCallback ).toHaveBeenCalledWith( 'test-entry-b' ); - expect( registryB.addCallback ).toHaveBeenCalledWith( 'test-entry-b' ); - expect( registryC.addCallback ).toHaveBeenCalledWith( 'test-entry-b' ); - expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); - expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); - expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); - }); - - it( 'adds the same entry to registry c', function(){ - registryA.addCallback.calls.reset(); - registryB.addCallback.calls.reset(); - registryC.addCallback.calls.reset(); - registryC.registry.add( 'test-entry-b' ); - expect( registryA.addCallback ).not.toHaveBeenCalled(); - expect( registryB.addCallback ).not.toHaveBeenCalled(); - expect( registryC.addCallback ).not.toHaveBeenCalled(); - expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); - expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); - expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-b' ]); - }); - - it( 'removes a single node entry from registry a', function(){ - registryA.registry.remove( 'test-entry-a' ); - expect( registryA.removeCallback ).toHaveBeenCalledWith( 'test-entry-a' ); - expect( registryB.removeCallback ).toHaveBeenCalledWith( 'test-entry-a' ); - expect( registryC.removeCallback ).toHaveBeenCalledWith( 'test-entry-a' ); - expect( registryA.registry.getAll() ).toEqual([ 'test-entry-b' ]); - expect( registryB.registry.getAll() ).toEqual([ 'test-entry-b' ]); - expect( registryC.registry.getAll() ).toEqual([ 'test-entry-b' ]); - }); - - it( 'removes a multi node entry from registry b', function(){ - registryA.removeCallback.calls.reset(); - registryB.removeCallback.calls.reset(); - registryC.removeCallback.calls.reset(); - registryB.registry.remove( 'test-entry-b' ); - expect( registryA.removeCallback ).not.toHaveBeenCalled(); - expect( registryB.removeCallback ).not.toHaveBeenCalled(); - expect( registryC.removeCallback ).not.toHaveBeenCalled(); - expect( registryA.registry.getAll() ).toEqual([ 'test-entry-b' ]); - expect( registryB.registry.getAll() ).toEqual([ 'test-entry-b' ]); - expect( registryC.registry.getAll() ).toEqual([ 'test-entry-b' ]); - }); - - it( 'removes a multi node entry from registry c', function(){ - registryA.removeCallback.calls.reset(); - registryB.removeCallback.calls.reset(); - registryC.removeCallback.calls.reset(); - registryC.registry.remove( 'test-entry-b' ); - expect( registryA.removeCallback ).toHaveBeenCalledWith( 'test-entry-b' ); - expect( registryB.removeCallback ).toHaveBeenCalledWith( 'test-entry-b' ); - expect( registryC.removeCallback ).toHaveBeenCalledWith( 'test-entry-b' ); - expect( registryA.registry.getAll() ).toEqual([]); - expect( registryB.registry.getAll() ).toEqual([]); - expect( registryC.registry.getAll() ).toEqual([]); - }); -}); - -describe( 'distributed-state-registry reconciles state in a cluster', function(){ - var messageConnector = new LocalMessageConnector(); - var registryA; - var registryB; - var registryC; - - it( 'creates the registries', function(){ - registryA = createRegistry( 'server-name-a', messageConnector ); - registryB = createRegistry( 'server-name-b', messageConnector ); - registryC = createRegistry( 'server-name-c', messageConnector ); - expect( messageConnector.subscribedTopics.length ).toBe( 3 ); - }); - - it( 'adds an entry to registry a', function(){ - registryA.registry.add( 'test-entry-a' ); - expect( registryA.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); - expect( registryB.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); - expect( registryC.addCallback ).toHaveBeenCalledWith( 'test-entry-a' ); - expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a' ]); - expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a' ]); - expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a' ]); - }); - - it( 'adds an entry to registry b, but drops the message resulting in a compromised state', function(){ - registryA.addCallback.calls.reset(); - registryB.addCallback.calls.reset(); - registryC.addCallback.calls.reset(); - messageConnector.dropNextMessage = true; - registryB.registry.add( 'test-entry-f' ); - expect( registryA.addCallback ).not.toHaveBeenCalled(); - expect( registryB.addCallback ).toHaveBeenCalledWith( 'test-entry-f' ); - expect( registryC.addCallback ).not.toHaveBeenCalled(); - expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a' ]); - expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-f' ]); - expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a' ]); - }); - - it( 'adds another entry to registry b, the other registries detect the compromised state', function( done ){ - registryA.addCallback.calls.reset(); - registryB.addCallback.calls.reset(); - registryC.addCallback.calls.reset(); - registryB.registry.add( 'test-entry-g' ); - expect( messageConnector.messages.length ).toBe( 2 ); - expect( registryA.addCallback ).toHaveBeenCalledWith( 'test-entry-g' ); - expect( registryB.addCallback ).toHaveBeenCalledWith( 'test-entry-g' ); - expect( registryC.addCallback ).toHaveBeenCalledWith( 'test-entry-g' ); - expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-g' ]); - expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-f', 'test-entry-g' ]); - expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-g' ]); - setTimeout( done, 50 ); - }); - - it( 'has reconciled the compromised state', function(){ - expect( messageConnector.messages.length ).toBe( 5 ); - - // add 'test-entry-a' - expect( messageConnector.messages[ 0 ].data.action ).toBe( 'DISTRIBUTED_STATE_ADD' ); - // add 'test-entry-g', 'test-entry-f' has been dropped - expect( messageConnector.messages[ 1 ].data.action ).toBe( 'DISTRIBUTED_STATE_ADD' ); - // full state request from either A or C arrives - expect( messageConnector.messages[ 2 ].data.action ).toBe( 'DISTRIBUTED_STATE_REQUEST_FULL_STATE' ); - // B response immediatly with full state - expect( messageConnector.messages[ 3 ].data.action ).toBe( 'DISTRIBUTED_STATE_FULL_STATE' ); - // full state request from the other registry (either A or C) arrives, but is ignored as fulls state has already - // been send within stateReconciliationTimeout - expect( messageConnector.messages[ 4 ].data.action ).toBe( 'DISTRIBUTED_STATE_REQUEST_FULL_STATE' ); - expect( registryA.addCallback ).toHaveBeenCalledWith( 'test-entry-f' ); - expect( registryC.addCallback ).toHaveBeenCalledWith( 'test-entry-f' ); - expect( registryA.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-g', 'test-entry-f' ]); - expect( registryB.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-f', 'test-entry-g' ]); - expect( registryC.registry.getAll() ).toEqual([ 'test-entry-a', 'test-entry-g', 'test-entry-f' ]); - }); -}); \ No newline at end of file diff --git a/test/utils/distributed-state-registrySpec.js b/test/utils/distributed-state-registrySpec.js deleted file mode 100644 index 9125ea539..000000000 --- a/test/utils/distributed-state-registrySpec.js +++ /dev/null @@ -1,269 +0,0 @@ -var DistributedStateRegistry = require( '../../src/utils/distributed-state-registry' ); -var MessageConnectorMock = require( '../mocks/message-connector-mock' ); - -describe( 'distributed-state-registry adds and removes names', function(){ - var registry; - - var options = { - serverName: 'server-name-a', - stateReconciliationTimeout: 10, - messageConnector: new MessageConnectorMock() - }; - - it( 'creates the registry', function(){ - registry = new DistributedStateRegistry( 'TEST_TOPIC', options ); - expect( typeof registry.add ).toBe( 'function' ); - }); - - it( 'adds a new local name', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'add', callback ); - registry.add( 'test-name-a' ); - - expect( options.messageConnector.lastPublishedTopic ).toBe( 'TEST_TOPIC' ); - expect( options.messageConnector.lastPublishedMessage ).toEqual({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_ADD', - data: [ 'test-name-a', 'server-name-a', 2467841850 ] - }); - - expect( callback ).toHaveBeenCalledWith( 'test-name-a' ); - expect( registry.getAll() ).toEqual([ 'test-name-a' ]); - }); - - it( 'adds another new local name', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'add', callback ); - registry.add( 'test-name-b' ); - - expect( options.messageConnector.lastPublishedTopic ).toBe( 'TEST_TOPIC' ); - expect( options.messageConnector.lastPublishedMessage ).toEqual({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_ADD', - data: [ 'test-name-b', 'server-name-a', 4935683701 ] - }); - - expect( callback ).toHaveBeenCalledWith( 'test-name-b' ); - expect( registry.getAll() ).toEqual([ 'test-name-a', 'test-name-b' ]); - }); - - it( 'adds an existing local name', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - - registry.once( 'add', callback ); - registry.add( 'test-name-b' ); - - expect( options.messageConnector.lastPublishedTopic ).toBe( null ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - expect( callback ).not.toHaveBeenCalled(); - expect( registry.getAll() ).toEqual([ 'test-name-a', 'test-name-b' ]); - }); - - it( 'adds a new remote name', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'add', callback ); - - options.messageConnector.simulateIncomingMessage({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_ADD', - data: [ 'test-name-c', 'server-name-b', 2467841852 ] - }); - - expect( options.messageConnector.lastPublishedTopic ).toBe( null ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - expect( callback ).toHaveBeenCalledWith( 'test-name-c' ); - expect( registry.getAll() ).toEqual([ 'test-name-a', 'test-name-b', 'test-name-c' ]); - }); - - it( 'adds another new remote name', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'add', callback ); - - options.messageConnector.simulateIncomingMessage({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_ADD', - data: [ 'test-name-d', 'server-name-b', 4935683705 ] - }); - - expect( options.messageConnector.lastPublishedTopic ).toBe( null ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - expect( callback ).toHaveBeenCalledWith( 'test-name-d' ); - expect( registry.getAll() ).toEqual([ 'test-name-a', 'test-name-b', 'test-name-c', 'test-name-d' ]); - }); - - it( 'adds an existing remote name', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'add', callback ); - - options.messageConnector.simulateIncomingMessage({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_ADD', - data: [ 'test-name-c', 'server-name-c', 2467841852 ] - }); - - expect( options.messageConnector.lastPublishedTopic ).toBe( null ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - expect( callback ).not.toHaveBeenCalled(); - expect( registry.getAll() ).toEqual([ 'test-name-a', 'test-name-b', 'test-name-c', 'test-name-d' ]); - }); - - it( 'removes a name that exists once locally', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'remove', callback ); - - registry.remove( 'test-name-a' ); - - expect( options.messageConnector.lastPublishedTopic ).toBe( 'TEST_TOPIC' ); - expect( options.messageConnector.lastPublishedMessage ).toEqual({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_REMOVE', - data: [ 'test-name-a', 'server-name-a', 2467841851 ] - }); - expect( callback ).toHaveBeenCalledWith( 'test-name-a' ); - expect( registry.getAll() ).toEqual([ 'test-name-b', 'test-name-c', 'test-name-d' ]); - }); - - it( 'removes a remote name that exists once', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'remove', callback ); - - options.messageConnector.simulateIncomingMessage({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_REMOVE', - data: [ 'test-name-d', 'server-name-b', 2467841852 ] - }); - - expect( options.messageConnector.lastPublishedTopic ).toBe( null ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - expect( callback ).toHaveBeenCalledWith( 'test-name-d' ); - expect( registry.getAll() ).toEqual([ 'test-name-b', 'test-name-c' ]); - }); - - it( 'doesnt remove a remote name that exists for another node', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'remove', callback ); - - options.messageConnector.simulateIncomingMessage({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_REMOVE', - data: [ 'test-name-c', 'server-name-b', 0 ] - }); - - expect( options.messageConnector.lastPublishedTopic ).toBe( null ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - expect( callback ).not.toHaveBeenCalled(); - expect( registry.getAll() ).toEqual([ 'test-name-b', 'test-name-c' ]); - }); - - it( 'removes a remote name once the last node is removed', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'remove', callback ); - - options.messageConnector.simulateIncomingMessage({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_REMOVE', - data: [ 'test-name-c', 'server-name-c', 0 ] - }); - - expect( options.messageConnector.lastPublishedTopic ).toBe( null ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - expect( callback ).toHaveBeenCalledWith( 'test-name-c' ); - expect( registry.getAll() ).toEqual([ 'test-name-b' ]); - }); - - it( 'ensures that no reconciliation messages where pending', function( done ){ - options.messageConnector.reset(); - setTimeout(function(){ - expect( options.messageConnector.lastPublishedTopic ).toBe( null ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - done(); - }, 50 ); - }); -}); - -describe( 'distributed-state-registry reconciles states', function(){ - var registry; - - var options = { - serverName: 'server-name-a', - stateReconciliationTimeout: 10, - logger: { log: function(){ console.log( arguments ); }}, - messageConnector: new MessageConnectorMock() - }; - - it( 'creates the registry', function(){ - registry = new DistributedStateRegistry( 'TEST_TOPIC', options ); - expect( typeof registry.add ).toBe( 'function' ); - }); - - it( 'adds a remote name with invalid checksum', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'add', callback ); - - options.messageConnector.simulateIncomingMessage({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_ADD', - data: [ 'test-name-z', 'server-name-b', 666 ] // should be 2467841875 - }); - - expect( options.messageConnector.lastPublishedTopic ).toBe( null ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - expect( callback ).toHaveBeenCalledWith( 'test-name-z' ); - expect( registry.getAll() ).toEqual([ 'test-name-z' ]); - }); - - it( 'adds a remote name with invalid checksum', function( done ){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'add', callback ); - - options.messageConnector.simulateIncomingMessage({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_ADD', - data: [ 'test-name-c', 'server-name-b', 666 ] // should be 1054 - }); - - expect( options.messageConnector.lastPublishedTopic ).toBe( null ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - expect( callback ).toHaveBeenCalledWith( 'test-name-c' ); - - setTimeout(function(){ - expect( options.messageConnector.lastPublishedTopic ).toBe( 'TEST_TOPIC' ); - expect( options.messageConnector.lastPublishedMessage ).toEqual({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_REQUEST_FULL_STATE', - data: [ 'server-name-b' ] - }); - expect( registry.getAll() ).toEqual([ 'test-name-z', 'test-name-c' ]); - done(); - }, 30 ); - }); - - it( 'receives a full state update', function(){ - options.messageConnector.reset(); - var callback = jasmine.createSpy( 'callback' ); - registry.once( 'add', callback ); - - options.messageConnector.simulateIncomingMessage({ - topic: 'TEST_TOPIC', - action: 'DISTRIBUTED_STATE_FULL_STATE', - data: [ 'server-name-b', [ 'test-name-x', 'test-name-c' ] ] - }); - - expect( options.messageConnector.lastPublishedTopic ).toBe( null ); - expect( options.messageConnector.lastPublishedMessage ).toBe( null ); - expect( callback ).toHaveBeenCalledWith( 'test-name-x' ); - expect( registry.getAll() ).toEqual([ 'test-name-c', 'test-name-x' ]); - }); -}); \ No newline at end of file diff --git a/test/utils/subscription-registrySpec.js b/test/utils/subscription-registrySpec.js index 443592ac5..0022a63fb 100644 --- a/test/utils/subscription-registrySpec.js +++ b/test/utils/subscription-registrySpec.js @@ -5,7 +5,15 @@ var SubscriptionRegistry = require( '../../src/utils/subscription-registry' ), lastLogEvent = null, socketWrapperOptions = {logger:{ log: function(){}}}, _msg = require( '../test-helper/test-helper' ).msg, - options = { logger: { log: function( level, event, message ){ lastLogEvent = event; } } }, + LocalMessageConnector = require( '../mocks/local-message-connector' ), + clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(), + options = { + clusterRegistry: clusterRegistryMock, + serverName: 'server-name-a', + stateReconciliationTimeout: 10, + messageConnector: new LocalMessageConnector(), + logger: { log: function( level, event, message ){ lastLogEvent = event; } } + }, subscriptionRegistry = new SubscriptionRegistry( options, 'E' ), subscriptionListenerMock = { onSubscriptionMade: jasmine.createSpy( 'onSubscriptionMade' ), @@ -38,12 +46,12 @@ describe( 'subscription-registry manages subscriptions', function(){ }); it( 'returns the subscribed socket', function(){ - expect( subscriptionRegistry.getSubscribers( 'someName' ) ).toEqual([ socketWrapperA ]); + expect( subscriptionRegistry.getLocalSubscribers( 'someName' ) ).toEqual([ socketWrapperA ]); }); it( 'determines if it has subscriptions', function(){ - expect( subscriptionRegistry.hasSubscribers( 'someName' ) ).toBe( true ); - expect( subscriptionRegistry.hasSubscribers( 'someOtherName' ) ).toBe( false ); + expect( subscriptionRegistry.hasLocalSubscribers( 'someName' ) ).toBe( true ); + expect( subscriptionRegistry.hasLocalSubscribers( 'someOtherName' ) ).toBe( false ); }); it( 'distributes messages to multiple subscribers', function(){ @@ -54,7 +62,7 @@ describe( 'subscription-registry manages subscriptions', function(){ }); it( 'returns a random subscribed socket', function(){ - expect( subscriptionRegistry.getSubscribers( 'someName' ) ).toEqual([ socketWrapperA, socketWrapperB ]); + expect( subscriptionRegistry.getLocalSubscribers( 'someName' ) ).toEqual([ socketWrapperA, socketWrapperB ]); var returnedA = false, returnedB = false, @@ -62,7 +70,7 @@ describe( 'subscription-registry manages subscriptions', function(){ i; for( i = 0; i < 100; i++ ) { - randomSubscriber = subscriptionRegistry.getRandomSubscriber( 'someName' ); + randomSubscriber = subscriptionRegistry.getRandomLocalSubscriber( 'someName' ); if( randomSubscriber === socketWrapperA ) returnedA = true; if( randomSubscriber === socketWrapperB ) returnedB = true; } @@ -114,7 +122,7 @@ describe( 'subscription-registry manages subscriptions', function(){ expect( subscriptionListenerMock.onSubscriptionRemoved ).not.toHaveBeenCalled(); subscriptionRegistry.unsubscribe( 'someName', socketWrapperA ); - expect( subscriptionListenerMock.onSubscriptionRemoved ).toHaveBeenCalledWith( 'someName', socketWrapperA, 0 ); + expect( subscriptionListenerMock.onSubscriptionRemoved ).toHaveBeenCalledWith( 'someName', socketWrapperA, 0, 0 ); expect( socketWrapperA.socket.lastSendMessage ).toBe( _msg( 'E|A|US|someName+' ) ); subscriptionRegistry.sendToSubscribers( 'someName', _msg( 'msg8+' ) ); expect( socketWrapperA.socket.lastSendMessage ).toBe( _msg( 'E|A|US|someName+' ) ); @@ -196,6 +204,6 @@ describe( 'subscription-registry handles empty states', function(){ var subscriptionRegistry = new SubscriptionRegistry( options, 'E' ); it( 'returns null if no subscriber is registered', function(){ - expect( subscriptionRegistry.getRandomSubscriber() ).toBe( null ); + expect( subscriptionRegistry.getRandomLocalSubscriber() ).toBe( null ); }); }); \ No newline at end of file diff --git a/test/webrtc/webrtc-b-to-a-callSpec.js b/test/webrtc/webrtc-b-to-a-callSpec.js index e253433b5..2afce8989 100644 --- a/test/webrtc/webrtc-b-to-a-callSpec.js +++ b/test/webrtc/webrtc-b-to-a-callSpec.js @@ -2,7 +2,9 @@ var WebRtcHandler = require( '../../src/webrtc/webrtc-handler' ); var SocketMock = require( '../mocks/socket-mock' ); var SocketWrapper = require( '../../src/message/socket-wrapper' ); var logger = { log: jasmine.createSpy( 'log' ) }; +var noopMessageConnector = require('../../src/default-plugins/noop-message-connector'); var msg = require( '../test-helper/test-helper' ).msg; +var clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(); describe( 'webrtc handler', function(){ @@ -12,7 +14,11 @@ describe( 'webrtc handler', function(){ it( 'initializes the WebRtcHandler', function(){ - webrtcHandler = new WebRtcHandler({ logger: logger }); + webrtcHandler = new WebRtcHandler({ + clusterRegistry: clusterRegistryMock, + messageConnector: noopMessageConnector, + logger: logger + }); expect( typeof webrtcHandler.handle ).toBe( 'function' ); }); diff --git a/test/webrtc/webrtc-callSpec.js b/test/webrtc/webrtc-callSpec.js index 2f695c999..be5e47191 100644 --- a/test/webrtc/webrtc-callSpec.js +++ b/test/webrtc/webrtc-callSpec.js @@ -3,6 +3,8 @@ var SocketMock = require( '../mocks/socket-mock' ); var SocketWrapper = require( '../../src/message/socket-wrapper' ); var logger = { log: jasmine.createSpy( 'log' ) }; var msg = require( '../test-helper/test-helper' ).msg; +var noopMessageConnector = require('../../src/default-plugins/noop-message-connector'); +var clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(); describe( 'webrtc handler', function(){ @@ -12,7 +14,11 @@ describe( 'webrtc handler', function(){ it( 'initializes the WebRtcHandler', function(){ - webrtcHandler = new WebRtcHandler({ logger: logger }); + webrtcHandler = new WebRtcHandler({ + clusterRegistry: clusterRegistryMock, + messageConnector: noopMessageConnector, + logger: logger + }); expect( typeof webrtcHandler.handle ).toBe( 'function' ); }); diff --git a/test/webrtc/webrtc-calleeSpec.js b/test/webrtc/webrtc-calleeSpec.js index aae2a7496..9b2f55551 100644 --- a/test/webrtc/webrtc-calleeSpec.js +++ b/test/webrtc/webrtc-calleeSpec.js @@ -3,6 +3,8 @@ var SocketMock = require( '../mocks/socket-mock' ); var SocketWrapper = require( '../../src/message/socket-wrapper' ); var logger = { log: jasmine.createSpy( 'log' ) }; var msg = require( '../test-helper/test-helper' ).msg; +var noopMessageConnector = require('../../src/default-plugins/noop-message-connector'); +var clusterRegistryMock = new (require( '../mocks/cluster-registry-mock' ))(); describe( 'webrtc handler', function(){ @@ -15,7 +17,11 @@ describe( 'webrtc handler', function(){ it( 'initializes the WebRtcHandler', function(){ - webrtcHandler = new WebRtcHandler({ logger: logger }); + webrtcHandler = new WebRtcHandler({ + clusterRegistry: clusterRegistryMock, + messageConnector: noopMessageConnector, + logger: logger + }); expect( typeof webrtcHandler.handle ).toBe( 'function' ); });