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
@@ -1,5 +1,5 @@
1
1
  class MessageBus::Client
2
- attr_accessor :client_id, :user_id, :group_ids, :connect_time, :subscribed_sets, :site_id, :cleanup_timer, :async_response
2
+ attr_accessor :client_id, :user_id, :group_ids, :connect_time, :subscribed_sets, :site_id, :cleanup_timer, :async_response, :io
3
3
  def initialize(opts)
4
4
  self.client_id = opts[:client_id]
5
5
  self.user_id = opts[:user_id]
@@ -9,8 +9,21 @@ class MessageBus::Client
9
9
  @subscriptions = {}
10
10
  end
11
11
 
12
+ def in_async?
13
+ @async_response || @io
14
+ end
15
+
16
+ def ensure_closed!
17
+ return unless in_async?
18
+ write_and_close "[]"
19
+ rescue
20
+ # we may have a dead socket, just nil the @io
21
+ @io = nil
22
+ @async_response = nil
23
+ end
24
+
12
25
  def close
13
- return unless @async_response
26
+ return unless in_async?
14
27
  write_and_close "[]"
15
28
  end
16
29
 
@@ -19,8 +32,9 @@ class MessageBus::Client
19
32
  end
20
33
 
21
34
  def subscribe(channel, last_seen_id)
35
+ last_seen_id = nil if last_seen_id == ""
22
36
  last_seen_id ||= MessageBus.last_id(channel)
23
- @subscriptions[channel] = last_seen_id
37
+ @subscriptions[channel] = last_seen_id.to_i
24
38
  end
25
39
 
26
40
  def subscriptions
@@ -46,6 +60,16 @@ class MessageBus::Client
46
60
  )
47
61
  end
48
62
 
63
+ def filter(msg)
64
+ filter = MessageBus.client_filter(msg.channel)
65
+
66
+ if filter
67
+ filter.call(self.user_id, msg)
68
+ else
69
+ msg
70
+ end
71
+ end
72
+
49
73
  def backlog
50
74
  r = []
51
75
  @subscriptions.each do |k,v|
@@ -64,15 +88,29 @@ class MessageBus::Client
64
88
  end
65
89
  end
66
90
  r << MessageBus::Message.new(-1, -1, '/__status', status_message) if status_message
67
- r
91
+
92
+ r.map!{|msg| filter(msg)}.compact!
93
+ r || []
68
94
  end
69
95
 
70
96
  protected
71
97
 
72
98
  def write_and_close(data)
73
- @async_response << data
74
- @async_response.done
75
- @async_response = nil
99
+ if @io
100
+ @io.write("HTTP/1.1 200 OK\r\n")
101
+ @io.write("Content-Type: application/json; charset=utf-8\r\n")
102
+ @io.write("Cache-Control: must-revalidate, private, max-age=0\r\n")
103
+ @io.write("Content-Length: #{data.bytes.to_a.length}\r\n")
104
+ @io.write("Connection: close\r\n")
105
+ @io.write("\r\n")
106
+ @io.write(data)
107
+ @io.close
108
+ @io = nil
109
+ else
110
+ @async_response << data
111
+ @async_response.done
112
+ @async_response = nil
113
+ end
76
114
  end
77
115
 
78
116
  def messages_to_json(msgs)
@@ -14,14 +14,38 @@ class MessageBus::ConnectionManager
14
14
 
15
15
  return unless subscription
16
16
 
17
- subscription.each do |client_id|
18
- client = @clients[client_id]
19
- if client && client.allowed?(msg)
20
- client << msg
21
- # turns out you can delete from a set while itereating
22
- remove_client(client)
17
+ around_filter = MessageBus.around_client_batch(msg.channel)
18
+
19
+ work = lambda do
20
+ subscription.each do |client_id|
21
+ client = @clients[client_id]
22
+ if client && client.allowed?(msg)
23
+ if copy = client.filter(msg)
24
+ begin
25
+ client << copy
26
+ rescue
27
+ # pipe may be broken, move on
28
+ end
29
+ # turns out you can delete from a set while itereating
30
+ remove_client(client)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ if around_filter
37
+ user_ids = subscription.map do |s|
38
+ c = @clients[s]
39
+ c && c.user_id
40
+ end.compact
41
+
42
+ if user_ids && user_ids.length > 0
43
+ around_filter.call(msg, user_ids, work)
23
44
  end
45
+ else
46
+ work.call
24
47
  end
48
+
25
49
  rescue => e
26
50
  MessageBus.logger.error "notify clients crash #{e} : #{e.backtrace}"
27
51
  end
@@ -40,7 +64,7 @@ class MessageBus::ConnectionManager
40
64
  @subscriptions[c.site_id].each do |k, set|
41
65
  set.delete c.client_id
42
66
  end
43
- c.cleanup_timer.cancel
67
+ c.cleanup_timer.cancel if c.cleanup_timer
44
68
  end
45
69
 
46
70
  def lookup_client(client_id)
@@ -56,6 +80,10 @@ class MessageBus::ConnectionManager
56
80
  set << client.client_id
57
81
  end
58
82
 
83
+ def client_count
84
+ @clients.length
85
+ end
86
+
59
87
  def stats
60
88
  {
61
89
  client_count: @clients.length,
@@ -0,0 +1,5 @@
1
+ module EM
2
+ def self.reactor_pid
3
+ @reactor_pid
4
+ end
5
+ end
@@ -1,17 +1,22 @@
1
- # our little message bus, accepts long polling and web sockets
2
- require 'thin'
3
- require 'eventmachine'
4
-
1
+ # our little message bus, accepts long polling and polling
5
2
  module MessageBus::Rack; end
6
3
 
7
4
  class MessageBus::Rack::Middleware
8
5
 
9
6
  def self.start_listener
10
7
  unless @started_listener
8
+
9
+ require 'eventmachine'
10
+ require 'message_bus/em_ext'
11
+
11
12
  MessageBus.subscribe do |msg|
12
13
  if EM.reactor_running?
13
14
  EM.next_tick do
14
- @@connection_manager.notify_clients(msg) if @@connection_manager
15
+ begin
16
+ @@connection_manager.notify_clients(msg) if @@connection_manager
17
+ rescue
18
+ MessageBus.logger.warn "Failed to notify clients: #{$!} #{$!.backtrace}"
19
+ end
15
20
  end
16
21
  end
17
22
  end
@@ -39,16 +44,16 @@ class MessageBus::Rack::Middleware
39
44
 
40
45
  def call(env)
41
46
 
42
- return @app.call(env) unless env['PATH_INFO'] =~ /^\/message-bus/
47
+ return @app.call(env) unless env['PATH_INFO'] =~ /^\/message-bus\//
43
48
 
44
49
  # special debug/test route
45
- if ::MessageBus.allow_broadcast? && env['PATH_INFO'] == '/message-bus/broadcast'
50
+ if ::MessageBus.allow_broadcast? && env['PATH_INFO'] == '/message-bus/broadcast'.freeze
46
51
  parsed = Rack::Request.new(env)
47
- ::MessageBus.publish parsed["channel"], parsed["data"]
48
- return [200,{"Content-Type" => "text/html"},["sent"]]
52
+ ::MessageBus.publish parsed["channel".freeze], parsed["data".freeze]
53
+ return [200,{"Content-Type".freeze => "text/html".freeze},["sent"]]
49
54
  end
50
55
 
51
- if env['PATH_INFO'].start_with? '/message-bus/_diagnostics'
56
+ if env['PATH_INFO'].start_with? '/message-bus/_diagnostics'.freeze
52
57
  diags = MessageBus::Rack::Diagnostics.new(@app)
53
58
  return diags.call(env)
54
59
  end
@@ -62,8 +67,6 @@ class MessageBus::Rack::Middleware
62
67
 
63
68
  client = MessageBus::Client.new(client_id: client_id, user_id: user_id, site_id: site_id, group_ids: group_ids)
64
69
 
65
- connection = env['em.connection']
66
-
67
70
  request = Rack::Request.new(env)
68
71
  request.POST.each do |k,v|
69
72
  client.subscribe(k, v)
@@ -74,98 +77,66 @@ class MessageBus::Rack::Middleware
74
77
  headers["Cache-Control"] = "must-revalidate, private, max-age=0"
75
78
  headers["Content-Type"] ="application/json; charset=utf-8"
76
79
 
80
+ ensure_reactor
81
+
82
+ long_polling = MessageBus.long_polling_enabled? &&
83
+ env['QUERY_STRING'] !~ /dlp=t/.freeze &&
84
+ EM.reactor_running? &&
85
+ @@connection_manager.client_count < MessageBus.max_active_clients
86
+
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}"
77
88
  if backlog.length > 0
78
89
  [200, headers, [self.class.backlog_to_json(backlog)] ]
79
- elsif MessageBus.long_polling_enabled? && env['QUERY_STRING'] !~ /dlp=t/ && EM.reactor_running?
80
- response = Thin::AsyncResponse.new(env)
81
- response.headers["Cache-Control"] = "must-revalidate, private, max-age=0"
82
- response.headers["Content-Type"] ="application/json; charset=utf-8"
90
+ elsif long_polling && env['rack.hijack'] && MessageBus.rack_hijack_enabled?
91
+ io = env['rack.hijack'].call
92
+ client.io = io
93
+
94
+ add_client_with_timeout(client)
95
+ [418, {}, ["I'm a teapot, undefined in spec"]]
96
+ elsif long_polling && env['async.callback']
97
+
98
+ response = nil
99
+ # load extension if needed
100
+ begin
101
+ response = Thin::AsyncResponse.new(env)
102
+ rescue NameError
103
+ require 'message_bus/rack/thin_ext'
104
+ response = Thin::AsyncResponse.new(env)
105
+ end
106
+
107
+ response.headers["Cache-Control"] = "must-revalidate, private, max-age=0".freeze
108
+ response.headers["Content-Type"] ="application/json; charset=utf-8".freeze
83
109
  response.status = 200
84
- client.async_response = response
85
110
 
86
- @@connection_manager.add_client(client)
111
+ client.async_response = response
87
112
 
88
- client.cleanup_timer = ::EM::Timer.new(MessageBus.long_polling_interval.to_f / 1000) {
89
- client.close
90
- @@connection_manager.remove_client(client)
91
- }
113
+ add_client_with_timeout(client)
92
114
 
93
115
  throw :async
94
116
  else
95
117
  [200, headers, ["[]"]]
96
118
  end
97
-
98
119
  end
99
- end
100
120
 
101
- # there is also another in cramp this is from https://github.com/macournoyer/thin_async/blob/master/lib/thin/async.rb
102
- module Thin
103
- unless defined?(DeferrableBody)
104
- # Based on version from James Tucker <raggi@rubyforge.org>
105
- class DeferrableBody
106
- include ::EM::Deferrable
107
-
108
- def initialize
109
- @queue = []
110
- end
111
-
112
- def call(body)
113
- @queue << body
114
- schedule_dequeue
115
- end
116
-
117
- def each(&blk)
118
- @body_callback = blk
119
- schedule_dequeue
120
- end
121
-
122
- private
123
- def schedule_dequeue
124
- return unless @body_callback
125
- ::EM.next_tick do
126
- next unless body = @queue.shift
127
- body.each do |chunk|
128
- @body_callback.call(chunk)
129
- end
130
- schedule_dequeue unless @queue.empty?
131
- end
132
- end
121
+ def ensure_reactor
122
+ # ensure reactor is running
123
+ if EM.reactor_pid != Process.pid
124
+ Thread.new { EM.run }
133
125
  end
134
126
  end
135
127
 
136
- # Response whos body is sent asynchronously.
137
- class AsyncResponse
138
- include Rack::Response::Helpers
139
-
140
- attr_reader :headers, :callback, :closed
141
- attr_accessor :status
142
-
143
- def initialize(env, status=200, headers={})
144
- @callback = env['async.callback']
145
- @body = DeferrableBody.new
146
- @status = status
147
- @headers = headers
148
- @headers_sent = false
149
- end
150
-
151
- def send_headers
152
- return if @headers_sent
153
- @callback.call [@status, @headers, @body]
154
- @headers_sent = true
155
- end
156
-
157
- def write(body)
158
- send_headers
159
- @body.call(body.respond_to?(:each) ? body : [body])
160
- end
161
- alias :<< :write
162
-
163
- # Tell Thin the response is complete and the connection can be closed.
164
- def done
165
- @closed = true
166
- send_headers
167
- ::EM.next_tick { @body.succeed }
168
- end
128
+ def add_client_with_timeout(client)
129
+ @@connection_manager.add_client(client)
169
130
 
131
+ client.cleanup_timer = ::EM::Timer.new(MessageBus.long_polling_interval.to_f / 1000) {
132
+ begin
133
+ client.cleanup_timer = nil
134
+ client.ensure_closed!
135
+ @@connection_manager.remove_client(client)
136
+ rescue
137
+ MessageBus.logger.warn "Failed to clean up client properly: #{$!} #{$!.backtrace}"
138
+ end
139
+ }
170
140
  end
171
141
  end
142
+
@@ -0,0 +1,71 @@
1
+ # there is also another in cramp this is from https://github.com/macournoyer/thin_async/blob/master/lib/thin/async.rb
2
+ module Thin
3
+ unless defined?(DeferrableBody)
4
+ # Based on version from James Tucker <raggi@rubyforge.org>
5
+ class DeferrableBody
6
+ include ::EM::Deferrable
7
+
8
+ def initialize
9
+ @queue = []
10
+ end
11
+
12
+ def call(body)
13
+ @queue << body
14
+ schedule_dequeue
15
+ end
16
+
17
+ def each(&blk)
18
+ @body_callback = blk
19
+ schedule_dequeue
20
+ end
21
+
22
+ private
23
+ def schedule_dequeue
24
+ return unless @body_callback
25
+ ::EM.next_tick do
26
+ next unless body = @queue.shift
27
+ body.each do |chunk|
28
+ @body_callback.call(chunk)
29
+ end
30
+ schedule_dequeue unless @queue.empty?
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ # Response whos body is sent asynchronously.
37
+ class AsyncResponse
38
+ include Rack::Response::Helpers
39
+
40
+ attr_reader :headers, :callback, :closed
41
+ attr_accessor :status
42
+
43
+ def initialize(env, status=200, headers={})
44
+ @callback = env['async.callback']
45
+ @body = DeferrableBody.new
46
+ @status = status
47
+ @headers = headers
48
+ @headers_sent = false
49
+ end
50
+
51
+ def send_headers
52
+ return if @headers_sent
53
+ @callback.call [@status, @headers, @body]
54
+ @headers_sent = true
55
+ end
56
+
57
+ def write(body)
58
+ send_headers
59
+ @body.call(body.respond_to?(:each) ? body : [body])
60
+ end
61
+ alias :<< :write
62
+
63
+ # Tell Thin the response is complete and the connection can be closed.
64
+ def done
65
+ @closed = true
66
+ send_headers
67
+ ::EM.next_tick { @body.succeed }
68
+ end
69
+
70
+ end
71
+ end
@@ -1,9 +1,12 @@
1
1
  module MessageBus; module Rails; end; end
2
2
 
3
+ # rails engine for asset pipeline
4
+ class MessageBus::Rails::Engine < ::Rails::Engine; end
5
+
3
6
  class MessageBus::Rails::Railtie < ::Rails::Railtie
4
7
  initializer "message_bus.configure_init" do |app|
5
8
  MessageBus::MessageHandler.load_handlers("#{Rails.root}/app/message_handlers")
6
- app.middleware.use MessageBus::Rack::Middleware
9
+ app.middleware.insert_after(ActiveRecord::QueryCache, MessageBus::Rack::Middleware)
7
10
  MessageBus.logger = Rails.logger
8
11
  end
9
12
  end