sneakers_max_retry_handler 0.1.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 +7 -0
- data/.gitignore +8 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/README.md +35 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/sneakers_max_retry_handler/maxretry.rb +216 -0
- data/lib/sneakers_max_retry_handler/version.rb +3 -0
- data/lib/sneakers_max_retry_handler.rb +1 -0
- data/sneakers_max_retry_handler.gemspec +22 -0
- metadata +92 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 11c75e850f1367f8a75940dba6f75c3652c92f2d4cbfde8ebe89a60dc671660a
|
4
|
+
data.tar.gz: 7642b2c40070abc4207de8fbbc8831d654cef6a7d484762f65ee383bf9de433d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '080715a56d2c622df989e2f292d4c462927a7fe749624c159889b09138b9f077326512ccb0aed36b42a124bd895a4b69008ad11a999c80bd6d5303cb1f85841d'
|
7
|
+
data.tar.gz: bda03c7af5ff41b9beefd68d3e9b45bdb04340b1c20ed81e1537079c1f12cd163bfba3f5d9af88c80481d7d1038826313cc9b0dab14ad8ece1751bfcd24f0d2d
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 KlimSem
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Sneakers-Max-Retry-Handler
|
2
|
+
|
3
|
+
A modified max retry handler for RabbitMQ and [Sneakers](https://github.com/ruby-amqp/kicks/)
|
4
|
+
|
5
|
+
A small modification of Sneaker's built-in [Maxretry class](https://github.com/ruby-amqp/kicks/blob/main/lib/sneakers/handlers/maxretry.rb)
|
6
|
+
the replacement class is called ```SneakersMaxRetryHandler::Maxretry```
|
7
|
+
has the exact same logic, but you can pass arguments for handler created queues
|
8
|
+
|
9
|
+
|
10
|
+
# Installation
|
11
|
+
|
12
|
+
Include it in your Gemfile.
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'sneakers_max_retry_handler'
|
16
|
+
```
|
17
|
+
|
18
|
+
Next install it with Bundler.
|
19
|
+
|
20
|
+
```bash
|
21
|
+
$ bundle install
|
22
|
+
```
|
23
|
+
|
24
|
+
|
25
|
+
# Use:
|
26
|
+
|
27
|
+
initialize your worker with the Maxretry handler:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class MyWorker
|
31
|
+
from_queue 'audit_service', {
|
32
|
+
handler: SneakersMaxRetryHandler::Maxretry,
|
33
|
+
}
|
34
|
+
end
|
35
|
+
```
|
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "sneakers_max_retry_handler"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
require "irb"
|
11
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module SneakersMaxRetryHandler
|
5
|
+
#
|
6
|
+
# Maxretry uses dead letter policies on Rabbitmq to requeue and retry
|
7
|
+
# messages after failure (rejections and errors). When the maximum
|
8
|
+
# number of retries is reached it will put the message on an error queue.
|
9
|
+
# This handler will only retry at the queue level. To accomplish that, the
|
10
|
+
# setup is a bit complex.
|
11
|
+
#
|
12
|
+
# Input:
|
13
|
+
# worker_exchange (eXchange)
|
14
|
+
# worker_queue (Queue)
|
15
|
+
# We create:
|
16
|
+
# worker_queue-retry - (X) where we setup the worker queue to dead-letter.
|
17
|
+
# worker_queue-retry - (Q) queue bound to ^ exchange, dead-letters to
|
18
|
+
# worker_queue-retry-requeue.
|
19
|
+
# worker_queue-error - (X) where to send max-retry failures
|
20
|
+
# worker_queue-error - (Q) bound to worker_queue-error.
|
21
|
+
# worker_queue-retry-requeue - (X) exchange to bind worker_queue to for
|
22
|
+
# requeuing directly to the worker_queue.
|
23
|
+
#
|
24
|
+
# This requires that you setup arguments to the worker queue to line up the
|
25
|
+
# dead letter queue. See the example for more information.
|
26
|
+
#
|
27
|
+
# Many of these can be override with options:
|
28
|
+
# - retry_exchange - sets retry exchange & queue
|
29
|
+
# - retry_error_exchange - sets error exchange and queue
|
30
|
+
# - retry_requeue_exchange - sets the exchange created to re-queue things
|
31
|
+
# back to the worker queue.
|
32
|
+
#
|
33
|
+
class Maxretry
|
34
|
+
|
35
|
+
def initialize(channel, queue, opts)
|
36
|
+
@worker_queue_name = queue.name
|
37
|
+
Sneakers.logger.debug do
|
38
|
+
"#{log_prefix} creating handler, opts=#{opts}"
|
39
|
+
end
|
40
|
+
|
41
|
+
@channel = channel
|
42
|
+
@opts = opts
|
43
|
+
|
44
|
+
# Construct names, defaulting where suitable
|
45
|
+
retry_name = @opts[:retry_exchange] || "#{@worker_queue_name}-retry"
|
46
|
+
error_name = @opts[:retry_error_exchange] || "#{@worker_queue_name}-error"
|
47
|
+
requeue_name = @opts[:retry_requeue_exchange] || "#{@worker_queue_name}-retry-requeue"
|
48
|
+
retry_routing_key = @opts[:retry_routing_key] || "#"
|
49
|
+
|
50
|
+
# Create the exchanges
|
51
|
+
@retry_exchange, @error_exchange, @requeue_exchange = [retry_name, error_name, requeue_name].map do |name|
|
52
|
+
Sneakers.logger.debug { "#{log_prefix} creating exchange=#{name}" }
|
53
|
+
@channel.exchange(name,
|
54
|
+
:type => 'topic',
|
55
|
+
:durable => exchange_durable?)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Create the queues and bindings
|
59
|
+
Sneakers.logger.debug do
|
60
|
+
"#{log_prefix} creating queue=#{retry_name} x-dead-letter-exchange=#{requeue_name}"
|
61
|
+
end
|
62
|
+
@retry_queue = @channel.queue(retry_name,
|
63
|
+
:durable => queue_durable?,
|
64
|
+
:arguments => {
|
65
|
+
:'x-dead-letter-exchange' => requeue_name,
|
66
|
+
:'x-message-ttl' => @opts[:retry_timeout] || 60000
|
67
|
+
})
|
68
|
+
@retry_queue.bind(@retry_exchange, :routing_key => '#')
|
69
|
+
|
70
|
+
Sneakers.logger.debug do
|
71
|
+
"#{log_prefix} creating queue=#{error_name}"
|
72
|
+
end
|
73
|
+
@error_queue = @channel.queue(error_name,
|
74
|
+
:durable => queue_durable?)
|
75
|
+
@error_queue.bind(@error_exchange, :routing_key => '#')
|
76
|
+
|
77
|
+
# Finally, bind the worker queue to our requeue exchange
|
78
|
+
queue.bind(@requeue_exchange, :routing_key => retry_routing_key)
|
79
|
+
|
80
|
+
@max_retries = @opts[:retry_max_times] || 5
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.configure_queue(name, opts)
|
84
|
+
retry_name = opts.fetch(:retry_exchange, "#{name}-retry")
|
85
|
+
opt_args = opts[:queue_options][:arguments] ? opts[:queue_options][:arguments].inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo} : {}
|
86
|
+
opts[:queue_options][:arguments] = { :'x-dead-letter-exchange' => retry_name }.merge(opt_args)
|
87
|
+
opts[:queue_options]
|
88
|
+
end
|
89
|
+
|
90
|
+
def acknowledge(hdr, props, msg)
|
91
|
+
@channel.acknowledge(hdr.delivery_tag, false)
|
92
|
+
end
|
93
|
+
|
94
|
+
def reject(hdr, props, msg, requeue = false)
|
95
|
+
if requeue
|
96
|
+
# This was explicitly rejected specifying it be requeued so we do not
|
97
|
+
# want it to pass through our retry logic.
|
98
|
+
@channel.reject(hdr.delivery_tag, requeue)
|
99
|
+
else
|
100
|
+
handle_retry(hdr, props, msg, :reject)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def error(hdr, props, msg, err)
|
106
|
+
handle_retry(hdr, props, msg, err)
|
107
|
+
end
|
108
|
+
|
109
|
+
def noop(hdr, props, msg)
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
# Helper logic for retry handling. This will reject the message if there
|
114
|
+
# are remaining retries left on it, otherwise it will publish it to the
|
115
|
+
# error exchange along with the reason.
|
116
|
+
# @param hdr [Bunny::DeliveryInfo]
|
117
|
+
# @param props [Bunny::MessageProperties]
|
118
|
+
# @param msg [String] The message
|
119
|
+
# @param reason [String, Symbol, Exception] Reason for the retry, included
|
120
|
+
# in the JSON we put on the error exchange.
|
121
|
+
def handle_retry(hdr, props, msg, reason)
|
122
|
+
# +1 for the current attempt
|
123
|
+
num_attempts = failure_count(props[:headers]) + 1
|
124
|
+
if num_attempts <= @max_retries
|
125
|
+
# We call reject which will route the message to the
|
126
|
+
# x-dead-letter-exchange (ie. retry exchange) on the queue
|
127
|
+
Sneakers.logger.info do
|
128
|
+
"#{log_prefix} msg=retrying, count=#{num_attempts}, headers=#{props[:headers]}"
|
129
|
+
end
|
130
|
+
@channel.reject(hdr.delivery_tag, false)
|
131
|
+
# TODO: metrics
|
132
|
+
else
|
133
|
+
# Retried more than the max times
|
134
|
+
# Publish the original message with the routing_key to the error exchange
|
135
|
+
Sneakers.logger.info do
|
136
|
+
"#{log_prefix} msg=failing, retry_count=#{num_attempts}, reason=#{reason}"
|
137
|
+
end
|
138
|
+
data = {
|
139
|
+
error: reason.to_s,
|
140
|
+
num_attempts: num_attempts,
|
141
|
+
failed_at: Time.now.iso8601,
|
142
|
+
properties: props.to_hash
|
143
|
+
}.tap do |hash|
|
144
|
+
if reason.is_a?(Exception)
|
145
|
+
hash[:error_class] = reason.class.to_s
|
146
|
+
hash[:error_message] = "#{reason}"
|
147
|
+
if reason.backtrace
|
148
|
+
hash[:backtrace] = reason.backtrace.take(10)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Preserve retry log in a list
|
154
|
+
if retry_info = props[:headers]['retry_info']
|
155
|
+
old_retry0 = JSON.parse(retry_info) rescue {error: "Failed to parse retry info"}
|
156
|
+
old_retry = Array(old_retry0)
|
157
|
+
# Prevent old retry from nesting
|
158
|
+
data[:properties][:headers].delete('retry_info')
|
159
|
+
data = old_retry.unshift(data)
|
160
|
+
end
|
161
|
+
|
162
|
+
@error_exchange.publish(msg, {
|
163
|
+
routing_key: hdr.routing_key,
|
164
|
+
headers: {
|
165
|
+
retry_info: data.to_json
|
166
|
+
}
|
167
|
+
})
|
168
|
+
@channel.acknowledge(hdr.delivery_tag, false)
|
169
|
+
# TODO: metrics
|
170
|
+
end
|
171
|
+
end
|
172
|
+
private :handle_retry
|
173
|
+
|
174
|
+
# Uses the x-death header to determine the number of failures this job has
|
175
|
+
# seen in the past. This does not count the current failure. So for
|
176
|
+
# instance, the first time the job fails, this will return 0, the second
|
177
|
+
# time, 1, etc.
|
178
|
+
# @param headers [Hash] Hash of headers that Rabbit delivers as part of
|
179
|
+
# the message
|
180
|
+
# @return [Integer] Count of number of failures.
|
181
|
+
def failure_count(headers)
|
182
|
+
if headers.nil? || headers['x-death'].nil?
|
183
|
+
0
|
184
|
+
else
|
185
|
+
x_death_array = headers['x-death'].select do |x_death|
|
186
|
+
x_death['queue'] == @worker_queue_name
|
187
|
+
end
|
188
|
+
if x_death_array.count > 0 && x_death_array.first['count']
|
189
|
+
# Newer versions of RabbitMQ return headers with a count key
|
190
|
+
x_death_array.inject(0) {|sum, x_death| sum + x_death['count']}
|
191
|
+
else
|
192
|
+
# Older versions return a separate x-death header for each failure
|
193
|
+
x_death_array.count
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
private :failure_count
|
198
|
+
|
199
|
+
# Prefix all of our log messages so they are easier to find. We don't have
|
200
|
+
# the worker, so the next best thing is the queue name.
|
201
|
+
def log_prefix
|
202
|
+
"Maxretry handler [queue=#{@worker_queue_name}]"
|
203
|
+
end
|
204
|
+
private :log_prefix
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
def queue_durable?
|
209
|
+
@opts.fetch(:queue_options, {}).fetch(:durable, false)
|
210
|
+
end
|
211
|
+
|
212
|
+
def exchange_durable?
|
213
|
+
queue_durable?
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'sneakers_max_retry_handler/maxretry.rb'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'sneakers_max_retry_handler/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "sneakers_max_retry_handler"
|
7
|
+
spec.version = SneakersMaxRetryHandler::VERSION
|
8
|
+
spec.authors = ["KlimSem"]
|
9
|
+
spec.email = ["klimsemikin@gmail.com"]
|
10
|
+
spec.licenses = ['MIT']
|
11
|
+
|
12
|
+
spec.summary = %q{Adds MaxRetry Handler to use with Kicks}
|
13
|
+
spec.description = %q{Adds MaxRetry handler which supports additional settings}
|
14
|
+
spec.homepage = "https://github.com/KlimSemikin/sneakers_max_retry_handler"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.require_paths = ["lib"]
|
18
|
+
|
19
|
+
spec.add_dependency "kicks", "~> 3.2.0"
|
20
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
21
|
+
spec.add_development_dependency "pry-byebug", "~> 3.5"
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sneakers_max_retry_handler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- KlimSem
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-08-29 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: kicks
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 3.2.0
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 3.2.0
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: bundler
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: pry-byebug
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.5'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.5'
|
54
|
+
description: Adds MaxRetry handler which supports additional settings
|
55
|
+
email:
|
56
|
+
- klimsemikin@gmail.com
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- ".gitignore"
|
62
|
+
- Gemfile
|
63
|
+
- LICENSE
|
64
|
+
- README.md
|
65
|
+
- bin/console
|
66
|
+
- bin/setup
|
67
|
+
- lib/sneakers_max_retry_handler.rb
|
68
|
+
- lib/sneakers_max_retry_handler/maxretry.rb
|
69
|
+
- lib/sneakers_max_retry_handler/version.rb
|
70
|
+
- sneakers_max_retry_handler.gemspec
|
71
|
+
homepage: https://github.com/KlimSemikin/sneakers_max_retry_handler
|
72
|
+
licenses:
|
73
|
+
- MIT
|
74
|
+
metadata: {}
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubygems_version: 3.6.6
|
90
|
+
specification_version: 4
|
91
|
+
summary: Adds MaxRetry Handler to use with Kicks
|
92
|
+
test_files: []
|