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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.travis.yml +7 -0
  4. data/CHANGELOG.md +397 -0
  5. data/Gemfile +22 -0
  6. data/Guardfile +5 -0
  7. data/LICENSE +22 -0
  8. data/README.md +315 -0
  9. data/Rakefile +14 -0
  10. data/bin/hutch +8 -0
  11. data/circle.yml +3 -0
  12. data/examples/consumer.rb +13 -0
  13. data/examples/producer.rb +10 -0
  14. data/hutch.gemspec +24 -0
  15. data/lib/hutch/broker.rb +356 -0
  16. data/lib/hutch/cli.rb +205 -0
  17. data/lib/hutch/config.rb +121 -0
  18. data/lib/hutch/consumer.rb +66 -0
  19. data/lib/hutch/error_handlers/airbrake.rb +26 -0
  20. data/lib/hutch/error_handlers/honeybadger.rb +28 -0
  21. data/lib/hutch/error_handlers/logger.rb +16 -0
  22. data/lib/hutch/error_handlers/sentry.rb +23 -0
  23. data/lib/hutch/error_handlers.rb +8 -0
  24. data/lib/hutch/exceptions.rb +7 -0
  25. data/lib/hutch/logging.rb +32 -0
  26. data/lib/hutch/message.rb +33 -0
  27. data/lib/hutch/tracers/newrelic.rb +19 -0
  28. data/lib/hutch/tracers/null_tracer.rb +15 -0
  29. data/lib/hutch/tracers.rb +6 -0
  30. data/lib/hutch/version.rb +4 -0
  31. data/lib/hutch/worker.rb +110 -0
  32. data/lib/hutch.rb +60 -0
  33. data/spec/hutch/broker_spec.rb +325 -0
  34. data/spec/hutch/cli_spec.rb +80 -0
  35. data/spec/hutch/config_spec.rb +126 -0
  36. data/spec/hutch/consumer_spec.rb +130 -0
  37. data/spec/hutch/error_handlers/airbrake_spec.rb +34 -0
  38. data/spec/hutch/error_handlers/honeybadger_spec.rb +36 -0
  39. data/spec/hutch/error_handlers/logger_spec.rb +15 -0
  40. data/spec/hutch/error_handlers/sentry_spec.rb +20 -0
  41. data/spec/hutch/logger_spec.rb +28 -0
  42. data/spec/hutch/message_spec.rb +38 -0
  43. data/spec/hutch/worker_spec.rb +98 -0
  44. data/spec/hutch_spec.rb +87 -0
  45. data/spec/spec_helper.rb +35 -0
  46. metadata +187 -0
@@ -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
@@ -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