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
|
@@ -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,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
|
data/lib/hutch/worker.rb
ADDED
|
@@ -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
|
+
|