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 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: []