message_bus 0.9.3.2 → 0.9.4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of message_bus might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG +8 -0
- data/Guardfile +1 -1
- data/README.md +17 -0
- data/assets/message-bus.js +1 -0
- data/examples/minimal/Gemfile +3 -0
- data/examples/minimal/config.ru +7 -0
- data/lib/message_bus.rb +96 -29
- data/lib/message_bus/client.rb +5 -4
- data/lib/message_bus/connection_manager.rb +3 -2
- data/lib/message_bus/rack/diagnostics.rb +6 -5
- data/lib/message_bus/rack/middleware.rb +33 -22
- data/lib/message_bus/reliable_pub_sub.rb +7 -1
- data/lib/message_bus/version.rb +1 -1
- data/spec/lib/connection_manager_spec.rb +2 -1
- data/spec/lib/fake_async_middleware.rb +31 -22
- data/spec/lib/message_bus_spec.rb +60 -0
- data/spec/lib/middleware_spec.rb +99 -45
- data/vendor/assets/javascripts/message-bus.js +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bd00357fe6b7e4663c46c3c34c74a5d8529433c5
|
4
|
+
data.tar.gz: df3641a9d63749c6035ecd0b45d656bd53361e4d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c08c802702acc7c30db0c3ebfb1c0949644553b7fc6a63330a8f260d414a350e94c2fb40143bd3956a068eda1a8e764e3fc3f858568bedb5075d25d82392185d
|
7
|
+
data.tar.gz: 41c4d7584df49be995a21495841d63d65998aab5582c5ec6987fc620f86b995cc632e505fa641558c15568d8e6545075d075f4eec1c6501ad21fc4563220dc99
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
13-01-2014
|
2
|
+
- Version 0.9.4
|
3
|
+
- Added support for /global/ channel to publish messages across a multisite
|
4
|
+
- Cleaned up test harness so it uses local bus as opposed to global
|
5
|
+
- Fix bug where we could subscribe to a channel but miss starting messages
|
6
|
+
- Added method for destroying a local MessageBus instance
|
7
|
+
- ensure_reactor could say the reactor is running, but it was not, on first call
|
8
|
+
|
1
9
|
06-12-2013
|
2
10
|
- Version 0.9.3.2
|
3
11
|
- Fix permissions in gem
|
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -60,6 +60,23 @@ end
|
|
60
60
|
|
61
61
|
```
|
62
62
|
|
63
|
+
### Multisite support
|
64
|
+
|
65
|
+
MessageBus can be used in an environment that hosts multiple sites by multiplexing channels. To use this mode
|
66
|
+
|
67
|
+
```
|
68
|
+
# define a site_id lookup method
|
69
|
+
MessageBus.site_id_lookup do
|
70
|
+
some_method_that_returns_site_id_string
|
71
|
+
end
|
72
|
+
|
73
|
+
# you may post messages just to this site
|
74
|
+
MessageBus.publish "/channel", "some message"
|
75
|
+
|
76
|
+
# you may publish messages to ALL sites using the /global/ prefix
|
77
|
+
MessageBus.publish "/global/channel", "will go to all sites"
|
78
|
+
```
|
79
|
+
|
63
80
|
JavaScript can listen on any channel (and receive notification via polling or long polling):
|
64
81
|
|
65
82
|
```html
|
data/assets/message-bus.js
CHANGED
@@ -119,6 +119,7 @@ window.MessageBus = (function() {
|
|
119
119
|
},
|
120
120
|
success: function(messages) {
|
121
121
|
failCount = 0;
|
122
|
+
if (messages === null) return; // server unexpectedly closed connection
|
122
123
|
$.each(messages,function(_,message) {
|
123
124
|
gotData = true;
|
124
125
|
$.each(callbacks, function(_,callback) {
|
data/lib/message_bus.rb
CHANGED
@@ -12,6 +12,7 @@ require "message_bus/message_handler"
|
|
12
12
|
require "message_bus/diagnostics"
|
13
13
|
require "message_bus/rack/middleware"
|
14
14
|
require "message_bus/rack/diagnostics"
|
15
|
+
require "monitor.rb"
|
15
16
|
|
16
17
|
# we still need to take care of the logger
|
17
18
|
if defined?(::Rails)
|
@@ -19,8 +20,20 @@ if defined?(::Rails)
|
|
19
20
|
end
|
20
21
|
|
21
22
|
module MessageBus; end
|
23
|
+
class MessageBus::InvalidMessage < Exception; end
|
24
|
+
class MessageBus::BusDestroyed < Exception; end
|
25
|
+
|
22
26
|
module MessageBus::Implementation
|
23
27
|
|
28
|
+
# Like Mutex but safe for recursive calls
|
29
|
+
class Synchronizer
|
30
|
+
include MonitorMixin
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@mutex = Synchronizer.new
|
35
|
+
end
|
36
|
+
|
24
37
|
def cache_assets=(val)
|
25
38
|
@cache_assets = val
|
26
39
|
end
|
@@ -164,7 +177,10 @@ module MessageBus::Implementation
|
|
164
177
|
end
|
165
178
|
|
166
179
|
def reliable_pub_sub
|
167
|
-
@
|
180
|
+
@mutex.synchronize do
|
181
|
+
return nil if @destroyed
|
182
|
+
@reliable_pub_sub ||= MessageBus::ReliablePubSub.new redis_config
|
183
|
+
end
|
168
184
|
end
|
169
185
|
|
170
186
|
def enable_diagnostics
|
@@ -173,6 +189,9 @@ module MessageBus::Implementation
|
|
173
189
|
|
174
190
|
def publish(channel, data, opts = nil)
|
175
191
|
return if @off
|
192
|
+
@mutex.synchronize do
|
193
|
+
raise ::MessageBus::BusDestroyed if @destroyed
|
194
|
+
end
|
176
195
|
|
177
196
|
user_ids = nil
|
178
197
|
group_ids = nil
|
@@ -181,6 +200,8 @@ module MessageBus::Implementation
|
|
181
200
|
group_ids = opts[:group_ids]
|
182
201
|
end
|
183
202
|
|
203
|
+
raise ::MessageBus::InvalidMessage if (user_ids || group_ids) && global?(channel)
|
204
|
+
|
184
205
|
encoded_data = JSON.dump({
|
185
206
|
data: data,
|
186
207
|
user_ids: user_ids,
|
@@ -202,7 +223,7 @@ module MessageBus::Implementation
|
|
202
223
|
|
203
224
|
# encode channel name to include site
|
204
225
|
def encode_channel_name(channel)
|
205
|
-
if site_id_lookup
|
226
|
+
if site_id_lookup && !global?(channel)
|
206
227
|
raise ArgumentError.new channel if channel.include? ENCODE_SITE_TOKEN
|
207
228
|
"#{channel}#{ENCODE_SITE_TOKEN}#{site_id_lookup.call}"
|
208
229
|
else
|
@@ -229,7 +250,7 @@ module MessageBus::Implementation
|
|
229
250
|
|
230
251
|
# subscribe only on current site
|
231
252
|
def local_subscribe(channel=nil, &blk)
|
232
|
-
site_id = site_id_lookup.call if site_id_lookup
|
253
|
+
site_id = site_id_lookup.call if site_id_lookup && ! global?(channel)
|
233
254
|
subscribe_impl(channel, site_id, &blk)
|
234
255
|
end
|
235
256
|
|
@@ -238,7 +259,7 @@ module MessageBus::Implementation
|
|
238
259
|
if channel
|
239
260
|
reliable_pub_sub.backlog(encode_channel_name(channel), last_id)
|
240
261
|
else
|
241
|
-
reliable_pub_sub.global_backlog(
|
262
|
+
reliable_pub_sub.global_backlog(last_id)
|
242
263
|
end
|
243
264
|
|
244
265
|
old.each{ |m|
|
@@ -247,14 +268,26 @@ module MessageBus::Implementation
|
|
247
268
|
old
|
248
269
|
end
|
249
270
|
|
250
|
-
|
251
271
|
def last_id(channel)
|
252
272
|
reliable_pub_sub.last_id(encode_channel_name(channel))
|
253
273
|
end
|
254
274
|
|
275
|
+
def last_message(channel)
|
276
|
+
if last_id = last_id(channel)
|
277
|
+
messages = backlog(channel, last_id-1)
|
278
|
+
if messages
|
279
|
+
messages[0]
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
255
283
|
|
256
284
|
def destroy
|
257
|
-
|
285
|
+
@mutex.synchronize do
|
286
|
+
@subscriptions ||= {}
|
287
|
+
reliable_pub_sub.global_unsubscribe
|
288
|
+
@destroyed = true
|
289
|
+
end
|
290
|
+
@subscriber_thread.join if @subscriber_thread
|
258
291
|
end
|
259
292
|
|
260
293
|
def after_fork
|
@@ -262,8 +295,21 @@ module MessageBus::Implementation
|
|
262
295
|
ensure_subscriber_thread
|
263
296
|
end
|
264
297
|
|
298
|
+
def listening?
|
299
|
+
@subscriber_thread && @subscriber_thread.alive?
|
300
|
+
end
|
301
|
+
|
302
|
+
# will reset all keys
|
303
|
+
def reset!
|
304
|
+
reliable_pub_sub.reset!
|
305
|
+
end
|
306
|
+
|
265
307
|
protected
|
266
308
|
|
309
|
+
def global?(channel)
|
310
|
+
channel && channel.start_with?('/global/'.freeze)
|
311
|
+
end
|
312
|
+
|
267
313
|
def decode_message!(msg)
|
268
314
|
channel, site_id = decode_channel_name(msg.channel)
|
269
315
|
msg.channel = channel
|
@@ -275,15 +321,27 @@ module MessageBus::Implementation
|
|
275
321
|
end
|
276
322
|
|
277
323
|
def subscribe_impl(channel, site_id, &blk)
|
324
|
+
|
325
|
+
raise MessageBus::BusDestroyed if @destroyed
|
326
|
+
|
278
327
|
@subscriptions ||= {}
|
279
328
|
@subscriptions[site_id] ||= {}
|
280
329
|
@subscriptions[site_id][channel] ||= []
|
281
330
|
@subscriptions[site_id][channel] << blk
|
282
331
|
ensure_subscriber_thread
|
332
|
+
|
333
|
+
attempts = 100
|
334
|
+
while attempts > 0 && !reliable_pub_sub.subscribed
|
335
|
+
sleep 0.001
|
336
|
+
attempts-=1
|
337
|
+
end
|
338
|
+
|
339
|
+
raise MessageBus::BusDestroyed if @destroyed
|
283
340
|
blk
|
284
341
|
end
|
285
342
|
|
286
343
|
def unsubscribe_impl(channel, site_id, &blk)
|
344
|
+
|
287
345
|
@mutex.synchronize do
|
288
346
|
if blk
|
289
347
|
@subscriptions[site_id][channel].delete blk
|
@@ -291,42 +349,50 @@ module MessageBus::Implementation
|
|
291
349
|
@subscriptions[site_id][channel] = []
|
292
350
|
end
|
293
351
|
end
|
352
|
+
|
294
353
|
end
|
295
354
|
|
296
355
|
|
297
356
|
def ensure_subscriber_thread
|
298
|
-
@mutex ||= Mutex.new
|
299
357
|
@mutex.synchronize do
|
300
|
-
return if @subscriber_thread && @subscriber_thread.alive?
|
301
|
-
@subscriber_thread =
|
302
|
-
|
303
|
-
|
304
|
-
decode_message!(msg)
|
358
|
+
return if (@subscriber_thread && @subscriber_thread.alive?) || @destroyed
|
359
|
+
@subscriber_thread = new_subscriber_thread
|
360
|
+
end
|
361
|
+
end
|
305
362
|
|
306
|
-
|
307
|
-
|
308
|
-
|
363
|
+
def new_subscriber_thread
|
364
|
+
Thread.new do
|
365
|
+
global_subscribe_thread unless @destroyed
|
366
|
+
end
|
367
|
+
end
|
309
368
|
|
310
|
-
|
311
|
-
|
369
|
+
def global_subscribe_thread
|
370
|
+
reliable_pub_sub.global_subscribe do |msg|
|
371
|
+
begin
|
372
|
+
decode_message!(msg)
|
373
|
+
globals, locals, local_globals, global_globals = nil
|
312
374
|
|
313
|
-
|
314
|
-
|
375
|
+
@mutex.synchronize do
|
376
|
+
raise MessageBus::BusDestroyed if @destroyed
|
377
|
+
globals = @subscriptions[nil]
|
378
|
+
locals = @subscriptions[msg.site_id] if msg.site_id
|
315
379
|
|
316
|
-
|
317
|
-
|
318
|
-
c.call msg
|
319
|
-
rescue => e
|
320
|
-
MessageBus.logger.warn "failed to deliver message, skipping #{msg.inspect}\n ex: #{e} backtrace: #{e.backtrace}"
|
321
|
-
end
|
322
|
-
end
|
323
|
-
end
|
380
|
+
global_globals = globals[nil] if globals
|
381
|
+
local_globals = locals[nil] if locals
|
324
382
|
|
383
|
+
globals = globals[msg.channel] if globals
|
384
|
+
locals = locals[msg.channel] if locals
|
385
|
+
end
|
386
|
+
|
387
|
+
multi_each(globals,locals, global_globals, local_globals) do |c|
|
388
|
+
begin
|
389
|
+
c.call msg
|
325
390
|
rescue => e
|
326
|
-
MessageBus.logger.warn "failed to
|
391
|
+
MessageBus.logger.warn "failed to deliver message, skipping #{msg.inspect}\n ex: #{e} backtrace: #{e.backtrace}"
|
327
392
|
end
|
328
|
-
|
329
393
|
end
|
394
|
+
rescue => e
|
395
|
+
MessageBus.logger.warn "failed to process message #{msg.inspect}\n ex: #{e} backtrace: #{e.backtrace}"
|
330
396
|
end
|
331
397
|
end
|
332
398
|
end
|
@@ -341,6 +407,7 @@ end
|
|
341
407
|
|
342
408
|
module MessageBus
|
343
409
|
extend MessageBus::Implementation
|
410
|
+
initialize
|
344
411
|
end
|
345
412
|
|
346
413
|
# allows for multiple buses per app
|
data/lib/message_bus/client.rb
CHANGED
@@ -6,6 +6,7 @@ class MessageBus::Client
|
|
6
6
|
self.group_ids = opts[:group_ids] || []
|
7
7
|
self.site_id = opts[:site_id]
|
8
8
|
self.connect_time = Time.now
|
9
|
+
@bus = opts[:message_bus] || MessageBus
|
9
10
|
@subscriptions = {}
|
10
11
|
end
|
11
12
|
|
@@ -33,7 +34,7 @@ class MessageBus::Client
|
|
33
34
|
|
34
35
|
def subscribe(channel, last_seen_id)
|
35
36
|
last_seen_id = nil if last_seen_id == ""
|
36
|
-
last_seen_id ||=
|
37
|
+
last_seen_id ||= @bus.last_id(channel)
|
37
38
|
@subscriptions[channel] = last_seen_id.to_i
|
38
39
|
end
|
39
40
|
|
@@ -61,7 +62,7 @@ class MessageBus::Client
|
|
61
62
|
end
|
62
63
|
|
63
64
|
def filter(msg)
|
64
|
-
filter =
|
65
|
+
filter = @bus.client_filter(msg.channel)
|
65
66
|
|
66
67
|
if filter
|
67
68
|
filter.call(self.user_id, msg)
|
@@ -74,7 +75,7 @@ class MessageBus::Client
|
|
74
75
|
r = []
|
75
76
|
@subscriptions.each do |k,v|
|
76
77
|
next if v.to_i < 0
|
77
|
-
messages =
|
78
|
+
messages = @bus.backlog(k,v)
|
78
79
|
messages.each do |msg|
|
79
80
|
r << msg if allowed?(msg)
|
80
81
|
end
|
@@ -84,7 +85,7 @@ class MessageBus::Client
|
|
84
85
|
@subscriptions.each do |k,v|
|
85
86
|
if v.to_i == -1
|
86
87
|
status_message ||= {}
|
87
|
-
status_message[k] =
|
88
|
+
status_message[k] = @bus.last_id(k)
|
88
89
|
end
|
89
90
|
end
|
90
91
|
r << MessageBus::Message.new(-1, -1, '/__status', status_message) if status_message
|
@@ -2,9 +2,10 @@ require 'json' unless defined? ::JSON
|
|
2
2
|
|
3
3
|
class MessageBus::ConnectionManager
|
4
4
|
|
5
|
-
def initialize
|
5
|
+
def initialize(bus = nil)
|
6
6
|
@clients = {}
|
7
7
|
@subscriptions = {}
|
8
|
+
@bus = bus || MessageBus
|
8
9
|
end
|
9
10
|
|
10
11
|
def notify_clients(msg)
|
@@ -14,7 +15,7 @@ class MessageBus::ConnectionManager
|
|
14
15
|
|
15
16
|
return unless subscription
|
16
17
|
|
17
|
-
around_filter =
|
18
|
+
around_filter = @bus.around_client_batch(msg.channel)
|
18
19
|
|
19
20
|
work = lambda do
|
20
21
|
subscription.each do |client_id|
|
@@ -3,10 +3,11 @@ module MessageBus::Rack; end
|
|
3
3
|
class MessageBus::Rack::Diagnostics
|
4
4
|
def initialize(app, config = {})
|
5
5
|
@app = app
|
6
|
+
@bus = config[:message_bus] || MessageBus
|
6
7
|
end
|
7
8
|
|
8
9
|
def js_asset(name)
|
9
|
-
return generate_script_tag(name) unless
|
10
|
+
return generate_script_tag(name) unless @bus.cache_assets
|
10
11
|
@@asset_cache ||= {}
|
11
12
|
@@asset_cache[name] ||= generate_script_tag(name)
|
12
13
|
@@asset_cache[name]
|
@@ -65,21 +66,21 @@ HTML
|
|
65
66
|
|
66
67
|
route = env['PATH_INFO'].split('/message-bus/_diagnostics')[1]
|
67
68
|
|
68
|
-
if
|
69
|
+
if @bus.is_admin_lookup.nil? || !@bus.is_admin_lookup.call(env)
|
69
70
|
return [403, {}, ['not allowed']]
|
70
71
|
end
|
71
72
|
|
72
73
|
return index unless route
|
73
74
|
|
74
75
|
if route == '/discover'
|
75
|
-
user_id =
|
76
|
-
|
76
|
+
user_id = @bus.user_id_lookup.call(env)
|
77
|
+
@bus.publish('/_diagnostics/discover', user_id: user_id)
|
77
78
|
return [200, {}, ['ok']]
|
78
79
|
end
|
79
80
|
|
80
81
|
if route =~ /^\/hup\//
|
81
82
|
hostname, pid = route.split('/hup/')[1].split('/')
|
82
|
-
|
83
|
+
@bus.publish('/_diagnostics/hup', {hostname: hostname, pid: pid.to_i})
|
83
84
|
return [200, {}, ['ok']]
|
84
85
|
end
|
85
86
|
|
@@ -3,19 +3,19 @@ module MessageBus::Rack; end
|
|
3
3
|
|
4
4
|
class MessageBus::Rack::Middleware
|
5
5
|
|
6
|
-
def
|
6
|
+
def start_listener
|
7
7
|
unless @started_listener
|
8
8
|
|
9
9
|
require 'eventmachine'
|
10
10
|
require 'message_bus/em_ext'
|
11
11
|
|
12
|
-
|
12
|
+
@subscription = @bus.subscribe do |msg|
|
13
13
|
if EM.reactor_running?
|
14
14
|
EM.next_tick do
|
15
15
|
begin
|
16
|
-
|
16
|
+
@connection_manager.notify_clients(msg) if @connection_manager
|
17
17
|
rescue
|
18
|
-
|
18
|
+
@bus.logger.warn "Failed to notify clients: #{$!} #{$!.backtrace}"
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
@@ -26,8 +26,16 @@ class MessageBus::Rack::Middleware
|
|
26
26
|
|
27
27
|
def initialize(app, config = {})
|
28
28
|
@app = app
|
29
|
-
|
30
|
-
|
29
|
+
@bus = config[:message_bus] || MessageBus
|
30
|
+
@connection_manager = MessageBus::ConnectionManager.new(@bus)
|
31
|
+
self.start_listener
|
32
|
+
end
|
33
|
+
|
34
|
+
def stop_listener
|
35
|
+
if @subscription
|
36
|
+
@bus.unsubscribe(&@subscription)
|
37
|
+
@started_listener = false
|
38
|
+
end
|
31
39
|
end
|
32
40
|
|
33
41
|
def self.backlog_to_json(backlog)
|
@@ -47,25 +55,25 @@ class MessageBus::Rack::Middleware
|
|
47
55
|
return @app.call(env) unless env['PATH_INFO'] =~ /^\/message-bus\//
|
48
56
|
|
49
57
|
# special debug/test route
|
50
|
-
if
|
58
|
+
if @bus.allow_broadcast? && env['PATH_INFO'] == '/message-bus/broadcast'.freeze
|
51
59
|
parsed = Rack::Request.new(env)
|
52
|
-
|
60
|
+
@bus.publish parsed["channel".freeze], parsed["data".freeze]
|
53
61
|
return [200,{"Content-Type".freeze => "text/html".freeze},["sent"]]
|
54
62
|
end
|
55
63
|
|
56
64
|
if env['PATH_INFO'].start_with? '/message-bus/_diagnostics'.freeze
|
57
|
-
diags = MessageBus::Rack::Diagnostics.new(@app)
|
65
|
+
diags = MessageBus::Rack::Diagnostics.new(@app, message_bus: @bus)
|
58
66
|
return diags.call(env)
|
59
67
|
end
|
60
68
|
|
61
69
|
client_id = env['PATH_INFO'].split("/")[2]
|
62
70
|
return [404, {}, ["not found"]] unless client_id
|
63
71
|
|
64
|
-
user_id =
|
65
|
-
group_ids =
|
66
|
-
site_id =
|
72
|
+
user_id = @bus.user_id_lookup.call(env) if @bus.user_id_lookup
|
73
|
+
group_ids = @bus.group_ids_lookup.call(env) if @bus.group_ids_lookup
|
74
|
+
site_id = @bus.site_id_lookup.call(env) if @bus.site_id_lookup
|
67
75
|
|
68
|
-
client = MessageBus::Client.new(client_id: client_id, user_id: user_id, site_id: site_id, group_ids: group_ids)
|
76
|
+
client = MessageBus::Client.new(message_bus: @bus, client_id: client_id, user_id: user_id, site_id: site_id, group_ids: group_ids)
|
69
77
|
|
70
78
|
request = Rack::Request.new(env)
|
71
79
|
request.POST.each do |k,v|
|
@@ -79,22 +87,20 @@ class MessageBus::Rack::Middleware
|
|
79
87
|
|
80
88
|
ensure_reactor
|
81
89
|
|
82
|
-
long_polling =
|
90
|
+
long_polling = @bus.long_polling_enabled? &&
|
83
91
|
env['QUERY_STRING'] !~ /dlp=t/.freeze &&
|
84
92
|
EM.reactor_running? &&
|
85
|
-
|
93
|
+
@connection_manager.client_count < @bus.max_active_clients
|
86
94
|
|
87
|
-
#STDERR.puts "LONG POLLING lp enabled #{MessageBus.long_polling_enabled?}, reactor #{EM.reactor_running?} count: #{@@connection_manager.client_count} , active #{MessageBus.max_active_clients} #{long_polling}"
|
88
95
|
if backlog.length > 0
|
89
96
|
[200, headers, [self.class.backlog_to_json(backlog)] ]
|
90
|
-
elsif long_polling && env['rack.hijack'] &&
|
97
|
+
elsif long_polling && env['rack.hijack'] && @bus.rack_hijack_enabled?
|
91
98
|
io = env['rack.hijack'].call
|
92
99
|
client.io = io
|
93
100
|
|
94
101
|
add_client_with_timeout(client)
|
95
102
|
[418, {}, ["I'm a teapot, undefined in spec"]]
|
96
103
|
elsif long_polling && env['async.callback']
|
97
|
-
|
98
104
|
response = nil
|
99
105
|
# load extension if needed
|
100
106
|
begin
|
@@ -122,19 +128,24 @@ class MessageBus::Rack::Middleware
|
|
122
128
|
# ensure reactor is running
|
123
129
|
if EM.reactor_pid != Process.pid
|
124
130
|
Thread.new { EM.run }
|
131
|
+
i = 100
|
132
|
+
while !EM.reactor_running? && i > 0
|
133
|
+
sleep 0.001
|
134
|
+
i -= 1
|
135
|
+
end
|
125
136
|
end
|
126
137
|
end
|
127
138
|
|
128
139
|
def add_client_with_timeout(client)
|
129
|
-
|
140
|
+
@connection_manager.add_client(client)
|
130
141
|
|
131
|
-
client.cleanup_timer = ::EM::Timer.new(
|
142
|
+
client.cleanup_timer = ::EM::Timer.new( @bus.long_polling_interval.to_f / 1000) {
|
132
143
|
begin
|
133
144
|
client.cleanup_timer = nil
|
134
145
|
client.ensure_closed!
|
135
|
-
|
146
|
+
@connection_manager.remove_client(client)
|
136
147
|
rescue
|
137
|
-
|
148
|
+
@bus.logger.warn "Failed to clean up client properly: #{$!} #{$!.backtrace}"
|
138
149
|
end
|
139
150
|
}
|
140
151
|
end
|
@@ -9,6 +9,7 @@ require 'redis'
|
|
9
9
|
|
10
10
|
|
11
11
|
class MessageBus::ReliablePubSub
|
12
|
+
attr_reader :subscribed
|
12
13
|
|
13
14
|
UNSUB_MESSAGE = "$$UNSUBSCRIBE"
|
14
15
|
|
@@ -212,7 +213,6 @@ class MessageBus::ReliablePubSub
|
|
212
213
|
end
|
213
214
|
|
214
215
|
def global_unsubscribe
|
215
|
-
# TODO mutex
|
216
216
|
if @redis_global
|
217
217
|
pub_redis.publish(redis_channel_name, UNSUB_MESSAGE)
|
218
218
|
@redis_global.disconnect
|
@@ -249,7 +249,13 @@ class MessageBus::ReliablePubSub
|
|
249
249
|
if highest_id
|
250
250
|
clear_backlog.call(&blk)
|
251
251
|
end
|
252
|
+
@subscribed = true
|
252
253
|
end
|
254
|
+
|
255
|
+
on.unsubscribe do
|
256
|
+
@subscribed = false
|
257
|
+
end
|
258
|
+
|
253
259
|
on.message do |c,m|
|
254
260
|
if m == UNSUB_MESSAGE
|
255
261
|
@redis_global.unsubscribe
|
data/lib/message_bus/version.rb
CHANGED
@@ -23,7 +23,8 @@ end
|
|
23
23
|
describe MessageBus::ConnectionManager do
|
24
24
|
|
25
25
|
before do
|
26
|
-
@
|
26
|
+
@bus = MessageBus
|
27
|
+
@manager = MessageBus::ConnectionManager.new(@bus)
|
27
28
|
@client = MessageBus::Client.new(client_id: "xyz", user_id: 1, site_id: 10)
|
28
29
|
@resp = FakeAsync.new
|
29
30
|
@client.async_response = @resp
|
@@ -1,38 +1,47 @@
|
|
1
1
|
require 'http/parser'
|
2
2
|
class FakeAsyncMiddleware
|
3
3
|
|
4
|
-
def
|
5
|
-
|
6
|
-
|
4
|
+
def initialize(app,config={})
|
5
|
+
@app = app
|
6
|
+
@bus = config[:message_bus] || MessageBus
|
7
|
+
@simulate_thin_async = false
|
8
|
+
@simulate_hijack = false
|
9
|
+
@in_async = false
|
7
10
|
end
|
8
11
|
|
9
|
-
def
|
10
|
-
|
11
|
-
@@simulate_hijack = true
|
12
|
+
def app
|
13
|
+
@app
|
12
14
|
end
|
13
15
|
|
14
|
-
def
|
15
|
-
|
16
|
+
def simulate_thin_async
|
17
|
+
@simulate_thin_async = true
|
18
|
+
@simulate_hijack = false
|
16
19
|
end
|
17
20
|
|
18
|
-
def
|
19
|
-
@
|
21
|
+
def simulate_hijack
|
22
|
+
@simulate_thin_async = false
|
23
|
+
@simulate_hijack = true
|
20
24
|
end
|
21
25
|
|
22
|
-
def
|
23
|
-
|
26
|
+
def in_async?
|
27
|
+
@in_async
|
24
28
|
end
|
25
29
|
|
26
|
-
|
27
|
-
|
30
|
+
|
31
|
+
def simulate_thin_async?
|
32
|
+
@simulate_thin_async && @bus.long_polling_enabled?
|
33
|
+
end
|
34
|
+
|
35
|
+
def simulate_hijack?
|
36
|
+
@simulate_hijack && @bus.long_polling_enabled?
|
28
37
|
end
|
29
38
|
|
30
39
|
def call(env)
|
31
|
-
if simulate_thin_async
|
40
|
+
if simulate_thin_async?
|
32
41
|
call_thin_async(env)
|
33
|
-
elsif simulate_hijack
|
42
|
+
elsif simulate_hijack?
|
34
43
|
call_rack_hijack(env)
|
35
|
-
else
|
44
|
+
else
|
36
45
|
@app.call(env)
|
37
46
|
end
|
38
47
|
end
|
@@ -64,12 +73,12 @@ class FakeAsyncMiddleware
|
|
64
73
|
env['rack.hijack_io'] = io
|
65
74
|
|
66
75
|
result = @app.call(env)
|
67
|
-
|
76
|
+
|
68
77
|
EM::Timer.new(1) { EM.stop }
|
69
78
|
|
70
79
|
defer = lambda {
|
71
80
|
if !io || !io.closed?
|
72
|
-
|
81
|
+
@in_async = true
|
73
82
|
EM.next_tick do
|
74
83
|
defer.call
|
75
84
|
end
|
@@ -88,7 +97,7 @@ class FakeAsyncMiddleware
|
|
88
97
|
end
|
89
98
|
}
|
90
99
|
|
91
|
-
|
100
|
+
@in_async = false
|
92
101
|
result || [500, {}, ['timeout']]
|
93
102
|
|
94
103
|
end
|
@@ -116,7 +125,7 @@ class FakeAsyncMiddleware
|
|
116
125
|
|
117
126
|
defer = lambda {
|
118
127
|
if !result
|
119
|
-
|
128
|
+
@in_async = true
|
120
129
|
EM.next_tick do
|
121
130
|
defer.call
|
122
131
|
end
|
@@ -127,7 +136,7 @@ class FakeAsyncMiddleware
|
|
127
136
|
defer.call
|
128
137
|
}
|
129
138
|
|
130
|
-
|
139
|
+
@in_async = false
|
131
140
|
result || [500, {}, ['timeout']]
|
132
141
|
end
|
133
142
|
end
|
@@ -14,6 +14,7 @@ describe MessageBus do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
after do
|
17
|
+
@bus.reset!
|
17
18
|
@bus.destroy
|
18
19
|
end
|
19
20
|
|
@@ -79,6 +80,65 @@ describe MessageBus do
|
|
79
80
|
r.map{|i| i.data}.to_a.should == ['foo', 'bar']
|
80
81
|
end
|
81
82
|
|
83
|
+
it "allows you to look up last_message" do
|
84
|
+
@bus.publish("/bob", "dylan")
|
85
|
+
@bus.publish("/bob", "marley")
|
86
|
+
@bus.last_message("/bob").data.should == "marley"
|
87
|
+
@bus.last_message("/nothing").should == nil
|
88
|
+
end
|
89
|
+
|
90
|
+
context "global subscriptions" do
|
91
|
+
before do
|
92
|
+
seq = 0
|
93
|
+
@bus.site_id_lookup do
|
94
|
+
(seq+=1).to_s
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it "can get last_message" do
|
99
|
+
@bus.publish("/global/test", "test")
|
100
|
+
@bus.last_message("/global/test").data.should == "test"
|
101
|
+
end
|
102
|
+
|
103
|
+
it "can subscribe globally" do
|
104
|
+
|
105
|
+
data = nil
|
106
|
+
@bus.subscribe do |message|
|
107
|
+
data = message.data
|
108
|
+
end
|
109
|
+
|
110
|
+
@bus.publish("/global/test", "test")
|
111
|
+
wait_for(1000){ data }
|
112
|
+
|
113
|
+
data.should == "test"
|
114
|
+
end
|
115
|
+
|
116
|
+
it "can subscribe to channel" do
|
117
|
+
|
118
|
+
data = nil
|
119
|
+
@bus.subscribe("/global/test") do |message|
|
120
|
+
data = message.data
|
121
|
+
end
|
122
|
+
|
123
|
+
@bus.publish("/global/test", "test")
|
124
|
+
wait_for(1000){ data }
|
125
|
+
|
126
|
+
data.should == "test"
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should exception if publishing restricted messages to user" do
|
130
|
+
lambda do
|
131
|
+
@bus.publish("/global/test", "test", user_ids: [1])
|
132
|
+
end.should raise_error(MessageBus::InvalidMessage)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should exception if publishing restricted messages to group" do
|
136
|
+
lambda do
|
137
|
+
@bus.publish("/global/test", "test", user_ids: [1])
|
138
|
+
end.should raise_error(MessageBus::InvalidMessage)
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
82
142
|
|
83
143
|
it "should support forking properly do" do
|
84
144
|
data = nil
|
data/spec/lib/middleware_spec.rb
CHANGED
@@ -8,25 +8,37 @@ describe MessageBus::Rack::Middleware do
|
|
8
8
|
include Rack::Test::Methods
|
9
9
|
|
10
10
|
before do
|
11
|
-
|
11
|
+
bus = @bus = MessageBus::Instance.new
|
12
|
+
@bus.long_polling_enabled = false
|
13
|
+
|
14
|
+
builder = Rack::Builder.new {
|
15
|
+
use FakeAsyncMiddleware, :message_bus => bus
|
16
|
+
use MessageBus::Rack::Middleware, :message_bus => bus
|
17
|
+
run lambda {|env| [500, {'Content-Type' => 'text/html'}, 'should not be called' ]}
|
18
|
+
}
|
19
|
+
|
20
|
+
@async_middleware = builder.to_app
|
21
|
+
@message_bus_middleware = @async_middleware.app
|
22
|
+
end
|
23
|
+
|
24
|
+
after do |x|
|
25
|
+
@message_bus_middleware.stop_listener
|
26
|
+
@bus.reset!
|
27
|
+
@bus.destroy
|
12
28
|
end
|
13
29
|
|
14
30
|
def app
|
15
|
-
@
|
16
|
-
use FakeAsyncMiddleware
|
17
|
-
use MessageBus::Rack::Middleware
|
18
|
-
run lambda {|env| [500, {'Content-Type' => 'text/html'}, 'should not be called' ]}
|
19
|
-
}.to_app
|
31
|
+
@async_middleware
|
20
32
|
end
|
21
33
|
|
22
34
|
shared_examples "long polling" do
|
23
35
|
before do
|
24
|
-
|
36
|
+
@bus.long_polling_enabled = true
|
25
37
|
end
|
26
38
|
|
27
39
|
it "should respond right away if dlp=t" do
|
28
40
|
post "/message-bus/ABC?dlp=t", '/foo1' => 0
|
29
|
-
|
41
|
+
@async_middleware.in_async?.should == false
|
30
42
|
last_response.should be_ok
|
31
43
|
end
|
32
44
|
|
@@ -36,14 +48,16 @@ describe MessageBus::Rack::Middleware do
|
|
36
48
|
parsed = JSON.parse(last_response.body)
|
37
49
|
parsed.length.should == 1
|
38
50
|
parsed[0]["channel"].should == "/__status"
|
39
|
-
parsed[0]["data"]["/foo"].should ==
|
51
|
+
parsed[0]["data"]["/foo"].should == @bus.last_id("/foo")
|
40
52
|
end
|
41
53
|
|
42
54
|
it "should respond to long polls when data is available" do
|
55
|
+
middleware = @async_middleware
|
56
|
+
bus = @bus
|
43
57
|
|
44
58
|
Thread.new do
|
45
|
-
wait_for(2000) {
|
46
|
-
|
59
|
+
wait_for(2000) {middleware.in_async?}
|
60
|
+
bus.publish "/foo", "םוֹלשָׁ"
|
47
61
|
end
|
48
62
|
|
49
63
|
post "/message-bus/ABC", '/foo' => nil
|
@@ -56,21 +70,24 @@ describe MessageBus::Rack::Middleware do
|
|
56
70
|
|
57
71
|
it "should timeout within its alloted slot" do
|
58
72
|
begin
|
59
|
-
|
73
|
+
@bus.long_polling_interval = 10
|
60
74
|
s = Time.now.to_f * 1000
|
61
75
|
post "/message-bus/ABC", '/foo' => nil
|
62
76
|
(Time.now.to_f * 1000 - s).should < 30
|
63
77
|
ensure
|
64
|
-
|
78
|
+
@bus.long_polling_interval = 5000
|
65
79
|
end
|
66
80
|
end
|
67
81
|
|
68
82
|
it "should support batch filtering" do
|
69
|
-
|
83
|
+
bus = @bus
|
84
|
+
async_middleware = @async_middleware
|
85
|
+
|
86
|
+
bus.user_id_lookup do |env|
|
70
87
|
1
|
71
88
|
end
|
72
89
|
|
73
|
-
|
90
|
+
bus.around_client_batch("/demo") do |message, user_ids, callback|
|
74
91
|
begin
|
75
92
|
Thread.current["test"] = user_ids
|
76
93
|
callback.call
|
@@ -81,18 +98,18 @@ describe MessageBus::Rack::Middleware do
|
|
81
98
|
|
82
99
|
test = nil
|
83
100
|
|
84
|
-
|
101
|
+
bus.client_filter("/demo") do |user_id, message|
|
85
102
|
test = Thread.current["test"]
|
86
103
|
message
|
87
104
|
end
|
88
105
|
|
89
106
|
client_id = "ABCD"
|
90
107
|
|
91
|
-
id =
|
108
|
+
id = bus.publish("/demo", "test")
|
92
109
|
|
93
110
|
Thread.new do
|
94
|
-
wait_for(2000) {
|
95
|
-
|
111
|
+
wait_for(2000) { async_middleware.in_async? }
|
112
|
+
bus.publish "/demo", "test"
|
96
113
|
end
|
97
114
|
|
98
115
|
post "/message-bus/#{client_id}", {
|
@@ -105,15 +122,15 @@ describe MessageBus::Rack::Middleware do
|
|
105
122
|
|
106
123
|
describe "thin async" do
|
107
124
|
before do
|
108
|
-
|
125
|
+
@async_middleware.simulate_thin_async
|
109
126
|
end
|
110
127
|
it_behaves_like "long polling"
|
111
128
|
end
|
112
129
|
|
113
130
|
describe "hijack" do
|
114
131
|
before do
|
115
|
-
|
116
|
-
|
132
|
+
@async_middleware.simulate_hijack
|
133
|
+
@bus.rack_hijack_enabled = true
|
117
134
|
end
|
118
135
|
it_behaves_like "long polling"
|
119
136
|
end
|
@@ -126,13 +143,13 @@ describe MessageBus::Rack::Middleware do
|
|
126
143
|
end
|
127
144
|
|
128
145
|
it "should get a 200 with html for an authorized user" do
|
129
|
-
|
146
|
+
@bus.stub(:is_admin_lookup).and_return(lambda{|env| true })
|
130
147
|
get "/message-bus/_diagnostics"
|
131
148
|
last_response.status.should == 200
|
132
149
|
end
|
133
150
|
|
134
151
|
it "should get the script it asks for" do
|
135
|
-
|
152
|
+
@bus.stub(:is_admin_lookup).and_return(lambda{|env| true })
|
136
153
|
get "/message-bus/_diagnostics/assets/message-bus.js"
|
137
154
|
last_response.status.should == 200
|
138
155
|
last_response.content_type.should == "text/javascript;"
|
@@ -142,7 +159,7 @@ describe MessageBus::Rack::Middleware do
|
|
142
159
|
|
143
160
|
describe "polling" do
|
144
161
|
before do
|
145
|
-
|
162
|
+
@bus.long_polling_enabled = false
|
146
163
|
end
|
147
164
|
|
148
165
|
it "should respond with a 200 to a subscribe" do
|
@@ -158,7 +175,7 @@ describe MessageBus::Rack::Middleware do
|
|
158
175
|
|
159
176
|
it "should correctly understand that -1 means stuff from now onwards" do
|
160
177
|
|
161
|
-
|
178
|
+
@bus.publish('foo', 'bar')
|
162
179
|
|
163
180
|
post "/message-bus/ABCD", {
|
164
181
|
'/foo' => -1
|
@@ -167,15 +184,15 @@ describe MessageBus::Rack::Middleware do
|
|
167
184
|
parsed = JSON.parse(last_response.body)
|
168
185
|
parsed.length.should == 1
|
169
186
|
parsed[0]["channel"].should == "/__status"
|
170
|
-
parsed[0]["data"]["/foo"].should
|
187
|
+
parsed[0]["data"]["/foo"].should ==@bus.last_id("/foo")
|
171
188
|
|
172
189
|
end
|
173
190
|
|
174
191
|
it "should respond with the data if messages exist in the backlog" do
|
175
|
-
id
|
192
|
+
id =@bus.last_id('/foo')
|
176
193
|
|
177
|
-
|
178
|
-
|
194
|
+
@bus.publish("/foo", "barbs")
|
195
|
+
@bus.publish("/foo", "borbs")
|
179
196
|
|
180
197
|
client_id = "ABCD"
|
181
198
|
post "/message-bus/#{client_id}", {
|
@@ -189,9 +206,46 @@ describe MessageBus::Rack::Middleware do
|
|
189
206
|
parsed[1]["data"].should == "borbs"
|
190
207
|
end
|
191
208
|
|
209
|
+
it "should have no cross talk" do
|
210
|
+
|
211
|
+
seq = 0
|
212
|
+
@bus.site_id_lookup do
|
213
|
+
(seq+=1).to_s
|
214
|
+
end
|
215
|
+
|
216
|
+
# published on channel 1
|
217
|
+
msg = @bus.publish("/foo", "test")
|
218
|
+
|
219
|
+
# subscribed on channel 2
|
220
|
+
post "/message-bus/ABCD", {
|
221
|
+
'/foo' => (msg-1)
|
222
|
+
}
|
223
|
+
|
224
|
+
parsed = JSON.parse(last_response.body)
|
225
|
+
parsed.length.should == 0
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
it "should have global cross talk" do
|
230
|
+
|
231
|
+
seq = 0
|
232
|
+
@bus.site_id_lookup do
|
233
|
+
(seq+=1).to_s
|
234
|
+
end
|
235
|
+
|
236
|
+
msg = @bus.publish("/global/foo", "test")
|
237
|
+
|
238
|
+
post "/message-bus/ABCD", {
|
239
|
+
'/global/foo' => (msg-1)
|
240
|
+
}
|
241
|
+
|
242
|
+
parsed = JSON.parse(last_response.body)
|
243
|
+
parsed.length.should == 1
|
244
|
+
end
|
245
|
+
|
192
246
|
it "should not get consumed messages" do
|
193
|
-
|
194
|
-
id
|
247
|
+
@bus.publish("/foo", "barbs")
|
248
|
+
id =@bus.last_id('/foo')
|
195
249
|
|
196
250
|
client_id = "ABCD"
|
197
251
|
post "/message-bus/#{client_id}", {
|
@@ -203,8 +257,8 @@ describe MessageBus::Rack::Middleware do
|
|
203
257
|
end
|
204
258
|
|
205
259
|
it "should filter by user correctly" do
|
206
|
-
id
|
207
|
-
|
260
|
+
id =@bus.publish("/foo", "test", user_ids: [1])
|
261
|
+
@bus.user_id_lookup do |env|
|
208
262
|
0
|
209
263
|
end
|
210
264
|
|
@@ -216,7 +270,7 @@ describe MessageBus::Rack::Middleware do
|
|
216
270
|
parsed = JSON.parse(last_response.body)
|
217
271
|
parsed.length.should == 0
|
218
272
|
|
219
|
-
|
273
|
+
@bus.user_id_lookup do |env|
|
220
274
|
1
|
221
275
|
end
|
222
276
|
|
@@ -230,14 +284,14 @@ describe MessageBus::Rack::Middleware do
|
|
230
284
|
|
231
285
|
|
232
286
|
it "should filter by client_filter correctly" do
|
233
|
-
id =
|
234
|
-
|
287
|
+
id = @bus.publish("/filter", "test")
|
288
|
+
uid = 0
|
235
289
|
|
236
|
-
|
237
|
-
|
290
|
+
@bus.user_id_lookup do |env|
|
291
|
+
uid
|
238
292
|
end
|
239
293
|
|
240
|
-
|
294
|
+
@bus.client_filter("/filter") do |user_id, message|
|
241
295
|
if user_id == 0
|
242
296
|
message = message.dup
|
243
297
|
message.data += "_filter"
|
@@ -256,7 +310,7 @@ describe MessageBus::Rack::Middleware do
|
|
256
310
|
parsed = JSON.parse(last_response.body)
|
257
311
|
parsed[0]['data'].should == "test_filter"
|
258
312
|
|
259
|
-
|
313
|
+
uid = 1
|
260
314
|
|
261
315
|
post "/message-bus/#{client_id}", {
|
262
316
|
'/filter' => id - 1
|
@@ -266,7 +320,7 @@ describe MessageBus::Rack::Middleware do
|
|
266
320
|
parsed.length.should == 1
|
267
321
|
parsed[0]["data"].should == "test"
|
268
322
|
|
269
|
-
|
323
|
+
uid = 2
|
270
324
|
|
271
325
|
post "/message-bus/#{client_id}", {
|
272
326
|
'/filter' => id - 1
|
@@ -277,8 +331,8 @@ describe MessageBus::Rack::Middleware do
|
|
277
331
|
end
|
278
332
|
|
279
333
|
it "should filter by group correctly" do
|
280
|
-
id
|
281
|
-
|
334
|
+
id =@bus.publish("/foo", "test", group_ids: [3,4,5])
|
335
|
+
@bus.group_ids_lookup do |env|
|
282
336
|
[0,1,2]
|
283
337
|
end
|
284
338
|
|
@@ -290,7 +344,7 @@ describe MessageBus::Rack::Middleware do
|
|
290
344
|
parsed = JSON.parse(last_response.body)
|
291
345
|
parsed.length.should == 0
|
292
346
|
|
293
|
-
|
347
|
+
@bus.group_ids_lookup do |env|
|
294
348
|
[1,7,4,100]
|
295
349
|
end
|
296
350
|
|
@@ -119,6 +119,7 @@ window.MessageBus = (function() {
|
|
119
119
|
},
|
120
120
|
success: function(messages) {
|
121
121
|
failCount = 0;
|
122
|
+
if (messages === null) return; // server unexpectedly closed connection
|
122
123
|
$.each(messages,function(_,message) {
|
123
124
|
gotData = true;
|
124
125
|
$.each(callbacks, function(_,callback) {
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: message_bus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Saffron
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-01-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -80,6 +80,8 @@ files:
|
|
80
80
|
- examples/bench/unicorn.conf.rb
|
81
81
|
- examples/chat/chat.rb
|
82
82
|
- examples/chat/config.ru
|
83
|
+
- examples/minimal/Gemfile
|
84
|
+
- examples/minimal/config.ru
|
83
85
|
- lib/message_bus.rb
|
84
86
|
- lib/message_bus/client.rb
|
85
87
|
- lib/message_bus/connection_manager.rb
|