message_bus 0.0.2 → 0.9.3
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/.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
|