sidekiq-rescue 0.3.1 → 0.5.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f373ccbd6c038182766f55cd0d2091eb3052c2a008827b223aa9293728d41268
4
- data.tar.gz: d6b8c96d73cb0690446be1b8330eda3f805a63f386d1778d5a1682d70e379037
3
+ metadata.gz: 237d80187eecb22c8b7ef0d4f15ff93e2c728b024ea01119dae161ac7d75385d
4
+ data.tar.gz: 8861871097ab632c21ee355dc9ce4ef56700ea1088a19be504295374e6a9636b
5
5
  SHA512:
6
- metadata.gz: 967dd0e178742e6dd1df06f0e5382d2a07d889a18007db15c7202f2725954c29916a866b182b1823e35fe24ad5252df791201e26045c456272ac74b36f1c82c8
7
- data.tar.gz: 6ce5d97588e78c3cf1bd49e462a9af742bd7afa2fc25a5b6e798adc67a182e0d2236b8c24471d729de757a7cbb25d8544d7baac3e69d5fb01dec164fb72c34a8
6
+ metadata.gz: 543e8549fb9c61be288bd7cabe436d4e26d80db094a78661996f053861d474aef955efa8ea0448d06122cf78e7350ef2edb2f1fb5a322d37cd05673a3ab02acd
7
+ data.tar.gz: '09d8d2bb966fcb827376c88dc11e1c1c11bf9773d207ed064df9bd69b2e35bc6a02a3847aabf920ad08f4f4ae4f59254b7621332956c170289a1d580a809159f'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2024-10-17
4
+ - Add support for queue configuration [#5](https://github.com/moofkit/sidekiq-rescue/pull/5)
5
+
6
+ ## [0.4.0] - 2024-06-03
7
+ - Add support for jitter configuration [#4](https://github.com/moofkit/sidekiq-rescue/pull/4)
8
+ - Changes the strategy for retry delay. Now it's calculated using the formula `delay + delay * jitter * rand`
9
+
3
10
  ## [0.3.1] - 2024-05-30
4
11
 
5
12
  - Fix bug with inheritance of DSL options
@@ -7,7 +14,7 @@
7
14
  ## [0.3.0] - 2024-05-30
8
15
 
9
16
  - Fix issue with RSpec matcher when job is not rescueable
10
- - Add support for multiple invocations of the DSL #3
17
+ - Add support for multiple invocations of the DSL
11
18
  - Update documentation with new features
12
19
 
13
20
  ## [0.2.1] - 2024-02-27
@@ -30,7 +37,9 @@
30
37
  - Add documentation
31
38
  - Add CI
32
39
 
33
- [Unreleased]: https://github.com/moofkit/sidekiq-rescue/compare/v0.3.1...HEAD
40
+ [Unreleased]: https://github.com/moofkit/sidekiq-rescue/compare/v0.5.0...HEAD
41
+ [0.5.0]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.5.0
42
+ [0.4.0]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.4.0
34
43
  [0.3.1]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.3.1
35
44
  [0.3.0]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.3.0
36
45
  [0.2.1]: https://github.com/moofkit/sidekiq-rescue/releases/tag/v0.2.1
data/README.md CHANGED
@@ -16,14 +16,16 @@ class MyJob
16
16
 
17
17
  sidekiq_rescue CustomAppException # defaults to 60 seconds delay and 10 retries
18
18
  sidekiq_rescue AnotherCustomAppException, delay: ->(counter) { counter * 2 }
19
- sidekiq_rescue CustomInfrastructureException, delay: 5.minutes
20
- sidekiq_rescue ActiveRecord::Deadlocked, delay: 5.seconds, limit: 3
19
+ sidekiq_rescue CustomInfrastructureException, delay: 5.minutes.to_i
20
+ sidekiq_rescue ActiveRecord::Deadlocked, delay: 5.seconds.to_i, limit: 3
21
+ sidekiq_rescue TooManyRequestsError, queue: "slow"
21
22
  sidekiq_rescue Net::OpenTimeout, Timeout::Error, limit: 10 # retries at most 10 times for Net::OpenTimeout and Timeout::Error combined
22
23
 
23
24
  def perform(*args)
24
25
  # Might raise CustomAppException, AnotherCustomAppException, or YetAnotherCustomAppException for something domain specific
25
26
  # Might raise ActiveRecord::Deadlocked when a local db deadlock is detected
26
27
  # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down
28
+ # Might raise TooManyRequestsError when the rate limit is exceeded
27
29
  end
28
30
  end
29
31
  ```
@@ -74,14 +76,14 @@ end
74
76
 
75
77
  ## Configuration
76
78
 
77
- You can configure the number of retries and the delay (in seconds) between retries:
79
+ You can configure the number of retries and the delay *in seconds* between retries:
78
80
 
79
81
  ```ruby
80
82
  class MyJob
81
83
  include Sidekiq::Job
82
84
  include Sidekiq::Rescue::Dsl
83
85
 
84
- sidekiq_rescue ExpectedError, delay: 60, limit: 5
86
+ sidekiq_rescue ExpectedError, delay: 60, limit: 5, jitter: 0.15
85
87
 
86
88
  def perform(*)
87
89
  # ...
@@ -89,18 +91,25 @@ class MyJob
89
91
  end
90
92
  ```
91
93
 
92
- The `delay` is not the exact time between retries, but a minimum delay. The actual delay calculates based on retries counter and `delay` value. The formula is `delay + retries * rand(10)` seconds. Randomization is used to avoid retry storms.
94
+ * `delay` - the delay between retries in seconds
95
+ * `limit` - the number of retries. The number of attempts includes the original job execution.
96
+ * `jitter` - represents the upper bound of possible wait time (expressed as a percentage) and defaults to 0.15 (15%)
97
+ * `queue` - the queue to which the job will be enqueued if it fails
98
+
99
+ The `delay` is not the exact time between retries, but a minimum delay. The actual delay calculates based on jitter and `delay` value. The formula is `delay + delay * jitter * rand` seconds. Randomization is used to avoid retry storms.
93
100
 
94
101
  The default values are:
95
102
  - `delay`: 60 seconds
96
103
  - `limit`: 5 retries
104
+ - `jitter`: 0.15
97
105
 
98
- Delay and limit can be configured globally:
106
+ Delay, limit and jitter can be configured globally:
99
107
 
100
108
  ```ruby
101
109
  Sidekiq::Rescue.configure do |config|
102
110
  config.delay = 65
103
111
  config.limit = 10
112
+ config.jitter = 0.2
104
113
  end
105
114
  ```
106
115
 
@@ -7,12 +7,14 @@ module Sidekiq
7
7
  class Config
8
8
  DEFAULTS = {
9
9
  delay: 60,
10
- limit: 10
10
+ limit: 10,
11
+ jitter: 0.15
11
12
  }.freeze
12
13
 
13
14
  def initialize
14
15
  @delay = DEFAULTS[:delay]
15
16
  @limit = DEFAULTS[:limit]
17
+ @jitter = DEFAULTS[:jitter]
16
18
  @logger = Sidekiq.logger
17
19
  end
18
20
 
@@ -45,6 +47,22 @@ module Sidekiq
45
47
  @limit = limit
46
48
  end
47
49
 
50
+ # The jitter for the delay.
51
+ # @return [Integer, Float]
52
+ attr_reader :jitter
53
+
54
+ # @param jitter [Integer, Float] The jitter for the delay.
55
+ # @return [void]
56
+ # @raise [ArgumentError] if jitter is not an Integer or Float
57
+ def jitter=(jitter)
58
+ case jitter
59
+ when Integer, Float
60
+ @jitter = jitter
61
+ else
62
+ raise ArgumentError, "jitter must be Integer or Float"
63
+ end
64
+ end
65
+
48
66
  # The logger instance.
49
67
  # @return [Logger]
50
68
  # @note The default logger is Sidekiq's logger.
@@ -22,13 +22,20 @@ module Sidekiq
22
22
  # @raise [ArgumentError] if error is not an array of StandardError
23
23
  # @raise [ArgumentError] if delay is not an Integer or Float
24
24
  # @raise [ArgumentError] if limit is not an Integer
25
+ # @raise [ArgumentError] if jitter is not an Integer or Float
26
+ # @raise [ArgumentError] if queue is not a String
25
27
  # @example
26
28
  # sidekiq_rescue NetworkError, delay: 60, limit: 10
27
- def sidekiq_rescue(*errors, delay: Sidekiq::Rescue.config.delay, limit: Sidekiq::Rescue.config.limit)
29
+ def sidekiq_rescue(*errors, delay: Sidekiq::Rescue.config.delay, limit: Sidekiq::Rescue.config.limit,
30
+ jitter: Sidekiq::Rescue.config.jitter, queue: nil)
28
31
  unpacked_errors = validate_and_unpack_error_argument(errors)
29
32
  validate_delay_argument(delay)
30
33
  validate_limit_argument(limit)
31
- assign_sidekiq_rescue_options(unpacked_errors, delay, limit)
34
+ validate_jitter_argument(jitter)
35
+ validate_queue_argument(queue)
36
+ assign_sidekiq_rescue_options(
37
+ errors: unpacked_errors, delay: delay, limit: limit, jitter: jitter, queue: queue
38
+ )
32
39
  end
33
40
 
34
41
  # Find the error group and options for the given exception.
@@ -51,7 +58,6 @@ module Sidekiq
51
58
  end
52
59
 
53
60
  def validate_delay_argument(delay)
54
- return if delay.nil?
55
61
  return if delay.is_a?(Integer) || delay.is_a?(Float)
56
62
 
57
63
  if delay.is_a?(Proc)
@@ -68,9 +74,25 @@ module Sidekiq
68
74
  raise ArgumentError, "limit must be integer" if limit && !limit.is_a?(Integer)
69
75
  end
70
76
 
71
- def assign_sidekiq_rescue_options(errors, delay, limit)
77
+ def validate_jitter_argument(jitter)
78
+ return if jitter.is_a?(Integer) || jitter.is_a?(Float)
79
+
80
+ raise ArgumentError,
81
+ "jitter must be integer or float"
82
+ end
83
+
84
+ def validate_queue_argument(queue)
85
+ return if queue.nil? || queue.is_a?(String)
86
+
87
+ raise ArgumentError,
88
+ "queue must be a string"
89
+ end
90
+
91
+ def assign_sidekiq_rescue_options(errors:, delay:, limit:, jitter:, queue:)
72
92
  self.sidekiq_rescue_options ||= {}
73
- self.sidekiq_rescue_options = self.sidekiq_rescue_options.merge(errors => { delay: delay, limit: limit })
93
+ self.sidekiq_rescue_options = self.sidekiq_rescue_options.merge(errors => {
94
+ delay: delay, limit: limit, jitter: jitter, queue: queue
95
+ }.compact)
74
96
  end
75
97
  end
76
98
  end
@@ -15,6 +15,8 @@ module Sidekiq
15
15
  str = "expected #{actual} to be rescueable with #{expected}"
16
16
  str += " and delay #{@delay}" if @delay
17
17
  str += " and limit #{@limit}" if @limit
18
+ str += " and jitter #{@jitter}" if @jitter
19
+ str += " and queue #{@queue}" if @queue
18
20
  str
19
21
  end
20
22
  failure_message_when_negated { |actual| "expected #{actual} not to be rescueable with #{expected}" }
@@ -27,6 +29,14 @@ module Sidekiq
27
29
  @limit = limit
28
30
  end
29
31
 
32
+ chain :with_jitter do |jitter|
33
+ @jitter = jitter
34
+ end
35
+
36
+ chain :with_queue do |queue|
37
+ @queue = queue
38
+ end
39
+
30
40
  match do |actual|
31
41
  matched = actual.is_a?(Class) &&
32
42
  actual.include?(Sidekiq::Rescue::Dsl) &&
@@ -38,12 +48,16 @@ module Sidekiq
38
48
  _error_group, options = actual.sidekiq_rescue_error_group_with_options_by(expected.new)
39
49
 
40
50
  (@delay.nil? || options.fetch(:delay) == @delay) &&
41
- (@limit.nil? || options.fetch(:limit) == @limit)
51
+ (@limit.nil? || options.fetch(:limit) == @limit) &&
52
+ (@jitter.nil? || options.fetch(:jitter) == @jitter) &&
53
+ (@queue.nil? || options.fetch(:queue) == @queue)
42
54
  end
43
55
 
44
56
  match_when_negated do |actual|
45
57
  raise NotImplementedError, "it's confusing to use `not_to be_rescueable` with `with_delay`" if @delay
46
58
  raise NotImplementedError, "it's confusing to use `not_to be_rescueable` with `with_limit`" if @limit
59
+ raise NotImplementedError, "it's confusing to use `not_to be_rescueable` with `with_jitter`" if @jitter
60
+ raise NotImplementedError, "it's confusing to use `not_to be_rescueable` with `with_queue`" if @queue
47
61
 
48
62
  actual.is_a?(Class) &&
49
63
  actual.include?(Sidekiq::Rescue::Dsl) &&
@@ -30,14 +30,16 @@ module Sidekiq
30
30
  end
31
31
 
32
32
  def rescue_error(error, error_group, options, job_payload)
33
- delay, limit = options.fetch_values(:delay, :limit)
33
+ delay, limit, jitter = options.fetch_values(:delay, :limit, :jitter)
34
+ queue = options.fetch(:queue, job_payload["queue"])
35
+
34
36
  rescue_counter = increment_rescue_counter_for(error_group, job_payload)
35
37
  raise error if rescue_counter > limit
36
38
 
37
- reschedule_at = calculate_reschedule_time(delay, rescue_counter)
38
- log_reschedule_info(rescue_counter, error, reschedule_at)
39
- reschedule_job(job_payload: job_payload, reschedule_at: reschedule_at, rescue_counter: rescue_counter,
40
- error_group: error_group)
39
+ calculated_delay = calculate_delay(delay, rescue_counter, jitter)
40
+ log_reschedule_info(rescue_counter, error, calculated_delay)
41
+ reschedule_job(job_payload: job_payload, delay: calculated_delay, rescue_counter: rescue_counter,
42
+ error_group: error_group, queue: queue)
41
43
  end
42
44
 
43
45
  def increment_rescue_counter_for(error_group, job_payload)
@@ -46,23 +48,28 @@ module Sidekiq
46
48
  rescue_counter
47
49
  end
48
50
 
49
- def calculate_reschedule_time(delay, rescue_counter)
50
- # NOTE: we use the retry counter to increase the jitter
51
- # so that the jobs don't retry at the same time
52
- # inspired by sidekiq https://github.com/sidekiq/sidekiq/blob/73c150d0430a8394cadb5cd49218895b113613a0/lib/sidekiq/job_retry.rb#L188
53
- jitter = rand(10) * rescue_counter
51
+ def calculate_delay(delay, rescue_counter, jitter)
54
52
  delay = delay.call(rescue_counter) if delay.is_a?(Proc)
55
- Time.now.to_f + delay + jitter
53
+ jitter_delay = calculate_delay_jitter(jitter, delay)
54
+ delay + jitter_delay
55
+ end
56
+
57
+ def calculate_delay_jitter(jitter, delay)
58
+ return 0.0 if jitter.zero?
59
+
60
+ jitter * Kernel.rand * delay
56
61
  end
57
62
 
58
- def log_reschedule_info(rescue_counter, error, reschedule_at)
63
+ def log_reschedule_info(rescue_counter, error, delay)
59
64
  Sidekiq::Rescue.logger.info("[sidekiq_rescue] Job failed #{rescue_counter} times with error: " \
60
- "#{error.message}; rescheduling at #{reschedule_at}")
65
+ "#{error.message}; rescheduling in #{delay} seconds")
61
66
  end
62
67
 
63
- def reschedule_job(job_payload:, reschedule_at:, rescue_counter:, error_group:)
64
- payload = job_payload.merge("at" => reschedule_at,
65
- "sidekiq_rescue_exceptions_counter" => { error_group.to_s => rescue_counter })
68
+ def reschedule_job(job_payload:, delay:, rescue_counter:, error_group:, queue:)
69
+ payload = job_payload.dup
70
+ payload["at"] = Time.now.to_f + delay if delay.positive?
71
+ payload["sidekiq_rescue_exceptions_counter"] = { error_group.to_s => rescue_counter }
72
+ payload["queue"] = queue
66
73
  Sidekiq::Client.push(payload)
67
74
  end
68
75
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Rescue
5
- VERSION = "0.3.1"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-rescue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitrii Ivliev
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-05-30 00:00:00.000000000 Z
10
+ date: 2024-10-17 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: sidekiq
@@ -24,7 +23,6 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '6.5'
27
- description:
28
26
  email:
29
27
  - mail@ivda.dev
30
28
  executables: []
@@ -50,9 +48,8 @@ metadata:
50
48
  homepage_uri: https://github.com/moofkit/sidekiq-rescue
51
49
  source_code_uri: https://github.com/moofkit/sidekiq-rescue
52
50
  changelog_uri: https://github.com/moofkit/sidekiq-rescue/blob/master/CHANGELOG.md
53
- documentation_uri: https://rubydoc.info/gems/sidekiq-rescue/0.3.1
51
+ documentation_uri: https://rubydoc.info/gems/sidekiq-rescue/0.5.0
54
52
  rubygems_mfa_required: 'true'
55
- post_install_message:
56
53
  rdoc_options: []
57
54
  require_paths:
58
55
  - lib
@@ -67,8 +64,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
64
  - !ruby/object:Gem::Version
68
65
  version: '0'
69
66
  requirements: []
70
- rubygems_version: 3.5.9
71
- signing_key:
67
+ rubygems_version: 3.6.0.dev
72
68
  specification_version: 4
73
69
  summary: Rescue Sidekiq jobs on expected error and reschedule them
74
70
  test_files: []