philiprehberger-retry_queue 0.2.1 → 0.4.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 +10 -0
- data/README.md +24 -1
- data/lib/philiprehberger/retry_queue/processor.rb +16 -1
- data/lib/philiprehberger/retry_queue/result.rb +13 -1
- data/lib/philiprehberger/retry_queue/version.rb +1 -1
- data/lib/philiprehberger/retry_queue.rb +7 -2
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 933efd4881279b847d651e774a565205b246ae3859b624cc2715373549664d58
|
|
4
|
+
data.tar.gz: c24e1b713702a4775248ca23b4e88d08dab9b2e077c0f78ca866ad08ec50a629
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d17e2e333cbf2cf723ea0bbb2519186a24487fc5fe1fc4410302791ba211292b8092f9d84120a713951d433a3dfb1a237d051a7f5ebe6d31cd47c3f102b93462
|
|
7
|
+
data.tar.gz: 408fb3f24b0928ecf0438a5773c5fafc6404b73139cecbfe470235bcb5476741197bb402054e8b96b1e480b74f3ac19d6564f569db9bfa0c57e4d7e512c22f72
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-04-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Result#success_rate` — ratio of succeeded to total items as a Float in `[0.0, 1.0]`; returns `0.0` for empty batches
|
|
14
|
+
|
|
15
|
+
## [0.3.0] - 2026-04-17
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `on_failure:` callback for `process` and `Processor`, invoked with `(item, error)` when an item exhausts retries; hook exceptions are swallowed
|
|
19
|
+
|
|
10
20
|
## [0.2.1] - 2026-03-31
|
|
11
21
|
|
|
12
22
|
### Changed
|
data/README.md
CHANGED
|
@@ -67,6 +67,20 @@ result = Philiprehberger::RetryQueue.process(items, max_retries: 3, on_retry: [l
|
|
|
67
67
|
end
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
### Dead-letter Notifications
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
on_failure = ->(item, error) { Rails.logger.error("Dead-lettered #{item}: #{error.message}") }
|
|
74
|
+
|
|
75
|
+
result = Philiprehberger::RetryQueue.process(items, max_retries: 3, on_failure: on_failure) do |item|
|
|
76
|
+
process_item(item)
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The hook fires once per item that exhausts its retries, just as the item is recorded in
|
|
81
|
+
`Result#failed`. Exceptions raised inside the hook are swallowed so a faulty callback cannot
|
|
82
|
+
break the queue.
|
|
83
|
+
|
|
70
84
|
### DLQ Reprocessing
|
|
71
85
|
|
|
72
86
|
```ruby
|
|
@@ -93,14 +107,23 @@ stats = result.stats
|
|
|
93
107
|
# => { total: 100, succeeded: 97, failed: 3, success_rate: 0.97, elapsed: 1.23 }
|
|
94
108
|
```
|
|
95
109
|
|
|
110
|
+
### Success rate
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
result = Philiprehberger::RetryQueue.process(items, max_retries: 3) { |item| call(item) }
|
|
114
|
+
result.success_rate # => 0.92
|
|
115
|
+
```
|
|
116
|
+
|
|
96
117
|
## API
|
|
97
118
|
|
|
98
119
|
| Method | Description |
|
|
99
120
|
|--------|-------------|
|
|
100
|
-
| `.process(items, max_retries:, concurrency:, backoff:, retry_on:, on_retry:) { \|item\| }` | Process items with retry logic |
|
|
121
|
+
| `.process(items, max_retries:, concurrency:, backoff:, retry_on:, on_retry:, on_failure:) { \|item\| }` | Process items with retry logic |
|
|
122
|
+
| `on_failure:` | Callable `(item, error)` invoked once per item that exhausts retries; hook errors are swallowed |
|
|
101
123
|
| `Result#succeeded` | Array of successfully processed items |
|
|
102
124
|
| `Result#failed` | Array of hashes with `:item`, `:error`, `:attempts` |
|
|
103
125
|
| `Result#stats` | Hash with `:total`, `:succeeded`, `:failed`, `:success_rate`, `:elapsed` |
|
|
126
|
+
| `Result#success_rate` | Float in `[0.0, 1.0]`; ratio of succeeded to total items (`0.0` for empty batches) |
|
|
104
127
|
| `Result#reprocess_failed { \|item, error\| }` | Reprocess failed items, returns a new Result |
|
|
105
128
|
|
|
106
129
|
## Development
|
|
@@ -12,7 +12,10 @@ module Philiprehberger
|
|
|
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
14
|
# @param on_retry [Array<Proc>, Proc, nil] callbacks fired before each retry attempt
|
|
15
|
-
|
|
15
|
+
# @param on_failure [Proc, nil] callable invoked with `(item, error)` once per item that
|
|
16
|
+
# exhausts retries and moves to the dead-letter list; exceptions raised by the hook are
|
|
17
|
+
# 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)
|
|
16
19
|
raise Error, 'max_retries must be non-negative' unless max_retries.is_a?(Integer) && max_retries >= 0
|
|
17
20
|
|
|
18
21
|
@max_retries = max_retries
|
|
@@ -20,6 +23,7 @@ module Philiprehberger
|
|
|
20
23
|
@backoff = backoff || DEFAULT_BACKOFF
|
|
21
24
|
@retry_on = retry_on
|
|
22
25
|
@on_retry_hooks = Array(on_retry)
|
|
26
|
+
@on_failure = on_failure
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
# Process a collection of items with retry logic.
|
|
@@ -54,6 +58,7 @@ module Philiprehberger
|
|
|
54
58
|
rescue StandardError => e
|
|
55
59
|
if !retryable?(e) || attempts > @max_retries
|
|
56
60
|
failed << { item: item, error: e, attempts: attempts }
|
|
61
|
+
fire_on_failure_hook(item, e)
|
|
57
62
|
return
|
|
58
63
|
end
|
|
59
64
|
|
|
@@ -74,6 +79,16 @@ module Philiprehberger
|
|
|
74
79
|
@on_retry_hooks.each { |hook| hook.call(item, error, attempt) }
|
|
75
80
|
end
|
|
76
81
|
|
|
82
|
+
def fire_on_failure_hook(item, error)
|
|
83
|
+
return if @on_failure.nil?
|
|
84
|
+
|
|
85
|
+
@on_failure.call(item, error)
|
|
86
|
+
rescue StandardError
|
|
87
|
+
# Swallow hook errors — this is a best-effort notification and must never
|
|
88
|
+
# break the queue. Intentionally silent; users can log inside their hook.
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
77
92
|
def now
|
|
78
93
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
79
94
|
end
|
|
@@ -28,11 +28,23 @@ 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
|
+
# Ratio of succeeded items to total processed items.
|
|
37
|
+
#
|
|
38
|
+
# Returns 0.0 for an empty batch to avoid ZeroDivisionError.
|
|
39
|
+
#
|
|
40
|
+
# @return [Float] ratio in the range [0.0, 1.0]
|
|
41
|
+
def success_rate
|
|
42
|
+
total = @succeeded.size + @failed.size
|
|
43
|
+
return 0.0 if total.zero?
|
|
44
|
+
|
|
45
|
+
@succeeded.size.to_f / total
|
|
46
|
+
end
|
|
47
|
+
|
|
36
48
|
# Reprocess failed items by yielding each item and its last error to the block.
|
|
37
49
|
# Returns a new Result with the reprocessing outcomes.
|
|
38
50
|
#
|
|
@@ -16,15 +16,20 @@ module Philiprehberger
|
|
|
16
16
|
# @param backoff [Proc, nil] custom backoff strategy
|
|
17
17
|
# @param retry_on [Array<Class>, nil] exception classes to retry on; others go straight to failed
|
|
18
18
|
# @param on_retry [Array<Proc>, nil] callbacks fired before each retry attempt
|
|
19
|
+
# @param on_failure [Proc, nil] callable invoked with `(item, error)` once per item that
|
|
20
|
+
# exhausts retries and moves to the dead-letter list; exceptions raised by the hook are
|
|
21
|
+
# swallowed so a faulty hook cannot break the queue
|
|
19
22
|
# @yield [item] block that processes a single item
|
|
20
23
|
# @return [Result] processing result
|
|
21
|
-
def self.process(items, max_retries: 3, concurrency: 1, backoff: nil, retry_on: nil, on_retry: nil,
|
|
24
|
+
def self.process(items, max_retries: 3, concurrency: 1, backoff: nil, retry_on: nil, on_retry: nil,
|
|
25
|
+
on_failure: nil, &block)
|
|
22
26
|
processor = Processor.new(
|
|
23
27
|
max_retries: max_retries,
|
|
24
28
|
concurrency: concurrency,
|
|
25
29
|
backoff: backoff,
|
|
26
30
|
retry_on: retry_on,
|
|
27
|
-
on_retry: on_retry
|
|
31
|
+
on_retry: on_retry,
|
|
32
|
+
on_failure: on_failure
|
|
28
33
|
)
|
|
29
34
|
processor.call(items, &block)
|
|
30
35
|
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.4.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-
|
|
11
|
+
date: 2026-04-20 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
|
|
@@ -26,11 +26,11 @@ files:
|
|
|
26
26
|
- lib/philiprehberger/retry_queue/processor.rb
|
|
27
27
|
- lib/philiprehberger/retry_queue/result.rb
|
|
28
28
|
- lib/philiprehberger/retry_queue/version.rb
|
|
29
|
-
homepage: https://
|
|
29
|
+
homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-retry_queue
|
|
30
30
|
licenses:
|
|
31
31
|
- MIT
|
|
32
32
|
metadata:
|
|
33
|
-
homepage_uri: https://
|
|
33
|
+
homepage_uri: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-retry_queue
|
|
34
34
|
source_code_uri: https://github.com/philiprehberger/rb-retry-queue
|
|
35
35
|
changelog_uri: https://github.com/philiprehberger/rb-retry-queue/blob/main/CHANGELOG.md
|
|
36
36
|
bug_tracker_uri: https://github.com/philiprehberger/rb-retry-queue/issues
|