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 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: []