resque-pertry 1.0.0

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/.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: []