message_bus 2.2.0.pre.1 → 2.2.0.pre.2
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 +2 -0
- data/.rubocop.yml +1 -25
- data/CHANGELOG +9 -0
- data/Gemfile +5 -0
- data/README.md +90 -4
- data/Rakefile +50 -19
- data/assets/application.jsx +121 -0
- data/assets/babel.min.js +25 -0
- data/assets/react-dom.js +19851 -0
- data/assets/react.js +3029 -0
- data/docker-compose.yml +13 -0
- data/examples/diagnostics/Gemfile +5 -0
- data/examples/diagnostics/config.ru +21 -0
- data/lib/message_bus/http_client.rb +337 -0
- data/lib/message_bus/http_client/channel.rb +13 -0
- data/lib/message_bus/http_client/version.rb +7 -0
- data/lib/message_bus/rack/diagnostics.rb +10 -23
- data/lib/message_bus/version.rb +1 -1
- data/spec/fixtures/test/Gemfile +3 -0
- data/spec/fixtures/test/config.ru +17 -0
- data/spec/helpers.rb +19 -0
- data/spec/integration/http_client_spec.rb +197 -0
- data/spec/lib/message_bus/connection_manager_spec.rb +12 -4
- data/spec/lib/message_bus/rack/middleware_spec.rb +11 -3
- data/spec/performance/publish.rb +102 -0
- data/spec/spec_helper.rb +3 -18
- metadata +21 -8
- data/.rubocop_todo.yml +0 -659
- data/assets/application.handlebars +0 -7
- data/assets/application.js +0 -79
- data/assets/ember.js +0 -26839
- data/assets/handlebars.js +0 -2201
- data/assets/index.handlebars +0 -25
data/docker-compose.yml
CHANGED
@@ -33,6 +33,19 @@ services:
|
|
33
33
|
- bundle:/usr/local/bundle
|
34
34
|
ports:
|
35
35
|
- 8808:8808
|
36
|
+
example:
|
37
|
+
build:
|
38
|
+
context: .
|
39
|
+
command: bash -c "cd examples/diagnostics && bundle install && bundle exec rackup --server puma --host 0.0.0.0"
|
40
|
+
environment:
|
41
|
+
BUNDLE_TO: /usr/local/bundle
|
42
|
+
REDISURL: redis://redis:6379
|
43
|
+
volumes:
|
44
|
+
- .:/usr/src/app
|
45
|
+
- bundle_config:/home/src/app/.bundle
|
46
|
+
- bundle:/usr/local/bundle
|
47
|
+
ports:
|
48
|
+
- 9292:9292
|
36
49
|
redis:
|
37
50
|
image: redis:5.0
|
38
51
|
volumes:
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'message_bus'
|
2
|
+
|
3
|
+
MessageBus.configure(backend: :redis, url: ENV['REDISURL'])
|
4
|
+
MessageBus.enable_diagnostics
|
5
|
+
|
6
|
+
MessageBus.user_id_lookup do |_env|
|
7
|
+
1
|
8
|
+
end
|
9
|
+
|
10
|
+
MessageBus.is_admin_lookup do |_env|
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
use MessageBus::Rack::Middleware
|
15
|
+
run lambda { |_env|
|
16
|
+
[
|
17
|
+
200,
|
18
|
+
{ "Content-Type" => "text/html" },
|
19
|
+
['Howdy. Check out <a href="/message-bus/_diagnostics">the diagnostics UI</a>.']
|
20
|
+
]
|
21
|
+
}
|
@@ -0,0 +1,337 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'uri'
|
5
|
+
require 'message_bus/http_client/channel'
|
6
|
+
|
7
|
+
module MessageBus
|
8
|
+
# MessageBus client that enables subscription via long polling with support
|
9
|
+
# for chunked encoding. Falls back to normal polling if long polling is not
|
10
|
+
# available.
|
11
|
+
#
|
12
|
+
# @!attribute [r] channels
|
13
|
+
# @return [Hash] a map of the channels that the client is subscribed to
|
14
|
+
# @!attribute [r] stats
|
15
|
+
# @return [Stats] a Struct containing the statistics of failed and successful
|
16
|
+
# polling requests
|
17
|
+
#
|
18
|
+
# @!attribute enable_long_polling
|
19
|
+
# @return [Boolean] whether long polling is enabled
|
20
|
+
# @!attribute status
|
21
|
+
# @return [HTTPClient::STOPPED, HTTPClient::STARTED] the status of the client.
|
22
|
+
# @!attribute enable_chunked_encoding
|
23
|
+
# @return [Boolean] whether chunked encoding is enabled
|
24
|
+
# @!attribute min_poll_interval
|
25
|
+
# @return [Float] the min poll interval for long polling
|
26
|
+
# @!attribute max_poll_interval
|
27
|
+
# @return [Float] the max poll interval for long polling
|
28
|
+
# @!attribute background_callback_interval
|
29
|
+
# @return [Float] the polling interval
|
30
|
+
class HTTPClient
|
31
|
+
class InvalidChannel < StandardError; end
|
32
|
+
class MissingBlock < StandardError; end
|
33
|
+
|
34
|
+
attr_reader :channels,
|
35
|
+
:stats
|
36
|
+
|
37
|
+
attr_accessor :enable_long_polling,
|
38
|
+
:status,
|
39
|
+
:enable_chunked_encoding,
|
40
|
+
:min_poll_interval,
|
41
|
+
:max_poll_interval,
|
42
|
+
:background_callback_interval
|
43
|
+
|
44
|
+
CHUNK_SEPARATOR = "\r\n|\r\n".freeze
|
45
|
+
private_constant :CHUNK_SEPARATOR
|
46
|
+
STATUS_CHANNEL = "/__status".freeze
|
47
|
+
private_constant :STATUS_CHANNEL
|
48
|
+
|
49
|
+
STOPPED = 0
|
50
|
+
STARTED = 1
|
51
|
+
|
52
|
+
Stats = Struct.new(:failed, :success)
|
53
|
+
private_constant :Stats
|
54
|
+
|
55
|
+
# @param base_url [String] Base URL of the message_bus server to connect to
|
56
|
+
# @param enable_long_polling [Boolean] Enable long polling
|
57
|
+
# @param enable_chunked_encoding [Boolean] Enable chunk encoding
|
58
|
+
# @param min_poll_interval [Float, Integer] Min poll interval when long polling
|
59
|
+
# @param max_poll_interval [Float, Integer] Max poll interval when long polling.
|
60
|
+
# When requests fail, the client will backoff and this is the upper limit.
|
61
|
+
# @param background_callback_interval [Float, Integer] Interval to poll when
|
62
|
+
# when polling.
|
63
|
+
# @param headers [Hash] extra HTTP headers to be set on the polling requests.
|
64
|
+
#
|
65
|
+
# @return [Object] Instance of MessageBus::HTTPClient
|
66
|
+
def initialize(base_url, enable_long_polling: true,
|
67
|
+
enable_chunked_encoding: true,
|
68
|
+
min_poll_interval: 0.1,
|
69
|
+
max_poll_interval: 180,
|
70
|
+
background_callback_interval: 60,
|
71
|
+
headers: {})
|
72
|
+
|
73
|
+
@uri = URI(base_url)
|
74
|
+
@enable_long_polling = enable_long_polling
|
75
|
+
@enable_chunked_encoding = enable_chunked_encoding
|
76
|
+
@min_poll_interval = min_poll_interval
|
77
|
+
@max_poll_interval = max_poll_interval
|
78
|
+
@background_callback_interval = background_callback_interval
|
79
|
+
@headers = headers
|
80
|
+
@client_id = SecureRandom.hex
|
81
|
+
@channels = {}
|
82
|
+
@status = STOPPED
|
83
|
+
@mutex = Mutex.new
|
84
|
+
@stats = Stats.new(0, 0)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Starts a background thread that polls the message bus endpoint
|
88
|
+
# for the given base_url.
|
89
|
+
#
|
90
|
+
# Intervals for long polling can be configured via min_poll_interval and
|
91
|
+
# max_poll_interval.
|
92
|
+
#
|
93
|
+
# Intervals for polling can be configured via background_callback_interval.
|
94
|
+
#
|
95
|
+
# @return [Object] Instance of MessageBus::HTTPClient
|
96
|
+
def start
|
97
|
+
@mutex.synchronize do
|
98
|
+
return if started?
|
99
|
+
|
100
|
+
@status = STARTED
|
101
|
+
|
102
|
+
thread = Thread.new do
|
103
|
+
begin
|
104
|
+
while started?
|
105
|
+
unless @channels.empty?
|
106
|
+
poll
|
107
|
+
@stats.success += 1
|
108
|
+
@stats.failed = 0
|
109
|
+
end
|
110
|
+
|
111
|
+
sleep interval
|
112
|
+
end
|
113
|
+
rescue StandardError => e
|
114
|
+
@stats.failed += 1
|
115
|
+
warn("#{e.class} #{e.message}: #{e.backtrace.join("\n")}")
|
116
|
+
retry
|
117
|
+
ensure
|
118
|
+
stop
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
thread.abort_on_exception = true
|
123
|
+
end
|
124
|
+
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
# Stops the client from polling the message bus endpoint.
|
129
|
+
#
|
130
|
+
# @return [Integer] the current status of the client
|
131
|
+
def stop
|
132
|
+
@status = STOPPED
|
133
|
+
end
|
134
|
+
|
135
|
+
# Subscribes to a channel which executes the given callback when a message
|
136
|
+
# is published to the channel
|
137
|
+
#
|
138
|
+
# @example Subscribing to a channel for message
|
139
|
+
# client = MessageBus::HTTPClient.new('http://some.test.com')
|
140
|
+
#
|
141
|
+
# client.subscribe("/test") do |payload, _message_id, _global_id|
|
142
|
+
# puts payload
|
143
|
+
# end
|
144
|
+
#
|
145
|
+
# A last_message_id may be provided.
|
146
|
+
# * -1 will subscribe to all new messages
|
147
|
+
# * -2 will recieve last message + all new messages
|
148
|
+
# * -3 will recieve last 2 message + all new messages
|
149
|
+
#
|
150
|
+
# @example Subscribing to a channel with `last_message_id`
|
151
|
+
# client.subscribe("/test", last_message_id: -2) do |payload|
|
152
|
+
# puts payload
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# @param channel [String] channel to listen for messages on
|
156
|
+
# @param last_message_id [Integer] last message id to start polling on.
|
157
|
+
#
|
158
|
+
# @yield [data, message_id, global_id]
|
159
|
+
# callback to be executed whenever a message is received
|
160
|
+
#
|
161
|
+
# @yieldparam data [Hash] data payload of the message received on the channel
|
162
|
+
# @yieldparam message_id [Integer] id of the message in the channel
|
163
|
+
# @yieldparam global_id [Integer] id of the message in the global backlog
|
164
|
+
# @yieldreturn [void]
|
165
|
+
#
|
166
|
+
# @return [Integer] the current status of the client
|
167
|
+
def subscribe(channel, last_message_id: nil, &callback)
|
168
|
+
raise InvalidChannel unless channel.to_s.start_with?("/")
|
169
|
+
raise MissingBlock unless block_given?
|
170
|
+
|
171
|
+
last_message_id = -1 if last_message_id && !last_message_id.is_a?(Integer)
|
172
|
+
|
173
|
+
@channels[channel] ||= Channel.new
|
174
|
+
channel = @channels[channel]
|
175
|
+
channel.last_message_id = last_message_id if last_message_id
|
176
|
+
channel.callbacks.push(callback)
|
177
|
+
start if stopped?
|
178
|
+
end
|
179
|
+
|
180
|
+
# unsubscribes from a channel
|
181
|
+
#
|
182
|
+
# @example Unsubscribing from a channel
|
183
|
+
# client = MessageBus::HTTPClient.new('http://some.test.com')
|
184
|
+
# callback = -> { |payload| puts payload }
|
185
|
+
# client.subscribe("/test", &callback)
|
186
|
+
# client.unsubscribe("/test")
|
187
|
+
#
|
188
|
+
# If a callback is given, only the specific callback will be unsubscribed.
|
189
|
+
#
|
190
|
+
# @example Unsubscribing a callback from a channel
|
191
|
+
# client.unsubscribe("/test", &callback)
|
192
|
+
#
|
193
|
+
# When the client does not have any channels left, it will stop polling and
|
194
|
+
# waits until a new subscription is started.
|
195
|
+
#
|
196
|
+
# @param channel [String] channel to unsubscribe
|
197
|
+
# @yield [data, global_id, message_id] specific callback to unsubscribe
|
198
|
+
#
|
199
|
+
# @return [Integer] the current status of the client
|
200
|
+
def unsubscribe(channel, &callback)
|
201
|
+
if callback
|
202
|
+
@channels[channel].callbacks.delete(callback)
|
203
|
+
remove_channel(channel) if @channels[channel].callbacks.empty?
|
204
|
+
else
|
205
|
+
remove_channel(channel)
|
206
|
+
end
|
207
|
+
|
208
|
+
stop if @channels.empty?
|
209
|
+
@status
|
210
|
+
end
|
211
|
+
|
212
|
+
private
|
213
|
+
|
214
|
+
def stopped?
|
215
|
+
@status == STOPPED
|
216
|
+
end
|
217
|
+
|
218
|
+
def started?
|
219
|
+
@status == STARTED
|
220
|
+
end
|
221
|
+
|
222
|
+
def remove_channel(channel)
|
223
|
+
@channels.delete(channel)
|
224
|
+
end
|
225
|
+
|
226
|
+
def interval
|
227
|
+
if @enable_long_polling
|
228
|
+
if (failed_count = @stats.failed) > 2
|
229
|
+
(@min_poll_interval * failed_count).clamp(
|
230
|
+
@min_poll_interval, @max_poll_interval
|
231
|
+
)
|
232
|
+
else
|
233
|
+
@min_poll_interval
|
234
|
+
end
|
235
|
+
else
|
236
|
+
@background_callback_interval
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def poll
|
241
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
242
|
+
http.use_ssl = true if @uri.scheme == 'https'
|
243
|
+
request = Net::HTTP::Post.new(request_path, headers)
|
244
|
+
request.body = poll_payload
|
245
|
+
|
246
|
+
if @enable_long_polling
|
247
|
+
buffer = ''
|
248
|
+
|
249
|
+
http.request(request) do |response|
|
250
|
+
response.read_body do |chunk|
|
251
|
+
unless chunk.empty?
|
252
|
+
buffer << chunk
|
253
|
+
process_buffer(buffer)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
else
|
258
|
+
response = http.request(request)
|
259
|
+
notify_channels(JSON.parse(response.body))
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def is_chunked?
|
264
|
+
!headers["Dont-Chunk"]
|
265
|
+
end
|
266
|
+
|
267
|
+
def process_buffer(buffer)
|
268
|
+
index = buffer.index(CHUNK_SEPARATOR)
|
269
|
+
|
270
|
+
if is_chunked?
|
271
|
+
return unless index
|
272
|
+
|
273
|
+
messages = buffer[0..(index - 1)]
|
274
|
+
buffer.slice!("#{messages}#{CHUNK_SEPARATOR}")
|
275
|
+
else
|
276
|
+
messages = buffer[0..-1]
|
277
|
+
buffer.slice!(messages)
|
278
|
+
end
|
279
|
+
|
280
|
+
notify_channels(JSON.parse(messages))
|
281
|
+
end
|
282
|
+
|
283
|
+
def notify_channels(messages)
|
284
|
+
messages.each do |message|
|
285
|
+
current_channel = message['channel']
|
286
|
+
|
287
|
+
if current_channel == STATUS_CHANNEL
|
288
|
+
message["data"].each do |channel_name, last_message_id|
|
289
|
+
if (channel = @channels[channel_name])
|
290
|
+
channel.last_message_id = last_message_id
|
291
|
+
end
|
292
|
+
end
|
293
|
+
else
|
294
|
+
@channels.each do |channel_name, channel|
|
295
|
+
next unless channel_name == current_channel
|
296
|
+
|
297
|
+
channel.last_message_id = message['message_id']
|
298
|
+
|
299
|
+
channel.callbacks.each do |callback|
|
300
|
+
callback.call(
|
301
|
+
message['data'],
|
302
|
+
channel.last_message_id,
|
303
|
+
message['global_id']
|
304
|
+
)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def poll_payload
|
312
|
+
payload = {}
|
313
|
+
|
314
|
+
@channels.each do |channel_name, channel|
|
315
|
+
payload[channel_name] = channel.last_message_id
|
316
|
+
end
|
317
|
+
|
318
|
+
payload.to_json
|
319
|
+
end
|
320
|
+
|
321
|
+
def request_path
|
322
|
+
"/message-bus/#{@client_id}/poll"
|
323
|
+
end
|
324
|
+
|
325
|
+
def headers
|
326
|
+
headers = {}
|
327
|
+
headers['Content-Type'] = 'application/json'
|
328
|
+
headers['X-Silence-logger'] = 'true'
|
329
|
+
|
330
|
+
if !@enable_long_polling || !@enable_chunked_encoding
|
331
|
+
headers['Dont-Chunk'] = 'true'
|
332
|
+
end
|
333
|
+
|
334
|
+
headers.merge!(@headers)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module MessageBus
|
2
|
+
class HTTPClient
|
3
|
+
# @private
|
4
|
+
class Channel
|
5
|
+
attr_accessor :last_message_id, :callbacks
|
6
|
+
|
7
|
+
def initialize(last_message_id: -1, callbacks: [])
|
8
|
+
@last_message_id = last_message_id
|
9
|
+
@callbacks = callbacks
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -42,10 +42,7 @@ class MessageBus::Rack::Diagnostics
|
|
42
42
|
if asset && !asset !~ /\//
|
43
43
|
content = asset_contents(asset)
|
44
44
|
split = asset.split('.')
|
45
|
-
|
46
|
-
content = translate_handlebars(split[0], content)
|
47
|
-
end
|
48
|
-
return [200, { 'content-type' => 'text/javascript;' }, [content]]
|
45
|
+
return [200, { 'Content-Type' => 'application/javascript;charset=UTF-8' }, [content]]
|
49
46
|
end
|
50
47
|
|
51
48
|
return [404, {}, ['not found']]
|
@@ -53,16 +50,16 @@ class MessageBus::Rack::Diagnostics
|
|
53
50
|
|
54
51
|
private
|
55
52
|
|
56
|
-
def js_asset(name)
|
57
|
-
return generate_script_tag(name) unless @bus.cache_assets
|
53
|
+
def js_asset(name, type = "text/javascript")
|
54
|
+
return generate_script_tag(name, type) unless @bus.cache_assets
|
58
55
|
|
59
56
|
@@asset_cache ||= {}
|
60
|
-
@@asset_cache[name] ||= generate_script_tag(name)
|
57
|
+
@@asset_cache[name] ||= generate_script_tag(name, type)
|
61
58
|
@@asset_cache[name]
|
62
59
|
end
|
63
60
|
|
64
|
-
def generate_script_tag(name)
|
65
|
-
"<script src='/message-bus/_diagnostics/assets/#{name}?#{file_hash(name)}' type='
|
61
|
+
def generate_script_tag(name, type)
|
62
|
+
"<script src='/message-bus/_diagnostics/assets/#{name}?#{file_hash(name)}' type='#{type}'></script>"
|
66
63
|
end
|
67
64
|
|
68
65
|
def file_hash(asset)
|
@@ -87,24 +84,14 @@ class MessageBus::Rack::Diagnostics
|
|
87
84
|
<body>
|
88
85
|
<div id="app"></div>
|
89
86
|
#{js_asset "jquery-1.8.2.js"}
|
90
|
-
#{js_asset "
|
91
|
-
#{js_asset "
|
87
|
+
#{js_asset "react.js"}
|
88
|
+
#{js_asset "react-dom.js"}
|
89
|
+
#{js_asset "babel.min.js"}
|
92
90
|
#{js_asset "message-bus.js"}
|
93
|
-
#{js_asset "application.
|
94
|
-
#{js_asset "index.handlebars"}
|
95
|
-
#{js_asset "application.js"}
|
91
|
+
#{js_asset "application.jsx", "text/jsx"}
|
96
92
|
</body>
|
97
93
|
</html>
|
98
94
|
HTML
|
99
95
|
return [200, { "content-type" => "text/html;" }, [html]]
|
100
96
|
end
|
101
|
-
|
102
|
-
def translate_handlebars(name, content)
|
103
|
-
"Ember.TEMPLATES['#{name}'] = Ember.Handlebars.compile(#{indent(content).inspect});"
|
104
|
-
end
|
105
|
-
|
106
|
-
# from ember-rails
|
107
|
-
def indent(string)
|
108
|
-
string.gsub(/$(.)/m, "\\1 ").strip
|
109
|
-
end
|
110
97
|
end
|