hutch 0.19.0-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.travis.yml +11 -0
- data/CHANGELOG.md +438 -0
- data/Gemfile +22 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +317 -0
- data/Rakefile +14 -0
- data/bin/hutch +8 -0
- data/circle.yml +3 -0
- data/examples/consumer.rb +13 -0
- data/examples/producer.rb +10 -0
- data/hutch.gemspec +30 -0
- data/lib/hutch.rb +62 -0
- data/lib/hutch/adapter.rb +11 -0
- data/lib/hutch/adapters/bunny.rb +33 -0
- data/lib/hutch/adapters/march_hare.rb +37 -0
- data/lib/hutch/broker.rb +374 -0
- data/lib/hutch/cli.rb +205 -0
- data/lib/hutch/config.rb +125 -0
- data/lib/hutch/consumer.rb +75 -0
- data/lib/hutch/error_handlers.rb +8 -0
- data/lib/hutch/error_handlers/airbrake.rb +26 -0
- data/lib/hutch/error_handlers/honeybadger.rb +28 -0
- data/lib/hutch/error_handlers/logger.rb +16 -0
- data/lib/hutch/error_handlers/sentry.rb +23 -0
- data/lib/hutch/exceptions.rb +7 -0
- data/lib/hutch/logging.rb +32 -0
- data/lib/hutch/message.rb +31 -0
- data/lib/hutch/serializers/identity.rb +19 -0
- data/lib/hutch/serializers/json.rb +22 -0
- data/lib/hutch/tracers.rb +6 -0
- data/lib/hutch/tracers/newrelic.rb +19 -0
- data/lib/hutch/tracers/null_tracer.rb +15 -0
- data/lib/hutch/version.rb +4 -0
- data/lib/hutch/worker.rb +143 -0
- data/spec/hutch/broker_spec.rb +377 -0
- data/spec/hutch/cli_spec.rb +80 -0
- data/spec/hutch/config_spec.rb +126 -0
- data/spec/hutch/consumer_spec.rb +130 -0
- data/spec/hutch/error_handlers/airbrake_spec.rb +34 -0
- data/spec/hutch/error_handlers/honeybadger_spec.rb +36 -0
- data/spec/hutch/error_handlers/logger_spec.rb +15 -0
- data/spec/hutch/error_handlers/sentry_spec.rb +20 -0
- data/spec/hutch/logger_spec.rb +28 -0
- data/spec/hutch/message_spec.rb +38 -0
- data/spec/hutch/serializers/json_spec.rb +17 -0
- data/spec/hutch/worker_spec.rb +99 -0
- data/spec/hutch_spec.rb +87 -0
- data/spec/spec_helper.rb +40 -0
- 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,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,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
|
data/lib/hutch/worker.rb
ADDED
@@ -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
|