hutch 0.25.0.pre.rc1-java → 2.0.0-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
@@ -1,11 +1,11 @@
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
7
  # Error handler for the Honeybadger.io service
7
- class Honeybadger
8
- include Logging
8
+ class Honeybadger < Base
9
9
 
10
10
  def handle(properties, payload, consumer, ex)
11
11
  message_id = properties.message_id
@@ -20,6 +20,14 @@ module Hutch
20
20
  parameters: { payload: payload })
21
21
  end
22
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
+
23
31
  # Wrap API to support 3.0.0+
24
32
  #
25
33
  # @see https://github.com/honeybadger-io/honeybadger-ruby/blob/master/CHANGELOG.md#300---2017-02-06
@@ -27,7 +35,7 @@ module Hutch
27
35
  if ::Honeybadger.respond_to?(:notify_or_ignore)
28
36
  ::Honeybadger.notify_or_ignore(message)
29
37
  else
30
- ::Honeybadger.notify(message.merge(force: true))
38
+ ::Honeybadger.notify(message)
31
39
  end
32
40
  end
33
41
  end
@@ -1,9 +1,9 @@
1
1
  require 'hutch/logging'
2
+ require 'hutch/error_handlers/base'
2
3
 
3
4
  module Hutch
4
5
  module ErrorHandlers
5
- class Logger
6
- include Logging
6
+ class Logger < ErrorHandlers::Base
7
7
 
8
8
  def handle(properties, payload, consumer, ex)
9
9
  message_id = properties.message_id
@@ -12,6 +12,11 @@ module Hutch
12
12
  logger.error "#{prefix} #{ex.class} - #{ex.message}"
13
13
  logger.error (['backtrace:'] + ex.backtrace).join("\n")
14
14
  end
15
+
16
+ def handle_setup_exception(ex)
17
+ logger.error "#{ex.class} - #{ex.message}"
18
+ logger.error (['backtrace:'] + ex.backtrace).join("\n")
19
+ end
15
20
  end
16
21
  end
17
22
  end
@@ -0,0 +1,28 @@
1
+ require 'hutch/logging'
2
+ require 'rollbar'
3
+ require 'hutch/error_handlers/base'
4
+
5
+ module Hutch
6
+ module ErrorHandlers
7
+ class Rollbar < 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 Rollbar"
12
+ logger.error "#{prefix} #{ex.class} - #{ex.message}"
13
+
14
+ ::Rollbar.error(ex,
15
+ payload: payload,
16
+ consumer: consumer
17
+ )
18
+ end
19
+
20
+ def handle_setup_exception(ex)
21
+ logger.error "Logging setup exception to Rollbar"
22
+ logger.error "#{ex.class} - #{ex.message}"
23
+
24
+ ::Rollbar.error(ex)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,23 +1,25 @@
1
1
  require 'hutch/logging'
2
- require 'raven'
2
+ require 'sentry-ruby'
3
+ require 'hutch/error_handlers/base'
3
4
 
4
5
  module Hutch
5
6
  module ErrorHandlers
6
- class Sentry
7
- include Logging
8
-
9
- def initialize
10
- unless Raven.respond_to?(:capture_exception)
11
- raise "The Hutch Sentry error handler requires Raven >= 0.4.0"
12
- end
13
- end
14
-
7
+ class Sentry < Base
15
8
  def handle(properties, payload, consumer, ex)
16
9
  message_id = properties.message_id
17
10
  prefix = "message(#{message_id || '-'}):"
18
11
  logger.error "#{prefix} Logging event to Sentry"
19
12
  logger.error "#{prefix} #{ex.class} - #{ex.message}"
20
- Raven.capture_exception(ex, extra: { payload: payload })
13
+ ::Sentry.configure_scope do |scope|
14
+ scope.set_context("payload", JSON.parse(payload))
15
+ end
16
+ ::Sentry.capture_exception(ex)
17
+ end
18
+
19
+ def handle_setup_exception(ex)
20
+ logger.error "Logging setup exception to Sentry"
21
+ logger.error "#{ex.class} - #{ex.message}"
22
+ ::Sentry.capture_exception(ex)
21
23
  end
22
24
  end
23
25
  end
@@ -4,6 +4,7 @@ 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
+ autoload :Bugsnag, 'hutch/error_handlers/bugsnag'
8
9
  end
9
10
  end
@@ -42,7 +42,7 @@ module Hutch
42
42
  private
43
43
 
44
44
  def log_publication(serializer, payload, routing_key)
45
- logger.info {
45
+ logger.debug {
46
46
  spec =
47
47
  if serializer.binary?
48
48
  "#{payload.bytesize} bytes message"
@@ -1,4 +1,4 @@
1
- require 'multi_json'
1
+ require 'json'
2
2
  require 'active_support/core_ext/hash/indifferent_access'
3
3
 
4
4
  module Hutch
@@ -6,11 +6,11 @@ module Hutch
6
6
  class JSON
7
7
 
8
8
  def self.encode(payload)
9
- ::JSON.dump(payload)
9
+ ::JSON.generate(payload)
10
10
  end
11
11
 
12
12
  def self.decode(payload)
13
- ::MultiJson.load(payload).with_indifferent_access
13
+ ::JSON.parse(payload).with_indifferent_access
14
14
  end
15
15
 
16
16
  def self.binary? ; false ; end
@@ -0,0 +1,18 @@
1
+ require 'datadog'
2
+ require 'datadog/auto_instrument'
3
+
4
+ module Hutch
5
+ module Tracers
6
+ class Datadog
7
+ def initialize(klass)
8
+ @klass = klass
9
+ end
10
+
11
+ def handle(message)
12
+ ::Datadog::Tracing.trace(@klass.class.name, continue_from: nil, service: 'hutch', type: 'rabbitmq') do
13
+ @klass.process(message)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/hutch/tracers.rb CHANGED
@@ -2,6 +2,6 @@ module Hutch
2
2
  module Tracers
3
3
  autoload :NullTracer, 'hutch/tracers/null_tracer'
4
4
  autoload :NewRelic, 'hutch/tracers/newrelic'
5
- autoload :Opbeat, 'hutch/tracers/opbeat'
5
+ autoload :Datadog, 'hutch/tracers/datadog'
6
6
  end
7
7
  end
data/lib/hutch/version.rb CHANGED
@@ -1,4 +1,3 @@
1
1
  module Hutch
2
- VERSION = '0.25.0-rc1'.freeze
2
+ VERSION = '2.0.0'.freeze
3
3
  end
4
-
data/lib/hutch/waiter.rb CHANGED
@@ -50,7 +50,7 @@ module Hutch
50
50
  end
51
51
  end
52
52
 
53
- # @raises ContinueProcessingSignals
53
+ # @raise ContinueProcessingSignals
54
54
  def handle_user_signal(sig)
55
55
  case sig
56
56
  when 'USR2' then log_thread_backtraces
data/lib/hutch/worker.rb CHANGED
@@ -11,8 +11,9 @@ module Hutch
11
11
  include Logging
12
12
 
13
13
  def initialize(broker, consumers, setup_procs)
14
- @broker = broker
15
- self.consumers = consumers
14
+ @broker = broker
15
+ @recovery_lock = Mutex.new
16
+ self.consumers = consumers
16
17
  self.setup_procs = setup_procs
17
18
  end
18
19
 
@@ -36,21 +37,62 @@ module Hutch
36
37
  # Set up the queues for each of the worker's consumers.
37
38
  def setup_queues
38
39
  logger.info 'setting up queues'
39
- @consumers.each { |consumer| setup_queue(consumer) }
40
+ vetted = @consumers.reject { |c| group_configured? && group_restricted?(c) }
41
+ vetted.each do |c|
42
+ setup_queue(c)
43
+ end
40
44
  end
41
45
 
42
46
  # Bind a consumer's routing keys to its queue, and set up a subscription to
43
47
  # receive messages sent to the queue.
44
48
  def setup_queue(consumer)
45
- queue = @broker.queue(consumer.get_queue_name, consumer.get_arguments)
49
+ queue_name = consumer.get_queue_name
50
+ queue_name = @broker.namespaced_queue_name(queue_name) unless consumer.without_namespace?
51
+ logger.info "setting up queue: #{queue_name}"
52
+
53
+ queue = @broker.queue(queue_name, consumer.get_options)
46
54
  @broker.bind_queue(queue, consumer.routing_keys)
47
55
 
48
- queue.subscribe(consumer_tag: unique_consumer_tag, manual_ack: true) do |*args|
56
+ on_cancellation = proc { handle_cancellation(queue_name, queue.channel) }
57
+ queue.subscribe(consumer_tag: unique_consumer_tag, manual_ack: true,
58
+ on_cancellation: on_cancellation) do |*args|
49
59
  delivery_info, properties, payload = Hutch::Adapter.decode_message(*args)
50
60
  handle_message(consumer, delivery_info, properties, payload)
51
61
  end
52
62
  end
53
63
 
64
+ # A server-sent `basic.cancel` carries no reason, so a consumer timeout
65
+ # is indistinguishable from a queue deletion. Only the former is recoverable.
66
+ def handle_cancellation(queue_name, cancelled_channel)
67
+ unless @broker.queue_exists?(queue_name)
68
+ logger.error "consumer on queue #{queue_name} was cancelled by the server: the queue no longer exists"
69
+ return
70
+ end
71
+
72
+ logger.warn "consumer on queue #{queue_name} was cancelled by the server, re-subscribing"
73
+ resubscribe_on_a_new_channel(cancelled_channel)
74
+ rescue => ex
75
+ logger.error "consumer re-subscription failed: #{ex.class}: #{ex.message}"
76
+ end
77
+
78
+ # Runs on its own thread: closing the channel kills the consumer work
79
+ # pool this callback runs on. Returns the thread so that tests can join it.
80
+ def resubscribe_on_a_new_channel(cancelled_channel)
81
+ Thread.new do
82
+ begin
83
+ @recovery_lock.synchronize do
84
+ # All consumers share the channel, so only the first cancellation replaces it.
85
+ next unless @broker.channel.equal?(cancelled_channel)
86
+
87
+ @broker.replace_channel!
88
+ setup_queues
89
+ end
90
+ rescue => ex
91
+ logger.error "consumer re-subscription failed: #{ex.class}: #{ex.message}"
92
+ end
93
+ end
94
+ end
95
+
54
96
  # Called internally when a new messages comes in from RabbitMQ. Responsible
55
97
  # for wrapping up the message and passing it to the consumer.
56
98
  def handle_message(consumer, delivery_info, properties, payload)
@@ -66,10 +108,10 @@ module Hutch
66
108
  message = Message.new(delivery_info, properties, payload, serializer)
67
109
  consumer_instance = consumer.new.tap { |c| c.broker, c.delivery_info = @broker, delivery_info }
68
110
  with_tracing(consumer_instance).handle(message)
69
- @broker.ack(delivery_info.delivery_tag)
111
+ @broker.ack(delivery_info.delivery_tag, channel: delivery_info.channel) unless consumer_instance.message_rejected?
70
112
  rescue => ex
71
113
  acknowledge_error(delivery_info, properties, @broker, ex)
72
- handle_error(properties, payload, consumer, ex)
114
+ handle_error(properties, payload, consumer, ex, delivery_info)
73
115
  end
74
116
 
75
117
  def with_tracing(klass)
@@ -78,7 +120,7 @@ module Hutch
78
120
 
79
121
  def handle_error(*args)
80
122
  Hutch::Config[:error_handlers].each do |backend|
81
- backend.handle(*args)
123
+ backend.handle *args.first(backend.method(:handle).arity)
82
124
  end
83
125
  end
84
126
 
@@ -103,6 +145,30 @@ module Hutch
103
145
 
104
146
  private
105
147
 
148
+ def group_configured?
149
+ if group.present? && consumer_groups.blank?
150
+ logger.info 'Consumer groups are blank'
151
+ end
152
+ group.present?
153
+ end
154
+
155
+ def group_restricted?(consumer)
156
+ consumers_to_load = consumer_groups[group]
157
+ if consumers_to_load
158
+ !consumers_to_load.include?(consumer.name)
159
+ else
160
+ true
161
+ end
162
+ end
163
+
164
+ def group
165
+ Hutch::Config[:group]
166
+ end
167
+
168
+ def consumer_groups
169
+ Hutch::Config[:consumer_groups]
170
+ end
171
+
106
172
  attr_accessor :setup_procs
107
173
 
108
174
  def unique_consumer_tag
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
@@ -1,2 +1,2 @@
1
- YARD::Templates::Engine.register_template_path(File.dirname(__FILE__) + '/../../templates')
2
- require File.join(File.dirname(__FILE__), 'handler') if RUBY19
1
+ YARD::Templates::Engine.register_template_path(File.join(__dir__, '../../templates'))
2
+ require_relative 'handler'
@@ -92,6 +92,110 @@ describe Hutch::Broker do
92
92
 
93
93
  connection.close
94
94
  end
95
+
96
+ context 'when configured with a URI' do
97
+ context 'which specifies the port' do
98
+ before { config[:uri] = "amqp://#{config[:mq_username]}:#{config[:mq_password]}@#{config[:mq_host]}:#{config[:mq_port]}/" }
99
+
100
+ it 'successfully connects' do
101
+ c = broker.open_connection
102
+ expect(c).to be_open
103
+ c.close
104
+ end
105
+ end
106
+
107
+ context 'which does not specify port and uses the amqp scheme' do
108
+ before { config[:uri] = "amqp://#{config[:mq_username]}:#{config[:mq_password]}@#{config[:mq_host]}/" }
109
+
110
+ it 'successfully connects' do
111
+ c = broker.open_connection
112
+ expect(c).to be_open
113
+ c.close
114
+ end
115
+ end
116
+
117
+ context 'which specifies the amqps scheme' do
118
+ before { config[:uri] = "amqps://#{config[:mq_username]}:#{config[:mq_password]}@#{config[:mq_host]}/" }
119
+
120
+ it 'utilises TLS' do
121
+ expect(Hutch::Adapter).to receive(:new).with(
122
+ hash_including(tls: true)
123
+ ).and_return(instance_double('Hutch::Adapter', start: nil))
124
+
125
+ broker.open_connection
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ describe '#parse_uri' do
132
+ {
133
+ "amqp://host/vhost" => "vhost",
134
+ "amqp://host/v%2fhost" => "v/host",
135
+ "amqp://host/%2f" => "/",
136
+ "amqp://host/" => Hutch::Adapter::DEFAULT_VHOST,
137
+ "amqp://host" => Hutch::Adapter::DEFAULT_VHOST,
138
+ }.each do |uri, vhost|
139
+ it "parses the vhost of #{uri} as #{vhost.inspect}" do
140
+ config[:uri] = uri
141
+ broker.send(:parse_uri)
142
+
143
+ expect(config[:mq_vhost]).to eq(vhost)
144
+ end
145
+ end
146
+
147
+ it 'passes the vhost to the connection unchanged' do
148
+ config[:uri] = "amqp://host/%2f"
149
+
150
+ expect(broker.send(:connection_params)[:vhost]).to eq("/")
151
+ end
152
+
153
+ it 'percent-decodes the username and password' do
154
+ config[:uri] = "amqp://al%23pha:be%20ta@host:10000/vhost"
155
+ broker.send(:parse_uri)
156
+
157
+ expect(config[:mq_username]).to eq("al#pha")
158
+ expect(config[:mq_password]).to eq("be ta")
159
+ end
160
+
161
+ it 'leaves absent credentials nil' do
162
+ config[:uri] = "amqp://host:10000/vhost"
163
+ broker.send(:parse_uri)
164
+
165
+ expect(config[:mq_username]).to be_nil
166
+ expect(config[:mq_password]).to be_nil
167
+ end
168
+ end
169
+
170
+ describe '#bindings', 'vhost filtering' do
171
+ let(:api_bindings) do
172
+ [{ 'source' => 'hutch', 'vhost' => '/', 'destination' => 'q', 'routing_key' => 'key' }]
173
+ end
174
+
175
+ before do
176
+ config[:mq_exchange] = 'hutch'
177
+ broker.api_client = double('api_client', bindings: api_bindings)
178
+ end
179
+
180
+ it 'filters on the default vhost when the configured vhost is blank' do
181
+ config[:mq_vhost] = ''
182
+
183
+ expect(broker.bindings).to eq('q' => ['key'])
184
+ end
185
+ end
186
+
187
+ describe '#sanitized_uri' do
188
+ it 'percent-encodes the decoded username and vhost again' do
189
+ config[:uri] = 'amqp://al%23pha:be%20ta@host:10000/v%2Fhost'
190
+
191
+ expect(broker.send(:sanitized_uri)).to eq('amqp://al%23pha@host:10000/v%2Fhost')
192
+ end
193
+
194
+ it 'renders the default vhost as a bare trailing slash' do
195
+ config[:uri] = 'amqp://alpha:beta@host:10000/'
196
+
197
+ expect(broker.send(:sanitized_uri)).to eq('amqp://alpha@host:10000/')
198
+ end
95
199
  end
96
200
 
97
201
  describe '#open_connection!' do
@@ -165,6 +269,100 @@ describe Hutch::Broker do
165
269
  end
166
270
  end
167
271
 
272
+ describe '#replace_channel!', adapter: :bunny do
273
+ let(:old_channel) { double('Bunny::Channel', open?: true, close: nil) }
274
+ let(:new_channel) { double('Bunny::Channel').as_null_object }
275
+ let(:work_pool) { double('Bunny::ConsumerWorkPool', shutdown: nil, join: nil, kill: nil) }
276
+
277
+ before do
278
+ broker.channel = old_channel
279
+ allow(broker).to receive(:channel_work_pool).and_return(work_pool)
280
+ allow(broker).to receive(:open_channel).and_return(new_channel)
281
+ allow(broker).to receive(:declare_exchange!)
282
+ allow(broker).to receive(:declare_publisher!)
283
+ end
284
+
285
+ it 'drains the work pool before closing the old channel' do
286
+ expect(work_pool).to receive(:shutdown).ordered
287
+ expect(work_pool).to receive(:join).with(config[:graceful_exit_timeout]).ordered
288
+ expect(work_pool).to receive(:kill).ordered
289
+ expect(old_channel).to receive(:close).ordered
290
+
291
+ broker.replace_channel!
292
+ end
293
+
294
+ it 'redeclares the exchange and the publisher on the new channel' do
295
+ expect(broker).to receive(:declare_exchange!)
296
+ expect(broker).to receive(:declare_publisher!)
297
+
298
+ broker.replace_channel!
299
+
300
+ expect(broker.channel).to eq(new_channel)
301
+ end
302
+
303
+ context 'when the old channel is already closed' do
304
+ let(:old_channel) { double('Bunny::Channel', open?: false) }
305
+
306
+ it 'opens a new channel anyway' do
307
+ expect(old_channel).not_to receive(:close)
308
+ expect(work_pool).not_to receive(:shutdown)
309
+
310
+ broker.replace_channel!
311
+
312
+ expect(broker.channel).to eq(new_channel)
313
+ end
314
+ end
315
+
316
+ context 'when the server closes the old channel concurrently' do
317
+ it 'opens a new channel anyway' do
318
+ allow(old_channel).to receive(:close).
319
+ and_raise(Hutch::Adapter::ChannelAlreadyClosed.new('already closed', old_channel))
320
+
321
+ broker.replace_channel!
322
+
323
+ expect(broker.channel).to eq(new_channel)
324
+ end
325
+ end
326
+ end
327
+
328
+ describe 'acknowledgements' do
329
+ let(:current_channel) { double('Channel').as_null_object }
330
+ let(:superseded_channel) { double('Channel') }
331
+
332
+ before { broker.channel = current_channel }
333
+
334
+ it 'sends them on the channel the delivery arrived on' do
335
+ expect(current_channel).to receive(:ack).with('dt', false)
336
+ broker.ack('dt', channel: current_channel)
337
+
338
+ expect(current_channel).to receive(:nack).with('dt', false, false)
339
+ broker.nack('dt', channel: current_channel)
340
+
341
+ expect(current_channel).to receive(:reject).with('dt', false)
342
+ broker.reject('dt', channel: current_channel)
343
+
344
+ expect(current_channel).to receive(:reject).with('dt', true)
345
+ broker.requeue('dt', channel: current_channel)
346
+ end
347
+
348
+ it 'drops them when the channel has been replaced' do
349
+ expect(superseded_channel).not_to receive(:ack)
350
+ expect(superseded_channel).not_to receive(:nack)
351
+ expect(superseded_channel).not_to receive(:reject)
352
+
353
+ broker.ack('dt', channel: superseded_channel)
354
+ broker.nack('dt', channel: superseded_channel)
355
+ broker.reject('dt', channel: superseded_channel)
356
+ broker.requeue('dt', channel: superseded_channel)
357
+ end
358
+
359
+ it 'defaults to the current channel' do
360
+ expect(current_channel).to receive(:ack).with('dt', false)
361
+
362
+ broker.ack('dt')
363
+ end
364
+ end
365
+
168
366
  describe '#declare_exchange' do
169
367
  before do
170
368
  broker.open_connection!
@@ -211,9 +409,9 @@ describe Hutch::Broker do
211
409
  context 'when given invalid details' do
212
410
  before { config[:mq_api_host] = 'notarealhost' }
213
411
  after { broker.disconnect }
214
- let(:set_up_api_connection) { ->{ broker.set_up_api_connection } }
412
+ let(:set_up_api_connection) { broker.set_up_api_connection }
215
413
 
216
- specify { expect(set_up_api_connection).to raise_error(StandardError) }
414
+ specify { expect { broker.set_up_api_connection }.to raise_error(StandardError) }
217
415
  end
218
416
  end
219
417
 
@@ -222,13 +420,27 @@ describe Hutch::Broker do
222
420
  let(:arguments) { { foo: :bar } }
223
421
  before { allow(broker).to receive(:channel) { channel } }
224
422
 
225
- it 'applies a global namespace' do
226
- config[:namespace] = 'mirror-all.service'
227
- expect(broker.channel).to receive(:queue) do |*args|
228
- args.first == ''
229
- args.last == arguments
423
+ it 'creates a queue with the given name' do
424
+ expect(broker.channel).to receive(:queue).with('test', arguments: arguments)
425
+ broker.queue('test'.freeze, arguments: arguments)
426
+ end
427
+ end
428
+
429
+ describe '#namespaced_queue_name' do
430
+ context 'with a namespace configured' do
431
+ before { config[:namespace] = 'mirror-all.service' }
432
+
433
+ it 'prepends the namespace' do
434
+ expect(broker.namespaced_queue_name('test')).to eq('mirror-all.service:test')
435
+ end
436
+ end
437
+
438
+ context 'without a namespace configured' do
439
+ before { config[:namespace] = nil }
440
+
441
+ it 'returns the name unchanged' do
442
+ expect(broker.namespaced_queue_name('test')).to eq('test')
230
443
  end
231
- broker.queue('test', arguments)
232
444
  end
233
445
  end
234
446
 
@@ -245,7 +457,7 @@ describe Hutch::Broker do
245
457
 
246
458
  context 'with a binding' do
247
459
  around do |example|
248
- queue = broker.queue('test').bind(broker.exchange, routing_key: 'key')
460
+ queue = broker.queue('test', durable: true).bind(broker.exchange, routing_key: 'key')
249
461
  example.run
250
462
  queue.unbind(broker.exchange, routing_key: 'key').delete
251
463
  end
@@ -275,7 +487,7 @@ describe Hutch::Broker do
275
487
  end
276
488
 
277
489
  context '(rabbitmq integration test)', rabbitmq: true do
278
- let(:queue) { broker.queue('consumer') }
490
+ let(:queue) { broker.queue('consumer', durable: true) }
279
491
  let(:routing_key) { 'key' }
280
492
 
281
493
  before { allow(broker).to receive(:bindings).and_call_original }