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 +4 -4
- data/CHANGELOG.md +14 -1
- data/README.md +141 -3
- data/lib/sidekiq/rescue/config.rb +6 -3
- data/lib/sidekiq/rescue/dsl.rb +23 -16
- data/lib/sidekiq/rescue/rspec/matchers.rb +50 -0
- data/lib/sidekiq/rescue/server_middleware.rb +21 -4
- data/lib/sidekiq/rescue/version.rb +1 -1
- data/lib/sidekiq/rescue.rb +2 -2
- data/lib/sidekiq_rescue.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d1a8899e388b21924f60017c1eb3b724bc129bec25c96f4765a37ab1f1d8e7fb
|
4
|
+
data.tar.gz: 41c934da18716cf917af164e92ce1bccdcab1a5bbce08df1c4cbf6550d5cfe70
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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::
|
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::
|
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::
|
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
|
-
|
28
|
-
|
29
|
-
|
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.
|
data/lib/sidekiq/rescue/dsl.rb
CHANGED
@@ -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
|
5
|
+
# This module is included into the job class to provide the Dsl for
|
6
6
|
# configuring rescue options.
|
7
|
-
module
|
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
|
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
|
-
|
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
|
42
|
-
error_arg_valid = if error.is_a?(Array)
|
43
|
-
|
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
|
46
|
+
"error must be an ancestor of StandardError"
|
51
47
|
end
|
52
48
|
|
53
49
|
def validate_delay_argument(delay)
|
54
|
-
return
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/sidekiq/rescue.rb
CHANGED
@@ -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::
|
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::
|
13
|
+
# include Sidekiq::Rescue::Dsl
|
14
14
|
#
|
15
15
|
# sidekiq_rescue NetworkError, delay: 60, limit: 10
|
16
16
|
#
|
data/lib/sidekiq_rescue.rb
CHANGED
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
|
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-
|
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: []
|