hutch 0.24.0 → 1.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 (41) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +18 -13
  3. data/CHANGELOG.md +227 -4
  4. data/Gemfile +3 -3
  5. data/LICENSE +1 -0
  6. data/README.md +94 -94
  7. data/bin/ci/before_build.sh +20 -0
  8. data/bin/ci/install_on_debian.sh +17 -0
  9. data/hutch.gemspec +5 -5
  10. data/lib/hutch.rb +8 -4
  11. data/lib/hutch/broker.rb +37 -10
  12. data/lib/hutch/cli.rb +22 -11
  13. data/lib/hutch/config.rb +12 -0
  14. data/lib/hutch/consumer.rb +32 -2
  15. data/lib/hutch/error_handlers.rb +1 -1
  16. data/lib/hutch/error_handlers/airbrake.rb +20 -2
  17. data/lib/hutch/error_handlers/base.rb +15 -0
  18. data/lib/hutch/error_handlers/honeybadger.rb +28 -14
  19. data/lib/hutch/error_handlers/logger.rb +7 -2
  20. data/lib/hutch/error_handlers/rollbar.rb +28 -0
  21. data/lib/hutch/error_handlers/sentry.rb +9 -2
  22. data/lib/hutch/publisher.rb +1 -1
  23. data/lib/hutch/tracers.rb +0 -1
  24. data/lib/hutch/version.rb +1 -2
  25. data/lib/hutch/waiter.rb +1 -1
  26. data/lib/hutch/worker.rb +30 -1
  27. data/spec/hutch/broker_spec.rb +34 -0
  28. data/spec/hutch/cli_spec.rb +13 -0
  29. data/spec/hutch/consumer_spec.rb +82 -4
  30. data/spec/hutch/error_handlers/airbrake_spec.rb +19 -0
  31. data/spec/hutch/error_handlers/honeybadger_spec.rb +22 -1
  32. data/spec/hutch/error_handlers/logger_spec.rb +11 -0
  33. data/spec/hutch/error_handlers/rollbar_spec.rb +45 -0
  34. data/spec/hutch/error_handlers/sentry_spec.rb +15 -0
  35. data/spec/hutch/waiter_spec.rb +2 -2
  36. data/spec/hutch/worker_spec.rb +1 -1
  37. metadata +22 -17
  38. data/lib/hutch/error_handlers/opbeat.rb +0 -24
  39. data/lib/hutch/tracers/opbeat.rb +0 -37
  40. data/spec/hutch/error_handlers/opbeat_spec.rb +0 -22
  41. data/spec/tracers/opbeat_spec.rb +0 -44
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env sh
2
+
3
+ CTL=${BUNNY_RABBITMQCTL:-"sudo rabbitmqctl"}
4
+ PLUGINS=${BUNNY_RABBITMQ_PLUGINS:-"sudo rabbitmq-plugins"}
5
+
6
+ echo "Will use rabbitmqctl at ${CTL}"
7
+ echo "Will use rabbitmq-plugins at ${PLUGINS}"
8
+
9
+ $PLUGINS enable rabbitmq_management
10
+
11
+ sleep 3
12
+
13
+ # guest:guest has full access to /
14
+ $CTL add_vhost /
15
+ $CTL add_user guest guest
16
+ $CTL set_permissions -p / guest ".*" ".*" ".*"
17
+
18
+ # Reduce retention policy for faster publishing of stats
19
+ $CTL eval 'supervisor2:terminate_child(rabbit_mgmt_sup_sup, rabbit_mgmt_sup), application:set_env(rabbitmq_management, sample_retention_policies, [{global, [{605, 1}]}, {basic, [{605, 1}]}, {detailed, [{10, 1}]}]), rabbit_mgmt_sup_sup:start_child().' || true
20
+ $CTL eval 'supervisor2:terminate_child(rabbit_mgmt_agent_sup_sup, rabbit_mgmt_agent_sup), application:set_env(rabbitmq_management_agent, sample_retention_policies, [{global, [{605, 1}]}, {basic, [{605, 1}]}, {detailed, [{10, 1}]}]), rabbit_mgmt_agent_sup_sup:start_child().' || true
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+
3
+ sudo apt-get install -y wget
4
+ wget -O - "https://github.com/rabbitmq/signing-keys/releases/download/2.0/rabbitmq-release-signing-key.asc" | sudo apt-key add -
5
+
6
+ sudo tee /etc/apt/sources.list.d/bintray.rabbitmq.list <<EOF
7
+ deb https://dl.bintray.com/rabbitmq-erlang/debian bionic erlang
8
+ deb https://dl.bintray.com/rabbitmq/debian bionic main
9
+ EOF
10
+
11
+ sudo apt-get update -y
12
+ sudo apt-get upgrade -y
13
+ sudo apt-get install -y rabbitmq-server
14
+
15
+ sudo service rabbitmq-server start
16
+
17
+ until sudo lsof -i:5672; do echo "Waiting for RabbitMQ to start..."; sleep 1; done
data/hutch.gemspec CHANGED
@@ -3,21 +3,21 @@ require File.expand_path('../lib/hutch/version', __FILE__)
3
3
  Gem::Specification.new do |gem|
4
4
  if defined?(JRUBY_VERSION)
5
5
  gem.platform = 'java'
6
- gem.add_runtime_dependency 'march_hare', '>= 2.22.0'
6
+ gem.add_runtime_dependency 'march_hare', '>= 3.0.0'
7
7
  else
8
8
  gem.platform = Gem::Platform::RUBY
9
- gem.add_runtime_dependency 'bunny', '>= 2.6.3'
9
+ gem.add_runtime_dependency 'bunny', '>= 2.15', '< 2.16'
10
10
  end
11
11
  gem.add_runtime_dependency 'carrot-top', '~> 0.0.7'
12
- gem.add_runtime_dependency 'multi_json', '~> 1.12'
13
- gem.add_runtime_dependency 'activesupport', '>= 4.2', '< 6'
12
+ gem.add_runtime_dependency 'multi_json', '~> 1.14'
13
+ gem.add_runtime_dependency 'activesupport', '>= 4.2', '< 7'
14
14
 
15
15
  gem.name = 'hutch'
16
16
  gem.summary = 'Easy inter-service communication using RabbitMQ.'
17
17
  gem.description = 'Hutch is a Ruby library for enabling asynchronous ' \
18
18
  'inter-service communication using RabbitMQ.'
19
19
  gem.version = Hutch::VERSION.dup
20
- gem.required_ruby_version = '>= 2.0'
20
+ gem.required_ruby_version = '>= 2.2'
21
21
  gem.authors = ['Harry Marr']
22
22
  gem.email = ['developers@gocardless.com']
23
23
  gem.homepage = 'https://github.com/gocardless/hutch'
data/lib/hutch.rb CHANGED
@@ -14,6 +14,8 @@ require 'hutch/exceptions'
14
14
  require 'hutch/tracers'
15
15
 
16
16
  module Hutch
17
+ @@connection_mutex = Mutex.new
18
+
17
19
  def self.register_consumer(consumer)
18
20
  self.consumers << consumer
19
21
  end
@@ -40,10 +42,12 @@ module Hutch
40
42
  # @param config [Hash] Configuration
41
43
  # @option options [Boolean] :enable_http_api_use
42
44
  def self.connect(options = {}, config = Hutch::Config)
43
- return if connected?
44
-
45
- @broker = Hutch::Broker.new(config)
46
- @broker.connect(options)
45
+ @@connection_mutex.synchronize do
46
+ unless connected?
47
+ @broker = Hutch::Broker.new(config)
48
+ @broker.connect(options)
49
+ end
50
+ end
47
51
  end
48
52
 
49
53
  def self.disconnect
data/lib/hutch/broker.rb CHANGED
@@ -11,6 +11,24 @@ module Hutch
11
11
 
12
12
  attr_accessor :connection, :channel, :exchange, :api_client
13
13
 
14
+
15
+ DEFAULT_AMQP_PORT =
16
+ case RUBY_ENGINE
17
+ when "jruby" then
18
+ com.rabbitmq.client.ConnectionFactory::DEFAULT_AMQP_PORT
19
+ when "ruby" then
20
+ AMQ::Protocol::DEFAULT_PORT
21
+ end
22
+
23
+ DEFAULT_AMQPS_PORT =
24
+ case RUBY_ENGINE
25
+ when "jruby" then
26
+ com.rabbitmq.client.ConnectionFactory::DEFAULT_AMQP_OVER_SSL_PORT
27
+ when "ruby" then
28
+ AMQ::Protocol::TLS_PORT
29
+ end
30
+
31
+
14
32
  # @param config [nil,Hash] Configuration override
15
33
  def initialize(config = nil)
16
34
  @config = config || Hutch::Config
@@ -104,11 +122,12 @@ module Hutch
104
122
 
105
123
  def declare_exchange(ch = channel)
106
124
  exchange_name = @config[:mq_exchange]
125
+ exchange_type = @config[:mq_exchange_type]
107
126
  exchange_options = { durable: true }.merge(@config[:mq_exchange_options])
108
127
  logger.info "using topic exchange '#{exchange_name}'"
109
128
 
110
129
  with_bunny_precondition_handler('exchange') do
111
- ch.topic(exchange_name, exchange_options)
130
+ Bunny::Exchange.new(ch, exchange_type, exchange_name, exchange_options)
112
131
  end
113
132
  end
114
133
 
@@ -163,12 +182,15 @@ module Hutch
163
182
  # Return a mapping of queue names to the routing keys they're bound to.
164
183
  def bindings
165
184
  results = Hash.new { |hash, key| hash[key] = [] }
166
- api_client.bindings.each do |binding|
167
- next if binding['destination'] == binding['routing_key']
168
- next unless binding['source'] == @config[:mq_exchange]
169
- next unless binding['vhost'] == @config[:mq_vhost]
185
+
186
+ filtered = api_client.bindings.
187
+ reject { |b| b['destination'] == b['routing_key'] }.
188
+ select { |b| b['source'] == @config[:mq_exchange] && b['vhost'] == @config[:mq_vhost] }
189
+
190
+ filtered.each do |binding|
170
191
  results[binding['destination']] << binding['routing_key']
171
192
  end
193
+
172
194
  results
173
195
  end
174
196
 
@@ -176,8 +198,8 @@ module Hutch
176
198
  def unbind_redundant_bindings(queue, routing_keys)
177
199
  return unless http_api_use_enabled?
178
200
 
179
- bindings.each do |dest, keys|
180
- next unless dest == queue.name
201
+ filtered = bindings.select { |dest, keys| dest == queue.name }
202
+ filtered.each do |dest, keys|
181
203
  keys.reject { |key| routing_keys.include?(key) }.each do |key|
182
204
  logger.debug "removing redundant binding #{queue.name} <--> #{key}"
183
205
  queue.unbind(exchange, routing_key: key)
@@ -280,8 +302,8 @@ module Hutch
280
302
  params[:write_timeout] = @config[:write_timeout]
281
303
 
282
304
 
283
- params[:automatically_recover] = true
284
- params[:network_recovery_interval] = 1
305
+ params[:automatically_recover] = @config[:automatically_recover]
306
+ params[:network_recovery_interval] = @config[:network_recovery_interval]
285
307
 
286
308
  params[:logger] = @config[:client_logger] if @config[:client_logger]
287
309
  end
@@ -292,13 +314,18 @@ module Hutch
292
314
 
293
315
  u = URI.parse(@config[:uri])
294
316
 
317
+ @config[:mq_tls] = u.scheme == 'amqps'
295
318
  @config[:mq_host] = u.host
296
- @config[:mq_port] = u.port
319
+ @config[:mq_port] = u.port || default_mq_port
297
320
  @config[:mq_vhost] = u.path.sub(/^\//, "")
298
321
  @config[:mq_username] = u.user
299
322
  @config[:mq_password] = u.password
300
323
  end
301
324
 
325
+ def default_mq_port
326
+ @config[:mq_tls] ? DEFAULT_AMQPS_PORT : DEFAULT_AMQP_PORT
327
+ end
328
+
302
329
  def sanitized_uri
303
330
  p = connection_params
304
331
  scheme = p[:tls] ? "amqps" : "amqp"
data/lib/hutch/cli.rb CHANGED
@@ -33,6 +33,20 @@ module Hutch
33
33
  def load_app
34
34
  # Try to load a Rails app in the current directory
35
35
  load_rails_app('.') if Hutch::Config.autoload_rails
36
+ set_up_code_paths!
37
+
38
+ # Because of the order things are required when we run the Hutch binary
39
+ # in hutch/bin, the Sentry Raven gem gets required **after** the error
40
+ # handlers are set up. Due to this, we never got any Sentry notifications
41
+ # when an error occurred in any of the consumers.
42
+ if defined?(Raven)
43
+ Hutch::Config[:error_handlers] << Hutch::ErrorHandlers::Sentry.new
44
+ end
45
+
46
+ true
47
+ end
48
+
49
+ def set_up_code_paths!
36
50
  Hutch::Config.require_paths.each do |path|
37
51
  # See if each path is a Rails app. If so, try to load it.
38
52
  next if load_rails_app(path)
@@ -51,16 +65,6 @@ module Hutch
51
65
  $LOAD_PATH.pop
52
66
  end
53
67
  end
54
-
55
- # Because of the order things are required when we run the Hutch binary
56
- # in hutch/bin, the Sentry Raven gem gets required **after** the error
57
- # handlers are set up. Due to this, we never got any Sentry notifications
58
- # when an error occurred in any of the consumers.
59
- if defined?(Raven)
60
- Hutch::Config[:error_handlers] << Hutch::ErrorHandlers::Sentry.new
61
- end
62
-
63
- true
64
68
  end
65
69
 
66
70
  def load_rails_app(path)
@@ -72,8 +76,8 @@ module Hutch
72
76
  end
73
77
  rails_path = File.expand_path(File.join(path, 'config/environment.rb'))
74
78
  if is_rails_app && File.exist?(rails_path)
75
- logger.info "found rails project (#{path}), booting app"
76
79
  ENV['RACK_ENV'] ||= ENV['RAILS_ENV'] || 'development'
80
+ logger.info "found rails project (#{path}), booting app in #{ENV['RACK_ENV']} environment"
77
81
  require rails_path
78
82
  ::Rails.application.eager_load!
79
83
  return true
@@ -90,6 +94,9 @@ module Hutch
90
94
  @worker.run
91
95
  :success
92
96
  rescue ConnectionError, AuthenticationError, WorkerSetupError => ex
97
+ Hutch::Config[:error_handlers].each do |backend|
98
+ backend.handle_setup_exception(ex)
99
+ end
93
100
  logger.fatal ex.message
94
101
  :error
95
102
  end
@@ -189,6 +196,10 @@ module Hutch
189
196
  Hutch::Config.pidfile = pidfile
190
197
  end
191
198
 
199
+ opts.on('--only-group GROUP', 'Load only consumers in this group') do |group|
200
+ Hutch::Config.group = group
201
+ end
202
+
192
203
  opts.on('--version', 'Print the version and exit') do
193
204
  puts "hutch v#{VERSION}"
194
205
  exit 0
data/lib/hutch/config.rb CHANGED
@@ -49,6 +49,9 @@ module Hutch
49
49
  # RabbitMQ Exchange to use for publishing
50
50
  string_setting :mq_exchange, 'hutch'
51
51
 
52
+ # RabbitMQ Exchange type to use for publishing
53
+ string_setting :mq_exchange_type, 'topic'
54
+
52
55
  # RabbitMQ vhost to use
53
56
  string_setting :mq_vhost, '/'
54
57
 
@@ -89,6 +92,12 @@ module Hutch
89
92
  # Bunny's socket write timeout
90
93
  number_setting :write_timeout, 11
91
94
 
95
+ # Bunny's enable/disable network recovery
96
+ boolean_setting :automatically_recover, true
97
+
98
+ # Bunny's reconnect interval
99
+ number_setting :network_recovery_interval, 1
100
+
92
101
  # FIXME: DOCUMENT THIS
93
102
  number_setting :graceful_exit_timeout, 11
94
103
 
@@ -136,6 +145,8 @@ module Hutch
136
145
  # Prefix displayed on the consumers tags.
137
146
  string_setting :consumer_tag_prefix, 'hutch'
138
147
 
148
+ string_setting :group, ''
149
+
139
150
  # Set of all setting keys
140
151
  ALL_KEYS = @boolean_keys + @number_keys + @string_keys
141
152
 
@@ -167,6 +178,7 @@ module Hutch
167
178
  # that will fall back to "nack unconditionally"
168
179
  error_acknowledgements: [],
169
180
  setup_procs: [],
181
+ consumer_groups: {},
170
182
  tracer: Hutch::Tracers::NullTracer,
171
183
  namespace: nil,
172
184
  pidfile: nil,
@@ -29,14 +29,38 @@ module Hutch
29
29
  # wants to subscribe to.
30
30
  def consume(*routing_keys)
31
31
  @routing_keys = self.routing_keys.union(routing_keys)
32
+ # these are opt-in
33
+ @queue_mode = nil
34
+ @queue_type = nil
32
35
  end
33
36
 
37
+ attr_reader :queue_mode, :queue_type, :initial_group_size
38
+
34
39
  # Explicitly set the queue name
35
40
  def queue_name(name)
36
41
  @queue_name = name
37
42
  end
38
43
 
39
- # Allow to specify custom arguments that will be passed when creating the queue.
44
+ # Explicitly set the queue mode to 'lazy'
45
+ def lazy_queue
46
+ @queue_mode = 'lazy'
47
+ end
48
+
49
+ # Explicitly set the queue type to 'classic'
50
+ def classic_queue
51
+ @queue_type = 'classic'
52
+ end
53
+
54
+ # Explicitly set the queue type to 'quorum'
55
+ # @param [Hash] options the options params related to quorum queue
56
+ # @option options [Integer] :initial_group_size Initial Replication Factor
57
+ def quorum_queue(options = {})
58
+ @queue_type = 'quorum'
59
+ @initial_group_size = options[:initial_group_size]
60
+ end
61
+
62
+ # Configures an optional argument that will be passed when declaring the queue.
63
+ # Prefer using a policy to this DSL: https://www.rabbitmq.com/parameters.html#policies
40
64
  def arguments(arguments = {})
41
65
  @arguments = arguments
42
66
  end
@@ -58,7 +82,13 @@ module Hutch
58
82
 
59
83
  # Returns consumer custom arguments.
60
84
  def get_arguments
61
- @arguments || {}
85
+ all_arguments = @arguments || {}
86
+
87
+ all_arguments['x-queue-mode'] = @queue_mode if @queue_mode
88
+ all_arguments['x-queue-type'] = @queue_type if @queue_type
89
+ all_arguments['x-quorum-initial-group-size'] = @initial_group_size if @initial_group_size
90
+
91
+ all_arguments
62
92
  end
63
93
 
64
94
  # Accessor for the consumer's routing key.
@@ -4,6 +4,6 @@ module Hutch
4
4
  autoload :Sentry, 'hutch/error_handlers/sentry'
5
5
  autoload :Honeybadger, 'hutch/error_handlers/honeybadger'
6
6
  autoload :Airbrake, 'hutch/error_handlers/airbrake'
7
- autoload :Opbeat, 'hutch/error_handlers/opbeat'
7
+ autoload :Rollbar, 'hutch/error_handlers/rollbar'
8
8
  end
9
9
  end
@@ -1,10 +1,10 @@
1
1
  require 'hutch/logging'
2
2
  require 'airbrake'
3
+ require 'hutch/error_handlers/base'
3
4
 
4
5
  module Hutch
5
6
  module ErrorHandlers
6
- class Airbrake
7
- include Logging
7
+ class Airbrake < Base
8
8
 
9
9
  def handle(properties, payload, consumer, ex)
10
10
  message_id = properties.message_id
@@ -31,6 +31,24 @@ module Hutch
31
31
  })
32
32
  end
33
33
  end
34
+
35
+ def handle_setup_exception(ex)
36
+ logger.error "Logging setup exception to Airbrake"
37
+ logger.error "#{ex.class} - #{ex.message}"
38
+
39
+ if ::Airbrake.respond_to?(:notify_or_ignore)
40
+ ::Airbrake.notify_or_ignore(ex, {
41
+ error_class: ex.class.name,
42
+ error_message: "#{ ex.class.name }: #{ ex.message }",
43
+ backtrace: ex.backtrace,
44
+ cgi_data: ENV.to_hash,
45
+ })
46
+ else
47
+ ::Airbrake.notify(ex, {
48
+ cgi_data: ENV.to_hash,
49
+ })
50
+ end
51
+ end
34
52
  end
35
53
  end
36
54
  end
@@ -0,0 +1,15 @@
1
+ module Hutch
2
+ module ErrorHandlers
3
+ class Base
4
+ include Logging
5
+
6
+ def handle(properties, payload, consumer, ex)
7
+ raise NotImplementedError.new
8
+ end
9
+
10
+ def handle_setup_exception(ex)
11
+ raise NotImplementedError.new
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,28 +1,42 @@
1
1
  require 'hutch/logging'
2
2
  require 'honeybadger'
3
+ require 'hutch/error_handlers/base'
3
4
 
4
5
  module Hutch
5
6
  module ErrorHandlers
6
- class Honeybadger
7
- include Logging
7
+ # Error handler for the Honeybadger.io service
8
+ class Honeybadger < Base
8
9
 
9
10
  def handle(properties, payload, consumer, ex)
10
11
  message_id = properties.message_id
11
12
  prefix = "message(#{message_id || '-'}):"
12
13
  logger.error "#{prefix} Logging event to Honeybadger"
13
14
  logger.error "#{prefix} #{ex.class} - #{ex.message}"
14
- ::Honeybadger.notify_or_ignore(
15
- :error_class => ex.class.name,
16
- :error_message => "#{ ex.class.name }: #{ ex.message }",
17
- :backtrace => ex.backtrace,
18
- :context => {
19
- :message_id => message_id,
20
- :consumer => consumer
21
- },
22
- :parameters => {
23
- :payload => payload
24
- }
25
- )
15
+ notify_honeybadger(error_class: ex.class.name,
16
+ error_message: "#{ex.class.name}: #{ex.message}",
17
+ backtrace: ex.backtrace,
18
+ context: { message_id: message_id,
19
+ consumer: consumer },
20
+ parameters: { payload: payload })
21
+ end
22
+
23
+ def handle_setup_exception(ex)
24
+ logger.error "Logging setup exception to Honeybadger"
25
+ logger.error "#{ex.class} - #{ex.message}"
26
+ notify_honeybadger(error_class: ex.class.name,
27
+ error_message: "#{ex.class.name}: #{ex.message}",
28
+ backtrace: ex.backtrace)
29
+ end
30
+
31
+ # Wrap API to support 3.0.0+
32
+ #
33
+ # @see https://github.com/honeybadger-io/honeybadger-ruby/blob/master/CHANGELOG.md#300---2017-02-06
34
+ def notify_honeybadger(message)
35
+ if ::Honeybadger.respond_to?(:notify_or_ignore)
36
+ ::Honeybadger.notify_or_ignore(message)
37
+ else
38
+ ::Honeybadger.notify(message)
39
+ end
26
40
  end
27
41
  end
28
42
  end