hutch 0.19.0-java

Sign up to get free protection for your applications and to get access to all the features.
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