message_bus 3.3.7 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ });