activejob-retry 0.0.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 +7 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.travis.yml +15 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +197 -0
- data/LICENSE +22 -0
- data/README.md +47 -0
- data/Rakefile +75 -0
- data/activejob-retry.gemspec +38 -0
- data/lib/active_job-retry.rb +14 -0
- data/lib/active_job/retry.rb +130 -0
- data/lib/active_job/retry/deserialize_monkey_patch.rb +22 -0
- data/lib/active_job/retry/exponential_backoff.rb +92 -0
- data/lib/active_job/retry/exponential_options_validator.rb +57 -0
- data/lib/active_job/retry/invalid_configuration_error.rb +6 -0
- data/lib/active_job/retry/options_validator.rb +84 -0
- data/lib/active_job/retry/version.rb +5 -0
- data/spec/retry_spec.rb +228 -0
- data/spec/spec_helper.rb +23 -0
- metadata +158 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 9dd35a0525fd3f160a488f90b9471f7058a714de
|
|
4
|
+
data.tar.gz: 3e423dbca62f4337a765a54f7c212ccd3ef237eb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ac1daad64696deacbfeab3d8e28e1ffff49b7e219580de47f9f02f0867ae5f0977615c019d88f8f9d2afca3561df7b551fcb56a9b3d4ce2ccdea109e117c907a
|
|
7
|
+
data.tar.gz: 74b33077ae43b9232eace312851fc9a85165e72c8ae4b1b386f97d109daab75121909fb9a9b7bcec5c7b3e70673ca0d4ab5ed5a16448d7ab7d8769b0b95a7e50
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
|
File without changes
|
data/Gemfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
source 'https://rubygems.org'
|
|
2
|
+
gemspec
|
|
3
|
+
|
|
4
|
+
group :test do
|
|
5
|
+
gem 'resque', require: false
|
|
6
|
+
gem 'resque-scheduler', require: false
|
|
7
|
+
gem 'sidekiq', require: false
|
|
8
|
+
gem 'sucker_punch', require: false
|
|
9
|
+
gem 'delayed_job', require: false
|
|
10
|
+
gem 'queue_classic', "< 3.0.0", require: false, platforms: :ruby
|
|
11
|
+
gem 'sneakers', '0.1.1.pre', require: false
|
|
12
|
+
gem 'que', require: false
|
|
13
|
+
gem 'backburner', require: false
|
|
14
|
+
gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false
|
|
15
|
+
gem 'qu-redis', require: false
|
|
16
|
+
gem 'sequel', require: false
|
|
17
|
+
end
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
GIT
|
|
2
|
+
remote: git://github.com/bkeepers/qu.git
|
|
3
|
+
revision: d098e2657c92e89a6413bebd9c033930759c061f
|
|
4
|
+
branch: master
|
|
5
|
+
specs:
|
|
6
|
+
qu (0.2.0)
|
|
7
|
+
qu-rails (0.2.0)
|
|
8
|
+
qu (= 0.2.0)
|
|
9
|
+
railties (>= 3.2, < 5)
|
|
10
|
+
qu-redis (0.2.0)
|
|
11
|
+
qu (= 0.2.0)
|
|
12
|
+
redis-namespace
|
|
13
|
+
|
|
14
|
+
PATH
|
|
15
|
+
remote: .
|
|
16
|
+
specs:
|
|
17
|
+
activejob-retry (0.0.1)
|
|
18
|
+
activejob (>= 4.2)
|
|
19
|
+
activesupport (>= 4.2)
|
|
20
|
+
|
|
21
|
+
GEM
|
|
22
|
+
remote: https://rubygems.org/
|
|
23
|
+
specs:
|
|
24
|
+
actionpack (4.2.0)
|
|
25
|
+
actionview (= 4.2.0)
|
|
26
|
+
activesupport (= 4.2.0)
|
|
27
|
+
rack (~> 1.6.0)
|
|
28
|
+
rack-test (~> 0.6.2)
|
|
29
|
+
rails-dom-testing (~> 1.0, >= 1.0.5)
|
|
30
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.1)
|
|
31
|
+
actionview (4.2.0)
|
|
32
|
+
activesupport (= 4.2.0)
|
|
33
|
+
builder (~> 3.1)
|
|
34
|
+
erubis (~> 2.7.0)
|
|
35
|
+
rails-dom-testing (~> 1.0, >= 1.0.5)
|
|
36
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.1)
|
|
37
|
+
activejob (4.2.0)
|
|
38
|
+
activesupport (= 4.2.0)
|
|
39
|
+
globalid (>= 0.3.0)
|
|
40
|
+
activesupport (4.2.0)
|
|
41
|
+
i18n (~> 0.7)
|
|
42
|
+
json (~> 1.7, >= 1.7.7)
|
|
43
|
+
minitest (~> 5.1)
|
|
44
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
|
45
|
+
tzinfo (~> 1.1)
|
|
46
|
+
amq-protocol (1.9.2)
|
|
47
|
+
backburner (0.4.6)
|
|
48
|
+
beaneater (~> 0.3.1)
|
|
49
|
+
dante (~> 0.1.5)
|
|
50
|
+
beaneater (0.3.3)
|
|
51
|
+
builder (3.2.2)
|
|
52
|
+
bunny (1.1.9)
|
|
53
|
+
amq-protocol (>= 1.9.2)
|
|
54
|
+
byebug (3.5.1)
|
|
55
|
+
columnize (~> 0.8)
|
|
56
|
+
debugger-linecache (~> 1.2)
|
|
57
|
+
slop (~> 3.6)
|
|
58
|
+
celluloid (0.16.0)
|
|
59
|
+
timers (~> 4.0.0)
|
|
60
|
+
coderay (1.1.0)
|
|
61
|
+
columnize (0.9.0)
|
|
62
|
+
connection_pool (2.1.0)
|
|
63
|
+
dante (0.1.5)
|
|
64
|
+
debugger-linecache (1.2.0)
|
|
65
|
+
delayed_job (4.0.6)
|
|
66
|
+
activesupport (>= 3.0, < 5.0)
|
|
67
|
+
diff-lcs (1.2.5)
|
|
68
|
+
erubis (2.7.0)
|
|
69
|
+
globalid (0.3.0)
|
|
70
|
+
activesupport (>= 4.1.0)
|
|
71
|
+
hitimes (1.2.2)
|
|
72
|
+
i18n (0.7.0)
|
|
73
|
+
json (1.8.1)
|
|
74
|
+
loofah (2.0.1)
|
|
75
|
+
nokogiri (>= 1.5.9)
|
|
76
|
+
method_source (0.8.2)
|
|
77
|
+
mini_portile (0.6.2)
|
|
78
|
+
minitest (5.5.0)
|
|
79
|
+
mono_logger (1.1.0)
|
|
80
|
+
multi_json (1.10.1)
|
|
81
|
+
nokogiri (1.6.5)
|
|
82
|
+
mini_portile (~> 0.6.0)
|
|
83
|
+
pg (0.17.1)
|
|
84
|
+
pry (0.10.1)
|
|
85
|
+
coderay (~> 1.1.0)
|
|
86
|
+
method_source (~> 0.8.1)
|
|
87
|
+
slop (~> 3.4)
|
|
88
|
+
pry-byebug (2.0.0)
|
|
89
|
+
byebug (~> 3.4)
|
|
90
|
+
pry (~> 0.10)
|
|
91
|
+
que (0.9.0)
|
|
92
|
+
queue_classic (2.2.3)
|
|
93
|
+
pg (~> 0.17.0)
|
|
94
|
+
rack (1.6.0)
|
|
95
|
+
rack-protection (1.5.3)
|
|
96
|
+
rack
|
|
97
|
+
rack-test (0.6.2)
|
|
98
|
+
rack (>= 1.0)
|
|
99
|
+
rails-deprecated_sanitizer (1.0.3)
|
|
100
|
+
activesupport (>= 4.2.0.alpha)
|
|
101
|
+
rails-dom-testing (1.0.5)
|
|
102
|
+
activesupport (>= 4.2.0.beta, < 5.0)
|
|
103
|
+
nokogiri (~> 1.6.0)
|
|
104
|
+
rails-deprecated_sanitizer (>= 1.0.1)
|
|
105
|
+
rails-html-sanitizer (1.0.1)
|
|
106
|
+
loofah (~> 2.0)
|
|
107
|
+
railties (4.2.0)
|
|
108
|
+
actionpack (= 4.2.0)
|
|
109
|
+
activesupport (= 4.2.0)
|
|
110
|
+
rake (>= 0.8.7)
|
|
111
|
+
thor (>= 0.18.1, < 2.0)
|
|
112
|
+
rake (10.4.2)
|
|
113
|
+
redis (3.2.0)
|
|
114
|
+
redis-namespace (1.5.1)
|
|
115
|
+
redis (~> 3.0, >= 3.0.4)
|
|
116
|
+
resque (1.25.2)
|
|
117
|
+
mono_logger (~> 1.0)
|
|
118
|
+
multi_json (~> 1.0)
|
|
119
|
+
redis-namespace (~> 1.3)
|
|
120
|
+
sinatra (>= 0.9.2)
|
|
121
|
+
vegas (~> 0.1.2)
|
|
122
|
+
resque-scheduler (4.0.0)
|
|
123
|
+
mono_logger (~> 1.0)
|
|
124
|
+
redis (~> 3.0)
|
|
125
|
+
resque (~> 1.25)
|
|
126
|
+
rufus-scheduler (~> 3.0)
|
|
127
|
+
rspec (3.1.0)
|
|
128
|
+
rspec-core (~> 3.1.0)
|
|
129
|
+
rspec-expectations (~> 3.1.0)
|
|
130
|
+
rspec-mocks (~> 3.1.0)
|
|
131
|
+
rspec-core (3.1.7)
|
|
132
|
+
rspec-support (~> 3.1.0)
|
|
133
|
+
rspec-expectations (3.1.2)
|
|
134
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
135
|
+
rspec-support (~> 3.1.0)
|
|
136
|
+
rspec-its (1.1.0)
|
|
137
|
+
rspec-core (>= 3.0.0)
|
|
138
|
+
rspec-expectations (>= 3.0.0)
|
|
139
|
+
rspec-mocks (3.1.3)
|
|
140
|
+
rspec-support (~> 3.1.0)
|
|
141
|
+
rspec-support (3.1.2)
|
|
142
|
+
rufus-scheduler (3.0.9)
|
|
143
|
+
tzinfo
|
|
144
|
+
sequel (4.17.0)
|
|
145
|
+
serverengine (1.5.10)
|
|
146
|
+
sigdump (~> 0.2.2)
|
|
147
|
+
sidekiq (3.3.0)
|
|
148
|
+
celluloid (>= 0.16.0)
|
|
149
|
+
connection_pool (>= 2.0.0)
|
|
150
|
+
json
|
|
151
|
+
redis (>= 3.0.6)
|
|
152
|
+
redis-namespace (>= 1.3.1)
|
|
153
|
+
sigdump (0.2.2)
|
|
154
|
+
sinatra (1.4.5)
|
|
155
|
+
rack (~> 1.4)
|
|
156
|
+
rack-protection (~> 1.4)
|
|
157
|
+
tilt (~> 1.3, >= 1.3.4)
|
|
158
|
+
slop (3.6.0)
|
|
159
|
+
sneakers (0.1.1.pre)
|
|
160
|
+
bunny (~> 1.1.3)
|
|
161
|
+
serverengine
|
|
162
|
+
thor
|
|
163
|
+
thread
|
|
164
|
+
sucker_punch (1.3.2)
|
|
165
|
+
celluloid (~> 0.16.0)
|
|
166
|
+
thor (0.19.1)
|
|
167
|
+
thread (0.1.4)
|
|
168
|
+
thread_safe (0.3.4)
|
|
169
|
+
tilt (1.4.1)
|
|
170
|
+
timers (4.0.1)
|
|
171
|
+
hitimes
|
|
172
|
+
tzinfo (1.2.2)
|
|
173
|
+
thread_safe (~> 0.1)
|
|
174
|
+
vegas (0.1.11)
|
|
175
|
+
rack (>= 1.0.0)
|
|
176
|
+
|
|
177
|
+
PLATFORMS
|
|
178
|
+
ruby
|
|
179
|
+
|
|
180
|
+
DEPENDENCIES
|
|
181
|
+
activejob-retry!
|
|
182
|
+
backburner
|
|
183
|
+
delayed_job
|
|
184
|
+
pry-byebug
|
|
185
|
+
qu-rails!
|
|
186
|
+
qu-redis
|
|
187
|
+
que
|
|
188
|
+
queue_classic (< 3.0.0)
|
|
189
|
+
rake (>= 10.3)
|
|
190
|
+
resque
|
|
191
|
+
resque-scheduler
|
|
192
|
+
rspec
|
|
193
|
+
rspec-its
|
|
194
|
+
sequel
|
|
195
|
+
sidekiq
|
|
196
|
+
sneakers (= 0.1.1.pre)
|
|
197
|
+
sucker_punch
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2014 Isaac Seymour
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
Software), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
ActiveJob::Retry
|
|
2
|
+
================
|
|
3
|
+
|
|
4
|
+
A DSL for automatically retrying ActiveJobs, with exponential backoff.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
class ProcessWebhook < ActiveJob::Base
|
|
8
|
+
include ActiveJob::Retry
|
|
9
|
+
|
|
10
|
+
queue_as :webhooks
|
|
11
|
+
retry_with limit: 3, # Attempt three times and then raise (default: 1)
|
|
12
|
+
delay: 5, # Wait ~5 seconds between attempts (default: 0)
|
|
13
|
+
retry_exceptions: [TimeoutError] # Only retry when these errors are raised (default: all)
|
|
14
|
+
# Could alternatively use:
|
|
15
|
+
# fatal_exceptions: [StandardError] # Never catch these errors (default: none)
|
|
16
|
+
|
|
17
|
+
def perform(webhook)
|
|
18
|
+
webhook.process!
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
With exponential backoff:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
class ProcessWebhook < ActiveJob::Base
|
|
27
|
+
include ActiveJob::Retry::ExponentialBackoff
|
|
28
|
+
|
|
29
|
+
queue_as :webhooks
|
|
30
|
+
backoff_with strategy: [1, 5, 10, 30, 60] # Delay for 1, 5, ... seconds between subsequent retries
|
|
31
|
+
min_delay_multiplier: 0.8, # Multiply each delay by a random number between
|
|
32
|
+
max_delay_multiplier: 1.2 # 0.8 and 1.2 (rounded to nearest second)
|
|
33
|
+
|
|
34
|
+
def perform(webhook)
|
|
35
|
+
webhook.process!
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Contributing
|
|
41
|
+
------------
|
|
42
|
+
|
|
43
|
+
* Fork the project.
|
|
44
|
+
* Make your feature addition or bug fix.
|
|
45
|
+
* Add tests for it.
|
|
46
|
+
* Document it in the CHANGELOG.md.
|
|
47
|
+
* Open a PR.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'rake/testtask'
|
|
2
|
+
require 'rubygems/package_task'
|
|
3
|
+
|
|
4
|
+
ACTIVEJOB_ADAPTERS = %w(backburner delayed_job que resque sidekiq)
|
|
5
|
+
|
|
6
|
+
task default: :test
|
|
7
|
+
task test: 'test:default'
|
|
8
|
+
|
|
9
|
+
namespace :test do
|
|
10
|
+
desc 'Run all adapter tests'
|
|
11
|
+
task :default do
|
|
12
|
+
run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:#{a}" }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc 'Run all adapter tests in isolation'
|
|
16
|
+
task :isolated do
|
|
17
|
+
run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:isolated:#{a}" }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc 'Run integration tests for all adapters'
|
|
21
|
+
task :integration do
|
|
22
|
+
run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:integration:#{a}" }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
task 'env:integration' do
|
|
26
|
+
ENV['AJ_INTEGRATION_TESTS'] = "1"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
ACTIVEJOB_ADAPTERS.each do |adapter|
|
|
30
|
+
task("env:#{adapter}") { ENV['AJADAPTER'] = adapter }
|
|
31
|
+
|
|
32
|
+
Rake::TestTask.new(adapter => "test:env:#{adapter}") do |t|
|
|
33
|
+
t.description = "Run adapter tests for #{adapter}"
|
|
34
|
+
t.libs << 'test'
|
|
35
|
+
t.test_files = FileList['test/cases/**/*_test.rb']
|
|
36
|
+
t.verbose = true
|
|
37
|
+
t.warning = true
|
|
38
|
+
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
namespace :isolated do
|
|
42
|
+
task adapter => "test:env:#{adapter}" do
|
|
43
|
+
dir = File.dirname(__FILE__)
|
|
44
|
+
Dir.glob("#{dir}/test/cases/**/*_test.rb").all? do |file|
|
|
45
|
+
sh(Gem.ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file)
|
|
46
|
+
end or raise 'Failures'
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
namespace :integration do
|
|
51
|
+
Rake::TestTask.new(adapter => ["test:env:#{adapter}", 'test:env:integration']) do |t|
|
|
52
|
+
t.description = "Run integration tests for #{adapter}"
|
|
53
|
+
t.libs << 'test'
|
|
54
|
+
t.test_files = FileList['test/integration/**/*_test.rb']
|
|
55
|
+
t.verbose = true
|
|
56
|
+
t.warning = true
|
|
57
|
+
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def run_without_aborting(tasks)
|
|
64
|
+
errors = []
|
|
65
|
+
|
|
66
|
+
tasks.each do |task|
|
|
67
|
+
begin
|
|
68
|
+
Rake::Task[task].invoke
|
|
69
|
+
rescue Exception
|
|
70
|
+
errors << task
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
abort "Errors running #{errors.join(', ')}" if errors.any?
|
|
75
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'active_job/retry/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |s|
|
|
7
|
+
s.name = 'activejob-retry'
|
|
8
|
+
s.version = ActiveJob::Retry::VERSION
|
|
9
|
+
s.date = Date.today.strftime('%Y-%m-%d')
|
|
10
|
+
s.authors = ['Isaac Seymour']
|
|
11
|
+
s.email = ['isaac@isaacseymour.co.uk']
|
|
12
|
+
s.summary = 'Automatic retrying DSL for ActiveJob'
|
|
13
|
+
s.description = <<-EOL
|
|
14
|
+
activejob-retry provides a simple DSL for automatically retrying ActiveJobs when they
|
|
15
|
+
fail, with exponential backoff.
|
|
16
|
+
|
|
17
|
+
Features:
|
|
18
|
+
|
|
19
|
+
* (Should) work with any queue adapter that supports retries.
|
|
20
|
+
* Whitelist/blacklist exceptions to retry on.
|
|
21
|
+
* Exponential backoff (varying the delay between retries).
|
|
22
|
+
* Light and easy to override retry logic.
|
|
23
|
+
EOL
|
|
24
|
+
s.homepage = 'http://github.com/isaacseymour/activejob-retry'
|
|
25
|
+
s.license = 'MIT'
|
|
26
|
+
|
|
27
|
+
s.has_rdoc = false
|
|
28
|
+
s.files = `git ls-files`.split($/)
|
|
29
|
+
s.require_paths = %w[lib]
|
|
30
|
+
|
|
31
|
+
s.add_dependency('activejob', '>= 4.2')
|
|
32
|
+
s.add_dependency('activesupport', '>= 4.2')
|
|
33
|
+
|
|
34
|
+
s.add_development_dependency('rake', ' >= 10.3')
|
|
35
|
+
s.add_development_dependency('rspec')
|
|
36
|
+
s.add_development_dependency('rspec-its')
|
|
37
|
+
s.add_development_dependency('pry-byebug')
|
|
38
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'active_job'
|
|
2
|
+
require 'active_support'
|
|
3
|
+
|
|
4
|
+
require 'active_job/retry'
|
|
5
|
+
require 'active_job/retry/exponential_backoff'
|
|
6
|
+
require 'active_job/retry/invalid_configuration_error'
|
|
7
|
+
require 'active_job/retry/options_validator'
|
|
8
|
+
require 'active_job/retry/exponential_options_validator'
|
|
9
|
+
|
|
10
|
+
require 'active_job/retry/version'
|
|
11
|
+
|
|
12
|
+
unless ActiveJob::Base.method_defined?(:deserialize)
|
|
13
|
+
require 'active_job/retry/deserialize_monkey_patch'
|
|
14
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
module ActiveJob
|
|
2
|
+
module Retry
|
|
3
|
+
class UnsupportedAdapterError < StandardError; end
|
|
4
|
+
SUPPORTED_ADAPTERS = %i(backburner delayed_job que resque sidekiq).freeze
|
|
5
|
+
|
|
6
|
+
# If you want your job to retry on failure, simply include this module in your class,
|
|
7
|
+
#
|
|
8
|
+
# class DeliverWebHook < ActiveJob::Base
|
|
9
|
+
# include ActiveJob::Retry
|
|
10
|
+
# queue_as :web_hooks
|
|
11
|
+
#
|
|
12
|
+
# retry_with limit: 8, # default 1
|
|
13
|
+
# delay: 60, # default 0
|
|
14
|
+
# fatal_exceptions: [RuntimeError], # default [], i.e. none
|
|
15
|
+
# retry_exceptions: [TimeoutError] # default nil, i.e. all
|
|
16
|
+
#
|
|
17
|
+
# def perform(url, web_hook_id, hmac_key)
|
|
18
|
+
# work!
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
def self.included(base)
|
|
22
|
+
# This breaks all specs because the adapter gets set after class eval :(
|
|
23
|
+
# unless SUPPORTED_ADAPTERS.include?(ActiveJob::Base.queue_adapter)
|
|
24
|
+
# raise UnsupportedAdapterError,
|
|
25
|
+
# "Only Backburner, DelayedJob, Que, Resque, and Sidekiq support delayed " \
|
|
26
|
+
# "retries. #{ActiveJob::Base.queue_adapter} is not supported."
|
|
27
|
+
# end
|
|
28
|
+
|
|
29
|
+
base.extend(ClassMethods)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module ClassMethods
|
|
33
|
+
# Setup DSL
|
|
34
|
+
def retry_with(options)
|
|
35
|
+
OptionsValidator.new(options).validate!
|
|
36
|
+
|
|
37
|
+
@retry_limit = options[:limit] if options[:limit]
|
|
38
|
+
@retry_delay = options[:delay] if options[:delay]
|
|
39
|
+
@fatal_exceptions = options[:fatal_exceptions] if options[:fatal_exceptions]
|
|
40
|
+
@retry_exceptions = options[:retry_exceptions] if options[:retry_exceptions]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
############
|
|
44
|
+
# Defaults #
|
|
45
|
+
############
|
|
46
|
+
def retry_limit
|
|
47
|
+
@retry_limit ||= 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def retry_delay
|
|
51
|
+
@retry_delay ||= 0
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fatal_exceptions
|
|
55
|
+
@fatal_exceptions ||= []
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def retry_exceptions
|
|
59
|
+
@retry_exceptions ||= nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#################
|
|
63
|
+
# Retry helpers #
|
|
64
|
+
#################
|
|
65
|
+
def retry_exception?(exception)
|
|
66
|
+
return true if retry_exceptions.nil? && fatal_exceptions.empty?
|
|
67
|
+
return exception_whitelisted?(exception) unless retry_exceptions.nil?
|
|
68
|
+
!exception_blacklisted?(exception)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def exception_whitelisted?(exception)
|
|
72
|
+
retry_exceptions.any? { |ex| exception.is_a?(ex) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def exception_blacklisted?(exception)
|
|
76
|
+
fatal_exceptions.any? { |ex| exception.is_a?(ex) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#############
|
|
81
|
+
# Overrides #
|
|
82
|
+
#############
|
|
83
|
+
def serialize
|
|
84
|
+
super.merge('retry_attempt' => retry_attempt + 1)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def deserialize(job_data)
|
|
88
|
+
super(job_data)
|
|
89
|
+
@retry_attempt = job_data['retry_attempt']
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Override `rescue_with_handler` to make sure our catch is the last one, and doesn't
|
|
93
|
+
# happen if the exception has already been caught in a `rescue_from`
|
|
94
|
+
def rescue_with_handler(exception)
|
|
95
|
+
super || retry_or_reraise(exception)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
##################
|
|
99
|
+
# Retrying logic #
|
|
100
|
+
##################
|
|
101
|
+
def retry_attempt
|
|
102
|
+
@retry_attempt ||= 0
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Override me if you want more complex behaviour
|
|
106
|
+
def retry_delay
|
|
107
|
+
self.class.retry_delay
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def should_retry?(exception)
|
|
111
|
+
return false if retry_limit_reached?
|
|
112
|
+
return false unless self.class.retry_exception?(exception)
|
|
113
|
+
true
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def retry_limit_reached?
|
|
117
|
+
return true if self.class.retry_limit == 0
|
|
118
|
+
return false if self.class.retry_limit == -1
|
|
119
|
+
retry_attempt >= self.class.retry_limit
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def retry_or_reraise(exception)
|
|
123
|
+
raise exception unless should_retry?(exception)
|
|
124
|
+
|
|
125
|
+
this_delay = retry_delay
|
|
126
|
+
logger.log(Logger::INFO, "Retrying (attempt #{retry_attempt + 1}, waiting #{this_delay}s)")
|
|
127
|
+
retry_job(wait: this_delay)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# In Rails 4.2, ActiveJob externally applies deserialized job ID, queue name, arguments to
|
|
2
|
+
# the instantiated job in `ActiveJob::Base.deserialize`, which cannot be overridden in
|
|
3
|
+
# subclasses. https://github.com/rails/rails/pull/18260 changes this to delegate as much
|
|
4
|
+
# of the deserialization as possible to the instance, i.e. `ActiveJob::Base#deserialize`,
|
|
5
|
+
# which can be overridden. This allows us to store extra information in the queue (i.e.
|
|
6
|
+
# retry_attempt), which is essential for ActiveJob::Retry.
|
|
7
|
+
|
|
8
|
+
raise "Unnecessary monkey patch!" if ActiveJob::Base.method_defined?(:deserialize)
|
|
9
|
+
|
|
10
|
+
class ActiveJob::Base
|
|
11
|
+
def self.deserialize(job_data)
|
|
12
|
+
job = job_data['job_class'].constantize.new
|
|
13
|
+
job.deserialize(job_data)
|
|
14
|
+
job
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def deserialize(job_data)
|
|
18
|
+
self.job_id = job_data['job_id']
|
|
19
|
+
self.queue_name = job_data['queue_name']
|
|
20
|
+
self.serialized_arguments = job_data['arguments']
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module ActiveJob
|
|
2
|
+
module Retry
|
|
3
|
+
# If you want your job to retry on failure using a varying delay, simply
|
|
4
|
+
# extend your class with this module:
|
|
5
|
+
#
|
|
6
|
+
# class DeliverSMS < ActiveJob::Base
|
|
7
|
+
# extend ActiveJob::Retry::ExponentialBackoff
|
|
8
|
+
# queue_as :messages
|
|
9
|
+
#
|
|
10
|
+
# def perform(mobile_number, message)
|
|
11
|
+
# SmsService.deliver!(mobile_number, message)
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Easily do something custom:
|
|
16
|
+
#
|
|
17
|
+
# class DeliverSMS
|
|
18
|
+
# extend ActiveJob::Retry::ExponentialBackoff
|
|
19
|
+
# queue_as :messages
|
|
20
|
+
#
|
|
21
|
+
# # Options from ActiveJob::Retry can also be used
|
|
22
|
+
# retry_with strategy: [0, 60], # retry immediately, then after 60 seconds
|
|
23
|
+
# min_delay_multiplier: 0.8, # multiply the delay by a random number
|
|
24
|
+
# max_delay_multiplier: 1.5 # between 0.8 and 1.5
|
|
25
|
+
#
|
|
26
|
+
# def perform(mobile_number, message)
|
|
27
|
+
# SmsService.deliver!(mobile_number, message)
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
module ExponentialBackoff
|
|
32
|
+
include ActiveJob::Retry
|
|
33
|
+
|
|
34
|
+
def self.included(base)
|
|
35
|
+
base.extend(ClassMethods)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module ClassMethods
|
|
39
|
+
include ActiveJob::Retry::ClassMethods
|
|
40
|
+
|
|
41
|
+
# Setup DSL
|
|
42
|
+
def backoff_with(options)
|
|
43
|
+
retry_with(options)
|
|
44
|
+
ExponentialOptionsValidator.new(options).validate!
|
|
45
|
+
|
|
46
|
+
@retry_limit = options[:limit] || options[:strategy].length if options[:strategy]
|
|
47
|
+
@backoff_strategy = options[:strategy] if options[:strategy]
|
|
48
|
+
@min_delay_multiplier = options[:min_delay_multiplier] if options[:min_delay_multiplier]
|
|
49
|
+
@max_delay_multiplier = options[:max_delay_multiplier] if options[:max_delay_multiplier]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
############
|
|
53
|
+
# Defaults #
|
|
54
|
+
############
|
|
55
|
+
def retry_limit
|
|
56
|
+
@retry_limit ||= (backoff_strategy || []).length
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def backoff_strategy
|
|
60
|
+
@backoff_strategy ||= nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def min_delay_multiplier
|
|
64
|
+
@min_delay_multiplier ||= 1.0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def max_delay_multiplier
|
|
68
|
+
@max_delay_multiplier ||= 1.0
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
###############
|
|
73
|
+
# Retry logic #
|
|
74
|
+
###############
|
|
75
|
+
def retry_delay
|
|
76
|
+
delay = self.class.backoff_strategy[retry_attempt]
|
|
77
|
+
|
|
78
|
+
return (delay * self.class.max_delay_multiplier).to_i unless random_delay?
|
|
79
|
+
|
|
80
|
+
(delay * rand(random_delay_range)).to_i
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def random_delay?
|
|
84
|
+
self.class.min_delay_multiplier != self.class.max_delay_multiplier
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def random_delay_range
|
|
88
|
+
self.class.min_delay_multiplier..self.class.max_delay_multiplier
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module ActiveJob
|
|
2
|
+
module Retry
|
|
3
|
+
class ExponentialOptionsValidator
|
|
4
|
+
DELAY_MULTIPLIER_KEYS = %i(min_delay_multiplier max_delay_multiplier).freeze
|
|
5
|
+
|
|
6
|
+
def initialize(options)
|
|
7
|
+
@options = options
|
|
8
|
+
@retry_limit = options[:limit] || options.fetch(:strategy, []).length
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def validate!
|
|
12
|
+
validate_strategy!
|
|
13
|
+
validate_delay_multipliers!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :options, :retry_limit
|
|
19
|
+
|
|
20
|
+
def validate_strategy!
|
|
21
|
+
unless options[:strategy]
|
|
22
|
+
raise InvalidConfigurationError, "You must define a backoff strategy"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
return unless retry_limit
|
|
26
|
+
|
|
27
|
+
unless retry_limit > 0
|
|
28
|
+
raise InvalidConfigurationError,
|
|
29
|
+
"Exponential backoff cannot be used with infinite or no retries"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
return if options[:strategy].length == retry_limit
|
|
33
|
+
|
|
34
|
+
raise InvalidConfigurationError, "Strategy must have a delay for each retry"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_delay_multipliers!
|
|
38
|
+
unless both_or_neither_multiplier_supplied?
|
|
39
|
+
raise InvalidConfigurationError,
|
|
40
|
+
"If one of min/max_delay_multiplier is supplied, both are required"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
return unless options[:min_delay_multiplier] && options[:max_delay_multiplier]
|
|
44
|
+
|
|
45
|
+
return if options[:min_delay_multiplier] <= options[:max_delay_multiplier]
|
|
46
|
+
|
|
47
|
+
raise InvalidConfigurationError,
|
|
48
|
+
"min_delay_multiplier must be less than or equal to max_delay_multiplier"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def both_or_neither_multiplier_supplied?
|
|
52
|
+
supplied = DELAY_MULTIPLIER_KEYS.map { |key| options.key?(key) }
|
|
53
|
+
supplied.none? || supplied.all?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module ActiveJob
|
|
2
|
+
module Retry
|
|
3
|
+
class OptionsValidator
|
|
4
|
+
def initialize(options)
|
|
5
|
+
@options = options
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def validate!
|
|
9
|
+
validate_limit!
|
|
10
|
+
validate_delay!
|
|
11
|
+
validate_fatal_exceptions!
|
|
12
|
+
validate_retry_exceptions!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :options
|
|
18
|
+
|
|
19
|
+
# Limit must be an integer >= -1
|
|
20
|
+
# If it is -1 you *must* set `infinite_job: true` and understand that you're
|
|
21
|
+
# entering a world of pain and your ops team might hurt you.
|
|
22
|
+
def validate_limit!
|
|
23
|
+
return unless options[:limit]
|
|
24
|
+
|
|
25
|
+
unless options[:limit].is_a?(Fixnum)
|
|
26
|
+
raise InvalidConfigurationError, "Limit must be an integer"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
raise InvalidConfigurationError, "Limit must be >= -1" if options[:limit] < -1
|
|
30
|
+
|
|
31
|
+
if options[:limit] == -1 && !options[:infinite_job]
|
|
32
|
+
raise InvalidConfigurationError,
|
|
33
|
+
"You must set `infinite_job: true` to use an infinite job"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Delay must be non-negative
|
|
38
|
+
def validate_delay!
|
|
39
|
+
return unless options[:delay]
|
|
40
|
+
|
|
41
|
+
unless options[:delay] >= 0
|
|
42
|
+
raise InvalidConfigurationError, "Delay must be non-negative"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Fatal exceptions must be an array (cannot be nil, since then all exceptions would
|
|
47
|
+
# be fatal - for that just set `limit: 0`)
|
|
48
|
+
def validate_fatal_exceptions!
|
|
49
|
+
return unless options[:fatal_exceptions]
|
|
50
|
+
|
|
51
|
+
unless options[:retry_exceptions].nil?
|
|
52
|
+
raise InvalidConfigurationError,
|
|
53
|
+
"fatal_exceptions and retry_exceptions cannot be used together"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
unless options[:fatal_exceptions].is_a?(Array)
|
|
57
|
+
raise InvalidConfigurationError, "fatal_exceptions must be an array"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
unless options[:fatal_exceptions].all? { |ex| ex.is_a?(Class) && ex <= Exception }
|
|
61
|
+
raise InvalidConfigurationError, "fatal_exceptions must be exceptions!"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Retry exceptions must be an array of exceptions or `nil` to retry any exception
|
|
66
|
+
def validate_retry_exceptions!
|
|
67
|
+
return unless options[:retry_exceptions]
|
|
68
|
+
|
|
69
|
+
unless options[:fatal_exceptions].nil?
|
|
70
|
+
raise InvalidConfigurationError,
|
|
71
|
+
"retry_exceptions and fatal_exceptions cannot be used together"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
unless options[:retry_exceptions].is_a?(Array)
|
|
75
|
+
raise InvalidConfigurationError, "retry_exceptions must be an array or nil"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless options[:retry_exceptions].all? { |ex| ex.is_a?(Class) && ex <= Exception }
|
|
79
|
+
raise InvalidConfigurationError, "retry_exceptions must be exceptions!"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/spec/retry_spec.rb
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe ActiveJob::Retry do
|
|
4
|
+
subject(:job) { Class.new(ActiveJob::Base) { include ActiveJob::Retry } }
|
|
5
|
+
|
|
6
|
+
describe '.retry_with' do
|
|
7
|
+
context 'valid options' do
|
|
8
|
+
before { job.retry_with(options) }
|
|
9
|
+
|
|
10
|
+
context 'limit only' do
|
|
11
|
+
context 'infinite retries' do
|
|
12
|
+
let(:options) { { limit: -1, infinite_job: true } }
|
|
13
|
+
its(:retry_limit) { is_expected.to eq(-1) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context 'no retries' do
|
|
17
|
+
let(:options) { { limit: 0 } }
|
|
18
|
+
its(:retry_limit) { is_expected.to eq(0) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
context 'some retries' do
|
|
22
|
+
let(:options) { { limit: 3 } }
|
|
23
|
+
its(:retry_limit) { is_expected.to eq(3) }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
context 'delay only' do
|
|
28
|
+
let(:options) { { delay: 0 } }
|
|
29
|
+
its(:retry_delay) { is_expected.to eq(0) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
context 'fatal exceptions' do
|
|
33
|
+
let(:options) { { fatal_exceptions: [RuntimeError] } }
|
|
34
|
+
its(:fatal_exceptions) { is_expected.to eq([RuntimeError]) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context 'retry exceptions' do
|
|
38
|
+
let(:options) { { retry_exceptions: [StandardError, NoMethodError] } }
|
|
39
|
+
its(:retry_exceptions) { is_expected.to eq([StandardError, NoMethodError]) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
context 'multiple options' do
|
|
43
|
+
let(:options) { { limit: 3, delay: 10, retry_exceptions: [RuntimeError] } }
|
|
44
|
+
|
|
45
|
+
its(:retry_limit) { is_expected.to eq(3) }
|
|
46
|
+
its(:retry_delay) { is_expected.to eq(10) }
|
|
47
|
+
its(:retry_exceptions) { is_expected.to eq([RuntimeError]) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'invalid options' do
|
|
52
|
+
subject { -> { job.retry_with(options) } }
|
|
53
|
+
|
|
54
|
+
context 'bad limit' do
|
|
55
|
+
let(:options) { { limit: -2 } }
|
|
56
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
context 'accidental infinite limit' do
|
|
60
|
+
let(:options) { { limit: -1 } }
|
|
61
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
context 'bad delay' do
|
|
65
|
+
let(:options) { { delay: -1 } }
|
|
66
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'bad fatal exceptions' do
|
|
70
|
+
let(:options) { { fatal_exceptions: ['StandardError'] } }
|
|
71
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
context 'bad retry exceptions' do
|
|
75
|
+
let(:options) { { retry_exceptions: [:runtime] } }
|
|
76
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
context 'retry and fatal exceptions together' do
|
|
80
|
+
let(:options) { { fatal_exceptions: [StandardError], retry_exceptions: [] } }
|
|
81
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe '.retry_exception?' do
|
|
87
|
+
subject { job.retry_exception?(exception) }
|
|
88
|
+
|
|
89
|
+
context 'defaults (retry everything)' do
|
|
90
|
+
context 'Exception' do
|
|
91
|
+
let(:exception) { Exception.new }
|
|
92
|
+
it { is_expected.to be_truthy }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
context 'RuntimeError' do
|
|
96
|
+
let(:exception) { RuntimeError.new }
|
|
97
|
+
it { is_expected.to be_truthy }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
context 'InvalidConfigurationError' do
|
|
101
|
+
let(:exception) { ActiveJob::Retry::InvalidConfigurationError.new }
|
|
102
|
+
it { is_expected.to be_truthy }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
context 'with whitelist' do
|
|
107
|
+
before { job.retry_with(retry_exceptions: [RuntimeError]) }
|
|
108
|
+
|
|
109
|
+
context 'Exception' do
|
|
110
|
+
let(:exception) { Exception.new }
|
|
111
|
+
it { is_expected.to be_falsey }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
context 'RuntimeError' do
|
|
115
|
+
let(:exception) { RuntimeError.new }
|
|
116
|
+
it { is_expected.to be_truthy }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
context 'subclass of RuntimeError' do
|
|
120
|
+
let(:exception) { Class.new(RuntimeError).new }
|
|
121
|
+
it { is_expected.to be_truthy }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
context 'with blacklist' do
|
|
126
|
+
before { job.retry_with(fatal_exceptions: [RuntimeError]) }
|
|
127
|
+
|
|
128
|
+
context 'Exception' do
|
|
129
|
+
let(:exception) { Exception.new }
|
|
130
|
+
it { is_expected.to be_truthy }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
context 'RuntimeError' do
|
|
134
|
+
let(:exception) { RuntimeError.new }
|
|
135
|
+
it { is_expected.to be_falsey }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
context 'subclass of RuntimeError' do
|
|
139
|
+
let(:exception) { Class.new(RuntimeError).new }
|
|
140
|
+
it { is_expected.to be_falsey }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
describe '#retry_limit_reached?' do
|
|
146
|
+
let(:instance) { job.tap { |job| job.retry_with(options) }.new }
|
|
147
|
+
subject { instance.retry_limit_reached? }
|
|
148
|
+
|
|
149
|
+
context 'when the limit is infinite' do
|
|
150
|
+
let(:options) { { limit: -1, infinite_job: true } }
|
|
151
|
+
|
|
152
|
+
context 'first attempt' do
|
|
153
|
+
before { instance.instance_variable_set(:@retry_attempt, 1) }
|
|
154
|
+
it { is_expected.to be_falsey }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
context '99999th attempt' do
|
|
158
|
+
before { instance.instance_variable_set(:@retry_attempt, 99999) }
|
|
159
|
+
it { is_expected.to be_falsey }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
context 'when the limit is 0' do
|
|
164
|
+
let(:options) { { limit: 0 } }
|
|
165
|
+
|
|
166
|
+
context 'first attempt' do
|
|
167
|
+
before { instance.instance_variable_set(:@retry_attempt, 1) }
|
|
168
|
+
it { is_expected.to be_truthy }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
context '99999th attempt' do
|
|
172
|
+
before { instance.instance_variable_set(:@retry_attempt, 99999) }
|
|
173
|
+
it { is_expected.to be_truthy }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
context 'when the limit is 5' do
|
|
178
|
+
let(:options) { { limit: 5 } }
|
|
179
|
+
|
|
180
|
+
context 'first attempt' do
|
|
181
|
+
before { instance.instance_variable_set(:@retry_attempt, 1) }
|
|
182
|
+
it { is_expected.to be_falsey }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
context '4th attempt' do
|
|
186
|
+
before { instance.instance_variable_set(:@retry_attempt, 4) }
|
|
187
|
+
it { is_expected.to be_falsey }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
context '5th attempt' do
|
|
191
|
+
before { instance.instance_variable_set(:@retry_attempt, 5) }
|
|
192
|
+
it { is_expected.to be_truthy }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
describe '#retry_or_reraise' do
|
|
198
|
+
let(:instance) { job.new }
|
|
199
|
+
let(:exception) { RuntimeError.new }
|
|
200
|
+
subject(:retry_or_reraise) { instance.retry_or_reraise(exception) }
|
|
201
|
+
|
|
202
|
+
context 'when we should not retry' do
|
|
203
|
+
before do
|
|
204
|
+
allow(instance).to receive(:should_retry?).with(exception).and_return(false)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
specify { expect { retry_or_reraise }.to raise_error(exception) }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
context 'when we should retry' do
|
|
211
|
+
before do
|
|
212
|
+
allow(instance).to receive(:should_retry?).with(exception).and_return(true)
|
|
213
|
+
allow(instance).to receive(:retry_job).and_return(true)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it 'logs the retry' do
|
|
217
|
+
expect(ActiveJob::Base.logger).to receive(:log).
|
|
218
|
+
with(Logger::INFO, 'Retrying (attempt 1)')
|
|
219
|
+
retry_or_reraise
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it 'retries the job' do
|
|
223
|
+
expect(instance).to receive(:retry_job).with(wait: 0)
|
|
224
|
+
retry_or_reraise
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'active_job-retry'
|
|
2
|
+
require 'rspec/its'
|
|
3
|
+
require 'pry'
|
|
4
|
+
|
|
5
|
+
RSpec.configure do |config|
|
|
6
|
+
config.expect_with :rspec do |expectations|
|
|
7
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
config.mock_with :rspec do |mocks|
|
|
11
|
+
mocks.verify_partial_doubles = true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
config.disable_monkey_patching!
|
|
15
|
+
|
|
16
|
+
config.warnings = true
|
|
17
|
+
|
|
18
|
+
# config.profile_examples = 10
|
|
19
|
+
|
|
20
|
+
config.order = :random
|
|
21
|
+
|
|
22
|
+
Kernel.srand config.seed
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activejob-retry
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Isaac Seymour
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-01-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: '4.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - '>='
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '4.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activesupport
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - '>='
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '4.2'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - '>='
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '4.2'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - '>='
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '10.3'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - '>='
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '10.3'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
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
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec-its
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - '>='
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - '>='
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: pry-byebug
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - '>='
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - '>='
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
description: |2
|
|
98
|
+
activejob-retry provides a simple DSL for automatically retrying ActiveJobs when they
|
|
99
|
+
fail, with exponential backoff.
|
|
100
|
+
|
|
101
|
+
Features:
|
|
102
|
+
|
|
103
|
+
* (Should) work with any queue adapter that supports retries.
|
|
104
|
+
* Whitelist/blacklist exceptions to retry on.
|
|
105
|
+
* Exponential backoff (varying the delay between retries).
|
|
106
|
+
* Light and easy to override retry logic.
|
|
107
|
+
email:
|
|
108
|
+
- isaac@isaacseymour.co.uk
|
|
109
|
+
executables: []
|
|
110
|
+
extensions: []
|
|
111
|
+
extra_rdoc_files: []
|
|
112
|
+
files:
|
|
113
|
+
- .gitignore
|
|
114
|
+
- .rspec
|
|
115
|
+
- .travis.yml
|
|
116
|
+
- CHANGELOG.md
|
|
117
|
+
- Gemfile
|
|
118
|
+
- Gemfile.lock
|
|
119
|
+
- LICENSE
|
|
120
|
+
- README.md
|
|
121
|
+
- Rakefile
|
|
122
|
+
- activejob-retry.gemspec
|
|
123
|
+
- lib/active_job-retry.rb
|
|
124
|
+
- lib/active_job/retry.rb
|
|
125
|
+
- lib/active_job/retry/deserialize_monkey_patch.rb
|
|
126
|
+
- lib/active_job/retry/exponential_backoff.rb
|
|
127
|
+
- lib/active_job/retry/exponential_options_validator.rb
|
|
128
|
+
- lib/active_job/retry/invalid_configuration_error.rb
|
|
129
|
+
- lib/active_job/retry/options_validator.rb
|
|
130
|
+
- lib/active_job/retry/version.rb
|
|
131
|
+
- spec/retry_spec.rb
|
|
132
|
+
- spec/spec_helper.rb
|
|
133
|
+
homepage: http://github.com/isaacseymour/activejob-retry
|
|
134
|
+
licenses:
|
|
135
|
+
- MIT
|
|
136
|
+
metadata: {}
|
|
137
|
+
post_install_message:
|
|
138
|
+
rdoc_options: []
|
|
139
|
+
require_paths:
|
|
140
|
+
- lib
|
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - '>='
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - '>='
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '0'
|
|
151
|
+
requirements: []
|
|
152
|
+
rubyforge_project:
|
|
153
|
+
rubygems_version: 2.4.1
|
|
154
|
+
signing_key:
|
|
155
|
+
specification_version: 4
|
|
156
|
+
summary: Automatic retrying DSL for ActiveJob
|
|
157
|
+
test_files: []
|
|
158
|
+
has_rdoc: false
|