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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d4c061060637465cbab7c2348d2facefa1816493
4
- data.tar.gz: 3b76a1d34c68eeb3fae868900b75ca02192bc851
3
+ metadata.gz: e13254ff1b1a5e06977bbbbfa6a28843629f2133
4
+ data.tar.gz: e9ebf7d623865a0d067c060a804ccae233a34728
5
5
  SHA512:
6
- metadata.gz: 63bcdd107167973953477e6e591c8ce86d2a2957b41bb93cbe161153f5f2e6fd69e417718ba46d497f755fcde882373f2f992538c48b7aa11b7a449ba2aec867
7
- data.tar.gz: 4d34d77f1fe0b4ae1728495865f79f20b05ef7896ddf30d23d70cdef2d03caf57ff995617e2e6aff3b854a34865933a05913a638e2fb0e285f75685bb334771a
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][rq] plugin. Requires Resque ~> 1.25 & [resque-scheduler][rqs] ~> 4.0.
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
- $ gem install resque-retry
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][rqs].
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 suppression.
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
- logged in the Redis failed queue *(visible via resque-web)*.
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
- the additional resque-retry tabs. In order to do this, you must first create
143
- a configuration file. For the sake of this example we'll create the configuration
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'd like and place in your
146
- project in any directory of your choosing. The contents of the configuration file
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
- some ideas =), adapt for your own usage and feel free to pick and mix!
175
-
176
- ### Retry Defaults
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
- a delay of 120 seconds, or 2 minutes between retry attempts.
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
- Alternatively you could override the `retry_delay` method to do something
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 while the job is
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 method `sleep_after_requeue` to set the sleep value
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
- no delay, 1m, 10m, 1h, 3h, 6h
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
- Again, tweak to your own needs.
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
- array, unless you set `retry_limit` yourself.
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
- it so only specific exceptions are retried using `retry_exceptions`:
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
- exponential backoff.
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
- Use `@retry_exceptions = []` to **only** use callbacks, to determine if the
378
- job should retry.
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
- ### Retry Arguments
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
- You may override `retry_args`, which is passed the current
383
- job arguments, to modify the arguments for the next retry attempt.
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
- exception thrown, you may override `retry_args_for_exception`, which is passed
402
- the exception and the current job arguments, to modify the arguments for the
403
- next retry attempt.
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
- built using the `retry_identifier`. If you have a lot of arguments or really long
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 retry identifier is just your job arguments joined with a dash `-`.
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 its not expired too soon.
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
- [god]: http://github.com/mojombo/god
487
- [rq]: http://github.com/resque/resque
488
- [rqs]: http://github.com/resque/resque-scheduler
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
@@ -1,3 +1,3 @@
1
1
  module ResqueRetry
2
- VERSION = '1.4.0'
2
+ VERSION = '1.5.0'
3
3
  end
@@ -52,12 +52,15 @@ module Resque
52
52
  end
53
53
  end
54
54
 
55
- # Copy retry criteria checks on inheritance.
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
- retry_criteria_checks.each do |criteria_check|
275
- retry_based_on_criteria ||= !!instance_exec(exception, *args, &criteria_check)
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 Using a custom retry criteria check.
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 = can retry
332
+ # @yieldreturn [Boolean] false == dont retry, true == can retry
327
333
  #
328
334
  # @api public
329
- def retry_criteria_check(&block)
330
- retry_criteria_checks << block
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
- log_message 'retry criteria not sufficient for retry', args, exception
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
@@ -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
@@ -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.0
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-01-07 00:00:00.000000000 Z
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