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.
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