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.
- 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
|