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.

@@ -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
- write_and_close "[]"
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
- return unless in_async?
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
- write_and_close messages_to_json([msg])
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
- @io.write(HTTP_11)
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
- around_filter = @bus.around_client_batch(msg.channel)
23
-
24
- work = lambda do
25
- subscription.each do |client_id|
26
- client = @clients[client_id]
27
- if client && client.allowed?(msg)
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
- if backlog.length > 0
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
- add_client_with_timeout(client)
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
- client.async_response = response
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
- add_client_with_timeout(client)
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
- app.middleware.insert_before(ActionDispatch::Flash, MessageBus::Rack::Middleware)
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
@@ -1,3 +1,3 @@
1
1
  module MessageBus
2
- VERSION = "1.1.1"
2
+ VERSION = "2.0.0.beta.1"
3
3
  end
@@ -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('../../../../assets', __FILE__)
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