resque-pertry 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *.gem
2
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Anthony 'YoGiN' Powles
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+ resque-pertry
2
+ =============
3
+
4
+ Setup
5
+ -----
6
+
7
+ ```ruby
8
+ gem 'resque-pertry'
9
+ ```
10
+
11
+ Wherever you include resque's tasks, you should add:
12
+
13
+ ```ruby
14
+ require 'resque/tasks'
15
+ require 'resque/pertry/tasks'
16
+ ```
17
+
18
+ You'll need to run a migration to create the persistence table.
19
+
20
+ ```ruby
21
+ require 'resque/pertry/migrations/create_resque_pertry_table'
22
+
23
+ class AddResquePertry < Resque::Pertry::Migrations::CreateResquePertryTable
24
+ # if you are using db-charmer, you can easily specify the connection to use
25
+ db_magic :connection => :resque
26
+ end
27
+ ```
28
+
29
+ The associated model is `Resque::Pertry::ResquePertryPersistence`, feel free to add your own code in there if you need (such as db_magic)
30
+
31
+ You will also need to start [resque-scheduler](https://github.com/bvandenbos/resque-scheduler) and the resque pertry purger
32
+
33
+ ```
34
+ $ VERBOSE=1 rake resque:scheduler
35
+ $ VERBOSE=1 rake resque:pertry:purger
36
+ ```
37
+
38
+ The purger's default values will have it run every 5 minutes, and purge 100 failed jobs from redis and another 100 failed jobs from the database.
39
+ You can add hooks in your application if you want to process the purged jobs further (you may want to log the jobs you are purging):
40
+
41
+ ```ruby
42
+ Resque::Pertry::Purger.after_redis_purge do |job,redis|
43
+ # do something with this job from resque's failed queue
44
+ end
45
+
46
+ Resque::Pertry::Purger.after_database_purge do |job|
47
+ # do something with this job from the persistence table
48
+ end
49
+ ```
50
+
51
+ Usage
52
+ -----
53
+
54
+ Your resque jobs need to inherit from `Resque::Plugins::Pertry` :
55
+
56
+ ```ruby
57
+ class SomeJob < Resque::Plugins::Pertry
58
+
59
+ # specify Resque queue
60
+ in_queue :critical
61
+
62
+ def perform
63
+ # do something
64
+ end
65
+ end
66
+ ```
67
+
68
+ You can enqueue the job:
69
+
70
+ ```ruby
71
+ SomeJob.enqueue
72
+ ```
73
+
74
+ You can pass arguments to your job:
75
+
76
+ ```ruby
77
+ SomeJob.enqueue(:user_id => 42, :image_id => 24)
78
+ ```
79
+
80
+ In your job class, you specify the arguments with `needs` :
81
+
82
+ ```ruby
83
+ class SomeJob < Resque::Plugins::Pertry
84
+
85
+ # specify Resque queue
86
+ in_queue :critical
87
+
88
+ needs :user_id, :image_id
89
+
90
+ def perform
91
+ # do something with @user_id and @image_id
92
+ end
93
+ end
94
+ ```
95
+
96
+ The arguments `user_id` and `image_id` will then be accessible as instance variables `@user_id` and `@image_id`
97
+ You can also specify a default value for your needs:
98
+
99
+ ```ruby
100
+ needs :user_id, :image_id, :action => "delete"
101
+ ```
102
+
103
+ Finally you can specify if your job should be persistent or not, and if it is, the properties to retry failed persistent jobs.
104
+ All jobs including `Resque::Plugins::Pertry` are persistent by default. Non persistent jobs act just like regular resque jobs.
105
+
106
+ To set a job as non persistent, simply:
107
+
108
+ ```ruby
109
+ class SomeJob < Resque::Plugins::Pertry
110
+
111
+ # set job as non persistent, acts like a regular resque job
112
+ non_persistent
113
+
114
+ ...
115
+ end
116
+ ```
117
+
118
+ To retry a persistent job, you have several properties you can set:
119
+
120
+ * `set_retry_attempts <max_retries>`
121
+
122
+ Sets the maximum number of times we will retry a failed job.
123
+ The total number of times a job will run is max_retries + 1 (the original run).
124
+
125
+ * `set_retry_delay <seconds>`
126
+
127
+ Sets the number of seconds to wait before enqueueing a failed job again
128
+
129
+ * `set_retry_delays <seconds>, <seconds>, ...`
130
+
131
+ Set an array of delays.
132
+ The number of items will be used to defined `max_retries`, while each value will be used in turn for each consecutive retry delay.
133
+ If this is set, `set_retry_attempts` and `set_retry_delay` will be ignored
134
+
135
+ * `set_retry_exceptions <SomeException>, <SomeOtherException>, ...`
136
+
137
+ Sets a whitelist of exceptions you want to retry. Any other exceptions will not be retried.
138
+ If not set, all exceptions will be retried (default).
139
+ This property can be combined with any other property.
140
+
141
+ * `set_retry_ttl <seconds>`
142
+
143
+ Sets the maximum time to live for a job, after which the job will not be retried, even if it has attempts lefts.
144
+ This property can be combined with any other property.
145
+
146
+ The default settings for all persistent jobs are:
147
+
148
+ ```ruby
149
+ # retry a failed job once, enqueue the job immediately, job expires in 1 week
150
+ set_retry_attempts 1
151
+ set_retry_ttl 1.week
152
+ ```
153
+
154
+ Here are some exemples of retry settings:
155
+
156
+ ```ruby
157
+ # retry 5 times, enqueue job immediately
158
+ set_retry_attempts 5
159
+ ```
160
+
161
+ ```ruby
162
+ # retry 10 times, wait 1 minute before enqueueing the job again
163
+ set_retry_attempts 10
164
+ set_retry_delay 1.minute
165
+ ```
166
+
167
+ ```ruby
168
+ # retry 3 times, wait time before enqueueing job increases after each attempt
169
+ set_retry_delays 1.minute, 5.minutes, 15.minutes
170
+ ```
171
+
172
+ ```ruby
173
+ # retry every 30 seconds for 1 hour, as long as we are getting the exception ActiveRecord::ConnectionTimeoutError
174
+ set_retry_ttl 1.hour
175
+ set_retry_delay 30.seconds
176
+ set_retry_exceptions ActiveRecord::ConnectionTimeoutError
177
+ ```
178
+
179
+ TODO
180
+ ----
181
+
182
+ Write some specs!
183
+
184
+ How to contribute
185
+ -----------------
186
+
187
+ Tell me what you think about this gem, things you like or don't about it, how it could be better.
188
+ Pull Requests most welcomed.
189
+
190
+
@@ -0,0 +1,7 @@
1
+ require 'resque'
2
+ require 'resque_scheduler'
3
+ require 'resque/pertry/resque_pertry_persistence'
4
+ require 'resque/pertry/persistence'
5
+ require 'resque/pertry/retry'
6
+ require 'resque/pertry/purger'
7
+ require 'resque/plugins/pertry'
@@ -0,0 +1,30 @@
1
+ module Resque
2
+ module Pertry
3
+ module Migrations
4
+ class CreateResquePertryTable < ActiveRecord::Migration
5
+
6
+ def up
7
+ create_table :resque_pertry_persistence do |t|
8
+ t.string :audit_id, :null => false, :limit => 64
9
+ t.string :job, :null => false
10
+ t.text :arguments, :null => false, :limit => 64.kilobytes + 1
11
+ t.integer :attempt, :default => 1
12
+ t.datetime :enqueued_at
13
+ t.datetime :completed_at
14
+ t.datetime :failed_at
15
+ t.datetime :last_tried_at
16
+ t.datetime :expires_at
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :resque_pertry_persistence, :audit_id, :unique => true
21
+ end
22
+
23
+ def down
24
+ drop_table :resque_pertry_persistence
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,67 @@
1
+ module Resque
2
+ module Pertry
3
+ module Persistence
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :_persistent
8
+
9
+ # define all jobs as persistent by default
10
+ persistent
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ # Set job as persistent
16
+ def persistent
17
+ self._persistent = true
18
+ end
19
+ alias_method :durable, :persistent
20
+
21
+ def non_persistent
22
+ self._persistent = false
23
+ end
24
+ alias_method :non_durable, :non_persistent
25
+
26
+ # Check if job is persistent
27
+ def persistent?
28
+ !!self._persistent
29
+ end
30
+
31
+ # Resque before_enqueue hook
32
+ def before_enqueue_pertry_99_persistence(args = {})
33
+ pertry_key = Resque::Plugins::Pertry::JOB_HASH.to_s
34
+
35
+ args[pertry_key] ||= {}
36
+ args[pertry_key]['audit_id'] ||= UUIDTools::UUID.random_create.to_s
37
+ args[pertry_key]['queue_time'] ||= Time.now
38
+ args[pertry_key]['persist'] = persistent?
39
+
40
+ if persistent?
41
+ ResquePertryPersistence.create_job_if_needed(self, args)
42
+ end
43
+
44
+ # continue with enqueue
45
+ true
46
+ end
47
+
48
+ # Resque after_perform hook (job completed successfully)
49
+ def after_perform_pertry_00_persistence(args = {})
50
+ return unless persistent?
51
+
52
+ ResquePertryPersistence.finnish_job(self, args)
53
+ end
54
+
55
+ end
56
+
57
+ def audit_id
58
+ @_job_properties['audit_id']
59
+ end
60
+
61
+ def queue_time
62
+ Time.parse(@_job_properties['queue_time'])
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,214 @@
1
+ module Resque
2
+ module Pertry
3
+ class Purger
4
+
5
+ class << self
6
+
7
+ attr_accessor :verbose
8
+
9
+ # sleep time between purges
10
+ attr_writer :sleep_time
11
+
12
+ # sets the number of failed jobs to purge per run
13
+ attr_writer :failed_jobs_limit
14
+
15
+ def sleep_time
16
+ @sleep_time ||= 5.minutes
17
+ end
18
+
19
+ def failed_jobs_limit
20
+ @failed_jobs_limit ||= 100
21
+ end
22
+
23
+ def stats
24
+ @stats ||= {}
25
+ end
26
+
27
+ # main loop
28
+ def run
29
+ setup
30
+
31
+ loop do
32
+ purge
33
+ wait
34
+ end
35
+ end
36
+
37
+ # run a purge cycle
38
+ def purge
39
+ procline("working")
40
+ add_stat(:loops, 1)
41
+
42
+ purge_resque
43
+ purge_database
44
+ end
45
+
46
+ # display status of failed queues and persistence table
47
+ def status
48
+ show_config
49
+ show_info
50
+ end
51
+
52
+ # allows an app to set a hook to deal with the failed redis job
53
+ def after_redis_purge(&block)
54
+ @after_redis_purge = block
55
+ end
56
+
57
+ # allows an app to set a hook to deal with the failed persistence table job
58
+ def after_database_purge(&block)
59
+ @after_database_purge = block
60
+ end
61
+
62
+ private
63
+
64
+ # purger setup and init
65
+ def setup
66
+ stats[:pid] = Process.pid
67
+ stats[:started] = Time.now
68
+ stats[:loops] = 0
69
+
70
+ procline("starting")
71
+ status
72
+ register_signal_handlers
73
+ end
74
+
75
+ # intercept signals
76
+ def register_signal_handlers
77
+ trap("TERM") { shutdown }
78
+ trap("INT") { shutdown }
79
+ trap("QUIT") { shutdown }
80
+ trap("USR1") { status }
81
+ end
82
+
83
+ def show_config
84
+ log("Configuration:")
85
+ [ :sleep_time, :failed_jobs_limit ].each do |v|
86
+ log("\tconfig #{v} = #{send(v)}")
87
+ end
88
+ end
89
+
90
+ def show_info
91
+ with_redis do |redis|
92
+ set_stat("failed queue length on #{redis.id}", redis.llen(:failed))
93
+ end
94
+
95
+ log!("Status:")
96
+ stats.each do |key, value|
97
+ log!("\t#{key} : #{value}")
98
+ end
99
+ end
100
+
101
+ # just sleep for a while
102
+ def wait
103
+ procline("sleeping for #{sleep_time} seconds")
104
+ sleep(sleep_time)
105
+ end
106
+
107
+ # shutdown the process
108
+ def shutdown
109
+ procline("shutting down")
110
+ status
111
+ exit
112
+ end
113
+
114
+ # update process line
115
+ def procline(string)
116
+ log(string)
117
+ $0 = "resque-pertry purger: #{string}"
118
+ end
119
+
120
+ # purge jobs in the failed queue
121
+ def purge_resque
122
+ with_redis do |redis|
123
+ purge_redis(redis)
124
+ end
125
+ end
126
+
127
+ def with_redis(&block)
128
+ return {} unless block_given?
129
+
130
+ # testing the redis class name so we don't have to require resque_redis_composite
131
+ case Resque.redis.redis.class.name
132
+ when "Redis"
133
+ { redis.id => block.call(Resque.redis) }
134
+ when "Resque::RedisComposite"
135
+ Resque.redis.mapping.reduce({}) do |results, (queue, redis)|
136
+ results[redis.id] = block.call(redis)
137
+ results
138
+ end
139
+ else
140
+ raise NotImplementedError, "Unsupported redis client #{Resque.redis.inspect}"
141
+ end
142
+ end
143
+
144
+ # purge resque's failed queue on a redis client
145
+ def purge_redis(redis)
146
+ failed_jobs = redis.lrange(:failed, 0, failed_jobs_limit - 1)
147
+ return 0 if failed_jobs.empty?
148
+
149
+ log("purging #{failed_jobs.size} failed jobs from #{redis.id}")
150
+ redis.ltrim(:failed, failed_jobs.size, -1)
151
+
152
+ failed_jobs.each do |failed_job|
153
+ run_after_redis_purge(failed_job, redis)
154
+ end if @after_redis_purge
155
+
156
+ add_stat("purged from #{redis.id}", failed_jobs.size)
157
+ end
158
+
159
+ # purge resque-pertry persistence table
160
+ def purge_database
161
+ jobs = ResquePertryPersistence.finnished_or_expired.limit(failed_jobs_limit)
162
+ return 0 if jobs.empty?
163
+
164
+ log("purging #{jobs.size} completed, failed, or expired jobs from database")
165
+
166
+ jobs.each do |job|
167
+ ResquePertryPersistence.destroy(job.id)
168
+ run_after_database_purge(job)
169
+ end if @after_database_purge
170
+
171
+ add_stat("purged from database", jobs.size)
172
+ end
173
+
174
+ # run hook after_redis_purge
175
+ def run_after_redis_purge(job, redis)
176
+ return unless @after_redis_purge
177
+ @after_redis_purge.call(job, redis)
178
+ rescue => e
179
+ log!("exception #{e.inspect} while running hook after_redis_purge on job #{job.inspect}")
180
+ end
181
+
182
+ # run hook after_database_purge
183
+ def run_after_database_purge(job)
184
+ return unless @after_database_purge
185
+ @after_database_purge.call(job)
186
+ rescue => e
187
+ log!("exception #{e.inspect} while running hook after_database_purge on job #{job.inspect}")
188
+ end
189
+
190
+ # only print if verbose is turned on
191
+ def log(string)
192
+ log!(string) if verbose
193
+ end
194
+
195
+ # always print this string
196
+ def log!(string)
197
+ $stdout.puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")} - #{string}"
198
+ end
199
+
200
+ def add_stat(stat, value)
201
+ stats[stat] ||= 0
202
+ stats[stat] += value
203
+ value
204
+ end
205
+
206
+ def set_stat(stat, value)
207
+ stats[stat] = value
208
+ end
209
+
210
+ end
211
+
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,87 @@
1
+ module Resque
2
+ module Pertry
3
+ class ResquePertryPersistence < ActiveRecord::Base
4
+
5
+ self.table_name = "resque_pertry_persistence"
6
+
7
+ default_scope -> { order(:updated_at) }
8
+ scope :completed, -> { where("completed_at IS NOT NULL") }
9
+ scope :failed, -> { where("failed_at IS NOT NULL") }
10
+ scope :finnished, -> { where("completed_at IS NOT NULL OR failed_at IS NOT NULL") }
11
+ scope :ongoing, -> { where(:completed_at => nil, :failed_at => nil) }
12
+ scope :expired, -> { where("expires_at < ?", Time.now) }
13
+ scope :finnished_or_expired, -> { where("completed_at IS NOT NULL OR failed_at IS NOT NULL OR expires_at < ?", Time.now) }
14
+
15
+ class << self
16
+
17
+ def create_job_if_needed(klass, args)
18
+ with_job(klass, args) do |job|
19
+ # we already have a job for this, we don't want another
20
+ return if job
21
+
22
+ # creating a new job
23
+ params = {
24
+ :audit_id => field_from_args('audit_id', args),
25
+ :job => klass.to_s,
26
+ :arguments => Resque.encode(args),
27
+ :attempt => 0,
28
+ :enqueued_at => field_from_args('queue_time', args)
29
+ }
30
+
31
+ params[:expires_at] = params[:enqueued_at] + klass.retry_ttl if klass.retry_ttl
32
+
33
+ new(params).save!
34
+ end
35
+ end
36
+
37
+
38
+ def finnish_job(klass, args)
39
+ with_job(klass, args) do |job|
40
+ return unless job
41
+ job.update_attribute(:completed_at, Time.now)
42
+ end
43
+ end
44
+
45
+ def trying_job(klass, args)
46
+ with_job(klass, args) do |job|
47
+ return unless job
48
+ job.update_attributes( :last_tried_at => Time.now,
49
+ :attempt => job.attempt + 1)
50
+ end
51
+ end
52
+
53
+ def fail_job(klass, args)
54
+ with_job(klass, args) do |job|
55
+ return unless job
56
+ job.update_attribute(:failed_at, Time.now)
57
+ end
58
+ end
59
+
60
+ def with_job(klass, args, &block)
61
+ audit_id = field_from_args('audit_id', args)
62
+ return unless audit_id
63
+
64
+ job = find_by_audit_id(audit_id)
65
+ block.call(job) if block_given?
66
+ end
67
+
68
+ private
69
+
70
+ def field_from_args(field, args)
71
+ pertry_hash = args[Resque::Plugins::Pertry::JOB_HASH] || args[Resque::Plugins::Pertry::JOB_HASH.to_s] || {}
72
+ pertry_hash[field]
73
+ end
74
+
75
+ end
76
+
77
+ def has_completed?
78
+ completed_at
79
+ end
80
+
81
+ def payload
82
+ @payload ||= Resque.decode(arguments)
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,164 @@
1
+ module Resque
2
+ module Pertry
3
+ module Retry
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :_retry_delays
8
+ class_attribute :_retry_delay
9
+ class_attribute :_retry_attempts
10
+ class_attribute :_retry_ttl
11
+ class_attribute :_retry_exceptions
12
+
13
+ # set a default max number of retry attempts
14
+ set_retry_attempts 1
15
+
16
+ # set job to expire in 1 week (we need a default so we can purge the database)
17
+ set_retry_ttl 1.week
18
+ end
19
+
20
+ module ClassMethods
21
+
22
+ # Sets a number of seconds to wait before retrying
23
+ def set_retry_delay(delay)
24
+ self._retry_delays = nil
25
+ self._retry_delay = Integer(delay)
26
+ end
27
+
28
+ def retry_delay
29
+ self._retry_delay
30
+ end
31
+
32
+ # Sets a list of delays (list length will be the # of attempts)
33
+ def set_retry_delays(*delays)
34
+ self._retry_attempts = nil
35
+ self._retry_delay = nil
36
+ self._retry_delays = Array(delays).map { |delay| Integer(delay) }
37
+ end
38
+
39
+ def retry_delays
40
+ self._retry_delays
41
+ end
42
+
43
+ # Sets the maximum number of times we will retry
44
+ def set_retry_attempts(count)
45
+ self._retry_delays = nil
46
+ self._retry_attempts = Integer(count)
47
+ end
48
+
49
+ def retry_attempts
50
+ self._retry_attempts
51
+ end
52
+
53
+ # Sets the maximum time-to-live of the job, after which no attempts will ever be made
54
+ def set_retry_ttl(ttl)
55
+ self._retry_ttl = Integer(ttl)
56
+ end
57
+
58
+ def retry_ttl
59
+ self._retry_ttl
60
+ end
61
+
62
+ # Sets a list of exceptions that we want to retry
63
+ # If none are set, we will retry every exceptions
64
+ def set_retry_exceptions(*exceptions)
65
+ self._retry_exceptions = Array(exceptions)
66
+ end
67
+
68
+ def retry_exceptions
69
+ self._retry_exceptions
70
+ end
71
+
72
+ # Check if we will retry this job on failure
73
+ # There has to be a constraint on the number of times we will retry a failing job
74
+ # or have a ttl, otherwise we could be retrying job endlessly
75
+ def retryable?
76
+ retry_attempts || retry_delays || retry_ttl
77
+ end
78
+
79
+ # Resque around_perform hook
80
+ def around_perform_pertry_00_retry(args = {})
81
+ ResquePertryPersistence.trying_job(self, args)
82
+
83
+ yield
84
+ end
85
+
86
+ # Resque on_failure hook (job failed)
87
+ def on_failure_pertry_00_retry(exception, args = {})
88
+ return unless retryable?
89
+
90
+ ResquePertryPersistence.with_job(self, args) do |job_model|
91
+ job = instance(args)
92
+
93
+ unless job.retry!(job_model, exception)
94
+ ResquePertryPersistence.fail_job(self, args)
95
+ end
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ # Checks if we can retry
102
+ def retry?(model, exception)
103
+ # check the obvious
104
+ return false unless model
105
+ return false if model.has_completed?
106
+
107
+ # job has used up all it's allowed attempts
108
+ return false if max_attempt_reached?(model)
109
+
110
+ # job exception is not whitelisted for retries
111
+ return false unless exception_whitelisted?(model, exception)
112
+
113
+ # job has expired
114
+ return false if ttl_expired?(model)
115
+
116
+ # seems like we should be able to retry this job
117
+ return true
118
+ end
119
+
120
+ # Retry the job
121
+ def retry!(model, exception)
122
+ return false unless retry?(model, exception)
123
+
124
+ delay = delay_before_retry(model)
125
+ return false unless delay
126
+
127
+ Resque.enqueue_in(delay, self.class, payload)
128
+ end
129
+
130
+ def exception_whitelisted?(model, exception)
131
+ # all exceptions are whitelisted implicitly if we didn't set the exception list
132
+ return true unless self.class.retry_exceptions
133
+
134
+ self.class.retry_exceptions.include?(exception.class)
135
+ end
136
+
137
+ def ttl_expired?(model)
138
+ # if we didn't set a ttl, it hasn't expired
139
+ return false unless self.class.retry_ttl
140
+
141
+ model.expires_at < Time.now
142
+ end
143
+
144
+ def max_attempt_reached?(model)
145
+ if self.class.retry_attempts && self.class.retry_attempts < model.attempt
146
+ true
147
+ elsif self.class.retry_delays && self.class.retry_delays.size < model.attempt
148
+ true
149
+ else
150
+ false
151
+ end
152
+ end
153
+
154
+ def delay_before_retry(model)
155
+ if self.class.retry_delays
156
+ self.class.retry_delays[ model.attempt - 1 ]
157
+ else
158
+ self.class.retry_delay || 0
159
+ end
160
+ end
161
+
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,22 @@
1
+ require 'resque/tasks'
2
+ require 'resque_scheduler/tasks'
3
+
4
+ namespace :resque do
5
+ task :setup
6
+
7
+ namespace :pertry do
8
+ desc "Start Resque Pertry database and failed queue purger"
9
+ task :purger => :pertry_setup do
10
+ File.open(ENV['PIDFILE'], 'w') { |f| f << Process.pid.to_s } if ENV['PIDFILE']
11
+
12
+ Resque::Pertry::Purger.sleep_time = Integer(ENV['PURGER_SLEEP']) if ENV['PURGER_SLEEP']
13
+ Resque::Pertry::Purger.failed_jobs_limit = Integer(ENV['PURGER_LIMIT']) if ENV['PURGER_LIMIT']
14
+ Resque::Pertry::Purger.verbose = true if ENV['VERBOSE']
15
+ Resque::Pertry::Purger.run
16
+ end
17
+
18
+ task :pertry_setup do
19
+ Rake::Task['resque:setup'].invoke
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,115 @@
1
+ module Resque
2
+ module Plugins
3
+ class Pertry
4
+ JOB_HASH = :_pertry
5
+
6
+ include Resque::Pertry::Persistence
7
+ include Resque::Pertry::Retry
8
+
9
+ class << self
10
+
11
+ # Enqueue a job
12
+ def enqueue(args = {})
13
+ raise ArgumentError, "Invalid arguments, expecting a Hash but got: #{args.inspect}" unless Hash === args
14
+
15
+ args.symbolize_keys!
16
+ args = check_arguments(args)
17
+ raise ArgumentError, "Invalid arguments, #{JOB_HASH} is a reserved argument!" if args.key?(JOB_HASH)
18
+
19
+ Resque.enqueue(self, args)
20
+ end
21
+
22
+ # Perform a job
23
+ def perform(args = {})
24
+ raise ArgumentError, "Invalid arguments, expecting a Hash but got: #{args.inspect}" unless Hash === args
25
+
26
+ instance(args).perform
27
+ end
28
+
29
+ def instance(args = {})
30
+ args.symbolize_keys!
31
+ raise ArgumentError, "Job is not supported, missing key #{JOB_HASH} from payload #{args.inspect}" unless args.key?(JOB_HASH)
32
+
33
+ new(check_arguments(args), args[JOB_HASH])
34
+ end
35
+
36
+ # Specificy job queue
37
+ def in_queue(queue)
38
+ @queue = queue.to_sym
39
+ end
40
+
41
+ # Get job queue
42
+ def queue
43
+ @queue or raise ArgumentError, "No queue defined for job #{self.name}!"
44
+ end
45
+
46
+ # Define required job attributes
47
+ def needs(*arguments)
48
+ arguments.each do |argument|
49
+ if Hash === argument
50
+ argument.each do |key, default|
51
+ self.required_arguments << { :name => key, :default => default }
52
+ end
53
+ else
54
+ self.required_arguments << { :name => argument }
55
+ end
56
+ end
57
+ end
58
+
59
+ # List of required attributes
60
+ def required_arguments
61
+ @required_arguments ||= []
62
+ end
63
+
64
+ private
65
+
66
+ # Check that job arguments match required arguments
67
+ def check_arguments(provided_arguments)
68
+ required_arguments.inject({}) do |checked_arguments, argument|
69
+ raise ArgumentError, "#{self} is missing required argument #{argument[:name]} from #{provided_arguments.inspect}" unless provided_arguments.member?(argument[:name]) || argument.member?(:default)
70
+
71
+ provided_argument = provided_arguments[argument[:name]] || argument[:default]
72
+ # TODO check that provided_argument is serializable as json
73
+
74
+ checked_arguments[argument[:name]] = provided_argument
75
+ checked_arguments
76
+ end
77
+ end
78
+
79
+ end
80
+
81
+ def initialize(arguments, job_properties)
82
+ set_job_arguments(arguments)
83
+ set_job_properties(job_properties)
84
+ end
85
+
86
+ # Perform method needs to be overridden in job classes
87
+ def perform
88
+ raise NoMethodError, "No method #{self.class.name}#perform defined!"
89
+ end
90
+
91
+ def arguments
92
+ @_arguments
93
+ end
94
+
95
+ def payload
96
+ @_arguments.merge(JOB_HASH => @_job_properties)
97
+ end
98
+
99
+ private
100
+
101
+ def set_job_properties(hash)
102
+ @_job_properties ||= {}
103
+ @_job_properties.merge!(hash)
104
+ end
105
+
106
+ def set_job_arguments(hash)
107
+ @_arguments = hash
108
+ arguments.each do |key, val|
109
+ instance_variable_set("@#{key}", val)
110
+ end
111
+ end
112
+
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "resque-pertry"
3
+ s.description = "Adds persistence to Resque jobs, and retry properties"
4
+ s.version = "1.0.0"
5
+ s.authors = [ "Anthony Powles" ]
6
+ s.email = "rubygems+resque-pertry@idreamz.net"
7
+ s.summary = "Allows job to be persistent, and be retried in case of failure"
8
+ s.homepage = "https://github.com/yogin/resque-pertry"
9
+ s.license = "MIT"
10
+ s.files = `git ls-files`.split($/)
11
+
12
+ s.add_development_dependency "bundler", "~> 1.3"
13
+ s.add_development_dependency "rake"
14
+ s.add_development_dependency "pry"
15
+
16
+ s.add_dependency "activesupport", ">= 3.0.0"
17
+ s.add_dependency "activerecord", ">= 3.0.0"
18
+ s.add_dependency "resque"
19
+ s.add_dependency "resque-scheduler", ">= 2.0.0"
20
+ s.add_dependency "uuidtools", "~> 2.1.4"
21
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'resque/pertry/tasks'
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-pertry
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Anthony Powles
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: pry
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: activesupport
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: 3.0.0
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: 3.0.0
78
+ - !ruby/object:Gem::Dependency
79
+ name: activerecord
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: 3.0.0
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: 3.0.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: resque
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: resque-scheduler
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: 2.0.0
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: 2.0.0
126
+ - !ruby/object:Gem::Dependency
127
+ name: uuidtools
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: 2.1.4
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: 2.1.4
142
+ description: Adds persistence to Resque jobs, and retry properties
143
+ email: rubygems+resque-pertry@idreamz.net
144
+ executables: []
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - .gitignore
149
+ - Gemfile
150
+ - LICENSE
151
+ - README.md
152
+ - lib/resque/pertry.rb
153
+ - lib/resque/pertry/migrations/create_resque_pertry_table.rb
154
+ - lib/resque/pertry/persistence.rb
155
+ - lib/resque/pertry/purger.rb
156
+ - lib/resque/pertry/resque_pertry_persistence.rb
157
+ - lib/resque/pertry/retry.rb
158
+ - lib/resque/pertry/tasks.rb
159
+ - lib/resque/plugins/pertry.rb
160
+ - resque-pertry.gemspec
161
+ - tasks/resque_pertry.rake
162
+ homepage: https://github.com/yogin/resque-pertry
163
+ licenses:
164
+ - MIT
165
+ post_install_message:
166
+ rdoc_options: []
167
+ require_paths:
168
+ - lib
169
+ required_ruby_version: !ruby/object:Gem::Requirement
170
+ none: false
171
+ requirements:
172
+ - - ! '>='
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
176
+ none: false
177
+ requirements:
178
+ - - ! '>='
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubyforge_project:
183
+ rubygems_version: 1.8.23
184
+ signing_key:
185
+ specification_version: 3
186
+ summary: Allows job to be persistent, and be retried in case of failure
187
+ test_files: []