Skip to content

Commit 833d49d

Browse files
solnicCodex
andcommitted
feat(sidekiq): migrate to ActiveJob-first integration
Co-Authored-By: Codex <noreply@example.com>
1 parent 2ee2e77 commit 833d49d

14 files changed

Lines changed: 1397 additions & 212 deletions

‎sentry-sidekiq/Rakefile‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
# frozen_string_literal: true
22

33
require "bundler/gem_tasks"
4-
require_relative "../lib/sentry/test/rake_tasks"
5-
6-
Sentry::Test::RakeTasks.define_spec_tasks(
7-
spec_pattern: "spec/sentry/**/*_spec.rb",
8-
spec_rspec_opts: "--order rand --format progress",
9-
isolated_specs_pattern: "spec/isolated/**/*_spec.rb",
10-
isolated_rspec_opts: "--format progress"
11-
)
4+
require "rspec/core/rake_task"
5+
6+
RSpec::Core::RakeTask.new(:spec) do |task|
7+
task.pattern = "spec/sentry/**/*_spec.rb"
8+
task.rspec_opts = "--order rand --format progress"
9+
end
10+
11+
namespace :spec do
12+
isolated_tasks = Dir["spec/isolated/**/*_spec.rb"].sort.map do |file|
13+
task_name = "isolated:#{file.delete_prefix("spec/isolated/").delete_suffix("_spec.rb")}"
14+
15+
RSpec::Core::RakeTask.new(task_name) do |task|
16+
task.pattern = file
17+
task.rspec_opts = "--format progress"
18+
end
19+
20+
task_name
21+
end
22+
23+
task isolated: isolated_tasks
24+
end
1225

1326
task default: [:spec, :"spec:isolated"]

‎sentry-sidekiq/lib/sentry-sidekiq.rb‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require "sentry/integrable"
66
require "sentry/sidekiq/version"
77
require "sentry/sidekiq/configuration"
8+
require "sentry/sidekiq/job_extension"
89
require "sentry/sidekiq/error_handler"
910
require "sentry/sidekiq/sentry_context_middleware"
1011

@@ -14,12 +15,18 @@ module Sidekiq
1415

1516
register_integration name: "sidekiq", version: Sentry::Sidekiq::VERSION
1617

18+
def self.job_extension
19+
@job_extension ||= JobExtension.new
20+
end
21+
22+
Sentry.register_job_extension(job_extension)
23+
1724
if defined?(::Rails::Railtie)
1825
class Railtie < ::Rails::Railtie
1926
config.after_initialize do
2027
next unless Sentry.initialized? && defined?(::Sentry::Rails)
2128

22-
Sentry.configuration.rails.skippable_job_adapters << "ActiveJob::QueueAdapters::SidekiqAdapter"
29+
Sentry.configuration.rails.skippable_job_adapters << JobExtension::ACTIVE_JOB_ADAPTER
2330
end
2431
end
2532
end

‎sentry-sidekiq/lib/sentry/sidekiq/context_filter.rb‎

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,7 @@ def filtered
3535
end
3636

3737
def transaction_name
38-
class_name = (context["wrapped"] || context["class"] ||
39-
(context[:job] && (context[:job]["wrapped"] || context[:job]["class"]))
40-
)
41-
42-
if class_name
43-
"#{SIDEKIQ_NAME}/#{class_name}"
44-
elsif context[:event]
45-
"#{SIDEKIQ_NAME}/#{context[:event]}"
46-
else
47-
SIDEKIQ_NAME
48-
end
38+
Sentry::Sidekiq.job_extension.error_context_transaction_name(context)
4939
end
5040

5141
private
Lines changed: 13 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# frozen_string_literal: true
22

33
require "sentry/sidekiq/context_filter"
4+
require "sentry/sidekiq/failure_reporting"
5+
require "sentry/sidekiq/sentry_context_middleware"
46

57
module Sentry
68
module Sidekiq
79
class ErrorHandler
8-
WITH_SIDEKIQ_7 = ::Gem::Version.new(::Sidekiq::VERSION) >= ::Gem::Version.new("7.0")
10+
def initialize(failure_reporting: FailureReporting.new, consumer: SentryContextServerMiddleware.consumer, job_extension: Sentry::Sidekiq.job_extension)
11+
@failure_reporting = failure_reporting
12+
@consumer = consumer
13+
@job_extension = job_extension
14+
end
915

1016
# @param ex [Exception] the exception / error that occured
1117
# @param context [Hash or Array] Sidekiq error context
@@ -16,83 +22,19 @@ class ErrorHandler
1622
def call(ex, context, sidekiq_config = nil)
1723
return unless Sentry.initialized?
1824

19-
context_filter = Sentry::Sidekiq::ContextFilter.new(context)
20-
2125
scope = Sentry.get_current_scope
22-
scope.set_transaction_name(context_filter.transaction_name, source: :task) unless scope.transaction_name
23-
24-
retry_lim = retry_limit(context, sidekiq_config)
25-
26-
# If Sentry is configured to only report an error _after_ all retries have been exhausted,
27-
# and if the job is retryable, and have not exceeded the retry_limit,
28-
# return early.
29-
if Sentry.configuration.sidekiq.report_after_job_retries && retryable?(context)
30-
retry_count = context.dig(:job, "retry_count")
31-
if retry_count.nil? || retry_count < retry_lim - 1
32-
return
33-
end
34-
end
35-
36-
# See if we want to ignore jobs that have dead: false
37-
return if Sentry.configuration.sidekiq.report_only_dead_jobs && context.dig(:job, "dead") == false
38-
39-
# Check if the retry count is below the attempt_threshold
40-
attempt_threshold = context.dig(:job, "attempt_threshold")
41-
if attempt_threshold && retryable?(context)
42-
attempt_threshold = attempt_threshold.to_i
43-
retry_count = context.dig(:job, "retry_count")
44-
# attempt 1 - retry_count is nil
45-
# attempt 2 - this is your first retry so retry_count is 0
46-
# attempt 3 - you have retried once, retry_count is 1
47-
attempt = retry_count.nil? ? 1 : retry_count.to_i + 2
48-
# Cap at the final attempt so jobs with fewer retries than the threshold still report.
49-
effective_threshold = [attempt_threshold, retry_lim + 1].min
26+
scope.set_transaction_name(@job_extension.error_context_transaction_name(context), source: :task) unless scope.transaction_name
5027

51-
return if attempt < effective_threshold
52-
end
28+
job = context[:job]
29+
return unless @failure_reporting.report?(job, sidekiq_config)
5330

54-
Sentry::Sidekiq.capture_exception(
55-
ex,
56-
contexts: { sidekiq: context_filter.filtered },
57-
hint: { background: false }
31+
@consumer.capture_exception(
32+
job, ex,
33+
contexts: { sidekiq: ContextFilter.new(context).filtered }
5834
)
5935
ensure
6036
scope&.clear
6137
end
62-
63-
private
64-
65-
def retryable?(context)
66-
retry_option = context.dig(:job, "retry")
67-
# when `retry` is not specified, it's default is `true` and it means 25 retries.
68-
retry_option.nil? || retry_option == true || (retry_option.is_a?(Integer) && retry_option.positive?)
69-
end
70-
71-
# @return [Integer] the number of retries allowed for the job
72-
# Tries to fetch the retry limit from the job config first,
73-
# then falls back to Sidekiq's configuration.
74-
def retry_limit(context, sidekiq_config)
75-
limit = context.dig(:job, "retry")
76-
77-
case limit
78-
when Integer
79-
limit
80-
when TrueClass, NilClass
81-
max_retries =
82-
if WITH_SIDEKIQ_7
83-
# Sidekiq 7.1.5+ passes the config to the error handler, so we should use that.
84-
# Sidekiq 7.0 -> 7.1.5 provides ::Sidekiq.default_configuration.
85-
sidekiq_config.is_a?(::Sidekiq::Config) ?
86-
sidekiq_config[:max_retries] :
87-
::Sidekiq.default_configuration[:max_retries]
88-
else
89-
::Sidekiq.options[:max_retries]
90-
end
91-
max_retries || 25
92-
else
93-
0
94-
end
95-
end
9638
end
9739
end
9840
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
require "sentry/sidekiq/retry_policy"
4+
5+
module Sentry
6+
module Sidekiq
7+
class FailureReporting
8+
def initialize(retry_policy: RetryPolicy.build)
9+
@retry_policy = retry_policy
10+
end
11+
12+
def report?(job, sidekiq_config = nil)
13+
config = Sentry.configuration.sidekiq
14+
retry_limit = @retry_policy.limit(job, sidekiq_config)
15+
16+
return false if config.report_after_job_retries && retries_pending?(job, retry_limit)
17+
return false if config.report_only_dead_jobs && job_value(job, "dead") == false
18+
return false if below_attempt_threshold?(job, retry_limit)
19+
20+
true
21+
end
22+
23+
private
24+
25+
def retries_pending?(job, retry_limit)
26+
return false unless @retry_policy.retryable?(job)
27+
28+
retry_count = job_value(job, "retry_count")
29+
retry_count.nil? || retry_count < retry_limit - 1
30+
end
31+
32+
def below_attempt_threshold?(job, retry_limit)
33+
attempt_threshold = job_value(job, "attempt_threshold")
34+
return false unless attempt_threshold && @retry_policy.retryable?(job)
35+
36+
[attempt_threshold.to_i, retry_limit + 1].min > attempt(job)
37+
end
38+
39+
def attempt(job)
40+
retry_count = job_value(job, "retry_count")
41+
retry_count.nil? ? 1 : retry_count.to_i + 2
42+
end
43+
44+
def job_value(job, key)
45+
job && job[key]
46+
end
47+
end
48+
end
49+
end

0 commit comments

Comments
 (0)