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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 289a1d7d4fe45b2fe49121a2f132c18a68eddb7f9ebaae0a034fc0868bfd316c
4
- data.tar.gz: 0dadd128377a6ffed0cd0ea1eecb68734aeac75eaca3ef01ba6e8d6403ab71b9
3
+ metadata.gz: a0600c2766fa5f18cddf3e2d1287d416798eb18a71a157b822b1857165e01dbb
4
+ data.tar.gz: db7a3b1b6974ea377786afc0f13fc1a146096e47a123a39c52234b9c027cef5e
5
5
  SHA512:
6
- metadata.gz: ac758a0803e01ddd0e3475bbde13a59b5925d8e71ffecec5465ed323211d8d0f2f26d4ed08ace8c143526da40d8b2e9616d2bf57b25b6a8120285792dcdadc18
7
- data.tar.gz: a7e1f56022346513d9b1db113903326ff5ba8c57b37820a22f2574d72fb608646b29b7de7d7debfe19a590b616e7423ba57b4191512d9279bdd8933dcfaf2208
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
  [![Tests](https://github.com/philiprehberger/rb-retry-queue/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-retry-queue/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/philiprehberger-retry_queue.svg)](https://rubygems.org/gems/philiprehberger-retry_queue)
5
- [![License](https://img.shields.io/github/license/philiprehberger/rb-retry-queue)](LICENSE)
5
+ [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-retry-queue)](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
- ### Dead Letter Inspection
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.failed.each do |entry|
56
- puts "Item: #{entry[:item]}, Error: #{entry[:error].message}, Attempts: #{entry[:attempts]}"
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
- def initialize(max_retries: 3, concurrency: 1, backoff: nil)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module RetryQueue
5
- VERSION = '0.1.2'
5
+ VERSION = '0.2.1'
6
6
  end
7
7
  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(max_retries: max_retries, concurrency: concurrency, backoff: backoff)
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.2
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-25 00:00:00.000000000 Z
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