@jayesol/jayeson.lib.delivery 2.0.7-beta.0 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/Subscriber.js CHANGED
@@ -1,1083 +1,1083 @@
1
- var coreMsges = require("./CoreMessages")
2
- var deliveryStack = require("./DeliveryStack")
3
- var _ = require("underscore")
4
- var EventEmitter = require("eventemitter3");
5
-
6
- var streamFinder = require("@jayesol/jayeson.lib.streamfinder");
7
- var Discoverer = streamFinder.Discoverer;
8
-
9
- var util = require("./util")
10
- var _c = util._c
11
-
12
- var DefaultClient = deliveryStack.DefaultClient
13
- var EPEvent = deliveryStack.EPEvent
14
- var EPConnectedEvent = deliveryStack.EPConnectedEvent
15
- var EPConnectionError = deliveryStack.EPConnectionError
16
- var EPDisconnectedEvent = deliveryStack.EPDisconnectedEvent
17
- var IEndPointEventSource = deliveryStack.IEndPointEventSource
18
- var generateUUID = deliveryStack.generateUUID;
19
- var Util = deliveryStack.Util;
20
- var MessageWrapper = deliveryStack.MessageWrapper;
21
-
22
- var StreamRegistryMessageGroup = coreMsges.StreamRegistryMessageGroup;
23
- var EmptyMessageClass = coreMsges.EmptyMessageClass;
24
- var StringMessageClass = coreMsges.StringMessageClass;
25
- var JSonMessageClass = coreMsges.JSonMessageClass;
26
- var StreamRegistryRequest = coreMsges.StreamRegistryRequest;
27
- var StreamRegistryResponse = coreMsges.StreamRegistryResponse;
28
- var AuthGroup = coreMsges.AuthGroup;
29
- var GenericMessageGroup = coreMsges.GenericMessageGroup;
30
- var KeepAliveMessageGroup = coreMsges.KeepAliveMessageGroup;
31
- var TicketRenew = coreMsges.TicketRenew;
32
-
33
- /**
34
- * @author Ryan
35
- *
36
- * This depends on external libraries: underscorejs, eventemitter, promisejs
37
- *
38
- * This depends on internal libraries: coremessage, deliverystack, jsdiscoverer
39
- *
40
- * The subscriber has the following features:
41
- * TODO: FILL IN THE FEATURES
42
- *
43
- */
44
-
45
- /**
46
- * Represents a configuration being passed
47
- * into the subscriber. In the javascript context,
48
- *
49
- * The discovery pattern has the following format
50
- * {
51
- * id1 : [pattern 1, pattern 2 .... ],
52
- * id2 : [pattern A, pattern B .... ]
53
- * }
54
- *
55
- * example
56
- *
57
- * {
58
- * 8 : ["*_LIVE", "SBO_*"], // price feed
59
- * 7 : ["order_ryan"] // order data feed for user ryan
60
- * }
61
- *
62
- * The processor setup is as following:
63
- * {
64
- * id1 : processor 1 of type IMessageGroupProcessor,
65
- * id2 : processor 2 of type IMessageGroupProcessor
66
- *
67
- * }
68
- *
69
- */
70
- function SubscriberConfig() {
71
- this.autoDiscover = true; // by default, enable auto discovery
72
- this.streamFinder = ""; // the URL to connect to the stream finder
73
- this.username = ""; // if we want to login directly to the stream finder
74
- this.password = "";
75
- this.discoveryPattern = {};
76
- this.processors = {};
77
- this.schemes = ["ws"]; // by default we consume from ws
78
- this.ticketRenewalInterval = 14*60*1000; // default to 14 minutes
79
- this.reconnectionInterval = 3000; // reconnection to a relay node
80
- this.exclusionThreshold = 3; // the number of reconnection failed before the relay node is added to the excluded list
81
- this.feedScope = "";
82
- this.discoverByScope = true; //new version will discover by scope
83
- }
84
-
85
- /**
86
- * Represents a data node. This encapsulate the following information
87
- * <li>The client used to connect to the node</li>
88
- * <li>The stream being consumed from the node</li>
89
- *
90
- * It also provides functionalities such as reconnection. It listen to ticket being renewed
91
- * by the subscriber and send the ticket to the server accordingly.
92
- *
93
- * @params
94
- * <li>id - id of the data node</li>
95
- * <li>streamInfo - the first stream information used for connection<br/>
96
- * <pre>
97
- * {
98
- * "group": "<groupId>",
99
- * "stream": "<streamName>",
100
- * "level": <level>,
101
- * "serviceId" : <serviceId>,
102
- * "connection": {
103
- * "protocol": "<protocol>",
104
- * "hostname": "<hostname>",
105
- * "port": <port>
106
- * }
107
- * }
108
- * </pre>
109
- * </li>
110
- * <li>clientId - id of the client</li>
111
- * <li>ticket - ticket of the subscriber</li>
112
- * <li>eventBus - an Event Emitter which is responsible to propagate events from DataNode</li>
113
- * <li>subscriberConfig - configuration of the subscriber that created this data node</li>
114
- */
115
- var DATA_NODE_COUNTER = 0;
116
-
117
- function DataNode(id, clientId, ticket, streamInfo, eventBus, subscriberConfig) {
118
- this.client = null;
119
- /**
120
- * This data structure has the following form:
121
- * { group -> [stream information] }
122
- */
123
- this.streams = {};
124
- this.id = id; // id of the data node
125
-
126
- this.counter = DATA_NODE_COUNTER++;
127
- console.log("[DataNode] Node ", id, " counter ", this.counter, " is created!");
128
-
129
- /**
130
- * Current state of the data node
131
- */
132
- this.state = DataNode.State.CREATED;
133
- this.connection = streamInfo.connection;
134
-
135
- this.toBeConsumed = [];
136
-
137
- this.consumeStream(streamInfo);
138
-
139
- this.clientId = clientId;
140
- this.ticket = ticket;
141
-
142
- this.consumeTaskTimeoutID = 0;
143
-
144
- this.eventBus = eventBus;
145
-
146
- this.sConf = subscriberConfig;
147
-
148
- // the list of promises which will be resolved when stop consumption messages
149
- // for a stream is handled and replied by the server. The promise
150
- // shall be successful if there is disconnection while waiting for the reply
151
- // Format: streamId -> promiseObject e.g. "8_CROWN"
152
- // the promise value is true / false
153
- this.removalPromises = {};
154
- // the list of promises which will be resolved when consumption messages
155
- // for a stream is handled and replied by the server. The promise shall
156
- // resolve to failed if there is disconnection waiting for the reply. The promise
157
- // is created when the registration (not consumption) for the stream starts.
158
- // The promise value is true / false.
159
- this.consumptionPromises = {};
160
-
161
- };
162
- DataNode.State = {
163
- CREATED : 1,
164
- CONNECTING : 2,
165
- CONNECTED : 3,
166
- RECONNECTING : 4,
167
- DISCONNECTED : 5,
168
- DESTROYED : 6
169
- };
170
- DataNode.Event = {
171
- STATE : "DN_STATE",
172
- RECONNECTION_ERROR : "DN_RE_ERROR",
173
- CONSUMPTION_ERROR : "DN_CONSUMPTION_ERROR",
174
- CONSUMPTION_START : "DN_CONSUMPTION_START",
175
- CONSUMPTION_STOP : "DN_CONSUMPTION_STOP",
176
- };
177
- _c(DataNode, {
178
-
179
- /**
180
- * This method return a PROMISE of the removal of the stream from the
181
- * from consumption. This has to be asynchronous because involving sending
182
- * deregistration message to the stream registry of the consuming node. We however, only
183
- * send unregister when the node is in CONNECTED STATE and the person invoke this method
184
- * specify he want to do so (sendDeregistration = true)
185
- *
186
- * There are two cases that a stream can be removed from a data node:
187
- * <li>The node that consuming the stream has an error and needs reconnection</li>
188
- * <li>A better node can be found for the stream according to the stream selection strategy</li>
189
- *
190
- * @return a promise which is resolved when the stream is completely removed from consumption.
191
- */
192
- removeStream : function(group, stream, sendDeregistration) {
193
-
194
- var id = Util._streamIdFromNames(group, stream);
195
-
196
- var deregistrationAction = _.bind(function(registrationResult) {
197
- // the unregistration promise. This is done when the
198
- // the stream is unregister from this data node by sending message to
199
- // the data source
200
- var promise;
201
-
202
- // the action to be done, remove the stream from the array
203
- var unregisterPostAction = _.bind(function(result) {
204
- if (result && this.streams[group]) {
205
- var index;
206
-
207
- for (index = 0; index < this.streams[group].length; index++)
208
- if (stream == this.streams[group][index].stream) break;
209
-
210
- if (index < this.streams[group].length) this.streams[group].splice(index, 1);
211
-
212
- if (this.streams[group].length == 0) delete this.streams[group];
213
-
214
- // if there is no more stream this node is consuming, just destroy itself
215
- if (_.isEmpty(this.streams)) this.destroy();
216
- }
217
-
218
- return result;
219
- }, this);
220
-
221
- if (this.state == DataNode.State.CONNECTED && sendDeregistration) {
222
-
223
-
224
- promise = Util._newPromise();
225
-
226
-
227
- this.removalPromises[id] = promise;
228
-
229
- var req = new StreamRegistryRequest(group, [stream], StreamRegistryRequest.DEREGISTRATION);
230
- this.client.send(new MessageWrapper(req, StreamRegistryMessageGroup.REQUEST));
231
-
232
- var self = this;
233
- window.setTimeout(function() {
234
- //proceed with deregistration if no deregistration responses are received.
235
- if (self.removalPromises[id] !== undefined) {
236
- console.warn("Timeout waiting for deregistration response, proceeding to treat stream " + group + ":" + stream + " as deregistered for " + self.connection.hostname + ":" + self.connection.port);
237
- var streamResponse = {};
238
- streamResponse[stream] = StreamRegistryResponse.DEREGISTRATION_TIMEOUT;
239
- var response = new StreamRegistryResponse(group, streamResponse, StreamRegistryResponse.DEREGISTRATION);
240
-
241
- self._processDeregistrationResult(response);
242
- }
243
- }, 7000);
244
-
245
- // chain the promise
246
- return promise.then(unregisterPostAction);
247
-
248
- } else {
249
- // if the DataNode is not in connected state, no need to send anything!
250
- promise = Promise.resolve(unregisterPostAction(true));
251
- return promise;
252
- }
253
- }, this);
254
-
255
- // if there is a consumption in progress, wait for it! We will de-register the stream after the it finish consumption
256
- // de-registration only happens in failing over or switching to better stream, so it's okay to de-register
257
- // even when the pending consumption is successful. We switch anyway.
258
- if (this.consumptionPromises && this.consumptionPromises[id] !== undefined) {
259
- return this.consumptionPromises[id].then(deregistrationAction);
260
- } else {
261
- return deregistrationAction(true);
262
- }
263
-
264
- },
265
-
266
- hasStream : function(group, stream) {
267
- if (!this.streams[group]) {
268
- return false;
269
- }
270
- var stream = _.each(this.streams[group], function(node) { return node.stream == stream; } );
271
- return stream !== undefined;
272
- },
273
-
274
- /**
275
- * Start consumption of a particular stream by putting it into tobeConsumed queue.
276
- * When a stream is consumed successfully, DataNode will emit an event
277
- * so that other parties know about it.
278
- */
279
- consumeStream : function(streamInfo) {
280
-
281
- console.log("Node "+this.id+" is going to consume "+streamInfo.group+"_"+streamInfo.stream);
282
-
283
- this.toBeConsumed.push(streamInfo);
284
-
285
- if (!this.streams[streamInfo.group]) this.streams[streamInfo.group] = [];
286
-
287
- this.streams[streamInfo.group].push(streamInfo); // put the stream into the list of streams that this DataNode handling
288
-
289
- if (this.consumeTaskTimeoutID == 0 && this.state == DataNode.State.CONNECTED)
290
- this.consumeTaskTimeoutID = window.setTimeout(_.bind(this.consumeTask, this), 100);
291
- },
292
-
293
- _consumeStreamsOnReconnected : function(){
294
-
295
- if (_.isEmpty(this.streams)) this.destroy();
296
-
297
- for(var group in this.streams){
298
- if(this.streams.hasOwnProperty(group)){
299
- for(var i=0 ; i<this.streams[group].length; i++){
300
- this.toBeConsumed.push(this.streams[group][i]);
301
- }
302
- }
303
- }
304
- },
305
-
306
- connect : function() {
307
-
308
- console.log("[DataNode] Id ", this.id, " counter ", this.counter, " is going to connect. Current state ", this.state);
309
-
310
- //temp code
311
- //this.connection.hostname = "local.vodds.jayeson.com.sg";
312
- //this.connection.port = 5000;
313
-
314
- this.client = new DefaultClient({
315
- url : this._getUrl(this.connection),
316
- msgClass : AuthGroup.classById(1),
317
- params : {
318
- clientId : this.clientId,
319
- ticket : this.ticket,
320
- feedScope : this.sConf.feedScope
321
- }
322
- }, KeepAliveMessageGroup);
323
- this.client.attachListener(this);
324
- this.client.enableKeepAlive();
325
- this.client.registerGroup(AuthGroup, this);
326
- this.client.registerGroup(StreamRegistryMessageGroup, this);
327
-
328
- this.client.connect();
329
- },
330
-
331
- reconnect : function() {
332
-
333
- console.log("[DataNode] Id ", this.id, " counter ", this.counter, " is going to re-connect. Current state ", this.state);
334
-
335
- // update the parameters to be updated when reconnect
336
- // the ticket might have changed!!
337
- this.client.config.params = {
338
- clientId : this.clientId,
339
- ticket : this.ticket,
340
- feedScope : this.sConf.feedScope
341
- }
342
- this.client.connect();
343
- },
344
-
345
- _getUrl : function (connection) {
346
- return connection.protocol+"://"+connection.hostname+((connection.port !== undefined)?(":"+connection.port):"")+((connection.path !== undefined)?connection.path:"");
347
- },
348
-
349
- /**
350
- * check the consume queue and do registration one by one
351
- */
352
- consumeTask : function () {
353
-
354
- var self = this;
355
-
356
- if (this.state == DataNode.State.CONNECTED) {
357
- var streamByGroup = _.groupBy(this.toBeConsumed, function(streamInfo) { return streamInfo.group; } );
358
-
359
- // javascript is single threaded so no need to worry about synchronization here!!
360
- this.toBeConsumed = []; // clear the toBeConsumed list.
361
- this.consumeTaskTimeoutID = 0; // reset the task handler
362
-
363
- // for each group to be consumed, create a promise, send a message
364
- // the promise will be resolved by the onMessage method, or when disconnection
365
- _.each(streamByGroup, function(streams, group) {
366
-
367
- // if no one is removing the stream, then we can consume it
368
- // this is to avoid double consumption from two different node
369
- // because if removal can only be the result of failing over
370
- // or switching to a better source
371
- // if the stream is being in the process of consuming, we don't
372
- // resend registration as well
373
- var canConsume = _.filter(streams, function(streamInfo) {
374
- var sid = Util._streamId(streamInfo);
375
-
376
- return self.removalPromises[sid] === undefined &&
377
- self.consumptionPromises[sid] === undefined;
378
- });
379
-
380
- // we have to create one promise for each stream whose registration
381
- // is going to be sent, because we handle at stream level
382
- // store those promises to the consuming list
383
- var promises = _.reduce(canConsume, function(memo, streamInfo) {
384
-
385
- var sid = Util._streamId(streamInfo);
386
- memo[sid] = Util._newPromise();
387
- memo[sid].then(function(result) {
388
- console.log("Consumption result of ", sid, " : successful = ",result);
389
- });
390
- return memo;
391
-
392
- }, self.consumptionPromises);
393
-
394
- // send the registration messages
395
- var streamNames = _.map(canConsume, function(streamInfo) {
396
- return streamInfo.stream;
397
- });
398
-
399
- var req = new StreamRegistryRequest(group, streamNames, StreamRegistryRequest.REGISTRATION);
400
- // start consumption by first registration message
401
- self.client.send(new MessageWrapper(req, StreamRegistryMessageGroup.REQUEST));
402
-
403
- });
404
- }
405
-
406
- },
407
-
408
- onEvent : function(event) {
409
-
410
- console.log("[DataNode] Event from the stack client", event);
411
-
412
- if (event instanceof EPConnectedEvent) {
413
- if(this.state === DataNode.State.DISCONNECTED){
414
- this._consumeStreamsOnReconnected();
415
- }
416
- this.setState(DataNode.State.CONNECTED);
417
- console.log("[DataNode] Connect / Re-connect to ", this.connection.hostname,":", this.connection.port, "successfully. Consumption will be started!");
418
-
419
- this.consumeTaskTimeoutID = window.setTimeout(_.bind(this.consumeTask, this), 100);
420
-
421
- } else if (event instanceof EPDisconnectedEvent || event instanceof EPConnectionError) {
422
- if (this.state == DataNode.State.DESTROYED) return; // do nothing if the data node is destroyed.
423
-
424
- this.setState(DataNode.State.DISCONNECTED);
425
- console.log("Connect / Re-connect to ",this.connection.hostname,":", this.connection.port, " with error. Retry again later.");
426
-
427
- // disconnection will result in all consumptionPromises resolve to false, and all
428
- // de-registration promises resolve to true
429
- _.each(this.consumptionPromises, function(p, key) { p.resolve(false); } );
430
- _.each(this.removalPromises, function(p, key) { p.resolve(true); } );
431
-
432
- delete this.consumptionPromises;
433
- delete this.removalPromises;
434
-
435
- this.consumptionPromises = {};
436
- this.removalPromises = {};
437
-
438
- if (this.state != DataNode.State.DESTROYED) window.setTimeout(_.bind(this.reconnect, this), this.sConf.reconnectionInterval);
439
- else console.log("Data node ", this.id, " has been destroyed. Stop reconnecting.");
440
-
441
- if (this.consumeTaskTimeoutID !== 0) window.clearTimeout(this.consumeTaskTimeoutID);
442
-
443
- if (event instanceof EPConnectionError) this.eventBus.emit(DataNode.Event.RECONNECTION_ERROR, this, ++this.numFailedReconnection);
444
- else this.numFailedReconnection = 0;
445
-
446
- }
447
-
448
- },
449
-
450
- process : function(messageWrapper) {
451
-
452
- if (messageWrapper.message instanceof StreamRegistryResponse) {
453
- var rep = messageWrapper.message;
454
-
455
- if (rep.responseType == StreamRegistryResponse.REGISTRATION) { // registration response
456
-
457
- this._processRegistrationResult(rep);
458
-
459
- } else if (rep.responseType == StreamRegistryResponse.CONSUMPTION) {
460
-
461
- this._processConsumptionResult(rep);
462
-
463
- } else if (rep.responseType == StreamRegistryResponse.DEREGISTRATION) {
464
-
465
- this._processDeregistrationResult(rep);
466
-
467
- } else if (rep.responseType == StreamRegistryResponse.STOP_CONSUMPTION) {
468
- this._processStopConsumptionResult(rep);
469
- }
470
- }
471
-
472
- },
473
-
474
- _resolveConsumptionPromise : function(group, stream, value) {
475
- var id = Util._streamIdFromNames(group, stream);
476
- if (this.consumptionPromises[id] !== undefined)
477
- this.consumptionPromises[id].resolve(value);
478
-
479
- delete this.consumptionPromises[id];
480
- },
481
-
482
- _resolveRemovalPromise : function(group, stream, value) {
483
- var id = Util._streamIdFromNames(group, stream);
484
- if (this.removalPromises[id] !== undefined) {
485
- this.removalPromises[id].resolve(value);
486
- delete this.removalPromises[id];
487
- }
488
-
489
- delete this.consumptionPromises[id];
490
- },
491
-
492
- _processDeregistrationResult : function (rep) {
493
- var self = this;
494
-
495
- var group = rep.messageGroup;
496
- var errorConsumedStreamByGroup = {}; // contain all unsuccessfully consumed streams
497
- _.each(rep.streams, function(result, stream) {
498
- if (result == StreamRegistryResponse.FAILED) {
499
- self._resolveRemovalPromise(group, stream, false);
500
- } else {
501
- self._resolveRemovalPromise(group, stream, true);
502
- if(errorConsumedStreamByGroup[group] === undefined){
503
- errorConsumedStreamByGroup[group] = [];
504
- }
505
- errorConsumedStreamByGroup[group].push(stream);
506
- }
507
- });
508
- if(_.isEmpty(errorConsumedStreamByGroup) == false){
509
- console.log("[DataNode] Emitting consumption error after Deregistration")
510
- this.eventBus.emit(DataNode.Event.CONSUMPTION_ERROR, this, errorConsumedStreamByGroup);
511
- }
512
- },
513
-
514
- _processStopConsumptionResult : function(rep) {
515
- var self = this;
516
- var group = rep.messageGroup;
517
- var stoppedStreamsByGroup = {};
518
- _.each(rep.streams, function(result, stream) {
519
- console.log("Stopping stream " + stream);
520
- self.removeStream(group, stream, false);
521
- if(stoppedStreamsByGroup[group] === undefined){
522
- stoppedStreamsByGroup[group] = [];
523
- }
524
- stoppedStreamsByGroup[group].push(stream);
525
- });
526
- if (!_.isEmpty(stoppedStreamsByGroup)) {
527
- this.eventBus.emit(DataNode.Event.CONSUMPTION_STOP, this, stoppedStreamsByGroup);
528
- }
529
- },
530
-
531
- _processConsumptionResult : function (rep) {
532
- var self = this;
533
-
534
- var group = rep.messageGroup;
535
- var consumedStreamByGroup = {}; // contain all successfully consumed streams
536
- var errorConsumedStreamByGroup = {}; // contain all unsuccessfully consumed streams
537
-
538
- _.each(rep.streams, function(result, stream) {
539
- if (result == StreamRegistryResponse.FAILED) {
540
- self._resolveConsumptionPromise(group, stream, false);
541
- self.removeStream(group, stream, true);
542
-
543
- if(errorConsumedStreamByGroup[group] === undefined){
544
- errorConsumedStreamByGroup[group] = [];
545
- }
546
-
547
- errorConsumedStreamByGroup[group].push(stream);
548
- } else {
549
- self._resolveConsumptionPromise(group, stream, true);
550
- if(consumedStreamByGroup[group] === undefined){
551
- consumedStreamByGroup[group] = [];
552
- }
553
- consumedStreamByGroup[group].push(stream);
554
- }
555
- });
556
-
557
- if(_.isEmpty(consumedStreamByGroup) == false){
558
- this.eventBus.emit(DataNode.Event.CONSUMPTION_START, this, consumedStreamByGroup);
559
- }
560
- if(_.isEmpty(errorConsumedStreamByGroup) == false){
561
- this.eventBus.emit(DataNode.Event.CONSUMPTION_ERROR, this, errorConsumedStreamByGroup);
562
- }
563
-
564
- },
565
-
566
- _processRegistrationResult : function(rep) {
567
-
568
- var self = this;
569
- var group = rep.messageGroup;
570
-
571
- // complete and remove any failed promise
572
- var cannotConsume = _.pairs(rep.streams)
573
- .filter(function(p) { return p[1] == StreamRegistryResponse.FAILED; })
574
- .map(_.first);
575
-
576
- // remove these streams from the node
577
- cannotConsume.forEach(function(streamName) {
578
- self._resolveConsumptionPromise(group, streamName, false);
579
- self.removeStream(group, streamName, false); // the registration is failed, so no need to send deregistration
580
- });
581
-
582
- var canConsume = _.pairs(rep.streams)
583
- .filter(function(p) { return p[1] == StreamRegistryResponse.SUCCESS; })
584
- .map(_.first) // get the stream name
585
- .filter(function(streamName) {
586
- if (self.sConf.processors[group] !== undefined) { // can consume because processor for the group is defined
587
- return true;
588
- } else { // cannot consume because processor is undefined
589
- self._resolveConsumptionPromise(group, streamName, false);
590
- return false;
591
- }
592
- });
593
-
594
- // send consumption message, at this point, the server can be sure that the processor is ready to handle the stream
595
- if (!_.isEmpty(canConsume)) {
596
-
597
- var processor = self.sConf.processors[group];
598
- self.client.registerGroup(processor.group(), processor); // attach processor for all the consumable group, whose processor is defined.
599
-
600
- var req = new StreamRegistryRequest(group, canConsume, StreamRegistryRequest.CONSUMPTION);
601
- self.client.send(new MessageWrapper(req, StreamRegistryMessageGroup.REQUEST));
602
- }
603
-
604
- if (!_.isEmpty(cannotConsume)) {
605
- var eventData = {};
606
- eventData[group] = cannotConsume;
607
- console.log("Emitting consumption error due to registration result failed.", eventData);
608
-
609
- this.eventBus.emit(DataNode.Event.CONSUMPTION_ERROR, this, eventData);
610
- }
611
- },
612
-
613
- setState : function(state) {
614
-
615
- if (state != this.state) {
616
- var oldState = this.state;
617
- this.state = state;
618
- this.eventBus.emit(DataNode.Event.STATE, this, oldState);
619
- }
620
-
621
- },
622
-
623
- renewTicket : function(ticket) {
624
- this.ticket = ticket;
625
- var wrapper = new MessageWrapper(new TicketRenew(this.clientId, this.ticket), AuthGroup.TICKET_RENEW);
626
- if (this.state == DataNode.State.CONNECTED) this.client.send(wrapper);
627
- },
628
-
629
- destroy : function() {
630
- this.setState(DataNode.State.DESTROYED);
631
- console.log("[DataNode] Disconnect the destroyed data node.");
632
- this.client.disconnect();
633
- }
634
-
635
- });
636
-
637
- function Subscriber(configuration) {
638
- IEndPointEventSource.call(this); // initial IEndPointEventSource
639
- this.conf = new SubscriberConfig();
640
-
641
- _.extend(this.conf, configuration); // copy the configuration over
642
-
643
- this.consuming = {}; // a map from group / stream to the wrapper
644
- this.nodes = {}; // a map from source id to the information related to the source, e.g. state whether connecting, reconnecting, connected or the client used to connect to the node
645
-
646
- // store the currently available sources retrieved from the discoverer
647
- // this data will be used for fast failover whenever there is some
648
- // connection error in a DataNode.
649
- this.currentAvailableSources = null;
650
- // Check currentAvailableSources has used.
651
- this.hasUsedCurrentSources = false;
652
-
653
- this.clientId = this.uuid();
654
- this.ticket = null;
655
-
656
- this.eventBus = new EventEmitter();
657
-
658
- this.eventBus.on(DataNode.Event.RECONNECTION_ERROR, _.bind(this.onNodeReconnectionError, this));
659
- this.eventBus.on(DataNode.Event.STATE, _.bind(this.onNodeStateChanged, this));
660
- this.eventBus.on(DataNode.Event.CONSUMPTION_START, _.bind(this.onStreamConsumption,this));
661
- this.eventBus.on(DataNode.Event.CONSUMPTION_ERROR, _.bind(this.onStreamError, this));
662
- this.eventBus.on(DataNode.Event.CONSUMPTION_STOP, _.bind(this.onStreamStop, this));
663
- }
664
- function StreamConsumptionWrapper(state, node) {
665
- this.state = state;
666
- this.node = node;
667
- }
668
-
669
- /**
670
- * Define Subscriber event and will be fired everytime a client is connected or disconnected.
671
- */
672
- function ConsumptionStart(client, streams) {
673
- EPEvent.call(this, client._ep);
674
- this.client = client;
675
- this.streams = streams;
676
- }
677
- function ConsumptionStop(client, streams) {
678
- EPEvent.call(this, client._ep);
679
- this.client = client;
680
- this.streams = streams;
681
- }
682
- function ConsumptionError(client, streams) {
683
- EPEvent.call(this, client._ep);
684
- this.client = client;
685
- this.streams = streams;
686
- }
687
- _c(ConsumptionStart, {}, EPEvent);
688
- _c(ConsumptionStop, {}, EPEvent);
689
- _c(ConsumptionError, {}, EPEvent);
690
-
691
- Subscriber.Event = {
692
- ConsumptionStart : ConsumptionStart,
693
- ConsumptionStop : ConsumptionStop,
694
- ConsumptionError : ConsumptionError
695
- };
696
-
697
- _c(Subscriber, {
698
-
699
- consumptionState : {
700
- PENDING : 1, // a stream is in this state when it is waiting for a data node to connect and then perform registration
701
- ERROR : 3 // a stream is in this state when its data node is having connection problem
702
- },
703
-
704
- uuid : function (){
705
-
706
- return generateUUID();
707
-
708
- },
709
-
710
- config : function() { return this.conf; },
711
- /**
712
- * This method starts the discovery process
713
- */
714
- start : function() {
715
- var discoverer = new Discoverer(this.config().streamFinder); // create a discoverer to connect to stream finder
716
- if (this.conf.discoverByScope) {
717
- discoverer = new Discoverer(this.config().streamFinder, undefined, this.conf.feedScope); // discover by streamfinder scope
718
- }
719
-
720
- // configure discovery
721
- _.each(this.config().discoveryPattern, function(patternList, group) {
722
-
723
- _.each(patternList, function(pattern) {
724
- discoverer.discover(group, pattern);
725
- });
726
-
727
- });
728
-
729
- _.each(this.config().schemes, function(scheme) {
730
- discoverer.using(scheme);
731
- });
732
-
733
- discoverer.on("update", _.bind(this._updateSources, this));
734
- discoverer.on("error", _.bind(this._discovererError, this));
735
- setInterval(_.bind(this._reconnectStreams, this), 5000);
736
- this._makeTicket(true);
737
-
738
- this.discoverer = discoverer;
739
- },
740
-
741
- _makeTicket : function(isStarting) {
742
- SessionFactory.makeTicket(this.config().streamFinder, this.clientId, _.bind(function(ticket, error) {
743
- if (ticket.length != 0) {
744
-
745
- this.ticket = ticket;
746
- console.log("New ticket obtained "+this.ticket);
747
-
748
- if (isStarting) { // if this is not renewal, we should start the discovery process
749
- this.discoverer.start();
750
- } else {
751
- _.each(this.nodes, _.bind(function(node, id) {
752
- window.setTimeout(_.bind(node.renewTicket, node, this.ticket), 0); // asynchronously
753
- }, this));
754
- }
755
-
756
- window.setTimeout(_.bind(this._makeTicket, this), this.config().ticketRenewalInterval);
757
-
758
- } else if (error.length != 0) {
759
- window.setTimeout(_.bind(this._makeTicket, this), 1000);
760
- }
761
- }, this));
762
- },
763
-
764
- _updateSources : function(sources) {
765
-
766
- console.log("Updating Sources!");
767
- console.log(sources);
768
-
769
- this.oldSources = this.currentAvailableSources;
770
- this.currentAvailableSources = sources.streamConnections;
771
- this.hasUsedCurrentSources = false;
772
-
773
- var toBeConnected = {}; // a map of new nodes to be connected
774
- var toBeUpdated = {}; // a map of existing node to be updated with new group, streams
775
-
776
- if (this.oldSources && this.oldSources.length > 0) {
777
- var streamIds = [];
778
- var nodeIds = [];
779
-
780
- _.each(sources.streamConnections, function(source) {
781
- streamIds.push(Util._streamId(source));
782
- nodeIds.push(Util._nodeId(source));
783
- });
784
-
785
- // remove from corresponding old sources not exist in new sources from data nodes
786
- var stopStreamsByGroup = {};
787
- _.each(this.oldSources, _.bind(function(oldSource) {
788
-
789
- if (!_.contains(streamIds, Util._streamId(oldSource)) || !_.contains(nodeIds, Util._nodeId(oldSource))) {
790
-
791
- console.log("[Subscriber] ", oldSource, " is no longer in the discovery results. Going to be removed.")
792
-
793
- _.each(this.nodes, _.bind(function(dataNode) {
794
-
795
- if (dataNode.hasStream(oldSource.group, oldSource.stream)) {
796
- console.log("[Subscriber] Remove old stream ", oldSource, " from Data Node ", dataNode.id, " dataNode counter ", dataNode.counter);
797
- dataNode.removeStream(oldSource.group, oldSource.stream, true);
798
- if(stopStreamsByGroup[oldSource.group] === undefined){
799
- stopStreamsByGroup[oldSource.group] = [];
800
- }
801
- stopStreamsByGroup[oldSource.group].push(oldSource.stream);
802
- }
803
- if(!_.isEmpty(stopStreamsByGroup)) {
804
- console.log("[DataNode] Emitting consumption error after removing streams")
805
- this.eventBus.emit(DataNode.Event.CONSUMPTION_ERROR, dataNode, stopStreamsByGroup);
806
- }
807
- }, this));
808
- }
809
-
810
- }, this));
811
-
812
- }
813
-
814
- _.each(sources.streamConnections, _.bind(function(source) { // for each source, we check if we are already consuming it
815
-
816
- /**
817
- * if we are not currently consuming a particular group & stream,
818
- * or if we are consuming it but the connection for that group & stream is having
819
- * problem, we will gather the group & stream by source id.
820
- */
821
- if (!this._exist(source) ||
822
- // if there is error at the node currently consuming the stream
823
- // and the connection returned by discoverer is different from the one
824
- // currently use (which is causing error)
825
- this._error(source) || this._hasUpdatedConnection(source)) {
826
-
827
- var nodeId = Util._nodeId(source);
828
-
829
- if (this._hasNode(nodeId)) {
830
- if (toBeUpdated[nodeId] == undefined) toBeUpdated[nodeId] = [];
831
- toBeUpdated[nodeId].push(source);
832
- } else {
833
- if (toBeConnected[nodeId] == undefined) toBeConnected[nodeId] = [];
834
- toBeConnected[nodeId].push(source);
835
- }
836
- }
837
-
838
- }, this));
839
-
840
- // any group, and stream which are in these list should be removed from current
841
- // data node first, before being added to new data node
842
- this._connectUpdate(toBeConnected);
843
- this._connectUpdate(toBeUpdated);
844
-
845
- },
846
-
847
- _reconnectStreams : function(sources) {
848
- var toBeConnected = {}; // a map of new nodes to be connected
849
- var toBeUpdated = {}; // a map of existing node to be updated with new group, streams
850
- _.each(this.discoverer.getSources().streamConnections, _.bind(function(source) { // for each source, we check if we are already consuming it
851
-
852
- /**
853
- * if we are not currently consuming a particular group & stream,
854
- * or if we are consuming it but the connection for that group & stream is having
855
- * problem, we will gather the group & stream by source id.
856
- */
857
- if (!this._exist(source) ||
858
- // if there is error at the node currently consuming the stream
859
- // and the connection returned by discoverer is different from the one
860
- // currently use (which is causing error)
861
- (this._error(source) && this._hasUpdatedConnection(source))) {
862
-
863
- console.log("Reconnecting to stopped stream ", source);
864
- var nodeId = Util._nodeId(source);
865
- if (this._hasNode(nodeId)) {
866
- if (toBeUpdated[nodeId] == undefined) toBeUpdated[nodeId] = [];
867
- toBeUpdated[nodeId].push(source);
868
- } else {
869
- if (toBeConnected[nodeId] == undefined) toBeConnected[nodeId] = [];
870
- toBeConnected[nodeId].push(source);
871
- }
872
- }
873
-
874
- }, this));
875
- this._connectUpdate(toBeConnected);
876
- this._connectUpdate(toBeUpdated);
877
- },
878
-
879
- onNodeReconnectionError : function(dataNode, num) {
880
-
881
- if (num == this.config().exclusionThreshold) {
882
- this.discoverer.excluding(dataNode.id);
883
- this._updateSourcesOnNodeReconnectionError();
884
- console.log("Excluding node ", dataNode.id, " from discovery");
885
- }
886
-
887
- },
888
-
889
- _updateSourcesOnNodeReconnectionError : function(){
890
- // Check if currentAvailableSources is not empty and not used.
891
- if(this.hasUsedCurrentSource == false && _.isEmpty(this.currentAvailableSources) == false){
892
- this._updateSources(this.currentAvailableSources);
893
- this.hasUsedCurrentSources = true; // mark currentAvailableSources has used.
894
- }
895
- },
896
-
897
- onNodeStateChanged : function(dataNode, oldState) {
898
-
899
- // if the data node is destroyed or disconnected. Need to inform clients about the consumption error
900
- // also mark those sources provided by that nodes as having error state
901
- if (dataNode.state == DataNode.State.DESTROYED || dataNode.state == DataNode.State.DISCONNECTED) {
902
-
903
- for(var property in this.consuming){
904
- if(this.consuming.hasOwnProperty(property)){
905
- if(this.consuming[property].node.id == dataNode.id){
906
- this.consuming[property].state = this.consumptionState.ERROR;
907
- }
908
- }
909
- }
910
-
911
- var streamsByGroup = {};
912
-
913
- for(var property in dataNode.streams){
914
- if(dataNode.streams.hasOwnProperty(property)){
915
- streamsByGroup[property] = [];
916
- for (streamInfo in dataNode.streams[property])
917
- streamsByGroup[property].push(streamInfo.stream);
918
- }
919
- }
920
-
921
- if (!_.isEmpty(streamsByGroup)) { // if streamsByGroup doesn't contain any thing, no need to emit event
922
- console.log("[Subscriber] emitting consumption error due to data node failed.", dataNode.state);
923
- this.dispatch(new Subscriber.Event.ConsumptionError(dataNode.client, streamsByGroup));
924
- }
925
- }
926
-
927
- if (oldState === DataNode.State.DISCONNECTED && dataNode.state === DataNode.State.CONNECTED) {
928
- this.discoverer.removeExclusion(dataNode.id);
929
- console.log("Node ", dataNode.id, " resume successfully. Remove it from exclusion.");
930
- } else if (dataNode.state === DataNode.State.DESTROYED) {
931
- // when it's destroyed, we are not going to reconnect to it automatically, so remove from exclusion
932
- this.discoverer.removeExclusion(dataNode.id);
933
- delete this.nodes[dataNode.id];
934
- console.log("Node ", dataNode.id, " is destroyed and removed from subscriber!");
935
-
936
- }
937
-
938
- },
939
-
940
- onStreamConsumption : function(dataNode, streamsByGroup){
941
- this.dispatch(new Subscriber.Event.ConsumptionStart(dataNode.client, streamsByGroup));
942
- },
943
-
944
- onStreamError : function(dataNode, streamsByGroup){
945
- this.dispatch(new Subscriber.Event.ConsumptionError(dataNode.client, streamsByGroup));
946
- },
947
-
948
- onStreamStop : function(dataNode, streamsByGroup){
949
- this.dispatch(new Subscriber.Event.ConsumptionStop(dataNode.client, streamsByGroup));
950
- },
951
-
952
- _discovererError : function (err) {
953
-
954
- },
955
-
956
- /**
957
- * This method remove the group & stream which are going to be
958
- * consumed in NEW DataNode or to be ADDED to other EXISTING
959
- * DataNodes from the current DataNodes they belong to (because current DataNodes
960
- * might be having connection error, which marks the respective group & streams
961
- * as error). Only when there is no one else consuming the stream,
962
- * the stream can be registered into a new node.
963
- *
964
- * @param sources : a map from the nodeId of nodes to be added / updated to the group and stream.
965
- */
966
- _connectUpdate : function(sources) {
967
-
968
- _.each(sources, _.bind(function(connections, nodeId) {
969
-
970
- _.each(connections, _.bind(function(streamInfo) {
971
- var streamId = Util._streamId(streamInfo);
972
-
973
- if (!this.consuming[streamId]) this.consuming[streamId] = new StreamConsumptionWrapper();
974
-
975
- var wrapper = this.consuming[streamId];
976
-
977
- var promise = Promise.resolve(0);
978
-
979
- // this will automatically destroy the node if there is no stream for it to consume
980
- if (wrapper !== undefined && wrapper.node != undefined && wrapper.state == this.consumptionState.ERROR)
981
- promise = wrapper.node.removeStream(streamInfo.group, streamInfo.stream, false); // if consumption state is already error, no need to send deregistration
982
-
983
- promise.done = function(onFulfilled, onRejected) {
984
- this.then(onFulfilled, onRejected)
985
- .catch(function (reason) {
986
- console.log("Promise Rejected: " + reason + "\n" + reason.stack);
987
- });
988
- };
989
-
990
- // this stream is going to be consume soon
991
- wrapper.state = this.consumptionState.PENDING;
992
- /**
993
- * when the promise is done
994
- * it means that there is no one consuming the group & stream
995
- * it's okay now that I'll start consuming it
996
- */
997
- if (this.nodes[nodeId]) { // if the node already existed
998
- promise.done(_.bind(function(result) {
999
-
1000
- this.nodes[nodeId].consumeStream(streamInfo);
1001
- wrapper.node = this.nodes[nodeId]; // mark this so that no one else can consume this stream
1002
-
1003
- }, this));
1004
- } else { // a new data node has to be created
1005
-
1006
- this.nodes[nodeId] = new DataNode(nodeId, this.clientId, this.ticket, streamInfo, this.eventBus, this.config());
1007
- promise.done(_.bind(function(result) {
1008
- wrapper.node = this.nodes[nodeId]; // mark this so that no one else can consume this stream
1009
- this.nodes[nodeId].connect();
1010
-
1011
- }, this));
1012
-
1013
- }
1014
-
1015
- }, this));
1016
-
1017
- }, this));
1018
-
1019
- },
1020
-
1021
- /**
1022
- * Check if a group and stream existed
1023
- * This means, the subscriber is consuming data for this group and stream from
1024
- * a source.
1025
- */
1026
- _exist : function(source) {
1027
- var streamId = Util._streamId(source);
1028
-
1029
- if (this.consuming[streamId]) return true;
1030
-
1031
- return false;
1032
- },
1033
-
1034
- /**
1035
- * Check if a consuming group and stream is having any problem.
1036
- * A consuming group and stream has problem if the DataNode for it is having
1037
- * connection problem.
1038
- */
1039
- _error : function(source) {
1040
- var streamId = Util._streamId(source);
1041
-
1042
- if (this.consuming[streamId] && this.consuming[streamId].state == this.consumptionState.ERROR) return true;
1043
-
1044
- return false;
1045
- },
1046
-
1047
- /**
1048
- * Check if a source returned by discoverer has newer connection
1049
- * than the one being used
1050
- */
1051
- _hasUpdatedConnection : function (source) {
1052
- var streamId = Util._streamId(source);
1053
-
1054
- if (!this.consuming[streamId] || !this.consuming[streamId].node ||
1055
- this.consuming[streamId].node.state == DataNode.State.DESTROYED) {
1056
- console.log("Has updated source");
1057
-
1058
- if (!this.consuming[streamId])
1059
- console.log("Because the source is not consuming");
1060
- else if (!this.consuming[streamId].node)
1061
- console.log("Because the source is not consumed from a node");
1062
- else if (this.consuming[streamId].node == DataNode.State.DESTROYED)
1063
- console.log("Because the source's node does not exist");
1064
-
1065
- return true; // if this source is not in the consuming list or if it is, but there is no node for it
1066
- } else if (Util._nodeId(source) != this.consuming[streamId].node.id) {
1067
- console.log("Has updated source because the new node id is different from previous!");
1068
- return true; // if the service id of the source is different from service id of the node
1069
- }
1070
-
1071
- return false;
1072
- },
1073
-
1074
- _hasNode : function(nodeId) {
1075
- return this.nodes[nodeId] !== undefined;
1076
- },
1077
-
1078
- }, IEndPointEventSource);
1079
-
1080
- module.exports = exports = {
1081
- Subscriber : Subscriber,
1082
- SubscriberConfig : SubscriberConfig
1083
- }
1
+ var coreMsges = require("./CoreMessages")
2
+ var deliveryStack = require("./DeliveryStack")
3
+ var _ = require("underscore")
4
+ var EventEmitter = require("eventemitter3");
5
+
6
+ var streamFinder = require("@jayesol/jayeson.lib.streamfinder");
7
+ var Discoverer = streamFinder.Discoverer;
8
+
9
+ var util = require("./util")
10
+ var _c = util._c
11
+
12
+ var DefaultClient = deliveryStack.DefaultClient
13
+ var EPEvent = deliveryStack.EPEvent
14
+ var EPConnectedEvent = deliveryStack.EPConnectedEvent
15
+ var EPConnectionError = deliveryStack.EPConnectionError
16
+ var EPDisconnectedEvent = deliveryStack.EPDisconnectedEvent
17
+ var IEndPointEventSource = deliveryStack.IEndPointEventSource
18
+ var generateUUID = deliveryStack.generateUUID;
19
+ var Util = deliveryStack.Util;
20
+ var MessageWrapper = deliveryStack.MessageWrapper;
21
+
22
+ var StreamRegistryMessageGroup = coreMsges.StreamRegistryMessageGroup;
23
+ var EmptyMessageClass = coreMsges.EmptyMessageClass;
24
+ var StringMessageClass = coreMsges.StringMessageClass;
25
+ var JSonMessageClass = coreMsges.JSonMessageClass;
26
+ var StreamRegistryRequest = coreMsges.StreamRegistryRequest;
27
+ var StreamRegistryResponse = coreMsges.StreamRegistryResponse;
28
+ var AuthGroup = coreMsges.AuthGroup;
29
+ var GenericMessageGroup = coreMsges.GenericMessageGroup;
30
+ var KeepAliveMessageGroup = coreMsges.KeepAliveMessageGroup;
31
+ var TicketRenew = coreMsges.TicketRenew;
32
+
33
+ /**
34
+ * @author Ryan
35
+ *
36
+ * This depends on external libraries: underscorejs, eventemitter, promisejs
37
+ *
38
+ * This depends on internal libraries: coremessage, deliverystack, jsdiscoverer
39
+ *
40
+ * The subscriber has the following features:
41
+ * TODO: FILL IN THE FEATURES
42
+ *
43
+ */
44
+
45
+ /**
46
+ * Represents a configuration being passed
47
+ * into the subscriber. In the javascript context,
48
+ *
49
+ * The discovery pattern has the following format
50
+ * {
51
+ * id1 : [pattern 1, pattern 2 .... ],
52
+ * id2 : [pattern A, pattern B .... ]
53
+ * }
54
+ *
55
+ * example
56
+ *
57
+ * {
58
+ * 8 : ["*_LIVE", "SBO_*"], // price feed
59
+ * 7 : ["order_ryan"] // order data feed for user ryan
60
+ * }
61
+ *
62
+ * The processor setup is as following:
63
+ * {
64
+ * id1 : processor 1 of type IMessageGroupProcessor,
65
+ * id2 : processor 2 of type IMessageGroupProcessor
66
+ *
67
+ * }
68
+ *
69
+ */
70
+ function SubscriberConfig() {
71
+ this.autoDiscover = true; // by default, enable auto discovery
72
+ this.streamFinder = ""; // the URL to connect to the stream finder
73
+ this.username = ""; // if we want to login directly to the stream finder
74
+ this.password = "";
75
+ this.discoveryPattern = {};
76
+ this.processors = {};
77
+ this.schemes = ["ws"]; // by default we consume from ws
78
+ this.ticketRenewalInterval = 14*60*1000; // default to 14 minutes
79
+ this.reconnectionInterval = 3000; // reconnection to a relay node
80
+ this.exclusionThreshold = 3; // the number of reconnection failed before the relay node is added to the excluded list
81
+ this.feedScope = "";
82
+ this.discoverByScope = true; //new version will discover by scope
83
+ }
84
+
85
+ /**
86
+ * Represents a data node. This encapsulate the following information
87
+ * <li>The client used to connect to the node</li>
88
+ * <li>The stream being consumed from the node</li>
89
+ *
90
+ * It also provides functionalities such as reconnection. It listen to ticket being renewed
91
+ * by the subscriber and send the ticket to the server accordingly.
92
+ *
93
+ * @params
94
+ * <li>id - id of the data node</li>
95
+ * <li>streamInfo - the first stream information used for connection<br/>
96
+ * <pre>
97
+ * {
98
+ * "group": "<groupId>",
99
+ * "stream": "<streamName>",
100
+ * "level": <level>,
101
+ * "serviceId" : <serviceId>,
102
+ * "connection": {
103
+ * "protocol": "<protocol>",
104
+ * "hostname": "<hostname>",
105
+ * "port": <port>
106
+ * }
107
+ * }
108
+ * </pre>
109
+ * </li>
110
+ * <li>clientId - id of the client</li>
111
+ * <li>ticket - ticket of the subscriber</li>
112
+ * <li>eventBus - an Event Emitter which is responsible to propagate events from DataNode</li>
113
+ * <li>subscriberConfig - configuration of the subscriber that created this data node</li>
114
+ */
115
+ var DATA_NODE_COUNTER = 0;
116
+
117
+ function DataNode(id, clientId, ticket, streamInfo, eventBus, subscriberConfig) {
118
+ this.client = null;
119
+ /**
120
+ * This data structure has the following form:
121
+ * { group -> [stream information] }
122
+ */
123
+ this.streams = {};
124
+ this.id = id; // id of the data node
125
+
126
+ this.counter = DATA_NODE_COUNTER++;
127
+ console.log("[DataNode] Node ", id, " counter ", this.counter, " is created!");
128
+
129
+ /**
130
+ * Current state of the data node
131
+ */
132
+ this.state = DataNode.State.CREATED;
133
+ this.connection = streamInfo.connection;
134
+
135
+ this.toBeConsumed = [];
136
+
137
+ this.consumeStream(streamInfo);
138
+
139
+ this.clientId = clientId;
140
+ this.ticket = ticket;
141
+
142
+ this.consumeTaskTimeoutID = 0;
143
+
144
+ this.eventBus = eventBus;
145
+
146
+ this.sConf = subscriberConfig;
147
+
148
+ // the list of promises which will be resolved when stop consumption messages
149
+ // for a stream is handled and replied by the server. The promise
150
+ // shall be successful if there is disconnection while waiting for the reply
151
+ // Format: streamId -> promiseObject e.g. "8_CROWN"
152
+ // the promise value is true / false
153
+ this.removalPromises = {};
154
+ // the list of promises which will be resolved when consumption messages
155
+ // for a stream is handled and replied by the server. The promise shall
156
+ // resolve to failed if there is disconnection waiting for the reply. The promise
157
+ // is created when the registration (not consumption) for the stream starts.
158
+ // The promise value is true / false.
159
+ this.consumptionPromises = {};
160
+
161
+ };
162
+ DataNode.State = {
163
+ CREATED : 1,
164
+ CONNECTING : 2,
165
+ CONNECTED : 3,
166
+ RECONNECTING : 4,
167
+ DISCONNECTED : 5,
168
+ DESTROYED : 6
169
+ };
170
+ DataNode.Event = {
171
+ STATE : "DN_STATE",
172
+ RECONNECTION_ERROR : "DN_RE_ERROR",
173
+ CONSUMPTION_ERROR : "DN_CONSUMPTION_ERROR",
174
+ CONSUMPTION_START : "DN_CONSUMPTION_START",
175
+ CONSUMPTION_STOP : "DN_CONSUMPTION_STOP",
176
+ };
177
+ _c(DataNode, {
178
+
179
+ /**
180
+ * This method return a PROMISE of the removal of the stream from the
181
+ * from consumption. This has to be asynchronous because involving sending
182
+ * deregistration message to the stream registry of the consuming node. We however, only
183
+ * send unregister when the node is in CONNECTED STATE and the person invoke this method
184
+ * specify he want to do so (sendDeregistration = true)
185
+ *
186
+ * There are two cases that a stream can be removed from a data node:
187
+ * <li>The node that consuming the stream has an error and needs reconnection</li>
188
+ * <li>A better node can be found for the stream according to the stream selection strategy</li>
189
+ *
190
+ * @return a promise which is resolved when the stream is completely removed from consumption.
191
+ */
192
+ removeStream : function(group, stream, sendDeregistration) {
193
+
194
+ var id = Util._streamIdFromNames(group, stream);
195
+
196
+ var deregistrationAction = _.bind(function(registrationResult) {
197
+ // the unregistration promise. This is done when the
198
+ // the stream is unregister from this data node by sending message to
199
+ // the data source
200
+ var promise;
201
+
202
+ // the action to be done, remove the stream from the array
203
+ var unregisterPostAction = _.bind(function(result) {
204
+ if (result && this.streams[group]) {
205
+ var index;
206
+
207
+ for (index = 0; index < this.streams[group].length; index++)
208
+ if (stream == this.streams[group][index].stream) break;
209
+
210
+ if (index < this.streams[group].length) this.streams[group].splice(index, 1);
211
+
212
+ if (this.streams[group].length == 0) delete this.streams[group];
213
+
214
+ // if there is no more stream this node is consuming, just destroy itself
215
+ if (_.isEmpty(this.streams)) this.destroy();
216
+ }
217
+
218
+ return result;
219
+ }, this);
220
+
221
+ if (this.state == DataNode.State.CONNECTED && sendDeregistration) {
222
+
223
+
224
+ promise = Util._newPromise();
225
+
226
+
227
+ this.removalPromises[id] = promise;
228
+
229
+ var req = new StreamRegistryRequest(group, [stream], StreamRegistryRequest.DEREGISTRATION);
230
+ this.client.send(new MessageWrapper(req, StreamRegistryMessageGroup.REQUEST));
231
+
232
+ var self = this;
233
+ window.setTimeout(function() {
234
+ //proceed with deregistration if no deregistration responses are received.
235
+ if (self.removalPromises[id] !== undefined) {
236
+ console.warn("Timeout waiting for deregistration response, proceeding to treat stream " + group + ":" + stream + " as deregistered for " + self.connection.hostname + ":" + self.connection.port);
237
+ var streamResponse = {};
238
+ streamResponse[stream] = StreamRegistryResponse.DEREGISTRATION_TIMEOUT;
239
+ var response = new StreamRegistryResponse(group, streamResponse, StreamRegistryResponse.DEREGISTRATION);
240
+
241
+ self._processDeregistrationResult(response);
242
+ }
243
+ }, 7000);
244
+
245
+ // chain the promise
246
+ return promise.then(unregisterPostAction);
247
+
248
+ } else {
249
+ // if the DataNode is not in connected state, no need to send anything!
250
+ promise = Promise.resolve(unregisterPostAction(true));
251
+ return promise;
252
+ }
253
+ }, this);
254
+
255
+ // if there is a consumption in progress, wait for it! We will de-register the stream after the it finish consumption
256
+ // de-registration only happens in failing over or switching to better stream, so it's okay to de-register
257
+ // even when the pending consumption is successful. We switch anyway.
258
+ if (this.consumptionPromises && this.consumptionPromises[id] !== undefined) {
259
+ return this.consumptionPromises[id].then(deregistrationAction);
260
+ } else {
261
+ return deregistrationAction(true);
262
+ }
263
+
264
+ },
265
+
266
+ hasStream : function(group, stream) {
267
+ if (!this.streams[group]) {
268
+ return false;
269
+ }
270
+ var stream = _.each(this.streams[group], function(node) { return node.stream == stream; } );
271
+ return stream !== undefined;
272
+ },
273
+
274
+ /**
275
+ * Start consumption of a particular stream by putting it into tobeConsumed queue.
276
+ * When a stream is consumed successfully, DataNode will emit an event
277
+ * so that other parties know about it.
278
+ */
279
+ consumeStream : function(streamInfo) {
280
+
281
+ console.log("Node "+this.id+" is going to consume "+streamInfo.group+"_"+streamInfo.stream);
282
+
283
+ this.toBeConsumed.push(streamInfo);
284
+
285
+ if (!this.streams[streamInfo.group]) this.streams[streamInfo.group] = [];
286
+
287
+ this.streams[streamInfo.group].push(streamInfo); // put the stream into the list of streams that this DataNode handling
288
+
289
+ if (this.consumeTaskTimeoutID == 0 && this.state == DataNode.State.CONNECTED)
290
+ this.consumeTaskTimeoutID = window.setTimeout(_.bind(this.consumeTask, this), 100);
291
+ },
292
+
293
+ _consumeStreamsOnReconnected : function(){
294
+
295
+ if (_.isEmpty(this.streams)) this.destroy();
296
+
297
+ for(var group in this.streams){
298
+ if(this.streams.hasOwnProperty(group)){
299
+ for(var i=0 ; i<this.streams[group].length; i++){
300
+ this.toBeConsumed.push(this.streams[group][i]);
301
+ }
302
+ }
303
+ }
304
+ },
305
+
306
+ connect : function() {
307
+
308
+ console.log("[DataNode] Id ", this.id, " counter ", this.counter, " is going to connect. Current state ", this.state);
309
+
310
+ //temp code
311
+ //this.connection.hostname = "local.vodds.jayeson.com.sg";
312
+ //this.connection.port = 5000;
313
+
314
+ this.client = new DefaultClient({
315
+ url : this._getUrl(this.connection),
316
+ msgClass : AuthGroup.classById(1),
317
+ params : {
318
+ clientId : this.clientId,
319
+ ticket : this.ticket,
320
+ feedScope : this.sConf.feedScope
321
+ }
322
+ }, KeepAliveMessageGroup);
323
+ this.client.attachListener(this);
324
+ this.client.enableKeepAlive();
325
+ this.client.registerGroup(AuthGroup, this);
326
+ this.client.registerGroup(StreamRegistryMessageGroup, this);
327
+
328
+ this.client.connect();
329
+ },
330
+
331
+ reconnect : function() {
332
+
333
+ console.log("[DataNode] Id ", this.id, " counter ", this.counter, " is going to re-connect. Current state ", this.state);
334
+
335
+ // update the parameters to be updated when reconnect
336
+ // the ticket might have changed!!
337
+ this.client.config.params = {
338
+ clientId : this.clientId,
339
+ ticket : this.ticket,
340
+ feedScope : this.sConf.feedScope
341
+ }
342
+ this.client.connect();
343
+ },
344
+
345
+ _getUrl : function (connection) {
346
+ return connection.protocol+"://"+connection.hostname+((connection.port !== undefined)?(":"+connection.port):"")+((connection.path !== undefined)?connection.path:"");
347
+ },
348
+
349
+ /**
350
+ * check the consume queue and do registration one by one
351
+ */
352
+ consumeTask : function () {
353
+
354
+ var self = this;
355
+
356
+ if (this.state == DataNode.State.CONNECTED) {
357
+ var streamByGroup = _.groupBy(this.toBeConsumed, function(streamInfo) { return streamInfo.group; } );
358
+
359
+ // javascript is single threaded so no need to worry about synchronization here!!
360
+ this.toBeConsumed = []; // clear the toBeConsumed list.
361
+ this.consumeTaskTimeoutID = 0; // reset the task handler
362
+
363
+ // for each group to be consumed, create a promise, send a message
364
+ // the promise will be resolved by the onMessage method, or when disconnection
365
+ _.each(streamByGroup, function(streams, group) {
366
+
367
+ // if no one is removing the stream, then we can consume it
368
+ // this is to avoid double consumption from two different node
369
+ // because if removal can only be the result of failing over
370
+ // or switching to a better source
371
+ // if the stream is being in the process of consuming, we don't
372
+ // resend registration as well
373
+ var canConsume = _.filter(streams, function(streamInfo) {
374
+ var sid = Util._streamId(streamInfo);
375
+
376
+ return self.removalPromises[sid] === undefined &&
377
+ self.consumptionPromises[sid] === undefined;
378
+ });
379
+
380
+ // we have to create one promise for each stream whose registration
381
+ // is going to be sent, because we handle at stream level
382
+ // store those promises to the consuming list
383
+ var promises = _.reduce(canConsume, function(memo, streamInfo) {
384
+
385
+ var sid = Util._streamId(streamInfo);
386
+ memo[sid] = Util._newPromise();
387
+ memo[sid].then(function(result) {
388
+ console.log("Consumption result of ", sid, " : successful = ",result);
389
+ });
390
+ return memo;
391
+
392
+ }, self.consumptionPromises);
393
+
394
+ // send the registration messages
395
+ var streamNames = _.map(canConsume, function(streamInfo) {
396
+ return streamInfo.stream;
397
+ });
398
+
399
+ var req = new StreamRegistryRequest(group, streamNames, StreamRegistryRequest.REGISTRATION);
400
+ // start consumption by first registration message
401
+ self.client.send(new MessageWrapper(req, StreamRegistryMessageGroup.REQUEST));
402
+
403
+ });
404
+ }
405
+
406
+ },
407
+
408
+ onEvent : function(event) {
409
+
410
+ console.log("[DataNode] Event from the stack client", event);
411
+
412
+ if (event instanceof EPConnectedEvent) {
413
+ if(this.state === DataNode.State.DISCONNECTED){
414
+ this._consumeStreamsOnReconnected();
415
+ }
416
+ this.setState(DataNode.State.CONNECTED);
417
+ console.log("[DataNode] Connect / Re-connect to ", this.connection.hostname,":", this.connection.port, "successfully. Consumption will be started!");
418
+
419
+ this.consumeTaskTimeoutID = window.setTimeout(_.bind(this.consumeTask, this), 100);
420
+
421
+ } else if (event instanceof EPDisconnectedEvent || event instanceof EPConnectionError) {
422
+ if (this.state == DataNode.State.DESTROYED) return; // do nothing if the data node is destroyed.
423
+
424
+ this.setState(DataNode.State.DISCONNECTED);
425
+ console.log("Connect / Re-connect to ",this.connection.hostname,":", this.connection.port, " with error. Retry again later.");
426
+
427
+ // disconnection will result in all consumptionPromises resolve to false, and all
428
+ // de-registration promises resolve to true
429
+ _.each(this.consumptionPromises, function(p, key) { p.resolve(false); } );
430
+ _.each(this.removalPromises, function(p, key) { p.resolve(true); } );
431
+
432
+ delete this.consumptionPromises;
433
+ delete this.removalPromises;
434
+
435
+ this.consumptionPromises = {};
436
+ this.removalPromises = {};
437
+
438
+ if (this.state != DataNode.State.DESTROYED) window.setTimeout(_.bind(this.reconnect, this), this.sConf.reconnectionInterval);
439
+ else console.log("Data node ", this.id, " has been destroyed. Stop reconnecting.");
440
+
441
+ if (this.consumeTaskTimeoutID !== 0) window.clearTimeout(this.consumeTaskTimeoutID);
442
+
443
+ if (event instanceof EPConnectionError) this.eventBus.emit(DataNode.Event.RECONNECTION_ERROR, this, ++this.numFailedReconnection);
444
+ else this.numFailedReconnection = 0;
445
+
446
+ }
447
+
448
+ },
449
+
450
+ process : function(messageWrapper) {
451
+
452
+ if (messageWrapper.message instanceof StreamRegistryResponse) {
453
+ var rep = messageWrapper.message;
454
+
455
+ if (rep.responseType == StreamRegistryResponse.REGISTRATION) { // registration response
456
+
457
+ this._processRegistrationResult(rep);
458
+
459
+ } else if (rep.responseType == StreamRegistryResponse.CONSUMPTION) {
460
+
461
+ this._processConsumptionResult(rep);
462
+
463
+ } else if (rep.responseType == StreamRegistryResponse.DEREGISTRATION) {
464
+
465
+ this._processDeregistrationResult(rep);
466
+
467
+ } else if (rep.responseType == StreamRegistryResponse.STOP_CONSUMPTION) {
468
+ this._processStopConsumptionResult(rep);
469
+ }
470
+ }
471
+
472
+ },
473
+
474
+ _resolveConsumptionPromise : function(group, stream, value) {
475
+ var id = Util._streamIdFromNames(group, stream);
476
+ if (this.consumptionPromises[id] !== undefined)
477
+ this.consumptionPromises[id].resolve(value);
478
+
479
+ delete this.consumptionPromises[id];
480
+ },
481
+
482
+ _resolveRemovalPromise : function(group, stream, value) {
483
+ var id = Util._streamIdFromNames(group, stream);
484
+ if (this.removalPromises[id] !== undefined) {
485
+ this.removalPromises[id].resolve(value);
486
+ delete this.removalPromises[id];
487
+ }
488
+
489
+ delete this.consumptionPromises[id];
490
+ },
491
+
492
+ _processDeregistrationResult : function (rep) {
493
+ var self = this;
494
+
495
+ var group = rep.messageGroup;
496
+ var errorConsumedStreamByGroup = {}; // contain all unsuccessfully consumed streams
497
+ _.each(rep.streams, function(result, stream) {
498
+ if (result == StreamRegistryResponse.FAILED) {
499
+ self._resolveRemovalPromise(group, stream, false);
500
+ } else {
501
+ self._resolveRemovalPromise(group, stream, true);
502
+ if(errorConsumedStreamByGroup[group] === undefined){
503
+ errorConsumedStreamByGroup[group] = [];
504
+ }
505
+ errorConsumedStreamByGroup[group].push(stream);
506
+ }
507
+ });
508
+ if(_.isEmpty(errorConsumedStreamByGroup) == false){
509
+ console.log("[DataNode] Emitting consumption error after Deregistration")
510
+ this.eventBus.emit(DataNode.Event.CONSUMPTION_ERROR, this, errorConsumedStreamByGroup);
511
+ }
512
+ },
513
+
514
+ _processStopConsumptionResult : function(rep) {
515
+ var self = this;
516
+ var group = rep.messageGroup;
517
+ var stoppedStreamsByGroup = {};
518
+ _.each(rep.streams, function(result, stream) {
519
+ console.log("Stopping stream " + stream);
520
+ self.removeStream(group, stream, false);
521
+ if(stoppedStreamsByGroup[group] === undefined){
522
+ stoppedStreamsByGroup[group] = [];
523
+ }
524
+ stoppedStreamsByGroup[group].push(stream);
525
+ });
526
+ if (!_.isEmpty(stoppedStreamsByGroup)) {
527
+ this.eventBus.emit(DataNode.Event.CONSUMPTION_STOP, this, stoppedStreamsByGroup);
528
+ }
529
+ },
530
+
531
+ _processConsumptionResult : function (rep) {
532
+ var self = this;
533
+
534
+ var group = rep.messageGroup;
535
+ var consumedStreamByGroup = {}; // contain all successfully consumed streams
536
+ var errorConsumedStreamByGroup = {}; // contain all unsuccessfully consumed streams
537
+
538
+ _.each(rep.streams, function(result, stream) {
539
+ if (result == StreamRegistryResponse.FAILED) {
540
+ self._resolveConsumptionPromise(group, stream, false);
541
+ self.removeStream(group, stream, true);
542
+
543
+ if(errorConsumedStreamByGroup[group] === undefined){
544
+ errorConsumedStreamByGroup[group] = [];
545
+ }
546
+
547
+ errorConsumedStreamByGroup[group].push(stream);
548
+ } else {
549
+ self._resolveConsumptionPromise(group, stream, true);
550
+ if(consumedStreamByGroup[group] === undefined){
551
+ consumedStreamByGroup[group] = [];
552
+ }
553
+ consumedStreamByGroup[group].push(stream);
554
+ }
555
+ });
556
+
557
+ if(_.isEmpty(consumedStreamByGroup) == false){
558
+ this.eventBus.emit(DataNode.Event.CONSUMPTION_START, this, consumedStreamByGroup);
559
+ }
560
+ if(_.isEmpty(errorConsumedStreamByGroup) == false){
561
+ this.eventBus.emit(DataNode.Event.CONSUMPTION_ERROR, this, errorConsumedStreamByGroup);
562
+ }
563
+
564
+ },
565
+
566
+ _processRegistrationResult : function(rep) {
567
+
568
+ var self = this;
569
+ var group = rep.messageGroup;
570
+
571
+ // complete and remove any failed promise
572
+ var cannotConsume = _.pairs(rep.streams)
573
+ .filter(function(p) { return p[1] == StreamRegistryResponse.FAILED; })
574
+ .map(_.first);
575
+
576
+ // remove these streams from the node
577
+ cannotConsume.forEach(function(streamName) {
578
+ self._resolveConsumptionPromise(group, streamName, false);
579
+ self.removeStream(group, streamName, false); // the registration is failed, so no need to send deregistration
580
+ });
581
+
582
+ var canConsume = _.pairs(rep.streams)
583
+ .filter(function(p) { return p[1] == StreamRegistryResponse.SUCCESS; })
584
+ .map(_.first) // get the stream name
585
+ .filter(function(streamName) {
586
+ if (self.sConf.processors[group] !== undefined) { // can consume because processor for the group is defined
587
+ return true;
588
+ } else { // cannot consume because processor is undefined
589
+ self._resolveConsumptionPromise(group, streamName, false);
590
+ return false;
591
+ }
592
+ });
593
+
594
+ // send consumption message, at this point, the server can be sure that the processor is ready to handle the stream
595
+ if (!_.isEmpty(canConsume)) {
596
+
597
+ var processor = self.sConf.processors[group];
598
+ self.client.registerGroup(processor.group(), processor); // attach processor for all the consumable group, whose processor is defined.
599
+
600
+ var req = new StreamRegistryRequest(group, canConsume, StreamRegistryRequest.CONSUMPTION);
601
+ self.client.send(new MessageWrapper(req, StreamRegistryMessageGroup.REQUEST));
602
+ }
603
+
604
+ if (!_.isEmpty(cannotConsume)) {
605
+ var eventData = {};
606
+ eventData[group] = cannotConsume;
607
+ console.log("Emitting consumption error due to registration result failed.", eventData);
608
+
609
+ this.eventBus.emit(DataNode.Event.CONSUMPTION_ERROR, this, eventData);
610
+ }
611
+ },
612
+
613
+ setState : function(state) {
614
+
615
+ if (state != this.state) {
616
+ var oldState = this.state;
617
+ this.state = state;
618
+ this.eventBus.emit(DataNode.Event.STATE, this, oldState);
619
+ }
620
+
621
+ },
622
+
623
+ renewTicket : function(ticket) {
624
+ this.ticket = ticket;
625
+ var wrapper = new MessageWrapper(new TicketRenew(this.clientId, this.ticket), AuthGroup.TICKET_RENEW);
626
+ if (this.state == DataNode.State.CONNECTED) this.client.send(wrapper);
627
+ },
628
+
629
+ destroy : function() {
630
+ this.setState(DataNode.State.DESTROYED);
631
+ console.log("[DataNode] Disconnect the destroyed data node.");
632
+ this.client.disconnect();
633
+ }
634
+
635
+ });
636
+
637
+ function Subscriber(configuration) {
638
+ IEndPointEventSource.call(this); // initial IEndPointEventSource
639
+ this.conf = new SubscriberConfig();
640
+
641
+ _.extend(this.conf, configuration); // copy the configuration over
642
+
643
+ this.consuming = {}; // a map from group / stream to the wrapper
644
+ this.nodes = {}; // a map from source id to the information related to the source, e.g. state whether connecting, reconnecting, connected or the client used to connect to the node
645
+
646
+ // store the currently available sources retrieved from the discoverer
647
+ // this data will be used for fast failover whenever there is some
648
+ // connection error in a DataNode.
649
+ this.currentAvailableSources = null;
650
+ // Check currentAvailableSources has used.
651
+ this.hasUsedCurrentSources = false;
652
+
653
+ this.clientId = this.uuid();
654
+ this.ticket = null;
655
+
656
+ this.eventBus = new EventEmitter();
657
+
658
+ this.eventBus.on(DataNode.Event.RECONNECTION_ERROR, _.bind(this.onNodeReconnectionError, this));
659
+ this.eventBus.on(DataNode.Event.STATE, _.bind(this.onNodeStateChanged, this));
660
+ this.eventBus.on(DataNode.Event.CONSUMPTION_START, _.bind(this.onStreamConsumption,this));
661
+ this.eventBus.on(DataNode.Event.CONSUMPTION_ERROR, _.bind(this.onStreamError, this));
662
+ this.eventBus.on(DataNode.Event.CONSUMPTION_STOP, _.bind(this.onStreamStop, this));
663
+ }
664
+ function StreamConsumptionWrapper(state, node) {
665
+ this.state = state;
666
+ this.node = node;
667
+ }
668
+
669
+ /**
670
+ * Define Subscriber event and will be fired everytime a client is connected or disconnected.
671
+ */
672
+ function ConsumptionStart(client, streams) {
673
+ EPEvent.call(this, client._ep);
674
+ this.client = client;
675
+ this.streams = streams;
676
+ }
677
+ function ConsumptionStop(client, streams) {
678
+ EPEvent.call(this, client._ep);
679
+ this.client = client;
680
+ this.streams = streams;
681
+ }
682
+ function ConsumptionError(client, streams) {
683
+ EPEvent.call(this, client._ep);
684
+ this.client = client;
685
+ this.streams = streams;
686
+ }
687
+ _c(ConsumptionStart, {}, EPEvent);
688
+ _c(ConsumptionStop, {}, EPEvent);
689
+ _c(ConsumptionError, {}, EPEvent);
690
+
691
+ Subscriber.Event = {
692
+ ConsumptionStart : ConsumptionStart,
693
+ ConsumptionStop : ConsumptionStop,
694
+ ConsumptionError : ConsumptionError
695
+ };
696
+
697
+ _c(Subscriber, {
698
+
699
+ consumptionState : {
700
+ PENDING : 1, // a stream is in this state when it is waiting for a data node to connect and then perform registration
701
+ ERROR : 3 // a stream is in this state when its data node is having connection problem
702
+ },
703
+
704
+ uuid : function (){
705
+
706
+ return generateUUID();
707
+
708
+ },
709
+
710
+ config : function() { return this.conf; },
711
+ /**
712
+ * This method starts the discovery process
713
+ */
714
+ start : function() {
715
+ var discoverer = new Discoverer(this.config().streamFinder); // create a discoverer to connect to stream finder
716
+ if (this.conf.discoverByScope) {
717
+ discoverer = new Discoverer(this.config().streamFinder, undefined, this.conf.feedScope); // discover by streamfinder scope
718
+ }
719
+
720
+ // configure discovery
721
+ _.each(this.config().discoveryPattern, function(patternList, group) {
722
+
723
+ _.each(patternList, function(pattern) {
724
+ discoverer.discover(group, pattern);
725
+ });
726
+
727
+ });
728
+
729
+ _.each(this.config().schemes, function(scheme) {
730
+ discoverer.using(scheme);
731
+ });
732
+
733
+ discoverer.on("update", _.bind(this._updateSources, this));
734
+ discoverer.on("error", _.bind(this._discovererError, this));
735
+ setInterval(_.bind(this._reconnectStreams, this), 5000);
736
+ this._makeTicket(true);
737
+
738
+ this.discoverer = discoverer;
739
+ },
740
+
741
+ _makeTicket : function(isStarting) {
742
+ SessionFactory.makeTicket(this.config().streamFinder, this.clientId, _.bind(function(ticket, error) {
743
+ if (ticket.length != 0) {
744
+
745
+ this.ticket = ticket;
746
+ console.log("New ticket obtained "+this.ticket);
747
+
748
+ if (isStarting) { // if this is not renewal, we should start the discovery process
749
+ this.discoverer.start();
750
+ } else {
751
+ _.each(this.nodes, _.bind(function(node, id) {
752
+ window.setTimeout(_.bind(node.renewTicket, node, this.ticket), 0); // asynchronously
753
+ }, this));
754
+ }
755
+
756
+ window.setTimeout(_.bind(this._makeTicket, this), this.config().ticketRenewalInterval);
757
+
758
+ } else if (error.length != 0) {
759
+ window.setTimeout(_.bind(this._makeTicket, this), 1000);
760
+ }
761
+ }, this));
762
+ },
763
+
764
+ _updateSources : function(sources) {
765
+
766
+ console.log("Updating Sources!");
767
+ console.log(sources);
768
+
769
+ this.oldSources = this.currentAvailableSources;
770
+ this.currentAvailableSources = sources.streamConnections;
771
+ this.hasUsedCurrentSources = false;
772
+
773
+ var toBeConnected = {}; // a map of new nodes to be connected
774
+ var toBeUpdated = {}; // a map of existing node to be updated with new group, streams
775
+
776
+ if (this.oldSources && this.oldSources.length > 0) {
777
+ var streamIds = [];
778
+ var nodeIds = [];
779
+
780
+ _.each(sources.streamConnections, function(source) {
781
+ streamIds.push(Util._streamId(source));
782
+ nodeIds.push(Util._nodeId(source));
783
+ });
784
+
785
+ // remove from corresponding old sources not exist in new sources from data nodes
786
+ var stopStreamsByGroup = {};
787
+ _.each(this.oldSources, _.bind(function(oldSource) {
788
+
789
+ if (!_.contains(streamIds, Util._streamId(oldSource)) || !_.contains(nodeIds, Util._nodeId(oldSource))) {
790
+
791
+ console.log("[Subscriber] ", oldSource, " is no longer in the discovery results. Going to be removed.")
792
+
793
+ _.each(this.nodes, _.bind(function(dataNode) {
794
+
795
+ if (dataNode.hasStream(oldSource.group, oldSource.stream)) {
796
+ console.log("[Subscriber] Remove old stream ", oldSource, " from Data Node ", dataNode.id, " dataNode counter ", dataNode.counter);
797
+ dataNode.removeStream(oldSource.group, oldSource.stream, true);
798
+ if(stopStreamsByGroup[oldSource.group] === undefined){
799
+ stopStreamsByGroup[oldSource.group] = [];
800
+ }
801
+ stopStreamsByGroup[oldSource.group].push(oldSource.stream);
802
+ }
803
+ if(!_.isEmpty(stopStreamsByGroup)) {
804
+ console.log("[DataNode] Emitting consumption error after removing streams")
805
+ this.eventBus.emit(DataNode.Event.CONSUMPTION_ERROR, dataNode, stopStreamsByGroup);
806
+ }
807
+ }, this));
808
+ }
809
+
810
+ }, this));
811
+
812
+ }
813
+
814
+ _.each(sources.streamConnections, _.bind(function(source) { // for each source, we check if we are already consuming it
815
+
816
+ /**
817
+ * if we are not currently consuming a particular group & stream,
818
+ * or if we are consuming it but the connection for that group & stream is having
819
+ * problem, we will gather the group & stream by source id.
820
+ */
821
+ if (!this._exist(source) ||
822
+ // if there is error at the node currently consuming the stream
823
+ // and the connection returned by discoverer is different from the one
824
+ // currently use (which is causing error)
825
+ this._error(source) || this._hasUpdatedConnection(source)) {
826
+
827
+ var nodeId = Util._nodeId(source);
828
+
829
+ if (this._hasNode(nodeId)) {
830
+ if (toBeUpdated[nodeId] == undefined) toBeUpdated[nodeId] = [];
831
+ toBeUpdated[nodeId].push(source);
832
+ } else {
833
+ if (toBeConnected[nodeId] == undefined) toBeConnected[nodeId] = [];
834
+ toBeConnected[nodeId].push(source);
835
+ }
836
+ }
837
+
838
+ }, this));
839
+
840
+ // any group, and stream which are in these list should be removed from current
841
+ // data node first, before being added to new data node
842
+ this._connectUpdate(toBeConnected);
843
+ this._connectUpdate(toBeUpdated);
844
+
845
+ },
846
+
847
+ _reconnectStreams : function(sources) {
848
+ var toBeConnected = {}; // a map of new nodes to be connected
849
+ var toBeUpdated = {}; // a map of existing node to be updated with new group, streams
850
+ _.each(this.discoverer.getSources().streamConnections, _.bind(function(source) { // for each source, we check if we are already consuming it
851
+
852
+ /**
853
+ * if we are not currently consuming a particular group & stream,
854
+ * or if we are consuming it but the connection for that group & stream is having
855
+ * problem, we will gather the group & stream by source id.
856
+ */
857
+ if (!this._exist(source) ||
858
+ // if there is error at the node currently consuming the stream
859
+ // and the connection returned by discoverer is different from the one
860
+ // currently use (which is causing error)
861
+ (this._error(source) && this._hasUpdatedConnection(source))) {
862
+
863
+ console.log("Reconnecting to stopped stream ", source);
864
+ var nodeId = Util._nodeId(source);
865
+ if (this._hasNode(nodeId)) {
866
+ if (toBeUpdated[nodeId] == undefined) toBeUpdated[nodeId] = [];
867
+ toBeUpdated[nodeId].push(source);
868
+ } else {
869
+ if (toBeConnected[nodeId] == undefined) toBeConnected[nodeId] = [];
870
+ toBeConnected[nodeId].push(source);
871
+ }
872
+ }
873
+
874
+ }, this));
875
+ this._connectUpdate(toBeConnected);
876
+ this._connectUpdate(toBeUpdated);
877
+ },
878
+
879
+ onNodeReconnectionError : function(dataNode, num) {
880
+
881
+ if (num == this.config().exclusionThreshold) {
882
+ this.discoverer.excluding(dataNode.id);
883
+ this._updateSourcesOnNodeReconnectionError();
884
+ console.log("Excluding node ", dataNode.id, " from discovery");
885
+ }
886
+
887
+ },
888
+
889
+ _updateSourcesOnNodeReconnectionError : function(){
890
+ // Check if currentAvailableSources is not empty and not used.
891
+ if(this.hasUsedCurrentSource == false && _.isEmpty(this.currentAvailableSources) == false){
892
+ this._updateSources(this.currentAvailableSources);
893
+ this.hasUsedCurrentSources = true; // mark currentAvailableSources has used.
894
+ }
895
+ },
896
+
897
+ onNodeStateChanged : function(dataNode, oldState) {
898
+
899
+ // if the data node is destroyed or disconnected. Need to inform clients about the consumption error
900
+ // also mark those sources provided by that nodes as having error state
901
+ if (dataNode.state == DataNode.State.DESTROYED || dataNode.state == DataNode.State.DISCONNECTED) {
902
+
903
+ for(var property in this.consuming){
904
+ if(this.consuming.hasOwnProperty(property)){
905
+ if(this.consuming[property].node.id == dataNode.id){
906
+ this.consuming[property].state = this.consumptionState.ERROR;
907
+ }
908
+ }
909
+ }
910
+
911
+ var streamsByGroup = {};
912
+
913
+ for(var property in dataNode.streams){
914
+ if(dataNode.streams.hasOwnProperty(property)){
915
+ streamsByGroup[property] = [];
916
+ for (streamInfo in dataNode.streams[property])
917
+ streamsByGroup[property].push(streamInfo.stream);
918
+ }
919
+ }
920
+
921
+ if (!_.isEmpty(streamsByGroup)) { // if streamsByGroup doesn't contain any thing, no need to emit event
922
+ console.log("[Subscriber] emitting consumption error due to data node failed.", dataNode.state);
923
+ this.dispatch(new Subscriber.Event.ConsumptionError(dataNode.client, streamsByGroup));
924
+ }
925
+ }
926
+
927
+ if (oldState === DataNode.State.DISCONNECTED && dataNode.state === DataNode.State.CONNECTED) {
928
+ this.discoverer.removeExclusion(dataNode.id);
929
+ console.log("Node ", dataNode.id, " resume successfully. Remove it from exclusion.");
930
+ } else if (dataNode.state === DataNode.State.DESTROYED) {
931
+ // when it's destroyed, we are not going to reconnect to it automatically, so remove from exclusion
932
+ this.discoverer.removeExclusion(dataNode.id);
933
+ delete this.nodes[dataNode.id];
934
+ console.log("Node ", dataNode.id, " is destroyed and removed from subscriber!");
935
+
936
+ }
937
+
938
+ },
939
+
940
+ onStreamConsumption : function(dataNode, streamsByGroup){
941
+ this.dispatch(new Subscriber.Event.ConsumptionStart(dataNode.client, streamsByGroup));
942
+ },
943
+
944
+ onStreamError : function(dataNode, streamsByGroup){
945
+ this.dispatch(new Subscriber.Event.ConsumptionError(dataNode.client, streamsByGroup));
946
+ },
947
+
948
+ onStreamStop : function(dataNode, streamsByGroup){
949
+ this.dispatch(new Subscriber.Event.ConsumptionStop(dataNode.client, streamsByGroup));
950
+ },
951
+
952
+ _discovererError : function (err) {
953
+
954
+ },
955
+
956
+ /**
957
+ * This method remove the group & stream which are going to be
958
+ * consumed in NEW DataNode or to be ADDED to other EXISTING
959
+ * DataNodes from the current DataNodes they belong to (because current DataNodes
960
+ * might be having connection error, which marks the respective group & streams
961
+ * as error). Only when there is no one else consuming the stream,
962
+ * the stream can be registered into a new node.
963
+ *
964
+ * @param sources : a map from the nodeId of nodes to be added / updated to the group and stream.
965
+ */
966
+ _connectUpdate : function(sources) {
967
+
968
+ _.each(sources, _.bind(function(connections, nodeId) {
969
+
970
+ _.each(connections, _.bind(function(streamInfo) {
971
+ var streamId = Util._streamId(streamInfo);
972
+
973
+ if (!this.consuming[streamId]) this.consuming[streamId] = new StreamConsumptionWrapper();
974
+
975
+ var wrapper = this.consuming[streamId];
976
+
977
+ var promise = Promise.resolve(0);
978
+
979
+ // this will automatically destroy the node if there is no stream for it to consume
980
+ if (wrapper !== undefined && wrapper.node != undefined && wrapper.state == this.consumptionState.ERROR)
981
+ promise = wrapper.node.removeStream(streamInfo.group, streamInfo.stream, false); // if consumption state is already error, no need to send deregistration
982
+
983
+ promise.done = function(onFulfilled, onRejected) {
984
+ this.then(onFulfilled, onRejected)
985
+ .catch(function (reason) {
986
+ console.log("Promise Rejected: " + reason + "\n" + reason.stack);
987
+ });
988
+ };
989
+
990
+ // this stream is going to be consume soon
991
+ wrapper.state = this.consumptionState.PENDING;
992
+ /**
993
+ * when the promise is done
994
+ * it means that there is no one consuming the group & stream
995
+ * it's okay now that I'll start consuming it
996
+ */
997
+ if (this.nodes[nodeId]) { // if the node already existed
998
+ promise.done(_.bind(function(result) {
999
+
1000
+ this.nodes[nodeId].consumeStream(streamInfo);
1001
+ wrapper.node = this.nodes[nodeId]; // mark this so that no one else can consume this stream
1002
+
1003
+ }, this));
1004
+ } else { // a new data node has to be created
1005
+
1006
+ this.nodes[nodeId] = new DataNode(nodeId, this.clientId, this.ticket, streamInfo, this.eventBus, this.config());
1007
+ promise.done(_.bind(function(result) {
1008
+ wrapper.node = this.nodes[nodeId]; // mark this so that no one else can consume this stream
1009
+ this.nodes[nodeId].connect();
1010
+
1011
+ }, this));
1012
+
1013
+ }
1014
+
1015
+ }, this));
1016
+
1017
+ }, this));
1018
+
1019
+ },
1020
+
1021
+ /**
1022
+ * Check if a group and stream existed
1023
+ * This means, the subscriber is consuming data for this group and stream from
1024
+ * a source.
1025
+ */
1026
+ _exist : function(source) {
1027
+ var streamId = Util._streamId(source);
1028
+
1029
+ if (this.consuming[streamId]) return true;
1030
+
1031
+ return false;
1032
+ },
1033
+
1034
+ /**
1035
+ * Check if a consuming group and stream is having any problem.
1036
+ * A consuming group and stream has problem if the DataNode for it is having
1037
+ * connection problem.
1038
+ */
1039
+ _error : function(source) {
1040
+ var streamId = Util._streamId(source);
1041
+
1042
+ if (this.consuming[streamId] && this.consuming[streamId].state == this.consumptionState.ERROR) return true;
1043
+
1044
+ return false;
1045
+ },
1046
+
1047
+ /**
1048
+ * Check if a source returned by discoverer has newer connection
1049
+ * than the one being used
1050
+ */
1051
+ _hasUpdatedConnection : function (source) {
1052
+ var streamId = Util._streamId(source);
1053
+
1054
+ if (!this.consuming[streamId] || !this.consuming[streamId].node ||
1055
+ this.consuming[streamId].node.state == DataNode.State.DESTROYED) {
1056
+ console.log("Has updated source");
1057
+
1058
+ if (!this.consuming[streamId])
1059
+ console.log("Because the source is not consuming");
1060
+ else if (!this.consuming[streamId].node)
1061
+ console.log("Because the source is not consumed from a node");
1062
+ else if (this.consuming[streamId].node == DataNode.State.DESTROYED)
1063
+ console.log("Because the source's node does not exist");
1064
+
1065
+ return true; // if this source is not in the consuming list or if it is, but there is no node for it
1066
+ } else if (Util._nodeId(source) != this.consuming[streamId].node.id) {
1067
+ console.log("Has updated source because the new node id is different from previous!");
1068
+ return true; // if the service id of the source is different from service id of the node
1069
+ }
1070
+
1071
+ return false;
1072
+ },
1073
+
1074
+ _hasNode : function(nodeId) {
1075
+ return this.nodes[nodeId] !== undefined;
1076
+ },
1077
+
1078
+ }, IEndPointEventSource);
1079
+
1080
+ module.exports = exports = {
1081
+ Subscriber : Subscriber,
1082
+ SubscriberConfig : SubscriberConfig
1083
+ }