diff --git a/lib/raven/configuration.rb b/lib/raven/configuration.rb index 5a6ae8c40..92a48d6f2 100644 --- a/lib/raven/configuration.rb +++ b/lib/raven/configuration.rb @@ -91,6 +91,10 @@ class Configuration # We automatically try to set this to a git SHA or Capistrano release. attr_accessor :release + # Array of exception classes that should not be reported until all retries + # are exhausted for a Sidekiq job. + attr_accessor :retryable_exceptions + # The sampling factor to apply to events. A value of 0.0 will not send # any events, and a value of 1.0 will send 100% of events. attr_accessor :sample_rate @@ -198,6 +202,7 @@ def initialize self.project_root = detect_project_root self.rails_activesupport_breadcrumbs = false self.rails_report_rescued_exceptions = true + self.retryable_exceptions = [] self.release = detect_release self.sample_rate = 1.0 self.sanitize_credit_cards = true @@ -309,6 +314,10 @@ def exception_class_allowed?(exc) end end + def retryable_exception?(exc) + retryable_exceptions.any? { |x| get_exception_class(x) === exc } + end + private def detect_project_root diff --git a/lib/raven/integrations/sidekiq.rb b/lib/raven/integrations/sidekiq.rb index 38b96308f..00ad4db70 100644 --- a/lib/raven/integrations/sidekiq.rb +++ b/lib/raven/integrations/sidekiq.rb @@ -1,5 +1,10 @@ require 'time' require 'sidekiq' +begin + # Sidekiq 5 introduces JobRetry and stores the default max retry attempts there. + require 'sidekiq/job_retry' +rescue LoadError # rubocop:disable Lint/HandleExceptions +end module Raven class SidekiqCleanupMiddleware @@ -15,7 +20,10 @@ def call(_worker, job, queue) class SidekiqErrorHandler ACTIVEJOB_RESERVED_PREFIX = "_aj_".freeze - def call(ex, context) + def call(ex, context, options = {}) + configuration = options[:configuration] || Raven.configuration + return if configuration.retryable_exception?(ex) && remaining_retries?(context) + context = filter_context(context) Raven.context.transaction.push transaction_from_context(context) Raven.capture_exception( @@ -51,6 +59,27 @@ def filter_context_hash(key, value) [key, filter_context(value)] end + def remaining_retries?(context) + job = context[:job] || context # Sidekiq < 4 does not have job key. + return false unless job && job["retry"] + job["retry_count"] < retry_attempts_from(job["retry"]) + end + + def retry_attempts_from(retries) + if retries.is_a?(Integer) + retries + else + default_max_attempts = + if defined?(Sidekiq::JobRetry) + Sidekiq::JobRetry::DEFAULT_MAX_RETRY_ATTEMPTS # Sidekiq 5 + else + Sidekiq::Middleware::Server::RetryJobs::DEFAULT_MAX_RETRY_ATTEMPTS # Sidekiq < 5 + end + + Sidekiq.options.fetch(:max_retries, default_max_attempts) + end + end + # this will change in the future: # https://github.com/mperham/sidekiq/pull/3161 def transaction_from_context(context) diff --git a/spec/raven/integrations/sidekiq_spec.rb b/spec/raven/integrations/sidekiq_spec.rb index 37e76b9e4..8aae5493a 100644 --- a/spec/raven/integrations/sidekiq_spec.rb +++ b/spec/raven/integrations/sidekiq_spec.rb @@ -3,6 +3,7 @@ require 'raven/integrations/sidekiq' require 'sidekiq/processor' + require 'sidekiq/job_retry' RSpec.describe "Raven::SidekiqErrorHandler" do let(:context) do @@ -67,6 +68,42 @@ Raven::SidekiqErrorHandler.new.call(exception, aj_context) end + + context "with a retryable exception" do + class RetryableError < StandardError; end + + let(:configuration) do + Raven::Configuration.new.tap do |c| + c.retryable_exceptions << RetryableError + end + end + + [ + { "retry" => false, "retry_count" => 0 }, + { "retry" => true, "retry_count" => Sidekiq::JobRetry::DEFAULT_MAX_RETRY_ATTEMPTS }, + { "retry" => 2, "retry_count" => 2 } + ].each do |retry_context| + it "captures an exception when retries are exhausted" do + exception = RetryableError.new + + expect(Raven).to receive(:capture_exception).with(exception, anything) + Raven::SidekiqErrorHandler.new.call(exception, retry_context, :configuration => configuration) + end + end + + [ + { "retry" => true, "retry_count" => 0 }, + { "retry" => 2, "retry_count" => 0 }, + { "retry" => 2, "retry_count" => 1 } + ].each do |retry_context| + it "does not capture an exception when retries are remaining" do + exception = RetryableError.new + + expect(Raven).not_to receive(:capture_exception) + Raven::SidekiqErrorHandler.new.call(exception, retry_context, :configuration => configuration) + end + end + end end class HappyWorker