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,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
+