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.
@@ -1,4 +1,4 @@
1
1
  module Hutch
2
- VERSION = '0.21.0'.freeze
2
+ VERSION = '0.22.1'.freeze
3
3
  end
4
4
 
@@ -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
@@ -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
- # Set up signal handlers for graceful shutdown
25
- register_signal_handlers
27
+ Waiter.wait_until_signaled
26
28
 
27
- main_loop
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.info {
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
- def supported_shutdown_signals
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
@@ -0,0 +1,2 @@
1
+ YARD::Templates::Engine.register_template_path(File.dirname(__FILE__) + '/../../templates')
2
+ require File.join(File.dirname(__FILE__), 'handler') if RUBY19
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
  require 'hutch/broker'
3
3
 
4
4
  describe Hutch::Broker do
5
- let(:config) { deep_copy(Hutch::Config.user_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', rabbitmq: true do
57
- context 'with valid details' do
58
- before { broker.set_up_amqp_connection }
59
- after { broker.disconnect }
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
- describe '#connection', adapter: :bunny do
62
- subject { super().connection }
63
- it { is_expected.to be_a Hutch::Adapters::BunnyAdapter }
64
- end
62
+ broker.set_up_amqp_connection
63
+ end
64
+ end
65
65
 
66
- describe '#connection', adapter: :march_hare do
67
- subject { super().connection }
68
- it { is_expected.to be_a Hutch::Adapters::MarchHareAdapter }
69
- end
66
+ describe '#open_connection', rabbitmq: true do
67
+ describe 'return value' do
68
+ subject { broker.open_connection }
69
+ after { subject.close }
70
70
 
71
- describe '#channel', adapter: :bunny do
72
- subject { super().channel }
73
- it { is_expected.to be_a Bunny::Channel }
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
- describe '#channel', adapter: :march_hare do
77
- subject { super().channel }
78
- it { is_expected.to be_a MarchHare::Channel }
79
- end
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
- describe '#exchange', adapter: :bunny do
82
- subject { super().exchange }
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
- describe '#exchange', adapter: :march_hare do
87
- subject { super().exchange }
88
- it { is_expected.to be_a MarchHare::Exchange }
89
- end
83
+ expect(broker.connection).to be_nil
84
+
85
+ connection.close
90
86
  end
87
+ end
91
88
 
92
- context 'when given invalid details' do
93
- before { config[:mq_host] = 'notarealhost' }
94
- let(:set_up_amqp_connection) { ->{ broker.set_up_amqp_connection } }
89
+ describe '#open_connection!' do
90
+ it 'sets the #connection to #open_connection' do
91
+ connection = double('connection').as_null_object
95
92
 
96
- specify { expect(set_up_amqp_connection).to raise_error }
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
- to receive(:prefetch).with(prefetch_value)
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
- to receive(:prefetch=).with(prefetch_value)
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
- to receive(:confirm_select)
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
- to receive(:confirm_select)
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)
@@ -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