resque-exponential-backoff 0.1.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 <luke@lividpenguin.com>
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.
@@ -0,0 +1,117 @@
1
+ resque-exponential-backoff
2
+ ==========================
3
+
4
+ A [Resque][rq] plugin. Requires Resque 1.8.0 & [resque-scheduler][rqs].
5
+
6
+ resque-exponential-backoff is a plugin to add retry/exponential backoff to
7
+ your resque jobs.
8
+
9
+ Usage
10
+ -----
11
+ Simply extend your module/class with this module:
12
+
13
+ require 'resque-exponential-backoff'
14
+
15
+ class DeliverWebHook
16
+ extend Resque::Plugins::ExponentialBackoff
17
+
18
+ def self.perform(url, hook_id, hmac_key)
19
+ heavy_lifting
20
+ end
21
+ end
22
+
23
+
24
+ ### BEFORE performing job
25
+ The job increments the number of `attempts` in redis. The first attempt == 1.
26
+
27
+ ### SUCSESSFULL job
28
+ Sucsessful jobs clean up any 'attempts state' from redis.
29
+
30
+ ### FAILED job
31
+ If `attempts < max_attempts` the job will be requeued. The delay between retry
32
+ attempts is determine using the backoff strategy.
33
+
34
+ *Exceptions are always passed the failure backends.*
35
+
36
+ Customise & Extend
37
+ ------------------
38
+
39
+ ### Defaults
40
+
41
+ If you just extend with this module and nothing else, these are the defaults:
42
+
43
+ @max_attempts = 7
44
+
45
+ # key: m = minutes, h = hours
46
+ # no delay, 1m, 10m, 1h, 3h, 6h
47
+ @backoff_strategy = [0, 60, 600, 3600, 10800, 21600]
48
+
49
+ ### Job identifier/key
50
+
51
+ **n.b.** The job attempts count is incremented/stored in a redis key, the key
52
+ is built using the name of your jobs class/module and its arguments.
53
+
54
+ If you have lots of arguments, really long ones, you should consider overriding
55
+ `#identifier` to implement a more suitable custom identifier.
56
+
57
+ def self.identifier(database_id, massive_url_list, meow_purr)
58
+ "#{database_id}"
59
+ end
60
+
61
+ For the examples in this readme, the default key looks like this:
62
+
63
+ exponential-backoff:<name>:<args>
64
+ exponential-backoff:DeliverWebHook:http://lividpenguin.com-1305-cd8079192'
65
+
66
+
67
+ ### Custom max attempts, backoff strategy
68
+
69
+ # 4 attempts maximum.
70
+ #
71
+ # 1st retry: no delay
72
+ # 2nd retry: 60 seconds delay
73
+ # nth retry: 2 minutes delay
74
+ class DeliverWebHook
75
+ extend Resque::Plugins::ExponentialBackoff
76
+
77
+ @max_attempts = 5
78
+ @backoff_strategy = [0, 60, 120]
79
+
80
+ def self.perform(url, hook_id, hmac_key)
81
+ heavy_lifting
82
+ end
83
+ end
84
+
85
+ ### Custom delay handling
86
+
87
+ Override `#retry_delay_seconds` to implement your own delay handling.
88
+
89
+ class DeliverWebHook
90
+ extend Resque::Plugins::ExponentialBackoff
91
+ @max_attempts = 5
92
+
93
+ def self.perform(url, hook_id, hmac_key)
94
+ heavy_lifting
95
+ end
96
+
97
+ def self.retry_delay_seconds
98
+ (attempts * 60) ** 2
99
+ end
100
+ end
101
+
102
+
103
+ Install
104
+ -------
105
+
106
+ # Once I figure out how to push to rubygems.org...
107
+ gem install resque-exponential-backoff
108
+
109
+
110
+ License
111
+ -------
112
+ Copyright (c) 2010 Luke Antins <luke@lividpenguin.com>
113
+
114
+ Released under the MIT license. See LICENSE file for details.
115
+
116
+ [rq]: http://github.com/defunkt/resque
117
+ [rqs]: http://github.com/bvandenbos/resque-scheduler
@@ -0,0 +1,24 @@
1
+ require 'rake/testtask'
2
+ require 'fileutils'
3
+ require 'yard'
4
+ require 'yard/rake/yardoc_task'
5
+
6
+ task :default => :test
7
+
8
+ ##
9
+ # Test task.
10
+ Rake::TestTask.new(:test) do |task|
11
+ task.libs << 'lib' << 'test'
12
+ task.test_files = FileList['test/*_test.rb']
13
+ task.verbose = true
14
+ end
15
+
16
+ ##
17
+ # docs task.
18
+ YARD::Rake::YardocTask.new :yardoc do |t|
19
+ t.files = ['lib/**/*.rb']
20
+ t.options = ['--output-dir', "doc/",
21
+ '--files', 'LICENSE',
22
+ '--readme', 'README.md',
23
+ '--title', 'resque-exponential-backoff documentation']
24
+ end
@@ -0,0 +1,2 @@
1
+ require 'resque_scheduler'
2
+ require 'resque/plugins/exponential_backoff'
@@ -0,0 +1,160 @@
1
+ module Resque
2
+ module Plugins
3
+ ##
4
+ # resque-exponential-backoff is a plugin to add retry/exponential backoff
5
+ # to your resque jobs.
6
+ #
7
+ # Simply extend your module/class with this module:
8
+ #
9
+ # require 'resque-exponential-backoff'
10
+ #
11
+ # class DeliverWebHook
12
+ # extend Resque::Plugins::ExponentialBackoff
13
+ #
14
+ # def self.perform(url, hook_id, hmac_key)
15
+ # heavy_lifting
16
+ # end
17
+ # end
18
+ #
19
+ # Or do something more custom:
20
+ #
21
+ # class DeliverWebHook
22
+ # extend Resque::Plugins::ExponentialBackoff
23
+ #
24
+ # # max number of attempts.
25
+ # @max_attempts = 4
26
+ # # retry delay in seconds.
27
+ # @backoff_strategy = [0, 60]
28
+ #
29
+ # # used to build redis key to store job attempts counter.
30
+ # def self.identifier(url, hook_id, hmac_key)
31
+ # "#{url}-#{hook_id}"
32
+ # end
33
+ #
34
+ # def self.perform(url, hook_id, hmac_key)
35
+ # heavy_lifting
36
+ # end
37
+ # end
38
+ module ExponentialBackoff
39
+
40
+ ##
41
+ # @abstract You may override to implement a custom identifier,
42
+ # you should consider doing this if your job arguments
43
+ # are many/long or may not cleanly cleanly to strings.
44
+ #
45
+ # Builds an identifier using the job arguments. This identifier
46
+ # is used as part of the redis key.
47
+ #
48
+ # @param [Array] args job arguments
49
+ # @return [String] job identifier
50
+ def identifier(*args)
51
+ args.join('-')
52
+ end
53
+
54
+ ##
55
+ # Builds the redis key to be used for keeping state of the job
56
+ # attempts.
57
+ #
58
+ # @return [String] redis key
59
+ def key(*args)
60
+ ['exponential-backoff', name, identifier(*args)].compact.join(":")
61
+ end
62
+
63
+ ##
64
+ # Maximum number of attempts we can use to successfully perform the job.
65
+ # Default value: 7
66
+ #
67
+ # @return [Fixnum] number of attempts
68
+ def max_attempts
69
+ @max_attempts ||= 7
70
+ end
71
+
72
+ ##
73
+ # Number of attempts so far to try and perform the job.
74
+ # Default value: 0
75
+ #
76
+ # @return [Fixnum] number of attempts
77
+ def attempts
78
+ @attempts ||= 0
79
+ end
80
+
81
+ ##
82
+ # @abstract You may override to implement your own delay logic.
83
+ #
84
+ # Returns the number of seconds to delay until the job is tried
85
+ # again. By default, this delay is taken from the `#backoff_strategy`.
86
+ #
87
+ # @return [Number, #to_i] number of seconds to delay.
88
+ def retry_delay_seconds
89
+ backoff_strategy[attempts - 1] || backoff_strategy.last
90
+ end
91
+
92
+ ##
93
+ # Default backoff strategy.
94
+ #
95
+ # 1st retry : 0 delay
96
+ # 2nd retry : 1 minute
97
+ # 3rd retry : 10 minutes
98
+ # 4th retry : 1 hour
99
+ # 5th retry : 3 hours
100
+ # 6th retry : 6 hours
101
+ #
102
+ # You can set your own backoff strategy in your job module/class:
103
+ #
104
+ # @example custom backoff strategy, in your class/module:
105
+ # @backoff_strategy = [0, 0, 120]
106
+ #
107
+ # Using this strategy, the first two retries will be immediate,
108
+ # the third and any subsequent retries will be delayed by 2 minutes.
109
+ def backoff_strategy
110
+ @backoff_strategy ||= [0, 60, 600, 3600, 10_800, 21_600]
111
+ end
112
+
113
+ ##
114
+ # Called before `#perform`.
115
+ # - Initialise or increment attempts counter.
116
+ def before_perform_exponential_backoff(*args)
117
+ Resque.redis.setnx(key(*args), 0) # default to 0 if not set.
118
+ @attempts = Resque.redis.incr(key(*args)) # increment by 1.
119
+ end
120
+
121
+ ##
122
+ # Called after if `#perform` was successfully.
123
+ # - Delete attempts counter from redis.
124
+ def after_perform_exponential_backoff(*args)
125
+ delete_attempts_counter(*args)
126
+ end
127
+
128
+ ##
129
+ # Called if the job raises an exception.
130
+ # - Requeue the job if maximum attempts has not been reached.
131
+ def on_failure_exponential_backoff(exception, *args)
132
+ if attempts >= max_attempts
133
+ delete_attempts_counter(*args)
134
+ return
135
+ end
136
+
137
+ requeue(*args)
138
+ end
139
+
140
+ ##
141
+ # Delete the attempts counter from redis, keepin it clean ;-)
142
+ def delete_attempts_counter(*args)
143
+ Resque.redis.del(key(*args))
144
+ end
145
+
146
+ ##
147
+ # Requeue the current job, immediately or delayed if `#retry_delay_seconds`
148
+ # returns grater then zero.
149
+ #
150
+ # @param [Array] args job arguments
151
+ def requeue(*args)
152
+ if retry_delay_seconds > 0
153
+ Resque.enqueue_in(retry_delay_seconds, self, *args)
154
+ else
155
+ Resque.enqueue(self, *args)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,107 @@
1
+ require 'test_helper'
2
+
3
+ class GoodJob
4
+ extend Resque::Plugins::ExponentialBackoff
5
+ @queue = :testing
6
+
7
+ def self.perform(*args)
8
+ end
9
+ end
10
+
11
+ module BadJob
12
+ extend Resque::Plugins::ExponentialBackoff
13
+ @queue = :testing
14
+
15
+ def self.perform(*args)
16
+ raise
17
+ end
18
+ end
19
+
20
+ module CustomBackoffStrategyJob
21
+ extend Resque::Plugins::ExponentialBackoff
22
+ @queue = :testing
23
+ @backoff_strategy = [3600, 86_400]
24
+
25
+ def self.perform(*args)
26
+ raise
27
+ end
28
+ end
29
+
30
+ module DeliverWebHook
31
+ extend Resque::Plugins::ExponentialBackoff
32
+ @queue = :testing
33
+
34
+ def self.retry_delay_seconds
35
+ attempts * 10
36
+ end
37
+
38
+ def self.perform(url, hook_id, hmac_key)
39
+
40
+ raise
41
+ end
42
+ end
43
+
44
+ class ExponentialBackoffTest < Test::Unit::TestCase
45
+ def setup
46
+ Resque.redis.flushall
47
+ @worker = Resque::Worker.new(:testing)
48
+ end
49
+
50
+ def test_resque_plugin_lint
51
+ assert_nothing_raised do
52
+ Resque::Plugin.lint(Resque::Plugins::ExponentialBackoff)
53
+ end
54
+ end
55
+
56
+ def test_resque_version
57
+ major, minor, patch = Resque::Version.split('.')
58
+ assert_equal 1, major.to_i, 'major version does not match'
59
+ assert_operator minor.to_i, :>=, 8, 'minor version is too low'
60
+ end
61
+
62
+ def test_good_job
63
+ Resque.enqueue(GoodJob, 1234, { :cats => :maiow }, [true, false, false])
64
+ @worker.work(0)
65
+
66
+ assert_equal 1, Resque.info[:processed]
67
+ assert_equal 0, Resque.info[:failed]
68
+ assert_equal 0, Resque.delayed_queue_schedule_size
69
+ end
70
+
71
+ def test_retry_job
72
+ Resque.enqueue(BadJob, 1234)
73
+ @worker.work(0)
74
+
75
+ assert_equal 2, Resque.info[:processed]
76
+ assert_equal 2, Resque.info[:failed]
77
+ assert_equal 1, Resque.delayed_queue_schedule_size
78
+ # FIXME: below test can be a bit brittle when off by a second.
79
+ assert_equal Time.now.to_i + 60, Resque.delayed_queue_peek(0, 1).first
80
+ end
81
+
82
+ def test_custom_backoff_strategy_job
83
+ Resque.enqueue(CustomBackoffStrategyJob, 1234)
84
+ Resque.enqueue(CustomBackoffStrategyJob, 1234)
85
+ @worker.work(0)
86
+
87
+ assert_equal 2, Resque.info[:processed]
88
+ assert_equal 2, Resque.info[:failed]
89
+
90
+ delayed = Resque.delayed_queue_peek(0, 2)
91
+ assert_equal Time.now.to_i + 3600, delayed.first
92
+ assert_equal Time.now.to_i + 86_400, delayed.last
93
+ end
94
+
95
+ def test_custom_backoff_job
96
+ Resque.enqueue(DeliverWebHook, 'http://lividpenguin.com', 1305, 'cd8079192d379dc612f17c660591a6cfb05f1dda')
97
+ Resque.enqueue(DeliverWebHook, 'http://lividpenguin.com', 1305, 'cd8079192d379dc612f17c660591a6cfb05f1dda')
98
+ @worker.work(0)
99
+
100
+ assert_equal 2, Resque.info[:processed]
101
+ assert_equal 2, Resque.info[:failed]
102
+
103
+ delayed = Resque.delayed_queue_peek(0, 2)
104
+ assert_equal Time.now.to_i + 10, delayed.first
105
+ assert_equal Time.now.to_i + 20, delayed.last
106
+ end
107
+ end
@@ -0,0 +1,13 @@
1
+ rootdir = File.dirname(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift rootdir + '/test'
3
+ $LOAD_PATH.unshift rootdir + '/lib'
4
+
5
+ require 'test/unit'
6
+ require 'resque'
7
+
8
+ begin
9
+ require 'turn' # nicer test output.
10
+ rescue LoadError
11
+ end
12
+
13
+ require 'resque-exponential-backoff'
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-exponential-backoff
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 1
9
+ version: 0.1.1
10
+ platform: ruby
11
+ authors:
12
+ - Luke Antins
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-22 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: resque
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 8
30
+ - 0
31
+ version: 1.8.0
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: resque-scheduler
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 1
43
+ - 8
44
+ - 0
45
+ version: 1.8.0
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: hashie
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 0
57
+ - 2
58
+ - 0
59
+ version: 0.2.0
60
+ type: :runtime
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: turn
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ type: :development
73
+ version_requirements: *id004
74
+ - !ruby/object:Gem::Dependency
75
+ name: yard
76
+ prerelease: false
77
+ requirement: &id005 !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ type: :development
85
+ version_requirements: *id005
86
+ description: |
87
+ A resque plugin that adds retry/exponential backoff functionality to your
88
+ resque jobs.
89
+
90
+ Simply extend your module/class with this module:
91
+
92
+ require 'resque-exponential-backoff'
93
+
94
+ class DeliverWebHook
95
+ extend Resque::Plugins::ExponentialBackoff
96
+
97
+ def self.perform(url, hook_id, hmac_key)
98
+ heavy_lifting
99
+ end
100
+ end
101
+
102
+ email: luke@lividpenguin.com
103
+ executables: []
104
+
105
+ extensions: []
106
+
107
+ extra_rdoc_files: []
108
+
109
+ files:
110
+ - LICENSE
111
+ - Rakefile
112
+ - README.md
113
+ - test/exponential_backoff_test.rb
114
+ - test/test_helper.rb
115
+ - lib/resque/plugins/exponential_backoff.rb
116
+ - lib/resque-exponential-backoff.rb
117
+ has_rdoc: false
118
+ homepage: http://github.com/lantins/resque-exponential-backoff
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, add retry/exponential backoff to your resque jobs.
147
+ test_files: []
148
+