resque-retry 1.4.0 → 1.5.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/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
|