hutch 0.18.0 → 1.1.0
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 +5 -5
- data/.gitignore +3 -0
- data/.rspec +1 -0
- data/.travis.yml +20 -8
- data/.yardopts +5 -0
- data/CHANGELOG.md +466 -2
- data/Gemfile +18 -4
- data/Guardfile +13 -4
- data/LICENSE +2 -1
- data/README.md +397 -32
- data/Rakefile +8 -1
- data/bin/ci/before_build.sh +20 -0
- data/bin/ci/install_on_debian.sh +46 -0
- data/hutch.gemspec +6 -7
- data/lib/hutch/acknowledgements/base.rb +16 -0
- data/lib/hutch/acknowledgements/nack_on_all_failures.rb +19 -0
- data/lib/hutch/adapters/march_hare.rb +1 -1
- data/lib/hutch/broker.rb +127 -103
- data/lib/hutch/cli.rb +66 -25
- data/lib/hutch/config.rb +230 -55
- data/lib/hutch/consumer.rb +42 -3
- data/lib/hutch/error_handlers/airbrake.rb +44 -16
- data/lib/hutch/error_handlers/base.rb +15 -0
- data/lib/hutch/error_handlers/bugsnag.rb +30 -0
- data/lib/hutch/error_handlers/honeybadger.rb +33 -18
- data/lib/hutch/error_handlers/logger.rb +12 -6
- data/lib/hutch/error_handlers/rollbar.rb +28 -0
- data/lib/hutch/error_handlers/sentry.rb +15 -12
- data/lib/hutch/error_handlers/sentry_raven.rb +31 -0
- data/lib/hutch/error_handlers.rb +3 -0
- data/lib/hutch/exceptions.rb +8 -1
- data/lib/hutch/logging.rb +5 -5
- data/lib/hutch/message.rb +2 -4
- data/lib/hutch/publisher.rb +75 -0
- data/lib/hutch/serializers/identity.rb +19 -0
- data/lib/hutch/serializers/json.rb +22 -0
- data/lib/hutch/tracers/datadog.rb +17 -0
- data/lib/hutch/tracers.rb +1 -0
- data/lib/hutch/version.rb +1 -2
- data/lib/hutch/waiter.rb +104 -0
- data/lib/hutch/worker.rb +81 -75
- data/lib/hutch.rb +15 -6
- data/lib/yard-settings/handler.rb +38 -0
- data/lib/yard-settings/yard-settings.rb +2 -0
- data/spec/hutch/broker_spec.rb +162 -77
- data/spec/hutch/cli_spec.rb +16 -3
- data/spec/hutch/config_spec.rb +121 -22
- data/spec/hutch/consumer_spec.rb +82 -4
- data/spec/hutch/error_handlers/airbrake_spec.rb +25 -10
- data/spec/hutch/error_handlers/bugsnag_spec.rb +55 -0
- data/spec/hutch/error_handlers/honeybadger_spec.rb +24 -2
- data/spec/hutch/error_handlers/logger_spec.rb +14 -1
- data/spec/hutch/error_handlers/rollbar_spec.rb +45 -0
- data/spec/hutch/error_handlers/sentry_raven_spec.rb +37 -0
- data/spec/hutch/error_handlers/sentry_spec.rb +21 -2
- data/spec/hutch/logger_spec.rb +12 -6
- data/spec/hutch/message_spec.rb +2 -2
- data/spec/hutch/serializers/json_spec.rb +17 -0
- data/spec/hutch/tracers/datadog_spec.rb +44 -0
- data/spec/hutch/waiter_spec.rb +51 -0
- data/spec/hutch/worker_spec.rb +89 -5
- data/spec/spec_helper.rb +7 -5
- data/templates/default/class/html/settings.erb +0 -0
- data/templates/default/class/setup.rb +4 -0
- data/templates/default/fulldoc/html/css/hutch.css +13 -0
- data/templates/default/layout/html/setup.rb +7 -0
- data/templates/default/method_details/html/settings.erb +5 -0
- data/templates/default/method_details/setup.rb +4 -0
- data/templates/default/method_details/text/settings.erb +0 -0
- data/templates/default/module/html/settings.erb +40 -0
- data/templates/default/module/setup.rb +4 -0
- metadata +62 -43
- data/circle.yml +0 -3
| @@ -1,22 +1,25 @@ | |
| 1 1 | 
             
            require 'hutch/logging'
         | 
| 2 | 
            -
            require ' | 
| 2 | 
            +
            require 'sentry-ruby'
         | 
| 3 | 
            +
            require 'hutch/error_handlers/base'
         | 
| 3 4 |  | 
| 4 5 | 
             
            module Hutch
         | 
| 5 6 | 
             
              module ErrorHandlers
         | 
| 6 | 
            -
                class Sentry
         | 
| 7 | 
            -
                   | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
                     | 
| 11 | 
            -
             | 
| 7 | 
            +
                class Sentry < Base
         | 
| 8 | 
            +
                  def handle(properties, payload, consumer, ex)
         | 
| 9 | 
            +
                    message_id = properties.message_id
         | 
| 10 | 
            +
                    prefix = "message(#{message_id || '-'}):"
         | 
| 11 | 
            +
                    logger.error "#{prefix} Logging event to Sentry"
         | 
| 12 | 
            +
                    logger.error "#{prefix} #{ex.class} - #{ex.message}"
         | 
| 13 | 
            +
                    ::Sentry.configure_scope do |scope|
         | 
| 14 | 
            +
                      scope.set_context("payload", payload)
         | 
| 12 15 | 
             
                    end
         | 
| 16 | 
            +
                    ::Sentry.capture_exception(ex)
         | 
| 13 17 | 
             
                  end
         | 
| 14 18 |  | 
| 15 | 
            -
                  def  | 
| 16 | 
            -
                     | 
| 17 | 
            -
                    logger.error  | 
| 18 | 
            -
                     | 
| 19 | 
            -
                    Raven.capture_exception(ex)
         | 
| 19 | 
            +
                  def handle_setup_exception(ex)
         | 
| 20 | 
            +
                    logger.error "Logging setup exception to Sentry"
         | 
| 21 | 
            +
                    logger.error "#{ex.class} - #{ex.message}"
         | 
| 22 | 
            +
                    ::Sentry.capture_exception(ex)
         | 
| 20 23 | 
             
                  end
         | 
| 21 24 | 
             
                end
         | 
| 22 25 | 
             
              end
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            require 'hutch/logging'
         | 
| 2 | 
            +
            require 'raven'
         | 
| 3 | 
            +
            require 'hutch/error_handlers/base'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Hutch
         | 
| 6 | 
            +
              module ErrorHandlers
         | 
| 7 | 
            +
                class SentryRaven < Base
         | 
| 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(properties, payload, consumer, ex)
         | 
| 16 | 
            +
                    message_id = properties.message_id
         | 
| 17 | 
            +
                    prefix = "message(#{message_id || '-'}):"
         | 
| 18 | 
            +
                    logger.error "#{prefix} Logging event to Sentry"
         | 
| 19 | 
            +
                    logger.error "#{prefix} #{ex.class} - #{ex.message}"
         | 
| 20 | 
            +
                    Raven.capture_exception(ex, extra: { payload: payload })
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def handle_setup_exception(ex)
         | 
| 24 | 
            +
                    logger.error "Logging setup exception to Sentry"
         | 
| 25 | 
            +
                    logger.error "#{ex.class} - #{ex.message}"
         | 
| 26 | 
            +
                    Raven.capture_exception(ex)
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
    
        data/lib/hutch/error_handlers.rb
    CHANGED
    
    | @@ -2,7 +2,10 @@ module Hutch | |
| 2 2 | 
             
              module ErrorHandlers
         | 
| 3 3 | 
             
                autoload :Logger,      'hutch/error_handlers/logger'
         | 
| 4 4 | 
             
                autoload :Sentry,      'hutch/error_handlers/sentry'
         | 
| 5 | 
            +
                autoload :SentryRaven, 'hutch/error_handlers/sentry_raven'
         | 
| 5 6 | 
             
                autoload :Honeybadger, 'hutch/error_handlers/honeybadger'
         | 
| 6 7 | 
             
                autoload :Airbrake,    'hutch/error_handlers/airbrake'
         | 
| 8 | 
            +
                autoload :Rollbar,     'hutch/error_handlers/rollbar'
         | 
| 9 | 
            +
                autoload :Bugsnag,     'hutch/error_handlers/bugsnag'
         | 
| 7 10 | 
             
              end
         | 
| 8 11 | 
             
            end
         | 
    
        data/lib/hutch/exceptions.rb
    CHANGED
    
    | @@ -1,5 +1,12 @@ | |
| 1 1 | 
             
            module Hutch
         | 
| 2 | 
            -
               | 
| 2 | 
            +
              if defined?(JRUBY_VERSION)
         | 
| 3 | 
            +
                require 'march_hare/exceptions'
         | 
| 4 | 
            +
                class Exception < MarchHare::Exception; end
         | 
| 5 | 
            +
              else
         | 
| 6 | 
            +
                require "bunny/exceptions"
         | 
| 7 | 
            +
                # Bunny::Exception inherits from StandardError
         | 
| 8 | 
            +
                class Exception < Bunny::Exception; end
         | 
| 9 | 
            +
              end
         | 
| 3 10 | 
             
              class ConnectionError < Exception; end
         | 
| 4 11 | 
             
              class AuthenticationError < Exception; end
         | 
| 5 12 | 
             
              class WorkerSetupError < Exception; end
         | 
    
        data/lib/hutch/logging.rb
    CHANGED
    
    | @@ -9,12 +9,12 @@ module Hutch | |
| 9 9 | 
             
                  end
         | 
| 10 10 | 
             
                end
         | 
| 11 11 |  | 
| 12 | 
            -
                def self.setup_logger | 
| 12 | 
            +
                def self.setup_logger
         | 
| 13 13 | 
             
                  require 'hutch/config'
         | 
| 14 | 
            -
                  @logger = Logger.new( | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
                   | 
| 14 | 
            +
                  @logger = Logger.new($stdout).tap do |l|
         | 
| 15 | 
            +
                    l.level = Hutch::Config.log_level
         | 
| 16 | 
            +
                    l.formatter = HutchFormatter.new
         | 
| 17 | 
            +
                  end
         | 
| 18 18 | 
             
                end
         | 
| 19 19 |  | 
| 20 20 | 
             
                def self.logger
         | 
    
        data/lib/hutch/message.rb
    CHANGED
    
    | @@ -1,6 +1,4 @@ | |
| 1 | 
            -
            require 'multi_json'
         | 
| 2 1 | 
             
            require 'forwardable'
         | 
| 3 | 
            -
            require 'active_support/core_ext/hash/indifferent_access'
         | 
| 4 2 |  | 
| 5 3 | 
             
            module Hutch
         | 
| 6 4 | 
             
              class Message
         | 
| @@ -8,11 +6,11 @@ module Hutch | |
| 8 6 |  | 
| 9 7 | 
             
                attr_reader :delivery_info, :properties, :payload
         | 
| 10 8 |  | 
| 11 | 
            -
                def initialize(delivery_info, properties, payload)
         | 
| 9 | 
            +
                def initialize(delivery_info, properties, payload, serializer)
         | 
| 12 10 | 
             
                  @delivery_info = delivery_info
         | 
| 13 11 | 
             
                  @properties    = properties
         | 
| 14 12 | 
             
                  @payload       = payload
         | 
| 15 | 
            -
                  @body          =  | 
| 13 | 
            +
                  @body          = serializer.decode(payload)
         | 
| 16 14 | 
             
                end
         | 
| 17 15 |  | 
| 18 16 | 
             
                def_delegator :@body, :[]
         | 
| @@ -0,0 +1,75 @@ | |
| 1 | 
            +
            require 'securerandom'
         | 
| 2 | 
            +
            require 'hutch/logging'
         | 
| 3 | 
            +
            require 'hutch/exceptions'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Hutch
         | 
| 6 | 
            +
              class Publisher
         | 
| 7 | 
            +
                include Logging
         | 
| 8 | 
            +
                attr_reader :connection, :channel, :exchange, :config
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def initialize(connection, channel, exchange, config = Hutch::Config)
         | 
| 11 | 
            +
                  @connection = connection
         | 
| 12 | 
            +
                  @channel    = channel
         | 
| 13 | 
            +
                  @exchange   = exchange
         | 
| 14 | 
            +
                  @config     = config
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def publish(routing_key, message, properties = {}, options = {})
         | 
| 18 | 
            +
                  ensure_connection!(routing_key, message)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  serializer = options[:serializer] || config[:serializer]
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  non_overridable_properties = {
         | 
| 23 | 
            +
                    routing_key:  routing_key,
         | 
| 24 | 
            +
                    timestamp:    connection.current_timestamp,
         | 
| 25 | 
            +
                    content_type: serializer.content_type,
         | 
| 26 | 
            +
                  }
         | 
| 27 | 
            +
                  properties[:message_id]   ||= generate_id
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  payload = serializer.encode(message)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  log_publication(serializer, payload, routing_key)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  response = exchange.publish(payload, {persistent: true}.
         | 
| 34 | 
            +
                    merge(properties).
         | 
| 35 | 
            +
                    merge(global_properties).
         | 
| 36 | 
            +
                    merge(non_overridable_properties))
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  channel.wait_for_confirms if config[:force_publisher_confirms]
         | 
| 39 | 
            +
                  response
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                private
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def log_publication(serializer, payload, routing_key)
         | 
| 45 | 
            +
                  logger.debug {
         | 
| 46 | 
            +
                    spec =
         | 
| 47 | 
            +
                      if serializer.binary?
         | 
| 48 | 
            +
                        "#{payload.bytesize} bytes message"
         | 
| 49 | 
            +
                      else
         | 
| 50 | 
            +
                        "message '#{payload}'"
         | 
| 51 | 
            +
                      end
         | 
| 52 | 
            +
                    "publishing #{spec} to #{routing_key}"
         | 
| 53 | 
            +
                  }
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def raise_publish_error(reason, routing_key, message)
         | 
| 57 | 
            +
                  msg = "unable to publish - #{reason}. Message: #{JSON.dump(message)}, Routing key: #{routing_key}."
         | 
| 58 | 
            +
                  logger.error(msg)
         | 
| 59 | 
            +
                  raise PublishError, msg
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def ensure_connection!(routing_key, message)
         | 
| 63 | 
            +
                  raise_publish_error('no connection to broker', routing_key, message) unless connection
         | 
| 64 | 
            +
                  raise_publish_error('connection is closed', routing_key, message) unless connection.open?
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def generate_id
         | 
| 68 | 
            +
                  SecureRandom.uuid
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def global_properties
         | 
| 72 | 
            +
                  Hutch.global_properties.respond_to?(:call) ? Hutch.global_properties.call : Hutch.global_properties
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            module Hutch
         | 
| 2 | 
            +
              module Serializers
         | 
| 3 | 
            +
                class Identity
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  def self.encode(payload)
         | 
| 6 | 
            +
                    payload
         | 
| 7 | 
            +
                  end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def self.decode(payload)
         | 
| 10 | 
            +
                    payload
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def self.binary? ; false ; end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def self.content_type ; nil ; end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            require 'multi_json'
         | 
| 2 | 
            +
            require 'active_support/core_ext/hash/indifferent_access'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Hutch
         | 
| 5 | 
            +
              module Serializers
         | 
| 6 | 
            +
                class JSON
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def self.encode(payload)
         | 
| 9 | 
            +
                    ::MultiJson.dump(payload)
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def self.decode(payload)
         | 
| 13 | 
            +
                    ::MultiJson.load(payload).with_indifferent_access
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def self.binary? ; false ; end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def self.content_type ; 'application/json' ; end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            require 'ddtrace'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Hutch
         | 
| 4 | 
            +
              module Tracers
         | 
| 5 | 
            +
                class Datadog
         | 
| 6 | 
            +
                  def initialize(klass)
         | 
| 7 | 
            +
                    @klass = klass
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def handle(message)
         | 
| 11 | 
            +
                    ::Datadog.tracer.trace(@klass.class.name, service: 'hutch', span_type: 'rabbitmq') do
         | 
| 12 | 
            +
                      @klass.process(message)
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
    
        data/lib/hutch/tracers.rb
    CHANGED
    
    
    
        data/lib/hutch/version.rb
    CHANGED
    
    
    
        data/lib/hutch/waiter.rb
    ADDED
    
    | @@ -0,0 +1,104 @@ | |
| 1 | 
            +
            require 'hutch/logging'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Hutch
         | 
| 4 | 
            +
              # Signal-handling class.
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # Currently, the signal USR2 performs a thread dump,
         | 
| 7 | 
            +
              # while QUIT, TERM and INT all perform a graceful shutdown.
         | 
| 8 | 
            +
              class Waiter
         | 
| 9 | 
            +
                include Logging
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                class ContinueProcessingSignals < RuntimeError
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def self.supported_signals_of(list)
         | 
| 15 | 
            +
                  list.keep_if { |s| Signal.list.keys.include?(s) }.tap do |result|
         | 
| 16 | 
            +
                    result.delete('QUIT') if defined?(JRUBY_VERSION)
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                SHUTDOWN_SIGNALS = supported_signals_of(%w(QUIT TERM INT)).freeze
         | 
| 21 | 
            +
                # We have chosen a JRuby-supported signal
         | 
| 22 | 
            +
                USER_SIGNALS = supported_signals_of(%w(USR2)).freeze
         | 
| 23 | 
            +
                REGISTERED_SIGNALS = (SHUTDOWN_SIGNALS + USER_SIGNALS).freeze
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def self.wait_until_signaled
         | 
| 26 | 
            +
                  new.wait_until_signaled
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def wait_until_signaled
         | 
| 30 | 
            +
                  self.sig_read, self.sig_write = IO.pipe
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  register_signal_handlers
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  begin
         | 
| 35 | 
            +
                    wait_for_signal
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    sig = sig_read.gets.strip
         | 
| 38 | 
            +
                    handle_signal(sig)
         | 
| 39 | 
            +
                  rescue ContinueProcessingSignals
         | 
| 40 | 
            +
                    retry
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def handle_signal(sig)
         | 
| 45 | 
            +
                  raise ContinueProcessingSignals unless REGISTERED_SIGNALS.include?(sig)
         | 
| 46 | 
            +
                  if user_signal?(sig)
         | 
| 47 | 
            +
                    handle_user_signal(sig)
         | 
| 48 | 
            +
                  else
         | 
| 49 | 
            +
                    handle_shutdown_signal(sig)
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                # @raise ContinueProcessingSignals
         | 
| 54 | 
            +
                def handle_user_signal(sig)
         | 
| 55 | 
            +
                  case sig
         | 
| 56 | 
            +
                  when 'USR2' then log_thread_backtraces
         | 
| 57 | 
            +
                  else raise "Assertion failed - unhandled signal: #{sig.inspect}"
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                  raise ContinueProcessingSignals
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def handle_shutdown_signal(sig)
         | 
| 63 | 
            +
                  logger.info "caught SIG#{sig}, stopping hutch..."
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                private
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def log_thread_backtraces
         | 
| 69 | 
            +
                  logger.info 'Requested a VM-wide thread stack trace dump...'
         | 
| 70 | 
            +
                  Thread.list.each do |thread|
         | 
| 71 | 
            +
                    logger.info "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
         | 
| 72 | 
            +
                    logger.info backtrace_for(thread)
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def backtrace_for(thread)
         | 
| 77 | 
            +
                  if thread.backtrace
         | 
| 78 | 
            +
                    thread.backtrace.join("\n")
         | 
| 79 | 
            +
                  else
         | 
| 80 | 
            +
                    '<no backtrace available>'
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                attr_accessor :sig_read, :sig_write
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                def wait_for_signal
         | 
| 87 | 
            +
                  IO.select([sig_read])
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                def register_signal_handlers
         | 
| 91 | 
            +
                  REGISTERED_SIGNALS.each do |sig|
         | 
| 92 | 
            +
                    # This needs to be reentrant, so we queue up signals to be handled
         | 
| 93 | 
            +
                    # in the run loop, rather than acting on signals here
         | 
| 94 | 
            +
                    trap(sig) do
         | 
| 95 | 
            +
                      sig_write.puts(sig)
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def user_signal?(sig)
         | 
| 101 | 
            +
                  USER_SIGNALS.include?(sig)
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
              end
         | 
| 104 | 
            +
            end
         | 
    
        data/lib/hutch/worker.rb
    CHANGED
    
    | @@ -1,15 +1,19 @@ | |
| 1 1 | 
             
            require 'hutch/message'
         | 
| 2 2 | 
             
            require 'hutch/logging'
         | 
| 3 3 | 
             
            require 'hutch/broker'
         | 
| 4 | 
            +
            require 'hutch/acknowledgements/nack_on_all_failures'
         | 
| 5 | 
            +
            require 'hutch/waiter'
         | 
| 4 6 | 
             
            require 'carrot-top'
         | 
| 7 | 
            +
            require 'securerandom'
         | 
| 5 8 |  | 
| 6 9 | 
             
            module Hutch
         | 
| 7 10 | 
             
              class Worker
         | 
| 8 11 | 
             
                include Logging
         | 
| 9 12 |  | 
| 10 | 
            -
                def initialize(broker, consumers)
         | 
| 13 | 
            +
                def initialize(broker, consumers, setup_procs)
         | 
| 11 14 | 
             
                  @broker        = broker
         | 
| 12 15 | 
             
                  self.consumers = consumers
         | 
| 16 | 
            +
                  self.setup_procs = setup_procs
         | 
| 13 17 | 
             
                end
         | 
| 14 18 |  | 
| 15 19 | 
             
                # Run the main event loop. The consumers will be set up with queues, and
         | 
| @@ -17,46 +21,11 @@ module Hutch | |
| 17 21 | 
             
                # never returns.
         | 
| 18 22 | 
             
                def run
         | 
| 19 23 | 
             
                  setup_queues
         | 
| 24 | 
            +
                  setup_procs.each(&:call)
         | 
| 20 25 |  | 
| 21 | 
            -
                   | 
| 22 | 
            -
                  register_signal_handlers
         | 
| 26 | 
            +
                  Waiter.wait_until_signaled
         | 
| 23 27 |  | 
| 24 | 
            -
                   | 
| 25 | 
            -
                end
         | 
| 26 | 
            -
             | 
| 27 | 
            -
                def main_loop
         | 
| 28 | 
            -
                  if defined?(JRUBY_VERSION)
         | 
| 29 | 
            -
                    # Binds shutdown listener to notify main thread if channel was closed
         | 
| 30 | 
            -
                    bind_shutdown_handler
         | 
| 31 | 
            -
             | 
| 32 | 
            -
                    handle_signals until shutdown_not_called?(0.1)
         | 
| 33 | 
            -
                  else
         | 
| 34 | 
            -
                    # Take a break from Thread#join every 0.1 seconds to check if we've
         | 
| 35 | 
            -
                    # been sent any signals
         | 
| 36 | 
            -
                    handle_signals until @broker.wait_on_threads(0.1)
         | 
| 37 | 
            -
                  end
         | 
| 38 | 
            -
                end
         | 
| 39 | 
            -
             | 
| 40 | 
            -
                # Register handlers for SIG{QUIT,TERM,INT} to shut down the worker
         | 
| 41 | 
            -
                # gracefully. Forceful shutdowns are very bad!
         | 
| 42 | 
            -
                def register_signal_handlers
         | 
| 43 | 
            -
                  Thread.main[:signal_queue] = []
         | 
| 44 | 
            -
                  %w(QUIT TERM INT).keep_if { |s| Signal.list.keys.include? s }.map(&:to_sym).each do |sig|
         | 
| 45 | 
            -
                    # This needs to be reentrant, so we queue up signals to be handled
         | 
| 46 | 
            -
                    # in the run loop, rather than acting on signals here
         | 
| 47 | 
            -
                    trap(sig) do
         | 
| 48 | 
            -
                      Thread.main[:signal_queue] << sig
         | 
| 49 | 
            -
                    end
         | 
| 50 | 
            -
                  end
         | 
| 51 | 
            -
                end
         | 
| 52 | 
            -
             | 
| 53 | 
            -
                # Handle any pending signals
         | 
| 54 | 
            -
                def handle_signals
         | 
| 55 | 
            -
                  signal = Thread.main[:signal_queue].shift
         | 
| 56 | 
            -
                  if signal
         | 
| 57 | 
            -
                    logger.info "caught sig#{signal.downcase}, stopping hutch..."
         | 
| 58 | 
            -
                    stop
         | 
| 59 | 
            -
                  end
         | 
| 28 | 
            +
                  stop
         | 
| 60 29 | 
             
                end
         | 
| 61 30 |  | 
| 62 31 | 
             
                # Stop a running worker by killing all subscriber threads.
         | 
| @@ -64,36 +33,24 @@ module Hutch | |
| 64 33 | 
             
                  @broker.stop
         | 
| 65 34 | 
             
                end
         | 
| 66 35 |  | 
| 67 | 
            -
                # Binds shutdown handler, called if channel is closed or network Failed
         | 
| 68 | 
            -
                def bind_shutdown_handler
         | 
| 69 | 
            -
                  @broker.channel.on_shutdown do
         | 
| 70 | 
            -
                    Thread.main[:shutdown_received] = true
         | 
| 71 | 
            -
                  end
         | 
| 72 | 
            -
                end
         | 
| 73 | 
            -
             | 
| 74 | 
            -
                # Checks if shutdown handler was called, then sleeps for interval
         | 
| 75 | 
            -
                def shutdown_not_called?(interval)
         | 
| 76 | 
            -
                  if Thread.main[:shutdown_received]
         | 
| 77 | 
            -
                    true
         | 
| 78 | 
            -
                  else
         | 
| 79 | 
            -
                    sleep(interval)
         | 
| 80 | 
            -
                    false
         | 
| 81 | 
            -
                  end
         | 
| 82 | 
            -
                end
         | 
| 83 | 
            -
             | 
| 84 36 | 
             
                # Set up the queues for each of the worker's consumers.
         | 
| 85 37 | 
             
                def setup_queues
         | 
| 86 38 | 
             
                  logger.info 'setting up queues'
         | 
| 87 | 
            -
                  @consumers. | 
| 39 | 
            +
                  vetted = @consumers.reject { |c| group_configured? && group_restricted?(c) }
         | 
| 40 | 
            +
                  vetted.each do |c|
         | 
| 41 | 
            +
                    setup_queue(c)
         | 
| 42 | 
            +
                  end
         | 
| 88 43 | 
             
                end
         | 
| 89 44 |  | 
| 90 45 | 
             
                # Bind a consumer's routing keys to its queue, and set up a subscription to
         | 
| 91 46 | 
             
                # receive messages sent to the queue.
         | 
| 92 47 | 
             
                def setup_queue(consumer)
         | 
| 48 | 
            +
                  logger.info "setting up queue: #{consumer.get_queue_name}"
         | 
| 49 | 
            +
             | 
| 93 50 | 
             
                  queue = @broker.queue(consumer.get_queue_name, consumer.get_arguments)
         | 
| 94 51 | 
             
                  @broker.bind_queue(queue, consumer.routing_keys)
         | 
| 95 52 |  | 
| 96 | 
            -
                  queue.subscribe(manual_ack: true) do |*args|
         | 
| 53 | 
            +
                  queue.subscribe(consumer_tag: unique_consumer_tag, manual_ack: true) do |*args|
         | 
| 97 54 | 
             
                    delivery_info, properties, payload = Hutch::Adapter.decode_message(*args)
         | 
| 98 55 | 
             
                    handle_message(consumer, delivery_info, properties, payload)
         | 
| 99 56 | 
             
                  end
         | 
| @@ -102,30 +59,39 @@ module Hutch | |
| 102 59 | 
             
                # Called internally when a new messages comes in from RabbitMQ. Responsible
         | 
| 103 60 | 
             
                # for wrapping up the message and passing it to the consumer.
         | 
| 104 61 | 
             
                def handle_message(consumer, delivery_info, properties, payload)
         | 
| 105 | 
            -
                   | 
| 106 | 
            -
             | 
| 107 | 
            -
             | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 116 | 
            -
                   | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 119 | 
            -
                   | 
| 62 | 
            +
                  serializer = consumer.get_serializer || Hutch::Config[:serializer]
         | 
| 63 | 
            +
                  logger.debug {
         | 
| 64 | 
            +
                    spec   = serializer.binary? ? "#{payload.bytesize} bytes" : "#{payload}"
         | 
| 65 | 
            +
                    "message(#{properties.message_id || '-'}): " +
         | 
| 66 | 
            +
                    "routing key: #{delivery_info.routing_key}, " +
         | 
| 67 | 
            +
                    "consumer: #{consumer}, " +
         | 
| 68 | 
            +
                    "payload: #{spec}"
         | 
| 69 | 
            +
                  }
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  message = Message.new(delivery_info, properties, payload, serializer)
         | 
| 72 | 
            +
                  consumer_instance = consumer.new.tap { |c| c.broker, c.delivery_info = @broker, delivery_info }
         | 
| 73 | 
            +
                  with_tracing(consumer_instance).handle(message)
         | 
| 74 | 
            +
                  @broker.ack(delivery_info.delivery_tag)
         | 
| 75 | 
            +
                rescue => ex
         | 
| 76 | 
            +
                  acknowledge_error(delivery_info, properties, @broker, ex)
         | 
| 77 | 
            +
                  handle_error(properties, payload, consumer, ex)
         | 
| 120 78 | 
             
                end
         | 
| 121 79 |  | 
| 122 80 | 
             
                def with_tracing(klass)
         | 
| 123 81 | 
             
                  Hutch::Config[:tracer].new(klass)
         | 
| 124 82 | 
             
                end
         | 
| 125 83 |  | 
| 126 | 
            -
                def handle_error( | 
| 84 | 
            +
                def handle_error(*args)
         | 
| 127 85 | 
             
                  Hutch::Config[:error_handlers].each do |backend|
         | 
| 128 | 
            -
                    backend.handle( | 
| 86 | 
            +
                    backend.handle(*args)
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                def acknowledge_error(delivery_info, properties, broker, ex)
         | 
| 91 | 
            +
                  acks = error_acknowledgements +
         | 
| 92 | 
            +
                    [Hutch::Acknowledgements::NackOnAllFailures.new]
         | 
| 93 | 
            +
                  acks.find do |backend|
         | 
| 94 | 
            +
                    backend.handle(delivery_info, properties, broker, ex)
         | 
| 129 95 | 
             
                  end
         | 
| 130 96 | 
             
                end
         | 
| 131 97 |  | 
| @@ -135,5 +101,45 @@ module Hutch | |
| 135 101 | 
             
                  end
         | 
| 136 102 | 
             
                  @consumers = val
         | 
| 137 103 | 
             
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                def error_acknowledgements
         | 
| 106 | 
            +
                  Hutch::Config[:error_acknowledgements]
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                private
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                def group_configured?
         | 
| 112 | 
            +
                  if group.present? && consumer_groups.blank?
         | 
| 113 | 
            +
                    logger.info 'Consumer groups are blank'
         | 
| 114 | 
            +
                  end
         | 
| 115 | 
            +
                  group.present?
         | 
| 116 | 
            +
                end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                def group_restricted?(consumer)
         | 
| 119 | 
            +
                  consumers_to_load = consumer_groups[group]
         | 
| 120 | 
            +
                  if consumers_to_load
         | 
| 121 | 
            +
                    !consumers_to_load.include?(consumer.name)
         | 
| 122 | 
            +
                  else
         | 
| 123 | 
            +
                    true
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                def group
         | 
| 128 | 
            +
                  Hutch::Config[:group]
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                def consumer_groups
         | 
| 132 | 
            +
                  Hutch::Config[:consumer_groups]
         | 
| 133 | 
            +
                end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                attr_accessor :setup_procs
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                def unique_consumer_tag
         | 
| 138 | 
            +
                  prefix = Hutch::Config[:consumer_tag_prefix]
         | 
| 139 | 
            +
                  unique_part = SecureRandom.uuid
         | 
| 140 | 
            +
                  "#{prefix}-#{unique_part}".tap do |tag|
         | 
| 141 | 
            +
                    raise "Tag must be 255 bytes long at most, current one is #{tag.bytesize} ('#{tag}')" if tag.bytesize > 255
         | 
| 142 | 
            +
                  end
         | 
| 143 | 
            +
                end
         | 
| 138 144 | 
             
              end
         | 
| 139 145 | 
             
            end
         | 
    
        data/lib/hutch.rb
    CHANGED
    
    | @@ -3,6 +3,8 @@ require 'hutch/consumer' | |
| 3 3 | 
             
            require 'hutch/worker'
         | 
| 4 4 | 
             
            require 'hutch/broker'
         | 
| 5 5 | 
             
            require 'hutch/logging'
         | 
| 6 | 
            +
            require 'hutch/serializers/identity'
         | 
| 7 | 
            +
            require 'hutch/serializers/json'
         | 
| 6 8 | 
             
            require 'hutch/config'
         | 
| 7 9 | 
             
            require 'hutch/message'
         | 
| 8 10 | 
             
            require 'hutch/cli'
         | 
| @@ -12,6 +14,7 @@ require 'hutch/exceptions' | |
| 12 14 | 
             
            require 'hutch/tracers'
         | 
| 13 15 |  | 
| 14 16 | 
             
            module Hutch
         | 
| 17 | 
            +
              @@connection_mutex = Mutex.new
         | 
| 15 18 |  | 
| 16 19 | 
             
              def self.register_consumer(consumer)
         | 
| 17 20 | 
             
                self.consumers << consumer
         | 
| @@ -33,10 +36,17 @@ module Hutch | |
| 33 36 | 
             
                @global_properties ||= {}
         | 
| 34 37 | 
             
              end
         | 
| 35 38 |  | 
| 39 | 
            +
              # Connects to broker, if not yet connected.
         | 
| 40 | 
            +
              #
         | 
| 41 | 
            +
              # @param options [Hash] Connection options
         | 
| 42 | 
            +
              # @param config [Hash] Configuration
         | 
| 43 | 
            +
              # @option options [Boolean] :enable_http_api_use
         | 
| 36 44 | 
             
              def self.connect(options = {}, config = Hutch::Config)
         | 
| 37 | 
            -
                 | 
| 38 | 
            -
                   | 
| 39 | 
            -
             | 
| 45 | 
            +
                @@connection_mutex.synchronize do
         | 
| 46 | 
            +
                  unless connected?
         | 
| 47 | 
            +
                    @broker = Hutch::Broker.new(config)
         | 
| 48 | 
            +
                    @broker.connect(options)
         | 
| 49 | 
            +
                  end
         | 
| 40 50 | 
             
                end
         | 
| 41 51 | 
             
              end
         | 
| 42 52 |  | 
| @@ -48,10 +58,9 @@ module Hutch | |
| 48 58 | 
             
                @broker
         | 
| 49 59 | 
             
              end
         | 
| 50 60 |  | 
| 61 | 
            +
              # @return [Boolean]
         | 
| 51 62 | 
             
              def self.connected?
         | 
| 52 | 
            -
                 | 
| 53 | 
            -
                return false unless broker.connection
         | 
| 54 | 
            -
                broker.connection.open?
         | 
| 63 | 
            +
                broker && broker.connection && broker.connection.open?
         | 
| 55 64 | 
             
              end
         | 
| 56 65 |  | 
| 57 66 | 
             
              def self.publish(*args)
         |