sneakers-retry 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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: []