resque-exponential-backoff 0.1.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 <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
+