resque-retry 1.1.4 → 1.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 +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +11 -4
- data/HISTORY.md +6 -0
- data/README.md +185 -140
- data/lib/resque-retry/version.rb +1 -1
- data/lib/resque/failure/multiple_with_retry_suppression.rb +17 -1
- data/lib/resque/plugins/retry.rb +41 -12
- data/lib/resque/plugins/retry/logging.rb +23 -0
- data/resque-retry.gemspec +1 -1
- data/test/multiple_failure_test.rb +13 -2
- data/test/retry_test.rb +21 -1
- data/test/test_helper.rb +28 -0
- data/test/test_jobs.rb +26 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3424ef6139214b3f9ec367393e92e2da6ac888eb
|
4
|
+
data.tar.gz: 85ba61ab491cadba2783e34e3af7d05953e02f53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6424ef718b680bfd61cc7fa47ddc1c09ae397c5a9838fbeee01fa4af5ff79001c06052f982ab517aa8f2db73d7e1f99323ca41ad4df54d328897f104ea07bfaf
|
7
|
+
data.tar.gz: 840b6f3402ee1bf8e708ab78d6f7f376aac96f0ef843ff5d08c9668ed1771d69b6a51a819216be8f5611013e0cfdda9f30575ec1d330ec97af304f359298963e
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,9 +1,16 @@
|
|
1
|
-
# configuration settings for http://travis-ci.org
|
2
|
-
|
3
1
|
language: ruby
|
2
|
+
|
3
|
+
matrix:
|
4
|
+
allow_failures:
|
5
|
+
- rvm: jruby-19mode
|
6
|
+
- rvm: jruby-20mode
|
7
|
+
- rvm: rbx
|
8
|
+
|
4
9
|
rvm:
|
5
|
-
- 2.1.0
|
6
|
-
- 2.0.0
|
7
10
|
- 1.9.3
|
11
|
+
- 2.0.0
|
12
|
+
- 2.1.0
|
13
|
+
- 2.1.1
|
8
14
|
- jruby-19mode
|
15
|
+
- jruby-20mode
|
9
16
|
- rbx
|
data/HISTORY.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
## HEAD
|
2
2
|
|
3
|
+
## 1.2.0 (2014-05-19)
|
4
|
+
|
5
|
+
* Fixed scenario where job does not get retried correctly when `perform` is not called as expected.
|
6
|
+
* Feature: Optional `@expire_retry_key_after` settings; expires retry counters from redis to save you cleaning up stale state.
|
7
|
+
* Feature: Expose inner-workings of plugin through debug messages using `Resque.logger` (when logging level is Logger:DEBUG).
|
8
|
+
|
3
9
|
## 1.1.4 (2014-03-17)
|
4
10
|
|
5
11
|
* Fixed displaying retry information in resque web interface, caused by `Resque::Helpers` being deprecated.
|
data/README.md
CHANGED
@@ -26,35 +26,39 @@ If you're using [Bundler][bundler] to manage your dependencies, you should add `
|
|
26
26
|
'resque-retry'` to your projects `Gemfile`.
|
27
27
|
|
28
28
|
Add this to your `Rakefile`:
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
```ruby
|
30
|
+
require 'resque/tasks'
|
31
|
+
require 'resque_scheduler/tasks'
|
32
|
+
```
|
32
33
|
|
33
34
|
The delay between retry attempts is provided by [resque-scheduler][rqs].
|
34
35
|
You'll want to run the scheduler process, otherwise delayed retry attempts
|
35
36
|
will never perform:
|
36
|
-
|
37
|
-
|
37
|
+
```
|
38
|
+
$ rake resque:scheduler
|
39
|
+
```
|
38
40
|
|
39
41
|
Use the plugin:
|
42
|
+
```ruby
|
43
|
+
require 'resque-retry'
|
40
44
|
|
41
|
-
|
45
|
+
class ExampleRetryJob
|
46
|
+
extend Resque::Plugins::Retry
|
47
|
+
@queue = :example_queue
|
42
48
|
|
43
|
-
|
44
|
-
|
45
|
-
@queue = :example_queue
|
49
|
+
@retry_limit = 3
|
50
|
+
@retry_delay = 60
|
46
51
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
53
|
-
end
|
52
|
+
def self.perform(*args)
|
53
|
+
# your magic/heavy lifting goes here.
|
54
|
+
end
|
55
|
+
end
|
56
|
+
```
|
54
57
|
|
55
58
|
Then start up a resque worker as normal:
|
56
|
-
|
57
|
-
|
59
|
+
```
|
60
|
+
$ QUEUE=* rake resque:work
|
61
|
+
```
|
58
62
|
|
59
63
|
Now if you ExampleRetryJob fails, it will be retried 3 times, with a 60 second
|
60
64
|
delay between attempts.
|
@@ -79,14 +83,15 @@ actually completed successfully just by just using the resque-web interface.
|
|
79
83
|
`MultipleWithRetrySuppression` is a multiple failure backend, with retry suppression.
|
80
84
|
|
81
85
|
Here's an example, using the Redis failure backend:
|
86
|
+
```ruby
|
87
|
+
require 'resque-retry'
|
88
|
+
require 'resque/failure/redis'
|
82
89
|
|
83
|
-
|
84
|
-
require 'resque/failure/redis'
|
90
|
+
# require your jobs & application code.
|
85
91
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
|
92
|
+
Resque::Failure::MultipleWithRetrySuppression.classes = [Resque::Failure::Redis]
|
93
|
+
Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
|
94
|
+
```
|
90
95
|
|
91
96
|
If a job fails, but **can and will** retry, the failure details wont be
|
92
97
|
logged in the Redis failed queue *(visible via resque-web)*.
|
@@ -103,13 +108,14 @@ The new Retry tab displays delayed jobs with retry information; the number of
|
|
103
108
|
attempts and the exception details from the last failure.
|
104
109
|
|
105
110
|
Make sure you include this in your `config.ru` or similar file:
|
111
|
+
```ruby
|
112
|
+
require 'resque-retry'
|
113
|
+
require 'resque-retry/server'
|
106
114
|
|
107
|
-
|
108
|
-
require 'resque-retry/server'
|
109
|
-
|
110
|
-
# require your jobs & application code.
|
115
|
+
# require your jobs & application code.
|
111
116
|
|
112
|
-
|
117
|
+
run Resque::Server.new
|
118
|
+
```
|
113
119
|
|
114
120
|
Retry Options & Logic
|
115
121
|
---------------------
|
@@ -123,35 +129,37 @@ some ideas =), adapt for your own usage and feel free to pick and mix!
|
|
123
129
|
### Retry Defaults
|
124
130
|
|
125
131
|
Retry the job **once** on failure, with zero delay.
|
132
|
+
```ruby
|
133
|
+
require 'resque-retry'
|
126
134
|
|
127
|
-
|
135
|
+
class DeliverWebHook
|
136
|
+
extend Resque::Plugins::Retry
|
137
|
+
@queue = :web_hooks
|
128
138
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
heavy_lifting
|
135
|
-
end
|
136
|
-
end
|
139
|
+
def self.perform(url, hook_id, hmac_key)
|
140
|
+
heavy_lifting
|
141
|
+
end
|
142
|
+
end
|
143
|
+
```
|
137
144
|
|
138
145
|
When a job runs, the number of retry attempts is checked and incremented
|
139
146
|
in Redis. If your job fails, the number of retry attempts is used to
|
140
147
|
determine if we can requeue the job for another go.
|
141
148
|
|
142
149
|
### Custom Retry
|
150
|
+
```ruby
|
151
|
+
class DeliverWebHook
|
152
|
+
extend Resque::Plugins::Retry
|
153
|
+
@queue = :web_hooks
|
143
154
|
|
144
|
-
|
145
|
-
|
146
|
-
@queue = :web_hooks
|
147
|
-
|
148
|
-
@retry_limit = 10
|
149
|
-
@retry_delay = 120
|
155
|
+
@retry_limit = 10
|
156
|
+
@retry_delay = 120
|
150
157
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
158
|
+
def self.perform(url, hook_id, hmac_key)
|
159
|
+
heavy_lifting
|
160
|
+
end
|
161
|
+
end
|
162
|
+
```
|
155
163
|
|
156
164
|
The above modification will allow your job to retry up to 10 times, with
|
157
165
|
a delay of 120 seconds, or 2 minutes between retry attempts.
|
@@ -164,17 +172,18 @@ more special.
|
|
164
172
|
Sometimes it is useful to delay the worker that failed a job attempt, but
|
165
173
|
still requeue the job for immediate processing by other workers. This can be
|
166
174
|
done with `@sleep_after_requeue`:
|
175
|
+
```ruby
|
176
|
+
class DeliverWebHook
|
177
|
+
extend Resque::Plugins::Retry
|
178
|
+
@queue = :web_hooks
|
167
179
|
|
168
|
-
|
169
|
-
extend Resque::Plugins::Retry
|
170
|
-
@queue = :web_hooks
|
171
|
-
|
172
|
-
@sleep_after_requeue = 5
|
180
|
+
@sleep_after_requeue = 5
|
173
181
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
182
|
+
def self.perform(url, hook_id, hmac_key)
|
183
|
+
heavy_lifting
|
184
|
+
end
|
185
|
+
end
|
186
|
+
```
|
178
187
|
|
179
188
|
This retries the job once and causes the worker that failed to sleep for 5
|
180
189
|
seconds after requeuing the job. If there are multiple workers in the system
|
@@ -192,24 +201,26 @@ dynamically.
|
|
192
201
|
### Exponential Backoff
|
193
202
|
|
194
203
|
Use this if you wish to vary the delay between retry attempts:
|
204
|
+
```ruby
|
205
|
+
class DeliverSMS
|
206
|
+
extend Resque::Plugins::ExponentialBackoff
|
207
|
+
@queue = :mt_messages
|
195
208
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
heavy_lifting
|
202
|
-
end
|
203
|
-
end
|
209
|
+
def self.perform(mt_id, mobile_number, message)
|
210
|
+
heavy_lifting
|
211
|
+
end
|
212
|
+
end
|
213
|
+
```
|
204
214
|
|
205
215
|
**Default Settings**
|
216
|
+
```
|
217
|
+
key: m = minutes, h = hours
|
206
218
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
@retry_delay_multiplicand_max = 1.0
|
219
|
+
no delay, 1m, 10m, 1h, 3h, 6h
|
220
|
+
@backoff_strategy = [0, 60, 600, 3600, 10800, 21600]
|
221
|
+
@retry_delay_multiplicand_min = 1.0
|
222
|
+
@retry_delay_multiplicand_max = 1.0
|
223
|
+
```
|
213
224
|
|
214
225
|
The first delay will be 0 seconds, the 2nd will be 60 seconds, etc...
|
215
226
|
Again, tweak to your own needs.
|
@@ -228,17 +239,18 @@ them all retried on the same schedule.
|
|
228
239
|
|
229
240
|
The default will allow a retry for any type of exception. You may change
|
230
241
|
it so only specific exceptions are retried using `retry_exceptions`:
|
242
|
+
```ruby
|
243
|
+
class DeliverSMS
|
244
|
+
extend Resque::Plugins::Retry
|
245
|
+
@queue = :mt_messages
|
231
246
|
|
232
|
-
|
233
|
-
extend Resque::Plugins::Retry
|
234
|
-
@queue = :mt_messages
|
235
|
-
|
236
|
-
@retry_exceptions = [NetworkError]
|
247
|
+
@retry_exceptions = [NetworkError]
|
237
248
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
249
|
+
def self.perform(mt_id, mobile_number, message)
|
250
|
+
heavy_lifting
|
251
|
+
end
|
252
|
+
end
|
253
|
+
```
|
242
254
|
|
243
255
|
The above modification will **only** retry if a `NetworkError` (or subclass)
|
244
256
|
exception is thrown.
|
@@ -248,17 +260,18 @@ types. You may optionally set `@retry_exceptions` to a hash where the keys are
|
|
248
260
|
your specific exception classes to retry on, and the values are your retry
|
249
261
|
delays in seconds or an array of retry delays to be used similar to
|
250
262
|
exponential backoff.
|
263
|
+
```ruby
|
264
|
+
class DeliverSMS
|
265
|
+
extend Resque::Plugins::Retry
|
266
|
+
@queue = :mt_messages
|
251
267
|
|
252
|
-
|
253
|
-
extend Resque::Plugins::Retry
|
254
|
-
@queue = :mt_messages
|
268
|
+
@retry_exceptions = { NetworkError => 30, SystemCallError => [120, 240] }
|
255
269
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
end
|
270
|
+
def self.perform(mt_id, mobile_number, message)
|
271
|
+
heavy_lifting
|
272
|
+
end
|
273
|
+
end
|
274
|
+
```
|
262
275
|
|
263
276
|
In the above example, Resque would retry any `DeliverSMS` jobs which throw a
|
264
277
|
`NetworkError` or `SystemCallError`. If the job throws a `NetworkError` it
|
@@ -269,17 +282,18 @@ retry 120 seconds later then subsequent retry attempts 240 seconds later.
|
|
269
282
|
|
270
283
|
The default will allow a retry for any type of exception. You may change
|
271
284
|
it so specific exceptions fail immediately by using `fatal_exceptions`:
|
285
|
+
```ruby
|
286
|
+
class DeliverSMS
|
287
|
+
extend Resque::Plugins::Retry
|
288
|
+
@queue = :mt_divisions
|
272
289
|
|
273
|
-
|
274
|
-
extend Resque::Plugins::Retry
|
275
|
-
@queue = :mt_divisions
|
290
|
+
@fatal_exceptions = [NetworkError]
|
276
291
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
end
|
292
|
+
def self.perform(mt_id, mobile_number, message)
|
293
|
+
heavy_lifting
|
294
|
+
end
|
295
|
+
end
|
296
|
+
```
|
283
297
|
|
284
298
|
In the above example, Resque would retry any `DeliverSMS` jobs that throw any
|
285
299
|
type of error other than `NetworkError`. If the job throws a `NetworkError` it
|
@@ -288,25 +302,26 @@ will be marked as "failed" immediately.
|
|
288
302
|
### Custom Retry Criteria Check Callbacks
|
289
303
|
|
290
304
|
You may define custom retry criteria callbacks:
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
def self.perform(job_id)
|
307
|
-
heavy_lifting
|
308
|
-
end
|
305
|
+
```ruby
|
306
|
+
class TurkWorker
|
307
|
+
extend Resque::Plugins::Retry
|
308
|
+
@queue = :turk_job_processor
|
309
|
+
|
310
|
+
@retry_exceptions = [NetworkError]
|
311
|
+
|
312
|
+
retry_criteria_check do |exception, *args|
|
313
|
+
if exception.message =~ /InvalidJobId/
|
314
|
+
false # don't retry if we got passed a invalid job id.
|
315
|
+
else
|
316
|
+
true # its okay for a retry attempt to continue.
|
309
317
|
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def self.perform(job_id)
|
321
|
+
heavy_lifting
|
322
|
+
end
|
323
|
+
end
|
324
|
+
```
|
310
325
|
|
311
326
|
Similar to the previous example, this job will retry if either a
|
312
327
|
`NetworkError` (or subclass) exception is thrown **or** any of the callbacks
|
@@ -319,20 +334,21 @@ job should retry.
|
|
319
334
|
|
320
335
|
You may override `args_for_retry`, which is passed the current
|
321
336
|
job arguments, to modify the arguments for the next retry attempt.
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
337
|
+
```ruby
|
338
|
+
class DeliverViaSMSC
|
339
|
+
extend Resque::Plugins::Retry
|
340
|
+
@queue = :mt_smsc_messages
|
341
|
+
|
342
|
+
# retry using the emergency SMSC.
|
343
|
+
def self.args_for_retry(smsc_id, mt_message)
|
344
|
+
[999, mt_message]
|
345
|
+
end
|
346
|
+
|
347
|
+
self.perform(smsc_id, mt_message)
|
348
|
+
heavy_lifting
|
349
|
+
end
|
350
|
+
end
|
351
|
+
```
|
336
352
|
|
337
353
|
### Job Retry Identifier/Key
|
338
354
|
|
@@ -347,19 +363,48 @@ By default the key uses this format:
|
|
347
363
|
`resque-retry:<job class name>:<retry_identifier>`.
|
348
364
|
|
349
365
|
Or you can define the entire key by overriding `redis_retry_key`.
|
366
|
+
```ruby
|
367
|
+
class DeliverSMS
|
368
|
+
extend Resque::Plugins::Retry
|
369
|
+
@queue = :mt_messages
|
350
370
|
|
351
|
-
|
352
|
-
|
353
|
-
|
371
|
+
def self.retry_identifier(mt_id, mobile_number, message)
|
372
|
+
"#{mobile_number}:#{mt_id}"
|
373
|
+
end
|
354
374
|
|
355
|
-
|
356
|
-
|
357
|
-
|
375
|
+
self.perform(mt_id, mobile_number, message)
|
376
|
+
heavy_lifting
|
377
|
+
end
|
378
|
+
end
|
379
|
+
```
|
358
380
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
381
|
+
### Expire Retry Counters From Redis
|
382
|
+
|
383
|
+
Allow the Redis to expire stale retry counters from the database by setting
|
384
|
+
`@expire_retry_key_after`:
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
class DeliverSMS
|
388
|
+
extend Resque::Plugins::Retry
|
389
|
+
@queue = :mt_messages
|
390
|
+
@expire_retry_key_after = 3600 # expire key after `retry_delay` plus 1 hour
|
391
|
+
|
392
|
+
self.perform(mt_id, mobile_number, message)
|
393
|
+
heavy_lifting
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
```
|
398
|
+
|
399
|
+
This saves you from having to run a "house cleaning" or "errand" job.
|
400
|
+
|
401
|
+
The expiary timeout is "pushed forward" or "touched" after each failure to
|
402
|
+
ensure its not expired too soon.
|
403
|
+
|
404
|
+
### Debug Plugging Logging
|
405
|
+
|
406
|
+
The inner-workings of the plugin are output to the Resque [Logger](https://github.com/resque/resque/wiki/Logging)
|
407
|
+
when `Resque.logger.level` is set to `Logger::DEBUG`.
|
363
408
|
|
364
409
|
Contributing/Pull Requests
|
365
410
|
--------------------------
|
data/lib/resque-retry/version.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'resque/failure/multiple'
|
2
|
+
require 'resque/plugins/retry/logging'
|
2
3
|
|
3
4
|
module Resque
|
4
5
|
module Failure
|
@@ -17,6 +18,8 @@ module Resque
|
|
17
18
|
# Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
|
18
19
|
#
|
19
20
|
class MultipleWithRetrySuppression < Multiple
|
21
|
+
include Resque::Plugins::Retry::Logging
|
22
|
+
|
20
23
|
# Called when the job fails
|
21
24
|
#
|
22
25
|
# If the job will retry, suppress the failure from the other backends.
|
@@ -25,10 +28,17 @@ module Resque
|
|
25
28
|
#
|
26
29
|
# @api private
|
27
30
|
def save
|
28
|
-
|
31
|
+
log_message 'failure backend save', args_from(payload), exception
|
32
|
+
|
33
|
+
retryable = retryable?
|
34
|
+
job_being_retried = retryable && retrying?
|
35
|
+
|
36
|
+
if !job_being_retried
|
37
|
+
log_message "#{retryable ? '' : 'non-'}retriable job is not being retried - sending failure to superclass", args_from(payload), exception
|
29
38
|
cleanup_retry_failure_log!
|
30
39
|
super
|
31
40
|
elsif retry_delay > 0
|
41
|
+
log_message "retry_delay: #{retry_delay} > 0 - saving details in Redis", args_from(payload), exception
|
32
42
|
data = {
|
33
43
|
:failed_at => Time.now.strftime("%Y/%m/%d %H:%M:%S"),
|
34
44
|
:payload => payload,
|
@@ -40,6 +50,8 @@ module Resque
|
|
40
50
|
}
|
41
51
|
|
42
52
|
Resque.redis.setex(failure_key, 2*retry_delay, Resque.encode(data))
|
53
|
+
else
|
54
|
+
log_message "retry_delay: #{retry_delay} <= 0 - ignoring", args_from(payload), exception
|
43
55
|
end
|
44
56
|
end
|
45
57
|
|
@@ -82,6 +94,10 @@ module Resque
|
|
82
94
|
def cleanup_retry_failure_log!
|
83
95
|
Resque.redis.del(failure_key) if retryable?
|
84
96
|
end
|
97
|
+
|
98
|
+
def args_from(payload)
|
99
|
+
(payload || {})['args']
|
100
|
+
end
|
85
101
|
end
|
86
102
|
end
|
87
103
|
end
|
data/lib/resque/plugins/retry.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'digest/sha1'
|
2
|
+
require 'resque/plugins/retry/logging'
|
2
3
|
|
3
4
|
module Resque
|
4
5
|
module Plugins
|
@@ -35,6 +36,7 @@ module Resque
|
|
35
36
|
# end
|
36
37
|
#
|
37
38
|
module Retry
|
39
|
+
include Resque::Plugins::Retry::Logging
|
38
40
|
|
39
41
|
# Raised if the retry-strategy cannot be determined or has conflicts
|
40
42
|
#
|
@@ -45,7 +47,7 @@ module Resque
|
|
45
47
|
#
|
46
48
|
# @api private
|
47
49
|
def self.extended(receiver)
|
48
|
-
if receiver.instance_variable_get(
|
50
|
+
if receiver.instance_variable_get('@fatal_exceptions') && receiver.instance_variable_get('@retry_exceptions')
|
49
51
|
raise AmbiguousRetryStrategyException.new(%{You can't define both "@fatal_exceptions" and "@retry_exceptions"})
|
50
52
|
end
|
51
53
|
end
|
@@ -55,7 +57,7 @@ module Resque
|
|
55
57
|
# @api private
|
56
58
|
def inherited(subclass)
|
57
59
|
super(subclass)
|
58
|
-
subclass.instance_variable_set(
|
60
|
+
subclass.instance_variable_set('@retry_criteria_checks', retry_criteria_checks.dup)
|
59
61
|
end
|
60
62
|
|
61
63
|
# @abstract You may override to implement a custom retry identifier,
|
@@ -81,7 +83,7 @@ module Resque
|
|
81
83
|
#
|
82
84
|
# @api public
|
83
85
|
def redis_retry_key(*args)
|
84
|
-
['resque-retry', name, retry_identifier(*args)].compact.join(
|
86
|
+
['resque-retry', name, retry_identifier(*args)].compact.join(':').gsub(/\s/, '')
|
85
87
|
end
|
86
88
|
|
87
89
|
# Maximum number of retrys we can attempt to successfully perform the job
|
@@ -230,17 +232,25 @@ module Resque
|
|
230
232
|
# @api public
|
231
233
|
def retry_criteria_valid?(exception, *args)
|
232
234
|
# if the retry limit was reached, dont bother checking anything else.
|
233
|
-
|
235
|
+
if retry_limit_reached?
|
236
|
+
log_message 'retry limit reached', args, exception
|
237
|
+
return false
|
238
|
+
end
|
234
239
|
|
235
240
|
# We always want to retry if the exception matches.
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
+
retry_based_on_exception = retry_exception?(exception)
|
242
|
+
log_message "Exception is #{retry_based_on_exception ? '' : 'not '}sufficient for a retry", args, exception
|
243
|
+
|
244
|
+
retry_based_on_criteria = false
|
245
|
+
unless retry_based_on_exception
|
246
|
+
# call user retry criteria check blocks.
|
247
|
+
retry_criteria_checks.each do |criteria_check|
|
248
|
+
retry_based_on_criteria ||= !!instance_exec(exception, *args, &criteria_check)
|
249
|
+
end
|
241
250
|
end
|
251
|
+
log_message "user retry criteria is #{retry_based_on_criteria ? '' : 'not '}sufficient for a retry", args, exception
|
242
252
|
|
243
|
-
|
253
|
+
retry_based_on_exception || retry_based_on_criteria
|
244
254
|
end
|
245
255
|
|
246
256
|
# Retry criteria checks
|
@@ -297,11 +307,21 @@ module Resque
|
|
297
307
|
#
|
298
308
|
# @api private
|
299
309
|
def try_again(exception, *args)
|
310
|
+
log_message 'try_again', args, exception
|
300
311
|
# some plugins define retry_delay and have it take no arguments, so rather than break those,
|
301
312
|
# we'll just check here to see whether it takes the additional exception class argument or not
|
302
313
|
temp_retry_delay = ([-1, 1].include?(method(:retry_delay).arity) ? retry_delay(exception.class) : retry_delay)
|
303
314
|
|
304
315
|
retry_in_queue = retry_job_delegate ? retry_job_delegate : self
|
316
|
+
log_message "retry delay: #{temp_retry_delay} for class: #{retry_in_queue}", args, exception
|
317
|
+
|
318
|
+
# remember that this job is now being retried. before_perform_retry will increment
|
319
|
+
# this so it represents the retry count, and MultipleWithRetrySuppression uses
|
320
|
+
# the existence of this to determine if the job should be sent to the
|
321
|
+
# parent failure backend (e.g. failed queue) or not. Removing this means
|
322
|
+
# jobs that fail before ::perform will be both retried and sent to the failed queue.
|
323
|
+
Resque.redis.setnx(redis_retry_key(*args), -1)
|
324
|
+
|
305
325
|
if temp_retry_delay <= 0
|
306
326
|
# If the delay is 0, no point passing it through the scheduler
|
307
327
|
Resque.enqueue(retry_in_queue, *args_for_retry(*args))
|
@@ -322,12 +342,15 @@ module Resque
|
|
322
342
|
#
|
323
343
|
# @api private
|
324
344
|
def before_perform_retry(*args)
|
345
|
+
log_message 'before_perform_retry', args
|
325
346
|
@on_failure_retry_hook_already_called = false
|
326
347
|
|
327
348
|
# store number of retry attempts.
|
328
349
|
retry_key = redis_retry_key(*args)
|
329
350
|
Resque.redis.setnx(retry_key, -1) # default to -1 if not set.
|
330
351
|
@retry_attempt = Resque.redis.incr(retry_key) # increment by 1.
|
352
|
+
log_message "attempt: #{@retry_attempt} set in Redis", args
|
353
|
+
Resque.redis.expire(retry_key, @retry_delay.to_i + @expire_retry_key_after.to_i) if @expire_retry_key_after
|
331
354
|
end
|
332
355
|
|
333
356
|
# Resque after_perform hook
|
@@ -336,6 +359,7 @@ module Resque
|
|
336
359
|
#
|
337
360
|
# @api private
|
338
361
|
def after_perform_retry(*args)
|
362
|
+
log_message 'after_perform_retry, clearing retry key', args
|
339
363
|
clean_retry_key(*args)
|
340
364
|
end
|
341
365
|
|
@@ -350,11 +374,16 @@ module Resque
|
|
350
374
|
#
|
351
375
|
# @api private
|
352
376
|
def on_failure_retry(exception, *args)
|
353
|
-
|
377
|
+
log_message 'on_failure_retry', args, exception
|
378
|
+
if @on_failure_retry_hook_already_called
|
379
|
+
log_message 'on_failure_retry_hook_already_called', args, exception
|
380
|
+
return
|
381
|
+
end
|
354
382
|
|
355
383
|
if retry_criteria_valid?(exception, *args)
|
356
384
|
try_again(exception, *args)
|
357
385
|
else
|
386
|
+
log_message 'retry criteria not sufficient for retry', args, exception
|
358
387
|
clean_retry_key(*args)
|
359
388
|
end
|
360
389
|
|
@@ -381,9 +410,9 @@ module Resque
|
|
381
410
|
#
|
382
411
|
# @api private
|
383
412
|
def clean_retry_key(*args)
|
413
|
+
log_message 'clean_retry_key', args
|
384
414
|
Resque.redis.del(redis_retry_key(*args))
|
385
415
|
end
|
386
|
-
|
387
416
|
end
|
388
417
|
end
|
389
418
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Resque
|
2
|
+
module Plugins
|
3
|
+
module Retry
|
4
|
+
module Logging
|
5
|
+
# Log messages through the Resque logger (DEBUG level).
|
6
|
+
# Generally not for application logging-just for inner-workings of
|
7
|
+
# Resque and plugins.
|
8
|
+
#
|
9
|
+
# @param [String] message to log
|
10
|
+
# @param [Object] args of the resque job in context
|
11
|
+
# @param [Object] exception that might be causing a retry
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
def log_message(message, args=nil, exception=nil)
|
15
|
+
return unless Resque.logger
|
16
|
+
|
17
|
+
exception_portion = exception.nil? ? '' : " [#{exception.class}/#{exception}]"
|
18
|
+
Resque.logger.debug "resque-retry -- #{args.inspect}#{exception_portion}: #{message}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/resque-retry.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.name = 'resque-retry'
|
8
8
|
s.version = ResqueRetry::VERSION
|
9
9
|
s.date = Time.now.strftime('%Y-%m-%d')
|
10
|
-
s.authors = ['Luke Antins', 'Ryan Carver']
|
10
|
+
s.authors = ['Luke Antins', 'Ryan Carver', 'Jonathan W. Zaleski']
|
11
11
|
s.email = ['luke@lividpenguin.com']
|
12
12
|
s.summary = 'A resque plugin; provides retry, delay and exponential backoff support for resque jobs.'
|
13
13
|
s.description = <<-EOL
|
@@ -54,10 +54,11 @@ class MultipleFailureTest < MiniTest::Unit::TestCase
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def test_retry_key_splatting_args
|
57
|
-
# were expecting this to be called
|
57
|
+
# were expecting this to be called three times:
|
58
|
+
# - once when we queue the job to try again
|
58
59
|
# - once before the job is executed.
|
59
60
|
# - once by the failure backend.
|
60
|
-
RetryDefaultsJob.expects(:redis_retry_key).with({'a' => 1, 'b' => 2}).times(
|
61
|
+
RetryDefaultsJob.expects(:redis_retry_key).with({'a' => 1, 'b' => 2}).times(3)
|
61
62
|
|
62
63
|
Resque.enqueue(RetryDefaultsJob, {'a' => 1, 'b' => 2})
|
63
64
|
perform_next_job(@worker)
|
@@ -130,6 +131,16 @@ class MultipleFailureTest < MiniTest::Unit::TestCase
|
|
130
131
|
assert_equal 1, MockFailureBackend.errors.size
|
131
132
|
end
|
132
133
|
|
134
|
+
def test_failure_with_retry_bumps_key_expire
|
135
|
+
Resque.enqueue(FailFiveTimesWithExpiryJob, 'foo')
|
136
|
+
retry_key = FailFiveTimesWithExpiryJob.redis_retry_key('foo')
|
137
|
+
|
138
|
+
Resque.redis.expects(:expire).times(4).with(retry_key, 3600)
|
139
|
+
4.times do
|
140
|
+
perform_next_job(@worker)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
133
144
|
def teardown
|
134
145
|
Resque::Failure.backend = @old_failure_backend
|
135
146
|
end
|
data/test/retry_test.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'test_helper'
|
2
|
-
|
2
|
+
require 'resque/failure/redis'
|
3
3
|
require 'digest/sha1'
|
4
4
|
|
5
5
|
class RetryTest < MiniTest::Unit::TestCase
|
@@ -7,6 +7,7 @@ class RetryTest < MiniTest::Unit::TestCase
|
|
7
7
|
Resque.redis.flushall
|
8
8
|
@worker = Resque::Worker.new(:testing)
|
9
9
|
@worker.register_worker
|
10
|
+
Resque::Failure.backend = Resque::Failure::Redis
|
10
11
|
end
|
11
12
|
|
12
13
|
def test_resque_plugin_lint
|
@@ -73,6 +74,20 @@ class RetryTest < MiniTest::Unit::TestCase
|
|
73
74
|
assert_equal 10, Resque.info[:processed], 'processed job'
|
74
75
|
end
|
75
76
|
|
77
|
+
def test_failure_before_perform_does_not_both_requeue_and_fail_the_job
|
78
|
+
Resque::Failure::MultipleWithRetrySuppression.classes = [Resque::Failure::Redis]
|
79
|
+
Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
|
80
|
+
|
81
|
+
Resque.enqueue(FailsDuringConnectJob)
|
82
|
+
|
83
|
+
perform_next_job_fail_on_reconnect(@worker)
|
84
|
+
|
85
|
+
assert_equal 1, Resque.info[:processed], 'processed job'
|
86
|
+
assert_equal 0, Resque.info[:failed], "should not be any failed jobs: #{Resque::Failure.all(0, 100)}"
|
87
|
+
assert_equal 0, Resque.info[:pending], 'should not yet be any pending jobs'
|
88
|
+
assert_equal 1, delayed_jobs.size, 'should be a delayed job'
|
89
|
+
end
|
90
|
+
|
76
91
|
def test_retry_never_retry
|
77
92
|
Resque.enqueue(NeverRetryJob)
|
78
93
|
10.times do
|
@@ -265,4 +280,9 @@ class RetryTest < MiniTest::Unit::TestCase
|
|
265
280
|
assert_equal 13, PerExceptionClassRetryCountJob.retry_delay(Timeout::Error)
|
266
281
|
end
|
267
282
|
|
283
|
+
def test_expire_key_set
|
284
|
+
Resque.redis.expects(:expire).once.with(ExpiringJob.redis_retry_key('expiry_test'), 3600)
|
285
|
+
Resque.enqueue(ExpiringJob, 'expiry_test')
|
286
|
+
perform_next_job(@worker)
|
287
|
+
end
|
268
288
|
end
|
data/test/test_helper.rb
CHANGED
@@ -53,6 +53,34 @@ class MiniTest::Unit::TestCase
|
|
53
53
|
worker.done_working
|
54
54
|
end
|
55
55
|
|
56
|
+
def perform_next_job_fail_on_reconnect(worker,&block)
|
57
|
+
raise "No work for #{worker}" unless job = worker.reserve
|
58
|
+
worker.working_on job
|
59
|
+
|
60
|
+
# Similar to resque's Worker.work and Worker.process methods
|
61
|
+
begin
|
62
|
+
raise 'error from perform_next_job_fail_on_reconnect'
|
63
|
+
worker.perform(job, &block)
|
64
|
+
rescue Exception => exception
|
65
|
+
worker.report_failed_job(job, exception)
|
66
|
+
ensure
|
67
|
+
worker.done_working
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def delayed_jobs
|
72
|
+
# The double-checks here are so that we won't blow up if the config stops using redis-namespace
|
73
|
+
timestamps = Resque.redis.zrange("resque:delayed_queue_schedule", 0, -1) +
|
74
|
+
Resque.redis.zrange("delayed_queue_schedule", 0, -1)
|
75
|
+
|
76
|
+
delayed_jobs_as_json = timestamps.map do |timestamp|
|
77
|
+
Resque.redis.lrange("resque:delayed:#{timestamp}", 0, -1) +
|
78
|
+
Resque.redis.lrange("delayed:#{timestamp}", 0, -1)
|
79
|
+
end.flatten
|
80
|
+
|
81
|
+
delayed_jobs_as_json.map { |json| JSON.parse(json) }
|
82
|
+
end
|
83
|
+
|
56
84
|
def clean_perform_job(klass, *args)
|
57
85
|
Resque.redis.flushall
|
58
86
|
Resque.enqueue(klass, *args)
|
data/test/test_jobs.rb
CHANGED
@@ -19,6 +19,15 @@ class GoodJob
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
+
class ExpiringJob
|
23
|
+
extend Resque::Plugins::Retry
|
24
|
+
@queue = :testing
|
25
|
+
@expire_retry_key_after = 60 * 60
|
26
|
+
|
27
|
+
def self.perform(*args)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
22
31
|
class RetryDefaultSettingsJob
|
23
32
|
extend Resque::Plugins::Retry
|
24
33
|
@queue = :testing
|
@@ -135,6 +144,16 @@ class FailFiveTimesJob < RetryDefaultsJob
|
|
135
144
|
end
|
136
145
|
end
|
137
146
|
|
147
|
+
class FailFiveTimesWithExpiryJob < RetryDefaultsJob
|
148
|
+
@queue = :testing
|
149
|
+
@retry_limit = 6
|
150
|
+
@expire_retry_key_after = 60 * 60
|
151
|
+
|
152
|
+
def self.perform(*args)
|
153
|
+
raise if retry_attempt <= 4
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
138
157
|
class ExponentialBackoffJob < RetryDefaultsJob
|
139
158
|
extend Resque::Plugins::ExponentialBackoff
|
140
159
|
@queue = :testing
|
@@ -448,3 +467,10 @@ class PerExceptionClassRetryCountArrayJob
|
|
448
467
|
raise RuntimeError, 'I always fail with a RuntimeError'
|
449
468
|
end
|
450
469
|
end
|
470
|
+
|
471
|
+
# We can't design a job to fail during connect, see perform_next_job_fail_on_reconnect
|
472
|
+
class FailsDuringConnectJob < RetryDefaultsJob
|
473
|
+
@queue = :testing
|
474
|
+
@retry_limit = 3
|
475
|
+
@retry_delay = 10
|
476
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: resque-retry
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luke Antins
|
8
8
|
- Ryan Carver
|
9
|
+
- Jonathan W. Zaleski
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2014-
|
13
|
+
date: 2014-05-19 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: resque
|
@@ -177,6 +178,7 @@ files:
|
|
177
178
|
- lib/resque/failure/multiple_with_retry_suppression.rb
|
178
179
|
- lib/resque/plugins/exponential_backoff.rb
|
179
180
|
- lib/resque/plugins/retry.rb
|
181
|
+
- lib/resque/plugins/retry/logging.rb
|
180
182
|
- resque-retry.gemspec
|
181
183
|
- test/exponential_backoff_test.rb
|
182
184
|
- test/multiple_failure_test.rb
|