hutch 0.1.0
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 +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
|
+
|