rescue_like_a_pro 1.0.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 +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +128 -0
- data/Rakefile +12 -0
- data/lib/rescue_like_a_pro/active_job.rb +116 -0
- data/lib/rescue_like_a_pro/railtie.rb +9 -0
- data/lib/rescue_like_a_pro/version.rb +3 -0
- data/lib/rescue_like_a_pro.rb +5 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 381039cc44e8aabc625166b7d946c9f43af4ecc5118145ee8fe0d21d3adc1ba5
|
4
|
+
data.tar.gz: f05a0235ccf2228cd9eb9aad18352a67c35f02bba482ffb28fbc6857204f56a5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b00b75eee64fe164bf675642e7ad9c2d48812c1b2dcdcf13215ff0bb35f1de5466f50f9287a7dee81b0fbdd0fc62acc3dbd3eee00a663e7b2712c440d6708e80
|
7
|
+
data.tar.gz: 2b06313d25b86045a64eeac6a0a459d8f2ef84bfd05c565367565be45b524b5ead85f4b5d7d0ecc71ca22e19aa00efc1740caa3536f68dc4c7fbcaef0803481c
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022 thomas morgan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
# RescueLikeAPro
|
2
|
+
|
3
|
+
RescueLikeAPro rethinks ActiveJob's exception handling system by:
|
4
|
+
* Improving usage with class inheritance and mixins
|
5
|
+
* Adding fallback retries exhausted and discard handlers
|
6
|
+
* Making jitter computation more flexible
|
7
|
+
|
8
|
+
|
9
|
+
### Primary differences from standard ActiveJob
|
10
|
+
|
11
|
+
* Exceptions are always matched in order of most-specific to least-specific, regardless of the order of definition.
|
12
|
+
|
13
|
+
This allows for a more natural inheritance mechanism. It also eliminates ordering issues when using mixins.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
class ApplicationJob < ActiveJob::Base
|
17
|
+
discard_on ActiveJob::DeserializationError
|
18
|
+
end
|
19
|
+
class SomeJob < ApplicationJob
|
20
|
+
retry_on StandardError, attempts: 5
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
With ActiveJob's default exception handling, `DeserializationError`s will never be discarded by `SomeJob` because exceptions are processed from last to first. Since `DeserializationError` is a type of `StandardError`, `retry_on` will see it, reattempt 5 times, then trigger retries-exhausted--which in this case, without a block on `retry_on`, will bubble the error upward.
|
25
|
+
|
26
|
+
In contrast, RescueLikeAPro will recognize that `DeserializationError` is a more specific type of `StandardError` and will discard it immediately, while still retrying all other types of
|
27
|
+
`StandardError`s.
|
28
|
+
|
29
|
+
Child classes may, of course, still redefine handling for an exception previously defined in a parent.
|
30
|
+
|
31
|
+
When redefining an exception, the new rules (:attempts, :wait, etc) fully replace the previous ones. They are not merged.
|
32
|
+
|
33
|
+
* As a byproduct of the above, when two or more exceptions are defined together, retry attempts are counted per-exception class, and not in combination.
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
retry_on FirstError, SecondError, attempts: 5
|
37
|
+
```
|
38
|
+
|
39
|
+
Here, ActiveJob natively allows for 5 combined `FirstError` and `SecondError`s. In contrast, RescueLikeAPro will allow for 5 of each.
|
40
|
+
|
41
|
+
There is no behavior change when defining only a single exception per `retry_on`.
|
42
|
+
|
43
|
+
* Default handlers for retries-exhausted and discard are added at the job-class level. These are used as defaults when individual retry_on and discard_on calls don't specify their own block. These are properly inheritable from parent to child job classes.
|
44
|
+
|
45
|
+
* Specifying `retry_on(jitter: nil)` uses the default `retry_jitter` instead of becoming `0.0`. `jitter: 0` still works as expected.
|
46
|
+
|
47
|
+
For values < 1.0, ActiveJob's default behavior of adding a multiple of extra time remains the same. For example, Rails 6.1+'s default value of 0.15 adds between 0-15% extra time to the calculated `:wait` time.
|
48
|
+
|
49
|
+
RescueLikeAPro also recognizes ranges, which are treated as a seconds to be added to `:wait` (or subtracted if negative). Scalar values >= 1 are treated like the range `0..n`.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
self.retry_jitter = 0.05 # Adds 0-5% of jitter
|
53
|
+
self.retry_jitter = 5..10 # Adds 5 to 10 seconds of jitter
|
54
|
+
self.retry_jitter = -5..5 # Adds -5 to -5 seconds of jitter
|
55
|
+
self.retry_jitter = 30 # Adds 0 to 30 seconds of jitter
|
56
|
+
self.retry_jitter = 1.hour # Adds 0 to 3600 seconds of jitter
|
57
|
+
```
|
58
|
+
|
59
|
+
* Jitter is applied to all retries. In contrast, ActiveJob skips jitter when `:wait` is a Proc.
|
60
|
+
|
61
|
+
|
62
|
+
### Example syntax
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
class SomeJob < ApplicationJob
|
66
|
+
discard_on ActiveJob::DeserializationError
|
67
|
+
discard_on ApiError do |job, error|
|
68
|
+
# Called when job is discarded
|
69
|
+
end
|
70
|
+
|
71
|
+
retry_on SomeError, attempts: 5
|
72
|
+
retry_on AnotherError do |job, error|
|
73
|
+
# Called when retries are exhausted
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class ApplicationJob
|
78
|
+
# All of these work on individual job classes as well. Job class definitions take precedence over parent classes like here.
|
79
|
+
|
80
|
+
self.retry_jitter = 0.15 # Rails default: add 0-15% extra
|
81
|
+
self.retry_jitter = 7.seconds # Add 0-7 seconds extra
|
82
|
+
|
83
|
+
on_discard do |job, error|
|
84
|
+
# Add a default handler when a job is discarded. Only used when discard_on did not define a handler.
|
85
|
+
end
|
86
|
+
|
87
|
+
on_retries_exhausted do |job, error|
|
88
|
+
# Add a default handler when retries are exhausted. Only used when retry_on did not define a handler.
|
89
|
+
end
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
|
94
|
+
## Usage
|
95
|
+
|
96
|
+
With Rails, RescueLikeAPro automatically initializes itself. Simply add it to your Gemfile.
|
97
|
+
|
98
|
+
If you want to modify behavior of every job, *including* Rails' built-in jobs, add an initializer:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class ActiveJob::Base
|
102
|
+
# self.retry_jitter = 7.seconds
|
103
|
+
# on_discard{ ... }
|
104
|
+
# on_retries_exhausted{ ... }
|
105
|
+
# etc
|
106
|
+
```
|
107
|
+
|
108
|
+
Otherwise, to just modify all of your app's jobs, add instructions to `app/jobs/application_job.rb`.
|
109
|
+
|
110
|
+
And of course, add any per-Job instructions directly to that job class.
|
111
|
+
|
112
|
+
|
113
|
+
## Installation
|
114
|
+
|
115
|
+
As usual, add RescueLikeAPro to your Gemfile:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
gem "rescue_like_a_pro"
|
119
|
+
```
|
120
|
+
|
121
|
+
|
122
|
+
## Contributing
|
123
|
+
|
124
|
+
Pull requests welcomed. If unsure whether a proposed addition is in scope, feel free to open an Issue for discussion (not required though).
|
125
|
+
|
126
|
+
|
127
|
+
## License
|
128
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
module RescueLikeAPro::ActiveJob
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
|
5
|
+
prepended do
|
6
|
+
rescue_from Exception, with: :rescue_like_a_pro
|
7
|
+
|
8
|
+
class_attribute :retries_exhausted_handler, instance_writer: false, instance_predicate: false
|
9
|
+
class_attribute :discard_handler, instance_writer: false, instance_predicate: false
|
10
|
+
class_attribute :rescue_pro_rules, instance_writer: false, instance_predicate: false, default: {}
|
11
|
+
# {'Module::SomeException' => {..rules..}, ...}
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil, jitter: nil, &block)
|
17
|
+
exception_map = exceptions.each_with_object({}) do |exception, h|
|
18
|
+
h[exception.to_s] = { action: :retry_on, wait: wait, attempts: attempts, queue: queue, priority: priority, jitter: jitter, handler: block }
|
19
|
+
end
|
20
|
+
self.rescue_pro_rules = rescue_pro_rules.merge exception_map
|
21
|
+
end
|
22
|
+
|
23
|
+
def discard_on(*exceptions, &block)
|
24
|
+
exception_map = exceptions.each_with_object({}) do |exception, h|
|
25
|
+
h[exception.to_s] = { action: :discard_on, handler: block }
|
26
|
+
end
|
27
|
+
self.rescue_pro_rules = rescue_pro_rules.merge exception_map
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_retries_exhausted(&block)
|
31
|
+
self.retries_exhausted_handler = block
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_discard(&block)
|
35
|
+
self.discard_handler = block
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def rescue_like_a_pro(error)
|
44
|
+
rules = lookup_handler(error)
|
45
|
+
executions = executions_for([error])
|
46
|
+
case rules[:action]
|
47
|
+
when :retry_on
|
48
|
+
jitter = rules[:jitter] || self.class.retry_jitter
|
49
|
+
if rules[:attempts] == :unlimited || executions < rules[:attempts]
|
50
|
+
retry_job(
|
51
|
+
wait: determine_delay(seconds_or_duration_or_algorithm: rules[:wait], executions: executions, jitter: jitter),
|
52
|
+
queue: rules[:queue],
|
53
|
+
priority: rules[:priority],
|
54
|
+
error: error
|
55
|
+
)
|
56
|
+
return
|
57
|
+
else
|
58
|
+
handler = rules[:handler] || retries_exhausted_handler
|
59
|
+
inst_key = :retry_stopped
|
60
|
+
end
|
61
|
+
when :discard_on
|
62
|
+
handler = rules[:handler] || discard_handler || proc{}
|
63
|
+
inst_key = :discard
|
64
|
+
end
|
65
|
+
if handler
|
66
|
+
instrument inst_key, error: error do
|
67
|
+
handler.call(*[self, error].take(handler.arity))
|
68
|
+
end
|
69
|
+
else
|
70
|
+
instrument inst_key, error: error
|
71
|
+
raise error
|
72
|
+
end
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def lookup_handler(exception)
|
77
|
+
ex = exception.class
|
78
|
+
while ex
|
79
|
+
if r = rescue_pro_rules[ex.to_s]
|
80
|
+
return r
|
81
|
+
end
|
82
|
+
ex = ex.superclass
|
83
|
+
end
|
84
|
+
raise exception
|
85
|
+
end
|
86
|
+
|
87
|
+
def determine_delay(seconds_or_duration_or_algorithm:, executions:, jitter: nil)
|
88
|
+
case seconds_or_duration_or_algorithm
|
89
|
+
when :exponentially_longer
|
90
|
+
delay = executions**4
|
91
|
+
delay_jitter = determine_jitter_for_delay(delay, jitter)
|
92
|
+
delay + delay_jitter + 2
|
93
|
+
when ActiveSupport::Duration, Integer
|
94
|
+
delay = seconds_or_duration_or_algorithm.to_i
|
95
|
+
delay_jitter = determine_jitter_for_delay(delay, jitter)
|
96
|
+
delay + delay_jitter
|
97
|
+
when Proc
|
98
|
+
algorithm = seconds_or_duration_or_algorithm
|
99
|
+
delay = algorithm.call(executions)
|
100
|
+
delay_jitter = determine_jitter_for_delay(delay, jitter)
|
101
|
+
delay + delay_jitter
|
102
|
+
else
|
103
|
+
raise "Couldn't determine a delay based on #{seconds_or_duration_or_algorithm.inspect}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def determine_jitter_for_delay(delay, jitter)
|
108
|
+
if jitter.is_a?(Range) || jitter >= 1.0
|
109
|
+
Kernel.rand jitter
|
110
|
+
else
|
111
|
+
return 0.0 if jitter.zero?
|
112
|
+
Kernel.rand * delay * jitter
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rescue_like_a_pro
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- thomas morgan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-03-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activejob
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 7.0.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 7.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest-reporters
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: RescueLikeAPro rethinks ActiveJob's exception handling system to improve
|
70
|
+
usage with class inheritance and mixins, add fallback retries exhausted and discard
|
71
|
+
handlers, and improve jitter flexibility.
|
72
|
+
email:
|
73
|
+
- tm@iprog.com
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- LICENSE.txt
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- lib/rescue_like_a_pro.rb
|
82
|
+
- lib/rescue_like_a_pro/active_job.rb
|
83
|
+
- lib/rescue_like_a_pro/railtie.rb
|
84
|
+
- lib/rescue_like_a_pro/version.rb
|
85
|
+
homepage: https://github.com/zarqman/rescue_like_a_pro
|
86
|
+
licenses:
|
87
|
+
- MIT
|
88
|
+
metadata:
|
89
|
+
homepage_uri: https://github.com/zarqman/rescue_like_a_pro
|
90
|
+
source_code_uri: https://github.com/zarqman/rescue_like_a_pro
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
requirements: []
|
106
|
+
rubygems_version: 3.2.22
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Improve ActiveJob exception handling with inheritance, fallback handlers,
|
110
|
+
more jitter options, etc.
|
111
|
+
test_files: []
|