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 +4 -4
- data/CHANGELOG.md +13 -1
- data/README.md +89 -2
- data/lib/sidekiq/rescue/dsl.rb +11 -7
- data/lib/sidekiq/rescue/rspec/matchers.rb +57 -0
- data/lib/sidekiq/rescue/server_middleware.rb +25 -13
- data/lib/sidekiq/rescue/version.rb +1 -1
- data/lib/sidekiq_rescue.rb +1 -0
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01671c36411fb052587442c08db979d740c563e6d74311ed1c4d17293b266745
|
4
|
+
data.tar.gz: 90de1649e943e5a422dc6d601d7b7425ee74958418ed9f89ef635c27aac1b0b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
[](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::
|
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
|
|
data/lib/sidekiq/rescue/dsl.rb
CHANGED
@@ -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(*
|
28
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
15
|
-
|
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,
|
23
|
+
def sidekiq_rescue(job_payload, job_class)
|
25
24
|
yield
|
26
|
-
rescue
|
27
|
-
|
28
|
-
|
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,
|
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
|
36
|
-
rescue_counter = job_payload
|
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
|
56
|
-
|
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
|
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.
|
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-
|
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.
|
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.
|
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
|