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
@@ -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
@@ -1,5 +1,5 @@
1
1
  module ActiveJob
2
2
  module Retry
3
- VERSION = "0.0.1"
3
+ VERSION = '0.1.1'
4
4
  end
5
5
  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