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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +27 -1
- data/lib/philiprehberger/retry_queue/processor.rb +19 -3
- data/lib/philiprehberger/retry_queue/result.rb +27 -1
- data/lib/philiprehberger/retry_queue/version.rb +1 -1
- data/lib/philiprehberger/retry_queue.rb +8 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 84632c19e4b14130651e69107803a2b6ac6e7369cd44da66a1eaa7f4dd4af5bc
|
|
4
|
+
data.tar.gz: c68647a250798bb8640a67cb8a78fe531be97b1694b10c1a7ac8b9bbebb9f488
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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:
|
|
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
|
#
|
|
@@ -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.
|
|
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-
|
|
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
|