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
data/lib/message_bus/client.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
@
|
74
|
-
|
75
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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,
|
@@ -1,17 +1,22 @@
|
|
1
|
-
# our little message bus, accepts long polling and
|
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
|
-
|
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
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
111
|
+
client.async_response = response
|
87
112
|
|
88
|
-
client
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
137
|
-
|
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.
|
9
|
+
app.middleware.insert_after(ActiveRecord::QueryCache, MessageBus::Rack::Middleware)
|
7
10
|
MessageBus.logger = Rails.logger
|
8
11
|
end
|
9
12
|
end
|