philiprehberger-retry_queue 0.1.2 → 0.2.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: 289a1d7d4fe45b2fe49121a2f132c18a68eddb7f9ebaae0a034fc0868bfd316c
4
- data.tar.gz: 0dadd128377a6ffed0cd0ea1eecb68734aeac75eaca3ef01ba6e8d6403ab71b9
3
+ metadata.gz: 22b3442db8a011863de798c26bd3d255b65cc355655bc98c7b32f5dc0d233a01
4
+ data.tar.gz: 73279504fb90ae3768d615e2e3e1db7df57151946320490856ccb62a98e7a2d3
5
5
  SHA512:
6
- metadata.gz: ac758a0803e01ddd0e3475bbde13a59b5925d8e71ffecec5465ed323211d8d0f2f26d4ed08ace8c143526da40d8b2e9616d2bf57b25b6a8120285792dcdadc18
7
- data.tar.gz: a7e1f56022346513d9b1db113903326ff5ba8c57b37820a22f2574d72fb608646b29b7de7d7debfe19a590b616e7423ba57b4191512d9279bdd8933dcfaf2208
6
+ metadata.gz: 106135b96e386b6fbda72eb74adba46d85d8e11eb233736a91b8dc2fec4304baa7ce6497f05553c7a9c649ca7a4641131fd1e2d48a072e013a9f2f6c5bbdbf4e
7
+ data.tar.gz: e478bba48e5c678502099b50db3f61714d3e5873bd5f3b244a0e47ff2a781570a703805449983df313bd4c6f3a3833790fe4e54203b579de7ab3279abbabe500
data/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-03-29
11
+
12
+ ### Added
13
+ - Selective retry via `retry_on:` parameter to only retry specific exception classes
14
+ - Retry hooks via `on_retry:` parameter with callbacks fired before each retry attempt
15
+ - DLQ reprocessing via `Result#reprocess_failed` to iterate over failed items with their last error
16
+
10
17
  ## [0.1.2] - 2026-03-24
11
18
 
12
19
  ### Fixed
data/README.md CHANGED
@@ -2,7 +2,12 @@
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
+ [![GitHub release](https://img.shields.io/github/v/release/philiprehberger/rb-retry-queue)](https://github.com/philiprehberger/rb-retry-queue/releases)
6
+ [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-retry-queue)](https://github.com/philiprehberger/rb-retry-queue/commits/main)
5
7
  [![License](https://img.shields.io/github/license/philiprehberger/rb-retry-queue)](LICENSE)
8
+ [![Bug Reports](https://img.shields.io/github/issues/philiprehberger/rb-retry-queue/bug)](https://github.com/philiprehberger/rb-retry-queue/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
9
+ [![Feature Requests](https://img.shields.io/github/issues/philiprehberger/rb-retry-queue/enhancement)](https://github.com/philiprehberger/rb-retry-queue/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
10
+ [![Sponsor](https://img.shields.io/badge/sponsor-GitHub%20Sponsors-ec6cb9)](https://github.com/sponsors/philiprehberger)
6
11
 
7
12
  Batch processor with per-item retry, backoff, and dead letter collection
8
13
 
@@ -45,16 +50,41 @@ result = Philiprehberger::RetryQueue.process(items, max_retries: 5, backoff: ->(
45
50
  end
46
51
  ```
47
52
 
48
- ### Dead Letter Inspection
53
+ ### Selective Retry
54
+
55
+ ```ruby
56
+ result = Philiprehberger::RetryQueue.process(items, max_retries: 3, retry_on: [Net::OpenTimeout, Timeout::Error]) do |item|
57
+ api_call(item)
58
+ end
59
+
60
+ # Only Net::OpenTimeout and Timeout::Error trigger retries
61
+ # All other errors send the item straight to failed
62
+ ```
63
+
64
+ ### Retry Hooks
65
+
66
+ ```ruby
67
+ logger_hook = ->(item, error, attempt) { puts "Retrying #{item}: #{error.message} (attempt #{attempt})" }
68
+ metrics_hook = ->(item, _error, _attempt) { increment_counter("retry.#{item}") }
69
+
70
+ result = Philiprehberger::RetryQueue.process(items, max_retries: 3, on_retry: [logger_hook, metrics_hook]) do |item|
71
+ process_item(item)
72
+ end
73
+ ```
74
+
75
+ ### DLQ Reprocessing
49
76
 
50
77
  ```ruby
51
78
  result = Philiprehberger::RetryQueue.process(jobs, max_retries: 2) do |job|
52
79
  job.execute!
53
80
  end
54
81
 
55
- result.failed.each do |entry|
56
- puts "Item: #{entry[:item]}, Error: #{entry[:error].message}, Attempts: #{entry[:attempts]}"
82
+ reprocessed = result.reprocess_failed do |item, error|
83
+ fallback_handler(item, error)
57
84
  end
85
+
86
+ puts reprocessed.succeeded.size # => items recovered during reprocessing
87
+ puts reprocessed.failed.size # => items that failed reprocessing too
58
88
  ```
59
89
 
60
90
  ### Statistics
@@ -72,10 +102,11 @@ stats = result.stats
72
102
 
73
103
  | Method | Description |
74
104
  |--------|-------------|
75
- | `.process(items, max_retries:, concurrency:, backoff:) { \|item\| }` | Process items with retry logic |
105
+ | `.process(items, max_retries:, concurrency:, backoff:, retry_on:, on_retry:) { \|item\| }` | Process items with retry logic |
76
106
  | `Result#succeeded` | Array of successfully processed items |
77
107
  | `Result#failed` | Array of hashes with `:item`, `:error`, `:attempts` |
78
108
  | `Result#stats` | Hash with `:total`, `:succeeded`, `:failed`, `:success_rate`, `:elapsed` |
109
+ | `Result#reprocess_failed { \|item, error\| }` | Reprocess failed items, returns a new Result |
79
110
 
80
111
  ## Development
81
112
 
@@ -85,6 +116,13 @@ bundle exec rspec
85
116
  bundle exec rubocop
86
117
  ```
87
118
 
119
+ ## Support
120
+
121
+ If you find this package useful, consider giving it a star on GitHub — it helps motivate continued maintenance and development.
122
+
123
+ [![LinkedIn](https://img.shields.io/badge/Philip%20Rehberger-LinkedIn-0A66C2?logo=linkedin)](https://www.linkedin.com/in/philiprehberger)
124
+ [![More packages](https://img.shields.io/badge/more-open%20source%20packages-blue)](https://philiprehberger.com/open-source-packages)
125
+
88
126
  ## License
89
127
 
90
- MIT
128
+ [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.0'
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.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-25 00:00:00.000000000 Z
11
+ date: 2026-03-30 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