message_bus 0.0.2 → 0.9.3
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/.gitignore +18 -0
- data/.travis.yml +6 -0
- data/CHANGELOG +9 -0
- data/Gemfile +15 -0
- data/Guardfile +7 -0
- data/README.md +8 -0
- data/Rakefile +14 -0
- data/assets/application.handlebars +7 -0
- data/assets/application.js +79 -0
- data/assets/ember.js +26839 -0
- data/assets/handlebars.js +2201 -0
- data/assets/index.handlebars +25 -0
- data/assets/jquery-1.8.2.js +9440 -0
- data/assets/message-bus.js +247 -0
- data/examples/bench/ab.sample +1 -0
- data/examples/bench/config.ru +24 -0
- data/examples/bench/payload.post +1 -0
- data/examples/bench/unicorn.conf.rb +4 -0
- data/examples/chat/chat.rb +74 -0
- data/examples/chat/config.ru +2 -0
- data/lib/message_bus.rb +60 -5
- data/lib/message_bus/client.rb +45 -7
- data/lib/message_bus/connection_manager.rb +35 -7
- data/lib/message_bus/em_ext.rb +5 -0
- data/lib/message_bus/rack/middleware.rb +60 -89
- data/lib/message_bus/rack/thin_ext.rb +71 -0
- data/lib/message_bus/rails/railtie.rb +4 -1
- data/lib/message_bus/reliable_pub_sub.rb +22 -4
- data/lib/message_bus/version.rb +1 -1
- data/message_bus.gemspec +20 -0
- data/spec/lib/client_spec.rb +50 -0
- data/spec/lib/connection_manager_spec.rb +83 -0
- data/spec/lib/fake_async_middleware.rb +134 -0
- data/spec/lib/handlers/demo_message_handler.rb +5 -0
- data/spec/lib/message_bus_spec.rb +112 -0
- data/spec/lib/message_handler_spec.rb +39 -0
- data/spec/lib/middleware_spec.rb +306 -0
- data/spec/lib/multi_process_spec.rb +60 -0
- data/spec/lib/reliable_pub_sub_spec.rb +167 -0
- data/spec/spec_helper.rb +19 -0
- data/vendor/assets/javascripts/message-bus.js +247 -0
- metadata +55 -26
@@ -10,6 +10,8 @@ require 'redis'
|
|
10
10
|
|
11
11
|
class MessageBus::ReliablePubSub
|
12
12
|
|
13
|
+
UNSUB_MESSAGE = "$$UNSUBSCRIBE"
|
14
|
+
|
13
15
|
class NoMoreRetries < StandardError; end
|
14
16
|
class BackLogOutOfOrder < StandardError
|
15
17
|
attr_accessor :highest_id
|
@@ -57,6 +59,10 @@ class MessageBus::ReliablePubSub
|
|
57
59
|
::Redis.new(@redis_config)
|
58
60
|
end
|
59
61
|
|
62
|
+
def after_fork
|
63
|
+
pub_redis.client.reconnect
|
64
|
+
end
|
65
|
+
|
60
66
|
def redis_channel_name
|
61
67
|
db = @redis_config[:db] || 0
|
62
68
|
"discourse_#{db}"
|
@@ -126,9 +132,8 @@ class MessageBus::ReliablePubSub
|
|
126
132
|
end
|
127
133
|
|
128
134
|
def last_id(channel)
|
129
|
-
redis = pub_redis
|
130
135
|
backlog_id_key = backlog_id_key(channel)
|
131
|
-
|
136
|
+
pub_redis.get(backlog_id_key).to_i
|
132
137
|
end
|
133
138
|
|
134
139
|
def backlog(channel, last_id = nil)
|
@@ -206,6 +211,15 @@ class MessageBus::ReliablePubSub
|
|
206
211
|
highest_id
|
207
212
|
end
|
208
213
|
|
214
|
+
def global_unsubscribe
|
215
|
+
# TODO mutex
|
216
|
+
if @redis_global
|
217
|
+
pub_redis.publish(redis_channel_name, UNSUB_MESSAGE)
|
218
|
+
@redis_global.disconnect
|
219
|
+
@redis_global = nil
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
209
223
|
def global_subscribe(last_id=nil, &blk)
|
210
224
|
raise ArgumentError unless block_given?
|
211
225
|
highest_id = last_id
|
@@ -224,19 +238,23 @@ class MessageBus::ReliablePubSub
|
|
224
238
|
|
225
239
|
|
226
240
|
begin
|
227
|
-
|
241
|
+
@redis_global = new_redis_connection
|
228
242
|
|
229
243
|
if highest_id
|
230
244
|
clear_backlog.call(&blk)
|
231
245
|
end
|
232
246
|
|
233
|
-
|
247
|
+
@redis_global.subscribe(redis_channel_name) do |on|
|
234
248
|
on.subscribe do
|
235
249
|
if highest_id
|
236
250
|
clear_backlog.call(&blk)
|
237
251
|
end
|
238
252
|
end
|
239
253
|
on.message do |c,m|
|
254
|
+
if m == UNSUB_MESSAGE
|
255
|
+
@redis_global.unsubscribe
|
256
|
+
return
|
257
|
+
end
|
240
258
|
m = MessageBus::Message.decode m
|
241
259
|
|
242
260
|
# we have 2 options
|
data/lib/message_bus/version.rb
CHANGED
data/message_bus.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/message_bus/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Sam Saffron"]
|
6
|
+
gem.email = ["sam.saffron@gmail.com"]
|
7
|
+
gem.description = %q{A message bus for rack}
|
8
|
+
gem.summary = %q{}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "message_bus"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = MessageBus::VERSION
|
17
|
+
gem.add_runtime_dependency 'rack', '>= 1.1.3'
|
18
|
+
gem.add_runtime_dependency 'eventmachine'
|
19
|
+
gem.add_runtime_dependency 'redis'
|
20
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'message_bus'
|
3
|
+
|
4
|
+
describe MessageBus::Client do
|
5
|
+
|
6
|
+
describe "subscriptions" do
|
7
|
+
|
8
|
+
before do
|
9
|
+
@client = MessageBus::Client.new :client_id => 'abc'
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should provide a list of subscriptions" do
|
13
|
+
@client.subscribe('/hello', nil)
|
14
|
+
@client.subscriptions['/hello'].should_not be_nil
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should provide backlog for subscribed channel" do
|
18
|
+
@client.subscribe('/hello', nil)
|
19
|
+
MessageBus.publish '/hello', 'world'
|
20
|
+
log = @client.backlog
|
21
|
+
log.length.should == 1
|
22
|
+
log[0].channel.should == '/hello'
|
23
|
+
log[0].data.should == 'world'
|
24
|
+
end
|
25
|
+
|
26
|
+
context "targetted at group" do
|
27
|
+
before do
|
28
|
+
@message = MessageBus::Message.new(1,2,'/test', 'hello')
|
29
|
+
@message.group_ids = [1,2,3]
|
30
|
+
end
|
31
|
+
|
32
|
+
it "denies users that are not members of group" do
|
33
|
+
@client.group_ids = [77,0,10]
|
34
|
+
@client.allowed?(@message).should be_false
|
35
|
+
end
|
36
|
+
|
37
|
+
it "allows users that are members of group" do
|
38
|
+
@client.group_ids = [1,2,3]
|
39
|
+
@client.allowed?(@message).should be_true
|
40
|
+
end
|
41
|
+
|
42
|
+
it "allows all users if groups not set" do
|
43
|
+
@message.group_ids = nil
|
44
|
+
@client.group_ids = [77,0,10]
|
45
|
+
@client.allowed?(@message).should be_true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'message_bus'
|
3
|
+
|
4
|
+
class FakeAsync
|
5
|
+
|
6
|
+
attr_accessor :cleanup_timer
|
7
|
+
|
8
|
+
def <<(val)
|
9
|
+
@sent ||= ""
|
10
|
+
@sent << val
|
11
|
+
end
|
12
|
+
|
13
|
+
def sent; @sent; end
|
14
|
+
def done; @done = true; end
|
15
|
+
def done?; @done; end
|
16
|
+
end
|
17
|
+
|
18
|
+
class FakeTimer
|
19
|
+
attr_accessor :cancelled
|
20
|
+
def cancel; @cancelled = true; end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe MessageBus::ConnectionManager do
|
24
|
+
|
25
|
+
before do
|
26
|
+
@manager = MessageBus::ConnectionManager.new
|
27
|
+
@client = MessageBus::Client.new(client_id: "xyz", user_id: 1, site_id: 10)
|
28
|
+
@resp = FakeAsync.new
|
29
|
+
@client.async_response = @resp
|
30
|
+
@client.subscribe('test', -1)
|
31
|
+
@manager.add_client(@client)
|
32
|
+
@client.cleanup_timer = FakeTimer.new
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should cancel the timer after its responds" do
|
36
|
+
m = MessageBus::Message.new(1,1,"test","data")
|
37
|
+
m.site_id = 10
|
38
|
+
@manager.notify_clients(m)
|
39
|
+
@client.cleanup_timer.cancelled.should == true
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should be able to lookup an identical client" do
|
43
|
+
@manager.lookup_client(@client.client_id).should == @client
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should be subscribed to a channel" do
|
47
|
+
@manager.stats[:subscriptions][10]["test"].length == 1
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should not notify clients on incorrect site" do
|
51
|
+
m = MessageBus::Message.new(1,1,"test","data")
|
52
|
+
m.site_id = 9
|
53
|
+
@manager.notify_clients(m)
|
54
|
+
@resp.sent.should == nil
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should notify clients on the correct site" do
|
58
|
+
m = MessageBus::Message.new(1,1,"test","data")
|
59
|
+
m.site_id = 10
|
60
|
+
@manager.notify_clients(m)
|
61
|
+
@resp.sent.should_not == nil
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should strip site id and user id from the payload delivered" do
|
65
|
+
m = MessageBus::Message.new(1,1,"test","data")
|
66
|
+
m.user_ids = [1]
|
67
|
+
m.site_id = 10
|
68
|
+
@manager.notify_clients(m)
|
69
|
+
parsed = JSON.parse(@resp.sent)
|
70
|
+
parsed[0]["site_id"].should == nil
|
71
|
+
parsed[0]["user_id"].should == nil
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should not deliver unselected" do
|
75
|
+
m = MessageBus::Message.new(1,1,"test","data")
|
76
|
+
m.user_ids = [5]
|
77
|
+
m.site_id = 10
|
78
|
+
@manager.notify_clients(m)
|
79
|
+
@resp.sent.should == nil
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'http/parser'
|
2
|
+
class FakeAsyncMiddleware
|
3
|
+
|
4
|
+
def self.simulate_thin_async
|
5
|
+
@@simulate_thin_async = true
|
6
|
+
@@simulate_hijack = false
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.simulate_hijack
|
10
|
+
@@simulate_thin_async = false
|
11
|
+
@@simulate_hijack = true
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.in_async?
|
15
|
+
@@in_async if defined? @@in_async
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(app,config={})
|
19
|
+
@app = app
|
20
|
+
end
|
21
|
+
|
22
|
+
def simulate_thin_async
|
23
|
+
@@simulate_thin_async && MessageBus.long_polling_enabled?
|
24
|
+
end
|
25
|
+
|
26
|
+
def simulate_hijack
|
27
|
+
@@simulate_hijack && MessageBus.long_polling_enabled?
|
28
|
+
end
|
29
|
+
|
30
|
+
def call(env)
|
31
|
+
if simulate_thin_async
|
32
|
+
call_thin_async(env)
|
33
|
+
elsif simulate_hijack
|
34
|
+
call_rack_hijack(env)
|
35
|
+
else
|
36
|
+
@app.call(env)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def translate_io_result(io)
|
41
|
+
data = io.string
|
42
|
+
body = ""
|
43
|
+
|
44
|
+
parser = Http::Parser.new
|
45
|
+
parser.on_body = proc { |chunk| body << chunk }
|
46
|
+
parser << data
|
47
|
+
|
48
|
+
[parser.status_code, parser.headers, [body]]
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def call_rack_hijack(env)
|
53
|
+
# this is not to spec, the spec actually return, but here we will simply simulate and block
|
54
|
+
result = nil
|
55
|
+
hijacked = false
|
56
|
+
io = nil
|
57
|
+
|
58
|
+
EM.run {
|
59
|
+
env['rack.hijack'] = lambda {
|
60
|
+
hijacked = true
|
61
|
+
io = StringIO.new
|
62
|
+
}
|
63
|
+
|
64
|
+
env['rack.hijack_io'] = io
|
65
|
+
|
66
|
+
result = @app.call(env)
|
67
|
+
|
68
|
+
EM::Timer.new(1) { EM.stop }
|
69
|
+
|
70
|
+
defer = lambda {
|
71
|
+
if !io || !io.closed?
|
72
|
+
@@in_async = true
|
73
|
+
EM.next_tick do
|
74
|
+
defer.call
|
75
|
+
end
|
76
|
+
else
|
77
|
+
if io.closed?
|
78
|
+
result = translate_io_result(io)
|
79
|
+
end
|
80
|
+
EM.next_tick { EM.stop }
|
81
|
+
end
|
82
|
+
}
|
83
|
+
|
84
|
+
if !hijacked
|
85
|
+
EM.next_tick { EM.stop }
|
86
|
+
else
|
87
|
+
defer.call
|
88
|
+
end
|
89
|
+
}
|
90
|
+
|
91
|
+
@@in_async = false
|
92
|
+
result || [500, {}, ['timeout']]
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
def call_thin_async(env)
|
97
|
+
result = nil
|
98
|
+
EM.run {
|
99
|
+
env['async.callback'] = lambda { |r|
|
100
|
+
# more judo with deferrable body, at this point we just have headers
|
101
|
+
r[2].callback do
|
102
|
+
# even more judo cause rack test does not call each like the spec says
|
103
|
+
body = ""
|
104
|
+
r[2].each do |m|
|
105
|
+
body << m
|
106
|
+
end
|
107
|
+
r[2] = [body]
|
108
|
+
result = r
|
109
|
+
end
|
110
|
+
}
|
111
|
+
catch(:async) {
|
112
|
+
result = @app.call(env)
|
113
|
+
}
|
114
|
+
|
115
|
+
EM::Timer.new(1) { EM.stop }
|
116
|
+
|
117
|
+
defer = lambda {
|
118
|
+
if !result
|
119
|
+
@@in_async = true
|
120
|
+
EM.next_tick do
|
121
|
+
defer.call
|
122
|
+
end
|
123
|
+
else
|
124
|
+
EM.next_tick { EM.stop }
|
125
|
+
end
|
126
|
+
}
|
127
|
+
defer.call
|
128
|
+
}
|
129
|
+
|
130
|
+
@@in_async = false
|
131
|
+
result || [500, {}, ['timeout']]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'message_bus'
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
|
6
|
+
describe MessageBus do
|
7
|
+
|
8
|
+
before do
|
9
|
+
@bus = MessageBus::Instance.new
|
10
|
+
@bus.site_id_lookup do
|
11
|
+
"magic"
|
12
|
+
end
|
13
|
+
@bus.redis_config = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
after do
|
17
|
+
@bus.destroy
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should automatically decode hashed messages" do
|
21
|
+
data = nil
|
22
|
+
@bus.subscribe("/chuck") do |msg|
|
23
|
+
data = msg.data
|
24
|
+
end
|
25
|
+
@bus.publish("/chuck", {:norris => true})
|
26
|
+
wait_for(2000){ data }
|
27
|
+
|
28
|
+
data["norris"].should == true
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should get a message if it subscribes to it" do
|
32
|
+
user_ids,data,site_id,channel = nil
|
33
|
+
|
34
|
+
@bus.subscribe("/chuck") do |msg|
|
35
|
+
data = msg.data
|
36
|
+
site_id = msg.site_id
|
37
|
+
channel = msg.channel
|
38
|
+
user_ids = msg.user_ids
|
39
|
+
end
|
40
|
+
|
41
|
+
@bus.publish("/chuck", "norris", user_ids: [1,2,3])
|
42
|
+
|
43
|
+
wait_for(2000){data}
|
44
|
+
|
45
|
+
data.should == 'norris'
|
46
|
+
site_id.should == 'magic'
|
47
|
+
channel.should == '/chuck'
|
48
|
+
user_ids.should == [1,2,3]
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
it "should get global messages if it subscribes to them" do
|
54
|
+
data,site_id,channel = nil
|
55
|
+
|
56
|
+
@bus.subscribe do |msg|
|
57
|
+
data = msg.data
|
58
|
+
site_id = msg.site_id
|
59
|
+
channel = msg.channel
|
60
|
+
end
|
61
|
+
|
62
|
+
@bus.publish("/chuck", "norris")
|
63
|
+
|
64
|
+
wait_for(2000){data}
|
65
|
+
|
66
|
+
data.should == 'norris'
|
67
|
+
site_id.should == 'magic'
|
68
|
+
channel.should == '/chuck'
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should have the ability to grab the backlog messages in the correct order" do
|
73
|
+
id = @bus.publish("/chuck", "norris")
|
74
|
+
@bus.publish("/chuck", "foo")
|
75
|
+
@bus.publish("/chuck", "bar")
|
76
|
+
|
77
|
+
r = @bus.backlog("/chuck", id)
|
78
|
+
|
79
|
+
r.map{|i| i.data}.to_a.should == ['foo', 'bar']
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
it "should support forking properly do" do
|
84
|
+
data = nil
|
85
|
+
@bus.subscribe do |msg|
|
86
|
+
data = msg.data
|
87
|
+
end
|
88
|
+
|
89
|
+
@bus.publish("/hello", "world")
|
90
|
+
wait_for(2000){ data }
|
91
|
+
if child = Process.fork
|
92
|
+
wait_for(2000) { data == "ready" }
|
93
|
+
@bus.publish("/hello", "world1")
|
94
|
+
wait_for(2000) { data == "got it" }
|
95
|
+
data.should == "got it"
|
96
|
+
Process.wait(child)
|
97
|
+
else
|
98
|
+
@bus.after_fork
|
99
|
+
@bus.publish("/hello", "ready")
|
100
|
+
wait_for(2000) { data == "world1" }
|
101
|
+
if(data=="world1")
|
102
|
+
@bus.publish("/hello", "got it")
|
103
|
+
end
|
104
|
+
|
105
|
+
$stdout.reopen("/dev/null", "w")
|
106
|
+
$stderr.reopen("/dev/null", "w")
|
107
|
+
exit
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|