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
@@ -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
@@ -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).ordered { |delivery_info, properties, broker, e|
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
- 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
 
@@ -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