msgr 1.2.0 → 1.3.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +8 -0
  3. data/.github/workflows/build.yml +52 -0
  4. data/.github/workflows/lint.yml +20 -0
  5. data/.rubocop.yml +9 -48
  6. data/.travis.yml +21 -35
  7. data/Appraisals +18 -0
  8. data/CHANGELOG.md +11 -1
  9. data/Gemfile +8 -15
  10. data/README.md +8 -20
  11. data/Rakefile +5 -5
  12. data/bin/msgr +1 -0
  13. data/gemfiles/rails_5.2.gemfile +14 -0
  14. data/gemfiles/rails_6.0.gemfile +14 -0
  15. data/gemfiles/rails_6.1.gemfile +14 -0
  16. data/gemfiles/rails_master.gemfile +14 -0
  17. data/lib/msgr.rb +1 -0
  18. data/lib/msgr/binding.rb +13 -8
  19. data/lib/msgr/channel.rb +5 -3
  20. data/lib/msgr/cli.rb +18 -11
  21. data/lib/msgr/client.rb +17 -20
  22. data/lib/msgr/connection.rb +13 -1
  23. data/lib/msgr/consumer.rb +2 -3
  24. data/lib/msgr/dispatcher.rb +7 -9
  25. data/lib/msgr/logging.rb +2 -0
  26. data/lib/msgr/message.rb +1 -2
  27. data/lib/msgr/railtie.rb +14 -69
  28. data/lib/msgr/route.rb +1 -4
  29. data/lib/msgr/routes.rb +2 -0
  30. data/lib/msgr/tasks/msgr/drain.rake +11 -0
  31. data/lib/msgr/test_pool.rb +1 -3
  32. data/lib/msgr/version.rb +1 -1
  33. data/msgr.gemspec +2 -6
  34. data/scripts/simple_test.rb +2 -3
  35. data/spec/fixtures/{msgr-routes-test-1.rb → msgr_routes_test_1.rb} +0 -0
  36. data/spec/integration/dummy/Rakefile +1 -1
  37. data/spec/{msgr/support/.keep → integration/dummy/app/assets/config/manifest.js} +0 -0
  38. data/spec/integration/dummy/bin/bundle +1 -1
  39. data/spec/integration/dummy/bin/rails +1 -1
  40. data/spec/integration/dummy/config/application.rb +1 -1
  41. data/spec/integration/dummy/config/boot.rb +2 -2
  42. data/spec/integration/dummy/config/environment.rb +1 -1
  43. data/spec/integration/dummy/config/rabbitmq.yml +1 -1
  44. data/spec/integration/msgr/dispatcher_spec.rb +28 -12
  45. data/spec/integration/msgr/railtie_spec.rb +10 -120
  46. data/spec/integration/spec_helper.rb +2 -3
  47. data/spec/integration/{msgr_spec.rb → test_controller_spec.rb} +1 -1
  48. data/spec/unit/msgr/client_spec.rb +88 -0
  49. data/spec/{msgr → unit}/msgr/connection_spec.rb +1 -1
  50. data/spec/{msgr → unit}/msgr/consumer_spec.rb +0 -0
  51. data/spec/unit/msgr/dispatcher_spec.rb +45 -0
  52. data/spec/{msgr → unit}/msgr/route_spec.rb +15 -14
  53. data/spec/{msgr → unit}/msgr/routes_spec.rb +32 -35
  54. data/spec/{msgr → unit}/msgr_spec.rb +25 -16
  55. data/spec/{msgr → unit}/spec_helper.rb +1 -1
  56. data/spec/unit/support/.keep +0 -0
  57. metadata +37 -33
  58. data/gemfiles/Gemfile.rails-4-2 +0 -7
  59. data/gemfiles/Gemfile.rails-5-0 +0 -7
  60. data/gemfiles/Gemfile.rails-5-1 +0 -7
  61. data/gemfiles/Gemfile.rails-5-2 +0 -7
  62. data/gemfiles/Gemfile.rails-master +0 -14
  63. data/spec/msgr/msgr/client_spec.rb +0 -60
  64. data/spec/msgr/msgr/dispatcher_spec.rb +0 -44
  65. data/spec/support/setup.rb +0 -29
@@ -26,6 +26,7 @@ require 'msgr/railtie' if defined? Rails
26
26
  module Msgr
27
27
  class << self
28
28
  attr_writer :client, :config
29
+
29
30
  delegate :publish, to: :client
30
31
 
31
32
  def config
@@ -4,7 +4,14 @@ module Msgr
4
4
  class Binding
5
5
  include Logging
6
6
 
7
- attr_reader :queue, :subscription, :connection, :channel, :route, :dispatcher
7
+ attr_reader(
8
+ :channel,
9
+ :connection,
10
+ :dispatcher,
11
+ :queue,
12
+ :route,
13
+ :subscription
14
+ )
8
15
 
9
16
  def initialize(connection, route, dispatcher)
10
17
  @connection = connection
@@ -43,13 +50,11 @@ module Msgr
43
50
 
44
51
  def subscribe
45
52
  @subscription = queue.subscribe(manual_ack: true) do |*args|
46
- begin
47
- dispatcher.call Message.new(channel, *args, route)
48
- rescue => err
49
- log(:error) do
50
- "Rescued error from subscribe: #{err.class.name}: " \
51
- "#{err}\n#{err.backtrace.join("\n")}"
52
- end
53
+ dispatcher.call Message.new(channel, *args, route)
54
+ rescue StandardError => e
55
+ log(:error) do
56
+ "Rescued error from subscribe: #{e.class.name}: " \
57
+ "#{e}\n#{e.backtrace.join("\n")}"
53
58
  end
54
59
  end
55
60
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Msgr
2
4
  class Channel
3
5
  include Logging
@@ -26,8 +28,8 @@ module Msgr
26
28
  end
27
29
  end
28
30
 
29
- def queue(name)
30
- @channel.queue(prefix(name), durable: true).tap do |queue|
31
+ def queue(name, **opts)
32
+ @channel.queue(prefix(name), durable: true, **opts).tap do |queue|
31
33
  log(:debug) do
32
34
  "Create queue #{queue.name} (durable: #{queue.durable?}, " \
33
35
  "auto_delete: #{queue.auto_delete?})"
@@ -62,4 +64,4 @@ module Msgr
62
64
  @channel.close if @channel.open?
63
65
  end
64
66
  end
65
- end
67
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optionparser'
2
4
 
3
5
  module Msgr
@@ -8,7 +10,8 @@ module Msgr
8
10
  @options = options
9
11
 
10
12
  if !File.exist?(options[:require]) ||
11
- (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
13
+ (File.directory?(options[:require]) &&
14
+ !File.exist?("#{options[:require]}/config/application.rb"))
12
15
  raise <<~ERR
13
16
  Rails application or required ruby file not found: #{options[:require]}
14
17
  ERR
@@ -22,15 +25,13 @@ module Msgr
22
25
  require 'rails'
23
26
  if ::Rails::VERSION::MAJOR == 4
24
27
  require File.expand_path("#{options[:require]}/config/application.rb")
25
- ::Rails::Application.initializer "msgr.eager_load" do
28
+ ::Rails::Application.initializer 'msgr.eager_load' do
26
29
  ::Rails.application.config.eager_load = true
27
30
  end
28
- require 'msgr/railtie'
29
- require File.expand_path("#{options[:require]}/config/environment.rb")
30
- else
31
- require 'msgr/railtie'
32
- require File.expand_path("#{options[:require]}/config/environment.rb")
33
31
  end
32
+
33
+ require 'msgr/railtie'
34
+ require File.expand_path("#{options[:require]}/config/environment.rb")
34
35
  else
35
36
  require(options[:require])
36
37
  end
@@ -43,7 +44,7 @@ module Msgr
43
44
  Msgr.logger = Logger.new(STDOUT)
44
45
  Msgr.client.start
45
46
 
46
- while readable = IO.select([r])
47
+ while (readable = IO.select([r]))
47
48
  case readable.first[0].gets.strip
48
49
  when 'INT', 'TERM'
49
50
  Msgr.client.stop
@@ -61,18 +62,24 @@ module Msgr
61
62
 
62
63
  private
63
64
 
64
- def parse(argv)
65
+ def parse(_argv)
65
66
  options = {
66
67
  require: Dir.pwd,
67
68
  environment: 'development'
68
69
  }
69
70
 
70
71
  OptionParser.new do |o|
71
- o.on '-r', '--require [PATH|DIR]', 'Location of Rails application (default to current directory)' do |arg|
72
+ o.on(
73
+ '-r', '--require [PATH|DIR]',
74
+ 'Location of Rails application (default to current directory)'
75
+ ) do |arg|
72
76
  options[:require] = arg
73
77
  end
74
78
 
75
- o.on '-e', '--environment [env]', 'Rails environment (default to development)' do |arg|
79
+ o.on(
80
+ '-e', '--environment [env]',
81
+ 'Rails environment (default to development)'
82
+ ) do |arg|
76
83
  options[:environment] = arg
77
84
  end
78
85
  end.parse!
@@ -2,15 +2,14 @@
2
2
 
3
3
  require 'uri'
4
4
  require 'cgi'
5
+ require 'json'
5
6
 
6
7
  module Msgr
7
- # rubocop:disable Metrics/ClassLength
8
8
  class Client
9
9
  include Logging
10
10
 
11
11
  attr_reader :config
12
12
 
13
- # rubocop:disable MethodLength
14
13
  def initialize(config = {})
15
14
  @config = {
16
15
  host: '127.0.0.1',
@@ -18,7 +17,7 @@ module Msgr
18
17
  max: 2
19
18
  }
20
19
 
21
- @config.merge! parse(config.delete(:uri)) if config.key?(:uri)
20
+ @config.merge! parse(config.delete(:uri)) if config[:uri]
22
21
  @config.merge! config.symbolize_keys
23
22
 
24
23
  @mutex = ::Mutex.new
@@ -27,12 +26,7 @@ module Msgr
27
26
 
28
27
  log(:debug) { "Created new client on process ##{@pid}..." }
29
28
  end
30
- # rubocop:enable all
31
29
 
32
- # rubocop:disable AbcSize
33
- # rubocop:disable MethodLength
34
- # rubocop:disable PerceivedComplexity
35
- # rubocop:disable CyclomaticComplexity
36
30
  def uri
37
31
  @uri = begin
38
32
  uri = ::URI.parse('amqp://localhost')
@@ -50,7 +44,6 @@ module Msgr
50
44
  uri
51
45
  end
52
46
  end
53
- # rubocop:enable all
54
47
 
55
48
  def running?
56
49
  mutex.synchronize do
@@ -59,7 +52,6 @@ module Msgr
59
52
  end
60
53
  end
61
54
 
62
- # rubocop:disable AbcSize
63
55
  def start
64
56
  mutex.synchronize do
65
57
  check_process!
@@ -72,7 +64,6 @@ module Msgr
72
64
  connection.bind(@routes)
73
65
  end
74
66
  end
75
- # rubocop:enable all
76
67
 
77
68
  def connect
78
69
  mutex.synchronize do
@@ -85,7 +76,6 @@ module Msgr
85
76
  end
86
77
  end
87
78
 
88
- # rubocop:disable AbcSize
89
79
  def stop(opts = {})
90
80
  mutex.synchronize do
91
81
  check_process!
@@ -100,7 +90,6 @@ module Msgr
100
90
  reset
101
91
  end
102
92
  end
103
- # rubocop:enable all
104
93
 
105
94
  def purge(release: false)
106
95
  mutex.synchronize do
@@ -112,6 +101,15 @@ module Msgr
112
101
  end
113
102
  end
114
103
 
104
+ ##
105
+ # Purge all queues known to Msgr, if they exist.
106
+ #
107
+ def drain
108
+ @routes.each do |route|
109
+ connection.purge_queue(route.name)
110
+ end
111
+ end
112
+
115
113
  def publish(payload, opts = {})
116
114
  mutex.synchronize do
117
115
  check_process!
@@ -149,6 +147,7 @@ module Msgr
149
147
 
150
148
  def check_process!
151
149
  return if ::Process.pid == @pid
150
+
152
151
  log(:warn) do
153
152
  "Fork detected. Reset internal state. (Old PID: #{@pid} / " \
154
153
  "New PID: #{::Process.pid}"
@@ -178,22 +177,20 @@ module Msgr
178
177
  @dispatcher = nil
179
178
  end
180
179
 
181
- # rubocop:disable AbcSize
182
180
  def parse(uri)
183
181
  # Legacy parsing of URI configuration; does not follow usual
184
182
  # AMQP vhost encoding but used regular URL path
185
183
  uri = ::URI.parse(uri)
186
184
 
187
185
  config = {}
188
- config[:user] ||= uri.user if uri.user
189
- config[:pass] ||= uri.password if uri.password
190
- config[:host] ||= uri.host if uri.host
191
- config[:port] ||= uri.port if uri.port
192
- config[:vhost] ||= uri.path unless uri.path.empty?
186
+ config[:user] ||= uri.user if uri.user
187
+ config[:pass] ||= uri.password if uri.password
188
+ config[:host] ||= uri.host if uri.host
189
+ config[:port] ||= uri.port if uri.port
190
+ config[:vhost] ||= uri.path unless uri.path.empty?
193
191
  config[:ssl] ||= uri.scheme.casecmp('amqps').zero?
194
192
 
195
193
  config
196
194
  end
197
- # rubocop:enable all
198
195
  end
199
196
  end
@@ -3,7 +3,6 @@
3
3
  require 'bunny'
4
4
 
5
5
  module Msgr
6
- # rubocop:disable Metrics/ClassLength
7
6
  class Connection
8
7
  include Logging
9
8
 
@@ -49,6 +48,7 @@ module Msgr
49
48
 
50
49
  def release
51
50
  return if bindings.empty?
51
+
52
52
  log(:debug) { "Release bindings (#{bindings.size})..." }
53
53
 
54
54
  bindings.each(&:release)
@@ -56,6 +56,7 @@ module Msgr
56
56
 
57
57
  def delete
58
58
  return if bindings.empty?
59
+
59
60
  log(:debug) { "Delete bindings (#{bindings.size})..." }
60
61
 
61
62
  bindings.each(&:delete)
@@ -63,11 +64,22 @@ module Msgr
63
64
 
64
65
  def purge(**kwargs)
65
66
  return if bindings.empty?
67
+
66
68
  log(:debug) { "Purge bindings (#{bindings.size})..." }
67
69
 
68
70
  bindings.each {|b| b.purge(**kwargs) }
69
71
  end
70
72
 
73
+ def purge_queue(name)
74
+ # Creating the queue in passive mode ensures that queues that do not exist
75
+ # won't be created just to purge them.
76
+ # That requires creating a new channel every time, as exceptions (on
77
+ # missing queues) invalidate the channel.
78
+ channel.queue(name, passive: true).purge
79
+ rescue Bunny::NotFound
80
+ nil
81
+ end
82
+
71
83
  def bindings
72
84
  @bindings ||= []
73
85
  end
@@ -5,6 +5,7 @@ module Msgr
5
5
  include Logging
6
6
 
7
7
  attr_reader :message
8
+
8
9
  delegate :payload, to: :@message
9
10
  delegate :action, to: :'@message.route'
10
11
  delegate :consumer, to: :'@message.consumer'
@@ -14,9 +15,7 @@ module Msgr
14
15
  @auto_ack || @auto_ack.nil?
15
16
  end
16
17
 
17
- def auto_ack=(val)
18
- @auto_ack = val
19
- end
18
+ attr_writer :auto_ack
20
19
  end
21
20
 
22
21
  def dispatch(message)
@@ -27,9 +27,6 @@ module Msgr
27
27
  end
28
28
  end
29
29
 
30
- # rubocop:disable Metrics/AbcSize
31
- # rubocop:disable Metrics/MethodLength
32
- # rubocop:disable Metrics/CyclomaticComplexity
33
30
  def dispatch(message)
34
31
  consumer_class = Object.const_get message.route.consumer
35
32
 
@@ -37,17 +34,18 @@ module Msgr
37
34
 
38
35
  consumer_class.new.dispatch message
39
36
 
40
- # Acknowledge message unless it is already acknowledged or auto_ack is disabled.
41
- message.ack unless message.acked? or not consumer_class.auto_ack?
42
- rescue => error
37
+ # Acknowledge message only if it is not already acknowledged and auto
38
+ # acknowledgment is enabled.
39
+ message.ack unless message.acked? || !consumer_class.auto_ack?
40
+ rescue StandardError => e
43
41
  message.nack unless message.acked?
44
42
 
45
43
  log(:error) do
46
- "Dispatcher error: #{error.class.name}: #{error}\n" +
47
- error.backtrace.join("\n")
44
+ "Dispatcher error: #{e.class.name}: #{e}\n" +
45
+ e.backtrace.join("\n")
48
46
  end
49
47
 
50
- raise error if config[:raise_exceptions]
48
+ raise e if config[:raise_exceptions]
51
49
  ensure
52
50
  if defined?(ActiveRecord) &&
53
51
  ActiveRecord::Base.connection_pool.active_connection?
@@ -3,7 +3,9 @@
3
3
  module Msgr
4
4
  module Logging
5
5
  def log(level)
6
+ # rubocop:disable Style/SafeNavigation - Msgr.logger can be false
6
7
  Msgr.logger.send(level) { "#{log_name} #{yield}" } if Msgr.logger
8
+ # rubocop:enable all
7
9
  end
8
10
 
9
11
  def log_name
@@ -11,8 +11,7 @@ module Msgr
11
11
  @payload = payload
12
12
  @route = route
13
13
 
14
- # rubocop:disable Style/GuardClause
15
- if content_type == 'application/json'
14
+ if content_type == 'application/json' # rubocop:disable Style/GuardClause
16
15
  @payload = JSON.parse(payload)
17
16
  @payload.symbolize_keys! if @payload.respond_to? :symbolize_keys!
18
17
  end
@@ -4,6 +4,11 @@ module Msgr
4
4
  class Railtie < ::Rails::Railtie
5
5
  config.msgr = ActiveSupport::OrderedOptions.new
6
6
 
7
+ DEFAULT_OPTIONS = {
8
+ checkcredentials: true,
9
+ routing_file: "#{Rails.root}/config/msgr.rb"
10
+ }.freeze
11
+
7
12
  if File.exist?("#{Rails.root}/app/consumers")
8
13
  config.autoload_paths << File.expand_path("#{Rails.root}/app/consumers")
9
14
  end
@@ -12,84 +17,24 @@ module Msgr
12
17
  app.config.msgr.logger ||= Rails.logger
13
18
  end
14
19
 
15
- # Start msgr
16
20
  initializer 'msgr.start' do
17
21
  config.after_initialize do |app|
18
22
  Msgr.logger = app.config.msgr.logger
19
23
 
20
- self.class.load app.config.msgr
24
+ self.class.load(app.config_for(:rabbitmq))
21
25
  end
22
26
  end
23
27
 
24
- class << self
25
- def load(rails_config)
26
- cfg = parse_config load_config rails_config
27
- return unless cfg # no config given -> does not load Msgr
28
-
29
- Msgr.config = cfg
30
- Msgr.client.connect if cfg[:checkcredentials]
31
- Msgr.start if cfg[:autostart]
32
- end
33
-
34
- def parse_config(cfg)
35
- unless cfg.is_a? Hash
36
- Rails.logger.warn '[Msgr] Could not load rabbitmq config: Config must be a Hash'
37
- return nil
38
- end
39
-
40
- unless cfg[Rails.env].is_a?(Hash)
41
- Rails.logger.warn "Could not load rabbitmq config for environment \"#{Rails.env}\": is not a Hash"
42
- return nil
43
- end
44
-
45
- cfg = HashWithIndifferentAccess.new cfg[Rails.env]
46
- unless cfg[:uri]
47
- raise ArgumentError.new('Could not load rabbitmq environment config: URI missing.')
48
- end
49
-
50
- case cfg[:autostart]
51
- when true, 'true', 'enabled'
52
- cfg[:autostart] = true
53
- when false, 'false', 'disabled', nil
54
- cfg[:autostart] = false
55
- else
56
- raise ArgumentError.new("Invalid value for rabbitmq config autostart: \"#{cfg[:autostart]}\"")
57
- end
58
-
59
- case cfg[:checkcredentials]
60
- when true, 'true', 'enabled', nil
61
- cfg[:checkcredentials] = true
62
- when false, 'false', 'disabled'
63
- cfg[:checkcredentials] = false
64
- else
65
- raise ArgumentError.new("Invalid value for rabbitmq config checkcredentials: \"#{cfg[:checkcredentials]}\"")
66
- end
67
-
68
- case cfg[:raise_exceptions]
69
- when true, 'true', 'enabled'
70
- cfg[:raise_exceptions] = true
71
- when false, 'false', 'disabled', nil
72
- cfg[:raise_exceptions] = false
73
- else
74
- raise ArgumentError.new("Invalid value for rabbitmq config raise_exceptions: \"#{cfg[:raise_exceptions]}\"")
75
- end
76
-
77
- cfg[:routing_file] ||= Rails.root.join('config/msgr.rb').to_s
78
- cfg
79
- end
80
-
81
- def load_config(options)
82
- if options.rabbitmq_config || !Rails.application.respond_to?(:config_for)
83
- load_file options.rabbitmq_config || Rails.root.join('config', 'rabbitmq.yml')
84
- else
85
- conf = Rails.application.config_for :rabbitmq
28
+ rake_tasks do
29
+ load File.expand_path('tasks/msgr/drain.rake', __dir__)
30
+ end
86
31
 
87
- {Rails.env.to_s => conf}
88
- end
89
- end
32
+ class << self
33
+ def load(config)
34
+ config = DEFAULT_OPTIONS.merge(config)
90
35
 
91
- def load_file(path)
92
- YAML.safe_load ERB.new(File.read(path)).result
36
+ Msgr.config = config
37
+ Msgr.client.connect if config.fetch(:checkcredentials)
93
38
  end
94
39
  end
95
40
  end