philiprehberger-retry_queue 0.3.0 → 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: a5c95df7fb98070ed959408d5fce1be2d1b920a800221d831f24214ffe9425d1
4
- data.tar.gz: d681958fb8eddf3710283315eb52ff81cc13bec03d67c0e72d1199ec5025ec9e
3
+ metadata.gz: 84632c19e4b14130651e69107803a2b6ac6e7369cd44da66a1eaa7f4dd4af5bc
4
+ data.tar.gz: c68647a250798bb8640a67cb8a78fe531be97b1694b10c1a7ac8b9bbebb9f488
5
5
  SHA512:
6
- metadata.gz: 4b91743778fba30ed468fe23da3157c4df3086a81c5aa1784f5d304a967967ad271e7dd9646a54d75a54379b5123f8589614249c346fa760ccc05111f7e6bdcc
7
- data.tar.gz: e63ef43720e3915d2942c06fd34e33ca79aabe147b3fc678db7812759c0501e6b660e80e4ae37afcc59ab031f8250e04371766888cd873e2edd7f1dc1f9ae0bf
6
+ metadata.gz: 0b08ca27ce50965c960de23dd07b8c645603fd7fa0dcbfff7fb6094d6f09a0d51f19be649f8135c507cf08d826064c0bd286e014c75889984adbebf40bd6f56b
7
+ data.tar.gz: e33e336e614769dcd56bd7833ac2d5074ce46a795f995ef886c0284ab9ffaa91acc08d419853c669bb313bc984a6d073d896701be84df8ad8d1a06bf6484fc12
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-04-23
11
+
12
+ ### Added
13
+ - Optional `jitter:` (0.0..1.0) on `RetryQueue.process` — multiplies backoff delay by `1 + rand * jitter` to reduce thundering-herd risk.
14
+ - `Result#empty?` and `Result#size` convenience methods.
15
+ - YARD clarification that `max_retries: 0` means one attempt, no retries.
16
+
17
+ ## [0.4.0] - 2026-04-19
18
+
19
+ ### Added
20
+ - `Result#success_rate` — ratio of succeeded to total items as a Float in `[0.0, 1.0]`; returns `0.0` for empty batches
21
+
10
22
  ## [0.3.0] - 2026-04-17
11
23
 
12
24
  ### Added
data/README.md CHANGED
@@ -45,6 +45,20 @@ result = Philiprehberger::RetryQueue.process(items, max_retries: 5, backoff: ->(
45
45
  end
46
46
  ```
47
47
 
48
+ ### Jitter
49
+
50
+ Reduce thundering-herd risk by randomizing the backoff delay. Pass a fraction in
51
+ `0.0..1.0`; the computed delay is multiplied by `1 + rand * jitter`. A value of
52
+ `0.0` (default) disables jitter.
53
+
54
+ ```ruby
55
+ result = Philiprehberger::RetryQueue.process(items, max_retries: 3, jitter: 0.3) do |item|
56
+ external_api_call(item)
57
+ end
58
+ ```
59
+
60
+ Values outside `0.0..1.0` or non-Numeric values raise `ArgumentError`.
61
+
48
62
  ### Selective Retry
49
63
 
50
64
  ```ruby
@@ -107,15 +121,27 @@ stats = result.stats
107
121
  # => { total: 100, succeeded: 97, failed: 3, success_rate: 0.97, elapsed: 1.23 }
108
122
  ```
109
123
 
124
+ ### Success rate
125
+
126
+ ```ruby
127
+ result = Philiprehberger::RetryQueue.process(items, max_retries: 3) { |item| call(item) }
128
+ result.success_rate # => 0.92
129
+ ```
130
+
110
131
  ## API
111
132
 
112
133
  | Method | Description |
113
134
  |--------|-------------|
114
- | `.process(items, max_retries:, concurrency:, backoff:, retry_on:, on_retry:, on_failure:) { \|item\| }` | Process items with retry logic |
135
+ | `.process(items, max_retries:, concurrency:, backoff:, retry_on:, on_retry:, on_failure:, jitter:) { \|item\| }` | Process items with retry logic |
136
+ | `max_retries:` | Integer >= 0. `0` means one attempt with no retries (not zero attempts) |
137
+ | `jitter:` | Numeric in `0.0..1.0`; multiplies backoff delay by `1 + rand * jitter`. Defaults to `0.0` |
115
138
  | `on_failure:` | Callable `(item, error)` invoked once per item that exhausts retries; hook errors are swallowed |
116
139
  | `Result#succeeded` | Array of successfully processed items |
117
140
  | `Result#failed` | Array of hashes with `:item`, `:error`, `:attempts` |
118
141
  | `Result#stats` | Hash with `:total`, `:succeeded`, `:failed`, `:success_rate`, `:elapsed` |
142
+ | `Result#success_rate` | Float in `[0.0, 1.0]`; ratio of succeeded to total items (`0.0` for empty batches) |
143
+ | `Result#empty?` | `true` when `stats[:total]` is 0 |
144
+ | `Result#size` | Returns `stats[:total]` (succeeded + failed count) |
119
145
  | `Result#reprocess_failed { \|item, error\| }` | Reprocess failed items, returns a new Result |
120
146
 
121
147
  ## Development
@@ -7,7 +7,8 @@ module Philiprehberger
7
7
  # Default backoff strategy: exponential with base 0.1s.
8
8
  DEFAULT_BACKOFF = ->(attempt) { 0.1 * (2**attempt) }
9
9
 
10
- # @param max_retries [Integer] maximum retry attempts per item
10
+ # @param max_retries [Integer] maximum retry attempts per item. `max_retries: 0` means
11
+ # one attempt with no retries (not zero attempts).
11
12
  # @param concurrency [Integer] number of concurrent workers (reserved for future use)
12
13
  # @param backoff [Proc, nil] proc receiving attempt number, returns sleep duration
13
14
  # @param retry_on [Array<Class>, nil] exception classes to retry on; nil means retry all
@@ -15,8 +16,12 @@ module Philiprehberger
15
16
  # @param on_failure [Proc, nil] callable invoked with `(item, error)` once per item that
16
17
  # exhausts retries and moves to the dead-letter list; exceptions raised by the hook are
17
18
  # swallowed so a faulty hook cannot break the queue
18
- def initialize(max_retries: 3, concurrency: 1, backoff: nil, retry_on: nil, on_retry: nil, on_failure: nil)
19
+ # @param jitter [Numeric] fraction in `0.0..1.0` applied to the computed backoff delay as
20
+ # `delay * (1 + rand * jitter)`. Defaults to `0.0` (no jitter).
21
+ def initialize(max_retries: 3, concurrency: 1, backoff: nil, retry_on: nil, on_retry: nil, on_failure: nil,
22
+ jitter: 0.0)
19
23
  raise Error, 'max_retries must be non-negative' unless max_retries.is_a?(Integer) && max_retries >= 0
24
+ raise ArgumentError, 'jitter must be a Numeric in 0.0..1.0' unless valid_jitter?(jitter)
20
25
 
21
26
  @max_retries = max_retries
22
27
  @concurrency = concurrency
@@ -24,6 +29,7 @@ module Philiprehberger
24
29
  @retry_on = retry_on
25
30
  @on_retry_hooks = Array(on_retry)
26
31
  @on_failure = on_failure
32
+ @jitter = jitter.to_f
27
33
  end
28
34
 
29
35
  # Process a collection of items with retry logic.
@@ -64,11 +70,21 @@ module Philiprehberger
64
70
 
65
71
  fire_on_retry_hooks(item, e, attempts)
66
72
 
67
- sleep_duration = @backoff.call(attempts - 1)
73
+ sleep_duration = apply_jitter(@backoff.call(attempts - 1))
68
74
  sleep(sleep_duration) if sleep_duration.positive?
69
75
  end
70
76
  end
71
77
 
78
+ def apply_jitter(delay)
79
+ return delay if @jitter.zero?
80
+
81
+ delay * (1 + (rand * @jitter))
82
+ end
83
+
84
+ def valid_jitter?(jitter)
85
+ jitter.is_a?(Numeric) && jitter >= 0.0 && jitter <= 1.0
86
+ end
87
+
72
88
  def retryable?(error)
73
89
  return true if @retry_on.nil?
74
90
 
@@ -28,11 +28,37 @@ module Philiprehberger
28
28
  total: total,
29
29
  succeeded: @succeeded.size,
30
30
  failed: @failed.size,
31
- success_rate: total.zero? ? 0.0 : @succeeded.size.to_f / total,
31
+ success_rate: success_rate,
32
32
  elapsed: @elapsed
33
33
  }
34
34
  end
35
35
 
36
+ # Whether the batch processed zero items.
37
+ #
38
+ # @return [Boolean] true when no items were processed (succeeded and failed both empty)
39
+ def empty?
40
+ stats[:total].zero?
41
+ end
42
+
43
+ # Total number of items processed, including both succeeded and failed.
44
+ #
45
+ # @return [Integer] `stats[:total]`
46
+ def size
47
+ stats[:total]
48
+ end
49
+
50
+ # Ratio of succeeded items to total processed items.
51
+ #
52
+ # Returns 0.0 for an empty batch to avoid ZeroDivisionError.
53
+ #
54
+ # @return [Float] ratio in the range [0.0, 1.0]
55
+ def success_rate
56
+ total = @succeeded.size + @failed.size
57
+ return 0.0 if total.zero?
58
+
59
+ @succeeded.size.to_f / total
60
+ end
61
+
36
62
  # Reprocess failed items by yielding each item and its last error to the block.
37
63
  # Returns a new Result with the reprocessing outcomes.
38
64
  #
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module RetryQueue
5
- VERSION = '0.3.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
@@ -11,7 +11,9 @@ module Philiprehberger
11
11
  # Process items with per-item retry, backoff, and dead letter collection.
12
12
  #
13
13
  # @param items [Array] items to process
14
- # @param max_retries [Integer] maximum retry attempts per item
14
+ # @param max_retries [Integer] maximum retry attempts per item. Note: `max_retries: 0`
15
+ # means one attempt with no retries (not zero attempts); the item is processed once
16
+ # and moves straight to the dead-letter list on failure.
15
17
  # @param concurrency [Integer] number of concurrent workers
16
18
  # @param backoff [Proc, nil] custom backoff strategy
17
19
  # @param retry_on [Array<Class>, nil] exception classes to retry on; others go straight to failed
@@ -19,17 +21,20 @@ module Philiprehberger
19
21
  # @param on_failure [Proc, nil] callable invoked with `(item, error)` once per item that
20
22
  # exhausts retries and moves to the dead-letter list; exceptions raised by the hook are
21
23
  # swallowed so a faulty hook cannot break the queue
24
+ # @param jitter [Numeric] fraction in `0.0..1.0` applied to the computed backoff delay as
25
+ # `delay * (1 + rand * jitter)` to reduce thundering-herd risk. Defaults to `0.0` (no jitter).
22
26
  # @yield [item] block that processes a single item
23
27
  # @return [Result] processing result
24
28
  def self.process(items, max_retries: 3, concurrency: 1, backoff: nil, retry_on: nil, on_retry: nil,
25
- on_failure: nil, &block)
29
+ on_failure: nil, jitter: 0.0, &block)
26
30
  processor = Processor.new(
27
31
  max_retries: max_retries,
28
32
  concurrency: concurrency,
29
33
  backoff: backoff,
30
34
  retry_on: retry_on,
31
35
  on_retry: on_retry,
32
- on_failure: on_failure
36
+ on_failure: on_failure,
37
+ jitter: jitter
33
38
  )
34
39
  processor.call(items, &block)
35
40
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-retry_queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-18 00:00:00.000000000 Z
11
+ date: 2026-04-23 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Processes collections of items with configurable per-item retry logic,
14
14
  exponential backoff, and dead letter collection for failed items. Returns detailed