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.

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