activejob-retry 0.0.1 → 0.1.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 +4 -4
- data/.rubocop.yml +22 -0
- data/.travis.yml +20 -5
- data/CHANGELOG.md +3 -0
- data/Gemfile +9 -6
- data/Gemfile.lock +63 -51
- data/LICENSE +1 -1
- data/README.md +57 -25
- data/Rakefile +14 -13
- data/activejob-retry.gemspec +9 -12
- data/lib/active_job/retry.rb +69 -90
- data/lib/active_job/retry/constant_backoff_strategy.rb +50 -0
- data/lib/active_job/retry/constant_options_validator.rb +76 -0
- data/lib/active_job/retry/deserialize_monkey_patch.rb +17 -12
- data/lib/active_job/retry/errors.rb +6 -0
- data/lib/active_job/retry/variable_backoff_strategy.rb +34 -0
- data/lib/active_job/retry/variable_options_validator.rb +56 -0
- data/lib/active_job/retry/version.rb +1 -1
- data/spec/retry/constant_backoff_strategy_spec.rb +115 -0
- data/spec/retry/constant_options_validator_spec.rb +81 -0
- data/spec/retry/variable_backoff_strategy_spec.rb +121 -0
- data/spec/retry/variable_options_validator_spec.rb +83 -0
- data/spec/retry_spec.rb +114 -170
- data/spec/spec_helper.rb +3 -2
- data/test/adapters/backburner.rb +3 -0
- data/test/adapters/delayed_job.rb +7 -0
- data/test/adapters/inline.rb +1 -0
- data/test/adapters/qu.rb +3 -0
- data/test/adapters/que.rb +4 -0
- data/test/adapters/queue_classic.rb +2 -0
- data/test/adapters/resque.rb +2 -0
- data/test/adapters/sidekiq.rb +2 -0
- data/test/adapters/sneakers.rb +2 -0
- data/test/adapters/sucker_punch.rb +2 -0
- data/test/cases/adapter_test.rb +8 -0
- data/test/cases/argument_serialization_test.rb +84 -0
- data/test/cases/callbacks_test.rb +23 -0
- data/test/cases/job_serialization_test.rb +15 -0
- data/test/cases/logging_test.rb +114 -0
- data/test/cases/queue_naming_test.rb +102 -0
- data/test/cases/queuing_test.rb +44 -0
- data/test/cases/rescue_test.rb +35 -0
- data/test/cases/test_case_test.rb +14 -0
- data/test/cases/test_helper_test.rb +226 -0
- data/test/helper.rb +21 -0
- data/test/integration/queuing_test.rb +46 -0
- data/test/jobs/callback_job.rb +31 -0
- data/test/jobs/gid_job.rb +10 -0
- data/test/jobs/hello_job.rb +9 -0
- data/test/jobs/logging_job.rb +12 -0
- data/test/jobs/nested_job.rb +12 -0
- data/test/jobs/rescue_job.rb +31 -0
- data/test/models/person.rb +20 -0
- data/test/support/backburner/inline.rb +8 -0
- data/test/support/delayed_job/delayed/backend/test.rb +111 -0
- data/test/support/delayed_job/delayed/serialization/test.rb +0 -0
- data/test/support/integration/adapters/backburner.rb +38 -0
- data/test/support/integration/adapters/delayed_job.rb +20 -0
- data/test/support/integration/adapters/que.rb +37 -0
- data/test/support/integration/adapters/resque.rb +49 -0
- data/test/support/integration/adapters/sidekiq.rb +58 -0
- data/test/support/integration/dummy_app_template.rb +28 -0
- data/test/support/integration/helper.rb +30 -0
- data/test/support/integration/jobs_manager.rb +27 -0
- data/test/support/integration/test_case_helpers.rb +48 -0
- data/test/support/job_buffer.rb +19 -0
- data/test/support/que/inline.rb +9 -0
- metadata +60 -12
- data/lib/active_job-retry.rb +0 -14
- data/lib/active_job/retry/exponential_backoff.rb +0 -92
- data/lib/active_job/retry/exponential_options_validator.rb +0 -57
- data/lib/active_job/retry/invalid_configuration_error.rb +0 -6
- data/lib/active_job/retry/options_validator.rb +0 -84
data/spec/retry_spec.rb
CHANGED
@@ -1,228 +1,172 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe ActiveJob::Retry do
|
4
|
-
subject(:job)
|
4
|
+
subject(:job) do
|
5
|
+
Class.new(ActiveJob::Base) do
|
6
|
+
include ActiveJob::Retry
|
5
7
|
|
6
|
-
|
7
|
-
|
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]) }
|
8
|
+
def perform(*_args)
|
9
|
+
raise RuntimeError
|
40
10
|
end
|
11
|
+
end
|
12
|
+
end
|
41
13
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
its(:retry_delay) { is_expected.to eq(10) }
|
47
|
-
its(:retry_exceptions) { is_expected.to eq([RuntimeError]) }
|
48
|
-
end
|
14
|
+
describe '.constant_retry' do
|
15
|
+
it 'sets a ConstantBackoffStrategy' do
|
16
|
+
job.constant_retry(limit: 10, delay: 5)
|
17
|
+
expect(job.backoff_strategy).to be_a(ActiveJob::Retry::ConstantBackoffStrategy)
|
49
18
|
end
|
50
19
|
|
51
20
|
context 'invalid options' do
|
52
|
-
|
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
|
21
|
+
let(:options) { { limit: -2 } }
|
68
22
|
|
69
|
-
|
70
|
-
|
71
|
-
|
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) }
|
23
|
+
specify do
|
24
|
+
expect { job.constant_retry(options) }.
|
25
|
+
to raise_error(ActiveJob::Retry::InvalidConfigurationError)
|
82
26
|
end
|
83
27
|
end
|
84
28
|
end
|
85
29
|
|
86
|
-
describe '.
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
let(:exception) { Exception.new }
|
92
|
-
it { is_expected.to be_truthy }
|
93
|
-
end
|
30
|
+
describe '.variable_retry' do
|
31
|
+
it 'sets a VariableBackoffStrategy' do
|
32
|
+
job.variable_retry(delays: [0, 5, 10, 60, 200])
|
33
|
+
expect(job.backoff_strategy).to be_a(ActiveJob::Retry::VariableBackoffStrategy)
|
34
|
+
end
|
94
35
|
|
95
|
-
|
96
|
-
|
97
|
-
it { is_expected.to be_truthy }
|
98
|
-
end
|
36
|
+
context 'invalid options' do
|
37
|
+
let(:options) { {} }
|
99
38
|
|
100
|
-
|
101
|
-
|
102
|
-
|
39
|
+
specify do
|
40
|
+
expect { job.variable_retry(options) }.
|
41
|
+
to raise_error(ActiveJob::Retry::InvalidConfigurationError)
|
103
42
|
end
|
104
43
|
end
|
44
|
+
end
|
105
45
|
|
106
|
-
|
107
|
-
|
46
|
+
describe '.retry_with' do
|
47
|
+
it 'rejects invalid backoff strategies' do
|
48
|
+
expect { job.retry_with(Object.new) }.
|
49
|
+
to raise_error(ActiveJob::Retry::InvalidConfigurationError)
|
50
|
+
end
|
108
51
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
52
|
+
it 'sets the backoff_strategy when it is valid' do
|
53
|
+
module CustomBackoffStrategy
|
54
|
+
def self.should_retry?(_attempt, _exception)
|
55
|
+
true
|
56
|
+
end
|
113
57
|
|
114
|
-
|
115
|
-
|
116
|
-
|
58
|
+
def self.retry_delay(_attempt, _exception)
|
59
|
+
5
|
60
|
+
end
|
117
61
|
end
|
118
62
|
|
119
|
-
|
120
|
-
|
121
|
-
it { is_expected.to be_truthy }
|
122
|
-
end
|
63
|
+
job.retry_with(CustomBackoffStrategy)
|
64
|
+
expect(job.backoff_strategy).to eq(CustomBackoffStrategy)
|
123
65
|
end
|
66
|
+
end
|
124
67
|
|
125
|
-
|
126
|
-
|
68
|
+
describe '#serialize' do
|
69
|
+
let(:instance) { job.new }
|
70
|
+
subject { instance.serialize }
|
127
71
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
end
|
72
|
+
context 'first instantiated' do
|
73
|
+
it { is_expected.to include('retry_attempt' => 1) }
|
74
|
+
end
|
132
75
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
76
|
+
context '1st attempt' do
|
77
|
+
before { instance.instance_variable_set(:@retry_attempt, 1) }
|
78
|
+
it { is_expected.to include('retry_attempt' => 1) }
|
79
|
+
end
|
137
80
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
end
|
81
|
+
context '7th attempt' do
|
82
|
+
before { instance.instance_variable_set(:@retry_attempt, 7) }
|
83
|
+
it { is_expected.to include('retry_attempt' => 7) }
|
142
84
|
end
|
143
85
|
end
|
144
86
|
|
145
|
-
describe '#
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
context 'when the limit is infinite' do
|
150
|
-
let(:options) { { limit: -1, infinite_job: true } }
|
87
|
+
describe '#deserialize' do
|
88
|
+
subject(:instance) { job.new }
|
89
|
+
before { instance.deserialize(job_data) }
|
90
|
+
before { instance.send(:deserialize_arguments_if_needed) }
|
151
91
|
|
152
|
-
|
153
|
-
|
154
|
-
|
92
|
+
context '1st attempt' do
|
93
|
+
let(:job_data) do
|
94
|
+
{
|
95
|
+
'job_class' => 'SomeJob',
|
96
|
+
'job_id' => 'uuid',
|
97
|
+
'arguments' => ['arg1', { 'arg' => 2 }],
|
98
|
+
'retry_attempt' => 1
|
99
|
+
}
|
155
100
|
end
|
156
101
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
end
|
102
|
+
its(:job_id) { is_expected.to eq('uuid') }
|
103
|
+
its(:arguments) { is_expected.to eq(['arg1', { 'arg' => 2 }]) }
|
104
|
+
its(:retry_attempt) { is_expected.to eq(1) }
|
161
105
|
end
|
162
106
|
|
163
|
-
context '
|
164
|
-
let(:
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
107
|
+
context '7th attempt' do
|
108
|
+
let(:job_data) do
|
109
|
+
{
|
110
|
+
'job_class' => 'SomeJob',
|
111
|
+
'job_id' => 'uuid',
|
112
|
+
'arguments' => ['arg1', { 'arg' => 2 }],
|
113
|
+
'retry_attempt' => 7
|
114
|
+
}
|
169
115
|
end
|
170
116
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
end
|
117
|
+
its(:job_id) { is_expected.to eq('uuid') }
|
118
|
+
its(:arguments) { is_expected.to eq(['arg1', { 'arg' => 2 }]) }
|
119
|
+
its(:retry_attempt) { is_expected.to eq(7) }
|
175
120
|
end
|
121
|
+
end
|
176
122
|
|
177
|
-
|
178
|
-
|
123
|
+
describe '#rescue_with_handler' do
|
124
|
+
let(:backoff_strategy) { described_class::ConstantBackoffStrategy.new(limit: 100) }
|
125
|
+
let(:instance) { job.new }
|
126
|
+
before { job.retry_with(backoff_strategy) }
|
127
|
+
subject(:perform) { instance.perform_now }
|
179
128
|
|
180
|
-
|
181
|
-
|
182
|
-
|
129
|
+
context 'when the job should be retried' do
|
130
|
+
before do
|
131
|
+
expect(backoff_strategy).to receive(:should_retry?).
|
132
|
+
with(1, instance_of(RuntimeError)).
|
133
|
+
and_return(true)
|
134
|
+
expect(backoff_strategy).to receive(:retry_delay).
|
135
|
+
with(1, instance_of(RuntimeError)).
|
136
|
+
and_return(5)
|
183
137
|
end
|
184
138
|
|
185
|
-
|
186
|
-
|
187
|
-
|
139
|
+
it 'retries the job with the defined delay' do
|
140
|
+
expect(instance).to receive(:retry_job).with(hash_including(wait: 5))
|
141
|
+
|
142
|
+
perform
|
188
143
|
end
|
189
144
|
|
190
|
-
|
191
|
-
|
192
|
-
|
145
|
+
it 'increases the retry_attempt count' do
|
146
|
+
perform
|
147
|
+
expect(instance.retry_attempt).to eq(2)
|
193
148
|
end
|
194
|
-
end
|
195
|
-
end
|
196
149
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
subject(:retry_or_reraise) { instance.retry_or_reraise(exception) }
|
150
|
+
pending 'logs the retry' do
|
151
|
+
expect(ActiveJob::Base.logger).to receive(:log).
|
152
|
+
with(Logger::INFO, 'Retrying (attempt 1, waiting 0s)')
|
201
153
|
|
202
|
-
|
203
|
-
before do
|
204
|
-
allow(instance).to receive(:should_retry?).with(exception).and_return(false)
|
154
|
+
perform
|
205
155
|
end
|
206
|
-
|
207
|
-
specify { expect { retry_or_reraise }.to raise_error(exception) }
|
208
156
|
end
|
209
157
|
|
210
|
-
context 'when
|
158
|
+
context 'when the job should not be retried' do
|
211
159
|
before do
|
212
|
-
|
213
|
-
|
160
|
+
expect(backoff_strategy).to receive(:should_retry?).
|
161
|
+
with(1, instance_of(RuntimeError)).
|
162
|
+
and_return(false)
|
214
163
|
end
|
215
164
|
|
216
|
-
it '
|
217
|
-
expect(
|
218
|
-
with(Logger::INFO, 'Retrying (attempt 1)')
|
219
|
-
retry_or_reraise
|
220
|
-
end
|
165
|
+
it 'does not retry the job' do
|
166
|
+
expect(instance).to_not receive(:retry_job)
|
221
167
|
|
222
|
-
|
223
|
-
expect(instance).to receive(:retry_job).with(wait: 0)
|
224
|
-
retry_or_reraise
|
168
|
+
expect { perform }.to raise_error(RuntimeError)
|
225
169
|
end
|
226
170
|
end
|
227
171
|
end
|
228
|
-
end
|
172
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
ActiveJob::Base.queue_adapter = :inline
|
data/test/adapters/qu.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class AdapterTest < ActiveSupport::TestCase
|
4
|
+
test "should load #{ENV['AJADAPTER']} adapter" do
|
5
|
+
ActiveJob::Base.queue_adapter = ENV['AJADAPTER'].to_sym
|
6
|
+
assert_equal "active_job/queue_adapters/#{ENV['AJADAPTER']}_adapter".classify.constantize, ActiveJob::Base.queue_adapter
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'active_job/arguments'
|
3
|
+
require 'models/person'
|
4
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
5
|
+
|
6
|
+
class ArgumentSerializationTest < ActiveSupport::TestCase
|
7
|
+
setup do
|
8
|
+
@person = Person.find('5')
|
9
|
+
end
|
10
|
+
|
11
|
+
[ nil, 1, 1.0, 1_000_000_000_000_000_000_000,
|
12
|
+
'a', true, false,
|
13
|
+
[ 1, 'a' ],
|
14
|
+
{ 'a' => 1 }
|
15
|
+
].each do |arg|
|
16
|
+
test "serializes #{arg.class} verbatim" do
|
17
|
+
assert_arguments_unchanged arg
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
[ :a, Object.new, self, Person.find('5').to_gid ].each do |arg|
|
22
|
+
test "does not serialize #{arg.class}" do
|
23
|
+
assert_raises ActiveJob::SerializationError do
|
24
|
+
ActiveJob::Arguments.serialize [ arg ]
|
25
|
+
end
|
26
|
+
|
27
|
+
assert_raises ActiveJob::DeserializationError do
|
28
|
+
ActiveJob::Arguments.deserialize [ arg ]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
test 'should convert records to Global IDs' do
|
34
|
+
assert_arguments_roundtrip [@person], ['_aj_globalid' => @person.to_gid.to_s]
|
35
|
+
end
|
36
|
+
|
37
|
+
test 'should dive deep into arrays and hashes' do
|
38
|
+
assert_arguments_roundtrip [3, [@person]], [3, ['_aj_globalid' => @person.to_gid.to_s]]
|
39
|
+
assert_arguments_roundtrip [{ 'a' => @person }], [{ 'a' => { '_aj_globalid' => @person.to_gid.to_s }}.with_indifferent_access]
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'should stringify symbol hash keys' do
|
43
|
+
assert_equal [ 'a' => 1 ], ActiveJob::Arguments.serialize([ a: 1 ])
|
44
|
+
end
|
45
|
+
|
46
|
+
test 'should disallow non-string/symbol hash keys' do
|
47
|
+
assert_raises ActiveJob::SerializationError do
|
48
|
+
ActiveJob::Arguments.serialize [ { 1 => 2 } ]
|
49
|
+
end
|
50
|
+
|
51
|
+
assert_raises ActiveJob::SerializationError do
|
52
|
+
ActiveJob::Arguments.serialize [ { :a => [{ 2 => 3 }] } ]
|
53
|
+
end
|
54
|
+
|
55
|
+
assert_raises ActiveJob::SerializationError do
|
56
|
+
ActiveJob::Arguments.serialize [ '_aj_globalid' => 1 ]
|
57
|
+
end
|
58
|
+
|
59
|
+
assert_raises ActiveJob::SerializationError do
|
60
|
+
ActiveJob::Arguments.serialize [ :_aj_globalid => 1 ]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
test 'should not allow non-primitive objects' do
|
65
|
+
assert_raises ActiveJob::SerializationError do
|
66
|
+
ActiveJob::Arguments.serialize [Object.new]
|
67
|
+
end
|
68
|
+
|
69
|
+
assert_raises ActiveJob::SerializationError do
|
70
|
+
ActiveJob::Arguments.serialize [1, [Object.new]]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def assert_arguments_unchanged(*args)
|
76
|
+
assert_arguments_roundtrip args, args
|
77
|
+
end
|
78
|
+
|
79
|
+
def assert_arguments_roundtrip(args, expected_serialized_args)
|
80
|
+
serialized = ActiveJob::Arguments.serialize(args)
|
81
|
+
assert_equal expected_serialized_args, serialized
|
82
|
+
assert_equal args, ActiveJob::Arguments.deserialize(serialized)
|
83
|
+
end
|
84
|
+
end
|