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.

Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +18 -0
  3. data/.travis.yml +6 -0
  4. data/CHANGELOG +9 -0
  5. data/Gemfile +15 -0
  6. data/Guardfile +7 -0
  7. data/README.md +8 -0
  8. data/Rakefile +14 -0
  9. data/assets/application.handlebars +7 -0
  10. data/assets/application.js +79 -0
  11. data/assets/ember.js +26839 -0
  12. data/assets/handlebars.js +2201 -0
  13. data/assets/index.handlebars +25 -0
  14. data/assets/jquery-1.8.2.js +9440 -0
  15. data/assets/message-bus.js +247 -0
  16. data/examples/bench/ab.sample +1 -0
  17. data/examples/bench/config.ru +24 -0
  18. data/examples/bench/payload.post +1 -0
  19. data/examples/bench/unicorn.conf.rb +4 -0
  20. data/examples/chat/chat.rb +74 -0
  21. data/examples/chat/config.ru +2 -0
  22. data/lib/message_bus.rb +60 -5
  23. data/lib/message_bus/client.rb +45 -7
  24. data/lib/message_bus/connection_manager.rb +35 -7
  25. data/lib/message_bus/em_ext.rb +5 -0
  26. data/lib/message_bus/rack/middleware.rb +60 -89
  27. data/lib/message_bus/rack/thin_ext.rb +71 -0
  28. data/lib/message_bus/rails/railtie.rb +4 -1
  29. data/lib/message_bus/reliable_pub_sub.rb +22 -4
  30. data/lib/message_bus/version.rb +1 -1
  31. data/message_bus.gemspec +20 -0
  32. data/spec/lib/client_spec.rb +50 -0
  33. data/spec/lib/connection_manager_spec.rb +83 -0
  34. data/spec/lib/fake_async_middleware.rb +134 -0
  35. data/spec/lib/handlers/demo_message_handler.rb +5 -0
  36. data/spec/lib/message_bus_spec.rb +112 -0
  37. data/spec/lib/message_handler_spec.rb +39 -0
  38. data/spec/lib/middleware_spec.rb +306 -0
  39. data/spec/lib/multi_process_spec.rb +60 -0
  40. data/spec/lib/reliable_pub_sub_spec.rb +167 -0
  41. data/spec/spec_helper.rb +19 -0
  42. data/vendor/assets/javascripts/message-bus.js +247 -0
  43. 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
- redis.get(backlog_id_key).to_i
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
- redis = new_redis_connection
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
- redis.subscribe(redis_channel_name) do |on|
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
@@ -1,3 +1,3 @@
1
1
  module MessageBus
2
- VERSION = "0.0.2"
2
+ VERSION = "0.9.3"
3
3
  end
@@ -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,5 @@
1
+ class DemoMessageHandler < MessageBus::MessageHandler
2
+ handle "/dupe" do |m, uid|
3
+ "#{m}#{m}"
4
+ end
5
+ end
@@ -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