sneakers-retry 0.1.5
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/lib/sneakers-retry.rb +1 -0
- data/lib/sneakers-retry/handlers/maxretry2.rb +206 -0
- metadata +45 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a88e547379620874a8f3dff5b12858419003a17d
|
4
|
+
data.tar.gz: 6cf05f66a5c12b1c7d232192c0f6fd8e16d85236
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 12c77a3b7fc742398803b349064d2db53017b2b98a3389c971dab1a388330ad50a8410f6ccc83c193442d5641637d6f41463d86a72877aff4352eb15e051bd47
|
7
|
+
data.tar.gz: 1e05ab1a2eb137560f3ed43ea81be33132ec2be704dd4c9ead94abb6d6c3dc09297e63942f17d944271f76e1ccf79bc49658c5221c70e8c16b453aa3f9c0c4f3
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'sneakers-retry/handlers/maxretry2.rb'
|
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module SneakersRetry
|
4
|
+
module Handlers
|
5
|
+
#
|
6
|
+
# Maxretry uses dead letter policies on Rabbitmq to requeue and retry
|
7
|
+
# messages after failure (rejections, errors and timeouts). 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 Maxretry2
|
34
|
+
|
35
|
+
def initialize(channel, queue, opts)
|
36
|
+
puts "################################"
|
37
|
+
@worker_queue_name = queue.name
|
38
|
+
Sneakers.logger.debug do
|
39
|
+
"#{log_prefix} creating handler, opts=#{opts}"
|
40
|
+
end
|
41
|
+
|
42
|
+
@channel = channel
|
43
|
+
@opts = opts
|
44
|
+
|
45
|
+
# Construct names, defaulting where suitable
|
46
|
+
retry_name = @opts[:retry_exchange] || "#{@worker_queue_name}-retry"
|
47
|
+
error_name = @opts[:retry_error_exchange] || "#{@worker_queue_name}-error"
|
48
|
+
requeue_name = @opts[:retry_requeue_exchange] || "#{@worker_queue_name}-retry-requeue"
|
49
|
+
retry_routing_key = @opts[:retry_routing_key] || "#"
|
50
|
+
|
51
|
+
# Create the exchanges
|
52
|
+
@retry_exchange, @error_exchange, @requeue_exchange = [retry_name, error_name, requeue_name].map do |name|
|
53
|
+
Sneakers.logger.debug { "#{log_prefix} creating exchange=#{name}" }
|
54
|
+
@channel.exchange(name,
|
55
|
+
:type => 'topic',
|
56
|
+
:durable => exchange_durable?)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Create the queues and bindings
|
60
|
+
Sneakers.logger.debug do
|
61
|
+
"#{log_prefix} creating queue=#{retry_name} x-dead-letter-exchange=#{requeue_name}"
|
62
|
+
end
|
63
|
+
@retry_queue = @channel.queue(retry_name,
|
64
|
+
:durable => queue_durable?,
|
65
|
+
:arguments => {
|
66
|
+
:'x-dead-letter-exchange' => requeue_name,
|
67
|
+
:'x-message-ttl' => @opts[:retry_timeout] || 60000
|
68
|
+
})
|
69
|
+
@retry_queue.bind(@retry_exchange, :routing_key => '#')
|
70
|
+
|
71
|
+
Sneakers.logger.debug do
|
72
|
+
"#{log_prefix} creating queue=#{error_name}"
|
73
|
+
end
|
74
|
+
@error_queue = @channel.queue(error_name,
|
75
|
+
:durable => queue_durable?)
|
76
|
+
@error_queue.bind(@error_exchange, :routing_key => '#')
|
77
|
+
|
78
|
+
# Finally, bind the worker queue to our requeue exchange
|
79
|
+
queue.bind(@requeue_exchange, :routing_key => retry_routing_key)
|
80
|
+
|
81
|
+
@max_retries = @opts[:retry_max_times] || 5
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
def acknowledge(hdr, props, msg)
|
86
|
+
@channel.acknowledge(hdr.delivery_tag, false)
|
87
|
+
end
|
88
|
+
|
89
|
+
def reject(hdr, props, msg, requeue = false)
|
90
|
+
if requeue
|
91
|
+
# This was explicitly rejected specifying it be requeued so we do not
|
92
|
+
# want it to pass through our retry logic.
|
93
|
+
@channel.reject(hdr.delivery_tag, requeue)
|
94
|
+
else
|
95
|
+
handle_retry(hdr, props, msg, :reject)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def error(hdr, props, msg, err)
|
101
|
+
handle_retry(hdr, props, msg, err)
|
102
|
+
end
|
103
|
+
|
104
|
+
def timeout(hdr, props, msg)
|
105
|
+
handle_retry(hdr, props, msg, :timeout)
|
106
|
+
end
|
107
|
+
|
108
|
+
def noop(hdr, props, msg)
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
# Helper logic for retry handling. This will reject the message if there
|
113
|
+
# are remaining retries left on it, otherwise it will publish it to the
|
114
|
+
# error exchange along with the reason.
|
115
|
+
# @param hdr [Bunny::DeliveryInfo]
|
116
|
+
# @param props [Bunny::MessageProperties]
|
117
|
+
# @param msg [String] The message
|
118
|
+
# @param reason [String, Symbol, Exception] Reason for the retry, included
|
119
|
+
# in the JSON we put on the error exchange.
|
120
|
+
def handle_retry(hdr, props, msg, reason)
|
121
|
+
# +1 for the current attempt
|
122
|
+
num_attempts = failure_count(props[:headers]) + 1
|
123
|
+
if num_attempts <= @max_retries
|
124
|
+
# We call reject which will route the message to the
|
125
|
+
# x-dead-letter-exchange (ie. retry exchange) on the queue
|
126
|
+
Sneakers.logger.info do
|
127
|
+
"#{log_prefix} msg=retrying, count=#{num_attempts}, headers=#{props[:headers]}"
|
128
|
+
end
|
129
|
+
@channel.reject(hdr.delivery_tag, false)
|
130
|
+
# TODO: metrics
|
131
|
+
else
|
132
|
+
# Retried more than the max times
|
133
|
+
# Publish the original message with the routing_key to the error exchange
|
134
|
+
Sneakers.logger.info do
|
135
|
+
"#{log_prefix} msg=failing, retry_count=#{num_attempts}, reason=#{reason}"
|
136
|
+
end
|
137
|
+
data = {
|
138
|
+
error: reason.to_s,
|
139
|
+
num_attempts: num_attempts,
|
140
|
+
failed_at: Time.now.iso8601,
|
141
|
+
properties: props.to_hash
|
142
|
+
}.tap do |hash|
|
143
|
+
if reason.is_a?(Exception)
|
144
|
+
hash[:error_class] = reason.class.to_s
|
145
|
+
hash[:error_message] = "#{reason}"
|
146
|
+
if reason.backtrace
|
147
|
+
hash[:backtrace] = reason.backtrace.take(10)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end.to_json
|
151
|
+
@error_exchange.publish(msg, {
|
152
|
+
routing_key: hdr.routing_key,
|
153
|
+
headers: {
|
154
|
+
retry_info: data
|
155
|
+
}
|
156
|
+
})
|
157
|
+
@channel.acknowledge(hdr.delivery_tag, false)
|
158
|
+
# TODO: metrics
|
159
|
+
end
|
160
|
+
end
|
161
|
+
private :handle_retry
|
162
|
+
|
163
|
+
# Uses the x-death header to determine the number of failures this job has
|
164
|
+
# seen in the past. This does not count the current failure. So for
|
165
|
+
# instance, the first time the job fails, this will return 0, the second
|
166
|
+
# time, 1, etc.
|
167
|
+
# @param headers [Hash] Hash of headers that Rabbit delivers as part of
|
168
|
+
# the message
|
169
|
+
# @return [Integer] Count of number of failures.
|
170
|
+
def failure_count(headers)
|
171
|
+
if headers.nil? || headers['x-death'].nil?
|
172
|
+
0
|
173
|
+
else
|
174
|
+
x_death_array = headers['x-death'].select do |x_death|
|
175
|
+
x_death['queue'] == @worker_queue_name
|
176
|
+
end
|
177
|
+
if x_death_array.count > 0 && x_death_array.first['count']
|
178
|
+
# Newer versions of RabbitMQ return headers with a count key
|
179
|
+
x_death_array.inject(0) {|sum, x_death| sum + x_death['count']}
|
180
|
+
else
|
181
|
+
# Older versions return a separate x-death header for each failure
|
182
|
+
x_death_array.count
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
private :failure_count
|
187
|
+
|
188
|
+
# Prefix all of our log messages so they are easier to find. We don't have
|
189
|
+
# the worker, so the next best thing is the queue name.
|
190
|
+
def log_prefix
|
191
|
+
"Maxretry handler [queue=#{@worker_queue_name}]"
|
192
|
+
end
|
193
|
+
private :log_prefix
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def queue_durable?
|
198
|
+
@opts.fetch(:queue_options, {}).fetch(:durable, false)
|
199
|
+
end
|
200
|
+
|
201
|
+
def exchange_durable?
|
202
|
+
queue_durable?
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
metadata
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sneakers-retry
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eetay Natan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-08-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A simple hello world gem
|
14
|
+
email: eetay2@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/sneakers-retry.rb
|
20
|
+
- lib/sneakers-retry/handlers/maxretry2.rb
|
21
|
+
homepage: https://github.com/eetay/sneakers-retry
|
22
|
+
licenses:
|
23
|
+
- MIT
|
24
|
+
metadata: {}
|
25
|
+
post_install_message:
|
26
|
+
rdoc_options: []
|
27
|
+
require_paths:
|
28
|
+
- lib
|
29
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
requirements: []
|
40
|
+
rubyforge_project:
|
41
|
+
rubygems_version: 2.6.11
|
42
|
+
signing_key:
|
43
|
+
specification_version: 4
|
44
|
+
summary: Retry handler for sneakers
|
45
|
+
test_files: []
|