message_bus 3.3.6 → 4.0.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc.js +3 -2
  3. data/.github/workflows/ci.yml +79 -32
  4. data/.prettierrc +1 -0
  5. data/CHANGELOG +104 -53
  6. data/DEV.md +0 -2
  7. data/Gemfile +0 -27
  8. data/LICENSE +1 -1
  9. data/README.md +40 -62
  10. data/Rakefile +31 -26
  11. data/assets/message-bus-ajax.js +3 -3
  12. data/bench/codecs/marshal.rb +1 -1
  13. data/bench/codecs/packed_string.rb +1 -1
  14. data/docker-compose.yml +1 -1
  15. data/examples/bench/bench.lua +2 -2
  16. data/lib/message_bus/backends/base.rb +8 -3
  17. data/lib/message_bus/backends/memory.rb +6 -0
  18. data/lib/message_bus/backends/postgres.rb +29 -16
  19. data/lib/message_bus/backends/redis.rb +11 -2
  20. data/lib/message_bus/client.rb +6 -7
  21. data/lib/message_bus/connection_manager.rb +1 -1
  22. data/lib/message_bus/distributed_cache.rb +3 -1
  23. data/lib/message_bus/http_client.rb +2 -2
  24. data/lib/message_bus/rack/middleware.rb +6 -6
  25. data/lib/message_bus/rack/thin_ext.rb +2 -1
  26. data/lib/message_bus/version.rb +1 -1
  27. data/lib/message_bus.rb +47 -77
  28. data/message_bus.gemspec +21 -3
  29. data/package-lock.json +1575 -23
  30. data/package.json +9 -7
  31. data/spec/assets/SpecHelper.js +6 -5
  32. data/spec/assets/message-bus.spec.js +9 -6
  33. data/spec/helpers.rb +23 -7
  34. data/spec/integration/http_client_spec.rb +1 -1
  35. data/spec/lib/fake_async_middleware.rb +1 -0
  36. data/spec/lib/message_bus/backend_spec.rb +15 -46
  37. data/spec/lib/message_bus/client_spec.rb +7 -6
  38. data/spec/lib/message_bus/connection_manager_spec.rb +4 -0
  39. data/spec/lib/message_bus/distributed_cache_spec.rb +5 -7
  40. data/spec/lib/message_bus/multi_process_spec.rb +21 -10
  41. data/spec/lib/message_bus/rack/middleware_spec.rb +8 -44
  42. data/spec/lib/message_bus/timer_thread_spec.rb +1 -5
  43. data/spec/lib/message_bus_spec.rb +22 -9
  44. data/spec/performance/publish.rb +4 -4
  45. data/spec/spec_helper.rb +8 -9
  46. data/spec/support/jasmine-browser.json +16 -0
  47. data/vendor/assets/javascripts/message-bus-ajax.js +3 -3
  48. metadata +220 -19
  49. data/assets/application.jsx +0 -121
  50. data/assets/babel.min.js +0 -25
  51. data/assets/react-dom.js +0 -19851
  52. data/assets/react.js +0 -3029
  53. data/examples/diagnostics/Gemfile +0 -6
  54. data/examples/diagnostics/config.ru +0 -22
  55. data/lib/message_bus/diagnostics.rb +0 -62
  56. data/lib/message_bus/rack/diagnostics.rb +0 -98
  57. data/spec/assets/support/jasmine.yml +0 -126
  58. data/spec/assets/support/jasmine_helper.rb +0 -11
@@ -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
@@ -220,7 +223,7 @@ class MessageBus::Client
220
223
 
221
224
  private
222
225
 
223
- # heavily optimised to avoid all uneeded allocations
226
+ # heavily optimised to avoid all unneeded allocations
224
227
  NEWLINE = "\r\n".freeze
225
228
  COLON_SPACE = ": ".freeze
226
229
  HTTP_11 = "HTTP/1.1 200 OK\r\n".freeze
@@ -261,7 +264,7 @@ class MessageBus::Client
261
264
  @wrote_headers = true
262
265
  end
263
266
 
264
- # chunked encoding may be "re-chunked" by proxies, so add a seperator
267
+ # chunked encoding may be "re-chunked" by proxies, so add a separator
265
268
  postfix = NEWLINE + "|" + NEWLINE
266
269
  data = data.gsub(postfix, NEWLINE + "||" + NEWLINE)
267
270
  chunk_length = data.bytesize + postfix.bytesize
@@ -275,11 +278,7 @@ class MessageBus::Client
275
278
  @async_response << postfix
276
279
  @async_response << NEWLINE
277
280
  else
278
- @io.write(chunk_length.to_s(16))
279
- @io.write(NEWLINE)
280
- @io.write(data)
281
- @io.write(postfix)
282
- @io.write(NEWLINE)
281
+ @io.write(chunk_length.to_s(16) << NEWLINE << data << postfix << NEWLINE)
283
282
  end
284
283
  end
285
284
 
@@ -37,7 +37,7 @@ class MessageBus::ConnectionManager
37
37
  rescue
38
38
  # pipe may be broken, move on
39
39
  end
40
- # turns out you can delete from a set while itereating
40
+ # turns out you can delete from a set while iterating
41
41
  remove_client(client) if client.closed?
42
42
  end
43
43
  end
@@ -22,6 +22,7 @@ module MessageBus
22
22
  @lock = Mutex.new
23
23
  @message_bus = message_bus || MessageBus
24
24
  @publish_queue_in_memory = publish_queue_in_memory
25
+ @app_version = nil
25
26
  end
26
27
 
27
28
  def subscribers
@@ -45,7 +46,8 @@ module MessageBus
45
46
  hash = current.hash(message.site_id || DEFAULT_SITE_ID)
46
47
 
47
48
  case payload["op"]
48
- when "set" then hash[payload["key"]] = payload["marshalled"] ? Marshal.load(Base64.decode64(payload["value"])) : payload["value"]
49
+ # TODO: consider custom marshal support with a restricted set
50
+ when "set" then hash[payload["key"]] = payload["marshalled"] ? Marshal.load(Base64.decode64(payload["value"])) : payload["value"] # rubocop:disable Security/MarshalLoad
49
51
  when "delete" then hash.delete(payload["key"])
50
52
  when "clear" then hash.clear
51
53
  end
@@ -146,8 +146,8 @@ module MessageBus
146
146
  #
147
147
  # A last_message_id may be provided.
148
148
  # * -1 will subscribe to all new messages
149
- # * -2 will recieve last message + all new messages
150
- # * -3 will recieve last 2 message + all new messages
149
+ # * -2 will receive last message + all new messages
150
+ # * -3 will receive last 2 message + all new messages
151
151
  #
152
152
  # @example Subscribing to a channel with `last_message_id`
153
153
  # client.subscribe("/test", last_message_id: -2) do |payload|
@@ -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
@@ -66,6 +65,12 @@ class MessageBus::Rack::Middleware
66
65
  private
67
66
 
68
67
  def handle_request(env)
68
+ # Prevent simple polling from clobbering the session
69
+ # See: https://github.com/discourse/message_bus/issues/257
70
+ if (rack_session_options = env[Rack::RACK_SESSION_OPTIONS])
71
+ rack_session_options[:skip] = true
72
+ end
73
+
69
74
  # special debug/test route
70
75
  if @bus.allow_broadcast? && env['PATH_INFO'] == @broadcast_route
71
76
  parsed = Rack::Request.new(env)
@@ -73,11 +78,6 @@ class MessageBus::Rack::Middleware
73
78
  return [200, { "Content-Type" => "text/html" }, ["sent"]]
74
79
  end
75
80
 
76
- if env['PATH_INFO'].start_with? @diagnostics_route
77
- diags = MessageBus::Rack::Diagnostics.new(@app, message_bus: @bus)
78
- return diags.call(env)
79
- end
80
-
81
81
  client_id = env['PATH_INFO'][@base_route_length..-1].split("/")[0]
82
82
  return [404, {}, ["not found"]] unless client_id
83
83
 
@@ -9,6 +9,7 @@ module Thin
9
9
 
10
10
  def initialize
11
11
  @queue = []
12
+ @body_callback = nil
12
13
  end
13
14
 
14
15
  def call(body)
@@ -38,7 +39,7 @@ module Thin
38
39
  end
39
40
  end
40
41
 
41
- # Response whos body is sent asynchronously.
42
+ # Response which body is sent asynchronously.
42
43
  class AsyncResponse
43
44
  include Rack::Response::Helpers
44
45
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MessageBus
4
- VERSION = "3.3.6"
4
+ VERSION = "4.0.0"
5
5
  end
data/lib/message_bus.rb CHANGED
@@ -7,9 +7,7 @@ require_relative "message_bus/version"
7
7
  require_relative "message_bus/message"
8
8
  require_relative "message_bus/client"
9
9
  require_relative "message_bus/connection_manager"
10
- require_relative "message_bus/diagnostics"
11
10
  require_relative "message_bus/rack/middleware"
12
- require_relative "message_bus/rack/diagnostics"
13
11
  require_relative "message_bus/timer_thread"
14
12
  require_relative "message_bus/codec/base"
15
13
  require_relative "message_bus/backends"
@@ -41,21 +39,10 @@ module MessageBus::Implementation
41
39
  def initialize
42
40
  @config = {}
43
41
  @mutex = Synchronizer.new
44
- end
45
-
46
- # @param [Boolean] val whether or not to cache static assets for the diagnostics pages
47
- # @return [void]
48
- def cache_assets=(val)
49
- configure(cache_assets: val)
50
- end
51
-
52
- # @return [Boolean] whether or not to cache static assets for the diagnostics pages
53
- def cache_assets
54
- if defined? @config[:cache_assets]
55
- @config[:cache_assets]
56
- else
57
- true
58
- end
42
+ @off = false
43
+ @destroyed = false
44
+ @timer_thread = nil
45
+ @subscriber_thread = nil
59
46
  end
60
47
 
61
48
  # @param [Logger] logger a logger object to be used by the bus
@@ -100,14 +87,14 @@ module MessageBus::Implementation
100
87
  configure(long_polling_enabled: val)
101
88
  end
102
89
 
103
- # @param [Integer] val The number of simultanuous clients we can service;
90
+ # @param [Integer] val The number of simultaneous clients we can service;
104
91
  # will revert to polling if we are out of slots
105
92
  # @return [void]
106
93
  def max_active_clients=(val)
107
94
  configure(max_active_clients: val)
108
95
  end
109
96
 
110
- # @return [Integer] The number of simultanuous clients we can service;
97
+ # @return [Integer] The number of simultaneous clients we can service;
111
98
  # will revert to polling if we are out of slots. Defaults to 1000 if not
112
99
  # explicitly set.
113
100
  def max_active_clients
@@ -275,7 +262,7 @@ module MessageBus::Implementation
275
262
  # set, defaults to false unless we're in Rails test or development mode.
276
263
  def allow_broadcast?
277
264
  @config[:allow_broadcast] ||=
278
- if defined? ::Rails
265
+ if defined? ::Rails.env
279
266
  ::Rails.env.test? || ::Rails.env.development?
280
267
  else
281
268
  false
@@ -285,7 +272,7 @@ module MessageBus::Implementation
285
272
  # @param [MessageBus::Codec::Base] codec used to encode and decode Message payloads
286
273
  # @return [void]
287
274
  def transport_codec=(codec)
288
- configure(trasport_codec: codec)
275
+ configure(transport_codec: codec)
289
276
  end
290
277
 
291
278
  # @return [MessageBus::Codec::Base] codec used to encode and decode Message payloads
@@ -293,15 +280,20 @@ module MessageBus::Implementation
293
280
  @config[:transport_codec] ||= MessageBus::Codec::Json.new
294
281
  end
295
282
 
296
- # @param [MessageBus::Backend::Base] pub_sub a configured backend
283
+ # @param [MessageBus::Backend::Base] backend_instance A configured backend
297
284
  # @return [void]
285
+ def backend_instance=(backend_instance)
286
+ configure(backend_instance: backend_instance)
287
+ end
288
+
298
289
  def reliable_pub_sub=(pub_sub)
299
- configure(reliable_pub_sub: pub_sub)
290
+ logger.warn "MessageBus.reliable_pub_sub= is deprecated, use MessageBus.backend_instance= instead."
291
+ self.backend_instance = pub_sub
300
292
  end
301
293
 
302
294
  # @return [MessageBus::Backend::Base] the configured backend. If not
303
295
  # explicitly set, will be loaded based on the configuration provided.
304
- def reliable_pub_sub
296
+ def backend_instance
305
297
  @mutex.synchronize do
306
298
  return nil if @destroyed
307
299
 
@@ -309,7 +301,7 @@ module MessageBus::Implementation
309
301
  # passed to backend.
310
302
  logger
311
303
 
312
- @config[:reliable_pub_sub] ||= begin
304
+ @config[:backend_instance] ||= begin
313
305
  @config[:backend_options] ||= {}
314
306
  require "message_bus/backends/#{backend}"
315
307
  MessageBus::BACKENDS[backend].new @config
@@ -317,17 +309,16 @@ module MessageBus::Implementation
317
309
  end
318
310
  end
319
311
 
312
+ def reliable_pub_sub
313
+ logger.warn "MessageBus.reliable_pub_sub is deprecated, use MessageBus.backend_instance instead."
314
+ backend_instance
315
+ end
316
+
320
317
  # @return [Symbol] the name of the backend implementation configured
321
318
  def backend
322
319
  @config[:backend] || :redis
323
320
  end
324
321
 
325
- # Enables diagnostics tracking
326
- # @return [void]
327
- def enable_diagnostics
328
- MessageBus::Diagnostics.enable(self)
329
- end
330
-
331
322
  # Publishes a message to a channel
332
323
  #
333
324
  # @param [String] channel the name of the channel to which the message should be published
@@ -338,7 +329,7 @@ module MessageBus::Implementation
338
329
  # @option opts [Array<String,Integer>] :group_ids (`nil`) the group IDs to which the message should be available. If nil, available to all.
339
330
  # @option opts [String] :site_id (`nil`) the site ID to scope the message to; used for hosting multiple
340
331
  # applications or instances of an application against a single message_bus
341
- # @option opts [nil,Integer] :max_backlog_age the longest amount of time a message may live in a backlog before beging removed, in seconds
332
+ # @option opts [nil,Integer] :max_backlog_age the longest amount of time a message may live in a backlog before being removed, in seconds
342
333
  # @option opts [nil,Integer] :max_backlog_size the largest permitted size (number of messages) for the channel backlog; beyond this capacity, old messages will be dropped
343
334
  #
344
335
  # @return [Integer] the channel-specific ID the message was given
@@ -394,7 +385,7 @@ module MessageBus::Implementation
394
385
  end
395
386
 
396
387
  encoded_channel_name = encode_channel_name(channel, site_id)
397
- reliable_pub_sub.publish(encoded_channel_name, encoded_data, channel_opts)
388
+ backend_instance.publish(encoded_channel_name, encoded_data, channel_opts)
398
389
  end
399
390
 
400
391
  # Subscribe to messages. Each message will be delivered by yielding to the
@@ -410,9 +401,9 @@ module MessageBus::Implementation
410
401
  # @return [void]
411
402
  def blocking_subscribe(channel = nil, &blk)
412
403
  if channel
413
- reliable_pub_sub.subscribe(encode_channel_name(channel), &blk)
404
+ backend_instance.subscribe(encode_channel_name(channel), &blk)
414
405
  else
415
- reliable_pub_sub.global_subscribe(&blk)
406
+ backend_instance.global_subscribe(&blk)
416
407
  end
417
408
  end
418
409
 
@@ -491,9 +482,9 @@ module MessageBus::Implementation
491
482
  def backlog(channel = nil, last_id = nil, site_id = nil)
492
483
  old =
493
484
  if channel
494
- reliable_pub_sub.backlog(encode_channel_name(channel, site_id), last_id)
485
+ backend_instance.backlog(encode_channel_name(channel, site_id), last_id)
495
486
  else
496
- reliable_pub_sub.global_backlog(last_id)
487
+ backend_instance.global_backlog(last_id)
497
488
  end
498
489
 
499
490
  old.each do |m|
@@ -509,7 +500,7 @@ module MessageBus::Implementation
509
500
  #
510
501
  # @return [Integer] the channel-specific ID of the last message published to the given channel
511
502
  def last_id(channel, site_id = nil)
512
- reliable_pub_sub.last_id(encode_channel_name(channel, site_id))
503
+ backend_instance.last_id(encode_channel_name(channel, site_id))
513
504
  end
514
505
 
515
506
  # Get the last message published on a channel
@@ -527,12 +518,13 @@ module MessageBus::Implementation
527
518
  end
528
519
 
529
520
  # Stops listening for publications and stops executing scheduled tasks.
530
- # Mostly used in tests to detroy entire bus.
521
+ # Mostly used in tests to destroy entire bus.
531
522
  # @return [void]
532
523
  def destroy
533
524
  return if @destroyed
534
525
 
535
- reliable_pub_sub.global_unsubscribe
526
+ backend_instance.global_unsubscribe
527
+ backend_instance.destroy
536
528
 
537
529
  @mutex.synchronize do
538
530
  return if @destroyed
@@ -550,7 +542,7 @@ module MessageBus::Implementation
550
542
  # scheduled tasks.
551
543
  # @return [void]
552
544
  def after_fork
553
- reliable_pub_sub.after_fork
545
+ backend_instance.after_fork
554
546
  ensure_subscriber_thread
555
547
  # will ensure timer is running
556
548
  timer.queue {}
@@ -559,12 +551,12 @@ module MessageBus::Implementation
559
551
  # @return [Boolean] whether or not the server is actively listening for
560
552
  # publications on the bus
561
553
  def listening?
562
- @subscriber_thread && @subscriber_thread.alive?
554
+ @subscriber_thread&.alive?
563
555
  end
564
556
 
565
557
  # (see MessageBus::Backend::Base#reset!)
566
558
  def reset!
567
- reliable_pub_sub.reset! if reliable_pub_sub
559
+ backend_instance.reset! if backend_instance
568
560
  end
569
561
 
570
562
  # @return [MessageBus::TimerThread] the timer thread used for triggering
@@ -692,12 +684,6 @@ module MessageBus::Implementation
692
684
  @subscriptions[site_id][channel] << blk
693
685
  ensure_subscriber_thread
694
686
 
695
- attempts = 100
696
- while attempts > 0 && !reliable_pub_sub.subscribed
697
- sleep 0.001
698
- attempts -= 1
699
- end
700
-
701
687
  raise MessageBus::BusDestroyed if @destroyed
702
688
 
703
689
  blk
@@ -715,10 +701,17 @@ module MessageBus::Implementation
715
701
 
716
702
  def ensure_subscriber_thread
717
703
  @mutex.synchronize do
718
- return if (@subscriber_thread && @subscriber_thread.alive?) || @destroyed
704
+ return if @destroyed
705
+ next if @subscriber_thread&.alive?
719
706
 
720
707
  @subscriber_thread = new_subscriber_thread
721
708
  end
709
+
710
+ attempts = 100
711
+ while attempts > 0 && !backend_instance.subscribed
712
+ sleep 0.001
713
+ attempts -= 1
714
+ end
722
715
  end
723
716
 
724
717
  MIN_KEEPALIVE = 20
@@ -739,34 +732,11 @@ module MessageBus::Implementation
739
732
  if !@destroyed && thread.alive? && keepalive_interval > MIN_KEEPALIVE
740
733
 
741
734
  publish("/__mb_keepalive__/", Process.pid, user_ids: [-1])
742
- # going for x3 keepalives missed for a restart, need to ensure this only very rarely happens
743
- # note: after_fork will sort out a bad @last_message date, but thread will be dead anyway
744
735
  if (Time.now - (@last_message || Time.now)) > keepalive_interval * 3
745
- logger.warn "Global messages on #{Process.pid} timed out, restarting process"
746
- # No other clean way to remove this thread, its listening on a socket
747
- # no data is arriving
748
- #
749
- # In production we see this kind of situation ... sometimes ... when there is
750
- # a VRRP failover, or weird networking condition
751
- pid = Process.pid
752
-
753
- # do the best we can to terminate self cleanly
754
- fork do
755
- Process.kill('TERM', pid)
756
- sleep 10
757
- begin
758
- Process.kill('KILL', pid)
759
- rescue Errno::ESRCH
760
- logger.warn "#{Process.pid} successfully terminated by `TERM` signal."
761
- end
762
- end
763
-
764
- sleep 10
765
- Process.kill('KILL', pid)
766
-
767
- else
768
- timer.queue(keepalive_interval, &blk) if keepalive_interval > MIN_KEEPALIVE
736
+ logger.warn "Global messages on #{Process.pid} timed out, message bus is no longer functioning correctly"
769
737
  end
738
+
739
+ timer.queue(keepalive_interval, &blk) if keepalive_interval > MIN_KEEPALIVE
770
740
  end
771
741
  end
772
742
 
@@ -778,7 +748,7 @@ module MessageBus::Implementation
778
748
  def global_subscribe_thread
779
749
  # pretend we just got a message
780
750
  @last_message = Time.now
781
- reliable_pub_sub.global_subscribe do |msg|
751
+ backend_instance.global_subscribe do |msg|
782
752
  begin
783
753
  @last_message = Time.now
784
754
  decode_message!(msg)
data/message_bus.gemspec CHANGED
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- # -*- encoding: utf-8 -*-
3
2
 
4
3
  require File.expand_path('../lib/message_bus/version', __FILE__)
5
4
 
@@ -8,7 +7,7 @@ Gem::Specification.new do |gem|
8
7
  gem.email = ["sam.saffron@gmail.com"]
9
8
  gem.description = %q{A message bus for rack}
10
9
  gem.summary = %q{}
11
- gem.homepage = "https://github.com/SamSaffron/message_bus"
10
+ gem.homepage = "https://github.com/discourse/message_bus"
12
11
  gem.license = "MIT"
13
12
  gem.files = `git ls-files`.split($\) +
14
13
  ["vendor/assets/javascripts/message-bus.js", "vendor/assets/javascripts/message-bus-ajax.js"]
@@ -17,8 +16,27 @@ Gem::Specification.new do |gem|
17
16
  gem.name = "message_bus"
18
17
  gem.require_paths = ["lib"]
19
18
  gem.version = MessageBus::VERSION
20
- gem.required_ruby_version = ">= 2.4.0"
19
+ gem.required_ruby_version = ">= 2.6.0"
20
+
21
21
  gem.add_runtime_dependency 'rack', '>= 1.1.3'
22
+
23
+ # Optional runtime dependencies
22
24
  gem.add_development_dependency 'redis'
23
25
  gem.add_development_dependency 'pg'
26
+ gem.add_development_dependency 'concurrent-ruby' # for distributed-cache
27
+
28
+ gem.add_development_dependency 'minitest'
29
+ gem.add_development_dependency 'minitest-hooks'
30
+ gem.add_development_dependency 'minitest-global_expectations'
31
+ gem.add_development_dependency 'rake'
32
+ gem.add_development_dependency 'http_parser.rb'
33
+ gem.add_development_dependency 'thin'
34
+ gem.add_development_dependency 'rack-test'
35
+ gem.add_development_dependency 'puma'
36
+ gem.add_development_dependency 'm'
37
+ gem.add_development_dependency 'byebug'
38
+ gem.add_development_dependency 'oj'
39
+ gem.add_development_dependency 'yard'
40
+ gem.add_development_dependency 'rubocop-discourse'
41
+ gem.add_development_dependency 'rubocop-rspec'
24
42
  end