message_bus 4.0.0 → 4.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65630299cab9aae5df5b4f3131043d42063575978d5a83e9267436bed2b7d729
4
- data.tar.gz: 2bd69d120552b01ae82c73a775d065c23575ac623016d2bd2ac25a5ebd637d6f
3
+ metadata.gz: 11a14ba0ec9ad142d3335fb7eb764be8e9b975a8dc8367fd4bf359e128a56095
4
+ data.tar.gz: bf41cede85c8e75e56829447cd3cf9ed6cd531ba27fcddfbccb6043f3a82da34
5
5
  SHA512:
6
- metadata.gz: 65a5d250126fc935053dfffef037baaf780678764ac95e4924603b901016f82b451a79a51f18fc699f2cfd8006cb805cd4887d206c12ca68ac68f059f1f13409
7
- data.tar.gz: 2ebcc465985827a16866a948690a7bb4c03252050c8bfb1ee458809c6f0eda8c8fd581ba2f9bb1b496d3c7952456a11ad6ac51dd63e69c465238cffa17d7c2da
6
+ metadata.gz: 23c1d181028194c16e98c5a3df42e445458aef4779c97ae2d6d5100adf5053680f62ea4c914bea7c84c124bd453127e141c68ffeb1c742cc208e1d040bc8558f
7
+ data.tar.gz: 7efffe711fac2520356b115820acba2d4206d53f35017ba64c86e3c571a85fb423e943cb9301a03d3b47d6f442f1626dea0495c0242220283685eaa944c890b6
@@ -20,7 +20,7 @@ jobs:
20
20
  strategy:
21
21
  fail-fast: false
22
22
  matrix:
23
- ruby: [2.6, 2.7, 3.0]
23
+ ruby: [2.6, 2.7, '3.0', 3.1]
24
24
  redis: [5, 6]
25
25
 
26
26
  services:
data/CHANGELOG CHANGED
@@ -1,3 +1,12 @@
1
+ 16-02-2022
2
+
3
+ - Version 4.1.0
4
+
5
+ - PERF: Optimize Client#backlog for up-to-date clients
6
+
7
+ Also introduces a new MessageBus#last_ids(*channels) api for fetching the last_ids of
8
+ multiple channels simultaneously
9
+
1
10
  11-01-2022
2
11
 
3
12
  - Version 4.0.0
@@ -118,6 +118,15 @@ module MessageBus
118
118
  raise ConcreteClassMustImplementError
119
119
  end
120
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
+
121
130
  # Get messages from a channel backlog
122
131
  #
123
132
  # @param [String] channel the name of the channel in question
@@ -244,6 +244,13 @@ module MessageBus
244
244
  client.max_id(channel)
245
245
  end
246
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
+
247
254
  # (see Base#backlog)
248
255
  def backlog(channel, last_id = 0)
249
256
  items = client.backlog channel, last_id.to_i
@@ -132,6 +132,21 @@ module MessageBus
132
132
  end
133
133
  end
134
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
+
135
150
  def publish(channel, data)
136
151
  hold { |conn| exec_prepared(conn, 'publish', [channel, data]) }
137
152
  end
@@ -175,9 +190,16 @@ module MessageBus
175
190
  end
176
191
 
177
192
  def create_table(conn)
178
- 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)'
179
- conn.exec 'CREATE INDEX table_channel_id_index ON message_bus (channel, id)'
180
- conn.exec 'CREATE INDEX table_added_at_index ON message_bus (added_at)'
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
+
181
203
  nil
182
204
  end
183
205
 
@@ -214,11 +236,7 @@ module MessageBus
214
236
  def new_pg_connection
215
237
  conn = raw_pg_connection
216
238
 
217
- begin
218
- conn.exec("SELECT 'message_bus'::regclass")
219
- rescue PG::UndefinedTable
220
- create_table(conn)
221
- end
239
+ create_table(conn)
222
240
 
223
241
  conn.exec 'PREPARE insert_message AS INSERT INTO message_bus (channel, value) VALUES ($1, $2) RETURNING id'
224
242
  conn.exec 'PREPARE clear_global_backlog AS DELETE FROM message_bus WHERE (id <= $1)'
@@ -228,6 +246,7 @@ module MessageBus
228
246
  conn.exec "PREPARE expire AS DELETE FROM message_bus WHERE added_at < CURRENT_TIMESTAMP - ($1::text || ' seconds')::interval"
229
247
  conn.exec 'PREPARE get_message AS SELECT value FROM message_bus WHERE ((channel = $1) AND (id = $2))'
230
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'
231
250
  conn.exec 'PREPARE max_id AS SELECT max(id) FROM message_bus'
232
251
  conn.exec 'PREPARE publish AS SELECT pg_notify($1, $2)'
233
252
 
@@ -310,7 +329,12 @@ module MessageBus
310
329
  client.max_id(channel)
311
330
  end
312
331
 
313
- # (see Base#last_id)
332
+ # (see Base#last_ids)
333
+ def last_ids(*channels)
334
+ client.max_ids(*channels)
335
+ end
336
+
337
+ # (see Base#backlog)
314
338
  def backlog(channel, last_id = 0)
315
339
  items = client.backlog channel, last_id.to_i
316
340
 
@@ -194,6 +194,13 @@ LUA
194
194
  pub_redis.get(backlog_id_key).to_i
195
195
  end
196
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
+
197
204
  # (see Base#backlog)
198
205
  def backlog(channel, last_id = 0)
199
206
  redis = pub_redis
@@ -178,31 +178,32 @@ class MessageBus::Client
178
178
  r = []
179
179
  new_message_ids = nil
180
180
 
181
- @subscriptions.each do |k, v|
182
- id = v.to_i
181
+ last_bus_ids = @bus.last_ids(*@subscriptions.keys, site_id: site_id)
183
182
 
184
- if id < -1
185
- last_id = @bus.last_id(k, site_id)
186
- id = last_id + id + 1
187
- id = 0 if id < 0
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
188
197
  end
189
198
 
190
- next if id < 0
191
-
192
- messages = @bus.backlog(k, id, site_id)
199
+ messages = @bus.backlog(k, last_client_id, site_id)
193
200
 
194
- if messages.length == 0
195
- if id > @bus.last_id(k, site_id)
196
- @subscriptions[k] = -1
197
- end
198
- else
199
- messages.each do |msg|
200
- if allowed?(msg)
201
- r << msg
202
- else
203
- new_message_ids ||= {}
204
- new_message_ids[k] = msg.message_id
205
- 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
206
207
  end
207
208
  end
208
209
  end
@@ -212,7 +213,7 @@ class MessageBus::Client
212
213
  @subscriptions.each do |k, v|
213
214
  if v.to_i == -1 || (new_message_ids && new_message_ids[k])
214
215
  status_message ||= {}
215
- @subscriptions[k] = status_message[k] = @bus.last_id(k, site_id)
216
+ @subscriptions[k] = status_message[k] = last_bus_ids[k]
216
217
  end
217
218
  end
218
219
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MessageBus
4
- VERSION = "4.0.0"
4
+ VERSION = "4.1.0"
5
5
  end
data/lib/message_bus.rb CHANGED
@@ -503,6 +503,18 @@ module MessageBus::Implementation
503
503
  backend_instance.last_id(encode_channel_name(channel, site_id))
504
504
  end
505
505
 
506
+ # Get the ID of the last message published on multiple channels
507
+ #
508
+ # @param [Array<String>] channels - array of channels to fetch
509
+ # @param [String] site_id - the ID of the site by which to filter
510
+ #
511
+ # @return [Hash] the channel-specific IDs of the last message published to each requested channel
512
+ def last_ids(*channels, site_id: nil)
513
+ encoded_channel_names = channels.map { |c| encode_channel_name(c, site_id) }
514
+ ids = backend_instance.last_ids(*encoded_channel_names)
515
+ channels.zip(ids).to_h
516
+ end
517
+
506
518
  # Get the last message published on a channel
507
519
  #
508
520
  # @param [String] channel the name of the channel in question
data/message_bus.gemspec CHANGED
@@ -11,8 +11,6 @@ Gem::Specification.new do |gem|
11
11
  gem.license = "MIT"
12
12
  gem.files = `git ls-files`.split($\) +
13
13
  ["vendor/assets/javascripts/message-bus.js", "vendor/assets/javascripts/message-bus-ajax.js"]
14
- gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
15
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
14
  gem.name = "message_bus"
17
15
  gem.require_paths = ["lib"]
18
16
  gem.version = MessageBus::VERSION
@@ -91,6 +91,22 @@ describe BACKEND_CLASS do
91
91
  @bus.last_id("/foo").must_equal 1
92
92
  end
93
93
 
94
+ it "should allow us to get multiple last_ids" do
95
+ @bus.last_ids("/foo", "/bar", "/foobar").must_equal [0, 0, 0]
96
+
97
+ @bus.publish("/foo", "one")
98
+ @bus.publish("/foo", "two")
99
+ @bus.publish("/foobar", "three")
100
+
101
+ @bus.last_ids("/foo", "/bar", "/foobar").must_equal(
102
+ [
103
+ @bus.last_id("/foo"),
104
+ @bus.last_id("/bar"),
105
+ @bus.last_id("/foobar")
106
+ ]
107
+ )
108
+ end
109
+
94
110
  it "can set backlog age" do
95
111
  @bus.max_backlog_age = 1
96
112
 
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..', 'lib')
4
+ require 'logger'
5
+ require 'benchmark'
6
+ require 'message_bus'
7
+
8
+ require_relative "../helpers"
9
+
10
+ backends = ENV['MESSAGE_BUS_BACKENDS'].split(",").map(&:to_sym)
11
+ channel = "/foo"
12
+ iterations = 10_000
13
+ results = []
14
+
15
+ puts "Running backlog benchmark with #{iterations} iterations on backends: #{backends.inspect}"
16
+
17
+ run_benchmark = lambda do |bm, backend|
18
+ bus = MessageBus::Instance.new
19
+ bus.configure(test_config_for_backend(backend))
20
+
21
+ bus.backend_instance.max_backlog_size = 100
22
+ bus.backend_instance.max_global_backlog_size = 1000
23
+
24
+ channel_names = 10.times.map { |i| "channel#{i}" }
25
+
26
+ 100.times do |i|
27
+ channel_names.each do |ch|
28
+ bus.publish(ch, { message_number_is: i })
29
+ end
30
+ end
31
+
32
+ last_ids = channel_names.map { |ch| [ch, bus.last_id(ch)] }.to_h
33
+
34
+ 1000.times do
35
+ # Warmup
36
+ client = MessageBus::Client.new(message_bus: bus)
37
+ channel_names.each { |ch| client.subscribe(ch, -1) }
38
+ client.backlog
39
+ end
40
+
41
+ bm.report("#{backend} - #backlog with no backlogs requested") do
42
+ iterations.times do
43
+ client = MessageBus::Client.new(message_bus: bus)
44
+ channel_names.each { |ch| client.subscribe(ch, -1) }
45
+ client.backlog
46
+ end
47
+ end
48
+
49
+ (0..5).each do |ch_i|
50
+ channels_with_messages = (ch_i) * 2
51
+
52
+ bm.report("#{backend} - #backlog when #{channels_with_messages}/10 channels have new messages") do
53
+ iterations.times do
54
+ client = MessageBus::Client.new(message_bus: bus)
55
+ channel_names.each_with_index do |ch, i|
56
+ client.subscribe(ch, last_ids[ch] + ((i < channels_with_messages) ? -1 : 0))
57
+ end
58
+ result = client.backlog
59
+ if result.length != channels_with_messages
60
+ raise "Result has #{result.length} messages. Expected #{channels_with_messages}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ bus.reset!
67
+ bus.destroy
68
+ end
69
+
70
+ puts
71
+
72
+ Benchmark.benchmark(" duration\n", 60, "%10.2rs\n", "") do |bm|
73
+ backends.each do |backend|
74
+ run_benchmark.call(bm, backend)
75
+ end
76
+ end
77
+ puts
78
+ results.each do |result|
79
+ puts result
80
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: message_bus
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-11 00:00:00.000000000 Z
11
+ date: 2022-02-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -344,6 +344,7 @@ files:
344
344
  - spec/lib/message_bus/rack/middleware_spec.rb
345
345
  - spec/lib/message_bus/timer_thread_spec.rb
346
346
  - spec/lib/message_bus_spec.rb
347
+ - spec/performance/backlog.rb
347
348
  - spec/performance/publish.rb
348
349
  - spec/spec_helper.rb
349
350
  - spec/support/jasmine-browser.json
@@ -373,23 +374,4 @@ rubygems_version: 3.1.6
373
374
  signing_key:
374
375
  specification_version: 4
375
376
  summary: ''
376
- test_files:
377
- - spec/assets/SpecHelper.js
378
- - spec/assets/message-bus.spec.js
379
- - spec/fixtures/test/Gemfile
380
- - spec/fixtures/test/config.ru
381
- - spec/helpers.rb
382
- - spec/integration/http_client_spec.rb
383
- - spec/lib/fake_async_middleware.rb
384
- - spec/lib/message_bus/assets/asset_encoding_spec.rb
385
- - spec/lib/message_bus/backend_spec.rb
386
- - spec/lib/message_bus/client_spec.rb
387
- - spec/lib/message_bus/connection_manager_spec.rb
388
- - spec/lib/message_bus/distributed_cache_spec.rb
389
- - spec/lib/message_bus/multi_process_spec.rb
390
- - spec/lib/message_bus/rack/middleware_spec.rb
391
- - spec/lib/message_bus/timer_thread_spec.rb
392
- - spec/lib/message_bus_spec.rb
393
- - spec/performance/publish.rb
394
- - spec/spec_helper.rb
395
- - spec/support/jasmine-browser.json
377
+ test_files: []