message_bus 3.3.7 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.eslintrc.js +1 -8
- data/.github/workflows/ci.yml +50 -20
- data/.prettierrc +1 -0
- data/CHANGELOG +99 -46
- data/README.md +31 -53
- data/Rakefile +22 -22
- data/docker-compose.yml +1 -1
- data/lib/message_bus/backends/base.rb +14 -0
- data/lib/message_bus/backends/memory.rb +13 -0
- data/lib/message_bus/backends/postgres.rb +55 -22
- data/lib/message_bus/backends/redis.rb +17 -1
- data/lib/message_bus/client.rb +26 -22
- data/lib/message_bus/distributed_cache.rb +1 -0
- data/lib/message_bus/rack/middleware.rb +0 -6
- data/lib/message_bus/rack/thin_ext.rb +1 -0
- data/lib/message_bus/version.rb +1 -1
- data/lib/message_bus.rb +53 -71
- data/message_bus.gemspec +4 -3
- data/package.json +2 -5
- data/spec/helpers.rb +6 -1
- data/spec/lib/fake_async_middleware.rb +1 -0
- data/spec/lib/message_bus/backend_spec.rb +20 -3
- data/spec/lib/message_bus/client_spec.rb +1 -0
- data/spec/lib/message_bus/connection_manager_spec.rb +4 -0
- data/spec/lib/message_bus/multi_process_spec.rb +21 -10
- data/spec/lib/message_bus/rack/middleware_spec.rb +2 -49
- data/spec/lib/message_bus/timer_thread_spec.rb +1 -5
- data/spec/lib/message_bus_spec.rb +12 -3
- data/spec/performance/backlog.rb +80 -0
- data/spec/performance/publish.rb +4 -4
- data/spec/spec_helper.rb +1 -1
- data/vendor/assets/javascripts/message-bus-ajax.js +38 -0
- data/vendor/assets/javascripts/message-bus.js +549 -0
- metadata +8 -31
- data/assets/application.jsx +0 -121
- data/assets/babel.min.js +0 -25
- data/assets/react-dom.js +0 -19851
- data/assets/react.js +0 -3029
- data/examples/diagnostics/Gemfile +0 -6
- data/examples/diagnostics/config.ru +0 -22
- data/lib/message_bus/diagnostics.rb +0 -62
- data/lib/message_bus/rack/diagnostics.rb +0 -120
data/Rakefile
CHANGED
@@ -5,18 +5,16 @@ require 'bundler'
|
|
5
5
|
require 'bundler/gem_tasks'
|
6
6
|
require 'bundler/setup'
|
7
7
|
require 'rubocop/rake_task'
|
8
|
+
require 'yard'
|
8
9
|
|
9
|
-
|
10
|
+
Bundler.require(:default, :test)
|
10
11
|
|
11
|
-
|
12
|
+
RuboCop::RakeTask.new
|
12
13
|
YARD::Rake::YardocTask.new
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
18
|
-
|
19
|
-
Bundler.require(:default, :test)
|
15
|
+
BACKENDS = Dir["lib/message_bus/backends/*.rb"].map { |file| file.match(%r{backends/(?<backend>.*).rb})[:backend] } - ["base"]
|
16
|
+
SPEC_FILES = Dir['spec/**/*_spec.rb']
|
17
|
+
INTEGRATION_FILES = Dir['spec/integration/**/*_spec.rb']
|
20
18
|
|
21
19
|
module CustomBuild
|
22
20
|
def build_gem
|
@@ -31,6 +29,11 @@ module Bundler
|
|
31
29
|
end
|
32
30
|
end
|
33
31
|
|
32
|
+
desc "Generate documentation for Yard, and fail if there are any warnings"
|
33
|
+
task :test_doc do
|
34
|
+
sh "yard --fail-on-warning #{'--no-progress' if ENV['CI']}"
|
35
|
+
end
|
36
|
+
|
34
37
|
namespace :jasmine do
|
35
38
|
desc "Run Jasmine tests in headless mode"
|
36
39
|
task 'ci' do
|
@@ -40,18 +43,16 @@ namespace :jasmine do
|
|
40
43
|
end
|
41
44
|
end
|
42
45
|
|
43
|
-
backends = Dir["lib/message_bus/backends/*.rb"].map { |file| file.match(%r{backends/(?<backend>.*).rb})[:backend] } - ["base"]
|
44
|
-
|
45
46
|
namespace :spec do
|
46
|
-
|
47
|
-
integration_files = Dir['spec/integration/**/*_spec.rb']
|
48
|
-
|
49
|
-
backends.each do |backend|
|
47
|
+
BACKENDS.each do |backend|
|
50
48
|
desc "Run tests on the #{backend} backend"
|
51
49
|
task backend do
|
52
50
|
begin
|
53
51
|
ENV['MESSAGE_BUS_BACKEND'] = backend
|
54
|
-
|
52
|
+
Rake::TestTask.new(backend) do |t|
|
53
|
+
t.test_files = SPEC_FILES - INTEGRATION_FILES
|
54
|
+
end
|
55
|
+
Rake::Task[backend].invoke
|
55
56
|
ensure
|
56
57
|
ENV.delete('MESSAGE_BUS_BACKEND')
|
57
58
|
end
|
@@ -74,7 +75,10 @@ namespace :spec do
|
|
74
75
|
ENV['MESSAGE_BUS_BACKEND'] = 'memory'
|
75
76
|
pid = spawn("bundle exec puma -p 9292 spec/fixtures/test/config.ru")
|
76
77
|
sleep 1 while port_available?(9292)
|
77
|
-
|
78
|
+
Rake::TestTask.new(:integration) do |t|
|
79
|
+
t.test_files = INTEGRATION_FILES
|
80
|
+
end
|
81
|
+
Rake::Task[:integration].invoke
|
78
82
|
ensure
|
79
83
|
ENV.delete('MESSAGE_BUS_BACKEND')
|
80
84
|
Process.kill('TERM', pid) if pid
|
@@ -83,12 +87,12 @@ namespace :spec do
|
|
83
87
|
end
|
84
88
|
|
85
89
|
desc "Run tests on all backends, plus client JS tests"
|
86
|
-
task spec:
|
90
|
+
task spec: BACKENDS.map { |backend| "spec:#{backend}" } + ["jasmine:ci", "spec:integration"]
|
87
91
|
|
88
92
|
desc "Run performance benchmarks on all backends"
|
89
93
|
task :performance do
|
90
94
|
begin
|
91
|
-
ENV['MESSAGE_BUS_BACKENDS'] =
|
95
|
+
ENV['MESSAGE_BUS_BACKENDS'] = BACKENDS.join(",")
|
92
96
|
sh "#{FileUtils::RUBY} -e \"ARGV.each{|f| load f}\" #{Dir['spec/performance/*.rb'].to_a.join(' ')}"
|
93
97
|
ensure
|
94
98
|
ENV.delete('MESSAGE_BUS_BACKENDS')
|
@@ -97,7 +101,3 @@ end
|
|
97
101
|
|
98
102
|
desc "Run all tests, link checks and confirms documentation compiles without error"
|
99
103
|
task default: [:spec, :rubocop, :test_doc]
|
100
|
-
|
101
|
-
Rake::Task['release'].enhance do
|
102
|
-
sh "yarn publish"
|
103
|
-
end
|
data/docker-compose.yml
CHANGED
@@ -36,7 +36,7 @@ services:
|
|
36
36
|
example:
|
37
37
|
build:
|
38
38
|
context: .
|
39
|
-
command: bash -c "cd examples/
|
39
|
+
command: bash -c "cd examples/chat && bundle install && bundle exec rackup --server puma --host 0.0.0.0"
|
40
40
|
environment:
|
41
41
|
BUNDLE_TO: /usr/local/bundle
|
42
42
|
REDISURL: redis://redis:6379
|
@@ -84,6 +84,11 @@ module MessageBus
|
|
84
84
|
raise ConcreteClassMustImplementError
|
85
85
|
end
|
86
86
|
|
87
|
+
# Closes all open connections to the storage.
|
88
|
+
def destroy
|
89
|
+
raise ConcreteClassMustImplementError
|
90
|
+
end
|
91
|
+
|
87
92
|
# Deletes all backlogs and their data. Does not delete non-backlog data that message_bus may persist, depending on the concrete backend implementation. Use with extreme caution.
|
88
93
|
# @abstract
|
89
94
|
def expire_all_backlogs!
|
@@ -113,6 +118,15 @@ module MessageBus
|
|
113
118
|
raise ConcreteClassMustImplementError
|
114
119
|
end
|
115
120
|
|
121
|
+
# Get the ID of the last message published on multiple channels
|
122
|
+
#
|
123
|
+
# @param [Array<String>] channels - array of channels to fetch
|
124
|
+
#
|
125
|
+
# @return [Array<Integer>] the channel-specific IDs of the last message published to each requested channel
|
126
|
+
def last_ids(*channels)
|
127
|
+
raise ConcreteClassMustImplementError
|
128
|
+
end
|
129
|
+
|
116
130
|
# Get messages from a channel backlog
|
117
131
|
#
|
118
132
|
# @param [String] channel the name of the channel in question
|
@@ -212,6 +212,12 @@ module MessageBus
|
|
212
212
|
client.reset!
|
213
213
|
end
|
214
214
|
|
215
|
+
# No-op; this backend doesn't maintain any storage connections.
|
216
|
+
# (see Base#destroy)
|
217
|
+
def destroy
|
218
|
+
nil
|
219
|
+
end
|
220
|
+
|
215
221
|
# (see Base#expire_all_backlogs!)
|
216
222
|
def expire_all_backlogs!
|
217
223
|
client.expire_all_backlogs!
|
@@ -238,6 +244,13 @@ module MessageBus
|
|
238
244
|
client.max_id(channel)
|
239
245
|
end
|
240
246
|
|
247
|
+
# (see Base#last_ids)
|
248
|
+
def last_ids(*channels)
|
249
|
+
channels.map do |c|
|
250
|
+
last_id(c)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
241
254
|
# (see Base#backlog)
|
242
255
|
def backlog(channel, last_id = 0)
|
243
256
|
items = client.backlog channel, last_id.to_i
|
@@ -45,6 +45,7 @@ module MessageBus
|
|
45
45
|
@available = []
|
46
46
|
@allocated = {}
|
47
47
|
@subscribe_connection = nil
|
48
|
+
@subscribed = false
|
48
49
|
@mutex = Mutex.new
|
49
50
|
@pid = Process.pid
|
50
51
|
end
|
@@ -86,10 +87,12 @@ module MessageBus
|
|
86
87
|
hold { |conn| exec_prepared(conn, 'get_message', [channel, id]) { |r| r.getvalue(0, 0) } }
|
87
88
|
end
|
88
89
|
|
89
|
-
def
|
90
|
+
def after_fork
|
90
91
|
sync do
|
91
|
-
@
|
92
|
+
@pid = Process.pid
|
93
|
+
INHERITED_CONNECTIONS.concat(@available)
|
92
94
|
@available.clear
|
95
|
+
@listening_on.clear
|
93
96
|
end
|
94
97
|
end
|
95
98
|
|
@@ -101,6 +104,13 @@ module MessageBus
|
|
101
104
|
end
|
102
105
|
end
|
103
106
|
|
107
|
+
def destroy
|
108
|
+
sync do
|
109
|
+
@available.each(&:close)
|
110
|
+
@available.clear
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
104
114
|
# use with extreme care, will nuke all of the data
|
105
115
|
def expire_all_backlogs!
|
106
116
|
reset!
|
@@ -122,6 +132,21 @@ module MessageBus
|
|
122
132
|
end
|
123
133
|
end
|
124
134
|
|
135
|
+
def max_ids(*channels)
|
136
|
+
block = proc do |pg_result|
|
137
|
+
ids = Array.new(channels.size, 0)
|
138
|
+
pg_result.ntuples.times do |i|
|
139
|
+
channel = pg_result.getvalue(i, 0)
|
140
|
+
max_id = pg_result.getvalue(i, 1)
|
141
|
+
channel_index = channels.index(channel)
|
142
|
+
ids[channel_index] = max_id.to_i
|
143
|
+
end
|
144
|
+
ids
|
145
|
+
end
|
146
|
+
|
147
|
+
hold { |conn| exec_prepared(conn, 'max_channel_ids', [PG::TextEncoder::Array.new.encode(channels)], &block) }
|
148
|
+
end
|
149
|
+
|
125
150
|
def publish(channel, data)
|
126
151
|
hold { |conn| exec_prepared(conn, 'publish', [channel, data]) }
|
127
152
|
end
|
@@ -165,20 +190,23 @@ module MessageBus
|
|
165
190
|
end
|
166
191
|
|
167
192
|
def create_table(conn)
|
168
|
-
|
169
|
-
|
170
|
-
|
193
|
+
sync do
|
194
|
+
begin
|
195
|
+
conn.exec("SELECT 'message_bus'::regclass")
|
196
|
+
rescue PG::UndefinedTable
|
197
|
+
conn.exec 'CREATE TABLE message_bus (id bigserial PRIMARY KEY, channel text NOT NULL, value text NOT NULL CHECK (octet_length(value) >= 2), added_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL)'
|
198
|
+
conn.exec 'CREATE INDEX table_channel_id_index ON message_bus (channel, id)'
|
199
|
+
conn.exec 'CREATE INDEX table_added_at_index ON message_bus (added_at)'
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
171
203
|
nil
|
172
204
|
end
|
173
205
|
|
174
206
|
def hold
|
175
207
|
current_pid = Process.pid
|
176
208
|
if current_pid != @pid
|
177
|
-
|
178
|
-
sync do
|
179
|
-
INHERITED_CONNECTIONS.concat(@available)
|
180
|
-
@available.clear
|
181
|
-
end
|
209
|
+
after_fork
|
182
210
|
end
|
183
211
|
|
184
212
|
if conn = sync { @allocated[Thread.current] }
|
@@ -208,11 +236,7 @@ module MessageBus
|
|
208
236
|
def new_pg_connection
|
209
237
|
conn = raw_pg_connection
|
210
238
|
|
211
|
-
|
212
|
-
conn.exec("SELECT 'message_bus'::regclass")
|
213
|
-
rescue PG::UndefinedTable
|
214
|
-
create_table(conn)
|
215
|
-
end
|
239
|
+
create_table(conn)
|
216
240
|
|
217
241
|
conn.exec 'PREPARE insert_message AS INSERT INTO message_bus (channel, value) VALUES ($1, $2) RETURNING id'
|
218
242
|
conn.exec 'PREPARE clear_global_backlog AS DELETE FROM message_bus WHERE (id <= $1)'
|
@@ -222,6 +246,7 @@ module MessageBus
|
|
222
246
|
conn.exec "PREPARE expire AS DELETE FROM message_bus WHERE added_at < CURRENT_TIMESTAMP - ($1::text || ' seconds')::interval"
|
223
247
|
conn.exec 'PREPARE get_message AS SELECT value FROM message_bus WHERE ((channel = $1) AND (id = $2))'
|
224
248
|
conn.exec 'PREPARE max_channel_id AS SELECT max(id) FROM message_bus WHERE (channel = $1)'
|
249
|
+
conn.exec 'PREPARE max_channel_ids AS SELECT channel, max(id) FROM message_bus WHERE (channel = ANY($1)) GROUP BY channel'
|
225
250
|
conn.exec 'PREPARE max_id AS SELECT max(id) FROM message_bus'
|
226
251
|
conn.exec 'PREPARE publish AS SELECT pg_notify($1, $2)'
|
227
252
|
|
@@ -253,12 +278,14 @@ module MessageBus
|
|
253
278
|
# after 7 days inactive backlogs will be removed
|
254
279
|
@max_backlog_age = 604800
|
255
280
|
@clear_every = config[:clear_every] || 1
|
281
|
+
@mutex = Mutex.new
|
282
|
+
@client = nil
|
256
283
|
end
|
257
284
|
|
258
285
|
# Reconnects to Postgres; used after a process fork, typically triggered by a forking webserver
|
259
286
|
# @see Base#after_fork
|
260
287
|
def after_fork
|
261
|
-
client.
|
288
|
+
client.after_fork
|
262
289
|
end
|
263
290
|
|
264
291
|
# (see Base#reset!)
|
@@ -266,6 +293,11 @@ module MessageBus
|
|
266
293
|
client.reset!
|
267
294
|
end
|
268
295
|
|
296
|
+
# (see Base#destroy)
|
297
|
+
def destroy
|
298
|
+
client.destroy
|
299
|
+
end
|
300
|
+
|
269
301
|
# (see Base#expire_all_backlogs!)
|
270
302
|
def expire_all_backlogs!
|
271
303
|
client.expire_all_backlogs!
|
@@ -297,7 +329,12 @@ module MessageBus
|
|
297
329
|
client.max_id(channel)
|
298
330
|
end
|
299
331
|
|
300
|
-
# (see Base#
|
332
|
+
# (see Base#last_ids)
|
333
|
+
def last_ids(*channels)
|
334
|
+
client.max_ids(*channels)
|
335
|
+
end
|
336
|
+
|
337
|
+
# (see Base#backlog)
|
301
338
|
def backlog(channel, last_id = 0)
|
302
339
|
items = client.backlog channel, last_id.to_i
|
303
340
|
|
@@ -401,11 +438,7 @@ module MessageBus
|
|
401
438
|
private
|
402
439
|
|
403
440
|
def client
|
404
|
-
@client ||=
|
405
|
-
end
|
406
|
-
|
407
|
-
def new_connection
|
408
|
-
Client.new(@config)
|
441
|
+
@client || @mutex.synchronize { @client ||= Client.new(@config) }
|
409
442
|
end
|
410
443
|
|
411
444
|
def postgresql_channel_name
|
@@ -58,6 +58,8 @@ module MessageBus
|
|
58
58
|
@in_memory_backlog = []
|
59
59
|
@lock = Mutex.new
|
60
60
|
@flush_backlog_thread = nil
|
61
|
+
@pub_redis = nil
|
62
|
+
@subscribed = false
|
61
63
|
# after 7 days inactive backlogs will be removed
|
62
64
|
@max_backlog_age = 604800
|
63
65
|
end
|
@@ -65,7 +67,7 @@ module MessageBus
|
|
65
67
|
# Reconnects to Redis; used after a process fork, typically triggered by a forking webserver
|
66
68
|
# @see Base#after_fork
|
67
69
|
def after_fork
|
68
|
-
pub_redis
|
70
|
+
@pub_redis&.disconnect!
|
69
71
|
end
|
70
72
|
|
71
73
|
# (see Base#reset!)
|
@@ -75,6 +77,11 @@ module MessageBus
|
|
75
77
|
end
|
76
78
|
end
|
77
79
|
|
80
|
+
# (see Base#destroy)
|
81
|
+
def destroy
|
82
|
+
@pub_redis&.disconnect!
|
83
|
+
end
|
84
|
+
|
78
85
|
# Deletes all backlogs and their data. Does not delete ID pointers, so new publications will get IDs that continue from the last publication before the expiry. Use with extreme caution.
|
79
86
|
# @see Base#expire_all_backlogs!
|
80
87
|
def expire_all_backlogs!
|
@@ -187,6 +194,13 @@ LUA
|
|
187
194
|
pub_redis.get(backlog_id_key).to_i
|
188
195
|
end
|
189
196
|
|
197
|
+
# (see Base#last_ids)
|
198
|
+
def last_ids(*channels)
|
199
|
+
return [] if channels.size == 0
|
200
|
+
backlog_id_keys = channels.map { |c| backlog_id_key(c) }
|
201
|
+
pub_redis.mget(*backlog_id_keys).map(&:to_i)
|
202
|
+
end
|
203
|
+
|
190
204
|
# (see Base#backlog)
|
191
205
|
def backlog(channel, last_id = 0)
|
192
206
|
redis = pub_redis
|
@@ -254,6 +268,7 @@ LUA
|
|
254
268
|
new_redis.publish(redis_channel_name, UNSUB_MESSAGE)
|
255
269
|
ensure
|
256
270
|
new_redis&.disconnect!
|
271
|
+
@subscribed = false
|
257
272
|
end
|
258
273
|
end
|
259
274
|
|
@@ -296,6 +311,7 @@ LUA
|
|
296
311
|
|
297
312
|
on.message do |_c, m|
|
298
313
|
if m == UNSUB_MESSAGE
|
314
|
+
@subscribed = false
|
299
315
|
global_redis.unsubscribe
|
300
316
|
return
|
301
317
|
end
|
data/lib/message_bus/client.rb
CHANGED
@@ -46,6 +46,9 @@ class MessageBus::Client
|
|
46
46
|
@bus = opts[:message_bus] || MessageBus
|
47
47
|
@subscriptions = {}
|
48
48
|
@chunks_sent = 0
|
49
|
+
@async_response = nil
|
50
|
+
@io = nil
|
51
|
+
@wrote_headers = false
|
49
52
|
end
|
50
53
|
|
51
54
|
# @yield executed with a lock on the Client instance
|
@@ -175,31 +178,32 @@ class MessageBus::Client
|
|
175
178
|
r = []
|
176
179
|
new_message_ids = nil
|
177
180
|
|
178
|
-
@subscriptions.
|
179
|
-
id = v.to_i
|
181
|
+
last_bus_ids = @bus.last_ids(*@subscriptions.keys, site_id: site_id)
|
180
182
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
183
|
+
@subscriptions.each do |k, v|
|
184
|
+
last_client_id = v.to_i
|
185
|
+
last_bus_id = last_bus_ids[k]
|
186
|
+
|
187
|
+
if last_client_id < -1 # Client requesting backlog relative to bus position
|
188
|
+
last_client_id = last_bus_id + last_client_id + 1
|
189
|
+
last_client_id = 0 if last_client_id < 0
|
190
|
+
elsif last_client_id == -1 # Client not requesting backlog
|
191
|
+
next
|
192
|
+
elsif last_client_id == last_bus_id # Client already up-to-date
|
193
|
+
next
|
194
|
+
elsif last_client_id > last_bus_id # Client ahead of the bus
|
195
|
+
@subscriptions[k] = -1
|
196
|
+
next
|
185
197
|
end
|
186
198
|
|
187
|
-
|
199
|
+
messages = @bus.backlog(k, last_client_id, site_id)
|
188
200
|
|
189
|
-
messages
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
else
|
196
|
-
messages.each do |msg|
|
197
|
-
if allowed?(msg)
|
198
|
-
r << msg
|
199
|
-
else
|
200
|
-
new_message_ids ||= {}
|
201
|
-
new_message_ids[k] = msg.message_id
|
202
|
-
end
|
201
|
+
messages.each do |msg|
|
202
|
+
if allowed?(msg)
|
203
|
+
r << msg
|
204
|
+
else
|
205
|
+
new_message_ids ||= {}
|
206
|
+
new_message_ids[k] = msg.message_id
|
203
207
|
end
|
204
208
|
end
|
205
209
|
end
|
@@ -209,7 +213,7 @@ class MessageBus::Client
|
|
209
213
|
@subscriptions.each do |k, v|
|
210
214
|
if v.to_i == -1 || (new_message_ids && new_message_ids[k])
|
211
215
|
status_message ||= {}
|
212
|
-
@subscriptions[k] = status_message[k] =
|
216
|
+
@subscriptions[k] = status_message[k] = last_bus_ids[k]
|
213
217
|
end
|
214
218
|
end
|
215
219
|
|
@@ -41,7 +41,6 @@ class MessageBus::Rack::Middleware
|
|
41
41
|
@started_listener = false
|
42
42
|
@base_route = "#{@bus.base_route}message-bus/"
|
43
43
|
@base_route_length = @base_route.length
|
44
|
-
@diagnostics_route = "#{@base_route}_diagnostics"
|
45
44
|
@broadcast_route = "#{@base_route}broadcast"
|
46
45
|
start_listener unless @bus.off?
|
47
46
|
end
|
@@ -79,11 +78,6 @@ class MessageBus::Rack::Middleware
|
|
79
78
|
return [200, { "Content-Type" => "text/html" }, ["sent"]]
|
80
79
|
end
|
81
80
|
|
82
|
-
if env['PATH_INFO'].start_with? @diagnostics_route
|
83
|
-
diags = MessageBus::Rack::Diagnostics.new(@app, message_bus: @bus)
|
84
|
-
return diags.call(env)
|
85
|
-
end
|
86
|
-
|
87
81
|
client_id = env['PATH_INFO'][@base_route_length..-1].split("/")[0]
|
88
82
|
return [404, {}, ["not found"]] unless client_id
|
89
83
|
|
data/lib/message_bus/version.rb
CHANGED