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