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 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