message_bus 0.9.3.2 → 0.9.4
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.
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
|