hutch 0.19.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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.travis.yml +11 -0
  4. data/CHANGELOG.md +438 -0
  5. data/Gemfile +22 -0
  6. data/Guardfile +5 -0
  7. data/LICENSE +22 -0
  8. data/README.md +317 -0
  9. data/Rakefile +14 -0
  10. data/bin/hutch +8 -0
  11. data/circle.yml +3 -0
  12. data/examples/consumer.rb +13 -0
  13. data/examples/producer.rb +10 -0
  14. data/hutch.gemspec +30 -0
  15. data/lib/hutch.rb +62 -0
  16. data/lib/hutch/adapter.rb +11 -0
  17. data/lib/hutch/adapters/bunny.rb +33 -0
  18. data/lib/hutch/adapters/march_hare.rb +37 -0
  19. data/lib/hutch/broker.rb +374 -0
  20. data/lib/hutch/cli.rb +205 -0
  21. data/lib/hutch/config.rb +125 -0
  22. data/lib/hutch/consumer.rb +75 -0
  23. data/lib/hutch/error_handlers.rb +8 -0
  24. data/lib/hutch/error_handlers/airbrake.rb +26 -0
  25. data/lib/hutch/error_handlers/honeybadger.rb +28 -0
  26. data/lib/hutch/error_handlers/logger.rb +16 -0
  27. data/lib/hutch/error_handlers/sentry.rb +23 -0
  28. data/lib/hutch/exceptions.rb +7 -0
  29. data/lib/hutch/logging.rb +32 -0
  30. data/lib/hutch/message.rb +31 -0
  31. data/lib/hutch/serializers/identity.rb +19 -0
  32. data/lib/hutch/serializers/json.rb +22 -0
  33. data/lib/hutch/tracers.rb +6 -0
  34. data/lib/hutch/tracers/newrelic.rb +19 -0
  35. data/lib/hutch/tracers/null_tracer.rb +15 -0
  36. data/lib/hutch/version.rb +4 -0
  37. data/lib/hutch/worker.rb +143 -0
  38. data/spec/hutch/broker_spec.rb +377 -0
  39. data/spec/hutch/cli_spec.rb +80 -0
  40. data/spec/hutch/config_spec.rb +126 -0
  41. data/spec/hutch/consumer_spec.rb +130 -0
  42. data/spec/hutch/error_handlers/airbrake_spec.rb +34 -0
  43. data/spec/hutch/error_handlers/honeybadger_spec.rb +36 -0
  44. data/spec/hutch/error_handlers/logger_spec.rb +15 -0
  45. data/spec/hutch/error_handlers/sentry_spec.rb +20 -0
  46. data/spec/hutch/logger_spec.rb +28 -0
  47. data/spec/hutch/message_spec.rb +38 -0
  48. data/spec/hutch/serializers/json_spec.rb +17 -0
  49. data/spec/hutch/worker_spec.rb +99 -0
  50. data/spec/hutch_spec.rb +87 -0
  51. data/spec/spec_helper.rb +40 -0
  52. metadata +194 -0
@@ -0,0 +1,16 @@
1
+ require 'hutch/logging'
2
+
3
+ module Hutch
4
+ module ErrorHandlers
5
+ class Logger
6
+ include Logging
7
+
8
+ def handle(message_id, payload, consumer, ex)
9
+ prefix = "message(#{message_id || '-'}): "
10
+ logger.error prefix + "error in consumer '#{consumer}'"
11
+ logger.error prefix + "#{ex.class} - #{ex.message}"
12
+ logger.error (['backtrace:'] + ex.backtrace).join("\n")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ require 'hutch/logging'
2
+ require 'raven'
3
+
4
+ module Hutch
5
+ 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
+
15
+ def handle(message_id, payload, consumer, ex)
16
+ prefix = "message(#{message_id || '-'}): "
17
+ logger.error prefix + "Logging event to Sentry"
18
+ logger.error prefix + "#{ex.class} - #{ex.message}"
19
+ Raven.capture_exception(ex)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ module Hutch
2
+ class Exception < StandardError; end
3
+ class ConnectionError < Exception; end
4
+ class AuthenticationError < Exception; end
5
+ class WorkerSetupError < Exception; end
6
+ class PublishError < Exception; end
7
+ end
@@ -0,0 +1,32 @@
1
+ require 'logger'
2
+ require 'time'
3
+
4
+ module Hutch
5
+ module Logging
6
+ class HutchFormatter < Logger::Formatter
7
+ def call(severity, time, program_name, message)
8
+ "#{time.utc.iso8601} #{Process.pid} #{severity} -- #{message}\n"
9
+ end
10
+ end
11
+
12
+ def self.setup_logger(target = $stdout)
13
+ require 'hutch/config'
14
+ @logger = Logger.new(target)
15
+ @logger.level = Hutch::Config.log_level
16
+ @logger.formatter = HutchFormatter.new
17
+ @logger
18
+ end
19
+
20
+ def self.logger
21
+ @logger || setup_logger
22
+ end
23
+
24
+ def self.logger=(logger)
25
+ @logger = logger
26
+ end
27
+
28
+ def logger
29
+ Hutch::Logging.logger
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ require 'forwardable'
2
+
3
+ module Hutch
4
+ class Message
5
+ extend Forwardable
6
+
7
+ attr_reader :delivery_info, :properties, :payload
8
+
9
+ def initialize(delivery_info, properties, payload, serializer)
10
+ @delivery_info = delivery_info
11
+ @properties = properties
12
+ @payload = payload
13
+ @body = serializer.decode(payload)
14
+ end
15
+
16
+ def_delegator :@body, :[]
17
+ def_delegators :@properties, :message_id, :timestamp
18
+ def_delegators :@delivery_info, :routing_key, :exchange
19
+
20
+ attr_reader :body
21
+
22
+ def to_s
23
+ attrs = { :@body => body.to_s, message_id: message_id,
24
+ timestamp: timestamp, routing_key: routing_key }
25
+ "#<Message #{attrs.map { |k,v| "#{k}=#{v.inspect}" }.join(', ')}>"
26
+ end
27
+
28
+ alias_method :inspect, :to_s
29
+ end
30
+ end
31
+
@@ -0,0 +1,19 @@
1
+ module Hutch
2
+ module Serializers
3
+ class Identity
4
+
5
+ def self.encode(payload)
6
+ payload
7
+ end
8
+
9
+ def self.decode(payload)
10
+ payload
11
+ end
12
+
13
+ def self.binary? ; false ; end
14
+
15
+ def self.content_type ; nil ; end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ require 'multi_json'
2
+ require 'active_support/core_ext/hash/indifferent_access'
3
+
4
+ module Hutch
5
+ module Serializers
6
+ class JSON
7
+
8
+ def self.encode(payload)
9
+ ::JSON.dump(payload)
10
+ end
11
+
12
+ def self.decode(payload)
13
+ ::MultiJson.load(payload).with_indifferent_access
14
+ end
15
+
16
+ def self.binary? ; false ; end
17
+
18
+ def self.content_type ; 'application/json' ; end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ module Hutch
2
+ module Tracers
3
+ autoload :NullTracer, 'hutch/tracers/null_tracer'
4
+ autoload :NewRelic, 'hutch/tracers/newrelic'
5
+ end
6
+ end
@@ -0,0 +1,19 @@
1
+ require 'newrelic_rpm'
2
+
3
+ module Hutch
4
+ module Tracers
5
+ class NewRelic
6
+ include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation
7
+
8
+ def initialize(klass)
9
+ @klass = klass
10
+ end
11
+
12
+ def handle(message)
13
+ @klass.process(message)
14
+ end
15
+
16
+ add_transaction_tracer :handle, :category => 'OtherTransaction/HutchConsumer', :path => '#{@klass.class.name}'
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Hutch
2
+ module Tracers
3
+ class NullTracer
4
+
5
+ def initialize(klass)
6
+ @klass = klass
7
+ end
8
+
9
+ def handle(message)
10
+ @klass.process(message)
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ module Hutch
2
+ VERSION = '0.19.0'.freeze
3
+ end
4
+
@@ -0,0 +1,143 @@
1
+ require 'hutch/message'
2
+ require 'hutch/logging'
3
+ require 'hutch/broker'
4
+ require 'carrot-top'
5
+
6
+ module Hutch
7
+ class Worker
8
+ include Logging
9
+
10
+ def initialize(broker, consumers)
11
+ @broker = broker
12
+ self.consumers = consumers
13
+ end
14
+
15
+ # Run the main event loop. The consumers will be set up with queues, and
16
+ # process the messages in their respective queues indefinitely. This method
17
+ # never returns.
18
+ def run
19
+ setup_queues
20
+
21
+ # Set up signal handlers for graceful shutdown
22
+ register_signal_handlers
23
+
24
+ main_loop
25
+ end
26
+
27
+ def main_loop
28
+ if defined?(JRUBY_VERSION)
29
+ # Binds shutdown listener to notify main thread if channel was closed
30
+ bind_shutdown_handler
31
+
32
+ handle_signals until shutdown_not_called?(0.1)
33
+ else
34
+ # Take a break from Thread#join every 0.1 seconds to check if we've
35
+ # been sent any signals
36
+ handle_signals until @broker.wait_on_threads(0.1)
37
+ end
38
+ end
39
+
40
+ # Register handlers for SIG{QUIT,TERM,INT} to shut down the worker
41
+ # gracefully. Forceful shutdowns are very bad!
42
+ def register_signal_handlers
43
+ Thread.main[:signal_queue] = []
44
+ %w(QUIT TERM INT).keep_if { |s| Signal.list.keys.include? s }.map(&:to_sym).each do |sig|
45
+ # This needs to be reentrant, so we queue up signals to be handled
46
+ # in the run loop, rather than acting on signals here
47
+ trap(sig) do
48
+ Thread.main[:signal_queue] << sig
49
+ end
50
+ end
51
+ end
52
+
53
+ # Handle any pending signals
54
+ def handle_signals
55
+ signal = Thread.main[:signal_queue].shift
56
+ if signal
57
+ logger.info "caught sig#{signal.downcase}, stopping hutch..."
58
+ stop
59
+ end
60
+ end
61
+
62
+ # Stop a running worker by killing all subscriber threads.
63
+ def stop
64
+ @broker.stop
65
+ end
66
+
67
+ # Binds shutdown handler, called if channel is closed or network Failed
68
+ def bind_shutdown_handler
69
+ @broker.channel.on_shutdown do
70
+ Thread.main[:shutdown_received] = true
71
+ end
72
+ end
73
+
74
+ # Checks if shutdown handler was called, then sleeps for interval
75
+ def shutdown_not_called?(interval)
76
+ if Thread.main[:shutdown_received]
77
+ true
78
+ else
79
+ sleep(interval)
80
+ false
81
+ end
82
+ end
83
+
84
+ # Set up the queues for each of the worker's consumers.
85
+ def setup_queues
86
+ logger.info 'setting up queues'
87
+ @consumers.each { |consumer| setup_queue(consumer) }
88
+ end
89
+
90
+ # Bind a consumer's routing keys to its queue, and set up a subscription to
91
+ # receive messages sent to the queue.
92
+ def setup_queue(consumer)
93
+ queue = @broker.queue(consumer.get_queue_name, consumer.get_arguments)
94
+ @broker.bind_queue(queue, consumer.routing_keys)
95
+
96
+ queue.subscribe(manual_ack: true) do |*args|
97
+ delivery_info, properties, payload = Hutch::Adapter.decode_message(*args)
98
+ handle_message(consumer, delivery_info, properties, payload)
99
+ end
100
+ end
101
+
102
+ # Called internally when a new messages comes in from RabbitMQ. Responsible
103
+ # for wrapping up the message and passing it to the consumer.
104
+ def handle_message(consumer, delivery_info, properties, payload)
105
+ broker = @broker
106
+ begin
107
+ serializer = consumer.get_serializer || Hutch::Config[:serializer]
108
+ logger.info {
109
+ spec = serializer.binary? ? "#{payload.bytesize} bytes" : "#{payload}"
110
+ "message(#{properties.message_id || '-'}): " +
111
+ "routing key: #{delivery_info.routing_key}, " +
112
+ "consumer: #{consumer}, " +
113
+ "payload: #{spec}"
114
+ }
115
+
116
+ message = Message.new(delivery_info, properties, payload, serializer)
117
+ consumer_instance = consumer.new.tap { |c| c.broker, c.delivery_info = @broker, delivery_info }
118
+ with_tracing(consumer_instance).handle(message)
119
+ broker.ack(delivery_info.delivery_tag)
120
+ rescue StandardError => ex
121
+ broker.nack(delivery_info.delivery_tag)
122
+ handle_error(properties.message_id, payload, consumer, ex)
123
+ end
124
+ end
125
+
126
+ def with_tracing(klass)
127
+ Hutch::Config[:tracer].new(klass)
128
+ end
129
+
130
+ def handle_error(message_id, payload, consumer, ex)
131
+ Hutch::Config[:error_handlers].each do |backend|
132
+ backend.handle(message_id, payload, consumer, ex)
133
+ end
134
+ end
135
+
136
+ def consumers=(val)
137
+ if val.empty?
138
+ logger.warn "no consumer loaded, ensure there's no configuration issue"
139
+ end
140
+ @consumers = val
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,377 @@
1
+ require 'spec_helper'
2
+ require 'hutch/broker'
3
+
4
+ describe Hutch::Broker do
5
+ let(:config) { deep_copy(Hutch::Config.user_config) }
6
+ subject(:broker) { Hutch::Broker.new(config) }
7
+
8
+ describe '#connect' do
9
+ before { allow(broker).to receive(:set_up_amqp_connection) }
10
+ before { allow(broker).to receive(:set_up_api_connection) }
11
+ before { allow(broker).to receive(:disconnect) }
12
+
13
+ it 'sets up the amqp connection' do
14
+ expect(broker).to receive(:set_up_amqp_connection)
15
+ broker.connect
16
+ end
17
+
18
+ it 'sets up the api connection' do
19
+ expect(broker).to receive(:set_up_api_connection)
20
+ broker.connect
21
+ end
22
+
23
+ it 'does not disconnect' do
24
+ expect(broker).not_to receive(:disconnect)
25
+ broker.connect
26
+ end
27
+
28
+ context 'when given a block' do
29
+ it 'disconnects' do
30
+ expect(broker).to receive(:disconnect).once
31
+ broker.connect { }
32
+ end
33
+ end
34
+
35
+ context 'when given a block that fails' do
36
+ let(:exception) { Class.new(StandardError) }
37
+
38
+ it 'disconnects' do
39
+ expect(broker).to receive(:disconnect).once
40
+ expect do
41
+ broker.connect { fail exception }
42
+ end.to raise_error(exception)
43
+ end
44
+ end
45
+
46
+ context "with options" do
47
+ let(:options) { { enable_http_api_use: false } }
48
+
49
+ it "doesnt set up api" do
50
+ expect(broker).not_to receive(:set_up_api_connection)
51
+ broker.connect options
52
+ end
53
+ end
54
+ end
55
+
56
+ describe '#set_up_amqp_connection', rabbitmq: true do
57
+ context 'with valid details' do
58
+ before { broker.set_up_amqp_connection }
59
+ after { broker.disconnect }
60
+
61
+ describe '#connection', adapter: :bunny do
62
+ subject { super().connection }
63
+ it { is_expected.to be_a Hutch::Adapters::BunnyAdapter }
64
+ end
65
+
66
+ describe '#connection', adapter: :march_hare do
67
+ subject { super().connection }
68
+ it { is_expected.to be_a Hutch::Adapters::MarchHareAdapter }
69
+ end
70
+
71
+ describe '#channel', adapter: :bunny do
72
+ subject { super().channel }
73
+ it { is_expected.to be_a Bunny::Channel }
74
+ end
75
+
76
+ describe '#channel', adapter: :march_hare do
77
+ subject { super().channel }
78
+ it { is_expected.to be_a MarchHare::Channel }
79
+ end
80
+
81
+ describe '#exchange', adapter: :bunny do
82
+ subject { super().exchange }
83
+ it { is_expected.to be_a Bunny::Exchange }
84
+ end
85
+
86
+ describe '#exchange', adapter: :march_hare do
87
+ subject { super().exchange }
88
+ it { is_expected.to be_a MarchHare::Exchange }
89
+ end
90
+ end
91
+
92
+ context 'when given invalid details' do
93
+ before { config[:mq_host] = 'notarealhost' }
94
+ let(:set_up_amqp_connection) { ->{ broker.set_up_amqp_connection } }
95
+
96
+ specify { expect(set_up_amqp_connection).to raise_error }
97
+ end
98
+
99
+ context 'with channel_prefetch set' do
100
+ let(:prefetch_value) { 1 }
101
+ before { config[:channel_prefetch] = prefetch_value }
102
+ after { broker.disconnect }
103
+
104
+ it "set's channel's prefetch", adapter: :bunny do
105
+ expect_any_instance_of(Bunny::Channel).
106
+ to receive(:prefetch).with(prefetch_value)
107
+ broker.set_up_amqp_connection
108
+ end
109
+
110
+ it "set's channel's prefetch", adapter: :march_hare do
111
+ expect_any_instance_of(MarchHare::Channel).
112
+ to receive(:prefetch=).with(prefetch_value)
113
+ broker.set_up_amqp_connection
114
+ end
115
+ end
116
+
117
+ context 'with force_publisher_confirms set' do
118
+ let(:force_publisher_confirms_value) { true }
119
+ before { config[:force_publisher_confirms] = force_publisher_confirms_value }
120
+ after { broker.disconnect }
121
+
122
+ it 'waits for confirmation', adapter: :bunny do
123
+ expect_any_instance_of(Bunny::Channel).
124
+ to receive(:confirm_select)
125
+ broker.set_up_amqp_connection
126
+ end
127
+
128
+ it 'waits for confirmation', adapter: :march_hare do
129
+ expect_any_instance_of(MarchHare::Channel).
130
+ to receive(:confirm_select)
131
+ broker.set_up_amqp_connection
132
+ end
133
+ end
134
+ end
135
+
136
+ describe '#set_up_api_connection', rabbitmq: true do
137
+ context 'with valid details' do
138
+ before { broker.set_up_api_connection }
139
+ after { broker.disconnect }
140
+
141
+ describe '#api_client' do
142
+ subject { super().api_client }
143
+ it { is_expected.to be_a CarrotTop }
144
+ end
145
+ end
146
+
147
+ context 'when given invalid details' do
148
+ before { config[:mq_api_host] = 'notarealhost' }
149
+ after { broker.disconnect }
150
+ let(:set_up_api_connection) { ->{ broker.set_up_api_connection } }
151
+
152
+ specify { expect(set_up_api_connection).to raise_error }
153
+ end
154
+ end
155
+
156
+ describe '#queue' do
157
+ let(:channel) { double('Channel') }
158
+ let(:arguments) { { foo: :bar } }
159
+ before { allow(broker).to receive(:channel) { channel } }
160
+
161
+ it 'applies a global namespace' do
162
+ config[:namespace] = 'mirror-all.service'
163
+ expect(broker.channel).to receive(:queue) do |*args|
164
+ args.first == ''
165
+ args.last == arguments
166
+ end
167
+ broker.queue('test', arguments)
168
+ end
169
+ end
170
+
171
+ describe '#bindings', rabbitmq: true do
172
+ around { |example| broker.connect { example.run } }
173
+ subject { broker.bindings }
174
+
175
+ context 'with no bindings' do
176
+ describe '#keys' do
177
+ subject { super().keys }
178
+ it { is_expected.not_to include 'test' }
179
+ end
180
+ end
181
+
182
+ context 'with a binding' do
183
+ around do |example|
184
+ queue = broker.queue('test').bind(broker.exchange, routing_key: 'key')
185
+ example.run
186
+ queue.unbind(broker.exchange, routing_key: 'key').delete
187
+ end
188
+
189
+ it { is_expected.to include({ 'test' => ['key'] }) }
190
+ end
191
+ end
192
+
193
+ describe '#bind_queue' do
194
+
195
+ around { |example| broker.connect { example.run } }
196
+
197
+ let(:routing_keys) { %w( a b c ) }
198
+ let(:queue) { double('Queue', bind: nil, unbind: nil, name: 'consumer') }
199
+ before { allow(broker).to receive(:bindings).and_return('consumer' => ['d']) }
200
+
201
+ it 'calls bind for each routing key' do
202
+ routing_keys.each do |key|
203
+ expect(queue).to receive(:bind).with(broker.exchange, routing_key: key)
204
+ end
205
+ broker.bind_queue(queue, routing_keys)
206
+ end
207
+
208
+ it 'calls unbind for each redundant existing binding' do
209
+ expect(queue).to receive(:unbind).with(broker.exchange, routing_key: 'd')
210
+ broker.bind_queue(queue, routing_keys)
211
+ end
212
+
213
+ context '(rabbitmq integration test)', rabbitmq: true do
214
+ let(:queue) { broker.queue('consumer') }
215
+ let(:routing_key) { 'key' }
216
+
217
+ before { allow(broker).to receive(:bindings).and_call_original }
218
+ before { queue.bind(broker.exchange, routing_key: 'redundant-key') }
219
+ after { queue.unbind(broker.exchange, routing_key: routing_key).delete }
220
+
221
+ it 'results in the correct bindings' do
222
+ broker.bind_queue(queue, [routing_key])
223
+ expect(broker.bindings).to include({ queue.name => [routing_key] })
224
+ end
225
+ end
226
+ end
227
+
228
+ describe '#wait_on_threads' do
229
+ let(:thread) { double('Thread') }
230
+ before { allow(broker).to receive(:work_pool_threads).and_return(threads) }
231
+
232
+ context 'when all threads finish within the timeout' do
233
+ let(:threads) { [double(join: thread), double(join: thread)] }
234
+ specify { expect(broker.wait_on_threads(1)).to be_truthy }
235
+ end
236
+
237
+ context 'when timeout expires for one thread' do
238
+ let(:threads) { [double(join: thread), double(join: nil)] }
239
+ specify { expect(broker.wait_on_threads(1)).to be_falsey }
240
+ end
241
+ end
242
+
243
+ describe '#stop', adapter: :bunny do
244
+ let(:thread_1) { double('Thread') }
245
+ let(:thread_2) { double('Thread') }
246
+ let(:work_pool) { double('Bunny::ConsumerWorkPool') }
247
+ let(:config) { { graceful_exit_timeout: 2 } }
248
+
249
+ before do
250
+ allow(broker).to receive(:channel_work_pool).and_return(work_pool)
251
+ end
252
+
253
+ it 'gracefully stops the work pool' do
254
+ expect(work_pool).to receive(:shutdown)
255
+ expect(work_pool).to receive(:join).with(2)
256
+ expect(work_pool).to receive(:kill)
257
+
258
+ broker.stop
259
+ end
260
+ end
261
+
262
+ describe '#stop', adapter: :march_hare do
263
+ let(:channel) { double('MarchHare::Channel')}
264
+
265
+ before do
266
+ allow(broker).to receive(:channel).and_return(channel)
267
+ end
268
+
269
+ it 'gracefully stops the channel' do
270
+ expect(channel).to receive(:close)
271
+
272
+ broker.stop
273
+ end
274
+ end
275
+
276
+ describe '#publish' do
277
+ context 'with a valid connection' do
278
+ before { broker.set_up_amqp_connection }
279
+ after { broker.disconnect }
280
+
281
+ it 'publishes to the exchange' do
282
+ expect(broker.exchange).to receive(:publish).once
283
+ broker.publish('test.key', 'message')
284
+ end
285
+
286
+ it 'sets default properties' do
287
+ expect(broker.exchange).to receive(:publish).with(
288
+ JSON.dump("message"),
289
+ hash_including(
290
+ persistent: true,
291
+ routing_key: 'test.key',
292
+ content_type: 'application/json'
293
+ )
294
+ )
295
+
296
+ broker.publish('test.key', 'message')
297
+ end
298
+
299
+ it 'allows passing message properties' do
300
+ expect(broker.exchange).to receive(:publish).once
301
+ broker.publish('test.key', 'message', {expiration: "2000", persistent: false})
302
+ end
303
+
304
+ context 'when there are global properties' do
305
+ context 'as a hash' do
306
+ before do
307
+ allow(Hutch).to receive(:global_properties).and_return(app_id: 'app')
308
+ end
309
+
310
+ it 'merges the properties' do
311
+ expect(broker.exchange).
312
+ to receive(:publish).with('"message"', hash_including(app_id: 'app'))
313
+ broker.publish('test.key', 'message')
314
+ end
315
+ end
316
+
317
+ context 'as a callable object' do
318
+ before do
319
+ allow(Hutch).to receive(:global_properties).and_return(proc { { app_id: 'app' } })
320
+ end
321
+
322
+ it 'calls the proc and merges the properties' do
323
+ expect(broker.exchange).
324
+ to receive(:publish).with('"message"', hash_including(app_id: 'app'))
325
+ broker.publish('test.key', 'message')
326
+ end
327
+ end
328
+ end
329
+
330
+ context 'with force_publisher_confirms not set in the config' do
331
+ it 'does not wait for confirms on the channel', adapter: :bunny do
332
+ expect_any_instance_of(Bunny::Channel).
333
+ to_not receive(:wait_for_confirms)
334
+ broker.publish('test.key', 'message')
335
+ end
336
+
337
+ it 'does not wait for confirms on the channel', adapter: :march_hare do
338
+ expect_any_instance_of(MarchHare::Channel).
339
+ to_not receive(:wait_for_confirms)
340
+ broker.publish('test.key', 'message')
341
+ end
342
+ end
343
+
344
+ context 'with force_publisher_confirms set in the config' do
345
+ let(:force_publisher_confirms_value) { true }
346
+
347
+ before do
348
+ config[:force_publisher_confirms] = force_publisher_confirms_value
349
+ end
350
+
351
+ it 'waits for confirms on the channel', adapter: :bunny do
352
+ expect_any_instance_of(Bunny::Channel).
353
+ to receive(:wait_for_confirms)
354
+ broker.publish('test.key', 'message')
355
+ end
356
+
357
+ it 'waits for confirms on the channel', adapter: :march_hare do
358
+ expect_any_instance_of(MarchHare::Channel).
359
+ to receive(:wait_for_confirms)
360
+ broker.publish('test.key', 'message')
361
+ end
362
+ end
363
+ end
364
+
365
+ context 'without a valid connection' do
366
+ it 'raises an exception' do
367
+ expect { broker.publish('test.key', 'message') }.
368
+ to raise_exception(Hutch::PublishError)
369
+ end
370
+
371
+ it 'logs an error' do
372
+ expect(broker.logger).to receive(:error)
373
+ broker.publish('test.key', 'message') rescue nil
374
+ end
375
+ end
376
+ end
377
+ end