message_bus 1.1.1 → 2.0.0.beta.1
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/CHANGELOG +8 -0
- data/README.md +49 -8
- data/assets/message-bus.js +111 -25
- data/examples/chat/chat.rb +2 -0
- data/examples/chat/docker_container/chat.yml +22 -10
- data/examples/chat/docker_container/update_chat +2 -2
- data/lib/message_bus.rb +10 -13
- data/lib/message_bus/client.rb +105 -27
- data/lib/message_bus/connection_manager.rb +10 -27
- data/lib/message_bus/rack/middleware.rb +29 -8
- data/lib/message_bus/rails/railtie.rb +6 -3
- data/lib/message_bus/version.rb +1 -1
- data/spec/lib/fake_async_middleware.rb +7 -0
- data/spec/lib/{assets → message_bus/assets}/asset_encoding_spec.rb +1 -1
- data/spec/lib/{client_spec.rb → message_bus/client_spec.rb} +78 -0
- data/spec/lib/{connection_manager_spec.rb → message_bus/connection_manager_spec.rb} +0 -0
- data/spec/lib/{multi_process_spec.rb → message_bus/multi_process_spec.rb} +0 -0
- data/spec/lib/{middleware_spec.rb → message_bus/rack/middleware_spec.rb} +2 -89
- data/spec/lib/{redis → message_bus/redis}/reliable_pub_sub_spec.rb +4 -3
- data/spec/lib/{timer_thread_spec.rb → message_bus/timer_thread_spec.rb} +6 -22
- data/spec/spec_helper.rb +6 -1
- data/vendor/assets/javascripts/message-bus.js +111 -25
- metadata +19 -24
- data/lib/message_bus/message_handler.rb +0 -26
- data/spec/lib/handlers/demo_message_handler.rb +0 -5
- data/spec/lib/message_handler_spec.rb +0 -39
data/lib/message_bus/client.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
class MessageBus::Client
|
2
2
|
attr_accessor :client_id, :user_id, :group_ids, :connect_time,
|
3
3
|
:subscribed_sets, :site_id, :cleanup_timer,
|
4
|
-
:async_response, :io, :headers, :seq
|
4
|
+
:async_response, :io, :headers, :seq, :use_chunked
|
5
5
|
|
6
6
|
def initialize(opts)
|
7
7
|
self.client_id = opts[:client_id]
|
@@ -10,8 +10,14 @@ class MessageBus::Client
|
|
10
10
|
self.site_id = opts[:site_id]
|
11
11
|
self.seq = opts[:seq].to_i
|
12
12
|
self.connect_time = Time.now
|
13
|
+
@lock = Mutex.new
|
13
14
|
@bus = opts[:message_bus] || MessageBus
|
14
15
|
@subscriptions = {}
|
16
|
+
@chunks_sent = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def synchronize
|
20
|
+
@lock.synchronize { yield }
|
15
21
|
end
|
16
22
|
|
17
23
|
def cancel
|
@@ -27,9 +33,39 @@ class MessageBus::Client
|
|
27
33
|
@async_response || @io
|
28
34
|
end
|
29
35
|
|
36
|
+
def deliver_backlog(backlog)
|
37
|
+
if backlog.length > 0
|
38
|
+
if use_chunked
|
39
|
+
write_chunk(messages_to_json(backlog))
|
40
|
+
else
|
41
|
+
write_and_close messages_to_json(backlog)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def ensure_first_chunk_sent
|
47
|
+
if use_chunked && @chunks_sent == 0
|
48
|
+
write_chunk("[]".freeze)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
30
52
|
def ensure_closed!
|
31
53
|
return unless in_async?
|
32
|
-
|
54
|
+
if use_chunked
|
55
|
+
write_chunk("[]".freeze)
|
56
|
+
if @io
|
57
|
+
@io.write("0\r\n\r\n".freeze)
|
58
|
+
@io.close
|
59
|
+
@io = nil
|
60
|
+
end
|
61
|
+
if @async_response
|
62
|
+
@async_response << ("0\r\n\r\n".freeze)
|
63
|
+
@async_response.done
|
64
|
+
@async_response = nil
|
65
|
+
end
|
66
|
+
else
|
67
|
+
write_and_close "[]"
|
68
|
+
end
|
33
69
|
rescue
|
34
70
|
# we may have a dead socket, just nil the @io
|
35
71
|
@io = nil
|
@@ -37,12 +73,11 @@ class MessageBus::Client
|
|
37
73
|
end
|
38
74
|
|
39
75
|
def close
|
40
|
-
|
41
|
-
write_and_close "[]"
|
76
|
+
ensure_closed!
|
42
77
|
end
|
43
78
|
|
44
|
-
def closed
|
45
|
-
!@async_response
|
79
|
+
def closed?
|
80
|
+
!@async_response && !@io
|
46
81
|
end
|
47
82
|
|
48
83
|
def subscribe(channel, last_seen_id)
|
@@ -56,7 +91,12 @@ class MessageBus::Client
|
|
56
91
|
end
|
57
92
|
|
58
93
|
def <<(msg)
|
59
|
-
|
94
|
+
json = messages_to_json([msg])
|
95
|
+
if use_chunked
|
96
|
+
write_chunk json
|
97
|
+
else
|
98
|
+
write_and_close json
|
99
|
+
end
|
60
100
|
end
|
61
101
|
|
62
102
|
def subscriptions
|
@@ -75,16 +115,6 @@ class MessageBus::Client
|
|
75
115
|
)
|
76
116
|
end
|
77
117
|
|
78
|
-
def filter(msg)
|
79
|
-
filter = @bus.client_filter(msg.channel)
|
80
|
-
|
81
|
-
if filter
|
82
|
-
filter.call(self.user_id, msg)
|
83
|
-
else
|
84
|
-
msg
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
118
|
def backlog
|
89
119
|
r = []
|
90
120
|
@subscriptions.each do |k,v|
|
@@ -99,12 +129,11 @@ class MessageBus::Client
|
|
99
129
|
@subscriptions.each do |k,v|
|
100
130
|
if v.to_i == -1
|
101
131
|
status_message ||= {}
|
102
|
-
status_message[k] = @bus.last_id(k, site_id)
|
132
|
+
@subscriptions[k] = status_message[k] = @bus.last_id(k, site_id)
|
103
133
|
end
|
104
134
|
end
|
105
135
|
r << MessageBus::Message.new(-1, -1, '/__status', status_message) if status_message
|
106
136
|
|
107
|
-
r.map!{|msg| filter(msg)}.compact!
|
108
137
|
r || []
|
109
138
|
end
|
110
139
|
|
@@ -117,20 +146,69 @@ class MessageBus::Client
|
|
117
146
|
HTTP_11 = "HTTP/1.1 200 OK\r\n".freeze
|
118
147
|
CONTENT_LENGTH = "Content-Length: ".freeze
|
119
148
|
CONNECTION_CLOSE = "Connection: close\r\n".freeze
|
149
|
+
CHUNKED_ENCODING = "Transfer-Encoding: chunked\r\n".freeze
|
150
|
+
NO_SNIFF = "X-Content-Type-Options: nosniff\r\n".freeze
|
151
|
+
|
152
|
+
TYPE_TEXT = "Content-Type: text/plain; charset=utf-8\r\n".freeze
|
153
|
+
TYPE_JSON = "Content-Type: application/json; charset=utf-8\r\n".freeze
|
154
|
+
|
155
|
+
def write_headers
|
156
|
+
@io.write(HTTP_11)
|
157
|
+
@headers.each do |k,v|
|
158
|
+
next if k == "Content-Type"
|
159
|
+
@io.write(k)
|
160
|
+
@io.write(COLON_SPACE)
|
161
|
+
@io.write(v)
|
162
|
+
@io.write(NEWLINE)
|
163
|
+
end
|
164
|
+
@io.write(CONNECTION_CLOSE)
|
165
|
+
if use_chunked
|
166
|
+
@io.write(TYPE_TEXT)
|
167
|
+
else
|
168
|
+
@io.write(TYPE_JSON)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def write_chunk(data)
|
173
|
+
@bus.logger.debug "Delivering messages #{data} to client #{client_id} for user #{user_id} (chunked)"
|
174
|
+
if @io && !@wrote_headers
|
175
|
+
write_headers
|
176
|
+
@io.write(CHUNKED_ENCODING)
|
177
|
+
# this is required otherwise chrome will delay onprogress calls
|
178
|
+
@io.write(NO_SNIFF)
|
179
|
+
@io.write(NEWLINE)
|
180
|
+
@wrote_headers = true
|
181
|
+
end
|
182
|
+
|
183
|
+
# chunked encoding may be "re-chunked" by proxies, so add a seperator
|
184
|
+
postfix = NEWLINE + "|" + NEWLINE
|
185
|
+
data = data.gsub(postfix, NEWLINE + "||" + NEWLINE)
|
186
|
+
chunk_length = data.bytesize + postfix.bytesize
|
187
|
+
|
188
|
+
@chunks_sent += 1
|
189
|
+
|
190
|
+
if @async_response
|
191
|
+
@async_response << chunk_length.to_s(16)
|
192
|
+
@async_response << NEWLINE
|
193
|
+
@async_response << data
|
194
|
+
@async_response << postfix
|
195
|
+
@async_response << NEWLINE
|
196
|
+
else
|
197
|
+
@io.write(chunk_length.to_s(16))
|
198
|
+
@io.write(NEWLINE)
|
199
|
+
@io.write(data)
|
200
|
+
@io.write(postfix)
|
201
|
+
@io.write(NEWLINE)
|
202
|
+
end
|
203
|
+
end
|
120
204
|
|
121
205
|
def write_and_close(data)
|
206
|
+
@bus.logger.debug "Delivering messages #{data} to client #{client_id} for user #{user_id}"
|
122
207
|
if @io
|
123
|
-
|
124
|
-
@headers.each do |k,v|
|
125
|
-
@io.write(k)
|
126
|
-
@io.write(COLON_SPACE)
|
127
|
-
@io.write(v)
|
128
|
-
@io.write(NEWLINE)
|
129
|
-
end
|
208
|
+
write_headers
|
130
209
|
@io.write(CONTENT_LENGTH)
|
131
210
|
@io.write(data.bytes.to_a.length)
|
132
211
|
@io.write(NEWLINE)
|
133
|
-
@io.write(CONNECTION_CLOSE)
|
134
212
|
@io.write(NEWLINE)
|
135
213
|
@io.write(data)
|
136
214
|
@io.close
|
@@ -19,38 +19,21 @@ class MessageBus::ConnectionManager
|
|
19
19
|
|
20
20
|
return unless subscription
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
if copy = client.filter(msg)
|
29
|
-
begin
|
30
|
-
client << copy
|
31
|
-
rescue
|
32
|
-
# pipe may be broken, move on
|
33
|
-
end
|
34
|
-
# turns out you can delete from a set while itereating
|
35
|
-
remove_client(client)
|
22
|
+
subscription.each do |client_id|
|
23
|
+
client = @clients[client_id]
|
24
|
+
if client && client.allowed?(msg)
|
25
|
+
begin
|
26
|
+
client.synchronize do
|
27
|
+
client << msg
|
36
28
|
end
|
29
|
+
rescue
|
30
|
+
# pipe may be broken, move on
|
37
31
|
end
|
32
|
+
# turns out you can delete from a set while itereating
|
33
|
+
remove_client(client) if client.closed?
|
38
34
|
end
|
39
35
|
end
|
40
36
|
|
41
|
-
if around_filter
|
42
|
-
user_ids = subscription.map do |s|
|
43
|
-
c = @clients[s]
|
44
|
-
c && c.user_id
|
45
|
-
end.compact
|
46
|
-
|
47
|
-
if user_ids && user_ids.length > 0
|
48
|
-
around_filter.call(msg, user_ids, work)
|
49
|
-
end
|
50
|
-
else
|
51
|
-
work.call
|
52
|
-
end
|
53
|
-
|
54
37
|
rescue => e
|
55
38
|
MessageBus.logger.error "notify clients crash #{e} : #{e.backtrace}"
|
56
39
|
end
|
@@ -93,9 +93,7 @@ class MessageBus::Rack::Middleware
|
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
|
-
backlog = client.backlog
|
97
96
|
headers = {}
|
98
|
-
|
99
97
|
headers["Cache-Control"] = "must-revalidate, private, max-age=0"
|
100
98
|
headers["Content-Type"] = "application/json; charset=utf-8"
|
101
99
|
headers["Pragma"] = "no-cache"
|
@@ -115,14 +113,28 @@ class MessageBus::Rack::Middleware
|
|
115
113
|
env['QUERY_STRING'] !~ /dlp=t/.freeze &&
|
116
114
|
@connection_manager.client_count < @bus.max_active_clients
|
117
115
|
|
118
|
-
|
116
|
+
allow_chunked = env['HTTP_VERSION'.freeze] == 'HTTP/1.1'.freeze
|
117
|
+
allow_chunked &&= !env['HTTP_DONT_CHUNK'.freeze]
|
118
|
+
allow_chunked &&= @bus.chunked_encoding_enabled?
|
119
|
+
|
120
|
+
client.use_chunked = allow_chunked
|
121
|
+
|
122
|
+
backlog = client.backlog
|
123
|
+
|
124
|
+
if backlog.length > 0 && !allow_chunked
|
125
|
+
client.cancel
|
126
|
+
@bus.logger.debug "Delivering backlog #{backlog} to client #{client_id} for user #{user_id}"
|
119
127
|
[200, headers, [self.class.backlog_to_json(backlog)] ]
|
120
128
|
elsif long_polling && env['rack.hijack'] && @bus.rack_hijack_enabled?
|
121
129
|
io = env['rack.hijack'].call
|
130
|
+
# TODO disable client till deliver backlog is called
|
122
131
|
client.io = io
|
123
132
|
client.headers = headers
|
124
|
-
|
125
|
-
|
133
|
+
client.synchronize do
|
134
|
+
client.deliver_backlog(backlog)
|
135
|
+
add_client_with_timeout(client)
|
136
|
+
client.ensure_first_chunk_sent
|
137
|
+
end
|
126
138
|
[418, {}, ["I'm a teapot, undefined in spec"]]
|
127
139
|
elsif long_polling && env['async.callback']
|
128
140
|
response = nil
|
@@ -137,11 +149,20 @@ class MessageBus::Rack::Middleware
|
|
137
149
|
headers.each do |k,v|
|
138
150
|
response.headers[k] = v
|
139
151
|
end
|
140
|
-
response.status = 200
|
141
152
|
|
142
|
-
|
153
|
+
if allow_chunked
|
154
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
155
|
+
response.headers["Transfer-Encoding"] = "chunked"
|
156
|
+
response.headers["Content-Type"] = "text/plain; charset=utf-8"
|
157
|
+
end
|
143
158
|
|
144
|
-
|
159
|
+
response.status = 200
|
160
|
+
client.async_response = response
|
161
|
+
client.synchronize do
|
162
|
+
add_client_with_timeout(client)
|
163
|
+
client.deliver_backlog(backlog)
|
164
|
+
client.ensure_first_chunk_sent
|
165
|
+
end
|
145
166
|
|
146
167
|
throw :async
|
147
168
|
else
|
@@ -5,15 +5,18 @@ class MessageBus::Rails::Engine < ::Rails::Engine; end
|
|
5
5
|
|
6
6
|
class MessageBus::Rails::Railtie < ::Rails::Railtie
|
7
7
|
initializer "message_bus.configure_init" do |app|
|
8
|
-
MessageBus::MessageHandler.load_handlers("#{Rails.root}/app/message_handlers")
|
9
|
-
|
10
8
|
# We want MessageBus to show up after the session middleware, but depending on how
|
11
9
|
# the Rails app is configured that might be ActionDispatch::Session::CookieStore, or potentially
|
12
10
|
# ActionDispatch::Session::ActiveRecordStore.
|
13
11
|
#
|
14
12
|
# To handle either case, we insert it before ActionDispatch::Flash.
|
15
13
|
#
|
16
|
-
|
14
|
+
begin
|
15
|
+
app.middleware.insert_before(ActionDispatch::Flash, MessageBus::Rack::Middleware)
|
16
|
+
rescue
|
17
|
+
app.middleware.use(MessageBus::Rack::Middleware)
|
18
|
+
end
|
19
|
+
|
17
20
|
MessageBus.logger = Rails.logger
|
18
21
|
end
|
19
22
|
end
|
data/lib/message_bus/version.rb
CHANGED
@@ -23,6 +23,10 @@ class FakeAsyncMiddleware
|
|
23
23
|
@simulate_hijack = true
|
24
24
|
end
|
25
25
|
|
26
|
+
def allow_chunked
|
27
|
+
@allow_chunked = true
|
28
|
+
end
|
29
|
+
|
26
30
|
def in_async?
|
27
31
|
@in_async
|
28
32
|
end
|
@@ -37,6 +41,9 @@ class FakeAsyncMiddleware
|
|
37
41
|
end
|
38
42
|
|
39
43
|
def call(env)
|
44
|
+
unless @allow_chunked
|
45
|
+
env['HTTP_DONT_CHUNK'] = 'True'
|
46
|
+
end
|
40
47
|
if simulate_thin_async?
|
41
48
|
call_thin_async(env)
|
42
49
|
elsif simulate_hijack?
|
@@ -1,4 +1,4 @@
|
|
1
|
-
asset_directory = File.expand_path('
|
1
|
+
asset_directory = File.expand_path('../../../../../assets', __FILE__)
|
2
2
|
asset_file_paths = Dir.glob(File.join(asset_directory, 'message-bus.js'))
|
3
3
|
asset_file_names = asset_file_paths.map{|e| File.basename(e) }
|
4
4
|
|
@@ -15,6 +15,84 @@ describe MessageBus::Client do
|
|
15
15
|
@bus.destroy
|
16
16
|
end
|
17
17
|
|
18
|
+
def http_parse(message)
|
19
|
+
lines = message.split("\r\n")
|
20
|
+
|
21
|
+
status = lines.shift.split(" ")[1]
|
22
|
+
headers = {}
|
23
|
+
chunks = []
|
24
|
+
|
25
|
+
while line = lines.shift
|
26
|
+
break if line == ""
|
27
|
+
name,val = line.split(": ")
|
28
|
+
headers[name] = val
|
29
|
+
end
|
30
|
+
|
31
|
+
length = nil
|
32
|
+
while line = lines.shift
|
33
|
+
length = line.to_i(16)
|
34
|
+
break if length == 0
|
35
|
+
rest = lines.join("\r\n")
|
36
|
+
chunks << rest[0...length]
|
37
|
+
lines = (rest[length+2..-1] || "").split("\r\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
# split/join gets tricky
|
41
|
+
chunks[-1] << "\r\n"
|
42
|
+
|
43
|
+
[status, headers, chunks]
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse_chunk(data)
|
47
|
+
payload,_ = data.split(/\r\n\|\r\n/m)
|
48
|
+
JSON.parse(payload)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "can chunk replies" do
|
52
|
+
@client.use_chunked = true
|
53
|
+
r,w = IO.pipe
|
54
|
+
@client.io = w
|
55
|
+
@client.headers = {"Content-Type" => "application/json; charset=utf-8"}
|
56
|
+
@client << MessageBus::Message.new(1, 1, '/test', 'test')
|
57
|
+
@client << MessageBus::Message.new(2, 2, '/test', "a|\r\n|\r\n|b")
|
58
|
+
|
59
|
+
lines = r.read_nonblock(8000)
|
60
|
+
|
61
|
+
status, headers, chunks = http_parse(lines)
|
62
|
+
|
63
|
+
headers["Content-Type"].should == "text/plain; charset=utf-8"
|
64
|
+
status.should == "200"
|
65
|
+
chunks.length.should == 2
|
66
|
+
|
67
|
+
chunk1 = parse_chunk(chunks[0])
|
68
|
+
chunk1.length.should == 1
|
69
|
+
chunk1.first["data"].should == 'test'
|
70
|
+
|
71
|
+
chunk2 = parse_chunk(chunks[1])
|
72
|
+
chunk2.length.should == 1
|
73
|
+
chunk2.first["data"].should == "a|\r\n|\r\n|b"
|
74
|
+
|
75
|
+
@client << MessageBus::Message.new(3, 3, '/test', 'test3')
|
76
|
+
@client.close
|
77
|
+
|
78
|
+
data = r.read
|
79
|
+
|
80
|
+
data[-5..-1].should == "0\r\n\r\n"
|
81
|
+
|
82
|
+
_,_,chunks = http_parse("HTTP/1.1 200 OK\r\n\r\n" << data)
|
83
|
+
|
84
|
+
chunks.length.should == 2
|
85
|
+
|
86
|
+
chunk1 = parse_chunk(chunks[0])
|
87
|
+
chunk1.length.should == 1
|
88
|
+
chunk1.first["data"].should == 'test3'
|
89
|
+
|
90
|
+
# end with []
|
91
|
+
chunk2 = parse_chunk(chunks[1])
|
92
|
+
chunk2.length.should == 0
|
93
|
+
|
94
|
+
end
|
95
|
+
|
18
96
|
it "does not bleed data accross sites" do
|
19
97
|
@client.site_id = "test"
|
20
98
|
|