resque-retry 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+