hutch 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/Gemfile +18 -0
- data/Guardfile +5 -0
- data/README.md +136 -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 +22 -0
- data/lib/hutch.rb +40 -0
- data/lib/hutch/broker.rb +175 -0
- data/lib/hutch/cli.rb +151 -0
- data/lib/hutch/config.rb +66 -0
- data/lib/hutch/consumer.rb +33 -0
- data/lib/hutch/error_handlers/logger.rb +16 -0
- data/lib/hutch/error_handlers/sentry.rb +23 -0
- data/lib/hutch/exceptions.rb +5 -0
- data/lib/hutch/logging.rb +32 -0
- data/lib/hutch/message.rb +26 -0
- data/lib/hutch/version.rb +4 -0
- data/lib/hutch/worker.rb +104 -0
- data/spec/hutch/broker_spec.rb +157 -0
- data/spec/hutch/config_spec.rb +69 -0
- data/spec/hutch/consumer_spec.rb +80 -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 +35 -0
- data/spec/hutch/worker_spec.rb +80 -0
- data/spec/hutch_spec.rb +16 -0
- data/spec/spec_helper.rb +23 -0
- metadata +144 -0
@@ -0,0 +1,157 @@
|
|
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 { broker.stub(:set_up_amqp_connection) }
|
10
|
+
before { broker.stub(:set_up_api_connection) }
|
11
|
+
before { broker.stub(:disconnect) }
|
12
|
+
|
13
|
+
it 'sets up the amqp connection' do
|
14
|
+
broker.should_receive(:set_up_amqp_connection)
|
15
|
+
broker.connect
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'sets up the api connection' do
|
19
|
+
broker.should_receive(:set_up_api_connection)
|
20
|
+
broker.connect
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'does not disconnect' do
|
24
|
+
broker.should_not_receive(:disconnect)
|
25
|
+
broker.connect
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when given a block' do
|
29
|
+
it 'disconnects' do
|
30
|
+
broker.should_receive(:disconnect).once
|
31
|
+
broker.connect { }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#set_up_amqp_connection', rabbitmq: true do
|
37
|
+
context 'with valid details' do
|
38
|
+
before { broker.set_up_amqp_connection }
|
39
|
+
after { broker.disconnect }
|
40
|
+
|
41
|
+
its(:connection) { should be_a Bunny::Session }
|
42
|
+
its(:channel) { should be_a Bunny::Channel }
|
43
|
+
its(:exchange) { should be_a Bunny::Exchange }
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'when given invalid details' do
|
47
|
+
before { config[:mq_host] = 'notarealhost' }
|
48
|
+
let(:set_up_amqp_connection) { ->{ broker.set_up_amqp_connection } }
|
49
|
+
|
50
|
+
specify { set_up_amqp_connection.should raise_error }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#set_up_api_connection', rabbitmq: true do
|
55
|
+
context 'with valid details' do
|
56
|
+
before { broker.set_up_api_connection }
|
57
|
+
after { broker.disconnect }
|
58
|
+
|
59
|
+
its(:api_client) { should be_a CarrotTop }
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'when given invalid details' do
|
63
|
+
before { config[:mq_api_host] = 'notarealhost' }
|
64
|
+
after { broker.disconnect }
|
65
|
+
let(:set_up_api_connection) { ->{ broker.set_up_api_connection } }
|
66
|
+
|
67
|
+
specify { set_up_api_connection.should raise_error }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '#bindings', rabbitmq: true do
|
72
|
+
around { |example| broker.connect { example.run } }
|
73
|
+
subject { broker.bindings }
|
74
|
+
|
75
|
+
context 'with no bindings' do
|
76
|
+
its(:keys) { should_not include 'test' }
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'with a binding' do
|
80
|
+
around do |example|
|
81
|
+
queue = broker.queue('test').bind(broker.exchange, routing_key: 'key')
|
82
|
+
example.run
|
83
|
+
queue.unbind(broker.exchange, routing_key: 'key').delete
|
84
|
+
end
|
85
|
+
|
86
|
+
it { should include({ 'test' => ['key'] }) }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#bind_queue' do
|
91
|
+
around { |example| broker.connect { example.run } }
|
92
|
+
let(:routing_keys) { %w( a b c ) }
|
93
|
+
let(:queue) { double('Queue', bind: nil, unbind: nil, name: 'consumer') }
|
94
|
+
before { broker.stub(bindings: { 'consumer' => ['d'] }) }
|
95
|
+
|
96
|
+
it 'calls bind for each routing key' do
|
97
|
+
routing_keys.each do |key|
|
98
|
+
queue.should_receive(:bind).with(broker.exchange, routing_key: key)
|
99
|
+
end
|
100
|
+
broker.bind_queue(queue, routing_keys)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'calls unbind for each redundant existing binding' do
|
104
|
+
queue.should_receive(:unbind).with(broker.exchange, routing_key: 'd')
|
105
|
+
broker.bind_queue(queue, routing_keys)
|
106
|
+
end
|
107
|
+
|
108
|
+
context '(rabbitmq integration test)', rabbitmq: true do
|
109
|
+
let(:queue) { broker.queue('consumer') }
|
110
|
+
let(:routing_key) { 'key' }
|
111
|
+
|
112
|
+
before { broker.unstub(:bindings) }
|
113
|
+
before { queue.bind(broker.exchange, routing_key: 'redundant-key') }
|
114
|
+
after { queue.unbind(broker.exchange, routing_key: routing_key).delete }
|
115
|
+
|
116
|
+
it 'results in the correct bindings' do
|
117
|
+
broker.bind_queue(queue, [routing_key])
|
118
|
+
broker.bindings.should include({ queue.name => [routing_key] })
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe '#wait_on_threads' do
|
124
|
+
let(:thread) { double('Thread') }
|
125
|
+
before { broker.stub(work_pool_threads: threads) }
|
126
|
+
|
127
|
+
context 'when all threads finish within the timeout' do
|
128
|
+
let(:threads) { [double(join: thread), double(join: thread)] }
|
129
|
+
specify { expect(broker.wait_on_threads(1)).to be_true }
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'when timeout expires for one thread' do
|
133
|
+
let(:threads) { [double(join: thread), double(join: nil)] }
|
134
|
+
specify { expect(broker.wait_on_threads(1)).to be_false }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe '#publish' do
|
139
|
+
context 'with a valid connection' do
|
140
|
+
before { broker.set_up_amqp_connection }
|
141
|
+
after { broker.disconnect }
|
142
|
+
|
143
|
+
it 'publishes to the exchange' do
|
144
|
+
broker.exchange.should_receive(:publish).once
|
145
|
+
broker.publish('test.key', 'message')
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'without a valid connection' do
|
150
|
+
it 'logs an error' do
|
151
|
+
broker.logger.should_receive(:error)
|
152
|
+
broker.publish('test.key', 'message')
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'hutch/config'
|
2
|
+
|
3
|
+
describe Hutch::Config do
|
4
|
+
let(:new_value) { 'not-localhost' }
|
5
|
+
|
6
|
+
describe '.get' do
|
7
|
+
context 'for valid attributes' do
|
8
|
+
subject { Hutch::Config.get(:mq_host) }
|
9
|
+
|
10
|
+
context 'with no overridden value' do
|
11
|
+
it { should == 'localhost' }
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'with an overridden value' do
|
15
|
+
before { Hutch::Config.stub(user_config: { mq_host: new_value }) }
|
16
|
+
it { should == new_value }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'for invalid attributes' do
|
21
|
+
let(:invalid_get) { ->{ Hutch::Config.get(:invalid_attr) } }
|
22
|
+
specify { invalid_get.should raise_error Hutch::UnknownAttributeError }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '.set' do
|
27
|
+
context 'for valid attributes' do
|
28
|
+
before { Hutch::Config.set(:mq_host, new_value) }
|
29
|
+
subject { Hutch::Config.user_config[:mq_host] }
|
30
|
+
|
31
|
+
context 'sets value in user config hash' do
|
32
|
+
it { should == new_value }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'for invalid attributes' do
|
37
|
+
let(:invalid_set) { ->{ Hutch::Config.set(:invalid_attr, new_value) } }
|
38
|
+
specify { invalid_set.should raise_error Hutch::UnknownAttributeError }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'a magic getter' do
|
43
|
+
context 'for a valid attribute' do
|
44
|
+
it 'calls get' do
|
45
|
+
Hutch::Config.should_receive(:get).with(:mq_host)
|
46
|
+
Hutch::Config.mq_host
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'for an invalid attribute' do
|
51
|
+
let(:invalid_getter) { ->{ Hutch::Config.invalid_attr } }
|
52
|
+
specify { invalid_getter.should raise_error NoMethodError }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'a magic setter' do
|
57
|
+
context 'for a valid attribute' do
|
58
|
+
it 'calls set' do
|
59
|
+
Hutch::Config.should_receive(:set).with(:mq_host, new_value)
|
60
|
+
Hutch::Config.mq_host = new_value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'for an invalid attribute' do
|
65
|
+
let(:invalid_setter) { ->{ Hutch::Config.invalid_attr = new_value } }
|
66
|
+
specify { invalid_setter.should raise_error NoMethodError }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hutch::Consumer do
|
4
|
+
around(:each) do |example|
|
5
|
+
isolate_constants do
|
6
|
+
example.run
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:simple_consumer) do
|
11
|
+
unless defined? SimpleConsumer
|
12
|
+
class SimpleConsumer
|
13
|
+
include Hutch::Consumer
|
14
|
+
consume 'hutch.test1'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
SimpleConsumer
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:complex_consumer) do
|
21
|
+
unless defined? ComplexConsumer
|
22
|
+
class ComplexConsumer
|
23
|
+
include Hutch::Consumer
|
24
|
+
consume 'hutch.test1', 'hutch.test2'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
ComplexConsumer
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'module inclusion' do
|
31
|
+
it 'registers the class as a consumer' do
|
32
|
+
Hutch.should_receive(:register_consumer) do |klass|
|
33
|
+
klass.should == simple_consumer
|
34
|
+
end
|
35
|
+
|
36
|
+
simple_consumer
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
describe '.consume' do
|
42
|
+
it 'saves the routing key to the consumer' do
|
43
|
+
simple_consumer.routing_keys.should include 'hutch.test1'
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'with multiple routing keys' do
|
47
|
+
it 'registers the class once for each routing key' do
|
48
|
+
complex_consumer.routing_keys.should include 'hutch.test1'
|
49
|
+
complex_consumer.routing_keys.should include 'hutch.test2'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when given the same routing key multiple times' do
|
54
|
+
subject { simple_consumer.routing_keys }
|
55
|
+
before { simple_consumer.consume 'hutch.test1' }
|
56
|
+
its(:length) { should == 1}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '.queue_name' do
|
61
|
+
it 'replaces module separators with colons' do
|
62
|
+
module Foo
|
63
|
+
class Bar
|
64
|
+
include Hutch::Consumer
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
Foo::Bar.queue_name.should == 'foo:bar'
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'converts camelcase class names to snake case' do
|
72
|
+
class FooBarBAZ
|
73
|
+
include Hutch::Consumer
|
74
|
+
end
|
75
|
+
|
76
|
+
FooBarBAZ.queue_name.should == 'foo_bar_baz'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hutch::ErrorHandlers::Logger do
|
4
|
+
let(:error_handler) { Hutch::ErrorHandlers::Logger.new }
|
5
|
+
|
6
|
+
describe '#handle' do
|
7
|
+
let(:error) { stub(message: "Stuff went wrong", class: "RuntimeError",
|
8
|
+
backtrace: ["line 1", "line 2"]) }
|
9
|
+
|
10
|
+
it "logs three separate lines" do
|
11
|
+
Hutch::Logging.logger.should_receive(:error).exactly(3).times
|
12
|
+
error_handler.handle("1", stub, error)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hutch::ErrorHandlers::Sentry do
|
4
|
+
let(:error_handler) { Hutch::ErrorHandlers::Sentry.new }
|
5
|
+
|
6
|
+
describe '#handle' do
|
7
|
+
let(:error) do
|
8
|
+
begin
|
9
|
+
raise "Stuff went wrong"
|
10
|
+
rescue RuntimeError => err
|
11
|
+
err
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it "logs the error to Sentry" do
|
16
|
+
Raven.should_receive(:capture_exception).with(error)
|
17
|
+
error_handler.handle("1", stub, error)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Hutch::Logging do
|
4
|
+
let(:dummy_object) do
|
5
|
+
class DummyObject
|
6
|
+
include Hutch::Logging
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '#logger' do
|
11
|
+
context 'with the default logger' do
|
12
|
+
subject { Hutch::Logging.logger }
|
13
|
+
|
14
|
+
it { should be_instance_of(Logger) }
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with a custom logger' do
|
18
|
+
let(:dummy_logger) { mock("Dummy logger", warn: true, info: true) }
|
19
|
+
after { Hutch::Logging.setup_logger }
|
20
|
+
|
21
|
+
it "users the custom logger" do
|
22
|
+
Hutch::Logging.logger = dummy_logger
|
23
|
+
Hutch::Logging.logger.should == dummy_logger
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'hutch/message'
|
2
|
+
|
3
|
+
describe Hutch::Message do
|
4
|
+
let(:delivery_info) { double('Delivery Info') }
|
5
|
+
let(:props) { double('Properties') }
|
6
|
+
let(:body) {{ foo: 'bar' }}
|
7
|
+
let(:json_body) { MultiJson.dump(body) }
|
8
|
+
subject(:message) { Hutch::Message.new(delivery_info, props, json_body) }
|
9
|
+
|
10
|
+
its(:body) { should == body }
|
11
|
+
|
12
|
+
describe '[]' do
|
13
|
+
subject { message[:foo] }
|
14
|
+
it { should == 'bar' }
|
15
|
+
end
|
16
|
+
|
17
|
+
[:message_id, :timestamp].each do |method|
|
18
|
+
describe method.to_s do
|
19
|
+
it 'delegates to @properties' do
|
20
|
+
props.should_receive(method)
|
21
|
+
message.send(method)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
[:routing_key, :exchange].each do |method|
|
27
|
+
describe method.to_s do
|
28
|
+
it 'delegates to @delivery_info' do
|
29
|
+
delivery_info.should_receive(method)
|
30
|
+
message.send(method)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'hutch/worker'
|
3
|
+
|
4
|
+
describe Hutch::Worker do
|
5
|
+
before { Raven.as_null_object }
|
6
|
+
let(:consumer) { double('Consumer', routing_keys: %w( a b c ),
|
7
|
+
queue_name: 'consumer') }
|
8
|
+
let(:consumers) { [consumer, double('Consumer')] }
|
9
|
+
let(:broker) { Hutch::Broker.new }
|
10
|
+
subject(:worker) { Hutch::Worker.new(broker, consumers) }
|
11
|
+
|
12
|
+
describe '#setup_queues' do
|
13
|
+
it 'sets up queues for each of the consumers' do
|
14
|
+
consumers.each do |consumer|
|
15
|
+
worker.should_receive(:setup_queue).with(consumer)
|
16
|
+
end
|
17
|
+
worker.setup_queues
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#setup_queue' do
|
22
|
+
let(:queue) { double('Queue', bind: nil, subscribe: nil) }
|
23
|
+
before { worker.stub(consumer_queue: queue) }
|
24
|
+
before { broker.stub(queue: queue, bind_queue: nil) }
|
25
|
+
|
26
|
+
it 'creates a queue' do
|
27
|
+
broker.should_receive(:queue).with(consumer.queue_name).and_return(queue)
|
28
|
+
worker.setup_queue(consumer)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'binds the queue to each of the routing keys' do
|
32
|
+
broker.should_receive(:bind_queue).with(queue, %w( a b c ))
|
33
|
+
worker.setup_queue(consumer)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'sets up a subscription' do
|
37
|
+
queue.should_receive(:subscribe).with(ack: true)
|
38
|
+
worker.setup_queue(consumer)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#handle_message' do
|
43
|
+
let(:payload) { '{}' }
|
44
|
+
let(:consumer_instance) { double('Consumer instance') }
|
45
|
+
let(:delivery_info) { double('Delivery Info', routing_key: '',
|
46
|
+
delivery_tag: 'dt') }
|
47
|
+
let(:properties) { double('Properties', message_id: nil) }
|
48
|
+
before { consumer.stub(new: consumer_instance) }
|
49
|
+
before { broker.stub(:ack) }
|
50
|
+
|
51
|
+
it 'passes the message to the consumer' do
|
52
|
+
consumer_instance.should_receive(:process).
|
53
|
+
with(an_instance_of(Hutch::Message))
|
54
|
+
worker.handle_message(consumer, delivery_info, properties, payload)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'acknowledges the message' do
|
58
|
+
consumer_instance.stub(:process)
|
59
|
+
broker.should_receive(:ack).with(delivery_info.delivery_tag)
|
60
|
+
worker.handle_message(consumer, delivery_info, properties, payload)
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'when the consumer raises an exception' do
|
64
|
+
before { consumer_instance.stub(:process).and_raise('a consumer error') }
|
65
|
+
|
66
|
+
it 'logs the error' do
|
67
|
+
Hutch::Config[:error_handlers].each do |backend|
|
68
|
+
backend.should_receive(:handle)
|
69
|
+
end
|
70
|
+
worker.handle_message(consumer, delivery_info, properties, payload)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'acknowledges the message' do
|
74
|
+
broker.should_receive(:ack).with(delivery_info.delivery_tag)
|
75
|
+
worker.handle_message(consumer, delivery_info, properties, payload)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|