hutch 0.25.0-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 (57) hide show
  1. checksums.yaml +4 -4
  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 +98 -35
  12. data/lib/hutch/cli.rb +16 -12
  13. data/lib/hutch/config.rb +42 -13
  14. data/lib/hutch/consumer.rb +63 -4
  15. data/lib/hutch/error_handlers/bugsnag.rb +30 -0
  16. data/lib/hutch/error_handlers/{opbeat.rb → rollbar.rb} +11 -13
  17. data/lib/hutch/error_handlers/sentry.rb +6 -11
  18. data/lib/hutch/error_handlers.rb +2 -1
  19. data/lib/hutch/publisher.rb +1 -1
  20. data/lib/hutch/serializers/json.rb +3 -3
  21. data/lib/hutch/tracers/datadog.rb +18 -0
  22. data/lib/hutch/tracers.rb +1 -1
  23. data/lib/hutch/version.rb +1 -2
  24. data/lib/hutch/worker.rb +46 -9
  25. data/lib/hutch.rb +8 -4
  26. data/lib/yard-settings/yard-settings.rb +2 -2
  27. data/spec/hutch/broker_spec.rb +192 -14
  28. data/spec/hutch/cli_spec.rb +12 -9
  29. data/spec/hutch/config_spec.rb +84 -12
  30. data/spec/hutch/consumer_spec.rb +93 -4
  31. data/spec/hutch/error_handlers/airbrake_spec.rb +4 -1
  32. data/spec/hutch/error_handlers/bugsnag_spec.rb +56 -0
  33. data/spec/hutch/error_handlers/honeybadger_spec.rb +1 -1
  34. data/spec/hutch/error_handlers/logger_spec.rb +1 -1
  35. data/spec/hutch/error_handlers/rollbar_spec.rb +45 -0
  36. data/spec/hutch/error_handlers/sentry_spec.rb +13 -3
  37. data/spec/hutch/message_spec.rb +1 -1
  38. data/spec/hutch/tracers/datadog_spec.rb +46 -0
  39. data/spec/hutch/waiter_spec.rb +2 -2
  40. data/spec/hutch/worker_spec.rb +138 -24
  41. data/spec/integration/channel_recovery_spec.rb +187 -0
  42. data/spec/integration/publish_consume_spec.rb +47 -0
  43. metadata +32 -59
  44. data/.gitignore +0 -10
  45. data/.rspec +0 -1
  46. data/.travis.yml +0 -16
  47. data/.yardopts +0 -5
  48. data/Gemfile +0 -33
  49. data/Guardfile +0 -14
  50. data/Rakefile +0 -21
  51. data/examples/consumer.rb +0 -13
  52. data/examples/producer.rb +0 -10
  53. data/hutch.gemspec +0 -29
  54. data/lib/hutch/tracers/opbeat.rb +0 -37
  55. data/spec/hutch/error_handlers/opbeat_spec.rb +0 -37
  56. data/spec/spec_helper.rb +0 -42
  57. data/spec/tracers/opbeat_spec.rb +0 -44
@@ -28,6 +28,32 @@ describe Hutch::Consumer do
28
28
  ComplexConsumer
29
29
  end
30
30
 
31
+ let(:consumer_using_quorum_queue) do
32
+ unless defined? ConsumerUsingQuorumQueue
33
+ class ConsumerUsingQuorumQueue
34
+ include Hutch::Consumer
35
+ consume 'hutch.test1'
36
+ arguments foo: :bar
37
+
38
+ quorum_queue
39
+ end
40
+ end
41
+ ConsumerUsingQuorumQueue
42
+ end
43
+
44
+ let(:consumer_using_classic_queue) do
45
+ unless defined? ConsumerUsingLazyQueue
46
+ class ConsumerUsingLazyQueue
47
+ include Hutch::Consumer
48
+ consume 'hutch.test1'
49
+ arguments foo: :bar
50
+ lazy_queue
51
+ classic_queue
52
+ end
53
+ end
54
+ ConsumerUsingLazyQueue
55
+ end
56
+
31
57
  describe 'module inclusion' do
32
58
  it 'registers the class as a consumer' do
33
59
  expect(Hutch).to receive(:register_consumer) do |klass|
@@ -71,6 +97,54 @@ describe Hutch::Consumer do
71
97
  end
72
98
  end
73
99
 
100
+ describe '.without_namespace' do
101
+ it 'is false by default' do
102
+ expect(simple_consumer.without_namespace?).to eq(false)
103
+ end
104
+
105
+ it 'returns true after calling without_namespace' do
106
+ simple_consumer.without_namespace
107
+ expect(simple_consumer.without_namespace?).to eq(true)
108
+ end
109
+ end
110
+
111
+ describe 'default queue mode' do
112
+ it 'does not specify any mode by default' do
113
+ expect(simple_consumer.queue_mode).to eq(nil)
114
+ expect(simple_consumer.queue_type).to eq(nil)
115
+ end
116
+ end
117
+
118
+ describe '.lazy_queue' do
119
+ context 'when queue mode has been set explicitly to lazy' do
120
+ it 'sets queue mode to lazy' do
121
+ expect(consumer_using_classic_queue.queue_mode).to eq('lazy')
122
+ end
123
+ end
124
+ end
125
+
126
+ describe '.classic_queue' do
127
+ context 'when queue type has been set explicitly to classic' do
128
+ it 'sets queue type to classic' do
129
+ expect(consumer_using_classic_queue.queue_type).to eq('classic')
130
+ end
131
+ end
132
+ end
133
+
134
+ describe '.quorum_queue' do
135
+ context 'when queue type has been set explicitly to quorum' do
136
+ it 'sets queue type to quorum' do
137
+ expect(consumer_using_quorum_queue.queue_type).to eq('quorum')
138
+ end
139
+
140
+ it 'accepts initial group size as an option' do
141
+ consumer = simple_consumer
142
+ expect { consumer.quorum_queue(initial_group_size: 3) }
143
+ .to change { consumer.initial_group_size }.to(3)
144
+ end
145
+ end
146
+ end
147
+
74
148
  describe '.arguments' do
75
149
  let(:args) { { foo: :bar} }
76
150
 
@@ -82,15 +156,30 @@ describe Hutch::Consumer do
82
156
  end
83
157
 
84
158
  describe '.get_arguments' do
85
-
86
159
  context 'when defined' do
87
- it { expect(complex_consumer.get_arguments).to eq(foo: :bar) }
160
+ it { expect(complex_consumer.get_arguments).to include(foo: :bar) }
88
161
  end
89
162
 
90
- context 'when not defined' do
91
- it { expect(simple_consumer.get_arguments).to eq({}) }
163
+ context 'when queue is lazy' do
164
+ it 'has the x-queue-mode argument set to lazy' do
165
+ expect(consumer_using_classic_queue.get_arguments['x-queue-mode'])
166
+ .to eq('lazy')
167
+ end
92
168
  end
93
169
 
170
+ context "when queue's type is quorum" do
171
+ let(:arguments) { consumer_using_quorum_queue.get_arguments }
172
+ it 'has the x-queue-type argument set to quorum' do
173
+ expect(arguments['x-queue-type']).to eq('quorum')
174
+ expect(arguments).to_not have_key('x-quorum-initial-group-size')
175
+ end
176
+
177
+ it 'has the x-quorum-initial-group-size argument set to quorum' do
178
+ consumer_using_quorum_queue.quorum_queue(initial_group_size: 5)
179
+ expect(arguments['x-queue-type']).to eq('quorum')
180
+ expect(arguments['x-quorum-initial-group-size']).to eq(5)
181
+ end
182
+ end
94
183
  end
95
184
 
96
185
  describe '.get_queue_name' do
@@ -1,5 +1,8 @@
1
1
  require 'spec_helper'
2
2
 
3
+ # The airbrake gem is MRI-only, see the Gemfile
4
+ return if defined?(JRUBY_VERSION)
5
+
3
6
  describe Hutch::ErrorHandlers::Airbrake do
4
7
  let(:error_handler) { Hutch::ErrorHandlers::Airbrake.new }
5
8
 
@@ -14,7 +17,7 @@ describe Hutch::ErrorHandlers::Airbrake do
14
17
 
15
18
  it "logs the error to Airbrake" do
16
19
  message_id = "1"
17
- properties = OpenStruct.new(message_id: message_id)
20
+ properties = Struct.new(:message_id).new(message_id)
18
21
  payload = "{}"
19
22
  consumer = double
20
23
  ex = error
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe Hutch::ErrorHandlers::Bugsnag do
6
+ let(:error_handler) { described_class.new }
7
+
8
+ before do
9
+ Bugsnag.configure do |bugsnag|
10
+ bugsnag.api_key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
11
+ bugsnag.logger = Logger.new(File::NULL) # suppress logging
12
+ end
13
+ end
14
+
15
+ describe "#handle" do
16
+ let(:error) do
17
+ begin
18
+ raise "Stuff went wrong"
19
+ rescue RuntimeError => err
20
+ err
21
+ end
22
+ end
23
+
24
+ it "logs the error to Bugsnag" do
25
+ message_id = "1"
26
+ properties = Struct.new(:message_id).new(message_id)
27
+ payload = "{}"
28
+ consumer = double
29
+ ex = error
30
+ message = {
31
+ payload: payload,
32
+ consumer: consumer
33
+ }
34
+
35
+ expect(::Bugsnag).to receive(:notify).with(ex).and_call_original
36
+ expect_any_instance_of(::Bugsnag::Report).to receive(:add_tab).with(:hutch, message)
37
+ error_handler.handle(properties, payload, consumer, ex)
38
+ end
39
+ end
40
+
41
+ describe "#handle_setup_exception" do
42
+ let(:error) do
43
+ begin
44
+ raise "Stuff went wrong"
45
+ rescue RuntimeError => err
46
+ err
47
+ end
48
+ end
49
+
50
+ it "logs the error to Bugsnag" do
51
+ ex = error
52
+ expect(::Bugsnag).to receive(:notify).with(ex)
53
+ error_handler.handle_setup_exception(ex)
54
+ end
55
+ end
56
+ end
@@ -14,7 +14,7 @@ describe Hutch::ErrorHandlers::Honeybadger do
14
14
 
15
15
  it "logs the error to Honeybadger" do
16
16
  message_id = "1"
17
- properties = OpenStruct.new(message_id: message_id)
17
+ properties = Struct.new(:message_id).new(message_id)
18
18
  payload = "{}"
19
19
  consumer = double
20
20
  ex = error
@@ -4,7 +4,7 @@ describe Hutch::ErrorHandlers::Logger do
4
4
  let(:error_handler) { Hutch::ErrorHandlers::Logger.new }
5
5
 
6
6
  describe '#handle' do
7
- let(:properties) { OpenStruct.new(message_id: "1") }
7
+ let(:properties) { Struct.new(:message_id).new("1") }
8
8
  let(:payload) { "{}" }
9
9
  let(:error) { double(message: "Stuff went wrong", class: "RuntimeError",
10
10
  backtrace: ["line 1", "line 2"]) }
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hutch::ErrorHandlers::Rollbar do
4
+ let(:error_handler) { Hutch::ErrorHandlers::Rollbar.new }
5
+
6
+ describe '#handle' do
7
+ let(:error) do
8
+ begin
9
+ raise "Stuff went wrong"
10
+ rescue RuntimeError => err
11
+ err
12
+ end
13
+ end
14
+
15
+ it "logs the error to Rollbar" do
16
+ message_id = "1"
17
+ properties = Struct.new(:message_id).new(message_id)
18
+ payload = "{}"
19
+ consumer = double
20
+ ex = error
21
+ message = {
22
+ payload: payload,
23
+ consumer: consumer
24
+ }
25
+ expect(::Rollbar).to receive(:error).with(ex, message)
26
+ error_handler.handle(properties, payload, consumer, ex)
27
+ end
28
+ end
29
+
30
+ describe '#handle_setup_exception' do
31
+ let(:error) do
32
+ begin
33
+ raise "Stuff went wrong"
34
+ rescue RuntimeError => err
35
+ err
36
+ end
37
+ end
38
+
39
+ it "logs the error to Rollbar" do
40
+ ex = error
41
+ expect(::Rollbar).to receive(:error).with(ex)
42
+ error_handler.handle_setup_exception(ex)
43
+ end
44
+ end
45
+ end
@@ -3,8 +3,16 @@ require 'spec_helper'
3
3
  describe Hutch::ErrorHandlers::Sentry do
4
4
  let(:error_handler) { Hutch::ErrorHandlers::Sentry.new }
5
5
 
6
+ before do
7
+ Sentry.init do
8
+ # initialize Sentry so that the integration acutally works
9
+ # otherwise, all its methods are going to return early
10
+ # so it will be impossible to check if it actually works
11
+ end
12
+ end
13
+
6
14
  describe '#handle' do
7
- let(:properties) { OpenStruct.new(message_id: "1") }
15
+ let(:properties) { Struct.new(:message_id).new("1") }
8
16
  let(:payload) { "{}" }
9
17
  let(:error) do
10
18
  begin
@@ -15,7 +23,8 @@ describe Hutch::ErrorHandlers::Sentry do
15
23
  end
16
24
 
17
25
  it "logs the error to Sentry" do
18
- expect(Raven).to receive(:capture_exception).with(error, extra: { payload: payload })
26
+ expect(::Sentry).to receive(:capture_exception).with(error).and_call_original
27
+
19
28
  error_handler.handle(properties, payload, double, error)
20
29
  end
21
30
  end
@@ -30,7 +39,8 @@ describe Hutch::ErrorHandlers::Sentry do
30
39
  end
31
40
 
32
41
  it "logs the error to Sentry" do
33
- expect(Raven).to receive(:capture_exception).with(error)
42
+ expect(::Sentry).to receive(:capture_exception).with(error).and_call_original
43
+
34
44
  error_handler.handle_setup_exception(error)
35
45
  end
36
46
  end
@@ -4,7 +4,7 @@ describe Hutch::Message do
4
4
  let(:delivery_info) { double('Delivery Info') }
5
5
  let(:props) { double('Properties', content_type: "application/json") }
6
6
  let(:body) {{ foo: 'bar' }.with_indifferent_access}
7
- let(:json_body) { MultiJson.dump(body) }
7
+ let(:json_body) { JSON.generate(body) }
8
8
  subject(:message) { Hutch::Message.new(delivery_info, props, json_body, Hutch::Config[:serializer]) }
9
9
 
10
10
  describe '#body' do
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Hutch::Tracers::Datadog do
4
+ ::Datadog.logger.level = Logger::FATAL # suppress logging
5
+
6
+ describe "#handle" do
7
+ subject(:handle) { tracer.handle(message) }
8
+
9
+ let(:tracer) { described_class.new(klass) }
10
+ let(:klass) do
11
+ Class.new do
12
+ attr_reader :message
13
+
14
+ def initialize
15
+ @message = nil
16
+ end
17
+
18
+ def class
19
+ Struct.new(:name).new('ClassName')
20
+ end
21
+
22
+ def process(message)
23
+ @message = message
24
+ end
25
+ end.new
26
+ end
27
+ let(:message) { double(:message) }
28
+
29
+ before do
30
+ allow(::Datadog::Tracing).to receive(:trace).and_call_original
31
+ end
32
+
33
+ it 'uses Datadog tracer' do
34
+ handle
35
+
36
+ expect(::Datadog::Tracing).to have_received(:trace).with('ClassName',
37
+ hash_including(service: 'hutch', type: 'rabbitmq'))
38
+ end
39
+
40
+ it 'processes the message' do
41
+ expect {
42
+ handle
43
+ }.to change { klass.message }.from(nil).to(message)
44
+ end
45
+ end
46
+ end
@@ -22,7 +22,7 @@ RSpec.describe Hutch::Waiter do
22
22
  end
23
23
  end
24
24
 
25
- context 'a TERM signal is received' do
25
+ context 'a TERM signal is received', if: !defined?(JRUBY_VERSION) do
26
26
  it 'logs that hutch is stopping' do
27
27
  expect(Hutch::Logging.logger).to receive(:info)
28
28
  .with('caught SIGTERM, stopping hutch...')
@@ -32,7 +32,7 @@ RSpec.describe Hutch::Waiter do
32
32
  end
33
33
  end
34
34
 
35
- context 'a INT signal is received' do
35
+ context 'a INT signal is received', if: !defined?(JRUBY_VERSION) do
36
36
  it 'logs that hutch is stopping' do
37
37
  expect(Hutch::Logging.logger).to receive(:info)
38
38
  .with('caught SIGINT, stopping hutch...')
@@ -4,7 +4,8 @@ require 'hutch/worker'
4
4
  describe Hutch::Worker do
5
5
  let(:consumer) { double('Consumer', routing_keys: %w( a b c ),
6
6
  get_queue_name: 'consumer', get_arguments: {},
7
- get_serializer: nil) }
7
+ get_options: {}, get_serializer: nil,
8
+ without_namespace?: false) }
8
9
  let(:consumers) { [consumer, double('Consumer')] }
9
10
  let(:broker) { Hutch::Broker.new }
10
11
  let(:setup_procs) { Array.new(2) { Proc.new {} } }
@@ -32,20 +33,34 @@ describe Hutch::Worker do
32
33
 
33
34
  describe '#setup_queue' do
34
35
  let(:queue) { double('Queue', bind: nil, subscribe: nil) }
35
- before { allow(broker).to receive_messages(queue: queue, bind_queue: nil) }
36
+ before { allow(broker).to receive_messages(queue: queue, bind_queue: nil, namespaced_queue_name: 'consumer') }
36
37
 
37
- it 'creates a queue' do
38
- expect(broker).to receive(:queue).with(consumer.get_queue_name, consumer.get_arguments).and_return(queue)
38
+ it 'creates a queue with a namespaced name' do
39
+ expect(broker).to receive(:namespaced_queue_name).with('consumer').and_return('ns:consumer')
40
+ expect(broker).to receive(:queue).with('ns:consumer', consumer.get_options).and_return(queue)
39
41
  worker.setup_queue(consumer)
40
42
  end
41
43
 
44
+ context 'when the consumer uses without_namespace' do
45
+ let(:consumer) { double('Consumer', routing_keys: %w( a b c ),
46
+ get_queue_name: 'consumer', get_arguments: {},
47
+ get_options: {}, get_serializer: nil,
48
+ without_namespace?: true) }
49
+
50
+ it 'creates a queue without applying the namespace' do
51
+ expect(broker).not_to receive(:namespaced_queue_name)
52
+ expect(broker).to receive(:queue).with('consumer', consumer.get_options).and_return(queue)
53
+ worker.setup_queue(consumer)
54
+ end
55
+ end
56
+
42
57
  it 'binds the queue to each of the routing keys' do
43
58
  expect(broker).to receive(:bind_queue).with(queue, %w( a b c ))
44
59
  worker.setup_queue(consumer)
45
60
  end
46
61
 
47
62
  it 'sets up a subscription' do
48
- expect(queue).to receive(:subscribe).with(consumer_tag: %r(^hutch\-.{36}$), manual_ack: true)
63
+ expect(queue).to receive(:subscribe).with(consumer_tag: %r(^hutch\-.{36}$), manual_ack: true, on_cancellation: kind_of(Proc))
49
64
  worker.setup_queue(consumer)
50
65
  end
51
66
 
@@ -53,7 +68,7 @@ describe Hutch::Worker do
53
68
  before { Hutch::Config.set(:consumer_tag_prefix, 'appname') }
54
69
 
55
70
  it 'sets up a subscription with the configured tag prefix' do
56
- expect(queue).to receive(:subscribe).with(consumer_tag: %r(^appname\-.{36}$), manual_ack: true)
71
+ expect(queue).to receive(:subscribe).with(consumer_tag: %r(^appname\-.{36}$), manual_ack: true, on_cancellation: kind_of(Proc))
57
72
  worker.setup_queue(consumer)
58
73
  end
59
74
  end
@@ -61,6 +76,7 @@ describe Hutch::Worker do
61
76
  context 'with a configured consumer tag prefix that is too long' do
62
77
  let(:maximum_size) { 255 - SecureRandom.uuid.size - 1 }
63
78
  before { Hutch::Config.set(:consumer_tag_prefix, 'a'.*(maximum_size + 1)) }
79
+ after { Hutch::Config.set(:consumer_tag_prefix, 'hutch') }
64
80
 
65
81
  it 'raises an error' do
66
82
  expect { worker.setup_queue(consumer) }.to raise_error(/Tag must be 255 bytes long at most/)
@@ -68,28 +84,87 @@ describe Hutch::Worker do
68
84
  end
69
85
  end
70
86
 
87
+ describe '#handle_cancellation' do
88
+ let(:cancelled_channel) { double('Channel') }
89
+ let(:new_channel) { double('Channel') }
90
+ let(:log) { StringIO.new }
91
+
92
+ before do
93
+ allow(Hutch::Logging).to receive(:logger).and_return(Logger.new(log))
94
+ allow(broker).to receive(:channel).and_return(cancelled_channel)
95
+ allow(broker).to receive(:replace_channel!) do
96
+ allow(broker).to receive(:channel).and_return(new_channel)
97
+ end
98
+ allow(worker).to receive(:setup_queues)
99
+ end
100
+
101
+ context 'when the queue still exists' do
102
+ before { allow(broker).to receive(:queue_exists?).with('consumer').and_return(true) }
103
+
104
+ it 'replaces the channel and re-subscribes' do
105
+ expect(broker).to receive(:replace_channel!)
106
+ expect(worker).to receive(:setup_queues)
107
+
108
+ worker.handle_cancellation('consumer', cancelled_channel).join
109
+ end
110
+
111
+ it 'replaces the channel once for all the consumers that shared it' do
112
+ expect(broker).to receive(:replace_channel!).once
113
+
114
+ [
115
+ worker.handle_cancellation('consumer', cancelled_channel),
116
+ worker.handle_cancellation('consumer', cancelled_channel)
117
+ ].each(&:join)
118
+ end
119
+
120
+ it 'does not replace a channel that has already been replaced' do
121
+ expect(broker).not_to receive(:replace_channel!)
122
+
123
+ worker.handle_cancellation('consumer', double('An older channel')).join
124
+ end
125
+ end
126
+
127
+ context 'when the queue is gone' do
128
+ before { allow(broker).to receive(:queue_exists?).with('consumer').and_return(false) }
129
+
130
+ it 'does not re-subscribe' do
131
+ expect(broker).not_to receive(:replace_channel!)
132
+
133
+ worker.handle_cancellation('consumer', cancelled_channel)
134
+
135
+ log.rewind
136
+ expect(log.read).to match(/the queue no longer exists/)
137
+ end
138
+ end
139
+ end
140
+
71
141
  describe '#handle_message' do
142
+ subject { worker.handle_message(consumer, delivery_info, properties, payload) }
72
143
  let(:payload) { '{}' }
73
144
  let(:consumer_instance) { double('Consumer instance') }
74
- let(:delivery_info) { double('Delivery Info', routing_key: '',
145
+ let(:delivery_info) { double('Delivery Info', routing_key: '', channel: nil,
75
146
  delivery_tag: 'dt') }
76
147
  let(:properties) { double('Properties', message_id: nil, content_type: "application/json") }
148
+ let(:log) { StringIO.new }
77
149
  before { allow(consumer).to receive_messages(new: consumer_instance) }
78
150
  before { allow(broker).to receive(:ack) }
79
151
  before { allow(broker).to receive(:nack) }
80
152
  before { allow(consumer_instance).to receive(:broker=) }
81
153
  before { allow(consumer_instance).to receive(:delivery_info=) }
154
+ before { allow(Hutch::Logging).to receive(:logger).and_return(Logger.new(log)) }
82
155
 
83
156
  it 'passes the message to the consumer' do
84
157
  expect(consumer_instance).to receive(:process).
85
158
  with(an_instance_of(Hutch::Message))
86
- worker.handle_message(consumer, delivery_info, properties, payload)
159
+ expect(consumer_instance).to receive(:message_rejected?).and_return(false)
160
+ subject
87
161
  end
88
162
 
89
163
  it 'acknowledges the message' do
90
164
  allow(consumer_instance).to receive(:process)
91
- expect(broker).to receive(:ack).with(delivery_info.delivery_tag)
92
- worker.handle_message(consumer, delivery_info, properties, payload)
165
+ expect(broker).to receive(:ack).with(delivery_info.delivery_tag, channel: delivery_info.channel)
166
+ expect(consumer_instance).to receive(:message_rejected?).and_return(false)
167
+ subject
93
168
  end
94
169
 
95
170
  context 'when the consumer fails and a requeue is configured' do
@@ -106,47 +181,86 @@ describe Hutch::Worker do
106
181
  expect(broker).to_not receive(:nack)
107
182
  expect(broker).to receive(:requeue)
108
183
 
109
- worker.handle_message(consumer, delivery_info, properties, payload)
184
+ subject
110
185
  end
111
186
  end
112
187
 
113
188
 
114
189
  context 'when the consumer raises an exception' do
190
+ let(:expected_log) { /ERROR .+ error in consumer .+ RuntimeError .+ backtrace:/m }
115
191
  before { allow(consumer_instance).to receive(:process).and_raise('a consumer error') }
116
192
 
117
193
  it 'logs the error' do
118
- Hutch::Config[:error_handlers].each do |backend|
119
- expect(backend).to receive(:handle)
120
- end
121
- worker.handle_message(consumer, delivery_info, properties, payload)
194
+ expect { subject }.to change { log.tap(&:rewind).read }.from("").to(expected_log)
122
195
  end
123
196
 
124
197
  it 'rejects the message' do
125
- expect(broker).to receive(:nack).with(delivery_info.delivery_tag)
126
- worker.handle_message(consumer, delivery_info, properties, payload)
198
+ expect(broker).to receive(:nack).with(delivery_info.delivery_tag, channel: delivery_info.channel)
199
+ subject
200
+ end
201
+
202
+ context 'when a custom error handler supports delivery info' do
203
+ let(:error_handler) do
204
+ Class.new(Hutch::ErrorHandlers::Base) do
205
+ def handle(_properties, _payload, _consumer, _ex, delivery_info)
206
+ raise unless delivery_info.delivery_tag == 'dt'
207
+ puts 'handled!'
208
+ end
209
+ end
210
+ end
211
+
212
+ around do |example|
213
+ original = Hutch::Config[:error_handlers]
214
+ Hutch::Config[:error_handlers] = [error_handler.new]
215
+ example.run
216
+ Hutch::Config[:error_handlers] = original
217
+ end
218
+
219
+ it 'calls the custom handler with delivery info' do
220
+ expect { subject }.to output("handled!\n").to_stdout
221
+ end
222
+ end
223
+
224
+ context 'when a custom error handler does not support delivery info' do
225
+ let(:error_handler) do
226
+ Class.new(Hutch::ErrorHandlers::Base) do
227
+ def handle(_properties, _payload, _consumer, _ex)
228
+ puts 'handled!'
229
+ end
230
+ end
231
+ end
232
+
233
+ around do |example|
234
+ original = Hutch::Config[:error_handlers]
235
+ Hutch::Config[:error_handlers] = [error_handler.new]
236
+ example.run
237
+ Hutch::Config[:error_handlers] = original
238
+ end
239
+
240
+ it 'calls the custom handler with delivery info' do
241
+ expect { subject }.to output("handled!\n").to_stdout
242
+ end
127
243
  end
128
244
  end
129
245
 
130
246
  context "when the payload is not valid json" do
131
247
  let(:payload) { "Not Valid JSON" }
248
+ let(:expected_log) { /ERROR .+ error in consumer .+ JSON::ParserError .+ backtrace:/m }
132
249
 
133
250
  it 'logs the error' do
134
- Hutch::Config[:error_handlers].each do |backend|
135
- expect(backend).to receive(:handle)
136
- end
137
- worker.handle_message(consumer, delivery_info, properties, payload)
251
+ expect { subject }.to change { log.tap(&:rewind).read }.from("").to(expected_log)
138
252
  end
139
253
 
140
254
  it 'rejects the message' do
141
- expect(broker).to receive(:nack).with(delivery_info.delivery_tag)
142
- worker.handle_message(consumer, delivery_info, properties, payload)
255
+ expect(broker).to receive(:nack).with(delivery_info.delivery_tag, channel: delivery_info.channel)
256
+ subject
143
257
  end
144
258
  end
145
259
  end
146
260
 
147
261
 
148
262
  describe '#acknowledge_error' do
149
- let(:delivery_info) { double('Delivery Info', routing_key: '',
263
+ let(:delivery_info) { double('Delivery Info', routing_key: '', channel: nil,
150
264
  delivery_tag: 'dt') }
151
265
  let(:properties) { double('Properties', message_id: 'abc123') }
152
266