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
@@ -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