resque-pertry 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +190 -0
- data/lib/resque/pertry.rb +7 -0
- data/lib/resque/pertry/migrations/create_resque_pertry_table.rb +30 -0
- data/lib/resque/pertry/persistence.rb +67 -0
- data/lib/resque/pertry/purger.rb +214 -0
- data/lib/resque/pertry/resque_pertry_persistence.rb +87 -0
- data/lib/resque/pertry/retry.rb +164 -0
- data/lib/resque/pertry/tasks.rb +22 -0
- data/lib/resque/plugins/pertry.rb +115 -0
- data/resque-pertry.gemspec +21 -0
- data/tasks/resque_pertry.rake +2 -0
- metadata +187 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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,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
|
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: []
|