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