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