message_bus 3.3.7 → 4.1.0

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc.js +1 -8
  3. data/.github/workflows/ci.yml +50 -20
  4. data/.prettierrc +1 -0
  5. data/CHANGELOG +99 -46
  6. data/README.md +31 -53
  7. data/Rakefile +22 -22
  8. data/docker-compose.yml +1 -1
  9. data/lib/message_bus/backends/base.rb +14 -0
  10. data/lib/message_bus/backends/memory.rb +13 -0
  11. data/lib/message_bus/backends/postgres.rb +55 -22
  12. data/lib/message_bus/backends/redis.rb +17 -1
  13. data/lib/message_bus/client.rb +26 -22
  14. data/lib/message_bus/distributed_cache.rb +1 -0
  15. data/lib/message_bus/rack/middleware.rb +0 -6
  16. data/lib/message_bus/rack/thin_ext.rb +1 -0
  17. data/lib/message_bus/version.rb +1 -1
  18. data/lib/message_bus.rb +53 -71
  19. data/message_bus.gemspec +4 -3
  20. data/package.json +2 -5
  21. data/spec/helpers.rb +6 -1
  22. data/spec/lib/fake_async_middleware.rb +1 -0
  23. data/spec/lib/message_bus/backend_spec.rb +20 -3
  24. data/spec/lib/message_bus/client_spec.rb +1 -0
  25. data/spec/lib/message_bus/connection_manager_spec.rb +4 -0
  26. data/spec/lib/message_bus/multi_process_spec.rb +21 -10
  27. data/spec/lib/message_bus/rack/middleware_spec.rb +2 -49
  28. data/spec/lib/message_bus/timer_thread_spec.rb +1 -5
  29. data/spec/lib/message_bus_spec.rb +12 -3
  30. data/spec/performance/backlog.rb +80 -0
  31. data/spec/performance/publish.rb +4 -4
  32. data/spec/spec_helper.rb +1 -1
  33. data/vendor/assets/javascripts/message-bus-ajax.js +38 -0
  34. data/vendor/assets/javascripts/message-bus.js +549 -0
  35. metadata +8 -31
  36. data/assets/application.jsx +0 -121
  37. data/assets/babel.min.js +0 -25
  38. data/assets/react-dom.js +0 -19851
  39. data/assets/react.js +0 -3029
  40. data/examples/diagnostics/Gemfile +0 -6
  41. data/examples/diagnostics/config.ru +0 -22
  42. data/lib/message_bus/diagnostics.rb +0 -62
  43. data/lib/message_bus/rack/diagnostics.rb +0 -120
@@ -32,8 +32,8 @@ benchmark_subscription_no_trimming = lambda do |bm, backend|
32
32
  bus = MessageBus::Instance.new
33
33
  bus.configure(test_config_for_backend(backend))
34
34
 
35
- bus.reliable_pub_sub.max_backlog_size = iterations
36
- bus.reliable_pub_sub.max_global_backlog_size = iterations
35
+ bus.backend_instance.max_backlog_size = iterations
36
+ bus.backend_instance.max_global_backlog_size = iterations
37
37
 
38
38
  messages_received = 0
39
39
  bus.after_fork
@@ -58,8 +58,8 @@ benchmark_subscription_with_trimming = lambda do |bm, backend|
58
58
  bus = MessageBus::Instance.new
59
59
  bus.configure(test_config_for_backend(backend))
60
60
 
61
- bus.reliable_pub_sub.max_backlog_size = (iterations / 10)
62
- bus.reliable_pub_sub.max_global_backlog_size = (iterations / 10)
61
+ bus.backend_instance.max_backlog_size = (iterations / 10)
62
+ bus.backend_instance.max_global_backlog_size = (iterations / 10)
63
63
 
64
64
  messages_received = 0
65
65
  bus.after_fork
data/spec/spec_helper.rb CHANGED
@@ -14,7 +14,7 @@ require_relative "helpers"
14
14
  CURRENT_BACKEND = (ENV['MESSAGE_BUS_BACKEND'] || :redis).to_sym
15
15
 
16
16
  require "message_bus/backends/#{CURRENT_BACKEND}"
17
- PUB_SUB_CLASS = MessageBus::BACKENDS.fetch(CURRENT_BACKEND)
17
+ BACKEND_CLASS = MessageBus::BACKENDS.fetch(CURRENT_BACKEND)
18
18
 
19
19
  puts "Running with backend: #{CURRENT_BACKEND}"
20
20
 
@@ -0,0 +1,38 @@
1
+ // A bare-bones implementation of $.ajax that MessageBus will use
2
+ // as a fallback if jQuery is not present
3
+ //
4
+ // Only implements methods & options used by MessageBus
5
+ (function(global) {
6
+ 'use strict';
7
+ if (!global.MessageBus){
8
+ throw new Error("MessageBus must be loaded before the ajax adapter");
9
+ }
10
+
11
+ global.MessageBus.ajax = function(options){
12
+ var XHRImpl = (global.MessageBus && global.MessageBus.xhrImplementation) || global.XMLHttpRequest;
13
+ var xhr = new XHRImpl();
14
+ xhr.dataType = options.dataType;
15
+ xhr.open('POST', options.url);
16
+ for (var name in options.headers){
17
+ xhr.setRequestHeader(name, options.headers[name]);
18
+ }
19
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
20
+ if (options.messageBus.chunked){
21
+ options.messageBus.onProgressListener(xhr);
22
+ }
23
+ xhr.onreadystatechange = function(){
24
+ if (xhr.readyState === 4){
25
+ var status = xhr.status;
26
+ if (status >= 200 && status < 300 || status === 304){
27
+ options.success(xhr.responseText);
28
+ } else {
29
+ options.error(xhr, xhr.statusText);
30
+ }
31
+ options.complete();
32
+ }
33
+ }
34
+ xhr.send(new URLSearchParams(options.data).toString());
35
+ return xhr;
36
+ };
37
+
38
+ })(window);
@@ -0,0 +1,549 @@
1
+ /*global define, jQuery*/
2
+
3
+ (function (root, factory) {
4
+ if (typeof define === "function" && define.amd) {
5
+ // AMD. Register as an anonymous module.
6
+ define([], function () {
7
+ // Also create a global in case some scripts
8
+ // that are loaded still are looking for
9
+ // a global even when an AMD loader is in use.
10
+ return (root.MessageBus = factory());
11
+ });
12
+ } else {
13
+ // Browser globals
14
+ root.MessageBus = factory();
15
+ }
16
+ })(typeof self !== "undefined" ? self : this, function () {
17
+ "use strict";
18
+
19
+ // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
20
+ var uniqueId = function () {
21
+ return "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, function (c) {
22
+ var r = (Math.random() * 16) | 0;
23
+ var v = c === "x" ? r : (r & 0x3) | 0x8;
24
+ return v.toString(16);
25
+ });
26
+ };
27
+
28
+ var me;
29
+ var delayPollTimeout;
30
+ var ajaxInProgress = false;
31
+ var started = false;
32
+ var clientId = uniqueId();
33
+ var callbacks = [];
34
+ var failCount = 0;
35
+ var baseUrl = "/";
36
+ var paused = false;
37
+ var later = [];
38
+ var chunkedBackoff = 0;
39
+ var stopped;
40
+ var pollTimeout = null;
41
+ var totalAjaxFailures = 0;
42
+ var totalAjaxCalls = 0;
43
+ var lastAjax;
44
+
45
+ var isHidden = (function () {
46
+ var prefixes = ["", "webkit", "ms", "moz"];
47
+ var hiddenProperty;
48
+ for (var i = 0; i < prefixes.length; i++) {
49
+ var prefix = prefixes[i];
50
+ var check = prefix + (prefix === "" ? "hidden" : "Hidden");
51
+ if (document[check] !== undefined) {
52
+ hiddenProperty = check;
53
+ }
54
+ }
55
+
56
+ return function () {
57
+ if (hiddenProperty !== undefined) {
58
+ return document[hiddenProperty];
59
+ } else {
60
+ return !document.hasFocus;
61
+ }
62
+ };
63
+ })();
64
+
65
+ var hasLocalStorage = (function () {
66
+ try {
67
+ localStorage.setItem("mbTestLocalStorage", Date.now());
68
+ localStorage.removeItem("mbTestLocalStorage");
69
+ return true;
70
+ } catch (e) {
71
+ return false;
72
+ }
73
+ })();
74
+
75
+ var updateLastAjax = function () {
76
+ if (hasLocalStorage) {
77
+ localStorage.setItem("__mbLastAjax", Date.now());
78
+ }
79
+ };
80
+
81
+ var hiddenTabShouldWait = function () {
82
+ if (hasLocalStorage && isHidden()) {
83
+ var lastAjaxCall = parseInt(localStorage.getItem("__mbLastAjax"), 10);
84
+ var deltaAjax = Date.now() - lastAjaxCall;
85
+
86
+ return deltaAjax >= 0 && deltaAjax < me.minHiddenPollInterval;
87
+ }
88
+ return false;
89
+ };
90
+
91
+ var hasonprogress = new XMLHttpRequest().onprogress === null;
92
+ var allowChunked = function () {
93
+ return me.enableChunkedEncoding && hasonprogress;
94
+ };
95
+
96
+ var shouldLongPoll = function () {
97
+ return (
98
+ me.alwaysLongPoll ||
99
+ (me.shouldLongPollCallback ? me.shouldLongPollCallback() : !isHidden())
100
+ );
101
+ };
102
+
103
+ var processMessages = function (messages) {
104
+ if (!messages || messages.length === 0) {
105
+ return false;
106
+ }
107
+
108
+ for (var i = 0; i < messages.length; i++) {
109
+ var message = messages[i];
110
+ for (var j = 0; j < callbacks.length; j++) {
111
+ var callback = callbacks[j];
112
+ if (callback.channel === message.channel) {
113
+ callback.last_id = message.message_id;
114
+ try {
115
+ callback.func(message.data, message.global_id, message.message_id);
116
+ } catch (e) {
117
+ if (console.log) {
118
+ console.log(
119
+ "MESSAGE BUS FAIL: callback " +
120
+ callback.channel +
121
+ " caused exception " +
122
+ e.stack
123
+ );
124
+ }
125
+ }
126
+ }
127
+ if (message.channel === "/__status") {
128
+ if (message.data[callback.channel] !== undefined) {
129
+ callback.last_id = message.data[callback.channel];
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ return true;
136
+ };
137
+
138
+ var reqSuccess = function (messages) {
139
+ failCount = 0;
140
+ if (paused) {
141
+ if (messages) {
142
+ for (var i = 0; i < messages.length; i++) {
143
+ later.push(messages[i]);
144
+ }
145
+ }
146
+ } else {
147
+ return processMessages(messages);
148
+ }
149
+ return false;
150
+ };
151
+
152
+ var longPoller = function (poll, data) {
153
+ if (ajaxInProgress) {
154
+ // never allow concurrent ajax reqs
155
+ return;
156
+ }
157
+
158
+ var gotData = false;
159
+ var aborted = false;
160
+ var rateLimited = false;
161
+ var rateLimitedSeconds;
162
+
163
+ lastAjax = new Date();
164
+ totalAjaxCalls += 1;
165
+ data.__seq = totalAjaxCalls;
166
+
167
+ var longPoll = shouldLongPoll() && me.enableLongPolling;
168
+ var chunked = longPoll && allowChunked();
169
+ if (chunkedBackoff > 0) {
170
+ chunkedBackoff--;
171
+ chunked = false;
172
+ }
173
+
174
+ var headers = { "X-SILENCE-LOGGER": "true" };
175
+ for (var name in me.headers) {
176
+ headers[name] = me.headers[name];
177
+ }
178
+
179
+ if (!chunked) {
180
+ headers["Dont-Chunk"] = "true";
181
+ }
182
+
183
+ var dataType = chunked ? "text" : "json";
184
+
185
+ var handle_progress = function (payload, position) {
186
+ var separator = "\r\n|\r\n";
187
+ var endChunk = payload.indexOf(separator, position);
188
+
189
+ if (endChunk === -1) {
190
+ return position;
191
+ }
192
+
193
+ var chunk = payload.substring(position, endChunk);
194
+ chunk = chunk.replace(/\r\n\|\|\r\n/g, separator);
195
+
196
+ try {
197
+ reqSuccess(JSON.parse(chunk));
198
+ } catch (e) {
199
+ if (console.log) {
200
+ console.log("FAILED TO PARSE CHUNKED REPLY");
201
+ console.log(data);
202
+ }
203
+ }
204
+
205
+ return handle_progress(payload, endChunk + separator.length);
206
+ };
207
+
208
+ var disableChunked = function () {
209
+ if (me.longPoll) {
210
+ me.longPoll.abort();
211
+ chunkedBackoff = 30;
212
+ }
213
+ };
214
+
215
+ if (!me.ajax) {
216
+ throw new Error("Either jQuery or the ajax adapter must be loaded");
217
+ }
218
+
219
+ updateLastAjax();
220
+
221
+ ajaxInProgress = true;
222
+ var req = me.ajax({
223
+ url:
224
+ me.baseUrl +
225
+ "message-bus/" +
226
+ me.clientId +
227
+ "/poll" +
228
+ (!longPoll ? "?dlp=t" : ""),
229
+ data: data,
230
+ async: true,
231
+ dataType: dataType,
232
+ type: "POST",
233
+ headers: headers,
234
+ messageBus: {
235
+ chunked: chunked,
236
+ onProgressListener: function (xhr) {
237
+ var position = 0;
238
+ // if it takes longer than 3000 ms to get first chunk, we have some proxy
239
+ // this is messing with us, so just backoff from using chunked for now
240
+ var chunkedTimeout = setTimeout(disableChunked, 3000);
241
+ return (xhr.onprogress = function () {
242
+ clearTimeout(chunkedTimeout);
243
+ if (
244
+ xhr.getResponseHeader("Content-Type") ===
245
+ "application/json; charset=utf-8"
246
+ ) {
247
+ chunked = false; // not chunked, we are sending json back
248
+ } else {
249
+ position = handle_progress(xhr.responseText, position);
250
+ }
251
+ });
252
+ },
253
+ },
254
+ xhr: function () {
255
+ var xhr = jQuery.ajaxSettings.xhr();
256
+ if (!chunked) {
257
+ return xhr;
258
+ }
259
+ this.messageBus.onProgressListener(xhr);
260
+ return xhr;
261
+ },
262
+ success: function (messages) {
263
+ if (!chunked) {
264
+ // we may have requested text so jQuery will not parse
265
+ if (typeof messages === "string") {
266
+ messages = JSON.parse(messages);
267
+ }
268
+ gotData = reqSuccess(messages);
269
+ }
270
+ },
271
+ error: function (xhr, textStatus) {
272
+ if (xhr.status === 429) {
273
+ var tryAfter =
274
+ parseInt(
275
+ xhr.getResponseHeader && xhr.getResponseHeader("Retry-After")
276
+ ) || 0;
277
+ tryAfter = tryAfter || 0;
278
+ if (tryAfter < 15) {
279
+ tryAfter = 15;
280
+ }
281
+ rateLimitedSeconds = tryAfter;
282
+ rateLimited = true;
283
+ } else if (textStatus === "abort") {
284
+ aborted = true;
285
+ } else {
286
+ failCount += 1;
287
+ totalAjaxFailures += 1;
288
+ }
289
+ },
290
+ complete: function () {
291
+ ajaxInProgress = false;
292
+
293
+ var interval;
294
+ try {
295
+ if (rateLimited) {
296
+ interval = Math.max(me.minPollInterval, rateLimitedSeconds * 1000);
297
+ } else if (gotData || aborted) {
298
+ interval = me.minPollInterval;
299
+ } else {
300
+ interval = me.callbackInterval;
301
+ if (failCount > 2) {
302
+ interval = interval * failCount;
303
+ } else if (!shouldLongPoll()) {
304
+ interval = me.backgroundCallbackInterval;
305
+ }
306
+ if (interval > me.maxPollInterval) {
307
+ interval = me.maxPollInterval;
308
+ }
309
+
310
+ interval -= new Date() - lastAjax;
311
+
312
+ if (interval < 100) {
313
+ interval = 100;
314
+ }
315
+ }
316
+ } catch (e) {
317
+ if (console.log && e.message) {
318
+ console.log("MESSAGE BUS FAIL: " + e.message);
319
+ }
320
+ }
321
+
322
+ if (pollTimeout) {
323
+ clearTimeout(pollTimeout);
324
+ pollTimeout = null;
325
+ }
326
+
327
+ if (started) {
328
+ pollTimeout = setTimeout(function () {
329
+ pollTimeout = null;
330
+ poll();
331
+ }, interval);
332
+ }
333
+
334
+ me.longPoll = null;
335
+ },
336
+ });
337
+
338
+ return req;
339
+ };
340
+
341
+ me = {
342
+ /* shared between all tabs */
343
+ minHiddenPollInterval: 1500,
344
+ enableChunkedEncoding: true,
345
+ enableLongPolling: true,
346
+ callbackInterval: 15000,
347
+ backgroundCallbackInterval: 60000,
348
+ minPollInterval: 100,
349
+ maxPollInterval: 3 * 60 * 1000,
350
+ callbacks: callbacks,
351
+ clientId: clientId,
352
+ alwaysLongPoll: false,
353
+ shouldLongPollCallback: undefined,
354
+ baseUrl: baseUrl,
355
+ headers: {},
356
+ ajax: typeof jQuery !== "undefined" && jQuery.ajax,
357
+ diagnostics: function () {
358
+ console.log("Stopped: " + stopped + " Started: " + started);
359
+ console.log("Current callbacks");
360
+ console.log(callbacks);
361
+ console.log(
362
+ "Total ajax calls: " +
363
+ totalAjaxCalls +
364
+ " Recent failure count: " +
365
+ failCount +
366
+ " Total failures: " +
367
+ totalAjaxFailures
368
+ );
369
+ console.log(
370
+ "Last ajax call: " + (new Date() - lastAjax) / 1000 + " seconds ago"
371
+ );
372
+ },
373
+
374
+ pause: function () {
375
+ paused = true;
376
+ },
377
+
378
+ resume: function () {
379
+ paused = false;
380
+ processMessages(later);
381
+ later = [];
382
+ },
383
+
384
+ stop: function () {
385
+ stopped = true;
386
+ started = false;
387
+ if (delayPollTimeout) {
388
+ clearTimeout(delayPollTimeout);
389
+ delayPollTimeout = null;
390
+ }
391
+ if (pollTimeout) {
392
+ clearTimeout(pollTimeout);
393
+ pollTimeout = null;
394
+ }
395
+ if (me.longPoll) {
396
+ me.longPoll.abort();
397
+ }
398
+ if (me.onVisibilityChange) {
399
+ document.removeEventListener("visibilitychange", me.onVisibilityChange);
400
+ me.onVisibilityChange = null;
401
+ }
402
+ },
403
+
404
+ // Start polling
405
+ start: function () {
406
+ if (started) return;
407
+ started = true;
408
+ stopped = false;
409
+
410
+ var poll = function () {
411
+ if (stopped) {
412
+ return;
413
+ }
414
+
415
+ if (callbacks.length === 0 || hiddenTabShouldWait()) {
416
+ if (!delayPollTimeout) {
417
+ delayPollTimeout = setTimeout(function () {
418
+ delayPollTimeout = null;
419
+ poll();
420
+ }, parseInt(500 + Math.random() * 500));
421
+ }
422
+ return;
423
+ }
424
+
425
+ var data = {};
426
+ for (var i = 0; i < callbacks.length; i++) {
427
+ data[callbacks[i].channel] = callbacks[i].last_id;
428
+ }
429
+
430
+ // could possibly already be started
431
+ // notice the delay timeout above
432
+ if (!me.longPoll) {
433
+ me.longPoll = longPoller(poll, data);
434
+ }
435
+ };
436
+
437
+ // monitor visibility, issue a new long poll when the page shows
438
+ if (document.addEventListener && "hidden" in document) {
439
+ me.onVisibilityChange = function () {
440
+ if (
441
+ !document.hidden &&
442
+ !me.longPoll &&
443
+ (pollTimeout || delayPollTimeout)
444
+ ) {
445
+ clearTimeout(pollTimeout);
446
+ clearTimeout(delayPollTimeout);
447
+
448
+ delayPollTimeout = null;
449
+ pollTimeout = null;
450
+ poll();
451
+ }
452
+ };
453
+
454
+ document.addEventListener("visibilitychange", me.onVisibilityChange);
455
+ }
456
+
457
+ poll();
458
+ },
459
+
460
+ status: function () {
461
+ if (paused) {
462
+ return "paused";
463
+ } else if (started) {
464
+ return "started";
465
+ } else if (stopped) {
466
+ return "stopped";
467
+ } else {
468
+ throw "Cannot determine current status";
469
+ }
470
+ },
471
+
472
+ // Subscribe to a channel
473
+ // if lastId is 0 or larger, it will recieve messages AFTER that id
474
+ // if lastId is negative it will perform lookbehind
475
+ // -1 will subscribe to all new messages
476
+ // -2 will recieve last message + all new messages
477
+ // -3 will recieve last 2 messages + all new messages
478
+ // if undefined will default to -1
479
+ subscribe: function (channel, func, lastId) {
480
+ if (!started && !stopped) {
481
+ me.start();
482
+ }
483
+
484
+ if (lastId === null || typeof lastId === "undefined") {
485
+ lastId = -1;
486
+ } else if (typeof lastId !== "number") {
487
+ throw (
488
+ "lastId has type " + typeof lastId + " but a number was expected."
489
+ );
490
+ }
491
+
492
+ if (typeof channel !== "string") {
493
+ throw "Channel name must be a string!";
494
+ }
495
+
496
+ callbacks.push({
497
+ channel: channel,
498
+ func: func,
499
+ last_id: lastId,
500
+ });
501
+ if (me.longPoll) {
502
+ me.longPoll.abort();
503
+ }
504
+
505
+ return func;
506
+ },
507
+
508
+ // Unsubscribe from a channel
509
+ unsubscribe: function (channel, func) {
510
+ // TODO allow for globbing in the middle of a channel name
511
+ // like /something/*/something
512
+ // at the moment we only support globbing /something/*
513
+ var glob = false;
514
+ if (channel.indexOf("*", channel.length - 1) !== -1) {
515
+ channel = channel.substr(0, channel.length - 1);
516
+ glob = true;
517
+ }
518
+
519
+ var removed = false;
520
+
521
+ for (var i = callbacks.length - 1; i >= 0; i--) {
522
+ var callback = callbacks[i];
523
+ var keep;
524
+
525
+ if (glob) {
526
+ keep = callback.channel.substr(0, channel.length) !== channel;
527
+ } else {
528
+ keep = callback.channel !== channel;
529
+ }
530
+
531
+ if (!keep && func && callback.func !== func) {
532
+ keep = true;
533
+ }
534
+
535
+ if (!keep) {
536
+ callbacks.splice(i, 1);
537
+ removed = true;
538
+ }
539
+ }
540
+
541
+ if (removed && me.longPoll) {
542
+ me.longPoll.abort();
543
+ }
544
+
545
+ return removed;
546
+ },
547
+ };
548
+ return me;
549
+ });