resque-status 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +23 -0
- data/LICENSE +20 -0
- data/README.rdoc +145 -0
- data/Rakefile +60 -0
- data/examples/sleep_job.rb +35 -0
- data/init.rb +1 -0
- data/lib/resque/job_with_status.rb +187 -0
- data/lib/resque/server/views/status.erb +57 -0
- data/lib/resque/server/views/status_styles.erb +98 -0
- data/lib/resque/server/views/statuses.erb +60 -0
- data/lib/resque/status.rb +206 -0
- data/lib/resque/status_server.rb +44 -0
- data/resque-status.gemspec +74 -0
- data/test/redis-test.conf +132 -0
- data/test/test_helper.rb +82 -0
- data/test/test_resque-job_with_status.rb +180 -0
- data/test/test_resque-status.rb +107 -0
- metadata +115 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Aaron Quint
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
= resque-status
|
2
|
+
|
3
|
+
resque-status is an extension to the resque queue system that provides simple trackable jobs.
|
4
|
+
|
5
|
+
== About
|
6
|
+
|
7
|
+
resque-status provides a set of simple classes that extend resque's default
|
8
|
+
functionality (with 0% monkey patching) to give apps a way to track specific
|
9
|
+
job instances and their status. It achieves this by giving job instances UUID's
|
10
|
+
and allowing the job instances to report their status from within their iterations.
|
11
|
+
|
12
|
+
== Installation
|
13
|
+
|
14
|
+
gem install resque-status
|
15
|
+
|
16
|
+
To use with Rails, you can install as a plugin or add the gem to you're config:
|
17
|
+
|
18
|
+
# environment.rb
|
19
|
+
config.gem 'resque-status', :lib => 'resque/status'
|
20
|
+
|
21
|
+
Then in an initializer:
|
22
|
+
|
23
|
+
# config/initializers/resque.rb
|
24
|
+
require 'resque/job_with_status'
|
25
|
+
|
26
|
+
Resque.redis = "your/redis/socket" # default localhost:6379
|
27
|
+
Resque::Status.expire_in = (24 * 60 * 60) # 24hrs in seconds
|
28
|
+
|
29
|
+
== Usage
|
30
|
+
|
31
|
+
The most direct way to use resque-status is to create your jobs using the
|
32
|
+
Resque::JobWithStatus class. An example job would look something like:
|
33
|
+
|
34
|
+
class SleepJob < Resque::JobWithStatus
|
35
|
+
|
36
|
+
def perform
|
37
|
+
total = options['length'].to_i || 1000
|
38
|
+
num = 0
|
39
|
+
while num < total
|
40
|
+
at(num, total, "At #{num} of #{total}")
|
41
|
+
sleep(1)
|
42
|
+
num += 1
|
43
|
+
end
|
44
|
+
completed
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
Instead of normal Resque job classes, we inherit from the JobWithStatus class.
|
50
|
+
Another major difference is that intead of implementing <tt>perform</tt> as a
|
51
|
+
class method, we do our job implementation within instances of the job class.
|
52
|
+
|
53
|
+
In order to queue a SleepJob up, we also won't use <tt>Resque.enqueue</tt>, instead
|
54
|
+
we'll use the <tt>create</tt> class method which will wrap <tt>enqueue</tt> and
|
55
|
+
creating a unique id (UUID) for us to track the job with.
|
56
|
+
|
57
|
+
job_id = SleepJob.create(:length => 100)
|
58
|
+
|
59
|
+
This will create a UUID enqueue the job and pass the :length option on the SleepJob
|
60
|
+
instance as options['length'] (as you can see above).
|
61
|
+
|
62
|
+
Now that we have a UUID its really easy to get the status:
|
63
|
+
|
64
|
+
status = Resque::Status.get(job_id)
|
65
|
+
|
66
|
+
This returns a Resque::Status object, which is a Hash (with benefits).
|
67
|
+
|
68
|
+
status.pct_complete #=> 0
|
69
|
+
status.status #=> 'queued'
|
70
|
+
status.queued? #=> true
|
71
|
+
status.working? #=> false
|
72
|
+
status.time #=> Time object
|
73
|
+
status.message #=> "Created at ..."
|
74
|
+
|
75
|
+
Once the worker reserves the job, the instance of SleepJob updates the status at
|
76
|
+
each iteration using <tt>at()</tt>
|
77
|
+
|
78
|
+
status = Resque::Status.get(job_id)
|
79
|
+
status.working? #=> true
|
80
|
+
status.num #=> 5
|
81
|
+
status.total => 100
|
82
|
+
status.pct_complete => 5
|
83
|
+
|
84
|
+
If an error occurs within the job instance, the status is set to 'failed' and then
|
85
|
+
the error is re-raised so that Resque can capture it.
|
86
|
+
|
87
|
+
Its also possible to get a list of current/recent job statuses:
|
88
|
+
|
89
|
+
Resque::Status.statuses #=> [#<Resque::Status>, ...]
|
90
|
+
|
91
|
+
=== Kill! Kill! Kill!
|
92
|
+
|
93
|
+
Because we're tracking UUIDs per instance, and we're checking in/updating the status
|
94
|
+
on each iteration (using <tt>at</tt> or <tt>tick</tt>) we can kill specific jobs
|
95
|
+
by UUID.
|
96
|
+
|
97
|
+
Resque::Status.kill(job_id)
|
98
|
+
|
99
|
+
The next time the job at job_id calls <tt>at</tt> or tick, it will raise a Killed
|
100
|
+
error and set the status to killed.
|
101
|
+
|
102
|
+
=== Expiration
|
103
|
+
|
104
|
+
Since Redis is RAM based, we probably don't want to keep these statuses around forever
|
105
|
+
(at least until @antirez releases the VM feature). By setting expire_in, all statuses
|
106
|
+
and thier related keys will expire in expire_in seconds from the last time theyre updated:
|
107
|
+
|
108
|
+
Resque::Status.expire_in = (60 * 60) # 1 hour
|
109
|
+
|
110
|
+
=== resque-web
|
111
|
+
|
112
|
+
Though the main purpose of these trackable jobs is to allow you to surface the status
|
113
|
+
of user created jobs through you're apps' own UI, I've added a simple example UI
|
114
|
+
as a plugin to resque-web.
|
115
|
+
|
116
|
+
To use, you need to setup a resque-web config file:
|
117
|
+
|
118
|
+
# ~/resque_conf.rb
|
119
|
+
require 'resque/status_server'
|
120
|
+
|
121
|
+
The start resque-web with your config:
|
122
|
+
|
123
|
+
resque-web ~/resque_conf.rb
|
124
|
+
|
125
|
+
This should launch resque-web in your browser and you should see a 'Statuses' tab.
|
126
|
+
|
127
|
+
http://img.skitch.com/20100119-k166xyijcjpkk6xtwnw3854a8g.jpg
|
128
|
+
|
129
|
+
== Thanks
|
130
|
+
|
131
|
+
Resque is awesome, @defunkt needs a shout-out.
|
132
|
+
|
133
|
+
== Note on Patches/Pull Requests
|
134
|
+
|
135
|
+
* Fork the project.
|
136
|
+
* Make your feature addition or bug fix.
|
137
|
+
* Add tests for it. This is important so I don't break it in a
|
138
|
+
future version unintentionally.
|
139
|
+
* Commit, do not mess with rakefile, version, or history.
|
140
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
141
|
+
* Send me a pull request. Bonus points for topic branches.
|
142
|
+
|
143
|
+
== Copyright
|
144
|
+
|
145
|
+
Copyright (c) 2010 Aaron Quint. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'lib/resque/status'
|
4
|
+
require 'resque/tasks'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gem|
|
9
|
+
gem.name = "resque-status"
|
10
|
+
gem.version = Resque::Status::VERSION
|
11
|
+
gem.summary = %Q{resque-status is an extension to the resque queue system that provides simple trackable jobs.}
|
12
|
+
gem.description = %Q{resque-status is an extension to the resque queue system that provides simple trackable jobs. It provides a Resque::Status class which can set/get the statuses of jobs and a Resque::JobWithStatus class that when subclassed provides easily trackable/killable jobs.}
|
13
|
+
gem.email = "aaron@quirkey.com"
|
14
|
+
gem.homepage = "http://github.com/quirkey/resque-status"
|
15
|
+
gem.rubyforge_project = "quirkey"
|
16
|
+
gem.authors = ["Aaron Quint"]
|
17
|
+
gem.add_dependency "uuid", ">=2.0.2"
|
18
|
+
gem.add_dependency "resque", ">=1.3.1"
|
19
|
+
gem.add_dependency "redisk", ">=0.2.0"
|
20
|
+
gem.add_development_dependency "shoulda", ">=2.10.2"
|
21
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
22
|
+
end
|
23
|
+
Jeweler::GemcutterTasks.new
|
24
|
+
rescue LoadError
|
25
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
Rake::TestTask.new(:test) do |test|
|
30
|
+
test.libs << 'lib' << 'test'
|
31
|
+
test.pattern = 'test/**/test_*.rb'
|
32
|
+
test.verbose = true
|
33
|
+
end
|
34
|
+
|
35
|
+
begin
|
36
|
+
require 'rcov/rcovtask'
|
37
|
+
Rcov::RcovTask.new do |test|
|
38
|
+
test.libs << 'test'
|
39
|
+
test.pattern = 'test/**/test_*.rb'
|
40
|
+
test.verbose = true
|
41
|
+
end
|
42
|
+
rescue LoadError
|
43
|
+
task :rcov do
|
44
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
task :test => :check_dependencies
|
49
|
+
|
50
|
+
task :default => :test
|
51
|
+
|
52
|
+
require 'rake/rdoctask'
|
53
|
+
Rake::RDocTask.new do |rdoc|
|
54
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
55
|
+
|
56
|
+
rdoc.rdoc_dir = 'rdoc'
|
57
|
+
rdoc.title = "resque-status #{version}"
|
58
|
+
rdoc.rdoc_files.include('README*')
|
59
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
60
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'resque/job_with_status' # in rails you would probably do this in an initializer
|
2
|
+
|
3
|
+
# sleeps for _length_ seconds updating the status every second
|
4
|
+
|
5
|
+
class SleepJob < Resque::JobWithStatus
|
6
|
+
|
7
|
+
def perform
|
8
|
+
total = options['length'].to_i || 1000
|
9
|
+
num = 0
|
10
|
+
while num < total
|
11
|
+
at(num, total, "At #{num} of #{total}")
|
12
|
+
sleep(1)
|
13
|
+
num += 1
|
14
|
+
end
|
15
|
+
completed
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
if __FILE__ == $0
|
22
|
+
# Make sure you have a worker running
|
23
|
+
# rake -rexamples/sleep_job.rb resque:work QUEUE=statused
|
24
|
+
|
25
|
+
# running the job
|
26
|
+
puts "Creating the SleepJob"
|
27
|
+
job_id = SleepJob.create :length => 100
|
28
|
+
puts "Got back #{job_id}"
|
29
|
+
|
30
|
+
# check the status until its complete
|
31
|
+
while status = Resque::Status.get(job_id) and !status.completed? && !status.failed?
|
32
|
+
sleep 1
|
33
|
+
puts status.inspect
|
34
|
+
end
|
35
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'resque/status'
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'resque/status'
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
|
5
|
+
# JobWithStatus is a base class that you're jobs will inherit from.
|
6
|
+
# It provides helper methods for updating the status/etc from within an
|
7
|
+
# instance as well as class methods for creating and queuing the jobs.
|
8
|
+
#
|
9
|
+
# All you have to do to get this functionality is inherit from JobWithStatus
|
10
|
+
# and then implement a <tt>perform<tt> method.
|
11
|
+
#
|
12
|
+
# For example:
|
13
|
+
#
|
14
|
+
# class ExampleJob < Resque::JobWithStatus
|
15
|
+
#
|
16
|
+
# def perform
|
17
|
+
# num = options['num']
|
18
|
+
# i = 0
|
19
|
+
# while i < num
|
20
|
+
# i += 1
|
21
|
+
# at(i, num)
|
22
|
+
# end
|
23
|
+
# completed("Finished!")
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# This job would iterate num times updating the status as it goes. At the end
|
29
|
+
# we update the status telling anyone listening to this job that its complete.
|
30
|
+
class JobWithStatus
|
31
|
+
|
32
|
+
# The error class raised when a job is killed
|
33
|
+
class Killed < RuntimeError; end
|
34
|
+
|
35
|
+
attr_reader :uuid, :options
|
36
|
+
|
37
|
+
# The default queue is :statused, this can be ovveridden in the specific job
|
38
|
+
# class to put the jobs on a specific worker queue
|
39
|
+
def self.queue
|
40
|
+
:statused
|
41
|
+
end
|
42
|
+
|
43
|
+
# used when displaying the Job in the resque-web UI and identifiyng the job
|
44
|
+
# type by status. By default this is the name of the job class, but can be
|
45
|
+
# ovveridden in the specific job class to present a more user friendly job
|
46
|
+
# name
|
47
|
+
def self.name
|
48
|
+
self.to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create is the primary method for adding jobs to the queue. This would be
|
52
|
+
# called on the job class to create a job of that type. Any options passed are
|
53
|
+
# passed to the Job instance as a hash of options. It returns the UUID of the
|
54
|
+
# job.
|
55
|
+
#
|
56
|
+
# == Example:
|
57
|
+
#
|
58
|
+
# class ExampleJob < Resque::JobWithStatus
|
59
|
+
#
|
60
|
+
# def perform
|
61
|
+
# set_status "Hey I'm a job num #{options['num']}"
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# job_id = ExampleJob.create(:num => 100)
|
67
|
+
#
|
68
|
+
def self.create(options = {})
|
69
|
+
self.enqueue(self, options)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Adds a job of type <tt>klass<tt> to the queue with <tt>options<tt>.
|
73
|
+
# Returns the UUID of the job
|
74
|
+
def self.enqueue(klass, options = {})
|
75
|
+
uuid = Resque::Status.create
|
76
|
+
Resque.enqueue(klass, uuid, options)
|
77
|
+
uuid
|
78
|
+
end
|
79
|
+
|
80
|
+
# This is the method called by Resque::Worker when processing jobs. It
|
81
|
+
# creates a new instance of the job class and populates it with the uuid and
|
82
|
+
# options.
|
83
|
+
#
|
84
|
+
# You should not override this method, rahter the <tt>perform</tt> instance method.
|
85
|
+
def self.perform(uuid, options = {})
|
86
|
+
instance = new(uuid, options)
|
87
|
+
instance.safe_perform!
|
88
|
+
instance
|
89
|
+
end
|
90
|
+
|
91
|
+
# Create a new instance with <tt>uuid</tt> and <tt>options</tt>
|
92
|
+
def initialize(uuid, options = {})
|
93
|
+
@uuid = uuid
|
94
|
+
@options = options
|
95
|
+
end
|
96
|
+
|
97
|
+
# Run by the Resque::Worker when processing this job. It wraps the <tt>perform</tt>
|
98
|
+
# method ensuring that the final status of the job is set regardless of error.
|
99
|
+
# If an error occurs within the job's work, it will set the status as failed and
|
100
|
+
# re-raise the error.
|
101
|
+
def safe_perform!
|
102
|
+
perform
|
103
|
+
completed unless status && status.completed?
|
104
|
+
rescue Killed
|
105
|
+
logger.info "Job #{self} Killed at #{Time.now}"
|
106
|
+
Resque::Status.killed(uuid)
|
107
|
+
rescue => e
|
108
|
+
logger.error e
|
109
|
+
failed("The task failed because of an error: #{e.inspect}")
|
110
|
+
raise e
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns a Redisk::Logger object scoped to this paticular job/uuid
|
114
|
+
def logger
|
115
|
+
@logger ||= Resque::Status.logger(uuid)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Set the jobs status. Can take an array of strings or hashes that are merged
|
119
|
+
# (in order) into a final status hash.
|
120
|
+
def status=(new_status)
|
121
|
+
Resque::Status.set(uuid, *new_status)
|
122
|
+
end
|
123
|
+
|
124
|
+
# get the Resque::Status object for the current uuid
|
125
|
+
def status
|
126
|
+
Resque::Status.get(uuid)
|
127
|
+
end
|
128
|
+
|
129
|
+
def name
|
130
|
+
self.class.name
|
131
|
+
end
|
132
|
+
|
133
|
+
# Checks against the kill list if this specific job instance should be killed
|
134
|
+
# on the next iteration
|
135
|
+
def should_kill?
|
136
|
+
Resque::Status.should_kill?(uuid)
|
137
|
+
end
|
138
|
+
|
139
|
+
# set the status of the job for the current itteration. <tt>num</tt> and
|
140
|
+
# <tt>total</tt> are passed to the status as well as any messages.
|
141
|
+
# This will kill the job if it has been added to the kill list with
|
142
|
+
# <tt>Resque::Status.kill()</tt>
|
143
|
+
def at(num, total, *messages)
|
144
|
+
tick({
|
145
|
+
'num' => num,
|
146
|
+
'total' => total
|
147
|
+
}, *messages)
|
148
|
+
end
|
149
|
+
|
150
|
+
# sets the status of the job for the current itteration. You should use
|
151
|
+
# the <tt>at</tt> method if you have actual numbers to track the iteration count.
|
152
|
+
# This will kill the job if it has been added to the kill list with
|
153
|
+
# <tt>Resque::Status.kill()</tt>
|
154
|
+
def tick(*messages)
|
155
|
+
kill! if should_kill?
|
156
|
+
set_status({'status' => 'working'}, *messages)
|
157
|
+
end
|
158
|
+
|
159
|
+
# set the status to 'failed' passing along any additional messages
|
160
|
+
def failed(*messages)
|
161
|
+
set_status({'status' => 'failed'}, *messages)
|
162
|
+
end
|
163
|
+
|
164
|
+
# set the status to 'completed' passing along any addional messages
|
165
|
+
def completed(*messages)
|
166
|
+
set_status({
|
167
|
+
'status' => 'completed',
|
168
|
+
'message' => "Completed at #{Time.now}"
|
169
|
+
}, *messages)
|
170
|
+
end
|
171
|
+
|
172
|
+
# kill the current job, setting the status to 'killed' and raising <tt>Killed</tt>
|
173
|
+
def kill!
|
174
|
+
set_status({
|
175
|
+
'status' => 'killed',
|
176
|
+
'message' => "Killed"
|
177
|
+
})
|
178
|
+
raise Killed
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
def set_status(*args)
|
183
|
+
self.status = [{'name' => self.name}, args].flatten
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
end
|