sidekiq-rescue 0.1.0 → 0.2.1

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: 8f87fcb8d7e00cd287d289735c0655237d16520c298e9c96b12bbd19f745848d
4
- data.tar.gz: 2be3ba005f0379b6f124974b0e464c0ac197f174952d2bb50c4be9ec4f6f979b
3
+ metadata.gz: d1a8899e388b21924f60017c1eb3b724bc129bec25c96f4765a37ab1f1d8e7fb
4
+ data.tar.gz: 41c934da18716cf917af164e92ce1bccdcab1a5bbce08df1c4cbf6550d5cfe70
5
5
  SHA512:
6
- metadata.gz: 0757e9ea80e869a0eb819e4147d2c65b1bb8e6763af382ae2cf5a8910172d93fafeeb96e1d9cc51af18619e26f7dae0e13b43b2b59cdc07d41ea2330b644bbce
7
- data.tar.gz: 3ab9dcffcbe32a7d26b89791b0c76b1b59d46132afd779ace5d02554f95e2bd418b33ecffb05c89ced58554a25e287d6c5a6a8db13988227c65ab47bbedbd2dd
6
+ metadata.gz: a644feb9b9c4d3e1eef97d24738b1157c7efc370102d3db19db3df23f2133382878a264818b5a9a99059e1f64e9b60cbe8e4ea8b297757a760e00fb1df71865b
7
+ data.tar.gz: 678d83f42a1ffff8dd71def36911fcdc42b7cf2186e053e47b02c9d4237ea51efc2bd10ed003c7909ec830006b3e725b5e64253faca3c0d4555d97a8536be114
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.1] - 2024-02-27
4
+
5
+ - Fix readme with correct middleware name
6
+ - Add RSpec matchers
7
+
8
+ ## [0.2.0] - 2024-02-03
9
+
10
+ - Rename `Sidekiq::Rescue::DSL` to `Sidekiq::Rescue::Dsl`
11
+ - Update the `delay` option to now accept a proc as an argument
12
+ - Update dsl to accept a list of errors
13
+
3
14
  ## [0.1.0] - 2024-01-20
4
15
 
5
16
  - Initial release
@@ -9,5 +20,7 @@
9
20
  - Add documentation
10
21
  - Add CI
11
22
 
12
- [Unreleased]: https://github.com/moofkit/sidekiq-rescue/compare/v0.1.0...HEAD
23
+ [Unreleased]: https://github.com/moofkit/sidekiq-rescue/compare/v0.2.1...HEAD
24
+ [0.2.1]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.2.1
25
+ [0.2.0]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.2.0
13
26
  [0.1.0]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.1.0
data/README.md CHANGED
@@ -27,7 +27,7 @@ Or install it yourself as:
27
27
  ```ruby
28
28
  Sidekiq.configure_server do |config|
29
29
  config.server_middleware do |chain|
30
- chain.add Sidekiq::Rescue::Middleware
30
+ chain.add Sidekiq::Rescue::ServerMiddleware
31
31
  end
32
32
  end
33
33
  ```
@@ -37,7 +37,7 @@ end
37
37
  ```ruby
38
38
  class MyJob
39
39
  include Sidekiq::Job
40
- include Sidekiq::Rescue::DSL
40
+ include Sidekiq::Rescue::Dsl
41
41
 
42
42
  sidekiq_rescue ExpectedError
43
43
 
@@ -54,7 +54,7 @@ You can configure the number of retries and the delay (in seconds) between retri
54
54
  ```ruby
55
55
  class MyJob
56
56
  include Sidekiq::Job
57
- include Sidekiq::Rescue::DSL
57
+ include Sidekiq::Rescue::Dsl
58
58
 
59
59
  sidekiq_rescue ExpectedError, delay: 60, limit: 5
60
60
 
@@ -79,10 +79,148 @@ Sidekiq::Rescue.configure do |config|
79
79
  end
80
80
  ```
81
81
 
82
+ You can also configure a job to have the delay to be a proc:
83
+
84
+ ```ruby
85
+ sidekiq_rescue ExpectedError, delay: ->(counter) { counter * 60 }
86
+ ```
87
+
88
+ or globally:
89
+
90
+ ```ruby
91
+ Sidekiq::Rescue.configure do |config|
92
+ config.delay = ->(counter) { counter * 60 }
93
+ end
94
+ ```
95
+
96
+ ### Testing
97
+
98
+ 1. Unit tests (recommended)
99
+
100
+ In case you want to test the rescue configuration, this gem provides RSpec matchers:
101
+
102
+ ```ruby
103
+ RSpec.cofigure do |config|
104
+ config.include Sidekiq::Rescue::RSpec::Matchers, type: :job
105
+ end
106
+
107
+ RSpec.describe MyJob do
108
+ it "rescues from expected errors" do
109
+ expect(MyJob).to have_sidekiq_rescue(ExpectedError)
110
+ end
111
+ end
112
+ ```
113
+
114
+ It also provides a way to test the delay and limit:
115
+
116
+ ```ruby
117
+ RSpec.describe MyJob do
118
+ it "rescues from expected errors with custom delay and limit" do
119
+ expect(MyJob).to have_sidekiq_rescue(ExpectedError).with_delay(60).with_limit(5)
120
+ end
121
+ end
122
+ ```
123
+
124
+ 2. Integration tests with `Sidekiq::Testing`
125
+ Firstly, you need to configure `Sidekiq::Testing` to use `Sidekiq::Rescue::ServerMiddleware` middleware:
126
+
127
+ ```ruby
128
+ # spec/spec_helper.rb or spec/rails_helper.rb
129
+ require "sidekiq/testing"
130
+
131
+ RSpec.configure do |config|
132
+ config.before(:all) do
133
+ Sidekiq::Testing.fake!
134
+
135
+ Sidekiq::Testing.server_middleware do |chain|
136
+ chain.add Sidekiq::Rescue::ServerMiddleware
137
+ end
138
+ end
139
+ end
140
+ ```
141
+
142
+ And test the job with the next snippet
143
+
144
+ ```ruby
145
+ # spec/jobs/my_job_spec.rb
146
+ RSpec.describe MyJob do
147
+ before do
148
+ allow(ApiClient).to receive(:new).and_raise(ApiClient::SomethingWentWrongError)
149
+ end
150
+
151
+ it "retries job if it fails with ExpectedError" do
152
+ MyJob.perform_async('test')
153
+ expect { MyJob.perform_one }.not_to raise_error # pefrom_one is a method from Sidekiq::Testing that runs the job once
154
+ end
155
+ end
156
+ ```
157
+
82
158
  ## Use cases
83
159
 
84
160
  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.
85
161
 
162
+ ## Examples
163
+
164
+ ### Retry a job that may failed due to a network error
165
+
166
+ ```ruby
167
+ class MyJob
168
+ include Sidekiq::Job
169
+ include Sidekiq::Rescue::Dsl
170
+
171
+ sidekiq_rescue Faraday::ConnectionFailed
172
+
173
+ def perform(*)
174
+ # ...
175
+ end
176
+ end
177
+ ```
178
+
179
+ ### Retry a job that may failed due to different errors
180
+
181
+ ```ruby
182
+ class MyJob
183
+ include Sidekiq::Job
184
+ include Sidekiq::Rescue::Dsl
185
+
186
+ sidekiq_rescue Faraday::ConnectionFailed, Faraday::TimeoutError
187
+
188
+ def perform(*)
189
+ # ...
190
+ end
191
+ end
192
+ ```
193
+
194
+ ### Retry a job that may failed due to different errors with custom delay
195
+
196
+ ```ruby
197
+ class MyJob
198
+ include Sidekiq::Job
199
+ include Sidekiq::Rescue::Dsl
200
+
201
+ sidekiq_rescue Faraday::ConnectionFailed, Faraday::TimeoutError, delay: 60
202
+
203
+ def perform(*)
204
+ # ...
205
+ end
206
+ end
207
+ ```
208
+
209
+ ### Retry a job that may failed due to different errors with custom delays and limits
210
+
211
+ ```ruby
212
+ class MyJob
213
+ include Sidekiq::Job
214
+ include Sidekiq::Rescue::Dsl
215
+
216
+ sidekiq_rescue Faraday::ConnectionFailed, Faraday::TimeoutError, delay: 60, limit: 5
217
+
218
+ def perform(*)
219
+ # ...
220
+ end
221
+ end
222
+ ```
223
+
86
224
  ## Motivation
87
225
 
88
226
  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.
@@ -24,9 +24,12 @@ module Sidekiq
24
24
  # @return [void]
25
25
  # @raise [ArgumentError] if delay is not an Integer or Float
26
26
  def delay=(delay)
27
- raise ArgumentError, "delay must be an Integer or Float" unless delay.is_a?(Integer) || delay.is_a?(Float)
28
-
29
- @delay = delay
27
+ case delay
28
+ when Integer, Float, Proc
29
+ @delay = delay
30
+ else
31
+ raise ArgumentError, "delay must be Integer, Float or Proc"
32
+ end
30
33
  end
31
34
 
32
35
  # The maximum number of retries.
@@ -2,20 +2,20 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Rescue
5
- # This module is included into the job class to provide the DSL for
5
+ # This module is included into the job class to provide the Dsl for
6
6
  # configuring rescue options.
7
- module DSL
7
+ module Dsl
8
8
  def self.included(base)
9
9
  base.extend(ClassMethods)
10
10
  base.sidekiq_class_attribute(:sidekiq_rescue_options)
11
11
  end
12
12
 
13
- # Module containing the DSL methods
13
+ # Module containing the Dsl methods
14
14
  module ClassMethods
15
15
  # Configure rescue options for the job.
16
16
  # @param error [StandardError] The error class to rescue.
17
17
  # @param error [Array<StandardError>] The error classes to rescue.
18
- # @param delay [Integer] The delay in seconds before retrying the job.
18
+ # @param delay [Integer, Float, Proc] The delay in seconds before retrying the job.
19
19
  # @param limit [Integer] The maximum number of retries.
20
20
  # @return [void]
21
21
  # @raise [ArgumentError] if error is not a StandardError
@@ -24,8 +24,8 @@ 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
- validate_error_argument(error)
27
+ def sidekiq_rescue(*error, delay: nil, limit: nil)
28
+ error = validate_and_unpack_error_argument(error)
29
29
  validate_delay_argument(delay)
30
30
  validate_limit_argument(limit)
31
31
 
@@ -38,23 +38,26 @@ module Sidekiq
38
38
 
39
39
  private
40
40
 
41
- def validate_error_argument(error)
42
- error_arg_valid = if error.is_a?(Array)
43
- error.all? { |e| e < StandardError }
44
- else
45
- error < StandardError
46
- end
47
- return if error_arg_valid
41
+ def validate_and_unpack_error_argument(error)
42
+ error_arg_valid = error.any? && error.flatten.all? { |e| e < StandardError } if error.is_a?(Array)
43
+ return error.flatten if error_arg_valid
48
44
 
49
45
  raise ArgumentError,
50
- "error must be an ancestor of StandardError or an array of ancestors of StandardError"
46
+ "error must be an ancestor of StandardError"
51
47
  end
52
48
 
53
49
  def validate_delay_argument(delay)
54
- return unless delay && !delay.is_a?(Integer) && !delay.is_a?(Float)
50
+ return if delay.nil?
51
+ return if delay.is_a?(Integer) || delay.is_a?(Float)
52
+
53
+ if delay.is_a?(Proc)
54
+ raise ArgumentError, "delay proc must accept counter as argument" if delay.arity.zero?
55
+
56
+ return
57
+ end
55
58
 
56
59
  raise ArgumentError,
57
- "delay must be integer or float"
60
+ "delay must be integer, float or proc"
58
61
  end
59
62
 
60
63
  def validate_limit_argument(limit)
@@ -62,5 +65,9 @@ module Sidekiq
62
65
  end
63
66
  end
64
67
  end
68
+ # Alias for Dsl; TODO: remove in 1.0.0
69
+ # @deprecated
70
+ # @see Dsl
71
+ DSL = Dsl
65
72
  end
66
73
  end
@@ -0,0 +1,50 @@
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
+ actual.is_a?(Class) &&
32
+ actual.include?(Sidekiq::Rescue::Dsl) &&
33
+ actual.sidekiq_rescue_options[:error].include?(expected) &&
34
+ (@delay.nil? || actual.sidekiq_rescue_options[:delay] == @delay) &&
35
+ (@limit.nil? || actual.sidekiq_rescue_options[:limit] == @limit)
36
+ end
37
+
38
+ match_when_negated do |actual|
39
+ raise NotImplementedError, "it's confusing to use `not_to be_rescueable` with `with_delay`" if @delay
40
+ raise NotImplementedError, "it's confusing to use `not_to be_rescueable` with `with_limit`" if @limit
41
+
42
+ actual.is_a?(Class) &&
43
+ actual.include?(Sidekiq::Rescue::Dsl) &&
44
+ !actual.sidekiq_rescue_options[:error].include?(expected)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -24,18 +24,35 @@ module Sidekiq
24
24
  def sidekiq_rescue(job_payload, delay:, limit:, error:, **)
25
25
  yield
26
26
  rescue *error => e
27
+ rescue_counter = increment_rescue_counter(job_payload)
28
+ raise e if rescue_counter > limit
29
+
30
+ 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)
33
+ end
34
+
35
+ def increment_rescue_counter(job_payload)
27
36
  rescue_counter = job_payload["sidekiq_rescue_counter"].to_i
28
37
  rescue_counter += 1
29
- raise e if rescue_counter > limit
38
+ rescue_counter
39
+ end
30
40
 
41
+ def calculate_reschedule_time(delay, rescue_counter)
31
42
  # NOTE: we use the retry counter to increase the jitter
32
43
  # so that the jobs don't retry at the same time
33
44
  # inspired by sidekiq https://github.com/sidekiq/sidekiq/blob/73c150d0430a8394cadb5cd49218895b113613a0/lib/sidekiq/job_retry.rb#L188
34
45
  jitter = rand(10) * rescue_counter
35
- reschedule_at = Time.now.to_f + delay + jitter
46
+ delay = delay.call(rescue_counter) if delay.is_a?(Proc)
47
+ Time.now.to_f + delay + jitter
48
+ end
49
+
50
+ def log_reschedule_info(rescue_counter, error, reschedule_at)
51
+ Sidekiq::Rescue.logger.info("[sidekiq_rescue] Job failed #{rescue_counter} times with error: " \
52
+ "#{error.message}; rescheduling at #{reschedule_at}")
53
+ end
36
54
 
37
- Sidekiq::Rescue.logger.info("[sidekiq_rescue] Job failed #{rescue_counter} times with error:" \
38
- "#{e.message}; rescheduling at #{reschedule_at}")
55
+ def reschedule_job(job_payload, reschedule_at, rescue_counter)
39
56
  Sidekiq::Client.push(job_payload.merge("at" => reschedule_at, "sidekiq_rescue_counter" => rescue_counter))
40
57
  end
41
58
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Rescue
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
@@ -4,13 +4,13 @@ module Sidekiq
4
4
  # Sidekiq::Rescue is a Sidekiq plugin which allows you to easily handle
5
5
  # exceptions thrown by your jobs.
6
6
  #
7
- # To use Sidekiq::Rescue, you need to include Sidekiq::Rescue::DSL module
7
+ # To use Sidekiq::Rescue, you need to include Sidekiq::Rescue::Dsl module
8
8
  # in your job class and use the sidekiq_rescue class method to define
9
9
  # exception handlers.
10
10
  #
11
11
  # class MyJob
12
12
  # include Sidekiq::Job
13
- # include Sidekiq::Rescue::DSL
13
+ # include Sidekiq::Rescue::Dsl
14
14
  #
15
15
  # sidekiq_rescue NetworkError, delay: 60, limit: 10
16
16
  #
@@ -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.1.0
4
+ version: 0.2.1
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-01-21 00:00:00.000000000 Z
11
+ date: 2024-02-27 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,6 +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
53
+ documentation_uri: https://rubydoc.info/gems/sidekiq-rescue/0.2.1
52
54
  rubygems_mfa_required: 'true'
53
55
  post_install_message:
54
56
  rdoc_options: []