resque-status 0.1.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/.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
|