philiprehberger-retry_queue 0.1.2 → 0.2.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/CHANGELOG.md +12 -0
- data/README.md +50 -6
- data/lib/philiprehberger/retry_queue/processor.rb +18 -2
- data/lib/philiprehberger/retry_queue/result.rb +23 -0
- data/lib/philiprehberger/retry_queue/version.rb +1 -1
- data/lib/philiprehberger/retry_queue.rb +10 -2
- 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: a0600c2766fa5f18cddf3e2d1287d416798eb18a71a157b822b1857165e01dbb
|
|
4
|
+
data.tar.gz: db7a3b1b6974ea377786afc0f13fc1a146096e47a123a39c52234b9c027cef5e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90c3abf8b7263c80b1ca4537bd3012edd6fbe3c21355cc827288be506bb2d06d0201897d83c24fe038d74fc26b4bf136ac07cf05f9e3565885ad77dbbf931742
|
|
7
|
+
data.tar.gz: a9561a49edbfaa6c1df8b08c9310cb3074de873f60e225426f57d70e40493c9f3f3d78e1711ce13e9475958fb6570e6476bbc7add8317f9c31f0c8b3a79ed7ff
|
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.2.1] - 2026-03-31
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Standardize README badges, support section, and license format
|
|
14
|
+
|
|
15
|
+
## [0.2.0] - 2026-03-29
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Selective retry via `retry_on:` parameter to only retry specific exception classes
|
|
19
|
+
- Retry hooks via `on_retry:` parameter with callbacks fired before each retry attempt
|
|
20
|
+
- DLQ reprocessing via `Result#reprocess_failed` to iterate over failed items with their last error
|
|
21
|
+
|
|
10
22
|
## [0.1.2] - 2026-03-24
|
|
11
23
|
|
|
12
24
|
### Fixed
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/philiprehberger/rb-retry-queue/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/philiprehberger-retry_queue)
|
|
5
|
-
[](https://github.com/philiprehberger/rb-retry-queue/commits/main)
|
|
6
6
|
|
|
7
7
|
Batch processor with per-item retry, backoff, and dead letter collection
|
|
8
8
|
|
|
@@ -45,16 +45,41 @@ result = Philiprehberger::RetryQueue.process(items, max_retries: 5, backoff: ->(
|
|
|
45
45
|
end
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
###
|
|
48
|
+
### Selective Retry
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
result = Philiprehberger::RetryQueue.process(items, max_retries: 3, retry_on: [Net::OpenTimeout, Timeout::Error]) do |item|
|
|
52
|
+
api_call(item)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Only Net::OpenTimeout and Timeout::Error trigger retries
|
|
56
|
+
# All other errors send the item straight to failed
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Retry Hooks
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
logger_hook = ->(item, error, attempt) { puts "Retrying #{item}: #{error.message} (attempt #{attempt})" }
|
|
63
|
+
metrics_hook = ->(item, _error, _attempt) { increment_counter("retry.#{item}") }
|
|
64
|
+
|
|
65
|
+
result = Philiprehberger::RetryQueue.process(items, max_retries: 3, on_retry: [logger_hook, metrics_hook]) do |item|
|
|
66
|
+
process_item(item)
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### DLQ Reprocessing
|
|
49
71
|
|
|
50
72
|
```ruby
|
|
51
73
|
result = Philiprehberger::RetryQueue.process(jobs, max_retries: 2) do |job|
|
|
52
74
|
job.execute!
|
|
53
75
|
end
|
|
54
76
|
|
|
55
|
-
result.
|
|
56
|
-
|
|
77
|
+
reprocessed = result.reprocess_failed do |item, error|
|
|
78
|
+
fallback_handler(item, error)
|
|
57
79
|
end
|
|
80
|
+
|
|
81
|
+
puts reprocessed.succeeded.size # => items recovered during reprocessing
|
|
82
|
+
puts reprocessed.failed.size # => items that failed reprocessing too
|
|
58
83
|
```
|
|
59
84
|
|
|
60
85
|
### Statistics
|
|
@@ -72,10 +97,11 @@ stats = result.stats
|
|
|
72
97
|
|
|
73
98
|
| Method | Description |
|
|
74
99
|
|--------|-------------|
|
|
75
|
-
| `.process(items, max_retries:, concurrency:, backoff:) { \|item\| }` | Process items with retry logic |
|
|
100
|
+
| `.process(items, max_retries:, concurrency:, backoff:, retry_on:, on_retry:) { \|item\| }` | Process items with retry logic |
|
|
76
101
|
| `Result#succeeded` | Array of successfully processed items |
|
|
77
102
|
| `Result#failed` | Array of hashes with `:item`, `:error`, `:attempts` |
|
|
78
103
|
| `Result#stats` | Hash with `:total`, `:succeeded`, `:failed`, `:success_rate`, `:elapsed` |
|
|
104
|
+
| `Result#reprocess_failed { \|item, error\| }` | Reprocess failed items, returns a new Result |
|
|
79
105
|
|
|
80
106
|
## Development
|
|
81
107
|
|
|
@@ -85,6 +111,24 @@ bundle exec rspec
|
|
|
85
111
|
bundle exec rubocop
|
|
86
112
|
```
|
|
87
113
|
|
|
114
|
+
## Support
|
|
115
|
+
|
|
116
|
+
If you find this project useful:
|
|
117
|
+
|
|
118
|
+
⭐ [Star the repo](https://github.com/philiprehberger/rb-retry-queue)
|
|
119
|
+
|
|
120
|
+
🐛 [Report issues](https://github.com/philiprehberger/rb-retry-queue/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
|
121
|
+
|
|
122
|
+
💡 [Suggest features](https://github.com/philiprehberger/rb-retry-queue/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
|
|
123
|
+
|
|
124
|
+
❤️ [Sponsor development](https://github.com/sponsors/philiprehberger)
|
|
125
|
+
|
|
126
|
+
🌐 [All Open Source Projects](https://philiprehberger.com/open-source-packages)
|
|
127
|
+
|
|
128
|
+
💻 [GitHub Profile](https://github.com/philiprehberger)
|
|
129
|
+
|
|
130
|
+
🔗 [LinkedIn Profile](https://www.linkedin.com/in/philiprehberger)
|
|
131
|
+
|
|
88
132
|
## License
|
|
89
133
|
|
|
90
|
-
MIT
|
|
134
|
+
[MIT](LICENSE)
|
|
@@ -10,12 +10,16 @@ module Philiprehberger
|
|
|
10
10
|
# @param max_retries [Integer] maximum retry attempts per item
|
|
11
11
|
# @param concurrency [Integer] number of concurrent workers (reserved for future use)
|
|
12
12
|
# @param backoff [Proc, nil] proc receiving attempt number, returns sleep duration
|
|
13
|
-
|
|
13
|
+
# @param retry_on [Array<Class>, nil] exception classes to retry on; nil means retry all
|
|
14
|
+
# @param on_retry [Array<Proc>, Proc, nil] callbacks fired before each retry attempt
|
|
15
|
+
def initialize(max_retries: 3, concurrency: 1, backoff: nil, retry_on: nil, on_retry: nil)
|
|
14
16
|
raise Error, 'max_retries must be non-negative' unless max_retries.is_a?(Integer) && max_retries >= 0
|
|
15
17
|
|
|
16
18
|
@max_retries = max_retries
|
|
17
19
|
@concurrency = concurrency
|
|
18
20
|
@backoff = backoff || DEFAULT_BACKOFF
|
|
21
|
+
@retry_on = retry_on
|
|
22
|
+
@on_retry_hooks = Array(on_retry)
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
# Process a collection of items with retry logic.
|
|
@@ -48,16 +52,28 @@ module Philiprehberger
|
|
|
48
52
|
succeeded << item
|
|
49
53
|
return
|
|
50
54
|
rescue StandardError => e
|
|
51
|
-
if attempts > @max_retries
|
|
55
|
+
if !retryable?(e) || attempts > @max_retries
|
|
52
56
|
failed << { item: item, error: e, attempts: attempts }
|
|
53
57
|
return
|
|
54
58
|
end
|
|
55
59
|
|
|
60
|
+
fire_on_retry_hooks(item, e, attempts)
|
|
61
|
+
|
|
56
62
|
sleep_duration = @backoff.call(attempts - 1)
|
|
57
63
|
sleep(sleep_duration) if sleep_duration.positive?
|
|
58
64
|
end
|
|
59
65
|
end
|
|
60
66
|
|
|
67
|
+
def retryable?(error)
|
|
68
|
+
return true if @retry_on.nil?
|
|
69
|
+
|
|
70
|
+
@retry_on.any? { |klass| error.is_a?(klass) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def fire_on_retry_hooks(item, error, attempt)
|
|
74
|
+
@on_retry_hooks.each { |hook| hook.call(item, error, attempt) }
|
|
75
|
+
end
|
|
76
|
+
|
|
61
77
|
def now
|
|
62
78
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
63
79
|
end
|
|
@@ -32,6 +32,29 @@ module Philiprehberger
|
|
|
32
32
|
elapsed: @elapsed
|
|
33
33
|
}
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
# Reprocess failed items by yielding each item and its last error to the block.
|
|
37
|
+
# Returns a new Result with the reprocessing outcomes.
|
|
38
|
+
#
|
|
39
|
+
# @yield [item, error] block that reprocesses a failed item
|
|
40
|
+
# @return [Result] new result with reprocessing outcomes
|
|
41
|
+
def reprocess_failed(&block)
|
|
42
|
+
raise Error, 'a reprocessing block is required' unless block
|
|
43
|
+
|
|
44
|
+
reprocess_succeeded = []
|
|
45
|
+
reprocess_failed = []
|
|
46
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
47
|
+
|
|
48
|
+
@failed.each do |entry|
|
|
49
|
+
block.call(entry[:item], entry[:error])
|
|
50
|
+
reprocess_succeeded << entry[:item]
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
reprocess_failed << { item: entry[:item], error: e, attempts: 1 }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
56
|
+
Result.new(succeeded: reprocess_succeeded, failed: reprocess_failed, elapsed: elapsed)
|
|
57
|
+
end
|
|
35
58
|
end
|
|
36
59
|
end
|
|
37
60
|
end
|
|
@@ -14,10 +14,18 @@ module Philiprehberger
|
|
|
14
14
|
# @param max_retries [Integer] maximum retry attempts per item
|
|
15
15
|
# @param concurrency [Integer] number of concurrent workers
|
|
16
16
|
# @param backoff [Proc, nil] custom backoff strategy
|
|
17
|
+
# @param retry_on [Array<Class>, nil] exception classes to retry on; others go straight to failed
|
|
18
|
+
# @param on_retry [Array<Proc>, nil] callbacks fired before each retry attempt
|
|
17
19
|
# @yield [item] block that processes a single item
|
|
18
20
|
# @return [Result] processing result
|
|
19
|
-
def self.process(items, max_retries: 3, concurrency: 1, backoff: nil, &block)
|
|
20
|
-
processor = Processor.new(
|
|
21
|
+
def self.process(items, max_retries: 3, concurrency: 1, backoff: nil, retry_on: nil, on_retry: nil, &block)
|
|
22
|
+
processor = Processor.new(
|
|
23
|
+
max_retries: max_retries,
|
|
24
|
+
concurrency: concurrency,
|
|
25
|
+
backoff: backoff,
|
|
26
|
+
retry_on: retry_on,
|
|
27
|
+
on_retry: on_retry
|
|
28
|
+
)
|
|
21
29
|
processor.call(items, &block)
|
|
22
30
|
end
|
|
23
31
|
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.1
|
|
4
|
+
version: 0.2.1
|
|
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-03-
|
|
11
|
+
date: 2026-03-31 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
|