hutch 0.21.0 → 0.22.1
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 +4 -4
- data/.gitignore +2 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +55 -1
- data/Gemfile +1 -1
- data/README.md +2 -2
- data/Rakefile +8 -1
- data/hutch.gemspec +4 -1
- data/lib/hutch.rb +11 -8
- data/lib/hutch/adapters/march_hare.rb +1 -1
- data/lib/hutch/broker.rb +89 -110
- data/lib/hutch/cli.rb +34 -10
- data/lib/hutch/config.rb +192 -55
- data/lib/hutch/error_handlers/sentry.rb +1 -1
- data/lib/hutch/logging.rb +1 -0
- data/lib/hutch/publisher.rb +75 -0
- data/lib/hutch/version.rb +1 -1
- data/lib/hutch/waiter.rb +41 -0
- data/lib/hutch/worker.rb +8 -60
- data/lib/yard-settings/handler.rb +38 -0
- data/lib/yard-settings/yard-settings.rb +2 -0
- data/spec/hutch/broker_spec.rb +107 -64
- data/spec/hutch/cli_spec.rb +3 -3
- data/spec/hutch/config_spec.rb +60 -22
- data/spec/hutch/error_handlers/sentry_spec.rb +1 -1
- data/spec/hutch/logger_spec.rb +12 -6
- data/spec/hutch/waiter_spec.rb +33 -0
- data/spec/hutch/worker_spec.rb +13 -2
- data/spec/spec_helper.rb +7 -5
- data/templates/default/class/html/settings.erb +0 -0
- data/templates/default/class/setup.rb +4 -0
- data/templates/default/fulldoc/html/css/hutch.css +13 -0
- data/templates/default/layout/html/setup.rb +7 -0
- data/templates/default/method_details/html/settings.erb +5 -0
- data/templates/default/method_details/setup.rb +4 -0
- data/templates/default/method_details/text/settings.erb +0 -0
- data/templates/default/module/html/settings.erb +40 -0
- data/templates/default/module/setup.rb +4 -0
- metadata +63 -5
data/lib/hutch/version.rb
CHANGED
data/lib/hutch/waiter.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'hutch/logging'
|
2
|
+
|
3
|
+
module Hutch
|
4
|
+
class Waiter
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
SHUTDOWN_SIGNALS = %w(QUIT TERM INT)
|
8
|
+
|
9
|
+
def self.wait_until_signaled
|
10
|
+
new.wait_until_signaled
|
11
|
+
end
|
12
|
+
|
13
|
+
def wait_until_signaled
|
14
|
+
self.sig_read, self.sig_write = IO.pipe
|
15
|
+
|
16
|
+
register_signal_handlers
|
17
|
+
wait_for_signal
|
18
|
+
|
19
|
+
sig = sig_read.gets.strip.downcase
|
20
|
+
logger.info "caught sig#{sig}, stopping hutch..."
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_accessor :sig_read, :sig_write
|
26
|
+
|
27
|
+
def wait_for_signal
|
28
|
+
IO.select([sig_read])
|
29
|
+
end
|
30
|
+
|
31
|
+
def register_signal_handlers
|
32
|
+
SHUTDOWN_SIGNALS.each do |sig|
|
33
|
+
# This needs to be reentrant, so we queue up signals to be handled
|
34
|
+
# in the run loop, rather than acting on signals here
|
35
|
+
trap(sig) do
|
36
|
+
sig_write.puts(sig)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/hutch/worker.rb
CHANGED
@@ -2,6 +2,7 @@ require 'hutch/message'
|
|
2
2
|
require 'hutch/logging'
|
3
3
|
require 'hutch/broker'
|
4
4
|
require 'hutch/acknowledgements/nack_on_all_failures'
|
5
|
+
require 'hutch/waiter'
|
5
6
|
require 'carrot-top'
|
6
7
|
|
7
8
|
module Hutch
|
@@ -10,9 +11,10 @@ module Hutch
|
|
10
11
|
|
11
12
|
SHUTDOWN_SIGNALS = %w(QUIT TERM INT)
|
12
13
|
|
13
|
-
def initialize(broker, consumers)
|
14
|
+
def initialize(broker, consumers, setup_procs)
|
14
15
|
@broker = broker
|
15
16
|
self.consumers = consumers
|
17
|
+
self.setup_procs = setup_procs
|
16
18
|
end
|
17
19
|
|
18
20
|
# Run the main event loop. The consumers will be set up with queues, and
|
@@ -20,46 +22,11 @@ module Hutch
|
|
20
22
|
# never returns.
|
21
23
|
def run
|
22
24
|
setup_queues
|
25
|
+
setup_procs.each(&:call)
|
23
26
|
|
24
|
-
|
25
|
-
register_signal_handlers
|
27
|
+
Waiter.wait_until_signaled
|
26
28
|
|
27
|
-
|
28
|
-
end
|
29
|
-
|
30
|
-
def main_loop
|
31
|
-
if defined?(JRUBY_VERSION)
|
32
|
-
# Binds shutdown listener to notify main thread if channel was closed
|
33
|
-
bind_shutdown_handler
|
34
|
-
|
35
|
-
handle_signals until shutdown_not_called?(0.1)
|
36
|
-
else
|
37
|
-
# Take a break from Thread#join every 0.1 seconds to check if we've
|
38
|
-
# been sent any signals
|
39
|
-
handle_signals until @broker.wait_on_threads(0.1)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
# Register handlers for SIG{QUIT,TERM,INT} to shut down the worker
|
44
|
-
# gracefully. Forceful shutdowns are very bad!
|
45
|
-
def register_signal_handlers
|
46
|
-
Thread.main[:signal_queue] = []
|
47
|
-
supported_shutdown_signals.each do |sig|
|
48
|
-
# This needs to be reentrant, so we queue up signals to be handled
|
49
|
-
# in the run loop, rather than acting on signals here
|
50
|
-
trap(sig) do
|
51
|
-
Thread.main[:signal_queue] << sig
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
# Handle any pending signals
|
57
|
-
def handle_signals
|
58
|
-
signal = Thread.main[:signal_queue].shift
|
59
|
-
if signal
|
60
|
-
logger.info "caught sig#{signal.downcase}, stopping hutch..."
|
61
|
-
stop
|
62
|
-
end
|
29
|
+
stop
|
63
30
|
end
|
64
31
|
|
65
32
|
# Stop a running worker by killing all subscriber threads.
|
@@ -67,23 +34,6 @@ module Hutch
|
|
67
34
|
@broker.stop
|
68
35
|
end
|
69
36
|
|
70
|
-
# Binds shutdown handler, called if channel is closed or network Failed
|
71
|
-
def bind_shutdown_handler
|
72
|
-
@broker.channel.on_shutdown do
|
73
|
-
Thread.main[:shutdown_received] = true
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# Checks if shutdown handler was called, then sleeps for interval
|
78
|
-
def shutdown_not_called?(interval)
|
79
|
-
if Thread.main[:shutdown_received]
|
80
|
-
true
|
81
|
-
else
|
82
|
-
sleep(interval)
|
83
|
-
false
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
37
|
# Set up the queues for each of the worker's consumers.
|
88
38
|
def setup_queues
|
89
39
|
logger.info 'setting up queues'
|
@@ -106,7 +56,7 @@ module Hutch
|
|
106
56
|
# for wrapping up the message and passing it to the consumer.
|
107
57
|
def handle_message(consumer, delivery_info, properties, payload)
|
108
58
|
serializer = consumer.get_serializer || Hutch::Config[:serializer]
|
109
|
-
logger.
|
59
|
+
logger.debug {
|
110
60
|
spec = serializer.binary? ? "#{payload.bytesize} bytes" : "#{payload}"
|
111
61
|
"message(#{properties.message_id || '-'}): " +
|
112
62
|
"routing key: #{delivery_info.routing_key}, " +
|
@@ -154,8 +104,6 @@ module Hutch
|
|
154
104
|
|
155
105
|
private
|
156
106
|
|
157
|
-
|
158
|
-
SHUTDOWN_SIGNALS.keep_if { |s| Signal.list.keys.include? s }.map(&:to_sym)
|
159
|
-
end
|
107
|
+
attr_accessor :setup_procs
|
160
108
|
end
|
161
109
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# :nodoc:
|
2
|
+
class SettingsHandlerBase < YARD::Handlers::Ruby::Base
|
3
|
+
handles method_call :string_setting
|
4
|
+
handles method_call :number_setting
|
5
|
+
handles method_call :boolean_setting
|
6
|
+
|
7
|
+
namespace_only
|
8
|
+
|
9
|
+
def process
|
10
|
+
name = statement.parameters.first.jump(:tstring_content, :ident).source
|
11
|
+
object = YARD::CodeObjects::MethodObject.new(namespace, name)
|
12
|
+
register(object)
|
13
|
+
|
14
|
+
# Modify the code object for the new instance method
|
15
|
+
object.dynamic = true
|
16
|
+
# Add custom metadata to the object
|
17
|
+
object['custom_field'] = '(Found using method_missing)'
|
18
|
+
|
19
|
+
# Module-level configuration notes
|
20
|
+
hutch_config = YARD::CodeObjects::ModuleObject.new(:root, "Hutch::Config")
|
21
|
+
collection_name = statement.first.first
|
22
|
+
default_value = statement.parameters[1].jump(:tstring_content, :ident).source
|
23
|
+
|
24
|
+
(hutch_config['setting_rows'] ||= []) << {
|
25
|
+
name: name,
|
26
|
+
default_value: default_value,
|
27
|
+
type: collection_name.sub('_setting', '').capitalize,
|
28
|
+
description: object.docstring,
|
29
|
+
first_line_of_description: first_line_of_description(object)
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def first_line_of_description(object)
|
34
|
+
return '' if object.docstring.blank?
|
35
|
+
|
36
|
+
object.docstring.lines.first
|
37
|
+
end
|
38
|
+
end
|
data/spec/hutch/broker_spec.rb
CHANGED
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
require 'hutch/broker'
|
3
3
|
|
4
4
|
describe Hutch::Broker do
|
5
|
-
let(:config) {
|
5
|
+
let(:config) { Hutch::Config.initialize(client_logger: Hutch::Logging.logger) }
|
6
6
|
subject(:broker) { Hutch::Broker.new(config) }
|
7
7
|
|
8
8
|
describe '#connect' do
|
@@ -53,86 +53,142 @@ describe Hutch::Broker do
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
-
describe '#set_up_amqp_connection'
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
describe '#set_up_amqp_connection' do
|
57
|
+
it 'opens a connection, channel and declares an exchange' do
|
58
|
+
expect(broker).to receive(:open_connection!).ordered
|
59
|
+
expect(broker).to receive(:open_channel!).ordered
|
60
|
+
expect(broker).to receive(:declare_exchange!).ordered
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
end
|
62
|
+
broker.set_up_amqp_connection
|
63
|
+
end
|
64
|
+
end
|
65
65
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
66
|
+
describe '#open_connection', rabbitmq: true do
|
67
|
+
describe 'return value' do
|
68
|
+
subject { broker.open_connection }
|
69
|
+
after { subject.close }
|
70
70
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
end
|
71
|
+
it(nil, adapter: :bunny) { is_expected.to be_a Hutch::Adapters::BunnyAdapter }
|
72
|
+
it(nil, adapter: :march_hare) { is_expected.to be_a Hutch::Adapters::MarchHareAdapter }
|
73
|
+
end
|
75
74
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
75
|
+
context 'when given invalid details' do
|
76
|
+
before { config[:mq_host] = 'notarealhost' }
|
77
|
+
it { expect { broker.open_connection }.to raise_error(StandardError) }
|
78
|
+
end
|
80
79
|
|
81
|
-
|
82
|
-
|
83
|
-
it { is_expected.to be_a Bunny::Exchange }
|
84
|
-
end
|
80
|
+
it 'does not set #connection' do
|
81
|
+
connection = broker.open_connection
|
85
82
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
end
|
83
|
+
expect(broker.connection).to be_nil
|
84
|
+
|
85
|
+
connection.close
|
90
86
|
end
|
87
|
+
end
|
91
88
|
|
92
|
-
|
93
|
-
|
94
|
-
|
89
|
+
describe '#open_connection!' do
|
90
|
+
it 'sets the #connection to #open_connection' do
|
91
|
+
connection = double('connection').as_null_object
|
95
92
|
|
96
|
-
|
93
|
+
expect(broker).to receive(:open_connection).and_return(connection)
|
94
|
+
|
95
|
+
broker.open_connection!
|
96
|
+
|
97
|
+
expect(broker.connection).to eq(connection)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe '#open_channel', rabbitmq: true do
|
102
|
+
before { broker.open_connection! }
|
103
|
+
after { broker.disconnect }
|
104
|
+
|
105
|
+
describe 'return value' do
|
106
|
+
subject { broker.open_channel }
|
107
|
+
|
108
|
+
it(nil, adapter: :bunny) { is_expected.to be_a Bunny::Channel }
|
109
|
+
it(nil, adapter: :march_hare) { is_expected.to be_a MarchHare::Channel }
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'does not set #channel' do
|
113
|
+
broker.open_channel
|
114
|
+
expect(broker.channel).to be_nil
|
97
115
|
end
|
98
116
|
|
99
117
|
context 'with channel_prefetch set' do
|
100
118
|
let(:prefetch_value) { 1 }
|
101
119
|
before { config[:channel_prefetch] = prefetch_value }
|
102
|
-
after { broker.disconnect }
|
103
120
|
|
104
121
|
it "set's channel's prefetch", adapter: :bunny do
|
105
|
-
expect_any_instance_of(Bunny::Channel).
|
106
|
-
|
107
|
-
broker.set_up_amqp_connection
|
122
|
+
expect_any_instance_of(Bunny::Channel).to receive(:prefetch).with(prefetch_value)
|
123
|
+
broker.open_channel
|
108
124
|
end
|
109
125
|
|
110
126
|
it "set's channel's prefetch", adapter: :march_hare do
|
111
|
-
expect_any_instance_of(MarchHare::Channel).
|
112
|
-
|
113
|
-
broker.set_up_amqp_connection
|
127
|
+
expect_any_instance_of(MarchHare::Channel).to receive(:prefetch=).with(prefetch_value)
|
128
|
+
broker.open_channel
|
114
129
|
end
|
115
130
|
end
|
116
131
|
|
117
132
|
context 'with force_publisher_confirms set' do
|
118
133
|
let(:force_publisher_confirms_value) { true }
|
119
134
|
before { config[:force_publisher_confirms] = force_publisher_confirms_value }
|
120
|
-
after { broker.disconnect }
|
121
135
|
|
122
136
|
it 'waits for confirmation', adapter: :bunny do
|
123
|
-
expect_any_instance_of(Bunny::Channel).
|
124
|
-
|
125
|
-
broker.set_up_amqp_connection
|
137
|
+
expect_any_instance_of(Bunny::Channel).to receive(:confirm_select)
|
138
|
+
broker.open_channel
|
126
139
|
end
|
127
140
|
|
128
141
|
it 'waits for confirmation', adapter: :march_hare do
|
129
|
-
expect_any_instance_of(MarchHare::Channel).
|
130
|
-
|
131
|
-
broker.set_up_amqp_connection
|
142
|
+
expect_any_instance_of(MarchHare::Channel).to receive(:confirm_select)
|
143
|
+
broker.open_channel
|
132
144
|
end
|
133
145
|
end
|
134
146
|
end
|
135
147
|
|
148
|
+
describe '#open_channel!' do
|
149
|
+
it 'sets the #channel to #open_channel' do
|
150
|
+
channel = double('channel').as_null_object
|
151
|
+
|
152
|
+
expect(broker).to receive(:open_channel).and_return(channel)
|
153
|
+
|
154
|
+
broker.open_channel!
|
155
|
+
|
156
|
+
expect(broker.channel).to eq(channel)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe '#declare_exchange' do
|
161
|
+
before do
|
162
|
+
broker.open_connection!
|
163
|
+
broker.open_channel!
|
164
|
+
end
|
165
|
+
after { broker.disconnect }
|
166
|
+
|
167
|
+
describe 'return value' do
|
168
|
+
subject { broker.declare_exchange }
|
169
|
+
|
170
|
+
it(nil, adapter: :bunny) { is_expected.to be_a Bunny::Exchange }
|
171
|
+
it(nil, adapter: :march_hare) { is_expected.to be_a MarchHare::Exchange }
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'does not set #exchange' do
|
175
|
+
broker.declare_exchange
|
176
|
+
expect(broker.exchange).to be_nil
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
describe '#declare_exchange!' do
|
181
|
+
it 'sets the #exchange to #declare_exchange' do
|
182
|
+
exchange = double('exchange').as_null_object
|
183
|
+
|
184
|
+
expect(broker).to receive(:declare_exchange).and_return(exchange)
|
185
|
+
|
186
|
+
broker.declare_exchange!
|
187
|
+
|
188
|
+
expect(broker.exchange).to eq(exchange)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
136
192
|
describe '#set_up_api_connection', rabbitmq: true do
|
137
193
|
context 'with valid details' do
|
138
194
|
before { broker.set_up_api_connection }
|
@@ -149,7 +205,7 @@ describe Hutch::Broker do
|
|
149
205
|
after { broker.disconnect }
|
150
206
|
let(:set_up_api_connection) { ->{ broker.set_up_api_connection } }
|
151
207
|
|
152
|
-
specify { expect(set_up_api_connection).to raise_error }
|
208
|
+
specify { expect(set_up_api_connection).to raise_error(StandardError) }
|
153
209
|
end
|
154
210
|
end
|
155
211
|
|
@@ -192,7 +248,7 @@ describe Hutch::Broker do
|
|
192
248
|
|
193
249
|
describe '#bind_queue' do
|
194
250
|
|
195
|
-
around { |example| broker.connect { example.run } }
|
251
|
+
around { |example| broker.connect(host: "127.0.0.1") { example.run } }
|
196
252
|
|
197
253
|
let(:routing_keys) { %w( a b c ) }
|
198
254
|
let(:queue) { double('Queue', bind: nil, unbind: nil, name: 'consumer') }
|
@@ -225,21 +281,6 @@ describe Hutch::Broker do
|
|
225
281
|
end
|
226
282
|
end
|
227
283
|
|
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
284
|
describe '#stop', adapter: :bunny do
|
244
285
|
let(:thread_1) { double('Thread') }
|
245
286
|
let(:thread_2) { double('Thread') }
|
@@ -363,6 +404,8 @@ describe Hutch::Broker do
|
|
363
404
|
end
|
364
405
|
|
365
406
|
context 'without a valid connection' do
|
407
|
+
before { broker.set_up_amqp_connection; broker.disconnect }
|
408
|
+
|
366
409
|
it 'raises an exception' do
|
367
410
|
expect { broker.publish('test.key', 'message') }.
|
368
411
|
to raise_exception(Hutch::PublishError)
|
data/spec/hutch/cli_spec.rb
CHANGED
@@ -13,7 +13,7 @@ describe Hutch::CLI do
|
|
13
13
|
it "bails" do
|
14
14
|
expect {
|
15
15
|
cli.parse_options(["--config=#{file}"])
|
16
|
-
}.to raise_error SystemExit
|
16
|
+
}.to raise_error SystemExit, "Config file '/path/to/nonexistant/file' not found"
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
@@ -37,7 +37,7 @@ describe Hutch::CLI do
|
|
37
37
|
it "bails" do
|
38
38
|
expect {
|
39
39
|
cli.parse_options(["--mq-tls-key=#{file}"])
|
40
|
-
}.to raise_error SystemExit
|
40
|
+
}.to raise_error SystemExit, "Private key file '/path/to/nonexistant/file' not found"
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
@@ -61,7 +61,7 @@ describe Hutch::CLI do
|
|
61
61
|
it "bails" do
|
62
62
|
expect {
|
63
63
|
cli.parse_options(["--mq-tls-cert=#{file}"])
|
64
|
-
}.to raise_error SystemExit
|
64
|
+
}.to raise_error SystemExit, "Certificate file '/path/to/nonexistant/file' not found"
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|