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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0600c2766fa5f18cddf3e2d1287d416798eb18a71a157b822b1857165e01dbb
4
- data.tar.gz: db7a3b1b6974ea377786afc0f13fc1a146096e47a123a39c52234b9c027cef5e
3
+ metadata.gz: 933efd4881279b847d651e774a565205b246ae3859b624cc2715373549664d58
4
+ data.tar.gz: c24e1b713702a4775248ca23b4e88d08dab9b2e077c0f78ca866ad08ec50a629
5
5
  SHA512:
6
- metadata.gz: 90c3abf8b7263c80b1ca4537bd3012edd6fbe3c21355cc827288be506bb2d06d0201897d83c24fe038d74fc26b4bf136ac07cf05f9e3565885ad77dbbf931742
7
- data.tar.gz: a9561a49edbfaa6c1df8b08c9310cb3074de873f60e225426f57d70e40493c9f3f3d78e1711ce13e9475958fb6570e6476bbc7add8317f9c31f0c8b3a79ed7ff
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
- def initialize(max_retries: 3, concurrency: 1, backoff: nil, retry_on: nil, on_retry: nil)
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: 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
+ # 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
  #
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module RetryQueue
5
- VERSION = '0.2.1'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
@@ -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, &block)
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.2.1
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-03-31 00:00:00.000000000 Z
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://github.com/philiprehberger/rb-retry-queue
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://github.com/philiprehberger/rb-retry-queue
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