dkastner-hutch 0.17.1

Sign up to get free protection for your applications and to get access to all the features.
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