message_bus 1.1.1 → 2.0.0.beta.1
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/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
|
|