resque-status 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ doc
23
+ .yardoc
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