msgr 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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