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,66 @@
1
+ require 'set'
2
+
3
+ module Hutch
4
+ # Include this module in a class to register it as a consumer. Consumers
5
+ # gain a class method called `consume`, which should be used to register
6
+ # the routing keys a consumer is interested in.
7
+ module Consumer
8
+ attr_accessor :broker, :delivery_info
9
+
10
+ def self.included(base)
11
+ base.extend(ClassMethods)
12
+ Hutch.register_consumer(base)
13
+ end
14
+
15
+ def reject!
16
+ broker.reject(delivery_info.delivery_tag)
17
+ end
18
+
19
+ def requeue!
20
+ broker.requeue(delivery_info.delivery_tag)
21
+ end
22
+
23
+ def logger
24
+ Hutch::Logging.logger
25
+ end
26
+
27
+ module ClassMethods
28
+ # Add one or more routing keys to the set of routing keys the consumer
29
+ # wants to subscribe to.
30
+ def consume(*routing_keys)
31
+ @routing_keys = self.routing_keys.union(routing_keys)
32
+ end
33
+
34
+ # Explicitly set the queue name
35
+ def queue_name(name)
36
+ @queue_name = name
37
+ end
38
+
39
+ # Allow to specify custom arguments that will be passed when creating the queue.
40
+ def arguments(arguments = {})
41
+ @arguments = arguments
42
+ end
43
+
44
+ # The RabbitMQ queue name for the consumer. This is derived from the
45
+ # fully-qualified class name. Module separators are replaced with single
46
+ # colons, camelcased class names are converted to snake case.
47
+ def get_queue_name
48
+ return @queue_name unless @queue_name.nil?
49
+ queue_name = self.name.gsub(/::/, ':')
50
+ queue_name.gsub!(/([^A-Z:])([A-Z])/) { "#{$1}_#{$2}" }
51
+ queue_name.downcase
52
+ end
53
+
54
+ # Returns consumer custom arguments.
55
+ def get_arguments
56
+ @arguments || {}
57
+ end
58
+
59
+ # Accessor for the consumer's routing key.
60
+ def routing_keys
61
+ @routing_keys ||= Set.new
62
+ end
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,26 @@
1
+ require 'hutch/logging'
2
+ require 'airbrake'
3
+
4
+ module Hutch
5
+ module ErrorHandlers
6
+ class Airbrake
7
+ include Logging
8
+
9
+ def handle(message_id, payload, consumer, ex)
10
+ prefix = "message(#{message_id || '-'}): "
11
+ logger.error prefix + "Logging event to Airbrake"
12
+ logger.error prefix + "#{ex.class} - #{ex.message}"
13
+ ::Airbrake.notify_or_ignore(ex, {
14
+ :error_class => ex.class.name,
15
+ :error_message => "#{ ex.class.name }: #{ ex.message }",
16
+ :backtrace => ex.backtrace,
17
+ :parameters => {
18
+ :payload => payload,
19
+ :consumer => consumer,
20
+ },
21
+ :cgi_data => ENV.to_hash,
22
+ })
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ require 'hutch/logging'
2
+ require 'honeybadger'
3
+
4
+ module Hutch
5
+ module ErrorHandlers
6
+ class Honeybadger
7
+ include Logging
8
+
9
+ def handle(message_id, payload, consumer, ex)
10
+ prefix = "message(#{message_id || '-'}): "
11
+ logger.error prefix + "Logging event to Honeybadger"
12
+ logger.error prefix + "#{ex.class} - #{ex.message}"
13
+ ::Honeybadger.notify_or_ignore(
14
+ :error_class => ex.class.name,
15
+ :error_message => "#{ ex.class.name }: #{ ex.message }",
16
+ :backtrace => ex.backtrace,
17
+ :context => {
18
+ :message_id => message_id,
19
+ :consumer => consumer
20
+ },
21
+ :parameters => {
22
+ :payload => payload
23
+ }
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ require 'hutch/logging'
2
+
3
+ module Hutch
4
+ module ErrorHandlers
5
+ class Logger
6
+ include Logging
7
+
8
+ def handle(message_id, payload, consumer, ex)
9
+ prefix = "message(#{message_id || '-'}): "
10
+ logger.error prefix + "error in consumer '#{consumer}'"
11
+ logger.error prefix + "#{ex.class} - #{ex.message}"
12
+ logger.error (['backtrace:'] + ex.backtrace).join("\n")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ require 'hutch/logging'
2
+ require 'raven'
3
+
4
+ module Hutch
5
+ module ErrorHandlers
6
+ class Sentry
7
+ include Logging
8
+
9
+ def initialize
10
+ unless Raven.respond_to?(:capture_exception)
11
+ raise "The Hutch Sentry error handler requires Raven >= 0.4.0"
12
+ end
13
+ end
14
+
15
+ def handle(message_id, payload, consumer, ex)
16
+ prefix = "message(#{message_id || '-'}): "
17
+ logger.error prefix + "Logging event to Sentry"
18
+ logger.error prefix + "#{ex.class} - #{ex.message}"
19
+ Raven.capture_exception(ex)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,8 @@
1
+ module Hutch
2
+ module ErrorHandlers
3
+ autoload :Logger, 'hutch/error_handlers/logger'
4
+ autoload :Sentry, 'hutch/error_handlers/sentry'
5
+ autoload :Honeybadger, 'hutch/error_handlers/honeybadger'
6
+ autoload :Airbrake, 'hutch/error_handlers/airbrake'
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module Hutch
2
+ class Exception < StandardError; end
3
+ class ConnectionError < Exception; end
4
+ class AuthenticationError < Exception; end
5
+ class WorkerSetupError < Exception; end
6
+ class PublishError < Exception; end
7
+ end
@@ -0,0 +1,32 @@
1
+ require 'logger'
2
+ require 'time'
3
+
4
+ module Hutch
5
+ module Logging
6
+ class HutchFormatter < Logger::Formatter
7
+ def call(severity, time, program_name, message)
8
+ "#{time.utc.iso8601} #{Process.pid} #{severity} -- #{message}\n"
9
+ end
10
+ end
11
+
12
+ def self.setup_logger(target = $stdout)
13
+ require 'hutch/config'
14
+ @logger = Logger.new(target)
15
+ @logger.level = Hutch::Config.log_level
16
+ @logger.formatter = HutchFormatter.new
17
+ @logger
18
+ end
19
+
20
+ def self.logger
21
+ @logger || setup_logger
22
+ end
23
+
24
+ def self.logger=(logger)
25
+ @logger = logger
26
+ end
27
+
28
+ def logger
29
+ Hutch::Logging.logger
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ require 'multi_json'
2
+ require 'forwardable'
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+
5
+ module Hutch
6
+ class Message
7
+ extend Forwardable
8
+
9
+ attr_reader :delivery_info, :properties, :payload
10
+
11
+ def initialize(delivery_info, properties, payload)
12
+ @delivery_info = delivery_info
13
+ @properties = properties
14
+ @payload = payload
15
+ @body = MultiJson.load(payload).with_indifferent_access
16
+ end
17
+
18
+ def_delegator :@body, :[]
19
+ def_delegators :@properties, :message_id, :timestamp
20
+ def_delegators :@delivery_info, :routing_key, :exchange
21
+
22
+ attr_reader :body
23
+
24
+ def to_s
25
+ attrs = { :@body => body.to_s, message_id: message_id,
26
+ timestamp: timestamp, routing_key: routing_key }
27
+ "#<Message #{attrs.map { |k,v| "#{k}=#{v.inspect}" }.join(', ')}>"
28
+ end
29
+
30
+ alias_method :inspect, :to_s
31
+ end
32
+ end
33
+
@@ -0,0 +1,19 @@
1
+ require 'newrelic_rpm'
2
+
3
+ module Hutch
4
+ module Tracers
5
+ class NewRelic
6
+ include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation
7
+
8
+ def initialize(klass)
9
+ @klass = klass
10
+ end
11
+
12
+ def handle(message)
13
+ @klass.process(message)
14
+ end
15
+
16
+ add_transaction_tracer :handle, :category => 'OtherTransaction/HutchConsumer', :path => '#{@klass.class.name}'
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Hutch
2
+ module Tracers
3
+ class NullTracer
4
+
5
+ def initialize(klass)
6
+ @klass = klass
7
+ end
8
+
9
+ def handle(message)
10
+ @klass.process(message)
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ module Hutch
2
+ module Tracers
3
+ autoload :NullTracer, 'hutch/tracers/null_tracer'
4
+ autoload :NewRelic, 'hutch/tracers/newrelic'
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ module Hutch
2
+ VERSION = '0.17.1'.freeze
3
+ end
4
+
@@ -0,0 +1,110 @@
1
+ require 'hutch/message'
2
+ require 'hutch/logging'
3
+ require 'hutch/broker'
4
+ require 'carrot-top'
5
+
6
+ module Hutch
7
+ class Worker
8
+ include Logging
9
+
10
+ def initialize(broker, consumers)
11
+ @broker = broker
12
+ self.consumers = consumers
13
+ end
14
+
15
+ # Run the main event loop. The consumers will be set up with queues, and
16
+ # process the messages in their respective queues indefinitely. This method
17
+ # never returns.
18
+ def run
19
+ setup_queues
20
+
21
+ # Set up signal handlers for graceful shutdown
22
+ register_signal_handlers
23
+
24
+ # Take a break from Thread#join every 0.1 seconds to check if we've
25
+ # been sent any signals
26
+ handle_signals until @broker.wait_on_threads(0.1)
27
+ end
28
+
29
+ # Register handlers for SIG{QUIT,TERM,INT} to shut down the worker
30
+ # gracefully. Forceful shutdowns are very bad!
31
+ def register_signal_handlers
32
+ Thread.main[:signal_queue] = []
33
+ %w(QUIT TERM INT).keep_if { |s| Signal.list.keys.include? s }.map(&:to_sym).each do |sig|
34
+ # This needs to be reentrant, so we queue up signals to be handled
35
+ # in the run loop, rather than acting on signals here
36
+ trap(sig) do
37
+ Thread.main[:signal_queue] << sig
38
+ end
39
+ end
40
+ end
41
+
42
+ # Handle any pending signals
43
+ def handle_signals
44
+ signal = Thread.main[:signal_queue].shift
45
+ if signal
46
+ logger.info "caught sig#{signal.downcase}, stopping hutch..."
47
+ stop
48
+ end
49
+ end
50
+
51
+ # Stop a running worker by killing all subscriber threads.
52
+ def stop
53
+ @broker.stop
54
+ end
55
+
56
+ # Set up the queues for each of the worker's consumers.
57
+ def setup_queues
58
+ logger.info 'setting up queues'
59
+ @consumers.each { |consumer| setup_queue(consumer) }
60
+ end
61
+
62
+ # Bind a consumer's routing keys to its queue, and set up a subscription to
63
+ # receive messages sent to the queue.
64
+ def setup_queue(consumer)
65
+ queue = @broker.queue(consumer.get_queue_name, consumer.get_arguments)
66
+ @broker.bind_queue(queue, consumer.routing_keys)
67
+
68
+ queue.subscribe(manual_ack: true) do |delivery_info, properties, payload|
69
+ handle_message(consumer, delivery_info, properties, payload)
70
+ end
71
+ end
72
+
73
+ # Called internally when a new messages comes in from RabbitMQ. Responsible
74
+ # for wrapping up the message and passing it to the consumer.
75
+ def handle_message(consumer, delivery_info, properties, payload)
76
+ logger.info("message(#{properties.message_id || '-'}): " +
77
+ "routing key: #{delivery_info.routing_key}, " +
78
+ "consumer: #{consumer}, " +
79
+ "payload: #{payload}")
80
+
81
+ broker = @broker
82
+ begin
83
+ message = Message.new(delivery_info, properties, payload)
84
+ consumer_instance = consumer.new.tap { |c| c.broker, c.delivery_info = @broker, delivery_info }
85
+ with_tracing(consumer_instance).handle(message)
86
+ broker.ack(delivery_info.delivery_tag)
87
+ rescue StandardError => ex
88
+ broker.nack(delivery_info.delivery_tag)
89
+ handle_error(properties.message_id, payload, consumer, ex)
90
+ end
91
+ end
92
+
93
+ def with_tracing(klass)
94
+ Hutch::Config[:tracer].new(klass)
95
+ end
96
+
97
+ def handle_error(message_id, payload, consumer, ex)
98
+ Hutch::Config[:error_handlers].each do |backend|
99
+ backend.handle(message_id, payload, consumer, ex)
100
+ end
101
+ end
102
+
103
+ def consumers=(val)
104
+ if val.empty?
105
+ logger.warn "no consumer loaded, ensure there's no configuration issue"
106
+ end
107
+ @consumers = val
108
+ end
109
+ end
110
+ end
data/lib/hutch.rb ADDED
@@ -0,0 +1,60 @@
1
+ require 'hutch/consumer'
2
+ require 'hutch/worker'
3
+ require 'hutch/broker'
4
+ require 'hutch/logging'
5
+ require 'hutch/config'
6
+ require 'hutch/message'
7
+ require 'hutch/cli'
8
+ require 'hutch/version'
9
+ require 'hutch/error_handlers'
10
+ require 'hutch/exceptions'
11
+ require 'hutch/tracers'
12
+
13
+ module Hutch
14
+
15
+ def self.register_consumer(consumer)
16
+ self.consumers << consumer
17
+ end
18
+
19
+ def self.consumers
20
+ @consumers ||= []
21
+ end
22
+
23
+ def self.logger
24
+ Hutch::Logging.logger
25
+ end
26
+
27
+ def self.global_properties=(properties)
28
+ @global_properties = properties
29
+ end
30
+
31
+ def self.global_properties
32
+ @global_properties ||= {}
33
+ end
34
+
35
+ def self.connect(options = {}, config = Hutch::Config)
36
+ unless connected?
37
+ @broker = Hutch::Broker.new(config)
38
+ @broker.connect(options)
39
+ end
40
+ end
41
+
42
+ def self.disconnect
43
+ @broker.disconnect if @broker
44
+ end
45
+
46
+ def self.broker
47
+ @broker
48
+ end
49
+
50
+ def self.connected?
51
+ return false unless broker
52
+ return false unless broker.connection
53
+ broker.connection.open?
54
+ end
55
+
56
+ def self.publish(*args)
57
+ broker.publish(*args)
58
+ end
59
+ end
60
+