dkastner-hutch 0.17.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 +7 -0
- data/.gitignore +7 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +397 -0
- data/Gemfile +22 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +315 -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 +24 -0
- data/lib/hutch/broker.rb +356 -0
- data/lib/hutch/cli.rb +205 -0
- data/lib/hutch/config.rb +121 -0
- data/lib/hutch/consumer.rb +66 -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/error_handlers.rb +8 -0
- data/lib/hutch/exceptions.rb +7 -0
- data/lib/hutch/logging.rb +32 -0
- data/lib/hutch/message.rb +33 -0
- data/lib/hutch/tracers/newrelic.rb +19 -0
- data/lib/hutch/tracers/null_tracer.rb +15 -0
- data/lib/hutch/tracers.rb +6 -0
- data/lib/hutch/version.rb +4 -0
- data/lib/hutch/worker.rb +110 -0
- data/lib/hutch.rb +60 -0
- data/spec/hutch/broker_spec.rb +325 -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/worker_spec.rb +98 -0
- data/spec/hutch_spec.rb +87 -0
- data/spec/spec_helper.rb +35 -0
- metadata +187 -0
data/lib/hutch/broker.rb
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
require 'bunny'
|
|
2
|
+
require 'carrot-top'
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'hutch/logging'
|
|
5
|
+
require 'hutch/exceptions'
|
|
6
|
+
|
|
7
|
+
module Hutch
|
|
8
|
+
class Broker
|
|
9
|
+
include Logging
|
|
10
|
+
|
|
11
|
+
attr_accessor :connection, :channel, :exchange, :api_client
|
|
12
|
+
|
|
13
|
+
def initialize(config = nil)
|
|
14
|
+
@config = config || Hutch::Config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def connect(options = {})
|
|
18
|
+
@options = options
|
|
19
|
+
set_up_amqp_connection
|
|
20
|
+
if http_api_use_enabled?
|
|
21
|
+
logger.info "HTTP API use is enabled"
|
|
22
|
+
set_up_api_connection
|
|
23
|
+
else
|
|
24
|
+
logger.info "HTTP API use is disabled"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if tracing_enabled?
|
|
28
|
+
logger.info "tracing is enabled using #{@config[:tracer]}"
|
|
29
|
+
else
|
|
30
|
+
logger.info "tracing is disabled"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if block_given?
|
|
34
|
+
begin
|
|
35
|
+
yield
|
|
36
|
+
ensure
|
|
37
|
+
disconnect
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def disconnect
|
|
43
|
+
@channel.close if @channel
|
|
44
|
+
@connection.close if @connection
|
|
45
|
+
@channel, @connection, @exchange, @api_client = nil, nil, nil, nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Connect to RabbitMQ via AMQP. This sets up the main connection and
|
|
49
|
+
# channel we use for talking to RabbitMQ. It also ensures the existance of
|
|
50
|
+
# the exchange we'll be using.
|
|
51
|
+
def set_up_amqp_connection
|
|
52
|
+
open_connection!
|
|
53
|
+
open_channel!
|
|
54
|
+
|
|
55
|
+
exchange_name = @config[:mq_exchange]
|
|
56
|
+
logger.info "using topic exchange '#{exchange_name}'"
|
|
57
|
+
|
|
58
|
+
with_bunny_precondition_handler('exchange') do
|
|
59
|
+
@exchange = @channel.topic(exchange_name, durable: true)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def open_connection!
|
|
64
|
+
logger.info "connecting to rabbitmq (#{sanitized_uri})"
|
|
65
|
+
|
|
66
|
+
@connection = Bunny.new(connection_params)
|
|
67
|
+
|
|
68
|
+
with_bunny_connection_handler(sanitized_uri) do
|
|
69
|
+
@connection.start
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
logger.info "connected to RabbitMQ at #{connection_params[:host]} as #{connection_params[:username]}"
|
|
73
|
+
@connection
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def open_channel!
|
|
77
|
+
logger.info "opening rabbitmq channel with pool size #{consumer_pool_size}"
|
|
78
|
+
@channel = connection.create_channel(nil, consumer_pool_size).tap do |ch|
|
|
79
|
+
ch.prefetch(@config[:channel_prefetch]) if @config[:channel_prefetch]
|
|
80
|
+
if @config[:publisher_confirms] || @config[:force_publisher_confirms]
|
|
81
|
+
logger.info 'enabling publisher confirms'
|
|
82
|
+
ch.confirm_select
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Set up the connection to the RabbitMQ management API. Unfortunately, this
|
|
88
|
+
# is necessary to do a few things that are impossible over AMQP. E.g.
|
|
89
|
+
# listing queues and bindings.
|
|
90
|
+
def set_up_api_connection
|
|
91
|
+
logger.info "connecting to rabbitmq HTTP API (#{api_config.sanitized_uri})"
|
|
92
|
+
|
|
93
|
+
with_authentication_error_handler do
|
|
94
|
+
with_connection_error_handler do
|
|
95
|
+
@api_client = CarrotTop.new(host: api_config.host, port: api_config.port,
|
|
96
|
+
user: api_config.username, password: api_config.password,
|
|
97
|
+
ssl: api_config.ssl)
|
|
98
|
+
@api_client.exchanges
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def http_api_use_enabled?
|
|
104
|
+
op = @options.fetch(:enable_http_api_use, true)
|
|
105
|
+
cf = if @config[:enable_http_api_use].nil?
|
|
106
|
+
true
|
|
107
|
+
else
|
|
108
|
+
@config[:enable_http_api_use]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
op && cf
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def tracing_enabled?
|
|
115
|
+
@config[:tracer] && @config[:tracer] != Hutch::Tracers::NullTracer
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Create / get a durable queue and apply namespace if it exists.
|
|
119
|
+
def queue(name, arguments = {})
|
|
120
|
+
with_bunny_precondition_handler('queue') do
|
|
121
|
+
namespace = @config[:namespace].to_s.downcase.gsub(/[^-_:\.\w]/, "")
|
|
122
|
+
name = name.prepend(namespace + ":") unless namespace.empty?
|
|
123
|
+
channel.queue(name, durable: true, arguments: arguments)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Return a mapping of queue names to the routing keys they're bound to.
|
|
128
|
+
def bindings
|
|
129
|
+
results = Hash.new { |hash, key| hash[key] = [] }
|
|
130
|
+
@api_client.bindings.each do |binding|
|
|
131
|
+
next if binding['destination'] == binding['routing_key']
|
|
132
|
+
next unless binding['source'] == @config[:mq_exchange]
|
|
133
|
+
next unless binding['vhost'] == @config[:mq_vhost]
|
|
134
|
+
results[binding['destination']] << binding['routing_key']
|
|
135
|
+
end
|
|
136
|
+
results
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Bind a queue to the broker's exchange on the routing keys provided. Any
|
|
140
|
+
# existing bindings on the queue that aren't present in the array of
|
|
141
|
+
# routing keys will be unbound.
|
|
142
|
+
def bind_queue(queue, routing_keys)
|
|
143
|
+
if http_api_use_enabled?
|
|
144
|
+
# Find the existing bindings, and unbind any redundant bindings
|
|
145
|
+
queue_bindings = bindings.select { |dest, keys| dest == queue.name }
|
|
146
|
+
queue_bindings.each do |dest, keys|
|
|
147
|
+
keys.reject { |key| routing_keys.include?(key) }.each do |key|
|
|
148
|
+
logger.debug "removing redundant binding #{queue.name} <--> #{key}"
|
|
149
|
+
queue.unbind(@exchange, routing_key: key)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Ensure all the desired bindings are present
|
|
155
|
+
routing_keys.each do |routing_key|
|
|
156
|
+
logger.debug "creating binding #{queue.name} <--> #{routing_key}"
|
|
157
|
+
queue.bind(@exchange, routing_key: routing_key)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Each subscriber is run in a thread. This calls Thread#join on each of the
|
|
162
|
+
# subscriber threads.
|
|
163
|
+
def wait_on_threads(timeout)
|
|
164
|
+
# Thread#join returns nil when the timeout is hit. If any return nil,
|
|
165
|
+
# the threads didn't all join so we return false.
|
|
166
|
+
per_thread_timeout = timeout.to_f / work_pool_threads.length
|
|
167
|
+
work_pool_threads.none? { |thread| thread.join(per_thread_timeout).nil? }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def stop
|
|
171
|
+
# Enqueue a failing job that kills the consumer loop
|
|
172
|
+
channel_work_pool.shutdown
|
|
173
|
+
# Give `timeout` seconds to jobs that are still being processed
|
|
174
|
+
channel_work_pool.join(@config[:graceful_exit_timeout])
|
|
175
|
+
# If after `timeout` they are still running, they are killed
|
|
176
|
+
channel_work_pool.kill
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def requeue(delivery_tag)
|
|
180
|
+
@channel.reject(delivery_tag, true)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def reject(delivery_tag, requeue=false)
|
|
184
|
+
@channel.reject(delivery_tag, requeue)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def ack(delivery_tag)
|
|
188
|
+
@channel.ack(delivery_tag, false)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def nack(delivery_tag)
|
|
192
|
+
@channel.nack(delivery_tag, false, false)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def publish(routing_key, message, properties = {})
|
|
196
|
+
ensure_connection!(routing_key, message)
|
|
197
|
+
|
|
198
|
+
non_overridable_properties = {
|
|
199
|
+
routing_key: routing_key,
|
|
200
|
+
timestamp: Time.now.to_i,
|
|
201
|
+
content_type: 'application/json'
|
|
202
|
+
}
|
|
203
|
+
properties[:message_id] ||= generate_id
|
|
204
|
+
|
|
205
|
+
json = JSON.dump(message)
|
|
206
|
+
logger.info("publishing message '#{json}' to #{routing_key}")
|
|
207
|
+
response = @exchange.publish(json, {persistent: true}.
|
|
208
|
+
merge(properties).
|
|
209
|
+
merge(global_properties).
|
|
210
|
+
merge(non_overridable_properties))
|
|
211
|
+
|
|
212
|
+
channel.wait_for_confirms if @config[:force_publisher_confirms]
|
|
213
|
+
response
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def confirm_select(*args)
|
|
217
|
+
@channel.confirm_select(*args)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def wait_for_confirms
|
|
221
|
+
@channel.wait_for_confirms
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def using_publisher_confirmations?
|
|
225
|
+
@channel.using_publisher_confirmations?
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
def raise_publish_error(reason, routing_key, message)
|
|
231
|
+
msg = "unable to publish - #{reason}. Message: #{JSON.dump(message)}, Routing key: #{routing_key}."
|
|
232
|
+
logger.error(msg)
|
|
233
|
+
raise PublishError, msg
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def ensure_connection!(routing_key, message)
|
|
237
|
+
raise_publish_error('no connection to broker', routing_key, message) unless @connection
|
|
238
|
+
raise_publish_error('connection is closed', routing_key, message) unless @connection.open?
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def api_config
|
|
242
|
+
@api_config ||= OpenStruct.new.tap do |config|
|
|
243
|
+
config.host = @config[:mq_api_host]
|
|
244
|
+
config.port = @config[:mq_api_port]
|
|
245
|
+
config.username = @config[:mq_username]
|
|
246
|
+
config.password = @config[:mq_password]
|
|
247
|
+
config.ssl = @config[:mq_api_ssl]
|
|
248
|
+
config.protocol = config.ssl ? "https://" : "http://"
|
|
249
|
+
config.sanitized_uri = "#{config.protocol}#{config.username}@#{config.host}:#{config.port}/"
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def connection_params
|
|
254
|
+
parse_uri
|
|
255
|
+
|
|
256
|
+
{}.tap do |params|
|
|
257
|
+
params[:host] = @config[:mq_host]
|
|
258
|
+
params[:port] = @config[:mq_port]
|
|
259
|
+
params[:vhost] = if @config[:mq_vhost] && "" != @config[:mq_vhost]
|
|
260
|
+
@config[:mq_vhost]
|
|
261
|
+
else
|
|
262
|
+
Bunny::Session::DEFAULT_VHOST
|
|
263
|
+
end
|
|
264
|
+
params[:username] = @config[:mq_username]
|
|
265
|
+
params[:password] = @config[:mq_password]
|
|
266
|
+
params[:tls] = @config[:mq_tls]
|
|
267
|
+
params[:tls_key] = @config[:mq_tls_key]
|
|
268
|
+
params[:tls_cert] = @config[:mq_tls_cert]
|
|
269
|
+
params[:heartbeat] = @config[:heartbeat]
|
|
270
|
+
params[:connection_timeout] = @config[:connection_timeout]
|
|
271
|
+
params[:read_timeout] = @config[:read_timeout]
|
|
272
|
+
params[:write_timeout] = @config[:write_timeout]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
params[:automatically_recover] = true
|
|
276
|
+
params[:network_recovery_interval] = 1
|
|
277
|
+
|
|
278
|
+
params[:client_logger] = @config[:client_logger] if @config[:client_logger]
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def parse_uri
|
|
283
|
+
return unless @config[:uri] && !@config[:uri].empty?
|
|
284
|
+
|
|
285
|
+
u = URI.parse(@config[:uri])
|
|
286
|
+
|
|
287
|
+
@config[:mq_host] = u.host
|
|
288
|
+
@config[:mq_port] = u.port
|
|
289
|
+
@config[:mq_vhost] = u.path.sub(/^\//, "")
|
|
290
|
+
@config[:mq_username] = u.user
|
|
291
|
+
@config[:mq_password] = u.password
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def sanitized_uri
|
|
295
|
+
p = connection_params
|
|
296
|
+
scheme = p[:tls] ? "amqps" : "amqp"
|
|
297
|
+
|
|
298
|
+
"#{scheme}://#{p[:username]}@#{p[:host]}:#{p[:port]}/#{p[:vhost].sub(/^\//, '')}"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def with_authentication_error_handler
|
|
302
|
+
yield
|
|
303
|
+
rescue Net::HTTPServerException => ex
|
|
304
|
+
logger.error "HTTP API connection error: #{ex.message.downcase}"
|
|
305
|
+
if ex.response.code == '401'
|
|
306
|
+
raise AuthenticationError.new('invalid HTTP API credentials')
|
|
307
|
+
else
|
|
308
|
+
raise
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def with_connection_error_handler
|
|
313
|
+
yield
|
|
314
|
+
rescue Errno::ECONNREFUSED => ex
|
|
315
|
+
logger.error "HTTP API connection error: #{ex.message.downcase}"
|
|
316
|
+
raise ConnectionError.new("couldn't connect to HTTP API at #{api_config.sanitized_uri}")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def with_bunny_precondition_handler(item)
|
|
320
|
+
yield
|
|
321
|
+
rescue Bunny::PreconditionFailed => ex
|
|
322
|
+
logger.error ex.message
|
|
323
|
+
s = "RabbitMQ responded with 406 Precondition Failed when creating this #{item}. " +
|
|
324
|
+
"Perhaps it is being redeclared with non-matching attributes"
|
|
325
|
+
raise WorkerSetupError.new(s)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def with_bunny_connection_handler(uri)
|
|
329
|
+
yield
|
|
330
|
+
rescue Bunny::TCPConnectionFailed => ex
|
|
331
|
+
logger.error "amqp connection error: #{ex.message.downcase}"
|
|
332
|
+
raise ConnectionError.new("couldn't connect to rabbitmq at #{uri}. Check your configuration, network connectivity and RabbitMQ logs.")
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def work_pool_threads
|
|
336
|
+
channel_work_pool.threads || []
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def channel_work_pool
|
|
340
|
+
@channel.work_pool
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def consumer_pool_size
|
|
344
|
+
@config[:consumer_pool_size]
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def generate_id
|
|
348
|
+
SecureRandom.uuid
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def global_properties
|
|
352
|
+
Hutch.global_properties.respond_to?(:call) ? Hutch.global_properties.call : Hutch.global_properties
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
data/lib/hutch/cli.rb
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
|
|
3
|
+
require 'hutch/version'
|
|
4
|
+
require 'hutch/logging'
|
|
5
|
+
require 'hutch/exceptions'
|
|
6
|
+
require 'hutch/config'
|
|
7
|
+
|
|
8
|
+
module Hutch
|
|
9
|
+
class CLI
|
|
10
|
+
include Logging
|
|
11
|
+
|
|
12
|
+
# Run a Hutch worker with the command line interface.
|
|
13
|
+
def run(argv = ARGV)
|
|
14
|
+
parse_options(argv)
|
|
15
|
+
|
|
16
|
+
::Process.daemon(true) if Hutch::Config.daemonise
|
|
17
|
+
|
|
18
|
+
write_pid if Hutch::Config.pidfile
|
|
19
|
+
|
|
20
|
+
Hutch.logger.info "hutch booted with pid #{::Process.pid}"
|
|
21
|
+
|
|
22
|
+
if load_app && start_work_loop == :success
|
|
23
|
+
# If we got here, the worker was shut down nicely
|
|
24
|
+
Hutch.logger.info 'hutch shut down gracefully'
|
|
25
|
+
exit 0
|
|
26
|
+
else
|
|
27
|
+
Hutch.logger.info 'hutch terminated due to an error'
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def load_app
|
|
33
|
+
# Try to load a Rails app in the current directory
|
|
34
|
+
load_rails_app('.') if Hutch::Config.autoload_rails
|
|
35
|
+
Hutch::Config.require_paths.each do |path|
|
|
36
|
+
# See if each path is a Rails app. If so, try to load it.
|
|
37
|
+
next if load_rails_app(path)
|
|
38
|
+
|
|
39
|
+
# Given path is not a Rails app, try requiring it as a file
|
|
40
|
+
logger.info "requiring '#{path}'"
|
|
41
|
+
begin
|
|
42
|
+
# Need to add '.' to load path for relative requires
|
|
43
|
+
$LOAD_PATH << '.'
|
|
44
|
+
require path
|
|
45
|
+
rescue LoadError
|
|
46
|
+
logger.fatal "could not load file '#{path}'"
|
|
47
|
+
return false
|
|
48
|
+
ensure
|
|
49
|
+
# Clean up load path
|
|
50
|
+
$LOAD_PATH.pop
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Because of the order things are required when we run the Hutch binary
|
|
55
|
+
# in hutch/bin, the Sentry Raven gem gets required **after** the error
|
|
56
|
+
# handlers are set up. Due to this, we never got any Sentry notifications
|
|
57
|
+
# when an error occurred in any of the consumers.
|
|
58
|
+
if defined?(Raven)
|
|
59
|
+
Hutch::Config[:error_handlers] << Hutch::ErrorHandlers::Sentry.new
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def load_rails_app(path)
|
|
66
|
+
# path should point to the app's top level directory
|
|
67
|
+
if File.directory?(path)
|
|
68
|
+
# Smells like a Rails app if it's got a config/environment.rb file
|
|
69
|
+
rails_path = File.expand_path(File.join(path, 'config/environment.rb'))
|
|
70
|
+
if File.exists?(rails_path)
|
|
71
|
+
logger.info "found rails project (#{path}), booting app"
|
|
72
|
+
ENV['RACK_ENV'] ||= ENV['RAILS_ENV'] || 'development'
|
|
73
|
+
require rails_path
|
|
74
|
+
::Rails.application.eager_load!
|
|
75
|
+
return true
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Kick off the work loop. This method returns when the worker is shut down
|
|
82
|
+
# gracefully (with a SIGQUIT, SIGTERM or SIGINT).
|
|
83
|
+
def start_work_loop
|
|
84
|
+
Hutch.connect
|
|
85
|
+
@worker = Hutch::Worker.new(Hutch.broker, Hutch.consumers)
|
|
86
|
+
@worker.run
|
|
87
|
+
:success
|
|
88
|
+
rescue ConnectionError, AuthenticationError, WorkerSetupError => ex
|
|
89
|
+
logger.fatal ex.message
|
|
90
|
+
:error
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_options(args = ARGV)
|
|
94
|
+
OptionParser.new do |opts|
|
|
95
|
+
opts.banner = 'usage: hutch [options]'
|
|
96
|
+
|
|
97
|
+
opts.on('--mq-host HOST', 'Set the RabbitMQ host') do |host|
|
|
98
|
+
Hutch::Config.mq_host = host
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
opts.on('--mq-port PORT', 'Set the RabbitMQ port') do |port|
|
|
102
|
+
Hutch::Config.mq_port = port
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
opts.on("-t", "--[no-]mq-tls", 'Use TLS for the AMQP connection') do |tls|
|
|
106
|
+
Hutch::Config.mq_tls = tls
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
opts.on('--mq-tls-cert FILE', 'Certificate for TLS client verification') do |file|
|
|
110
|
+
abort "Certificate file '#{file}' not found" unless File.exists?(file)
|
|
111
|
+
Hutch::Config.mq_tls_cert = file
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
opts.on('--mq-tls-key FILE', 'Private key for TLS client verification') do |file|
|
|
115
|
+
abort "Private key file '#{file}' not found" unless File.exists?(file)
|
|
116
|
+
Hutch::Config.mq_tls_key = file
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
opts.on('--mq-exchange EXCHANGE',
|
|
120
|
+
'Set the RabbitMQ exchange') do |exchange|
|
|
121
|
+
Hutch::Config.mq_exchange = exchange
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
opts.on('--mq-vhost VHOST', 'Set the RabbitMQ vhost') do |vhost|
|
|
125
|
+
Hutch::Config.mq_vhost = vhost
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
opts.on('--mq-username USERNAME',
|
|
129
|
+
'Set the RabbitMQ username') do |username|
|
|
130
|
+
Hutch::Config.mq_username = username
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
opts.on('--mq-password PASSWORD',
|
|
134
|
+
'Set the RabbitMQ password') do |password|
|
|
135
|
+
Hutch::Config.mq_password = password
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
opts.on('--mq-api-host HOST', 'Set the RabbitMQ API host') do |host|
|
|
139
|
+
Hutch::Config.mq_api_host = host
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
opts.on('--mq-api-port PORT', 'Set the RabbitMQ API port') do |port|
|
|
143
|
+
Hutch::Config.mq_api_port = port
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
opts.on("-s", "--[no-]mq-api-ssl", 'Use SSL for the RabbitMQ API') do |api_ssl|
|
|
147
|
+
Hutch::Config.mq_api_ssl = api_ssl
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
opts.on('--config FILE', 'Load Hutch configuration from a file') do |file|
|
|
151
|
+
begin
|
|
152
|
+
File.open(file) { |fp| Hutch::Config.load_from_file(fp) }
|
|
153
|
+
rescue Errno::ENOENT
|
|
154
|
+
abort "Config file '#{file}' not found"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
opts.on('--require PATH', 'Require a Rails app or path') do |path|
|
|
159
|
+
Hutch::Config.require_paths << path
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
opts.on('--[no-]autoload-rails', 'Require the current rails app directory') do |autoload_rails|
|
|
163
|
+
Hutch::Config.autoload_rails = autoload_rails
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
opts.on('-q', '--quiet', 'Quiet logging') do
|
|
167
|
+
Hutch::Config.log_level = Logger::WARN
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
opts.on('-v', '--verbose', 'Verbose logging') do
|
|
171
|
+
Hutch::Config.log_level = Logger::DEBUG
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
opts.on('--namespace NAMESPACE', 'Queue namespace') do |namespace|
|
|
175
|
+
Hutch::Config.namespace = namespace
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
opts.on('-d', '--daemonise', 'Daemonise') do |daemonise|
|
|
179
|
+
Hutch::Config.daemonise = daemonise
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
opts.on('--pidfile PIDFILE', 'Pidfile') do |pidfile|
|
|
183
|
+
Hutch::Config.pidfile = pidfile
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
opts.on('--version', 'Print the version and exit') do
|
|
187
|
+
puts "hutch v#{VERSION}"
|
|
188
|
+
exit 0
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
opts.on('-h', '--help', 'Show this message and exit') do
|
|
192
|
+
puts opts
|
|
193
|
+
exit 0
|
|
194
|
+
end
|
|
195
|
+
end.parse!(args)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def write_pid
|
|
199
|
+
pidfile = File.expand_path(Hutch::Config.pidfile)
|
|
200
|
+
Hutch.logger.info "writing pid in #{pidfile}"
|
|
201
|
+
File.open(pidfile, 'w') { |f| f.puts ::Process.pid }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
end
|
|
205
|
+
end
|
data/lib/hutch/config.rb
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require 'hutch/error_handlers/logger'
|
|
2
|
+
require 'erb'
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
module Hutch
|
|
6
|
+
class UnknownAttributeError < StandardError; end
|
|
7
|
+
|
|
8
|
+
module Config
|
|
9
|
+
require 'yaml'
|
|
10
|
+
|
|
11
|
+
def self.initialize(params={})
|
|
12
|
+
@config = {
|
|
13
|
+
mq_host: 'localhost',
|
|
14
|
+
mq_port: 5672,
|
|
15
|
+
mq_exchange: 'hutch', # TODO: should this be required?
|
|
16
|
+
mq_vhost: '/',
|
|
17
|
+
mq_tls: false,
|
|
18
|
+
mq_tls_cert: nil,
|
|
19
|
+
mq_tls_key: nil,
|
|
20
|
+
mq_username: 'guest',
|
|
21
|
+
mq_password: 'guest',
|
|
22
|
+
mq_api_host: 'localhost',
|
|
23
|
+
mq_api_port: 15672,
|
|
24
|
+
mq_api_ssl: false,
|
|
25
|
+
heartbeat: 30,
|
|
26
|
+
# placeholder, allows specifying connection parameters
|
|
27
|
+
# as a URI.
|
|
28
|
+
uri: nil,
|
|
29
|
+
log_level: Logger::INFO,
|
|
30
|
+
require_paths: [],
|
|
31
|
+
autoload_rails: true,
|
|
32
|
+
error_handlers: [Hutch::ErrorHandlers::Logger.new],
|
|
33
|
+
tracer: Hutch::Tracers::NullTracer,
|
|
34
|
+
namespace: nil,
|
|
35
|
+
daemonise: false,
|
|
36
|
+
pidfile: nil,
|
|
37
|
+
channel_prefetch: 0,
|
|
38
|
+
# enables publisher confirms, leaves it up to the app
|
|
39
|
+
# how they are tracked
|
|
40
|
+
publisher_confirms: false,
|
|
41
|
+
# like `publisher_confirms` above but also
|
|
42
|
+
# forces waiting for a confirm for every publish
|
|
43
|
+
force_publisher_confirms: false,
|
|
44
|
+
# Heroku needs > 10. MK.
|
|
45
|
+
connection_timeout: 11,
|
|
46
|
+
read_timeout: 11,
|
|
47
|
+
write_timeout: 11,
|
|
48
|
+
enable_http_api_use: true,
|
|
49
|
+
# Number of seconds that a running consumer is given
|
|
50
|
+
# to finish its job when gracefully exiting Hutch, before
|
|
51
|
+
# it's killed.
|
|
52
|
+
graceful_exit_timeout: 11,
|
|
53
|
+
client_logger: nil,
|
|
54
|
+
|
|
55
|
+
consumer_pool_size: 1,
|
|
56
|
+
}.merge(params)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.get(attr)
|
|
60
|
+
check_attr(attr)
|
|
61
|
+
user_config[attr]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.set(attr, value)
|
|
65
|
+
check_attr(attr)
|
|
66
|
+
user_config[attr] = value
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class << self
|
|
70
|
+
alias_method :[], :get
|
|
71
|
+
alias_method :[]=, :set
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.check_attr(attr)
|
|
75
|
+
unless user_config.key?(attr)
|
|
76
|
+
raise UnknownAttributeError, "#{attr} is not a valid config attribute"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.user_config
|
|
81
|
+
initialize unless @config
|
|
82
|
+
@config
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.to_hash
|
|
86
|
+
self.user_config
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.load_from_file(file)
|
|
90
|
+
YAML.load(ERB.new(File.read(file)).result).each do |attr, value|
|
|
91
|
+
Hutch::Config.send("#{attr}=", convert_value(attr, value))
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.convert_value(attr, value)
|
|
96
|
+
case attr
|
|
97
|
+
when "tracer"
|
|
98
|
+
Kernel.const_get(value)
|
|
99
|
+
else
|
|
100
|
+
value
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.method_missing(method, *args, &block)
|
|
105
|
+
attr = method.to_s.sub(/=$/, '').to_sym
|
|
106
|
+
return super unless user_config.key?(attr)
|
|
107
|
+
|
|
108
|
+
if method =~ /=$/
|
|
109
|
+
set(attr, args.first)
|
|
110
|
+
else
|
|
111
|
+
get(attr)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def deep_copy(obj)
|
|
118
|
+
Marshal.load(Marshal.dump(obj))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|