hutch 0.25.0.pre.rc1-java → 2.0.0.rc1-java

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 (63) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +402 -5
  3. data/LICENSE +1 -0
  4. data/README.md +157 -47
  5. data/bin/ci/before_build.sh +20 -0
  6. data/bin/ci/before_build_docker.sh +15 -0
  7. data/bin/ci/install_on_debian.sh +46 -0
  8. data/lib/hutch/acknowledgements/nack_on_all_failures.rb +1 -1
  9. data/lib/hutch/adapters/bunny.rb +29 -1
  10. data/lib/hutch/adapters/march_hare.rb +24 -0
  11. data/lib/hutch/broker.rb +122 -36
  12. data/lib/hutch/cli.rb +23 -12
  13. data/lib/hutch/config.rb +45 -13
  14. data/lib/hutch/consumer.rb +63 -4
  15. data/lib/hutch/error_handlers/airbrake.rb +20 -2
  16. data/lib/hutch/error_handlers/base.rb +15 -0
  17. data/lib/hutch/error_handlers/bugsnag.rb +30 -0
  18. data/lib/hutch/error_handlers/honeybadger.rb +11 -3
  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 +13 -11
  22. data/lib/hutch/error_handlers.rb +2 -1
  23. data/lib/hutch/publisher.rb +1 -1
  24. data/lib/hutch/serializers/json.rb +3 -3
  25. data/lib/hutch/tracers/datadog.rb +18 -0
  26. data/lib/hutch/tracers.rb +1 -1
  27. data/lib/hutch/version.rb +1 -2
  28. data/lib/hutch/waiter.rb +1 -1
  29. data/lib/hutch/worker.rb +74 -8
  30. data/lib/hutch.rb +8 -4
  31. data/lib/yard-settings/yard-settings.rb +2 -2
  32. data/spec/hutch/broker_spec.rb +222 -10
  33. data/spec/hutch/cli_spec.rb +25 -9
  34. data/spec/hutch/config_spec.rb +84 -12
  35. data/spec/hutch/consumer_spec.rb +93 -4
  36. data/spec/hutch/error_handlers/airbrake_spec.rb +23 -1
  37. data/spec/hutch/error_handlers/bugsnag_spec.rb +56 -0
  38. data/spec/hutch/error_handlers/honeybadger_spec.rb +22 -1
  39. data/spec/hutch/error_handlers/logger_spec.rb +12 -1
  40. data/spec/hutch/error_handlers/rollbar_spec.rb +45 -0
  41. data/spec/hutch/error_handlers/sentry_spec.rb +27 -2
  42. data/spec/hutch/message_spec.rb +1 -1
  43. data/spec/hutch/tracers/datadog_spec.rb +46 -0
  44. data/spec/hutch/waiter_spec.rb +2 -2
  45. data/spec/hutch/worker_spec.rb +139 -25
  46. data/spec/integration/channel_recovery_spec.rb +187 -0
  47. data/spec/integration/publish_consume_spec.rb +47 -0
  48. metadata +35 -61
  49. data/.gitignore +0 -10
  50. data/.rspec +0 -1
  51. data/.travis.yml +0 -19
  52. data/.yardopts +0 -5
  53. data/Gemfile +0 -33
  54. data/Guardfile +0 -14
  55. data/Rakefile +0 -21
  56. data/examples/consumer.rb +0 -13
  57. data/examples/producer.rb +0 -10
  58. data/hutch.gemspec +0 -29
  59. data/lib/hutch/error_handlers/opbeat.rb +0 -24
  60. data/lib/hutch/tracers/opbeat.rb +0 -37
  61. data/spec/hutch/error_handlers/opbeat_spec.rb +0 -22
  62. data/spec/spec_helper.rb +0 -42
  63. data/spec/tracers/opbeat_spec.rb +0 -44
data/lib/hutch/broker.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'uri'
1
2
  require 'active_support/core_ext/object/blank'
2
3
 
3
4
  require 'carrot-top'
@@ -11,6 +12,24 @@ module Hutch
11
12
 
12
13
  attr_accessor :connection, :channel, :exchange, :api_client
13
14
 
15
+
16
+ DEFAULT_AMQP_PORT =
17
+ case RUBY_ENGINE
18
+ when "jruby" then
19
+ com.rabbitmq.client.ConnectionFactory::DEFAULT_AMQP_PORT
20
+ when "ruby" then
21
+ AMQ::Protocol::DEFAULT_PORT
22
+ end
23
+
24
+ DEFAULT_AMQPS_PORT =
25
+ case RUBY_ENGINE
26
+ when "jruby" then
27
+ com.rabbitmq.client.ConnectionFactory::DEFAULT_AMQP_OVER_SSL_PORT
28
+ when "ruby" then
29
+ AMQ::Protocol::TLS_PORT
30
+ end
31
+
32
+
14
33
  # @param config [nil,Hash] Configuration override
15
34
  def initialize(config = nil)
16
35
  @config = config || Hutch::Config
@@ -95,6 +114,8 @@ module Hutch
95
114
  logger.info 'enabling publisher confirms'
96
115
  ch.confirm_select
97
116
  end
117
+
118
+ connection.install_channel_recovery(ch)
98
119
  end
99
120
  end
100
121
 
@@ -102,13 +123,23 @@ module Hutch
102
123
  @channel = open_channel
103
124
  end
104
125
 
126
+ # Closing the old channel makes RabbitMQ forget the consumers on it
127
+ # and requeue their unacknowledged deliveries.
128
+ def replace_channel!
129
+ close_consumer_channel
130
+ open_channel!
131
+ declare_exchange!
132
+ declare_publisher!
133
+ end
134
+
105
135
  def declare_exchange(ch = channel)
106
136
  exchange_name = @config[:mq_exchange]
137
+ exchange_type = @config[:mq_exchange_type]
107
138
  exchange_options = { durable: true }.merge(@config[:mq_exchange_options])
108
139
  logger.info "using topic exchange '#{exchange_name}'"
109
140
 
110
141
  with_bunny_precondition_handler('exchange') do
111
- ch.topic(exchange_name, exchange_options)
142
+ Adapter.new_exchange(ch, exchange_type, exchange_name, exchange_options)
112
143
  end
113
144
  end
114
145
 
@@ -151,24 +182,35 @@ module Hutch
151
182
  @config[:tracer] && @config[:tracer] != Hutch::Tracers::NullTracer
152
183
  end
153
184
 
154
- # Create / get a durable queue and apply namespace if it exists.
155
- def queue(name, arguments = {})
185
+ # Create / get a durable queue.
186
+ def queue(name, options = {})
156
187
  with_bunny_precondition_handler('queue') do
157
- namespace = @config[:namespace].to_s.downcase.gsub(/[^-_:\.\w]/, "")
158
- name = name.prepend(namespace + ":") if namespace.present?
159
- channel.queue(name, durable: true, arguments: arguments)
188
+ channel.queue(name, **options)
160
189
  end
161
190
  end
162
191
 
192
+ def queue_exists?(name)
193
+ connection.queue_exists?(name)
194
+ end
195
+
196
+ # Apply the configured namespace prefix to a queue name.
197
+ def namespaced_queue_name(name)
198
+ namespace = @config[:namespace].to_s.downcase.gsub(/[^-_:\.\w]/, "")
199
+ namespace.present? ? "#{namespace}:#{name}" : name
200
+ end
201
+
163
202
  # Return a mapping of queue names to the routing keys they're bound to.
164
203
  def bindings
165
204
  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]
205
+
206
+ filtered = api_client.bindings.
207
+ reject { |b| b['destination'] == b['routing_key'] }.
208
+ select { |b| b['source'] == @config[:mq_exchange] && b['vhost'] == vhost }
209
+
210
+ filtered.each do |binding|
170
211
  results[binding['destination']] << binding['routing_key']
171
212
  end
213
+
172
214
  results
173
215
  end
174
216
 
@@ -176,8 +218,8 @@ module Hutch
176
218
  def unbind_redundant_bindings(queue, routing_keys)
177
219
  return unless http_api_use_enabled?
178
220
 
179
- bindings.each do |dest, keys|
180
- next unless dest == queue.name
221
+ filtered = bindings.select { |dest, keys| dest == queue.name }
222
+ filtered.each do |dest, keys|
181
223
  keys.reject { |key| routing_keys.include?(key) }.each do |key|
182
224
  logger.debug "removing redundant binding #{queue.name} <--> #{key}"
183
225
  queue.unbind(exchange, routing_key: key)
@@ -202,29 +244,26 @@ module Hutch
202
244
  if defined?(JRUBY_VERSION)
203
245
  channel.close
204
246
  else
205
- # Enqueue a failing job that kills the consumer loop
206
- channel_work_pool.shutdown
207
- # Give `timeout` seconds to jobs that are still being processed
208
- channel_work_pool.join(@config[:graceful_exit_timeout])
209
- # If after `timeout` they are still running, they are killed
210
- channel_work_pool.kill
247
+ drain_consumer_work_pool
211
248
  end
212
249
  end
213
250
 
214
- def requeue(delivery_tag)
215
- channel.reject(delivery_tag, true)
251
+ # Delivery tags are scoped to their channel: a tag from a replaced one
252
+ # is dropped, the server has requeued that delivery anyway.
253
+ def requeue(delivery_tag, channel: self.channel)
254
+ channel.reject(delivery_tag, true) if current_channel?(channel)
216
255
  end
217
256
 
218
- def reject(delivery_tag, requeue=false)
219
- channel.reject(delivery_tag, requeue)
257
+ def reject(delivery_tag, requeue = false, channel: self.channel)
258
+ channel.reject(delivery_tag, requeue) if current_channel?(channel)
220
259
  end
221
260
 
222
- def ack(delivery_tag)
223
- channel.ack(delivery_tag, false)
261
+ def ack(delivery_tag, channel: self.channel)
262
+ channel.ack(delivery_tag, false) if current_channel?(channel)
224
263
  end
225
264
 
226
- def nack(delivery_tag)
227
- channel.nack(delivery_tag, false, false)
265
+ def nack(delivery_tag, channel: self.channel)
266
+ channel.nack(delivery_tag, false, false) if current_channel?(channel)
228
267
  end
229
268
 
230
269
  def publish(*args)
@@ -246,15 +285,39 @@ module Hutch
246
285
 
247
286
  private
248
287
 
288
+ def current_channel?(ch)
289
+ ch.equal?(channel)
290
+ end
291
+
292
+ def close_consumer_channel
293
+ return unless @channel && @channel.open?
294
+
295
+ drain_consumer_work_pool unless defined?(JRUBY_VERSION)
296
+ @channel.close
297
+ rescue Hutch::Adapter::ChannelAlreadyClosed
298
+ # The server closed the channel first: same outcome.
299
+ end
300
+
301
+ # Bunny kills the consumer work pool when the channel closes. Draining
302
+ # first gives the running handlers `graceful_exit_timeout` to finish.
303
+ def drain_consumer_work_pool
304
+ channel_work_pool.shutdown
305
+ channel_work_pool.join(@config[:graceful_exit_timeout])
306
+ channel_work_pool.kill
307
+ end
308
+
309
+ Config = Struct.new(:host, :port, :username, :password, :ssl, :protocol, :sanitized_uri)
310
+ private_constant :Config
311
+
249
312
  def api_config
250
- @api_config ||= OpenStruct.new.tap do |config|
313
+ @api_config ||= Config.new.tap do |config|
251
314
  config.host = @config[:mq_api_host]
252
315
  config.port = @config[:mq_api_port]
253
316
  config.username = @config[:mq_username]
254
317
  config.password = @config[:mq_password]
255
318
  config.ssl = @config[:mq_api_ssl]
256
319
  config.protocol = config.ssl ? "https://" : "http://"
257
- config.sanitized_uri = "#{config.protocol}#{config.username}@#{config.host}:#{config.port}/"
320
+ config.sanitized_uri = "#{config.protocol}#{URI.encode_uri_component(config.username.to_s)}@#{config.host}:#{config.port}/"
258
321
  end
259
322
  end
260
323
 
@@ -264,7 +327,8 @@ module Hutch
264
327
  {}.tap do |params|
265
328
  params[:host] = @config[:mq_host]
266
329
  params[:port] = @config[:mq_port]
267
- params[:vhost] = @config[:mq_vhost].presence || Hutch::Adapter::DEFAULT_VHOST
330
+ params[:vhost] = vhost
331
+ params[:auth_mechanism] = @config[:mq_auth_mechanism]
268
332
  params[:username] = @config[:mq_username]
269
333
  params[:password] = @config[:mq_password]
270
334
  params[:tls] = @config[:mq_tls]
@@ -275,13 +339,15 @@ module Hutch
275
339
  params[:tls_ca_certificates] = @config[:mq_tls_ca_certificates]
276
340
  end
277
341
  params[:heartbeat] = @config[:heartbeat]
342
+ params[:client_properties] = @config[:mq_client_properties]
343
+ params[:connection_name] = @config[:connection_name]
278
344
  params[:connection_timeout] = @config[:connection_timeout]
279
345
  params[:read_timeout] = @config[:read_timeout]
280
346
  params[:write_timeout] = @config[:write_timeout]
281
347
 
282
348
 
283
- params[:automatically_recover] = true
284
- params[:network_recovery_interval] = 1
349
+ params[:automatically_recover] = @config[:automatically_recover]
350
+ params[:network_recovery_interval] = @config[:network_recovery_interval]
285
351
 
286
352
  params[:logger] = @config[:client_logger] if @config[:client_logger]
287
353
  end
@@ -292,18 +358,38 @@ module Hutch
292
358
 
293
359
  u = URI.parse(@config[:uri])
294
360
 
361
+ @config[:mq_tls] = u.scheme == 'amqps'
295
362
  @config[:mq_host] = u.host
296
- @config[:mq_port] = u.port
297
- @config[:mq_vhost] = u.path.sub(/^\//, "")
298
- @config[:mq_username] = u.user
299
- @config[:mq_password] = u.password
363
+ @config[:mq_port] = u.port || default_mq_port
364
+ @config[:mq_vhost] = parse_vhost(u)
365
+ @config[:mq_username] = u.user && URI.decode_uri_component(u.user)
366
+ @config[:mq_password] = u.password && URI.decode_uri_component(u.password)
367
+ end
368
+
369
+ # Resolved here rather than in #connection_params, so that
370
+ # Config[:mq_vhost] holds the name the HTTP API reports too.
371
+ def parse_vhost(uri)
372
+ URI.decode_uri_component(uri.path.sub(%r{\A/}, "")).presence ||
373
+ Hutch::Adapter::DEFAULT_VHOST
374
+ end
375
+
376
+ # The vhost Hutch connects to. Config[:mq_vhost] can still be blank when it
377
+ # was set directly rather than through a URI, while the broker and its HTTP
378
+ # API call the default vhost `/`.
379
+ def vhost
380
+ @config[:mq_vhost].presence || Hutch::Adapter::DEFAULT_VHOST
381
+ end
382
+
383
+ def default_mq_port
384
+ @config[:mq_tls] ? DEFAULT_AMQPS_PORT : DEFAULT_AMQP_PORT
300
385
  end
301
386
 
302
387
  def sanitized_uri
303
388
  p = connection_params
304
389
  scheme = p[:tls] ? "amqps" : "amqp"
305
390
 
306
- "#{scheme}://#{p[:username]}@#{p[:host]}:#{p[:port]}/#{p[:vhost].sub(/^\//, '')}"
391
+ "#{scheme}://#{URI.encode_uri_component(p[:username].to_s)}" \
392
+ "@#{p[:host]}:#{p[:port]}/#{URI.encode_uri_component(p[:vhost].sub(%r{\A/}, ''))}"
307
393
  end
308
394
 
309
395
  def with_authentication_error_handler
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-ruby 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?(Sentry)
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)
@@ -48,19 +62,9 @@ module Hutch
48
62
  return false
49
63
  ensure
50
64
  # Clean up load path
51
- $LOAD_PATH.pop
65
+ $LOAD_PATH.delete('.')
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)
@@ -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
@@ -220,7 +231,7 @@ module Hutch
220
231
  end
221
232
 
222
233
  def abort_without_file(file, file_description, &block)
223
- abort_with_message("#{file_description} '#{file}' not found") unless File.exists?(file)
234
+ abort_with_message("#{file_description} '#{file}' not found") unless File.exist?(file)
224
235
 
225
236
  yield
226
237
  end
data/lib/hutch/config.rb CHANGED
@@ -49,17 +49,23 @@ 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
 
55
58
  # RabbitMQ username to use.
56
59
  #
57
- # As of RabbitMQ 3.3.0, <tt>guest</tt> can only can connect from localhost.
60
+ # The <tt>guest</tt> user can only connect from localhost.
58
61
  string_setting :mq_username, 'guest'
59
62
 
60
63
  # RabbitMQ password
61
64
  string_setting :mq_password, 'guest'
62
65
 
66
+ # RabbitMQ Auth Mechanism
67
+ string_setting :mq_auth_mechanism, 'PLAIN'
68
+
63
69
  # RabbitMQ URI (takes precedence over MQ username, password, host, port and vhost settings)
64
70
  string_setting :uri, nil
65
71
 
@@ -72,7 +78,7 @@ module Hutch
72
78
  # RabbitMQ HTTP API port
73
79
  number_setting :mq_api_port, 15672
74
80
 
75
- # [RabbitMQ heartbeat timeout](http://rabbitmq.com/heartbeats.html)
81
+ # [RabbitMQ heartbeat timeout](https://www.rabbitmq.com/docs/heartbeats)
76
82
  number_setting :heartbeat, 30
77
83
 
78
84
  # The <tt>basic.qos</tt> prefetch value to use.
@@ -80,6 +86,9 @@ module Hutch
80
86
  # Default: `0`, no limit. See Bunny and RabbitMQ documentation.
81
87
  number_setting :channel_prefetch, 0
82
88
 
89
+ # [Client-Provided Connection Name](https://www.rabbitmq.com/docs/connections#client-provided-names)
90
+ string_setting :connection_name, nil
91
+
83
92
  # Bunny's socket open timeout
84
93
  number_setting :connection_timeout, 11
85
94
 
@@ -89,7 +98,13 @@ module Hutch
89
98
  # Bunny's socket write timeout
90
99
  number_setting :write_timeout, 11
91
100
 
92
- # FIXME: DOCUMENT THIS
101
+ # Bunny's enable/disable network recovery
102
+ boolean_setting :automatically_recover, true
103
+
104
+ # Bunny's reconnect interval
105
+ number_setting :network_recovery_interval, 1
106
+
107
+ # Timeout (in seconds) for consumer threads to finish before being killed during graceful shutdown
93
108
  number_setting :graceful_exit_timeout, 11
94
109
 
95
110
  # Bunny consumer work pool size
@@ -136,6 +151,11 @@ module Hutch
136
151
  # Prefix displayed on the consumers tags.
137
152
  string_setting :consumer_tag_prefix, 'hutch'
138
153
 
154
+ # A namespace to help group your queues
155
+ string_setting :namespace, nil
156
+
157
+ string_setting :group, ''
158
+
139
159
  # Set of all setting keys
140
160
  ALL_KEYS = @boolean_keys + @number_keys + @string_keys
141
161
 
@@ -154,6 +174,7 @@ module Hutch
154
174
  # @return [Hash]
155
175
  def self.default_config
156
176
  @settings_defaults.merge({
177
+ mq_client_properties: {},
157
178
  mq_exchange_options: {},
158
179
  mq_tls_cert: nil,
159
180
  mq_tls_key: nil,
@@ -167,6 +188,7 @@ module Hutch
167
188
  # that will fall back to "nack unconditionally"
168
189
  error_acknowledgements: [],
169
190
  setup_procs: [],
191
+ consumer_groups: {},
170
192
  tracer: Hutch::Tracers::NullTracer,
171
193
  namespace: nil,
172
194
  pidfile: nil,
@@ -181,14 +203,7 @@ module Hutch
181
203
  env_keys_configured.each_with_object({}) {|attr, result|
182
204
  value = ENV[key_for(attr)]
183
205
 
184
- case
185
- when is_bool(attr) || value == 'false'
186
- result[attr] = to_bool(value)
187
- when is_num(attr)
188
- result[attr] = value.to_i
189
- else
190
- result[attr] = value
191
- end
206
+ result[attr] = type_cast(attr, value)
192
207
  }
193
208
  end
194
209
 
@@ -214,7 +229,12 @@ module Hutch
214
229
  end
215
230
 
216
231
  def self.to_bool(value)
217
- !(value.nil? || value == '' || value =~ /^(false|f|no|n|0)$/i || value == false)
232
+ case value
233
+ when nil, false, '', /^(false|f|no|n|0)$/i
234
+ false
235
+ else
236
+ true
237
+ end
218
238
  end
219
239
 
220
240
  def self.is_num(attr)
@@ -223,7 +243,7 @@ module Hutch
223
243
 
224
244
  def self.set(attr, value)
225
245
  check_attr(attr.to_sym)
226
- user_config[attr.to_sym] = value
246
+ user_config[attr.to_sym] = type_cast(attr, value)
227
247
  end
228
248
 
229
249
  class << self
@@ -260,6 +280,18 @@ module Hutch
260
280
  end
261
281
  end
262
282
 
283
+ def self.type_cast(attr, value)
284
+ case
285
+ when is_bool(attr) || value == 'false'
286
+ to_bool(value)
287
+ when is_num(attr)
288
+ value.to_i
289
+ else
290
+ value
291
+ end
292
+ end
293
+ private_class_method :type_cast
294
+
263
295
  def self.define_methods
264
296
  @config.keys.each do |key|
265
297
  define_singleton_method(key) do
@@ -13,11 +13,17 @@ module Hutch
13
13
  end
14
14
 
15
15
  def reject!
16
- broker.reject(delivery_info.delivery_tag)
16
+ @message_rejected = true
17
+ broker.reject(delivery_info.delivery_tag, channel: delivery_info.channel)
17
18
  end
18
19
 
19
20
  def requeue!
20
- broker.requeue(delivery_info.delivery_tag)
21
+ @message_rejected = true
22
+ broker.requeue(delivery_info.delivery_tag, channel: delivery_info.channel)
23
+ end
24
+
25
+ def message_rejected?
26
+ !!@message_rejected
21
27
  end
22
28
 
23
29
  def logger
@@ -29,18 +35,56 @@ module Hutch
29
35
  # wants to subscribe to.
30
36
  def consume(*routing_keys)
31
37
  @routing_keys = self.routing_keys.union(routing_keys)
38
+ # these are opt-in
39
+ @queue_mode = nil
40
+ @queue_type = nil
32
41
  end
33
42
 
43
+ attr_reader :queue_mode, :queue_type, :initial_group_size
44
+
34
45
  # Explicitly set the queue name
35
46
  def queue_name(name)
36
47
  @queue_name = name
37
48
  end
38
49
 
39
- # Allow to specify custom arguments that will be passed when creating the queue.
50
+ # Opt out of the namespace prefix for this consumer's queue
51
+ def without_namespace
52
+ @without_namespace = true
53
+ end
54
+
55
+ def without_namespace?
56
+ !!@without_namespace
57
+ end
58
+
59
+ # Explicitly set the queue mode to 'lazy'
60
+ def lazy_queue
61
+ @queue_mode = 'lazy'
62
+ end
63
+
64
+ # Explicitly set the queue type to 'classic'
65
+ def classic_queue
66
+ @queue_type = 'classic'
67
+ end
68
+
69
+ # Explicitly set the queue type to 'quorum'
70
+ # @param [Hash] options the options params related to quorum queue
71
+ # @option options [Integer] :initial_group_size Initial Replication Factor
72
+ def quorum_queue(options = {})
73
+ @queue_type = 'quorum'
74
+ @initial_group_size = options[:initial_group_size]
75
+ end
76
+
77
+ # Configures an optional argument that will be passed when declaring the queue.
78
+ # Prefer using a policy to this DSL: https://www.rabbitmq.com/parameters.html#policies
40
79
  def arguments(arguments = {})
41
80
  @arguments = arguments
42
81
  end
43
82
 
83
+ # Configures queue options that will be passed when declaring the queue.
84
+ def queue_options(options = {})
85
+ @queue_options = options
86
+ end
87
+
44
88
  # Set custom serializer class, override global value
45
89
  def serializer(name)
46
90
  @serializer = name
@@ -58,7 +102,22 @@ module Hutch
58
102
 
59
103
  # Returns consumer custom arguments.
60
104
  def get_arguments
61
- @arguments || {}
105
+ all_arguments = @arguments || {}
106
+
107
+ all_arguments['x-queue-mode'] = @queue_mode if @queue_mode
108
+ all_arguments['x-queue-type'] = @queue_type if @queue_type
109
+ all_arguments['x-quorum-initial-group-size'] = @initial_group_size if @initial_group_size
110
+
111
+ all_arguments
112
+ end
113
+
114
+ def get_options
115
+ default_options = { durable: true }
116
+
117
+ all_options = default_options.merge(@queue_options || {})
118
+ all_options[:arguments] = get_arguments
119
+
120
+ all_options
62
121
  end
63
122
 
64
123
  # Accessor for the consumer's routing key.
@@ -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
@@ -0,0 +1,30 @@
1
+ require "hutch/logging"
2
+ require "bugsnag"
3
+ require "hutch/error_handlers/base"
4
+
5
+ module Hutch
6
+ module ErrorHandlers
7
+ class Bugsnag < Base
8
+ def handle(properties, payload, consumer, ex)
9
+ message_id = properties.message_id
10
+ prefix = "message(#{message_id || "-"}):"
11
+ logger.error "#{prefix} Logging event to Bugsnag"
12
+ logger.error "#{prefix} #{ex.class} - #{ex.message}"
13
+
14
+ ::Bugsnag.notify(ex) do |report|
15
+ report.add_tab(:hutch, {
16
+ payload: payload,
17
+ consumer: consumer
18
+ })
19
+ end
20
+ end
21
+
22
+ def handle_setup_exception(ex)
23
+ logger.error "Logging setup exception to Bugsnag"
24
+ logger.error "#{ex.class} - #{ex.message}"
25
+
26
+ ::Bugsnag.notify(ex)
27
+ end
28
+ end
29
+ end
30
+ end