resque-retry 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Luke Antins
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ Software), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,164 @@
1
+ resque-retry
2
+ ============
3
+
4
+ A [Resque][rq] plugin. Requires Resque 1.8.0.
5
+
6
+ resque-retry provides retry, delay and exponential backoff support for
7
+ resque jobs.
8
+
9
+ ### Features
10
+
11
+ - Redis backed retry count/limit.
12
+ - Retry on all or specific exceptions.
13
+ - Exponential backoff (varying the delay between retrys).
14
+ - Small & Extendable - plenty of places to override retry logic/settings.
15
+
16
+ **n.b.** [resque-scheduler][rqs] is _really_ recommended if you wish to
17
+ delay between retry attempts, otherwise your workers will block
18
+ using `sleep`.
19
+
20
+ Usage / Examples
21
+ ----------------
22
+
23
+ Just extend your module/class with this module, and your ready to retry!
24
+
25
+ Customisation is pretty easy, the below examples should give you
26
+ some ideas =), adapt for your own usage and feel free to pick and mix!
27
+
28
+ ### Retry
29
+
30
+ Retry the job **once** on failure, with zero delay.
31
+
32
+ require 'require-retry'
33
+
34
+ class DeliverWebHook
35
+ extend Resque::Plugins::Retry
36
+
37
+ def self.perform(url, hook_id, hmac_key)
38
+ heavy_lifting
39
+ end
40
+ end
41
+
42
+ When a job runs, the number of retry attempts is checked and incremented
43
+ in Redis. If your job fails, the number of retry attempts is used to
44
+ determine if we can requeue the job for another go.
45
+
46
+ ### Custom Retry
47
+
48
+ class DeliverWebHook
49
+ extend Resque::Plugins::Retry
50
+ @retry_limit = 10
51
+ @retry_delay = 120
52
+
53
+ def self.perform(url, hook_id, hmac_key)
54
+ heavy_lifting
55
+ end
56
+ end
57
+
58
+ The above modification will allow your job to retry upto 10 times, with
59
+ a delay of 120 seconds, or 2 minutes between retry attempts.
60
+
61
+ Alternatively you could override the `retry_delay` method to do something
62
+ more special.
63
+
64
+ ### Exponential Backoff
65
+
66
+ Use this if you wish to vary the delay between retry attempts:
67
+
68
+ class DeliverSMS
69
+ extend Resque::Plugins::ExponentialBackoff
70
+
71
+ def self.perform(mt_id, mobile_number, message)
72
+ heavy_lifting
73
+ end
74
+ end
75
+
76
+ **Default Settings**
77
+
78
+ key: m = minutes, h = hours
79
+
80
+ no delay, 1m, 10m, 1h, 3h, 6h
81
+ @backoff_strategy = [0, 60, 600, 3600, 10800, 21600]
82
+
83
+ The first delay will be 0 seconds, the 2nd will be 60 seconds, etc...
84
+ Again, tweak to your own needs.
85
+
86
+ The number if retrys is equal to the size of the `backoff_strategy`
87
+ array, unless you set `retry_limit` yourself.
88
+
89
+ ### Retry Specific Exceptions
90
+
91
+ The default will allow a retry for any type of exception. You may change
92
+ it so only specific exceptions are retried using `retry_exceptions`:
93
+
94
+ class DeliverSMS
95
+ extend Resque::Plugins::Retry
96
+ @retry_exceptions = [NetworkError]
97
+
98
+ def self.perform(mt_id, mobile_number, message)
99
+ heavy_lifting
100
+ end
101
+ end
102
+
103
+ The above modification will **only** retry if a `NetworkError` (or subclass)
104
+ exception is thrown.
105
+
106
+ Customise & Extend
107
+ ------------------
108
+
109
+ Please take a look at the yardoc/code for more details on methods you may
110
+ wish to override.
111
+
112
+ Some things worth noting:
113
+
114
+ ### Job Identifier/Key
115
+
116
+ The retry attempt is incremented and stored in a Redis key. The key is
117
+ built using the `identifier`. If you have a lot of arguments or really long
118
+ ones, you should consider overriding `identifier` to define a more precise
119
+ or loose custom identifier.
120
+
121
+ The default identifier is just your job arguments joined with a dash `-`.
122
+
123
+ By default the key uses this format:
124
+ `resque-retry:<job class name>:<identifier>`.
125
+
126
+ Or you can define the entire key by overriding `redis_retry_key`.
127
+
128
+ class DeliverSMS
129
+ extend Resque::Plugins::Retry
130
+
131
+ def self.identifier(mt_id, mobile_number, message)
132
+ "#{mobile_number}:#{mt_id}"
133
+ end
134
+
135
+ self.perform(mt_id, mobile_number, message)
136
+ heavy_lifting
137
+ end
138
+ end
139
+
140
+ ### Retry Arguments
141
+
142
+ You may override `args_for_retry`, which is passed the current
143
+ job arguments, to modify the arguments for the next retry attempt.
144
+
145
+ class DeliverViaSMSC
146
+ extend Resque::Plugins::Retry
147
+
148
+ # retry using the emergency SMSC.
149
+ def self.args_for_retry(smsc_id, mt_message)
150
+ [999, mt_message]
151
+ end
152
+
153
+ self.perform(smsc_id, mt_message)
154
+ heavy_lifting
155
+ end
156
+ end
157
+
158
+ Install
159
+ -------
160
+
161
+ $ gem install resque-retry
162
+
163
+ [rq]: http://github.com/defunkt/resque
164
+ [rqs]: http://github.com/bvandenbos/resque-scheduler
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.unshift 'lib'
2
+
3
+ require 'rake/testtask'
4
+ require 'fileutils'
5
+ require 'yard'
6
+ require 'yard/rake/yardoc_task'
7
+
8
+ task :default => :test
9
+
10
+ ##
11
+ # Test task.
12
+ Rake::TestTask.new(:test) do |task|
13
+ task.test_files = FileList['test/*_test.rb']
14
+ task.verbose = true
15
+ end
16
+
17
+ ##
18
+ # docs task.
19
+ YARD::Rake::YardocTask.new :yardoc do |t|
20
+ t.files = ['lib/**/*.rb']
21
+ t.options = ['--output-dir', "doc/",
22
+ '--files', 'LICENSE',
23
+ '--readme', 'README.md',
24
+ '--title', 'resque-exponential-backoff documentation']
25
+ end
@@ -0,0 +1,4 @@
1
+ require 'resque'
2
+
3
+ require 'resque/plugins/retry'
4
+ require 'resque/plugins/exponential_backoff'
@@ -0,0 +1,66 @@
1
+ module Resque
2
+ module Plugins
3
+
4
+ ##
5
+ # If you want your job to retry on failure using a varying delay, simply
6
+ # extend your module/class with this module:
7
+ #
8
+ # class DeliverSMS
9
+ # extend Resque::Plugins::ExponentialBackoff
10
+ #
11
+ # def self.perform(mt_id, mobile_number, message)
12
+ # heavy_lifting
13
+ # end
14
+ # end
15
+ #
16
+ # Easily do something custom:
17
+ #
18
+ # class DeliverSMS
19
+ # extend Resque::Plugins::ExponentialBackoff
20
+ #
21
+ # @retry_limit = 4
22
+ #
23
+ # # retry delay in seconds; [0] => 1st retry, [1] => 2nd..4th retry.
24
+ # @backoff_strategy = [0, 60]
25
+ #
26
+ # # used to build redis key, for counting job attempts.
27
+ # def self.identifier(mt_id, mobile_number, message)
28
+ # "#{mobile_number}:#{mt_id}"
29
+ # end
30
+ #
31
+ # self.perform(mt_id, mobile_number, message)
32
+ # heavy_lifting
33
+ # end
34
+ # end
35
+ #
36
+ module ExponentialBackoff
37
+ include Resque::Plugins::Retry
38
+
39
+ ##
40
+ # Defaults to the number of delays in the backoff strategy.
41
+ #
42
+ # @return [Number] maximum number of retries
43
+ def retry_limit
44
+ @retry_limit ||= backoff_strategy.length
45
+ end
46
+
47
+ ##
48
+ # Selects the delay from the backoff strategy.
49
+ #
50
+ # @return [Number] seconds to delay until the next retry.
51
+ def retry_delay
52
+ backoff_strategy[retry_attempt] || backoff_strategy.last
53
+ end
54
+
55
+ ##
56
+ # @abstract
57
+ # The backoff strategy is used to vary the delay between retry attempts.
58
+ #
59
+ # @return [Array] array of delays. index = retry attempt
60
+ def backoff_strategy
61
+ @backoff_strategy ||= [0, 60, 600, 3600, 10_800, 21_600]
62
+ end
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,178 @@
1
+ module Resque
2
+ module Plugins
3
+
4
+ ##
5
+ # If you want your job to retry on failure, simply extend your module/class
6
+ # with this module:
7
+ #
8
+ # class DeliverWebHook
9
+ # extend Resque::Plugins::Retry # allows 1 retry by default.
10
+ #
11
+ # def self.perform(url, hook_id, hmac_key)
12
+ # heavy_lifting
13
+ # end
14
+ # end
15
+ #
16
+ # Easily do something custom:
17
+ #
18
+ # class DeliverWebHook
19
+ # extend Resque::Plugins::Retry
20
+ #
21
+ # @retry_limit = 8 # default: 1
22
+ # @retry_delay = 60 # default: 0
23
+ #
24
+ # # used to build redis key, for counting job attempts.
25
+ # def self.identifier(url, hook_id, hmac_key)
26
+ # "#{url}-#{hook_id}"
27
+ # end
28
+ #
29
+ # def self.perform(url, hook_id, hmac_key)
30
+ # heavy_lifting
31
+ # end
32
+ # end
33
+ #
34
+ module Retry
35
+ ##
36
+ # @abstract You may override to implement a custom identifier,
37
+ # you should consider doing this if your job arguments
38
+ # are many/long or may not cleanly cleanly to strings.
39
+ #
40
+ # Builds an identifier using the job arguments. This identifier
41
+ # is used as part of the redis key.
42
+ #
43
+ # @param [Array] args job arguments
44
+ # @return [String] job identifier
45
+ def identifier(*args)
46
+ args.join('-')
47
+ end
48
+
49
+ ##
50
+ # Builds the redis key to be used for keeping state of the job
51
+ # attempts.
52
+ #
53
+ # @return [String] redis key
54
+ def redis_retry_key(*args)
55
+ ['resque-retry', name, identifier(*args)].compact.join(":")
56
+ end
57
+
58
+ ##
59
+ # Maximum number of retrys we can attempt to successfully perform the job.
60
+ # A retry limit of 0 or below will retry forever.
61
+ #
62
+ # @return [Fixnum]
63
+ def retry_limit
64
+ @retry_limit ||= 1
65
+ end
66
+
67
+ ##
68
+ # Number of retry attempts used to try and perform the job.
69
+ #
70
+ # The real value is kept in Redis, it is accessed and incremented using
71
+ # a before_perform hook.
72
+ #
73
+ # @return [Fixnum] number of attempts
74
+ def retry_attempt
75
+ @retry_attempt ||= 0
76
+ end
77
+
78
+ ##
79
+ # @abstract
80
+ # Number of seconds to delay until the job is retried.
81
+ #
82
+ # @return [Number] number of seconds to delay
83
+ def retry_delay
84
+ @retry_delay ||= 0
85
+ end
86
+
87
+ ##
88
+ # @abstract
89
+ # Modify the arguments used to retry the job. Use this to do something
90
+ # other than try the exact same job again.
91
+ #
92
+ # @return [Array] new job arguments
93
+ def args_for_retry(*args)
94
+ args
95
+ end
96
+
97
+ ##
98
+ # Convenience method to test whether you may retry on a given exception.
99
+ #
100
+ # @return [Boolean]
101
+ def retry_exception?(exception)
102
+ return true if retry_exceptions.nil?
103
+ !! retry_exceptions.any? { |ex| ex >= exception }
104
+ end
105
+
106
+ ##
107
+ # @abstract
108
+ # Controls what exceptions may be retried.
109
+ #
110
+ # Default: `nil` - this will retry all exceptions.
111
+ #
112
+ # @return [Array, nil]
113
+ def retry_exceptions
114
+ @retry_exceptions ||= nil
115
+ end
116
+
117
+ ##
118
+ # Test if the retry criteria is valid.
119
+ #
120
+ # @param [Exception] exception
121
+ # @param [Array] args job arguments
122
+ # @return [Boolean]
123
+ def retry_criteria_valid?(exception, *args)
124
+ # FIXME: let people extend retry criteria, give them a chance to say no.
125
+ if retry_limit > 0
126
+ return false if retry_attempt >= retry_limit
127
+ end
128
+ retry_exception?(exception.class)
129
+ end
130
+
131
+ ##
132
+ # Will retry the job.
133
+ #
134
+ # n.b. If your not using the resque-scheduler plugin your job will block
135
+ # your worker, while it sleeps for `retry_delay`.
136
+ def try_again(*args)
137
+ if Resque.respond_to?(:enqueue_in) && retry_delay > 0
138
+ Resque.enqueue_in(retry_delay, self, *args_for_retry(*args))
139
+ else
140
+ sleep(retry_delay) if retry_delay > 0
141
+ Resque.enqueue(self, *args_for_retry(*args))
142
+ end
143
+ end
144
+
145
+ ##
146
+ # Resque before_perform hook.
147
+ #
148
+ # Increments and sets the `@retry_attempt` count.
149
+ def before_perform_retry(*args)
150
+ retry_key = redis_retry_key(*args)
151
+ Resque.redis.setnx(retry_key, -1) # default to -1 if not set.
152
+ @retry_attempt = Resque.redis.incr(retry_key) # increment by 1.
153
+ end
154
+
155
+ ##
156
+ # Resque after_perform hook.
157
+ #
158
+ # Deletes retry attempt count from Redis.
159
+ def after_perform_retry(*args)
160
+ Resque.redis.del(redis_retry_key(*args))
161
+ end
162
+
163
+ ##
164
+ # Resque on_failure hook.
165
+ #
166
+ # Checks if our retry criteria is valid, if it is we try again.
167
+ # Otherwise the retry attempt count is deleted from Redis.
168
+ def on_failure_retry(exception, *args)
169
+ if retry_criteria_valid?(exception, *args)
170
+ try_again(*args)
171
+ else
172
+ delete_retry_redis_key(*args)
173
+ end
174
+ end
175
+ end
176
+
177
+ end
178
+ end
@@ -0,0 +1,59 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class ExponentialBackoffTest < Test::Unit::TestCase
4
+ def setup
5
+ Resque.redis.flushall
6
+ @worker = Resque::Worker.new(:testing)
7
+ @worker.register_worker
8
+ end
9
+
10
+ def test_resque_plugin_lint
11
+ assert_nothing_raised do
12
+ Resque::Plugin.lint(Resque::Plugins::ExponentialBackoff)
13
+ end
14
+ end
15
+
16
+ def test_default_backoff_strategy
17
+ now = Time.now
18
+ Resque.enqueue(ExponentialBackoffJob)
19
+ 2.times do
20
+ perform_next_job @worker
21
+ end
22
+
23
+ assert_equal 2, Resque.info[:processed], 'processed jobs'
24
+ assert_equal 2, Resque.info[:failed], 'failed jobs'
25
+ assert_equal 0, Resque.info[:pending], 'pending jobs'
26
+
27
+ delayed = Resque.delayed_queue_peek(0, 1)
28
+ assert_equal now.to_i + 60, delayed[0], '2nd delay' # the first had a zero delay.
29
+
30
+ 5.times do
31
+ Resque.enqueue(ExponentialBackoffJob)
32
+ perform_next_job @worker
33
+ end
34
+
35
+ delayed = Resque.delayed_queue_peek(0, 5)
36
+ assert_equal now.to_i + 600, delayed[1], '3rd delay'
37
+ assert_equal now.to_i + 3600, delayed[2], '4th delay'
38
+ assert_equal now.to_i + 10_800, delayed[3], '5th delay'
39
+ assert_equal now.to_i + 21_600, delayed[4], '6th delay'
40
+ end
41
+
42
+ def test_custom_backoff_strategy
43
+ now = Time.now
44
+ 4.times do
45
+ Resque.enqueue(CustomExponentialBackoffJob, 'http://lividpenguin.com', 1305, 'cd8079192d379dc612f17c660591a6cfb05f1dda')
46
+ perform_next_job @worker
47
+ end
48
+
49
+ delayed = Resque.delayed_queue_peek(0, 3)
50
+ assert_equal now.to_i + 10, delayed[0], '1st delay'
51
+ assert_equal now.to_i + 20, delayed[1], '2nd delay'
52
+ assert_equal now.to_i + 30, delayed[2], '3rd delay'
53
+ assert_equal 2, Resque.delayed_timestamp_size(delayed[2]), '4th delay should share delay with 3rd'
54
+
55
+ assert_equal 4, Resque.info[:processed], 'processed jobs'
56
+ assert_equal 4, Resque.info[:failed], 'failed jobs'
57
+ assert_equal 0, Resque.info[:pending], 'pending jobs'
58
+ end
59
+ end
@@ -0,0 +1,132 @@
1
+ # Redis configuration file example
2
+
3
+ # By default Redis does not run as a daemon. Use 'yes' if you need it.
4
+ # Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
5
+ daemonize yes
6
+
7
+ # When run as a daemon, Redis write a pid file in /var/run/redis.pid by default.
8
+ # You can specify a custom pid file location here.
9
+ pidfile ./test/redis-test.pid
10
+
11
+ # Accept connections on the specified port, default is 6379
12
+ port 9736
13
+
14
+ # If you want you can bind a single interface, if the bind option is not
15
+ # specified all the interfaces will listen for connections.
16
+ #
17
+ # bind 127.0.0.1
18
+
19
+ # Close the connection after a client is idle for N seconds (0 to disable)
20
+ timeout 300
21
+
22
+ # Save the DB on disk:
23
+ #
24
+ # save <seconds> <changes>
25
+ #
26
+ # Will save the DB if both the given number of seconds and the given
27
+ # number of write operations against the DB occurred.
28
+ #
29
+ # In the example below the behaviour will be to save:
30
+ # after 900 sec (15 min) if at least 1 key changed
31
+ # after 300 sec (5 min) if at least 10 keys changed
32
+ # after 60 sec if at least 10000 keys changed
33
+ save 900 1
34
+ save 300 10
35
+ save 60 10000
36
+
37
+ # The filename where to dump the DB
38
+ dbfilename dump.rdb
39
+
40
+ # For default save/load DB in/from the working directory
41
+ # Note that you must specify a directory not a file name.
42
+ dir ./test/
43
+
44
+ # Set server verbosity to 'debug'
45
+ # it can be one of:
46
+ # debug (a lot of information, useful for development/testing)
47
+ # notice (moderately verbose, what you want in production probably)
48
+ # warning (only very important / critical messages are logged)
49
+ loglevel debug
50
+
51
+ # Specify the log file name. Also 'stdout' can be used to force
52
+ # the demon to log on the standard output. Note that if you use standard
53
+ # output for logging but daemonize, logs will be sent to /dev/null
54
+ logfile stdout
55
+
56
+ # Set the number of databases. The default database is DB 0, you can select
57
+ # a different one on a per-connection basis using SELECT <dbid> where
58
+ # dbid is a number between 0 and 'databases'-1
59
+ databases 16
60
+
61
+ ################################# REPLICATION #################################
62
+
63
+ # Master-Slave replication. Use slaveof to make a Redis instance a copy of
64
+ # another Redis server. Note that the configuration is local to the slave
65
+ # so for example it is possible to configure the slave to save the DB with a
66
+ # different interval, or to listen to another port, and so on.
67
+
68
+ # slaveof <masterip> <masterport>
69
+
70
+ ################################## SECURITY ###################################
71
+
72
+ # Require clients to issue AUTH <PASSWORD> before processing any other
73
+ # commands. This might be useful in environments in which you do not trust
74
+ # others with access to the host running redis-server.
75
+ #
76
+ # This should stay commented out for backward compatibility and because most
77
+ # people do not need auth (e.g. they run their own servers).
78
+
79
+ # requirepass foobared
80
+
81
+ ################################### LIMITS ####################################
82
+
83
+ # Set the max number of connected clients at the same time. By default there
84
+ # is no limit, and it's up to the number of file descriptors the Redis process
85
+ # is able to open. The special value '0' means no limts.
86
+ # Once the limit is reached Redis will close all the new connections sending
87
+ # an error 'max number of clients reached'.
88
+
89
+ # maxclients 128
90
+
91
+ # Don't use more memory than the specified amount of bytes.
92
+ # When the memory limit is reached Redis will try to remove keys with an
93
+ # EXPIRE set. It will try to start freeing keys that are going to expire
94
+ # in little time and preserve keys with a longer time to live.
95
+ # Redis will also try to remove objects from free lists if possible.
96
+ #
97
+ # If all this fails, Redis will start to reply with errors to commands
98
+ # that will use more memory, like SET, LPUSH, and so on, and will continue
99
+ # to reply to most read-only commands like GET.
100
+ #
101
+ # WARNING: maxmemory can be a good idea mainly if you want to use Redis as a
102
+ # 'state' server or cache, not as a real DB. When Redis is used as a real
103
+ # database the memory usage will grow over the weeks, it will be obvious if
104
+ # it is going to use too much memory in the long run, and you'll have the time
105
+ # to upgrade. With maxmemory after the limit is reached you'll start to get
106
+ # errors for write operations, and this may even lead to DB inconsistency.
107
+
108
+ # maxmemory <bytes>
109
+
110
+ ############################### ADVANCED CONFIG ###############################
111
+
112
+ # Glue small output buffers together in order to send small replies in a
113
+ # single TCP packet. Uses a bit more CPU but most of the times it is a win
114
+ # in terms of number of queries per second. Use 'yes' if unsure.
115
+ glueoutputbuf yes
116
+
117
+ # Use object sharing. Can save a lot of memory if you have many common
118
+ # string in your dataset, but performs lookups against the shared objects
119
+ # pool so it uses more CPU and can be a bit slower. Usually it's a good
120
+ # idea.
121
+ #
122
+ # When object sharing is enabled (shareobjects yes) you can use
123
+ # shareobjectspoolsize to control the size of the pool used in order to try
124
+ # object sharing. A bigger pool size will lead to better sharing capabilities.
125
+ # In general you want this value to be at least the double of the number of
126
+ # very common strings you have in your dataset.
127
+ #
128
+ # WARNING: object sharing is experimental, don't enable this feature
129
+ # in production before of Redis 1.0-stable. Still please try this feature in
130
+ # your development environment so that we can test it better.
131
+ shareobjects no
132
+ shareobjectspoolsize 1024
@@ -0,0 +1,18 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ # make sure the worlds not fallen from beneith us.
4
+ class ResqueTest < Test::Unit::TestCase
5
+ def test_resque_version
6
+ major, minor, patch = Resque::Version.split('.')
7
+ assert_equal 1, major.to_i, 'major version does not match'
8
+ assert_operator minor.to_i, :>=, 8, 'minor version is too low'
9
+ end
10
+
11
+ def test_good_job
12
+ clean_perform_job(GoodJob, 1234, { :cats => :maiow }, [true, false, false])
13
+
14
+ assert_equal 0, Resque.info[:failed], 'failed jobs'
15
+ assert_equal 1, Resque.info[:processed], 'processed job'
16
+ assert_equal 0, Resque.delayed_queue_schedule_size
17
+ end
18
+ end
@@ -0,0 +1,108 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class RetryTest < Test::Unit::TestCase
4
+ def setup
5
+ Resque.redis.flushall
6
+ @worker = Resque::Worker.new(:testing)
7
+ @worker.register_worker
8
+ end
9
+
10
+ def test_resque_plugin_lint
11
+ assert_nothing_raised do
12
+ Resque::Plugin.lint(Resque::Plugins::Retry)
13
+ end
14
+ end
15
+
16
+ def test_default_settings
17
+ assert_equal 1, RetryDefaultsJob.retry_limit, 'default retry limit'
18
+ assert_equal 0, RetryDefaultsJob.retry_attempt, 'default number of retry attempts'
19
+ assert_equal nil, RetryDefaultsJob.retry_exceptions, 'default retry exceptions; nil = any'
20
+ assert_equal 0, RetryDefaultsJob.retry_delay, 'default seconds until retry'
21
+ end
22
+
23
+ def test_retry_once_by_default
24
+ Resque.enqueue(RetryDefaultsJob)
25
+ 3.times do
26
+ perform_next_job(@worker)
27
+ end
28
+
29
+ assert_equal 0, Resque.info[:pending], 'pending jobs'
30
+ assert_equal 2, Resque.info[:failed], 'failed jobs'
31
+ assert_equal 2, Resque.info[:processed], 'processed job'
32
+ end
33
+
34
+ def test_job_args_are_maintained
35
+ test_args = ['maiow', 'cat', [42, 84]]
36
+
37
+ Resque.enqueue(RetryDefaultsJob, *test_args)
38
+ perform_next_job(@worker)
39
+
40
+ assert job = Resque.pop(:testing)
41
+ assert_equal test_args, job['args']
42
+ end
43
+
44
+ def test_job_args_may_be_modified
45
+ Resque.enqueue(RetryWithModifiedArgsJob, 'foo', 'bar')
46
+ perform_next_job(@worker)
47
+
48
+ assert job = Resque.pop(:testing)
49
+ assert_equal ['foobar', 'barbar'], job['args']
50
+ end
51
+
52
+ def test_retry_never_give_up
53
+ Resque.enqueue(NeverGiveUpJob)
54
+ 10.times do
55
+ perform_next_job(@worker)
56
+ end
57
+
58
+ assert_equal 1, Resque.info[:pending], 'pending jobs'
59
+ assert_equal 10, Resque.info[:failed], 'failed jobs'
60
+ assert_equal 10, Resque.info[:processed], 'processed job'
61
+ end
62
+
63
+ def test_fail_five_times_then_succeed
64
+ Resque.enqueue(FailFiveTimesJob)
65
+ 7.times do
66
+ perform_next_job(@worker)
67
+ end
68
+
69
+ assert_equal 5, Resque.info[:failed], 'failed jobs'
70
+ assert_equal 6, Resque.info[:processed], 'processed job'
71
+ assert_equal 0, Resque.info[:pending], 'pending jobs'
72
+ end
73
+
74
+ def test_can_determine_if_exception_may_be_retried
75
+ assert_equal true, RetryDefaultsJob.retry_exception?(StandardError), 'StandardError may retry'
76
+ assert_equal true, RetryDefaultsJob.retry_exception?(CustomException), 'CustomException may retry'
77
+ assert_equal true, RetryDefaultsJob.retry_exception?(HierarchyCustomException), 'HierarchyCustomException may retry'
78
+
79
+ assert_equal true, RetryCustomExceptionsJob.retry_exception?(CustomException), 'CustomException may retry'
80
+ assert_equal true, RetryCustomExceptionsJob.retry_exception?(HierarchyCustomException), 'HierarchyCustomException may retry'
81
+ assert_equal false, RetryCustomExceptionsJob.retry_exception?(AnotherCustomException), 'AnotherCustomException may not retry'
82
+ end
83
+
84
+ def test_retry_if_failed_and_exception_may_retry
85
+ Resque.enqueue(RetryCustomExceptionsJob, CustomException)
86
+ Resque.enqueue(RetryCustomExceptionsJob, HierarchyCustomException)
87
+ 4.times do
88
+ perform_next_job(@worker)
89
+ end
90
+
91
+ assert_equal 4, Resque.info[:failed], 'failed jobs'
92
+ assert_equal 4, Resque.info[:processed], 'processed job'
93
+ assert_equal 2, Resque.info[:pending], 'pending jobs'
94
+ end
95
+
96
+ def test_do_not_retry_if_failed_and_exception_does_not_allow_retry
97
+ Resque.enqueue(RetryCustomExceptionsJob, AnotherCustomException)
98
+ Resque.enqueue(RetryCustomExceptionsJob, RuntimeError)
99
+ 4.times do
100
+ perform_next_job(@worker)
101
+ end
102
+
103
+ assert_equal 2, Resque.info[:failed], 'failed jobs'
104
+ assert_equal 2, Resque.info[:processed], 'processed job'
105
+ assert_equal 0, Resque.info[:pending], 'pending jobs'
106
+ end
107
+
108
+ end
@@ -0,0 +1,65 @@
1
+ dir = File.dirname(File.expand_path(__FILE__))
2
+ $LOAD_PATH.unshift dir + '/../lib'
3
+ $TESTING = true
4
+
5
+ require 'test/unit'
6
+ require 'rubygems'
7
+ require 'resque'
8
+ require 'resque_scheduler'
9
+ require 'turn'
10
+
11
+ require 'resque-retry'
12
+ require dir + '/test_jobs'
13
+
14
+
15
+ ##
16
+ # make sure we can run redis
17
+ if !system("which redis-server")
18
+ puts '', "** can't find `redis-server` in your path"
19
+ puts "** try running `sudo rake install`"
20
+ abort ''
21
+ end
22
+
23
+
24
+ ##
25
+ # start our own redis when the tests start,
26
+ # kill it when they end
27
+ at_exit do
28
+ next if $!
29
+
30
+ if defined?(MiniTest)
31
+ exit_code = MiniTest::Unit.new.run(ARGV)
32
+ else
33
+ exit_code = Test::Unit::AutoRunner.run
34
+ end
35
+
36
+ pid = `ps -e -o pid,command | grep [r]edis-test`.split(" ")[0]
37
+ puts "Killing test redis server..."
38
+ `rm -f #{dir}/dump.rdb`
39
+ Process.kill("KILL", pid.to_i)
40
+ exit exit_code
41
+ end
42
+
43
+ puts "Starting redis for testing at localhost:9736..."
44
+ `redis-server #{dir}/redis-test.conf`
45
+ Resque.redis = '127.0.0.1:9736'
46
+
47
+ ##
48
+ # Test helpers
49
+ class Test::Unit::TestCase
50
+ def perform_next_job(worker, &block)
51
+ return unless job = @worker.reserve
52
+ @worker.perform(job, &block)
53
+ @worker.done_working
54
+ end
55
+
56
+ def clean_perform_job(klass, *args)
57
+ Resque.redis.flushall
58
+ Resque.enqueue(klass, *args)
59
+
60
+ worker = Resque::Worker.new(:testing)
61
+ return false unless job = worker.reserve
62
+ worker.perform(job)
63
+ worker.done_working
64
+ end
65
+ end
data/test/test_jobs.rb ADDED
@@ -0,0 +1,73 @@
1
+ CustomException = Class.new(StandardError)
2
+ HierarchyCustomException = Class.new(CustomException)
3
+ AnotherCustomException = Class.new(StandardError)
4
+
5
+ class GoodJob
6
+ @queue = :testing
7
+ def self.perform(*args)
8
+ end
9
+ end
10
+
11
+ class RetryDefaultsJob
12
+ extend Resque::Plugins::Retry
13
+ @queue = :testing
14
+
15
+ def self.perform(*args)
16
+ raise
17
+ end
18
+ end
19
+
20
+ class RetryWithModifiedArgsJob < RetryDefaultsJob
21
+ @queue = :testing
22
+
23
+ def self.args_for_retry(*args)
24
+ args.each { |arg| arg << 'bar' }
25
+ end
26
+ end
27
+
28
+ class NeverGiveUpJob < RetryDefaultsJob
29
+ @queue = :testing
30
+ @retry_limit = 0
31
+ end
32
+
33
+ class FailFiveTimesJob < RetryDefaultsJob
34
+ @queue = :testing
35
+ @retry_limit = 6
36
+
37
+ def self.perform(*args)
38
+ raise if retry_attempt <= 4
39
+ end
40
+ end
41
+
42
+ class ExponentialBackoffJob < RetryDefaultsJob
43
+ extend Resque::Plugins::ExponentialBackoff
44
+ @queue = :testing
45
+ end
46
+
47
+ class CustomExponentialBackoffJob
48
+ extend Resque::Plugins::ExponentialBackoff
49
+ @queue = :testing
50
+
51
+ @retry_limit = 4
52
+ @backoff_strategy = [10, 20, 30]
53
+
54
+ def self.perform(url, hook_id, hmac_key)
55
+ raise
56
+ end
57
+ end
58
+
59
+ class RetryCustomExceptionsJob < RetryDefaultsJob
60
+ @queue = :testing
61
+
62
+ @retry_limit = 5
63
+ @retry_exceptions = [CustomException, HierarchyCustomException]
64
+
65
+ def self.perform(exception)
66
+ case exception
67
+ when 'CustomException' then raise CustomException
68
+ when 'HierarchyCustomException' then raise HierarchyCustomException
69
+ when 'AnotherCustomException' then raise AnotherCustomException
70
+ else raise StandardError
71
+ end
72
+ end
73
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-retry
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Luke Antins
13
+ - Ryan Carver
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-04-27 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: resque
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 1
30
+ - 8
31
+ - 0
32
+ version: 1.8.0
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: turn
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 0
44
+ version: "0"
45
+ type: :development
46
+ version_requirements: *id002
47
+ - !ruby/object:Gem::Dependency
48
+ name: yard
49
+ prerelease: false
50
+ requirement: &id003 !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ type: :development
58
+ version_requirements: *id003
59
+ - !ruby/object:Gem::Dependency
60
+ name: resque-scheduler
61
+ prerelease: false
62
+ requirement: &id004 !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ type: :development
70
+ version_requirements: *id004
71
+ description: |
72
+ A resque plugin; provides retry, delay and exponential backoff support for
73
+ resque jobs.
74
+
75
+ Retry Example:
76
+
77
+ require 'resque-retry'
78
+
79
+ class DeliverWebHook
80
+ extend Resque::Plugins::Retry
81
+
82
+ def self.perform(url, hook_id, hmac_key)
83
+ heavy_lifting
84
+ end
85
+ end
86
+
87
+ Exponential Backoff Example:
88
+
89
+ class DeliverSMS
90
+ extend Resque::Plugins::ExponentialBackoff
91
+
92
+ def self.perform(mobile_number, message)
93
+ heavy_lifting
94
+ end
95
+ end
96
+
97
+ email: luke@lividpenguin.com
98
+ executables: []
99
+
100
+ extensions: []
101
+
102
+ extra_rdoc_files: []
103
+
104
+ files:
105
+ - LICENSE
106
+ - Rakefile
107
+ - README.md
108
+ - test/exponential_backoff_test.rb
109
+ - test/redis-test.conf
110
+ - test/resque_test.rb
111
+ - test/retry_test.rb
112
+ - test/test_helper.rb
113
+ - test/test_jobs.rb
114
+ - lib/resque/plugins/exponential_backoff.rb
115
+ - lib/resque/plugins/retry.rb
116
+ - lib/resque-retry.rb
117
+ has_rdoc: false
118
+ homepage: http://github.com/lantins/resque-retry
119
+ licenses: []
120
+
121
+ post_install_message:
122
+ rdoc_options: []
123
+
124
+ require_paths:
125
+ - lib
126
+ required_ruby_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ segments:
131
+ - 0
132
+ version: "0"
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ segments:
138
+ - 0
139
+ version: "0"
140
+ requirements: []
141
+
142
+ rubyforge_project:
143
+ rubygems_version: 1.3.6
144
+ signing_key:
145
+ specification_version: 3
146
+ summary: A resque plugin; provides retry, delay and exponential backoff support for resque jobs.
147
+ test_files: []
148
+