sidekiq-rescue 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8bcb824ca2945f9cf090e5cb6adfd0a4bc77366461c068915008e5ea953db31
4
- data.tar.gz: 97011a0a37cefea2f6500e7d96147b50059aa46289112de6ce09a5aa9773f963
3
+ metadata.gz: 01671c36411fb052587442c08db979d740c563e6d74311ed1c4d17293b266745
4
+ data.tar.gz: 90de1649e943e5a422dc6d601d7b7425ee74958418ed9f89ef635c27aac1b0b7
5
5
  SHA512:
6
- metadata.gz: 1f192b50d1b21265ef6902ce095129c0287df13c6684fc4eeaa125a70bdfe722aec40a2d7cc74dd1bbbf937bdeca436e3870ebe00ef1d3b7d818433df6efda08
7
- data.tar.gz: 79a2a91abcdb591ed762084676ec58732dfb4ce797fbd7335b42b8497933b1e383ec126ed64b16e5dabd5b7925d8c1b7bf0e931cd7cc71b90051bb6a23a0d045
6
+ metadata.gz: c60d5673e1c25f7634820a760e4e4f0463ab618187147f96a412527db866944f7a37b44327d50053066c83b2e2694c09570befba78902053969b27a116968df7
7
+ data.tar.gz: 291b24d3f090b425702bd9ec2f644f8bb92e374deb7cb20d9553574b30b3502d7091544de81a2769238fbb3f4ea3851b9939299a2630dd54d2b416285db3c587
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2024-05-30
4
+
5
+ - Fix issue with RSpec matcher when job is not rescueable
6
+ - Add support for multiple invocations of the DSL #3
7
+ - Update documentation with new features
8
+
9
+ ## [0.2.1] - 2024-02-27
10
+
11
+ - Fix readme with correct middleware name
12
+ - Add RSpec matchers
13
+
3
14
  ## [0.2.0] - 2024-02-03
4
15
 
5
16
  - Rename `Sidekiq::Rescue::DSL` to `Sidekiq::Rescue::Dsl`
@@ -15,6 +26,7 @@
15
26
  - Add documentation
16
27
  - Add CI
17
28
 
18
- [Unreleased]: https://github.com/moofkit/sidekiq-rescue/compare/v0.2.0...HEAD
29
+ [Unreleased]: https://github.com/moofkit/sidekiq-rescue/compare/v0.2.1...HEAD
30
+ [0.2.1]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.2.1
19
31
  [0.2.0]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.2.0
20
32
  [0.1.0]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.1.0
data/README.md CHANGED
@@ -3,6 +3,31 @@
3
3
  [![Build Status](https://github.com/moofkit/sidekiq-rescue/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/moofkit/sidekiq-rescue/actions/workflows/main.yml)
4
4
 
5
5
  [Sidekiq](https://github.com/sidekiq/sidekiq) plugin to rescue jobs from expected errors and retry them later.
6
+ Catch expected errors and retry the job with a delay and a limit. It's useful when you want to retry jobs that failed due to expected errors and not spam your exception tracker with these errors. If the exception will getting raised beyond the limit, it will be re-raised and will be handled by Sidekiq standard retry mechanism.
7
+
8
+ Handlers are searched from bottom to top, and up the inheritance chain. The first handler that `exception.is_a?(klass)` holds true will be used.
9
+
10
+ ## Example
11
+
12
+ ```ruby
13
+ class MyJob
14
+ include Sidekiq::Job
15
+ include Sidekiq::Rescue::Dsl
16
+
17
+ sidekiq_rescue CustomAppException # defaults to 60 seconds delay and 10 retries
18
+ sidekiq_rescue AnotherCustomAppException, delay: ->(counter) { counter * 2 }
19
+ sidekiq_rescue CustomInfrastructureException, delay: 5.minutes
20
+ sidekiq_rescue ActiveRecord::Deadlocked, delay: 5.seconds, limit: 3
21
+ sidekiq_rescue Net::OpenTimeout, Timeout::Error, limit: 10 # retries at most 10 times for Net::OpenTimeout and Timeout::Error combined
22
+
23
+ def perform(*args)
24
+ # Might raise CustomAppException, AnotherCustomAppException, or YetAnotherCustomAppException for something domain specific
25
+ # Might raise ActiveRecord::Deadlocked when a local db deadlock is detected
26
+ # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down
27
+ end
28
+ end
29
+ ```
30
+
6
31
 
7
32
  ## Installation
8
33
 
@@ -27,7 +52,7 @@ Or install it yourself as:
27
52
  ```ruby
28
53
  Sidekiq.configure_server do |config|
29
54
  config.server_middleware do |chain|
30
- chain.add Sidekiq::Rescue::Middleware
55
+ chain.add Sidekiq::Rescue::ServerMiddleware
31
56
  end
32
57
  end
33
58
  ```
@@ -93,6 +118,68 @@ Sidekiq::Rescue.configure do |config|
93
118
  end
94
119
  ```
95
120
 
121
+ ### Testing
122
+
123
+ 1. Unit tests (recommended)
124
+
125
+ In case you want to test the rescue configuration, this gem provides RSpec matchers:
126
+
127
+ ```ruby
128
+ RSpec.cofigure do |config|
129
+ config.include Sidekiq::Rescue::RSpec::Matchers, type: :job
130
+ end
131
+
132
+ RSpec.describe MyJob do
133
+ it "rescues from expected errors" do
134
+ expect(MyJob).to have_sidekiq_rescue(ExpectedError)
135
+ end
136
+ end
137
+ ```
138
+
139
+ It also provides a way to test the delay and limit:
140
+
141
+ ```ruby
142
+ RSpec.describe MyJob do
143
+ it "rescues from expected errors with custom delay and limit" do
144
+ expect(MyJob).to have_sidekiq_rescue(ExpectedError).with_delay(60).with_limit(5)
145
+ end
146
+ end
147
+ ```
148
+
149
+ 2. Integration tests with `Sidekiq::Testing`
150
+ Firstly, you need to configure `Sidekiq::Testing` to use `Sidekiq::Rescue::ServerMiddleware` middleware:
151
+
152
+ ```ruby
153
+ # spec/spec_helper.rb or spec/rails_helper.rb
154
+ require "sidekiq/testing"
155
+
156
+ RSpec.configure do |config|
157
+ config.before(:all) do
158
+ Sidekiq::Testing.fake!
159
+
160
+ Sidekiq::Testing.server_middleware do |chain|
161
+ chain.add Sidekiq::Rescue::ServerMiddleware
162
+ end
163
+ end
164
+ end
165
+ ```
166
+
167
+ And test the job with the next snippet
168
+
169
+ ```ruby
170
+ # spec/jobs/my_job_spec.rb
171
+ RSpec.describe MyJob do
172
+ before do
173
+ allow(ApiClient).to receive(:new).and_raise(ApiClient::SomethingWentWrongError)
174
+ end
175
+
176
+ it "retries job if it fails with ExpectedError" do
177
+ MyJob.perform_async('test')
178
+ expect { MyJob.perform_one }.not_to raise_error # pefrom_one is a method from Sidekiq::Testing that runs the job once
179
+ end
180
+ end
181
+ ```
182
+
96
183
  ## Use cases
97
184
 
98
185
  Sidekiq::Rescue is useful when you want to retry jobs that failed due to expected errors and not spam your exception tracker with these errors. For example, you may want to retry a job that failed due to a network error or a temporary outage of a third party service, rather than a bug in your code.
@@ -162,7 +249,7 @@ end
162
249
  ## Motivation
163
250
 
164
251
  Sidekiq provides a retry mechanism for jobs that failed due to unexpected errors. However, it does not provide a way to retry jobs that failed due to expected errors. This gem aims to fill this gap.
165
- In addition, it provides a way to configure the number of retries and the delay between retries independently from the Sidekiq standard retry mechanism.
252
+ In addition, it provides a way to configure the number of retries and the delay between retries independently from the Sidekiq standard retry mechanism. Mostly inspired by [ActiveJob](https://edgeapi.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html#method-i-retry_on)
166
253
 
167
254
  ## Supported Ruby versions
168
255
 
@@ -24,16 +24,15 @@ module Sidekiq
24
24
  # @raise [ArgumentError] if limit is not an Integer
25
25
  # @example
26
26
  # sidekiq_rescue NetworkError, delay: 60, limit: 10
27
- def sidekiq_rescue(*error, delay: nil, limit: nil)
28
- error = validate_and_unpack_error_argument(error)
27
+ def sidekiq_rescue(*errors, delay: Sidekiq::Rescue.config.delay, limit: Sidekiq::Rescue.config.limit)
28
+ unpacked_errors = validate_and_unpack_error_argument(errors)
29
29
  validate_delay_argument(delay)
30
30
  validate_limit_argument(limit)
31
+ assign_sidekiq_rescue_options(unpacked_errors, delay, limit)
32
+ end
31
33
 
32
- self.sidekiq_rescue_options = {
33
- error: error,
34
- delay: delay || Sidekiq::Rescue.config.delay,
35
- limit: limit || Sidekiq::Rescue.config.limit
36
- }
34
+ def sidekiq_rescue_options_for(error)
35
+ sidekiq_rescue_options&.find { |k, _v| k.include?(error) }&.last
37
36
  end
38
37
 
39
38
  private
@@ -63,6 +62,11 @@ module Sidekiq
63
62
  def validate_limit_argument(limit)
64
63
  raise ArgumentError, "limit must be integer" if limit && !limit.is_a?(Integer)
65
64
  end
65
+
66
+ def assign_sidekiq_rescue_options(errors, delay, limit)
67
+ self.sidekiq_rescue_options ||= {}
68
+ self.sidekiq_rescue_options.merge!(errors => { delay: delay, limit: limit })
69
+ end
66
70
  end
67
71
  end
68
72
  # Alias for Dsl; TODO: remove in 1.0.0
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless defined?(RSpec)
4
+
5
+ require "rspec/matchers"
6
+
7
+ module Sidekiq
8
+ module Rescue
9
+ module RSpec
10
+ # RSpec matchers for Sidekiq::Rescue
11
+ module Matchers
12
+ ::RSpec::Matchers.define :have_sidekiq_rescue do |expected| # rubocop:disable Metrics/BlockLength
13
+ description { "be rescueable with #{expected}" }
14
+ failure_message do |actual|
15
+ str = "expected #{actual} to be rescueable with #{expected}"
16
+ str += " and delay #{@delay}" if @delay
17
+ str += " and limit #{@limit}" if @limit
18
+ str
19
+ end
20
+ failure_message_when_negated { |actual| "expected #{actual} not to be rescueable with #{expected}" }
21
+
22
+ chain :with_delay do |delay|
23
+ @delay = delay
24
+ end
25
+
26
+ chain :with_limit do |limit|
27
+ @limit = limit
28
+ end
29
+
30
+ match do |actual|
31
+ matched = actual.is_a?(Class) &&
32
+ actual.include?(Sidekiq::Rescue::Dsl) &&
33
+ actual.respond_to?(:sidekiq_rescue_options) &&
34
+ actual&.sidekiq_rescue_options&.keys&.flatten&.include?(expected)
35
+
36
+ return false unless matched
37
+
38
+ options = actual.sidekiq_rescue_options_for(expected)
39
+
40
+ (@delay.nil? || options.fetch(:delay) == @delay) &&
41
+ (@limit.nil? || options.fetch(:limit) == @limit)
42
+ end
43
+
44
+ match_when_negated do |actual|
45
+ raise NotImplementedError, "it's confusing to use `not_to be_rescueable` with `with_delay`" if @delay
46
+ raise NotImplementedError, "it's confusing to use `not_to be_rescueable` with `with_limit`" if @limit
47
+
48
+ actual.is_a?(Class) &&
49
+ actual.include?(Sidekiq::Rescue::Dsl) &&
50
+ actual.respond_to?(:sidekiq_rescue_options) &&
51
+ !Array(actual&.sidekiq_rescue_options&.[](:error)).include?(expected)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -11,9 +11,8 @@ module Sidekiq
11
11
 
12
12
  def call(job_instance, job_payload, _queue, &block)
13
13
  job_class = job_instance.class
14
- options = job_class.sidekiq_rescue_options if job_class.respond_to?(:sidekiq_rescue_options)
15
- if options
16
- sidekiq_rescue(job_payload, **options, &block)
14
+ if job_class.respond_to?(:sidekiq_rescue_options) && !job_class.sidekiq_rescue_options.nil?
15
+ sidekiq_rescue(job_payload, job_class, &block)
17
16
  else
18
17
  yield
19
18
  end
@@ -21,19 +20,30 @@ module Sidekiq
21
20
 
22
21
  private
23
22
 
24
- def sidekiq_rescue(job_payload, delay:, limit:, error:, **)
23
+ def sidekiq_rescue(job_payload, job_class)
25
24
  yield
26
- rescue *error => e
27
- rescue_counter = increment_rescue_counter(job_payload)
28
- raise e if rescue_counter > limit
25
+ rescue StandardError => e
26
+ error_group, options = job_class.sidekiq_rescue_options.reverse_each.find do |error_group, _options|
27
+ Array(error_group).any? { |error| e.is_a?(error) }
28
+ end
29
+ raise e unless error_group
30
+
31
+ rescue_error(e, error_group, options, job_payload)
32
+ end
33
+
34
+ def rescue_error(error, error_group, options, job_payload)
35
+ delay, limit = options.fetch_values(:delay, :limit)
36
+ rescue_counter = increment_rescue_counter_for(error_group, job_payload)
37
+ raise error if rescue_counter > limit
29
38
 
30
39
  reschedule_at = calculate_reschedule_time(delay, rescue_counter)
31
- log_reschedule_info(rescue_counter, e, reschedule_at)
32
- reschedule_job(job_payload, reschedule_at, rescue_counter)
40
+ log_reschedule_info(rescue_counter, error, reschedule_at)
41
+ reschedule_job(job_payload: job_payload, reschedule_at: reschedule_at, rescue_counter: rescue_counter,
42
+ error_group: error_group)
33
43
  end
34
44
 
35
- def increment_rescue_counter(job_payload)
36
- rescue_counter = job_payload["sidekiq_rescue_counter"].to_i
45
+ def increment_rescue_counter_for(error_group, job_payload)
46
+ rescue_counter = job_payload.dig("sidekiq_rescue_exceptions_counter", error_group.to_s) || 0
37
47
  rescue_counter += 1
38
48
  rescue_counter
39
49
  end
@@ -52,8 +62,10 @@ module Sidekiq
52
62
  "#{error.message}; rescheduling at #{reschedule_at}")
53
63
  end
54
64
 
55
- def reschedule_job(job_payload, reschedule_at, rescue_counter)
56
- Sidekiq::Client.push(job_payload.merge("at" => reschedule_at, "sidekiq_rescue_counter" => rescue_counter))
65
+ def reschedule_job(job_payload:, reschedule_at:, rescue_counter:, error_group:)
66
+ payload = job_payload.merge("at" => reschedule_at,
67
+ "sidekiq_rescue_exceptions_counter" => { error_group.to_s => rescue_counter })
68
+ Sidekiq::Client.push(payload)
57
69
  end
58
70
  end
59
71
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Rescue
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -7,3 +7,4 @@ require_relative "sidekiq/rescue"
7
7
  require_relative "sidekiq/rescue/version"
8
8
  require_relative "sidekiq/rescue/dsl"
9
9
  require_relative "sidekiq/rescue/server_middleware"
10
+ require_relative "sidekiq/rescue/rspec/matchers"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-rescue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitrii Ivliev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-03 00:00:00.000000000 Z
11
+ date: 2024-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq
@@ -39,6 +39,7 @@ files:
39
39
  - lib/sidekiq/rescue.rb
40
40
  - lib/sidekiq/rescue/config.rb
41
41
  - lib/sidekiq/rescue/dsl.rb
42
+ - lib/sidekiq/rescue/rspec/matchers.rb
42
43
  - lib/sidekiq/rescue/server_middleware.rb
43
44
  - lib/sidekiq/rescue/version.rb
44
45
  - lib/sidekiq_rescue.rb
@@ -49,7 +50,7 @@ metadata:
49
50
  homepage_uri: https://github.com/moofkit/sidekiq-rescue
50
51
  source_code_uri: https://github.com/moofkit/sidekiq-rescue
51
52
  changelog_uri: https://github.com/moofkit/sidekiq-rescue/blob/master/CHANGELOG.md
52
- documentation_uri: https://rubydoc.info/gems/sidekiq-rescue/0.2.0
53
+ documentation_uri: https://rubydoc.info/gems/sidekiq-rescue/0.3.0
53
54
  rubygems_mfa_required: 'true'
54
55
  post_install_message:
55
56
  rdoc_options: []
@@ -66,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
67
  - !ruby/object:Gem::Version
67
68
  version: '0'
68
69
  requirements: []
69
- rubygems_version: 3.4.10
70
+ rubygems_version: 3.5.9
70
71
  signing_key:
71
72
  specification_version: 4
72
73
  summary: Rescue Sidekiq jobs on expected error and reschedule them