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
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'active_job/retry/errors'
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module Retry
|
5
|
+
class VariableOptionsValidator
|
6
|
+
DELAY_MULTIPLIER_KEYS = [:min_delay_multiplier, :max_delay_multiplier].freeze
|
7
|
+
|
8
|
+
def initialize(options)
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate!
|
13
|
+
validate_banned_basic_option!(:limit)
|
14
|
+
validate_banned_basic_option!(:delay)
|
15
|
+
validate_delays!
|
16
|
+
validate_delay_multipliers!
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :options, :retry_limit
|
22
|
+
|
23
|
+
def validate_banned_basic_option!(key)
|
24
|
+
return unless options[key]
|
25
|
+
|
26
|
+
raise InvalidConfigurationError, "Cannot use #{key} with VariableBackoffStrategy"
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate_delays!
|
30
|
+
return if options[:delays]
|
31
|
+
|
32
|
+
raise InvalidConfigurationError,
|
33
|
+
'You must define an array of delays between attempts'
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_delay_multipliers!
|
37
|
+
validate_delay_multipliers_supplied_together!
|
38
|
+
|
39
|
+
return unless options[:min_delay_multiplier] && options[:max_delay_multiplier]
|
40
|
+
|
41
|
+
return if options[:min_delay_multiplier] <= options[:max_delay_multiplier]
|
42
|
+
|
43
|
+
raise InvalidConfigurationError,
|
44
|
+
'min_delay_multiplier must be less than or equal to max_delay_multiplier'
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_delay_multipliers_supplied_together!
|
48
|
+
supplied = DELAY_MULTIPLIER_KEYS.map { |key| options.key?(key) }
|
49
|
+
return if supplied.none? || supplied.all?
|
50
|
+
|
51
|
+
raise InvalidConfigurationError,
|
52
|
+
'If one of min/max_delay_multiplier is supplied, both are required'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveJob::Retry::ConstantBackoffStrategy do
|
4
|
+
let(:strategy) { described_class.new(options) }
|
5
|
+
|
6
|
+
describe '#should_retry?' do
|
7
|
+
subject { strategy.should_retry?(attempt, exception) }
|
8
|
+
let(:attempt) { 1 }
|
9
|
+
let(:exception) { RuntimeError.new }
|
10
|
+
|
11
|
+
context 'when the limit is infinite' do
|
12
|
+
let(:options) { { limit: nil, unlimited_retries: true } }
|
13
|
+
|
14
|
+
context '1st attempt' do
|
15
|
+
let(:attempt) { 1 }
|
16
|
+
it { is_expected.to be(true) }
|
17
|
+
end
|
18
|
+
|
19
|
+
context '99999th attempt' do
|
20
|
+
let(:attempt) { 99_999 }
|
21
|
+
it { is_expected.to be(true) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when the limit is 0' do
|
26
|
+
let(:options) { { limit: 0 } }
|
27
|
+
|
28
|
+
context '1st attempt' do
|
29
|
+
let(:attempt) { 1 }
|
30
|
+
it { is_expected.to be(false) }
|
31
|
+
end
|
32
|
+
|
33
|
+
context '99999th attempt' do
|
34
|
+
let(:attempt) { 99_999 }
|
35
|
+
it { is_expected.to be(false) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'when the limit is 5' do
|
40
|
+
let(:options) { { limit: 5 } }
|
41
|
+
|
42
|
+
context '1st attempt' do
|
43
|
+
let(:attempt) { 1 }
|
44
|
+
it { is_expected.to be(true) }
|
45
|
+
end
|
46
|
+
|
47
|
+
context '4th attempt' do
|
48
|
+
let(:attempt) { 4 }
|
49
|
+
it { is_expected.to be(true) }
|
50
|
+
end
|
51
|
+
|
52
|
+
context '5th attempt' do
|
53
|
+
let(:attempt) { 5 }
|
54
|
+
it { is_expected.to be(false) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'defaults (retry everything)' do
|
59
|
+
let(:options) { { limit: 10 } }
|
60
|
+
|
61
|
+
context 'Exception' do
|
62
|
+
let(:exception) { Exception.new }
|
63
|
+
it { is_expected.to be(true) }
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'RuntimeError' do
|
67
|
+
let(:exception) { RuntimeError.new }
|
68
|
+
it { is_expected.to be(true) }
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'subclass of RuntimeError' do
|
72
|
+
let(:exception) { Class.new(RuntimeError).new }
|
73
|
+
it { is_expected.to be(true) }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'with whitelist' do
|
78
|
+
let(:options) { { limit: 10, retry_exceptions: [RuntimeError] } }
|
79
|
+
|
80
|
+
context 'Exception' do
|
81
|
+
let(:exception) { Exception.new }
|
82
|
+
it { is_expected.to be(false) }
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'RuntimeError' do
|
86
|
+
let(:exception) { RuntimeError.new }
|
87
|
+
it { is_expected.to be(true) }
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'subclass of RuntimeError' do
|
91
|
+
let(:exception) { Class.new(RuntimeError).new }
|
92
|
+
it { is_expected.to be(true) }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'with blacklist' do
|
97
|
+
let(:options) { { limit: 10, fatal_exceptions: [RuntimeError] } }
|
98
|
+
|
99
|
+
context 'Exception' do
|
100
|
+
let(:exception) { Exception.new }
|
101
|
+
it { is_expected.to be(true) }
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'RuntimeError' do
|
105
|
+
let(:exception) { RuntimeError.new }
|
106
|
+
it { is_expected.to be(false) }
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'subclass of RuntimeError' do
|
110
|
+
let(:exception) { Class.new(RuntimeError).new }
|
111
|
+
it { is_expected.to be(false) }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveJob::Retry::ConstantOptionsValidator do
|
4
|
+
let(:validator) { described_class.new(options) }
|
5
|
+
subject(:validate!) { -> { validator.validate! } }
|
6
|
+
|
7
|
+
context 'valid options' do
|
8
|
+
context 'unlimited retries' do
|
9
|
+
let(:options) { { limit: nil, unlimited_retries: true } }
|
10
|
+
it { is_expected.to_not raise_error }
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'no retries' do
|
14
|
+
let(:options) { { limit: 0 } }
|
15
|
+
it { is_expected.to_not raise_error }
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'some retries' do
|
19
|
+
let(:options) { { limit: 3 } }
|
20
|
+
it { is_expected.to_not raise_error }
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'zero delay' do
|
24
|
+
let(:options) { { delay: 0 } }
|
25
|
+
it { is_expected.to_not raise_error }
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'fatal exceptions' do
|
29
|
+
let(:options) { { fatal_exceptions: [RuntimeError] } }
|
30
|
+
it { is_expected.to_not raise_error }
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'retry exceptions' do
|
34
|
+
let(:options) { { retry_exceptions: [StandardError, NoMethodError] } }
|
35
|
+
it { is_expected.to_not raise_error }
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'multiple options' do
|
39
|
+
let(:options) { { limit: 3, delay: 10, retry_exceptions: [RuntimeError] } }
|
40
|
+
|
41
|
+
it { is_expected.to_not raise_error }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'invalid options' do
|
46
|
+
context 'bad limit' do
|
47
|
+
let(:options) { { limit: -1 } }
|
48
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'accidental infinite limit' do
|
52
|
+
let(:options) { { limit: nil } }
|
53
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'accidental finite limit' do
|
57
|
+
let(:options) { { unlimited_retries: true } }
|
58
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'bad delay' do
|
62
|
+
let(:options) { { delay: -1 } }
|
63
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'bad fatal exceptions' do
|
67
|
+
let(:options) { { fatal_exceptions: ['StandardError'] } }
|
68
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'bad retry exceptions' do
|
72
|
+
let(:options) { { retry_exceptions: [:runtime] } }
|
73
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'retry and fatal exceptions together' do
|
77
|
+
let(:options) { { fatal_exceptions: [StandardError], retry_exceptions: [] } }
|
78
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveJob::Retry::VariableBackoffStrategy do
|
4
|
+
let(:backoff_strategy) { described_class.new(options) }
|
5
|
+
|
6
|
+
describe '#should_retry?' do
|
7
|
+
subject { backoff_strategy.should_retry?(attempt, exception) }
|
8
|
+
let(:attempt) { 1 }
|
9
|
+
let(:exception) { RuntimeError.new }
|
10
|
+
|
11
|
+
context 'when the strategy is empty' do
|
12
|
+
let(:options) { { delays: [] } }
|
13
|
+
|
14
|
+
context '1st attempt' do
|
15
|
+
let(:attempt) { 1 }
|
16
|
+
it { is_expected.to be(false) }
|
17
|
+
end
|
18
|
+
|
19
|
+
context '99999th attempt' do
|
20
|
+
let(:attempt) { 99_999 }
|
21
|
+
it { is_expected.to be(false) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when the strategy has 4 delays' do
|
26
|
+
let(:options) { { delays: [0, 3, 5, 10] } }
|
27
|
+
|
28
|
+
context '1st attempt' do
|
29
|
+
let(:attempt) { 1 }
|
30
|
+
it { is_expected.to be(true) }
|
31
|
+
end
|
32
|
+
|
33
|
+
context '4th attempt' do
|
34
|
+
let(:attempt) { 4 }
|
35
|
+
it { is_expected.to be(true) }
|
36
|
+
end
|
37
|
+
|
38
|
+
context '5th attempt' do
|
39
|
+
let(:attempt) { 5 }
|
40
|
+
it { is_expected.to be(false) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'defaults (retry everything)' do
|
45
|
+
let(:options) { { delays: [0, 3, 5, 10, 60] } }
|
46
|
+
|
47
|
+
context 'Exception' do
|
48
|
+
let(:exception) { Exception.new }
|
49
|
+
it { is_expected.to be(true) }
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'RuntimeError' do
|
53
|
+
let(:exception) { RuntimeError.new }
|
54
|
+
it { is_expected.to be(true) }
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'subclass of RuntimeError' do
|
58
|
+
let(:exception) { Class.new(RuntimeError).new }
|
59
|
+
it { is_expected.to be(true) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'with whitelist' do
|
64
|
+
let(:options) { { delays: [10], retry_exceptions: [RuntimeError] } }
|
65
|
+
|
66
|
+
context 'Exception' do
|
67
|
+
let(:exception) { Exception.new }
|
68
|
+
it { is_expected.to be(false) }
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'RuntimeError' do
|
72
|
+
let(:exception) { RuntimeError.new }
|
73
|
+
it { is_expected.to be(true) }
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'subclass of RuntimeError' do
|
77
|
+
let(:exception) { Class.new(RuntimeError).new }
|
78
|
+
it { is_expected.to be(true) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'with blacklist' do
|
83
|
+
let(:options) { { delays: [10], fatal_exceptions: [RuntimeError] } }
|
84
|
+
|
85
|
+
context 'Exception' do
|
86
|
+
let(:exception) { Exception.new }
|
87
|
+
it { is_expected.to be(true) }
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'RuntimeError' do
|
91
|
+
let(:exception) { RuntimeError.new }
|
92
|
+
it { is_expected.to be(false) }
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'subclass of RuntimeError' do
|
96
|
+
let(:exception) { Class.new(RuntimeError).new }
|
97
|
+
it { is_expected.to be(false) }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#retry_delay' do
|
103
|
+
subject { backoff_strategy.retry_delay(attempt, exception) }
|
104
|
+
let(:attempt) { 1 }
|
105
|
+
let(:exception) { RuntimeError.new }
|
106
|
+
|
107
|
+
context 'when the strategy has 4 delays' do
|
108
|
+
let(:options) { { delays: [0, 3, 5, 10] } }
|
109
|
+
|
110
|
+
context '1st attempt' do
|
111
|
+
let(:attempt) { 1 }
|
112
|
+
it { is_expected.to eq(0) }
|
113
|
+
end
|
114
|
+
|
115
|
+
context '4th attempt' do
|
116
|
+
let(:attempt) { 4 }
|
117
|
+
it { is_expected.to eq(10) }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ActiveJob::Retry::VariableOptionsValidator do
|
4
|
+
let(:validator) { described_class.new(options) }
|
5
|
+
subject(:validate!) { -> { validator.validate! } }
|
6
|
+
|
7
|
+
context 'valid options' do
|
8
|
+
context 'empty delays' do
|
9
|
+
let(:options) { { delays: [] } }
|
10
|
+
it { is_expected.to_not raise_error }
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'with delays' do
|
14
|
+
let(:options) { { delays: [0, 3, 6, 10_000] } }
|
15
|
+
it { is_expected.to_not raise_error }
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'min and max delay multipliers' do
|
19
|
+
let(:options) do
|
20
|
+
{ delays: [0, 10, 60], min_delay_multiplier: 0.8, max_delay_multiplier: 1.2 }
|
21
|
+
end
|
22
|
+
it { is_expected.to_not raise_error }
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'fatal exceptions' do
|
26
|
+
let(:options) { { delays: [0, 10, 60], fatal_exceptions: [RuntimeError] } }
|
27
|
+
it { is_expected.to_not raise_error }
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'retry exceptions' do
|
31
|
+
let(:options) { { delays: [], retry_exceptions: [StandardError, NoMethodError] } }
|
32
|
+
it { is_expected.to_not raise_error }
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'multiple options' do
|
36
|
+
let(:options) { { delays: [0, 10, 60], retry_exceptions: [RuntimeError] } }
|
37
|
+
|
38
|
+
it { is_expected.to_not raise_error }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'invalid options' do
|
43
|
+
context 'without delays' do
|
44
|
+
let(:options) { {} }
|
45
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'with limit' do
|
49
|
+
let(:options) { { limit: 5 } }
|
50
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'with delay' do
|
54
|
+
let(:options) { { delay: 5 } }
|
55
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'min delay multiplier only' do
|
59
|
+
let(:options) { { min_delay_multiplier: 0.8 } }
|
60
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'max delay multiplier only' do
|
64
|
+
let(:options) { { max_delay_multiplier: 0.8 } }
|
65
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'bad fatal exceptions' do
|
69
|
+
let(:options) { { fatal_exceptions: ['StandardError'] } }
|
70
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'bad retry exceptions' do
|
74
|
+
let(:options) { { retry_exceptions: [:runtime] } }
|
75
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'retry and fatal exceptions together' do
|
79
|
+
let(:options) { { fatal_exceptions: [StandardError], retry_exceptions: [] } }
|
80
|
+
it { is_expected.to raise_error(ActiveJob::Retry::InvalidConfigurationError) }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|