message_bus 3.3.1 → 3.3.6
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/.eslintrc.js +13 -0
- data/.github/workflows/ci.yml +54 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +3 -1
- data/CHANGELOG +47 -0
- data/DEV.md +9 -0
- data/Gemfile +2 -0
- data/README.md +30 -6
- data/Rakefile +1 -1
- data/assets/message-bus-ajax.js +1 -7
- data/assets/message-bus.js +72 -76
- data/bench/codecs/all_codecs.rb +39 -0
- data/bench/codecs/marshal.rb +11 -0
- data/bench/codecs/packed_string.rb +67 -0
- data/bench/codecs/string_hack.rb +47 -0
- data/bench/codecs_large_user_list.rb +29 -0
- data/bench/codecs_standard_message.rb +29 -0
- data/lib/message_bus.rb +51 -24
- data/lib/message_bus/backends/base.rb +0 -2
- data/lib/message_bus/backends/memory.rb +0 -2
- data/lib/message_bus/backends/postgres.rb +0 -2
- data/lib/message_bus/backends/redis.rb +3 -5
- data/lib/message_bus/client.rb +0 -1
- data/lib/message_bus/codec/base.rb +18 -0
- data/lib/message_bus/codec/json.rb +15 -0
- data/lib/message_bus/codec/oj.rb +21 -0
- data/lib/message_bus/distributed_cache.rb +7 -2
- data/lib/message_bus/message.rb +2 -2
- data/lib/message_bus/rack/middleware.rb +16 -16
- data/lib/message_bus/timer_thread.rb +8 -1
- data/lib/message_bus/version.rb +1 -1
- data/message_bus.gemspec +1 -1
- data/package-lock.json +2192 -0
- data/package.json +8 -3
- data/spec/lib/message_bus_spec.rb +34 -5
- metadata +20 -9
- data/.travis.yml +0 -17
- data/lib/message_bus/em_ext.rb +0 -6
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './packed_string'
|
4
|
+
require_relative './string_hack'
|
5
|
+
require_relative './marshal'
|
6
|
+
|
7
|
+
def all_codecs
|
8
|
+
{
|
9
|
+
json: MessageBus::Codec::Json.new,
|
10
|
+
oj: MessageBus::Codec::Oj.new,
|
11
|
+
marshal: MarshalCodec.new,
|
12
|
+
packed_string_4_bytes: PackedString.new("V"),
|
13
|
+
packed_string_8_bytes: PackedString.new("Q"),
|
14
|
+
string_hack: StringHack.new
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def bench_decode(hash, user_needle)
|
19
|
+
encoded_data = all_codecs.map do |name, codec|
|
20
|
+
[
|
21
|
+
name, codec, codec.encode(hash.dup)
|
22
|
+
]
|
23
|
+
end
|
24
|
+
|
25
|
+
Benchmark.ips do |x|
|
26
|
+
|
27
|
+
encoded_data.each do |name, codec, encoded|
|
28
|
+
x.report(name) do |n|
|
29
|
+
while n > 0
|
30
|
+
decoded = codec.decode(encoded)
|
31
|
+
decoded["user_ids"].include?(user_needle)
|
32
|
+
n -= 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
x.compare!
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class PackedString
|
4
|
+
class FastIdList
|
5
|
+
def self.from_array(array, pack_with)
|
6
|
+
new(array.sort.pack("#{pack_with}*"), pack_with)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.from_string(string, pack_with)
|
10
|
+
new(string, pack_with)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(packed, pack_with)
|
14
|
+
raise "unknown pack format, expecting Q or V" if pack_with != "V" && pack_with != "Q"
|
15
|
+
@packed = packed
|
16
|
+
@pack_with = pack_with
|
17
|
+
@slot_size = pack_with == "V" ? 4 : 8
|
18
|
+
end
|
19
|
+
|
20
|
+
def include?(id)
|
21
|
+
found = (0...length).bsearch do |index|
|
22
|
+
@packed.byteslice(index * @slot_size, @slot_size).unpack1(@pack_with) >= id
|
23
|
+
end
|
24
|
+
|
25
|
+
found && @packed.byteslice(found * @slot_size, @slot_size).unpack1(@pack_with) == id
|
26
|
+
end
|
27
|
+
|
28
|
+
def length
|
29
|
+
@length ||= @packed.bytesize / @slot_size
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_a
|
33
|
+
@packed.unpack("#{@pack_with}*")
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
@packed
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(pack_with = "V")
|
42
|
+
@pack_with = pack_with
|
43
|
+
@oj_options = { mode: :compat }
|
44
|
+
end
|
45
|
+
|
46
|
+
def encode(hash)
|
47
|
+
|
48
|
+
if user_ids = hash["user_ids"]
|
49
|
+
hash["user_ids"] = FastIdList.from_array(hash["user_ids"], @pack_with).to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
hash["data"] = ::Oj.dump(hash["data"], @oj_options)
|
53
|
+
|
54
|
+
Marshal.dump(hash)
|
55
|
+
end
|
56
|
+
|
57
|
+
def decode(payload)
|
58
|
+
result = Marshal.load(payload)
|
59
|
+
result["data"] = ::Oj.load(result["data"], @oj_options)
|
60
|
+
|
61
|
+
if str = result["user_ids"]
|
62
|
+
result["user_ids"] = FastIdList.from_string(str, @pack_with)
|
63
|
+
end
|
64
|
+
|
65
|
+
result
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class StringHack
|
4
|
+
class FastIdList
|
5
|
+
def self.from_array(array)
|
6
|
+
new(",#{array.join(",")},")
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.from_string(string)
|
10
|
+
new(string)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(packed)
|
14
|
+
@packed = packed
|
15
|
+
end
|
16
|
+
|
17
|
+
def include?(id)
|
18
|
+
@packed.include?(",#{id},")
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
@packed
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@oj_options = { mode: :compat }
|
28
|
+
end
|
29
|
+
|
30
|
+
def encode(hash)
|
31
|
+
if user_ids = hash["user_ids"]
|
32
|
+
hash["user_ids"] = FastIdList.from_array(user_ids).to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
::Oj.dump(hash, @oj_options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def decode(payload)
|
39
|
+
result = ::Oj.load(payload, @oj_options)
|
40
|
+
|
41
|
+
if str = result["user_ids"]
|
42
|
+
result["user_ids"] = FastIdList.from_string(str)
|
43
|
+
end
|
44
|
+
|
45
|
+
result
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
gemfile do
|
6
|
+
source 'https://rubygems.org'
|
7
|
+
gem 'message_bus', path: '../'
|
8
|
+
gem 'benchmark-ips'
|
9
|
+
gem 'oj'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'benchmark/ips'
|
13
|
+
require 'message_bus'
|
14
|
+
require_relative 'codecs/all_codecs'
|
15
|
+
|
16
|
+
bench_decode({
|
17
|
+
"data" => "hello world",
|
18
|
+
"user_ids" => (1..10000).to_a,
|
19
|
+
"group_ids" => nil,
|
20
|
+
"client_ids" => nil
|
21
|
+
}, 5000
|
22
|
+
)
|
23
|
+
|
24
|
+
# packed_string_4_bytes: 127176.1 i/s
|
25
|
+
# packed_string_8_bytes: 94494.6 i/s - 1.35x (± 0.00) slower
|
26
|
+
# string_hack: 26403.4 i/s - 4.82x (± 0.00) slower
|
27
|
+
# marshal: 4985.5 i/s - 25.51x (± 0.00) slower
|
28
|
+
# oj: 3072.9 i/s - 41.39x (± 0.00) slower
|
29
|
+
# json: 2222.7 i/s - 57.22x (± 0.00) slower
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
gemfile do
|
6
|
+
source 'https://rubygems.org'
|
7
|
+
gem 'message_bus', path: '../'
|
8
|
+
gem 'benchmark-ips'
|
9
|
+
gem 'oj'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'benchmark/ips'
|
13
|
+
require 'message_bus'
|
14
|
+
require_relative 'codecs/all_codecs'
|
15
|
+
|
16
|
+
bench_decode({
|
17
|
+
"data" => { amazing: "hello world this is an amazing message hello there!!!", another_key: [2, 3, 4] },
|
18
|
+
"user_ids" => [1, 2, 3],
|
19
|
+
"group_ids" => [1],
|
20
|
+
"client_ids" => nil
|
21
|
+
}, 2
|
22
|
+
)
|
23
|
+
|
24
|
+
# marshal: 504885.6 i/s
|
25
|
+
# json: 401050.9 i/s - 1.26x (± 0.00) slower
|
26
|
+
# oj: 340847.4 i/s - 1.48x (± 0.00) slower
|
27
|
+
# string_hack: 296741.6 i/s - 1.70x (± 0.00) slower
|
28
|
+
# packed_string_4_bytes: 207942.6 i/s - 2.43x (± 0.00) slower
|
29
|
+
# packed_string_8_bytes: 206093.0 i/s - 2.45x (± 0.00) slower
|
data/lib/message_bus.rb
CHANGED
@@ -2,24 +2,29 @@
|
|
2
2
|
|
3
3
|
require "monitor"
|
4
4
|
require "set"
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
|
6
|
+
require_relative "message_bus/version"
|
7
|
+
require_relative "message_bus/message"
|
8
|
+
require_relative "message_bus/client"
|
9
|
+
require_relative "message_bus/connection_manager"
|
10
|
+
require_relative "message_bus/diagnostics"
|
11
|
+
require_relative "message_bus/rack/middleware"
|
12
|
+
require_relative "message_bus/rack/diagnostics"
|
13
|
+
require_relative "message_bus/timer_thread"
|
14
|
+
require_relative "message_bus/codec/base"
|
15
|
+
require_relative "message_bus/backends"
|
16
|
+
require_relative "message_bus/backends/base"
|
13
17
|
|
14
18
|
# we still need to take care of the logger
|
15
|
-
if defined?(::Rails)
|
16
|
-
|
19
|
+
if defined?(::Rails::Engine)
|
20
|
+
require_relative 'message_bus/rails/railtie'
|
17
21
|
end
|
18
22
|
|
19
23
|
# @see MessageBus::Implementation
|
20
24
|
module MessageBus; end
|
21
25
|
MessageBus::BACKENDS = {}
|
22
26
|
class MessageBus::InvalidMessage < StandardError; end
|
27
|
+
class MessageBus::InvalidMessageTarget < MessageBus::InvalidMessage; end
|
23
28
|
class MessageBus::BusDestroyed < StandardError; end
|
24
29
|
|
25
30
|
# The main server-side interface to a message bus for the purposes of
|
@@ -277,6 +282,17 @@ module MessageBus::Implementation
|
|
277
282
|
end
|
278
283
|
end
|
279
284
|
|
285
|
+
# @param [MessageBus::Codec::Base] codec used to encode and decode Message payloads
|
286
|
+
# @return [void]
|
287
|
+
def transport_codec=(codec)
|
288
|
+
configure(trasport_codec: codec)
|
289
|
+
end
|
290
|
+
|
291
|
+
# @return [MessageBus::Codec::Base] codec used to encode and decode Message payloads
|
292
|
+
def transport_codec
|
293
|
+
@config[:transport_codec] ||= MessageBus::Codec::Json.new
|
294
|
+
end
|
295
|
+
|
280
296
|
# @param [MessageBus::Backend::Base] pub_sub a configured backend
|
281
297
|
# @return [void]
|
282
298
|
def reliable_pub_sub=(pub_sub)
|
@@ -329,6 +345,7 @@ module MessageBus::Implementation
|
|
329
345
|
#
|
330
346
|
# @raise [MessageBus::BusDestroyed] if the bus is destroyed
|
331
347
|
# @raise [MessageBus::InvalidMessage] if attempting to put permission restrictions on a globally-published message
|
348
|
+
# @raise [MessageBus::InvalidMessageTarget] if attempting to publish to a empty group of users
|
332
349
|
def publish(channel, data, opts = nil)
|
333
350
|
return if @off
|
334
351
|
|
@@ -348,22 +365,32 @@ module MessageBus::Implementation
|
|
348
365
|
site_id = opts[:site_id]
|
349
366
|
end
|
350
367
|
|
351
|
-
|
368
|
+
if (user_ids || group_ids) && global?(channel)
|
369
|
+
raise ::MessageBus::InvalidMessage
|
370
|
+
end
|
352
371
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
372
|
+
if (user_ids == []) || (group_ids == []) || (client_ids == [])
|
373
|
+
raise ::MessageBus::InvalidMessageTarget
|
374
|
+
end
|
375
|
+
|
376
|
+
encoded_data = transport_codec.encode({
|
377
|
+
"data" => data,
|
378
|
+
"user_ids" => user_ids,
|
379
|
+
"group_ids" => group_ids,
|
380
|
+
"client_ids" => client_ids
|
381
|
+
})
|
382
|
+
|
383
|
+
channel_opts = {}
|
359
384
|
|
360
|
-
|
385
|
+
if opts
|
386
|
+
if ((age = opts[:max_backlog_age]) || (size = opts[:max_backlog_size]))
|
387
|
+
channel_opts[:max_backlog_size] = size
|
388
|
+
channel_opts[:max_backlog_age] = age
|
389
|
+
end
|
361
390
|
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
max_backlog_age: age
|
366
|
-
}
|
391
|
+
if opts.has_key?(:queue_in_memory)
|
392
|
+
channel_opts[:queue_in_memory] = opts[:queue_in_memory]
|
393
|
+
end
|
367
394
|
end
|
368
395
|
|
369
396
|
encoded_channel_name = encode_channel_name(channel, site_id)
|
@@ -614,7 +641,7 @@ module MessageBus::Implementation
|
|
614
641
|
channel, site_id = decode_channel_name(msg.channel)
|
615
642
|
msg.channel = channel
|
616
643
|
msg.site_id = site_id
|
617
|
-
parsed =
|
644
|
+
parsed = transport_codec.decode(msg.data)
|
618
645
|
msg.data = parsed["data"]
|
619
646
|
msg.user_ids = parsed["user_ids"]
|
620
647
|
msg.group_ids = parsed["group_ids"]
|
@@ -3,8 +3,6 @@
|
|
3
3
|
require 'redis'
|
4
4
|
require 'digest'
|
5
5
|
|
6
|
-
require "message_bus/backends/base"
|
7
|
-
|
8
6
|
module MessageBus
|
9
7
|
module Backends
|
10
8
|
# The Redis backend stores published messages in Redis sorted sets (using
|
@@ -104,8 +102,8 @@ module MessageBus
|
|
104
102
|
|
105
103
|
local global_id = redis.call("INCR", global_id_key)
|
106
104
|
local backlog_id = redis.call("INCR", backlog_id_key)
|
107
|
-
local payload =
|
108
|
-
local global_backlog_message =
|
105
|
+
local payload = table.concat({ global_id, backlog_id, start_payload }, "|")
|
106
|
+
local global_backlog_message = table.concat({ backlog_id, channel }, "|")
|
109
107
|
|
110
108
|
redis.call("ZADD", backlog_key, backlog_id, payload)
|
111
109
|
redis.call("EXPIRE", backlog_key, max_backlog_age)
|
@@ -318,7 +316,7 @@ LUA
|
|
318
316
|
end
|
319
317
|
end
|
320
318
|
rescue => error
|
321
|
-
@logger.warn "#{error} subscribe failed, reconnecting in 1 second. Call stack #{error.backtrace}"
|
319
|
+
@logger.warn "#{error} subscribe failed, reconnecting in 1 second. Call stack #{error.backtrace.join("\n")}"
|
322
320
|
sleep 1
|
323
321
|
global_redis&.disconnect!
|
324
322
|
retry
|
data/lib/message_bus/client.rb
CHANGED
@@ -133,7 +133,6 @@ class MessageBus::Client
|
|
133
133
|
user_allowed = false
|
134
134
|
group_allowed = false
|
135
135
|
|
136
|
-
# this is an inconsistency we should fix anyway, publishing `user_ids: nil` should work same as groups
|
137
136
|
has_users = msg.user_ids && msg.user_ids.length > 0
|
138
137
|
has_groups = msg.group_ids && msg.group_ids.length > 0
|
139
138
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MessageBus
|
4
|
+
module Codec
|
5
|
+
class Base
|
6
|
+
def encode(hash)
|
7
|
+
raise ConcreteClassMustImplementError
|
8
|
+
end
|
9
|
+
|
10
|
+
def decode(payload)
|
11
|
+
raise ConcreteClassMustImplementError
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
autoload :Json, File.expand_path("json", __dir__)
|
16
|
+
autoload :Oj, File.expand_path("oj", __dir__)
|
17
|
+
end
|
18
|
+
end
|