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.

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