activejob-retry 0.0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +22 -0
  3. data/.travis.yml +20 -5
  4. data/CHANGELOG.md +3 -0
  5. data/Gemfile +9 -6
  6. data/Gemfile.lock +63 -51
  7. data/LICENSE +1 -1
  8. data/README.md +57 -25
  9. data/Rakefile +14 -13
  10. data/activejob-retry.gemspec +9 -12
  11. data/lib/active_job/retry.rb +69 -90
  12. data/lib/active_job/retry/constant_backoff_strategy.rb +50 -0
  13. data/lib/active_job/retry/constant_options_validator.rb +76 -0
  14. data/lib/active_job/retry/deserialize_monkey_patch.rb +17 -12
  15. data/lib/active_job/retry/errors.rb +6 -0
  16. data/lib/active_job/retry/variable_backoff_strategy.rb +34 -0
  17. data/lib/active_job/retry/variable_options_validator.rb +56 -0
  18. data/lib/active_job/retry/version.rb +1 -1
  19. data/spec/retry/constant_backoff_strategy_spec.rb +115 -0
  20. data/spec/retry/constant_options_validator_spec.rb +81 -0
  21. data/spec/retry/variable_backoff_strategy_spec.rb +121 -0
  22. data/spec/retry/variable_options_validator_spec.rb +83 -0
  23. data/spec/retry_spec.rb +114 -170
  24. data/spec/spec_helper.rb +3 -2
  25. data/test/adapters/backburner.rb +3 -0
  26. data/test/adapters/delayed_job.rb +7 -0
  27. data/test/adapters/inline.rb +1 -0
  28. data/test/adapters/qu.rb +3 -0
  29. data/test/adapters/que.rb +4 -0
  30. data/test/adapters/queue_classic.rb +2 -0
  31. data/test/adapters/resque.rb +2 -0
  32. data/test/adapters/sidekiq.rb +2 -0
  33. data/test/adapters/sneakers.rb +2 -0
  34. data/test/adapters/sucker_punch.rb +2 -0
  35. data/test/cases/adapter_test.rb +8 -0
  36. data/test/cases/argument_serialization_test.rb +84 -0
  37. data/test/cases/callbacks_test.rb +23 -0
  38. data/test/cases/job_serialization_test.rb +15 -0
  39. data/test/cases/logging_test.rb +114 -0
  40. data/test/cases/queue_naming_test.rb +102 -0
  41. data/test/cases/queuing_test.rb +44 -0
  42. data/test/cases/rescue_test.rb +35 -0
  43. data/test/cases/test_case_test.rb +14 -0
  44. data/test/cases/test_helper_test.rb +226 -0
  45. data/test/helper.rb +21 -0
  46. data/test/integration/queuing_test.rb +46 -0
  47. data/test/jobs/callback_job.rb +31 -0
  48. data/test/jobs/gid_job.rb +10 -0
  49. data/test/jobs/hello_job.rb +9 -0
  50. data/test/jobs/logging_job.rb +12 -0
  51. data/test/jobs/nested_job.rb +12 -0
  52. data/test/jobs/rescue_job.rb +31 -0
  53. data/test/models/person.rb +20 -0
  54. data/test/support/backburner/inline.rb +8 -0
  55. data/test/support/delayed_job/delayed/backend/test.rb +111 -0
  56. data/test/support/delayed_job/delayed/serialization/test.rb +0 -0
  57. data/test/support/integration/adapters/backburner.rb +38 -0
  58. data/test/support/integration/adapters/delayed_job.rb +20 -0
  59. data/test/support/integration/adapters/que.rb +37 -0
  60. data/test/support/integration/adapters/resque.rb +49 -0
  61. data/test/support/integration/adapters/sidekiq.rb +58 -0
  62. data/test/support/integration/dummy_app_template.rb +28 -0
  63. data/test/support/integration/helper.rb +30 -0
  64. data/test/support/integration/jobs_manager.rb +27 -0
  65. data/test/support/integration/test_case_helpers.rb +48 -0
  66. data/test/support/job_buffer.rb +19 -0
  67. data/test/support/que/inline.rb +9 -0
  68. metadata +60 -12
  69. data/lib/active_job-retry.rb +0 -14
  70. data/lib/active_job/retry/exponential_backoff.rb +0 -92
  71. data/lib/active_job/retry/exponential_options_validator.rb +0 -57
  72. data/lib/active_job/retry/invalid_configuration_error.rb +0 -6
  73. 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) { Class.new(ActiveJob::Base) { include ActiveJob::Retry } }
4
+ subject(:job) do
5
+ Class.new(ActiveJob::Base) do
6
+ include ActiveJob::Retry
5
7
 
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]) }
8
+ def perform(*_args)
9
+ raise RuntimeError
40
10
  end
11
+ end
12
+ end
41
13
 
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
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
- 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
21
+ let(:options) { { limit: -2 } }
68
22
 
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) }
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 '.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
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
- context 'RuntimeError' do
96
- let(:exception) { RuntimeError.new }
97
- it { is_expected.to be_truthy }
98
- end
36
+ context 'invalid options' do
37
+ let(:options) { {} }
99
38
 
100
- context 'InvalidConfigurationError' do
101
- let(:exception) { ActiveJob::Retry::InvalidConfigurationError.new }
102
- it { is_expected.to be_truthy }
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
- context 'with whitelist' do
107
- before { job.retry_with(retry_exceptions: [RuntimeError]) }
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
- context 'Exception' do
110
- let(:exception) { Exception.new }
111
- it { is_expected.to be_falsey }
112
- end
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
- context 'RuntimeError' do
115
- let(:exception) { RuntimeError.new }
116
- it { is_expected.to be_truthy }
58
+ def self.retry_delay(_attempt, _exception)
59
+ 5
60
+ end
117
61
  end
118
62
 
119
- context 'subclass of RuntimeError' do
120
- let(:exception) { Class.new(RuntimeError).new }
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
- context 'with blacklist' do
126
- before { job.retry_with(fatal_exceptions: [RuntimeError]) }
68
+ describe '#serialize' do
69
+ let(:instance) { job.new }
70
+ subject { instance.serialize }
127
71
 
128
- context 'Exception' do
129
- let(:exception) { Exception.new }
130
- it { is_expected.to be_truthy }
131
- end
72
+ context 'first instantiated' do
73
+ it { is_expected.to include('retry_attempt' => 1) }
74
+ end
132
75
 
133
- context 'RuntimeError' do
134
- let(:exception) { RuntimeError.new }
135
- it { is_expected.to be_falsey }
136
- end
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
- context 'subclass of RuntimeError' do
139
- let(:exception) { Class.new(RuntimeError).new }
140
- it { is_expected.to be_falsey }
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 '#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 } }
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
- context 'first attempt' do
153
- before { instance.instance_variable_set(:@retry_attempt, 1) }
154
- it { is_expected.to be_falsey }
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
- context '99999th attempt' do
158
- before { instance.instance_variable_set(:@retry_attempt, 99999) }
159
- it { is_expected.to be_falsey }
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 '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 }
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
- context '99999th attempt' do
172
- before { instance.instance_variable_set(:@retry_attempt, 99999) }
173
- it { is_expected.to be_truthy }
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
- context 'when the limit is 5' do
178
- let(:options) { { limit: 5 } }
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
- context 'first attempt' do
181
- before { instance.instance_variable_set(:@retry_attempt, 1) }
182
- it { is_expected.to be_falsey }
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
- context '4th attempt' do
186
- before { instance.instance_variable_set(:@retry_attempt, 4) }
187
- it { is_expected.to be_falsey }
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
- context '5th attempt' do
191
- before { instance.instance_variable_set(:@retry_attempt, 5) }
192
- it { is_expected.to be_truthy }
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
- 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) }
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
- context 'when we should not retry' do
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 we should retry' do
158
+ context 'when the job should not be retried' do
211
159
  before do
212
- allow(instance).to receive(:should_retry?).with(exception).and_return(true)
213
- allow(instance).to receive(:retry_job).and_return(true)
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 '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
165
+ it 'does not retry the job' do
166
+ expect(instance).to_not receive(:retry_job)
221
167
 
222
- it 'retries the job' do
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
@@ -1,6 +1,7 @@
1
- require 'active_job-retry'
1
+ require 'active_job/retry'
2
2
  require 'rspec/its'
3
- require 'pry'
3
+
4
+ ActiveJob::Base.queue_adapter = :resque
4
5
 
5
6
  RSpec.configure do |config|
6
7
  config.expect_with :rspec do |expectations|
@@ -0,0 +1,3 @@
1
+ require 'support/backburner/inline'
2
+
3
+ ActiveJob::Base.queue_adapter = :backburner
@@ -0,0 +1,7 @@
1
+ ActiveJob::Base.queue_adapter = :delayed_job
2
+
3
+ $LOAD_PATH << File.dirname(__FILE__) + "/../support/delayed_job"
4
+
5
+ Delayed::Worker.delay_jobs = false
6
+ Delayed::Worker.backend = :test
7
+
@@ -0,0 +1 @@
1
+ ActiveJob::Base.queue_adapter = :inline
@@ -0,0 +1,3 @@
1
+ require 'qu-immediate'
2
+
3
+ ActiveJob::Base.queue_adapter = :qu
@@ -0,0 +1,4 @@
1
+ require 'support/que/inline'
2
+
3
+ ActiveJob::Base.queue_adapter = :que
4
+ Que.mode = :sync
@@ -0,0 +1,2 @@
1
+ require 'support/queue_classic/inline'
2
+ ActiveJob::Base.queue_adapter = :queue_classic
@@ -0,0 +1,2 @@
1
+ ActiveJob::Base.queue_adapter = :resque
2
+ Resque.inline = true
@@ -0,0 +1,2 @@
1
+ require 'sidekiq/testing/inline'
2
+ ActiveJob::Base.queue_adapter = :sidekiq
@@ -0,0 +1,2 @@
1
+ require 'support/sneakers/inline'
2
+ ActiveJob::Base.queue_adapter = :sneakers
@@ -0,0 +1,2 @@
1
+ require 'sucker_punch/testing/inline'
2
+ ActiveJob::Base.queue_adapter = :sucker_punch
@@ -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