rescue_like_a_pro 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,12 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.pattern = "test/**/*_test.rb"
9
+ t.verbose = false
10
+ end
11
+
12
+ task default: :test
@@ -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
@@ -0,0 +1,9 @@
1
+ module RescueLikeAPro
2
+ class Railtie < ::Rails::Railtie
3
+
4
+ ActiveSupport.on_load(:active_job) do
5
+ prepend RescueLikeAPro::ActiveJob
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module RescueLikeAPro
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,5 @@
1
+ %w(active_job version).each do |f|
2
+ require "rescue_like_a_pro/#{f}"
3
+ end
4
+
5
+ require "rescue_like_a_pro/railtie" if defined?(::Rails)
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: []