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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +402 -5
- data/LICENSE +1 -0
- data/README.md +157 -47
- data/bin/ci/before_build.sh +20 -0
- data/bin/ci/before_build_docker.sh +15 -0
- data/bin/ci/install_on_debian.sh +46 -0
- data/lib/hutch/acknowledgements/nack_on_all_failures.rb +1 -1
- data/lib/hutch/adapters/bunny.rb +29 -1
- data/lib/hutch/adapters/march_hare.rb +24 -0
- data/lib/hutch/broker.rb +122 -36
- data/lib/hutch/cli.rb +23 -12
- data/lib/hutch/config.rb +45 -13
- data/lib/hutch/consumer.rb +63 -4
- data/lib/hutch/error_handlers/airbrake.rb +20 -2
- data/lib/hutch/error_handlers/base.rb +15 -0
- data/lib/hutch/error_handlers/bugsnag.rb +30 -0
- data/lib/hutch/error_handlers/honeybadger.rb +11 -3
- data/lib/hutch/error_handlers/logger.rb +7 -2
- data/lib/hutch/error_handlers/rollbar.rb +28 -0
- data/lib/hutch/error_handlers/sentry.rb +13 -11
- data/lib/hutch/error_handlers.rb +2 -1
- data/lib/hutch/publisher.rb +1 -1
- data/lib/hutch/serializers/json.rb +3 -3
- data/lib/hutch/tracers/datadog.rb +18 -0
- data/lib/hutch/tracers.rb +1 -1
- data/lib/hutch/version.rb +1 -2
- data/lib/hutch/waiter.rb +1 -1
- data/lib/hutch/worker.rb +74 -8
- data/lib/hutch.rb +8 -4
- data/lib/yard-settings/yard-settings.rb +2 -2
- data/spec/hutch/broker_spec.rb +222 -10
- data/spec/hutch/cli_spec.rb +25 -9
- data/spec/hutch/config_spec.rb +84 -12
- data/spec/hutch/consumer_spec.rb +93 -4
- data/spec/hutch/error_handlers/airbrake_spec.rb +23 -1
- data/spec/hutch/error_handlers/bugsnag_spec.rb +56 -0
- data/spec/hutch/error_handlers/honeybadger_spec.rb +22 -1
- data/spec/hutch/error_handlers/logger_spec.rb +12 -1
- data/spec/hutch/error_handlers/rollbar_spec.rb +45 -0
- data/spec/hutch/error_handlers/sentry_spec.rb +27 -2
- data/spec/hutch/message_spec.rb +1 -1
- data/spec/hutch/tracers/datadog_spec.rb +46 -0
- data/spec/hutch/waiter_spec.rb +2 -2
- data/spec/hutch/worker_spec.rb +139 -25
- data/spec/integration/channel_recovery_spec.rb +187 -0
- data/spec/integration/publish_consume_spec.rb +47 -0
- metadata +35 -61
- data/.gitignore +0 -10
- data/.rspec +0 -1
- data/.travis.yml +0 -19
- data/.yardopts +0 -5
- data/Gemfile +0 -33
- data/Guardfile +0 -14
- data/Rakefile +0 -21
- data/examples/consumer.rb +0 -13
- data/examples/producer.rb +0 -10
- data/hutch.gemspec +0 -29
- data/lib/hutch/error_handlers/opbeat.rb +0 -24
- data/lib/hutch/tracers/opbeat.rb +0 -37
- data/spec/hutch/error_handlers/opbeat_spec.rb +0 -22
- data/spec/spec_helper.rb +0 -42
- data/spec/tracers/opbeat_spec.rb +0 -44
data/spec/hutch/worker_spec.rb
CHANGED
|
@@ -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(:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -97,7 +172,7 @@ describe Hutch::Worker do
|
|
|
97
172
|
it 'requeues the message' do
|
|
98
173
|
allow(consumer_instance).to receive(:process).and_raise('failed')
|
|
99
174
|
requeuer = double
|
|
100
|
-
allow(requeuer).to receive(:handle)
|
|
175
|
+
allow(requeuer).to receive(:handle) { |delivery_info, properties, broker, e|
|
|
101
176
|
broker.requeue delivery_info.delivery_tag
|
|
102
177
|
true
|
|
103
178
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Channel recovery is bunny-only, see MarchHareAdapter#install_channel_recovery
|
|
4
|
+
return if defined?(JRUBY_VERSION)
|
|
5
|
+
require 'hutch/broker'
|
|
6
|
+
require 'hutch/worker'
|
|
7
|
+
require 'hutch/consumer'
|
|
8
|
+
require 'bunny'
|
|
9
|
+
require 'json'
|
|
10
|
+
require 'securerandom'
|
|
11
|
+
|
|
12
|
+
describe 'channel recovery after delivery acknowledgement timeout', rabbitmq: true, adapter: :bunny do
|
|
13
|
+
CONSUMER_TIMEOUT_MS = 5_000
|
|
14
|
+
# A 4.3 quorum queue times the consumer out on its own timer, which fires at
|
|
15
|
+
# the deadline. Earlier series detect it from the channel tick instead, and
|
|
16
|
+
# `channel_tick_interval` defaults to 60s, so detection lags by up to that much.
|
|
17
|
+
BLOCKED_HANDLER_SECONDS = 20
|
|
18
|
+
BLOCKED_HANDLER_SECONDS_BEFORE_4_3 = 100
|
|
19
|
+
|
|
20
|
+
let(:log) { StringIO.new }
|
|
21
|
+
let(:logger) { Logger.new(log) }
|
|
22
|
+
let(:exchange_name) { "hutch.integration.exchange.#{SecureRandom.hex(4)}" }
|
|
23
|
+
let(:queue_name) { "hutch.integration.queue.#{SecureRandom.hex(4)}" }
|
|
24
|
+
let(:routing_key) { "hutch.integration.key.#{SecureRandom.hex(4)}" }
|
|
25
|
+
|
|
26
|
+
let(:processed) { [] }
|
|
27
|
+
let(:processed_lock) { Mutex.new }
|
|
28
|
+
let(:timed_out_once) { [false] }
|
|
29
|
+
|
|
30
|
+
let(:server_version) do
|
|
31
|
+
Gem::Version.new(publisher.server_properties['version'].to_s[/\A\d+(\.\d+)*/])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
let(:blocked_handler_seconds) do
|
|
35
|
+
if server_version >= Gem::Version.new('4.3')
|
|
36
|
+
BLOCKED_HANDLER_SECONDS
|
|
37
|
+
else
|
|
38
|
+
BLOCKED_HANDLER_SECONDS_BEFORE_4_3
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
let(:consumer_class) do
|
|
43
|
+
msgs = processed
|
|
44
|
+
lock = processed_lock
|
|
45
|
+
rk = routing_key
|
|
46
|
+
qn = queue_name
|
|
47
|
+
timed_out = timed_out_once
|
|
48
|
+
blocked = blocked_handler_seconds
|
|
49
|
+
|
|
50
|
+
Class.new do
|
|
51
|
+
include Hutch::Consumer
|
|
52
|
+
|
|
53
|
+
consume rk
|
|
54
|
+
queue_name qn
|
|
55
|
+
arguments(
|
|
56
|
+
'x-queue-type' => 'quorum',
|
|
57
|
+
'x-consumer-timeout' => CONSUMER_TIMEOUT_MS
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Both `CONSUMER_TIMEOUT_MS` and this sleep run from the same delivery,
|
|
61
|
+
# so the margin between them does not shrink on a loaded machine.
|
|
62
|
+
define_method(:process) do |message|
|
|
63
|
+
if message['id'] == 'trigger-timeout' && !timed_out[0]
|
|
64
|
+
timed_out[0] = true
|
|
65
|
+
sleep blocked
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
lock.synchronize { msgs << message['id'] }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
let(:broker) { Hutch::Broker.new }
|
|
74
|
+
let(:worker) { Hutch::Worker.new(broker, [consumer_class], []) }
|
|
75
|
+
|
|
76
|
+
let(:publisher) do
|
|
77
|
+
Bunny.new(
|
|
78
|
+
host: Hutch::Config[:mq_host],
|
|
79
|
+
port: Hutch::Config[:mq_port],
|
|
80
|
+
username: Hutch::Config[:mq_username],
|
|
81
|
+
password: Hutch::Config[:mq_password],
|
|
82
|
+
vhost: Hutch::Config[:mq_vhost]
|
|
83
|
+
).tap(&:start)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
let(:publisher_channel) { publisher.create_channel }
|
|
87
|
+
let(:exchange) { publisher_channel.topic(exchange_name, durable: true) }
|
|
88
|
+
|
|
89
|
+
before do
|
|
90
|
+
Hutch::Logging.logger = logger
|
|
91
|
+
Hutch::Config.set(:mq_exchange, exchange_name)
|
|
92
|
+
Hutch::Config.set(:force_publisher_confirms, false)
|
|
93
|
+
Hutch::Config.set(:client_logger, logger)
|
|
94
|
+
# Channel replacement waits this long for the deliberately blocked
|
|
95
|
+
# handler: the default of 11s would use up most of the consumption window.
|
|
96
|
+
@graceful_exit_timeout = Hutch::Config[:graceful_exit_timeout]
|
|
97
|
+
Hutch::Config.set(:graceful_exit_timeout, 1)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
after do
|
|
101
|
+
publisher_channel.close rescue nil
|
|
102
|
+
publisher.close rescue nil
|
|
103
|
+
broker.disconnect rescue nil
|
|
104
|
+
Hutch::Config.set(:graceful_exit_timeout, @graceful_exit_timeout)
|
|
105
|
+
Hutch::Logging.logger = Logger.new(File::NULL)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def wait_for(timeout, label, &condition)
|
|
109
|
+
await_condition(timeout, label, &condition)
|
|
110
|
+
rescue AwaitHelpers::ConditionTimeout => e
|
|
111
|
+
raise <<~MSG
|
|
112
|
+
#{e.message}
|
|
113
|
+
|
|
114
|
+
processed_messages=#{processed_messages.inspect}
|
|
115
|
+
channel_open=#{broker.channel.open? rescue 'unknown'}
|
|
116
|
+
channel_closed=#{broker.channel.closed? rescue 'unknown'}
|
|
117
|
+
|
|
118
|
+
log output:
|
|
119
|
+
#{log_output}
|
|
120
|
+
MSG
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def processed_messages
|
|
124
|
+
processed_lock.synchronize { processed.dup }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# `StringIO#string` does not touch the stream position: a rewind here would
|
|
128
|
+
# race with the worker threads that log concurrently and corrupt the buffer.
|
|
129
|
+
def log_output
|
|
130
|
+
log.string
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def publish_message(id)
|
|
134
|
+
exchange.publish(
|
|
135
|
+
JSON.dump('id' => id),
|
|
136
|
+
routing_key: routing_key,
|
|
137
|
+
content_type: 'application/json',
|
|
138
|
+
persistent: true
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# RabbitMQ up to 4.2 closes the channel on a delivery acknowledgement
|
|
143
|
+
# timeout; 4.3+ quorum queues instead cancel only the timed out consumer.
|
|
144
|
+
let(:cancels_the_consumer) { server_version >= Gem::Version.new('4.3') }
|
|
145
|
+
|
|
146
|
+
let(:recovery_log_pattern) do
|
|
147
|
+
if cancels_the_consumer
|
|
148
|
+
/cancelled by the server, re-subscribing/i
|
|
149
|
+
else
|
|
150
|
+
/delivery acknowledgement on channel \d+ timed out/i
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Replacing the channel requeues what the cancelled consumer still held, so
|
|
155
|
+
# consumption resumes at once. Reopening one does not, so it resumes at the
|
|
156
|
+
# next consumer timeout tick.
|
|
157
|
+
let(:recovery_seconds) { cancels_the_consumer ? 15 : 90 }
|
|
158
|
+
|
|
159
|
+
# This spec is intentionally slow because RabbitMQ enforces delivery
|
|
160
|
+
# acknowledgement timeouts periodically on a timer, not immediately at the deadline.
|
|
161
|
+
it 're-subscribes and consumes later messages after RabbitMQ times out an unacknowledged delivery' do
|
|
162
|
+
broker.connect
|
|
163
|
+
worker.setup_queues
|
|
164
|
+
|
|
165
|
+
publish_message('trigger-timeout')
|
|
166
|
+
|
|
167
|
+
wait_for(240, 'a delivery acknowledgement timeout to close the channel or cancel the consumer') do
|
|
168
|
+
log_output.match?(recovery_log_pattern)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
publish_message('after-recovery')
|
|
172
|
+
|
|
173
|
+
wait_for(recovery_seconds, 'after-recovery message consumption') do
|
|
174
|
+
processed_messages.include?('after-recovery')
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
expect(log_output).to match(recovery_log_pattern)
|
|
178
|
+
|
|
179
|
+
# A consumed message proves the recovery finished, the publisher rebuild
|
|
180
|
+
# included.
|
|
181
|
+
broker.publish(routing_key, 'id' => 'published-after-recovery')
|
|
182
|
+
|
|
183
|
+
wait_for(15, 'published-after-recovery message consumption') do
|
|
184
|
+
processed_messages.include?('published-after-recovery')
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'hutch/broker'
|
|
3
|
+
require 'hutch/worker'
|
|
4
|
+
require 'hutch/consumer'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
describe 'publishing and consuming messages', rabbitmq: true, adapter: :bunny do
|
|
8
|
+
let(:exchange_name) { "hutch.test.#{SecureRandom.hex(4)}" }
|
|
9
|
+
let(:routing_key) { "test.message" }
|
|
10
|
+
let(:received) { [] }
|
|
11
|
+
|
|
12
|
+
let(:consumer_class) do
|
|
13
|
+
msgs = received
|
|
14
|
+
rk = routing_key
|
|
15
|
+
qn = "test_consumer_#{SecureRandom.hex(4)}"
|
|
16
|
+
|
|
17
|
+
Class.new do
|
|
18
|
+
include Hutch::Consumer
|
|
19
|
+
consume rk
|
|
20
|
+
queue_name qn
|
|
21
|
+
|
|
22
|
+
define_method(:process) { |message| msgs << message.body }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
let(:broker) { Hutch::Broker.new }
|
|
27
|
+
let(:worker) { Hutch::Worker.new(broker, [consumer_class], []) }
|
|
28
|
+
|
|
29
|
+
before do
|
|
30
|
+
Hutch::Config.set(:mq_exchange, exchange_name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
after do
|
|
34
|
+
broker.disconnect rescue nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'publishes and consumes a message' do
|
|
38
|
+
broker.connect
|
|
39
|
+
worker.setup_queues
|
|
40
|
+
|
|
41
|
+
broker.publish(routing_key, { test: 'data' })
|
|
42
|
+
|
|
43
|
+
await_condition(15, 'message consumption') { received.any? }
|
|
44
|
+
|
|
45
|
+
expect(received.first).to eq('test' => 'data')
|
|
46
|
+
end
|
|
47
|
+
end
|