resque-retry 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/HISTORY.md +5 -0
- data/README.md +167 -76
- data/lib/resque-retry/version.rb +1 -1
- data/lib/resque/plugins/retry.rb +160 -12
- data/test/retry_callbacks_test.rb +121 -0
- data/test/retry_criteria_test.rb +11 -0
- data/test/test_jobs.rb +67 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e13254ff1b1a5e06977bbbbfa6a28843629f2133
|
4
|
+
data.tar.gz: e9ebf7d623865a0d067c060a804ccae233a34728
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa3fba3ab9b3945a9b9de6cd3f16a59ce85ac8365d5ac2fc7c962573e74b0236cc2e0526c0435f925c22a7648aefa0fa9f6dc9015ff9a415cb36137b2f239ff7
|
7
|
+
data.tar.gz: 30220fc7c9f30c3b7c0fa63105f86dbcc4c10f59e7cc1bd0e638581b87abf320d5fdae7deb0c9aadbe327f6c0af501fe5bf76842d6f488e15405ded60222af2f
|
data/HISTORY.md
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
## HEAD
|
2
2
|
|
3
|
+
# 1.5.0 (2015-10-24)
|
4
|
+
|
5
|
+
* Ability to define 'try again' and 'give up' callbacks/hooks (@thenovices)
|
6
|
+
* Allow `retry_criteria_check` to be registered with Symbols (@thenovices)
|
7
|
+
|
3
8
|
## 1.4.0 (2015-01-07)
|
4
9
|
|
5
10
|
* Dependency on `resque-scheduler` bumped to ~> 4.0
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
resque-retry
|
2
2
|
============
|
3
3
|
|
4
|
-
A [Resque][
|
4
|
+
A [Resque][resque] plugin. Requires Resque ~> 1.25 & [resque-scheduler][resque-scheduler] ~> 4.0.
|
5
5
|
|
6
6
|
This gem provides retry, delay and exponential backoff support for resque jobs.
|
7
7
|
|
@@ -18,11 +18,12 @@ Install & Quick Start
|
|
18
18
|
---------------------
|
19
19
|
|
20
20
|
To install:
|
21
|
+
```
|
22
|
+
$ gem install resque-retry
|
23
|
+
```
|
21
24
|
|
22
|
-
|
23
|
-
|
24
|
-
If you're using [Bundler][bundler] to manage your dependencies, you should add `gem
|
25
|
-
'resque-retry'` to your projects `Gemfile`.
|
25
|
+
If you're using [Bundler][bundler] to manage your dependencies, you should add
|
26
|
+
`gem 'resque-retry'` to your `Gemfile`.
|
26
27
|
|
27
28
|
Add this to your `Rakefile`:
|
28
29
|
```ruby
|
@@ -30,7 +31,7 @@ require 'resque/tasks'
|
|
30
31
|
require 'resque/scheduler/tasks'
|
31
32
|
```
|
32
33
|
|
33
|
-
The delay between retry attempts is provided by [resque-scheduler][
|
34
|
+
The delay between retry attempts is provided by [resque-scheduler][resque-scheduler].
|
34
35
|
You'll want to run the scheduler process, otherwise delayed retry attempts
|
35
36
|
will never perform:
|
36
37
|
```
|
@@ -79,7 +80,8 @@ actually completed successfully just by just using the resque-web interface.
|
|
79
80
|
|
80
81
|
### Failure Backend
|
81
82
|
|
82
|
-
`MultipleWithRetrySuppression` is a multiple failure backend, with retry
|
83
|
+
`MultipleWithRetrySuppression` is a multiple failure backend, with retry
|
84
|
+
suppression.
|
83
85
|
|
84
86
|
Here's an example, using the Redis failure backend:
|
85
87
|
```ruby
|
@@ -92,8 +94,8 @@ Resque::Failure::MultipleWithRetrySuppression.classes = [Resque::Failure::Redis]
|
|
92
94
|
Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
|
93
95
|
```
|
94
96
|
|
95
|
-
If a job fails, but **can and will** retry, the failure details wont be
|
96
|
-
|
97
|
+
If a job fails, but **can and will** retry, the failure details wont be logged
|
98
|
+
in the Redis failed queue *(visible via resque-web)*.
|
97
99
|
|
98
100
|
If the job fails, but **can't or won't** retry, the failure will be logged in
|
99
101
|
the Redis failed queue, like a normal failure *(without retry)* would.
|
@@ -101,7 +103,7 @@ the Redis failed queue, like a normal failure *(without retry)* would.
|
|
101
103
|
### Resque Web Additions
|
102
104
|
|
103
105
|
If you're using the `MultipleWithRetrySuppression` failure backend, you should
|
104
|
-
also checkout the resque-web additions!
|
106
|
+
also checkout the `resque-web` additions!
|
105
107
|
|
106
108
|
The new Retry tab displays delayed jobs with retry information; the number of
|
107
109
|
attempts and the exception details from the last failure.
|
@@ -109,17 +111,16 @@ attempts and the exception details from the last failure.
|
|
109
111
|
|
110
112
|
### Configuring and running the Resque-Web Interface
|
111
113
|
|
112
|
-
#### Using a Rack configuration:
|
113
|
-
|
114
|
-
One alternative is to use a rack configuration file. To use this, make
|
115
|
-
sure you include this in your `config.ru` or similar file:
|
114
|
+
#### Using a Rack configuration:
|
116
115
|
|
116
|
+
One alternative is to use a rack configuration file. To use this, make sure you
|
117
|
+
include this in your `config.ru` or similar file:
|
117
118
|
```ruby
|
118
119
|
require 'resque-retry'
|
119
120
|
require 'resque-retry/server'
|
120
121
|
|
121
122
|
# Make sure to require your workers & application code below this line:
|
122
|
-
# require '[path]/[to]/[jobs]/your_worker'
|
123
|
+
# require '[path]/[to]/[jobs]/your_worker'
|
123
124
|
|
124
125
|
# Run the server
|
125
126
|
run Resque::Server.new
|
@@ -132,33 +133,30 @@ rackup -p 9292 config.ru
|
|
132
133
|
|
133
134
|
When using bundler, you can also run the server like this:
|
134
135
|
```
|
135
|
-
bundle exec rackup -p 9292 config.ru
|
136
|
+
bundle exec rackup -p 9292 config.ru
|
136
137
|
```
|
137
138
|
|
138
139
|
|
139
140
|
#### Using the 'resque-web' command with a configuration file:
|
140
141
|
|
141
|
-
Another alternative is to use resque's built-in 'resque-web' command with
|
142
|
-
|
143
|
-
|
144
|
-
file in a 'config' directory, and name it 'resque_web_config.rb'. In practice
|
145
|
-
you could rename this configuration file to anything you
|
146
|
-
project in
|
142
|
+
Another alternative is to use resque's built-in 'resque-web' command with the
|
143
|
+
additional resque-retry tabs. In order to do this, you must first create a
|
144
|
+
configuration file. For the sake of this example we'll create the configuration
|
145
|
+
file in a 'config' directory, and name it 'resque_web_config.rb'. In practice
|
146
|
+
you could rename this configuration file to anything you like and place in your
|
147
|
+
project in a directory of your choosing. The contents of the configuration file
|
147
148
|
would look like this:
|
148
|
-
|
149
149
|
```ruby
|
150
150
|
# [app_dir]/config/resque_web_config.rb
|
151
151
|
require 'resque-retry'
|
152
152
|
require 'resque-retry/server'
|
153
153
|
|
154
154
|
# Make sure to require your workers & application code below this line:
|
155
|
-
# require '[path]/[to]/[jobs]/your_worker'
|
156
|
-
|
155
|
+
# require '[path]/[to]/[jobs]/your_worker'
|
157
156
|
```
|
158
157
|
|
159
158
|
Once you have the configuration file ready, you can pass the configuration file
|
160
159
|
to the resque-web command as a parameter, like so:
|
161
|
-
|
162
160
|
```
|
163
161
|
resque-web [app_dir]/config/resque_web_config.rb
|
164
162
|
```
|
@@ -170,10 +168,24 @@ Retry Options & Logic
|
|
170
168
|
Please take a look at the [yardoc](http://rubydoc.info/gems/resque-retry)/code
|
171
169
|
for more details on methods you may wish to override.
|
172
170
|
|
173
|
-
Customisation is pretty easy, the below examples should give you
|
174
|
-
|
175
|
-
|
176
|
-
|
171
|
+
Customisation is pretty easy, the below examples should give you some ideas =),
|
172
|
+
adapt for your own usage and feel free to pick and mix!
|
173
|
+
|
174
|
+
Here are a list of the options provided (click to jump):
|
175
|
+
* [Retry Defaults](#retry_defaults)
|
176
|
+
* [Custom Retry](#custom_retry)
|
177
|
+
* [Sleep After Requeuing](#sleep)
|
178
|
+
* [Exponential Backoff](#exp)
|
179
|
+
* [Retry Specific Exceptions](#specific)
|
180
|
+
* [Fail Fast For Specific Exceptions](#fail_fast)
|
181
|
+
* [Custom Retry Criteria Check Callbacks](#custom_check)
|
182
|
+
* [Retry Arguments](#retry_args)
|
183
|
+
* [Job Retry Identifier/Key](#retry_key)
|
184
|
+
* [Expire Retry Counters From Redis](#expire)
|
185
|
+
* [Try Again and Give Up Callbacks](#callbacks)
|
186
|
+
* [Debug Plugin Logging](#debug_log)
|
187
|
+
|
188
|
+
### <a name="retry_defaults"></a> Retry Defaults
|
177
189
|
|
178
190
|
Retry the job **once** on failure, with zero delay.
|
179
191
|
```ruby
|
@@ -193,7 +205,7 @@ When a job runs, the number of retry attempts is checked and incremented
|
|
193
205
|
in Redis. If your job fails, the number of retry attempts is used to
|
194
206
|
determine if we can requeue the job for another go.
|
195
207
|
|
196
|
-
### Custom Retry
|
208
|
+
### <a name="custom_retry"></a> Custom Retry
|
197
209
|
```ruby
|
198
210
|
class DeliverWebHook
|
199
211
|
extend Resque::Plugins::Retry
|
@@ -208,13 +220,12 @@ class DeliverWebHook
|
|
208
220
|
end
|
209
221
|
```
|
210
222
|
|
211
|
-
The above modification will allow your job to retry up to 10 times, with
|
212
|
-
|
223
|
+
The above modification will allow your job to retry up to 10 times, with a delay
|
224
|
+
of 120 seconds, or 2 minutes between retry attempts.
|
213
225
|
|
214
|
-
|
215
|
-
more special.
|
226
|
+
You can override the `retry_delay` method to set the delay value dynamically.
|
216
227
|
|
217
|
-
### Sleep After Requeuing
|
228
|
+
### <a name="sleep"></a> Sleep After Requeuing
|
218
229
|
|
219
230
|
Sometimes it is useful to delay the worker that failed a job attempt, but
|
220
231
|
still requeue the job for immediate processing by other workers. This can be
|
@@ -235,17 +246,17 @@ end
|
|
235
246
|
This retries the job once and causes the worker that failed to sleep for 5
|
236
247
|
seconds after requeuing the job. If there are multiple workers in the system
|
237
248
|
this allows the job to be retried immediately while the original worker heals
|
238
|
-
itself.For example failed jobs may cause other (non-worker) OS processes to
|
239
|
-
die. A system monitor such as [god][god] can fix the server
|
240
|
-
being retried on a different worker.
|
249
|
+
itself. For example failed jobs may cause other (non-worker) OS processes to
|
250
|
+
die. A system monitor such as [monit][monit] or [god][god] can fix the server
|
251
|
+
while the job is being retried on a different worker.
|
241
252
|
|
242
253
|
`@sleep_after_requeue` is independent of `@retry_delay`. If you set both, they
|
243
254
|
both take effect.
|
244
255
|
|
245
|
-
You can override the
|
256
|
+
You can override the `sleep_after_requeue` method to set the sleep value
|
246
257
|
dynamically.
|
247
258
|
|
248
|
-
### Exponential Backoff
|
259
|
+
### <a name="exp"></a> Exponential Backoff
|
249
260
|
|
250
261
|
Use this if you wish to vary the delay between retry attempts:
|
251
262
|
```ruby
|
@@ -263,17 +274,17 @@ end
|
|
263
274
|
```
|
264
275
|
key: m = minutes, h = hours
|
265
276
|
|
266
|
-
|
277
|
+
0s, 1m, 10m, 1h, 3h, 6h
|
267
278
|
@backoff_strategy = [0, 60, 600, 3600, 10800, 21600]
|
268
279
|
@retry_delay_multiplicand_min = 1.0
|
269
280
|
@retry_delay_multiplicand_max = 1.0
|
270
281
|
```
|
271
282
|
|
272
|
-
The first delay will be 0 seconds, the 2nd will be 60 seconds, etc...
|
273
|
-
|
283
|
+
The first delay will be 0 seconds, the 2nd will be 60 seconds, etc... Again,
|
284
|
+
tweak to your own needs.
|
274
285
|
|
275
|
-
The number of retries is equal to the size of the `backoff_strategy`
|
276
|
-
|
286
|
+
The number of retries is equal to the size of the `backoff_strategy` array,
|
287
|
+
unless you set `retry_limit` yourself.
|
277
288
|
|
278
289
|
The delay values will be multiplied by a random `Float` value between
|
279
290
|
`retry_delay_multiplicand_min` and `retry_delay_multiplicand_max` (both have a
|
@@ -282,10 +293,10 @@ attempt. This feature can be useful if you have a lot of jobs fail at the same
|
|
282
293
|
time (e.g. rate-limiting/throttling or connectivity issues) and you don't want
|
283
294
|
them all retried on the same schedule.
|
284
295
|
|
285
|
-
### Retry Specific Exceptions
|
296
|
+
### <a name="specific"></a> Retry Specific Exceptions
|
286
297
|
|
287
|
-
The default will allow a retry for any type of exception. You may change
|
288
|
-
|
298
|
+
The default will allow a retry for any type of exception. You may change it so
|
299
|
+
only specific exceptions are retried using `retry_exceptions`:
|
289
300
|
```ruby
|
290
301
|
class DeliverSMS
|
291
302
|
extend Resque::Plugins::Retry
|
@@ -305,8 +316,8 @@ exception is thrown.
|
|
305
316
|
You may also want to specify different retry delays for different exception
|
306
317
|
types. You may optionally set `@retry_exceptions` to a hash where the keys are
|
307
318
|
your specific exception classes to retry on, and the values are your retry
|
308
|
-
delays in seconds or an array of retry delays to be used similar to
|
309
|
-
|
319
|
+
delays in seconds or an array of retry delays to be used similar to exponential
|
320
|
+
backoff.
|
310
321
|
```ruby
|
311
322
|
class DeliverSMS
|
312
323
|
extend Resque::Plugins::Retry
|
@@ -325,7 +336,7 @@ In the above example, Resque would retry any `DeliverSMS` jobs which throw a
|
|
325
336
|
will be retried 30 seconds later, if it throws `SystemCallError` it will first
|
326
337
|
retry 120 seconds later then subsequent retry attempts 240 seconds later.
|
327
338
|
|
328
|
-
### Fail Fast For Specific Exceptions
|
339
|
+
### <a name="fail_fast"></a> Fail Fast For Specific Exceptions
|
329
340
|
|
330
341
|
The default will allow a retry for any type of exception. You may change
|
331
342
|
it so specific exceptions fail immediately by using `fatal_exceptions`:
|
@@ -346,7 +357,7 @@ In the above example, Resque would retry any `DeliverSMS` jobs that throw any
|
|
346
357
|
type of error other than `NetworkError`. If the job throws a `NetworkError` it
|
347
358
|
will be marked as "failed" immediately.
|
348
359
|
|
349
|
-
### Custom Retry Criteria Check Callbacks
|
360
|
+
### <a name="custom_check"></a> Custom Retry Criteria Check Callbacks
|
350
361
|
|
351
362
|
You may define custom retry criteria callbacks:
|
352
363
|
```ruby
|
@@ -374,13 +385,30 @@ Similar to the previous example, this job will retry if either a
|
|
374
385
|
`NetworkError` (or subclass) exception is thrown **or** any of the callbacks
|
375
386
|
return true.
|
376
387
|
|
377
|
-
|
378
|
-
job
|
388
|
+
You can also register a retry criteria check with a Symbol if the method is
|
389
|
+
already defined on the job class:
|
390
|
+
```
|
391
|
+
class AlwaysRetryJob
|
392
|
+
extend Resque::Plugins::Retry
|
393
|
+
|
394
|
+
retry_criteria_check :yes
|
379
395
|
|
380
|
-
|
396
|
+
def self.yes(ex, *args)
|
397
|
+
true
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
Use `@retry_exceptions = []` to **only** use your custom retry criteria checks
|
402
|
+
to determine if the job should retry.
|
381
403
|
|
382
|
-
|
383
|
-
|
404
|
+
NB: Your callback must be able to accept the exception and job arguments as
|
405
|
+
passed parameters, or else it cannot be called. e.g., in the example above,
|
406
|
+
defining `def self.yes; true; end` would not work.
|
407
|
+
|
408
|
+
### <a name="retry_args"></a> Retry Arguments
|
409
|
+
|
410
|
+
You may override `retry_args`, which is passed the current job arguments, to
|
411
|
+
modify the arguments for the next retry attempt.
|
384
412
|
```ruby
|
385
413
|
class DeliverViaSMSC
|
386
414
|
extend Resque::Plugins::Retry
|
@@ -397,10 +425,10 @@ class DeliverViaSMSC
|
|
397
425
|
end
|
398
426
|
```
|
399
427
|
|
400
|
-
Alternatively, if you require finer control of the args based on the
|
401
|
-
|
402
|
-
|
403
|
-
|
428
|
+
Alternatively, if you require finer control of the args based on the exception
|
429
|
+
thrown, you may override `retry_args_for_exception`, which is passed the
|
430
|
+
exception and the current job arguments, to modify the arguments for the next
|
431
|
+
retry attempt.
|
404
432
|
```ruby
|
405
433
|
class DeliverViaSMSC
|
406
434
|
extend Resque::Plugins::Retry
|
@@ -416,17 +444,17 @@ class DeliverViaSMSC
|
|
416
444
|
end
|
417
445
|
end
|
418
446
|
```
|
419
|
-
### Job Retry Identifier/Key
|
447
|
+
### <a name="retry_key"></a> Job Retry Identifier/Key
|
420
448
|
|
421
|
-
The retry attempt is incremented and stored in a Redis key. The key is
|
422
|
-
|
449
|
+
The retry attempt is incremented and stored in a Redis key. The key is built
|
450
|
+
using the `retry_identifier`. If you have a lot of arguments or really long
|
423
451
|
ones, you should consider overriding `retry_identifier` to define a more precise
|
424
452
|
or loose custom retry identifier.
|
425
453
|
|
426
|
-
The default
|
454
|
+
The default identifier is just your job arguments joined with a dash `'-'`.
|
427
455
|
|
428
|
-
By default the key uses this format:
|
429
|
-
`resque-retry:<job class name>:<retry_identifier
|
456
|
+
By default the key uses this format:
|
457
|
+
`'resque-retry:<job class name>:<retry_identifier>'`.
|
430
458
|
|
431
459
|
Or you can define the entire key by overriding `redis_retry_key`.
|
432
460
|
```ruby
|
@@ -444,11 +472,10 @@ class DeliverSMS
|
|
444
472
|
end
|
445
473
|
```
|
446
474
|
|
447
|
-
### Expire Retry Counters From Redis
|
475
|
+
### <a name="expire"></a> Expire Retry Counters From Redis
|
448
476
|
|
449
477
|
Allow the Redis to expire stale retry counters from the database by setting
|
450
478
|
`@expire_retry_key_after`:
|
451
|
-
|
452
479
|
```ruby
|
453
480
|
class DeliverSMS
|
454
481
|
extend Resque::Plugins::Retry
|
@@ -459,15 +486,78 @@ class DeliverSMS
|
|
459
486
|
heavy_lifting
|
460
487
|
end
|
461
488
|
end
|
462
|
-
|
463
489
|
```
|
464
490
|
|
465
491
|
This saves you from having to run a "house cleaning" or "errand" job.
|
466
492
|
|
467
493
|
The expiary timeout is "pushed forward" or "touched" after each failure to
|
468
|
-
ensure
|
494
|
+
ensure it's not expired too soon.
|
495
|
+
|
496
|
+
### <a name="callbacks"></a> Try Again and Give Up Callbacks
|
497
|
+
Resque's `on_failure` callbacks are always called, regardless of whether the
|
498
|
+
job is going to be retried or not. If you want to run a callback only when the
|
499
|
+
job is being retried, you can add a `try_again_callback`:
|
500
|
+
```ruby
|
501
|
+
class LoggedJob
|
502
|
+
extend Resque::Plugins::Retry
|
503
|
+
|
504
|
+
try_again_callback do |exception, *args|
|
505
|
+
logger.info("Received #{exception}, retrying job #{self.name} with #{args}")
|
506
|
+
end
|
507
|
+
end
|
508
|
+
```
|
509
|
+
|
510
|
+
Similarly, if you want to run a callback only when the job has failed, and is
|
511
|
+
_not_ retrying, you can add a `give_up_callback`:
|
512
|
+
```ruby
|
513
|
+
class LoggedJob
|
514
|
+
extend Resque::Plugins::Retry
|
515
|
+
|
516
|
+
give_up_callback do |exception, *args|
|
517
|
+
logger.error("Received #{exception}, job #{self.name} failed with #{args}")
|
518
|
+
end
|
519
|
+
end
|
520
|
+
```
|
521
|
+
|
522
|
+
You can register a callback with a Symbol if the method is already defined on
|
523
|
+
the job class:
|
524
|
+
```ruby
|
525
|
+
class LoggedJob
|
526
|
+
extend Resque::Plugins::Retry
|
527
|
+
|
528
|
+
give_up_callback :log_give_up
|
529
|
+
|
530
|
+
def self.log_give_up(ex, *args)
|
531
|
+
logger.error("Received #{exception}, job #{self.name} failed with #{args}")
|
532
|
+
end
|
533
|
+
end
|
534
|
+
```
|
535
|
+
|
536
|
+
You can register multiple callbacks, and they will be called in the order that
|
537
|
+
they were registered. You can also set callbacks by setting
|
538
|
+
`@try_again_callbacks` or `@give_up_callbacks` to an array where each element
|
539
|
+
is a `Proc` or `Symbol`.
|
540
|
+
```ruby
|
541
|
+
class CallbackJob
|
542
|
+
extend Resque::Plugins::Retry
|
543
|
+
|
544
|
+
@try_again_callbacks = [
|
545
|
+
:call_me_first,
|
546
|
+
:call_me_second,
|
547
|
+
lambda { |*args| call_me_third(*args) }
|
548
|
+
]
|
549
|
+
|
550
|
+
def self.call_me_first(ex, *args); end
|
551
|
+
def self.call_me_second(ex, *args); end
|
552
|
+
def self.call_me_third(ex, *args); end
|
553
|
+
end
|
554
|
+
```
|
555
|
+
|
556
|
+
Warning: Make sure your callbacks do not throw any exceptions. If they do,
|
557
|
+
subsequent callbacks will not be triggered, and the job will not be retried
|
558
|
+
(if it was trying again). The retry counter also will not be reset.
|
469
559
|
|
470
|
-
### Debug Plugin Logging
|
560
|
+
### <a name="debug_log"></a> Debug Plugin Logging
|
471
561
|
|
472
562
|
The inner-workings of the plugin are output to the Resque [Logger](https://github.com/resque/resque/wiki/Logging)
|
473
563
|
when `Resque.logger.level` is set to `Logger::DEBUG`.
|
@@ -483,7 +573,8 @@ Contributing/Pull Requests
|
|
483
573
|
* Send us a pull request. Bonus points for topic branches.
|
484
574
|
* If you edit the gemspec/version etc, please do so in another commit.
|
485
575
|
|
486
|
-
[
|
487
|
-
[
|
488
|
-
[
|
576
|
+
[monit]: https://mmonit.com
|
577
|
+
[god]: http://godrb.com
|
578
|
+
[resque]: http://github.com/resque/resque
|
579
|
+
[resque-scheduler]: http://github.com/resque/resque-scheduler
|
489
580
|
[bundler]: http://bundler.io
|
data/lib/resque-retry/version.rb
CHANGED
data/lib/resque/plugins/retry.rb
CHANGED
@@ -52,12 +52,15 @@ module Resque
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
-
# Copy retry criteria checks
|
55
|
+
# Copy retry criteria checks, try again callbacks, and give up callbacks
|
56
|
+
# on inheritance.
|
56
57
|
#
|
57
58
|
# @api private
|
58
59
|
def inherited(subclass)
|
59
60
|
super(subclass)
|
60
61
|
subclass.instance_variable_set('@retry_criteria_checks', retry_criteria_checks.dup)
|
62
|
+
subclass.instance_variable_set('@try_again_callbacks', try_again_callbacks.dup)
|
63
|
+
subclass.instance_variable_set('@give_up_callbacks', give_up_callbacks.dup)
|
61
64
|
end
|
62
65
|
|
63
66
|
# @abstract You may override to implement a custom retry identifier,
|
@@ -271,11 +274,9 @@ module Resque
|
|
271
274
|
retry_based_on_criteria = false
|
272
275
|
unless retry_based_on_exception
|
273
276
|
# call user retry criteria check blocks.
|
274
|
-
|
275
|
-
|
276
|
-
end
|
277
|
+
retry_based_on_criteria = retry_criteria_checks_pass?(exception, *args)
|
278
|
+
log_message "user retry criteria is #{retry_based_on_criteria ? '' : 'not '}sufficient for a retry", args, exception
|
277
279
|
end
|
278
|
-
log_message "user retry criteria is #{retry_based_on_criteria ? '' : 'not '}sufficient for a retry", args, exception
|
279
280
|
|
280
281
|
retry_based_on_exception || retry_based_on_criteria
|
281
282
|
end
|
@@ -305,11 +306,11 @@ module Resque
|
|
305
306
|
end
|
306
307
|
|
307
308
|
# Register a retry criteria check callback to be run before retrying
|
308
|
-
# the job again
|
309
|
+
# the job again. Can be registered with a block or a symbol.
|
309
310
|
#
|
310
311
|
# If any callback returns `true`, the job will be retried.
|
311
312
|
#
|
312
|
-
# @example
|
313
|
+
# @example Registering a custom retry criteria check.
|
313
314
|
#
|
314
315
|
# retry_criteria_check do |exception, *args|
|
315
316
|
# if exception.message =~ /InvalidJobId/
|
@@ -320,14 +321,36 @@ module Resque
|
|
320
321
|
# end
|
321
322
|
# end
|
322
323
|
#
|
324
|
+
# @example
|
325
|
+
#
|
326
|
+
# retry_criteria_check :my_check
|
327
|
+
#
|
328
|
+
# @param [Symbol?] method
|
323
329
|
# @yield [exception, *args]
|
324
330
|
# @yieldparam exception [Exception] the exception that was raised
|
325
331
|
# @yieldparam args [Array] job arguments
|
326
|
-
# @yieldreturn [Boolean] false == dont retry, true
|
332
|
+
# @yieldreturn [Boolean] false == dont retry, true == can retry
|
327
333
|
#
|
328
334
|
# @api public
|
329
|
-
def retry_criteria_check(&block)
|
330
|
-
|
335
|
+
def retry_criteria_check(method=nil, &block)
|
336
|
+
if method.is_a? Symbol
|
337
|
+
retry_criteria_checks << method
|
338
|
+
elsif block_given?
|
339
|
+
retry_criteria_checks << block
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
# Returns true if *any* of the retry criteria checks pass. When a retry
|
344
|
+
# criteria check passes, the remaining ones are not executed.
|
345
|
+
#
|
346
|
+
# @returns [Boolean] whether any of the retry criteria checks pass
|
347
|
+
#
|
348
|
+
# @api private
|
349
|
+
def retry_criteria_checks_pass?(exception, *args)
|
350
|
+
retry_criteria_checks.each do |criteria_check|
|
351
|
+
return true if !!call_symbol_or_block(criteria_check, exception, *args)
|
352
|
+
end
|
353
|
+
false
|
331
354
|
end
|
332
355
|
|
333
356
|
# Retries the job
|
@@ -335,6 +358,8 @@ module Resque
|
|
335
358
|
# @api private
|
336
359
|
def try_again(exception, *args)
|
337
360
|
log_message 'try_again', args, exception
|
361
|
+
run_try_again_callbacks(exception, *args)
|
362
|
+
|
338
363
|
# some plugins define retry_delay and have it take no arguments, so rather than break those,
|
339
364
|
# we'll just check here to see whether it takes the additional exception class argument or not
|
340
365
|
temp_retry_delay = ([-1, 1].include?(method(:retry_delay).arity) ? retry_delay(exception.class) : retry_delay)
|
@@ -365,6 +390,15 @@ module Resque
|
|
365
390
|
sleep(sleep_after_requeue) if sleep_after_requeue > 0
|
366
391
|
end
|
367
392
|
|
393
|
+
# We failed and we're not retrying.
|
394
|
+
#
|
395
|
+
# @api private
|
396
|
+
def give_up(exception, *args)
|
397
|
+
log_message 'retry criteria not sufficient for retry', args, exception
|
398
|
+
run_give_up_callbacks(exception, *args)
|
399
|
+
clean_retry_key(*args)
|
400
|
+
end
|
401
|
+
|
368
402
|
# Resque before_perform hook
|
369
403
|
#
|
370
404
|
# Increments `@retry_attempt` count and updates the "retry_key" expiration
|
@@ -422,8 +456,7 @@ module Resque
|
|
422
456
|
if retry_criteria_valid?(exception, *args)
|
423
457
|
try_again(exception, *args)
|
424
458
|
else
|
425
|
-
|
426
|
-
clean_retry_key(*args)
|
459
|
+
give_up(exception, *args)
|
427
460
|
end
|
428
461
|
|
429
462
|
@on_failure_retry_hook_already_called = true
|
@@ -452,6 +485,121 @@ module Resque
|
|
452
485
|
log_message 'clean_retry_key', args
|
453
486
|
Resque.redis.del(redis_retry_key(*args))
|
454
487
|
end
|
488
|
+
|
489
|
+
# Returns the try again callbacks.
|
490
|
+
#
|
491
|
+
# @return [Array<Proc>]
|
492
|
+
#
|
493
|
+
# @api public
|
494
|
+
def try_again_callbacks
|
495
|
+
@try_again_callbacks ||= []
|
496
|
+
end
|
497
|
+
|
498
|
+
# Register a try again callback that will be called when the job fails
|
499
|
+
# but is trying again. Can be registered with a block or a symbol.
|
500
|
+
#
|
501
|
+
# @example Registering a callback with a block
|
502
|
+
#
|
503
|
+
# try_again_callback do |exception, *args|
|
504
|
+
# logger.error(
|
505
|
+
# "Resque job received exception #{exception} and is trying again")
|
506
|
+
# end
|
507
|
+
#
|
508
|
+
# @example Registering a callback with a Symbol
|
509
|
+
#
|
510
|
+
# try_again_callback :my_callback
|
511
|
+
#
|
512
|
+
# @param [Symbol?] method
|
513
|
+
# @yield [exception, *args]
|
514
|
+
# @yieldparam exception [Exception] the exception that was raised
|
515
|
+
# @yieldparam args [Array] job arguments
|
516
|
+
#
|
517
|
+
# @api public
|
518
|
+
def try_again_callback(method=nil, &block)
|
519
|
+
if method.is_a? Symbol
|
520
|
+
try_again_callbacks << method
|
521
|
+
elsif block_given?
|
522
|
+
try_again_callbacks << block
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
# Runs all the try again callbacks.
|
527
|
+
#
|
528
|
+
# @param exception [Exception]
|
529
|
+
# @param args [Object...]
|
530
|
+
#
|
531
|
+
# @api private
|
532
|
+
def run_try_again_callbacks(exception, *args)
|
533
|
+
try_again_callbacks.each do |callback|
|
534
|
+
call_symbol_or_block(callback, exception, *args)
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
# Returns the give up callbacks.
|
539
|
+
#
|
540
|
+
# @return [Array<Proc>]
|
541
|
+
#
|
542
|
+
# @api public
|
543
|
+
def give_up_callbacks
|
544
|
+
@give_up_callbacks ||= []
|
545
|
+
end
|
546
|
+
|
547
|
+
# Register a give up callback that will be called when the job fails
|
548
|
+
# and is not retrying. Can be registered with a block or a symbol.
|
549
|
+
#
|
550
|
+
# @example Registering a callback with a block
|
551
|
+
#
|
552
|
+
# give_up_callback do |exception, *args|
|
553
|
+
# logger.error(
|
554
|
+
# "Resque job received exception #{exception} and is giving up")
|
555
|
+
# end
|
556
|
+
#
|
557
|
+
# @example Registering a callback with a Symbol
|
558
|
+
#
|
559
|
+
# give_up_callback :my_callback
|
560
|
+
#
|
561
|
+
# @param [Symbol?] method
|
562
|
+
# @yield [exception, *args]
|
563
|
+
# @yieldparam exception [Exception] the exception that was raised
|
564
|
+
# @yieldparam args [Array] job arguments
|
565
|
+
#
|
566
|
+
# @api public
|
567
|
+
def give_up_callback(method=nil, &block)
|
568
|
+
if method.is_a? Symbol
|
569
|
+
give_up_callbacks << method
|
570
|
+
elsif block_given?
|
571
|
+
give_up_callbacks << block
|
572
|
+
end
|
573
|
+
end
|
574
|
+
|
575
|
+
# Runs all the give up callbacks.
|
576
|
+
#
|
577
|
+
# @param exception [Exception]
|
578
|
+
# @param args [Object...]
|
579
|
+
#
|
580
|
+
# @api private
|
581
|
+
def run_give_up_callbacks(exception, *args)
|
582
|
+
give_up_callbacks.each do |callback|
|
583
|
+
call_symbol_or_block(callback, exception, *args)
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
# Helper to call functions that may be passed as Symbols or Procs. If
|
588
|
+
# a symbol, it is assumed to refer to a method that is already defined
|
589
|
+
# on this class.
|
590
|
+
#
|
591
|
+
# @param [Symbol|Proc] method
|
592
|
+
# @param [Object...] *args
|
593
|
+
# @return [Object]
|
594
|
+
#
|
595
|
+
# @api private
|
596
|
+
def call_symbol_or_block(method, *args)
|
597
|
+
if method.is_a?(Symbol)
|
598
|
+
send(method, *args)
|
599
|
+
elsif method.respond_to?(:call)
|
600
|
+
instance_exec(*args, &method)
|
601
|
+
end
|
602
|
+
end
|
455
603
|
end
|
456
604
|
end
|
457
605
|
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class RetryCallbacksTest < Minitest::Test
|
4
|
+
def setup
|
5
|
+
Resque.redis.flushall
|
6
|
+
@worker = Resque::Worker.new(:testing)
|
7
|
+
@worker.register_worker
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_try_again_callbacks_called
|
11
|
+
# Fail, but not fatally
|
12
|
+
Resque.enqueue(RetryCallbacksJob, false)
|
13
|
+
order = sequence('callback_order')
|
14
|
+
|
15
|
+
# Make sure that we're testing both blocks and symbols in our callbacks.
|
16
|
+
refute_empty RetryCallbacksJob.try_again_callbacks.select { |x| x.is_a? Symbol }
|
17
|
+
refute_empty RetryCallbacksJob.try_again_callbacks.select { |x| x.is_a? Proc }
|
18
|
+
|
19
|
+
RetryCallbacksJob.expects(:on_try_again).once
|
20
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
21
|
+
RetryCallbacksJob.expects(:on_try_again_a).once
|
22
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
23
|
+
RetryCallbacksJob.expects(:on_try_again_b).once
|
24
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
25
|
+
RetryCallbacksJob.expects(:on_try_again_c).once
|
26
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
27
|
+
|
28
|
+
RetryCallbacksJob.expects(:on_give_up).never
|
29
|
+
RetryCallbacksJob.expects(:on_give_up_a).never
|
30
|
+
RetryCallbacksJob.expects(:on_give_up_b).never
|
31
|
+
|
32
|
+
perform_next_job(@worker)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_give_up_callbacks_called
|
36
|
+
# Fail fatally
|
37
|
+
Resque.enqueue(RetryCallbacksJob, true)
|
38
|
+
order = sequence('callback_order')
|
39
|
+
|
40
|
+
# Make sure that we're testing both blocks and symbols in our callbacks.
|
41
|
+
refute_empty RetryCallbacksJob.give_up_callbacks.select { |x| x.is_a? Symbol }
|
42
|
+
refute_empty RetryCallbacksJob.give_up_callbacks.select { |x| x.is_a? Proc }
|
43
|
+
|
44
|
+
RetryCallbacksJob.expects(:on_give_up).once
|
45
|
+
.with(instance_of(CustomException), true).in_sequence(order)
|
46
|
+
RetryCallbacksJob.expects(:on_give_up_a).once
|
47
|
+
.with(instance_of(CustomException), true).in_sequence(order)
|
48
|
+
RetryCallbacksJob.expects(:on_give_up_b).once
|
49
|
+
.with(instance_of(CustomException), true).in_sequence(order)
|
50
|
+
RetryCallbacksJob.expects(:on_give_up_c).once
|
51
|
+
.with(instance_of(CustomException), true).in_sequence(order)
|
52
|
+
|
53
|
+
RetryCallbacksJob.expects(:on_try_again).never
|
54
|
+
RetryCallbacksJob.expects(:on_try_again_a).never
|
55
|
+
RetryCallbacksJob.expects(:on_try_again_b).never
|
56
|
+
|
57
|
+
perform_next_job(@worker)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_try_again_callbacks_called_then_give_up
|
61
|
+
# Try once, retry, then try a second time and give up.
|
62
|
+
Resque.enqueue(RetryCallbacksJob, false)
|
63
|
+
order = sequence('callback_order')
|
64
|
+
|
65
|
+
RetryCallbacksJob.expects(:on_try_again).once
|
66
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
67
|
+
RetryCallbacksJob.expects(:on_try_again_a).once
|
68
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
69
|
+
RetryCallbacksJob.expects(:on_try_again_b).once
|
70
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
71
|
+
RetryCallbacksJob.expects(:on_try_again_c).once
|
72
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
73
|
+
|
74
|
+
RetryCallbacksJob.expects(:on_give_up).once
|
75
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
76
|
+
RetryCallbacksJob.expects(:on_give_up_a).once
|
77
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
78
|
+
RetryCallbacksJob.expects(:on_give_up_b).once
|
79
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
80
|
+
RetryCallbacksJob.expects(:on_give_up_c).once
|
81
|
+
.with(instance_of(AnotherCustomException), false).in_sequence(order)
|
82
|
+
|
83
|
+
|
84
|
+
perform_next_job(@worker) # Fail and retry
|
85
|
+
perform_next_job(@worker) # Fail and give up
|
86
|
+
end
|
87
|
+
|
88
|
+
# If an exception is raised in a try again callback, then it should fail and
|
89
|
+
# not be retried.
|
90
|
+
def test_try_again_callback_exception
|
91
|
+
# Trigger a try again callback
|
92
|
+
Resque.enqueue(RetryCallbacksJob, false)
|
93
|
+
|
94
|
+
RetryCallbacksJob.expects(:on_try_again).once.raises(StandardError)
|
95
|
+
RetryCallbacksJob.expects(:on_try_again_a).never
|
96
|
+
RetryCallbacksJob.expects(:on_try_again_b).never
|
97
|
+
RetryCallbacksJob.expects(:on_try_again_c).never
|
98
|
+
|
99
|
+
perform_next_job(@worker)
|
100
|
+
|
101
|
+
assert_equal 0, Resque.info[:pending], 'pending jobs'
|
102
|
+
assert_equal 1, Resque.info[:failed], 'failed jobs'
|
103
|
+
end
|
104
|
+
|
105
|
+
# If an exception is raised in a give up callback, then it should fail and
|
106
|
+
# not be retried.
|
107
|
+
def test_give_up_callback_exception
|
108
|
+
# Trigger a give up callback
|
109
|
+
Resque.enqueue(RetryCallbacksJob, true)
|
110
|
+
|
111
|
+
RetryCallbacksJob.expects(:on_give_up).once.raises(StandardError)
|
112
|
+
RetryCallbacksJob.expects(:on_give_up_a).never
|
113
|
+
RetryCallbacksJob.expects(:on_give_up_b).never
|
114
|
+
RetryCallbacksJob.expects(:on_give_up_c).never
|
115
|
+
|
116
|
+
perform_next_job(@worker)
|
117
|
+
|
118
|
+
assert_equal 0, Resque.info[:pending], 'pending jobs'
|
119
|
+
assert_equal 1, Resque.info[:failed], 'failed jobs'
|
120
|
+
end
|
121
|
+
end
|
data/test/retry_criteria_test.rb
CHANGED
@@ -72,4 +72,15 @@ class RetryCriteriaTest < Minitest::Test
|
|
72
72
|
assert_equal 2, Resque.info[:failed], 'failed jobs'
|
73
73
|
assert_equal 3, Resque.info[:processed], 'processed job'
|
74
74
|
end
|
75
|
+
|
76
|
+
def test_retry_criteria_checks_can_be_registered_with_symbols
|
77
|
+
Resque.enqueue(CustomRetryCriteriaWithSymbol)
|
78
|
+
3.times do
|
79
|
+
perform_next_job(@worker)
|
80
|
+
end
|
81
|
+
|
82
|
+
assert_equal 0, Resque.info[:pending], 'pending jobs'
|
83
|
+
assert_equal 2, Resque.info[:failed], 'failed jobs'
|
84
|
+
assert_equal 2, Resque.info[:processed], 'processed job'
|
85
|
+
end
|
75
86
|
end
|
data/test/test_jobs.rb
CHANGED
@@ -409,6 +409,24 @@ class CustomRetryCriteriaCheckMultipleFailTwice
|
|
409
409
|
end
|
410
410
|
end
|
411
411
|
|
412
|
+
# A job that defines a custom retry criteria check via a symbol, for a method
|
413
|
+
# that is already defined.
|
414
|
+
class CustomRetryCriteriaWithSymbol
|
415
|
+
extend Resque::Plugins::Retry
|
416
|
+
@queue = :testing
|
417
|
+
|
418
|
+
# make sure the retry exceptions check will return false.
|
419
|
+
@retry_exceptions = [CustomException]
|
420
|
+
|
421
|
+
retry_criteria_check :yes
|
422
|
+
|
423
|
+
def self.yes(ex, *args); true; end
|
424
|
+
|
425
|
+
def self.perform(*args)
|
426
|
+
raise
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
412
430
|
# A job to test whether self.inherited is respected
|
413
431
|
# when added by other modules.
|
414
432
|
class InheritOrderingJobExtendFirst
|
@@ -512,3 +530,52 @@ class RetryKilledJob
|
|
512
530
|
Process.kill("KILL", Process.pid)
|
513
531
|
end
|
514
532
|
end
|
533
|
+
|
534
|
+
class RetryCallbacksJob
|
535
|
+
extend Resque::Plugins::Retry
|
536
|
+
@queue = :testing
|
537
|
+
|
538
|
+
@fatal_exceptions = [CustomException]
|
539
|
+
@retry_exceptions = [AnotherCustomException]
|
540
|
+
@retry_limit = 1
|
541
|
+
|
542
|
+
def self.perform(is_fatal)
|
543
|
+
if is_fatal
|
544
|
+
raise CustomException, "RetryCallbacksJob failed fatally"
|
545
|
+
else
|
546
|
+
raise AnotherCustomException, "RetryCallbacksJob failed"
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def self.on_try_again(ex, *args); end
|
551
|
+
def self.on_try_again_a(ex, *args); end
|
552
|
+
def self.on_try_again_b(ex, *args); end
|
553
|
+
def self.on_try_again_c(ex, *args); end
|
554
|
+
|
555
|
+
def self.on_give_up(ex, *args); end
|
556
|
+
def self.on_give_up_a(ex, *args); end
|
557
|
+
def self.on_give_up_b(ex, *args); end
|
558
|
+
def self.on_give_up_c(ex, *args); end
|
559
|
+
|
560
|
+
@try_again_callbacks = [
|
561
|
+
lambda { |*args| self.on_try_again(*args) },
|
562
|
+
:on_try_again_a
|
563
|
+
]
|
564
|
+
|
565
|
+
try_again_callback do |*args|
|
566
|
+
on_try_again_b(*args)
|
567
|
+
end
|
568
|
+
|
569
|
+
try_again_callback :on_try_again_c
|
570
|
+
|
571
|
+
@give_up_callbacks = [
|
572
|
+
lambda { |*args| self.on_give_up(*args) },
|
573
|
+
:on_give_up_a
|
574
|
+
]
|
575
|
+
|
576
|
+
give_up_callback do |*args|
|
577
|
+
on_give_up_b(*args)
|
578
|
+
end
|
579
|
+
|
580
|
+
give_up_callback :on_give_up_c
|
581
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: resque-retry
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luke Antins
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2015-
|
13
|
+
date: 2015-10-24 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: resque
|
@@ -184,6 +184,7 @@ files:
|
|
184
184
|
- test/multiple_failure_test.rb
|
185
185
|
- test/redis-test.conf
|
186
186
|
- test/resque_test.rb
|
187
|
+
- test/retry_callbacks_test.rb
|
187
188
|
- test/retry_criteria_test.rb
|
188
189
|
- test/retry_exception_delay_test.rb
|
189
190
|
- test/retry_inheriting_checks_test.rb
|