resque-status 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,213 @@
1
+ require 'resque/plugins/status/hash'
2
+
3
+ module Resque
4
+ module Plugins
5
+
6
+ # Resque::Plugins::Status is a module you're jobs will include.
7
+ # It provides helper methods for updating the status/etc from within an
8
+ # instance as well as class methods for creating and queuing the jobs.
9
+ #
10
+ # All you have to do to get this functionality is include Resque::Plugins::Status
11
+ # and then implement a <tt>perform<tt> method.
12
+ #
13
+ # For example
14
+ #
15
+ # class ExampleJob
16
+ # include Resque::Plugins::Status
17
+ #
18
+ # def perform
19
+ # num = options['num']
20
+ # i = 0
21
+ # while i < num
22
+ # i += 1
23
+ # at(i, num)
24
+ # end
25
+ # completed("Finished!")
26
+ # end
27
+ #
28
+ # end
29
+ #
30
+ # This job would iterate num times updating the status as it goes. At the end
31
+ # we update the status telling anyone listening to this job that its complete.
32
+ module Status
33
+
34
+ # The error class raised when a job is killed
35
+ class Killed < RuntimeError; end
36
+
37
+ attr_reader :uuid, :options
38
+
39
+ def self.included(base)
40
+ base.extend(ClassMethods)
41
+ end
42
+
43
+ module ClassMethods
44
+
45
+ # The default queue is :statused, this can be ovveridden in the specific job
46
+ # class to put the jobs on a specific worker queue
47
+ def queue
48
+ :statused
49
+ end
50
+
51
+ # used when displaying the Job in the resque-web UI and identifiyng the job
52
+ # type by status. By default this is the name of the job class, but can be
53
+ # ovveridden in the specific job class to present a more user friendly job
54
+ # name
55
+ def name
56
+ self.to_s
57
+ end
58
+
59
+ # Create is the primary method for adding jobs to the queue. This would be
60
+ # called on the job class to create a job of that type. Any options passed are
61
+ # passed to the Job instance as a hash of options. It returns the UUID of the
62
+ # job.
63
+ #
64
+ # == Example:
65
+ #
66
+ # class ExampleJob
67
+ # include Resque::Plugins::Status
68
+ #
69
+ # def perform
70
+ # set_status "Hey I'm a job num #{options['num']}"
71
+ # end
72
+ #
73
+ # end
74
+ #
75
+ # job_id = ExampleJob.create(:num => 100)
76
+ #
77
+ def create(options = {})
78
+ self.enqueue(self, options)
79
+ end
80
+
81
+ # Adds a job of type <tt>klass<tt> to the queue with <tt>options<tt>.
82
+ # Returns the UUID of the job
83
+ def enqueue(klass, options = {})
84
+ uuid = Resque::Plugins::Status::Hash.create :options => options
85
+ Resque.enqueue(klass, uuid, options)
86
+ uuid
87
+ end
88
+
89
+ # This is the method called by Resque::Worker when processing jobs. It
90
+ # creates a new instance of the job class and populates it with the uuid and
91
+ # options.
92
+ #
93
+ # You should not override this method, rahter the <tt>perform</tt> instance method.
94
+ def perform(uuid=nil, options = {})
95
+ uuid ||= Resque::Plugins::Status::Hash.generate_uuid
96
+ instance = new(uuid, options)
97
+ instance.safe_perform!
98
+ instance
99
+ end
100
+
101
+ # Wrapper API to forward a Resque::Job creation API call into a Resque::Plugins::Status call.
102
+ # This is needed to be used with resque scheduler
103
+ # http://github.com/bvandenbos/resque-scheduler
104
+ def scheduled(queue, klass, *args)
105
+ create(*args)
106
+ end
107
+ end
108
+
109
+ # Create a new instance with <tt>uuid</tt> and <tt>options</tt>
110
+ def initialize(uuid, options = {})
111
+ @uuid = uuid
112
+ @options = options
113
+ end
114
+
115
+ # Run by the Resque::Worker when processing this job. It wraps the <tt>perform</tt>
116
+ # method ensuring that the final status of the job is set regardless of error.
117
+ # If an error occurs within the job's work, it will set the status as failed and
118
+ # re-raise the error.
119
+ def safe_perform!
120
+ set_status({'status' => 'working'})
121
+ perform
122
+ completed unless status && status.completed?
123
+ on_success if respond_to?(:on_success)
124
+ rescue Killed
125
+ logger.info "Job #{self} Killed at #{Time.now}"
126
+ Resque::Plugins::Status::Hash.killed(uuid)
127
+ on_killed if respond_to?(:on_killed)
128
+ rescue => e
129
+ logger.error e
130
+ failed("The task failed because of an error: #{e}")
131
+ if respond_to?(:on_failure)
132
+ on_failure(e)
133
+ else
134
+ raise e
135
+ end
136
+ end
137
+
138
+ # Returns a Redisk::Logger object scoped to this paticular job/uuid
139
+ def logger
140
+ @logger ||= Resque::Plugins::Status::Hash.logger(uuid)
141
+ end
142
+
143
+ # Set the jobs status. Can take an array of strings or hashes that are merged
144
+ # (in order) into a final status hash.
145
+ def status=(new_status)
146
+ Resque::Plugins::Status::Hash.set(uuid, *new_status)
147
+ end
148
+
149
+ # get the Resque::Plugins::Status::Hash object for the current uuid
150
+ def status
151
+ Resque::Plugins::Status::Hash.get(uuid)
152
+ end
153
+
154
+ def name
155
+ "#{self.class.name}(#{options.inspect unless options.empty?})"
156
+ end
157
+
158
+ # Checks against the kill list if this specific job instance should be killed
159
+ # on the next iteration
160
+ def should_kill?
161
+ Resque::Plugins::Status::Hash.should_kill?(uuid)
162
+ end
163
+
164
+ # set the status of the job for the current itteration. <tt>num</tt> and
165
+ # <tt>total</tt> are passed to the status as well as any messages.
166
+ # This will kill the job if it has been added to the kill list with
167
+ # <tt>Resque::Plugins::Status::Hash.kill()</tt>
168
+ def at(num, total, *messages)
169
+ tick({
170
+ 'num' => num,
171
+ 'total' => total
172
+ }, *messages)
173
+ end
174
+
175
+ # sets the status of the job for the current itteration. You should use
176
+ # the <tt>at</tt> method if you have actual numbers to track the iteration count.
177
+ # This will kill the job if it has been added to the kill list with
178
+ # <tt>Resque::Plugins::Status::Hash.kill()</tt>
179
+ def tick(*messages)
180
+ kill! if should_kill?
181
+ set_status({'status' => 'working'}, *messages)
182
+ end
183
+
184
+ # set the status to 'failed' passing along any additional messages
185
+ def failed(*messages)
186
+ set_status({'status' => 'failed'}, *messages)
187
+ end
188
+
189
+ # set the status to 'completed' passing along any addional messages
190
+ def completed(*messages)
191
+ set_status({
192
+ 'status' => 'completed',
193
+ 'message' => "Completed at #{Time.now}"
194
+ }, *messages)
195
+ end
196
+
197
+ # kill the current job, setting the status to 'killed' and raising <tt>Killed</tt>
198
+ def kill!
199
+ set_status({
200
+ 'status' => 'killed',
201
+ 'message' => "Killed at #{Time.now}"
202
+ })
203
+ raise Killed
204
+ end
205
+
206
+ private
207
+ def set_status(*args)
208
+ self.status = [status, {'name' => self.name}, args].flatten
209
+ end
210
+
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,242 @@
1
+ module Resque
2
+ module Plugins
3
+ module Status
4
+ VERSION = '0.3.0'
5
+
6
+ # Resque::Plugins::Status::Hash is a Hash object that has helper methods for dealing with
7
+ # the common status attributes. It also has a number of class methods for
8
+ # creating/updating/retrieving status objects from Redis
9
+ class Hash < ::Hash
10
+
11
+ extend Resque::Helpers
12
+
13
+ # Create a status, generating a new UUID, passing the message to the status
14
+ # Returns the UUID of the new status.
15
+ def self.create(*messages)
16
+ uuid = generate_uuid
17
+ set(uuid, *messages)
18
+ redis.zadd(set_key, Time.now.to_i, uuid)
19
+ redis.zremrangebyscore(set_key, 0, Time.now.to_i - @expire_in) if @expire_in
20
+ uuid
21
+ end
22
+
23
+ # Get a status by UUID. Returns a Resque::Plugins::Status::Hash
24
+ def self.get(uuid)
25
+ val = redis.get(status_key(uuid))
26
+ val ? Resque::Plugins::Status::Hash.new(uuid, decode(val)) : nil
27
+ end
28
+
29
+ # set a status by UUID. <tt>messages</tt> can be any number of stirngs or hashes
30
+ # that are merged in order to create a single status.
31
+ def self.set(uuid, *messages)
32
+ val = Resque::Plugins::Status::Hash.new(uuid, *messages)
33
+ redis.set(status_key(uuid), encode(val))
34
+ if expire_in
35
+ redis.expire(status_key(uuid), expire_in)
36
+ end
37
+ val
38
+ end
39
+
40
+ # clear statuses from redis passing an optional range. See `statuses` for info
41
+ # about ranges
42
+ def self.clear(range_start = nil, range_end = nil)
43
+ status_ids(range_start, range_end).each do |id|
44
+ redis.del(status_key(id))
45
+ redis.zrem(set_key, id)
46
+ end
47
+ end
48
+
49
+ # returns a Redisk::Logger scoped to the UUID. Any options passed are passed
50
+ # to the logger initialization.
51
+ #
52
+ # Ensures that Redisk is logging to the same Redis connection as Resque.
53
+ def self.logger(uuid, options = {})
54
+ Redisk.redis = redis
55
+ Redisk::Logger.new(logger_key(uuid), options)
56
+ end
57
+
58
+ def self.count
59
+ redis.zcard(set_key)
60
+ end
61
+
62
+ # Return <tt>num</tt> Resque::Plugins::Status::Hash objects in reverse chronological order.
63
+ # By default returns the entire set.
64
+ # @param [Numeric] range_start The optional starting range
65
+ # @param [Numeric] range_end The optional ending range
66
+ # @example retuning the last 20 statuses
67
+ # Resque::Plugins::Status::Hash.statuses(0, 20)
68
+ def self.statuses(range_start = nil, range_end = nil)
69
+ status_ids(range_start, range_end).collect do |id|
70
+ get(id)
71
+ end.compact
72
+ end
73
+
74
+ # Return the <tt>num</tt> most recent status/job UUIDs in reverse chronological order.
75
+ def self.status_ids(range_start = nil, range_end = nil)
76
+ unless range_end && range_start
77
+ # Because we want a reverse chronological order, we need to get a range starting
78
+ # by the higest negative number.
79
+ redis.zrevrange(set_key, 0, -1) || []
80
+ else
81
+ # Because we want a reverse chronological order, we need to get a range starting
82
+ # by the higest negative number. The ordering is transparent from the API user's
83
+ # perspective so we need to convert the passed params
84
+ (redis.zrevrange(set_key, (range_start.abs), ((range_end || 1).abs)) || [])
85
+ end
86
+ end
87
+
88
+ # Kill the job at UUID on its next iteration this works by adding the UUID to a
89
+ # kill list (a.k.a. a list of jobs to be killed. Each iteration the job checks
90
+ # if it _should_ be killed by calling <tt>tick</tt> or <tt>at</tt>. If so, it raises
91
+ # a <tt>Resque::Plugins::Status::Killed</tt> error and sets the status to 'killed'.
92
+ def self.kill(uuid)
93
+ redis.sadd(kill_key, uuid)
94
+ end
95
+
96
+ # Remove the job at UUID from the kill list
97
+ def self.killed(uuid)
98
+ redis.srem(kill_key, uuid)
99
+ end
100
+
101
+ # Return the UUIDs of the jobs on the kill list
102
+ def self.kill_ids
103
+ redis.smembers(kill_key)
104
+ end
105
+
106
+ # Check whether a job with UUID is on the kill list
107
+ def self.should_kill?(uuid)
108
+ redis.sismember(kill_key, uuid)
109
+ end
110
+
111
+ # The time in seconds that jobs and statuses should expire from Redis (after
112
+ # the last time they are touched/updated)
113
+ def self.expire_in
114
+ @expire_in
115
+ end
116
+
117
+ # Set the <tt>expire_in</tt> time in seconds
118
+ def self.expire_in=(seconds)
119
+ @expire_in = seconds.nil? ? nil : seconds.to_i
120
+ end
121
+
122
+ def self.status_key(uuid)
123
+ "status:#{uuid}"
124
+ end
125
+
126
+ def self.set_key
127
+ "_statuses"
128
+ end
129
+
130
+ def self.kill_key
131
+ "_kill"
132
+ end
133
+
134
+ def self.logger_key(uuid)
135
+ "_log:#{uuid}"
136
+ end
137
+
138
+ def self.generate_uuid
139
+ UUID.generate(:compact)
140
+ end
141
+
142
+ def self.hash_accessor(name, options = {})
143
+ options[:default] ||= nil
144
+ coerce = options[:coerce] ? ".#{options[:coerce]}" : ""
145
+ module_eval <<-EOT
146
+ def #{name}
147
+ value = (self['#{name}'] ? self['#{name}']#{coerce} : #{options[:default].inspect})
148
+ yield value if block_given?
149
+ value
150
+ end
151
+
152
+ def #{name}=(value)
153
+ self['#{name}'] = value
154
+ end
155
+
156
+ def #{name}?
157
+ !!self['#{name}']
158
+ end
159
+ EOT
160
+ end
161
+
162
+ STATUSES = %w{queued working completed failed killed}.freeze
163
+
164
+ hash_accessor :uuid
165
+ hash_accessor :name
166
+ hash_accessor :status
167
+ hash_accessor :message
168
+ hash_accessor :time
169
+ hash_accessor :options
170
+
171
+ hash_accessor :num
172
+ hash_accessor :total
173
+
174
+ # Create a new Resque::Plugins::Status::Hash object. If multiple arguments are passed
175
+ # it is assumed the first argument is the UUID and the rest are status objects.
176
+ # All arguments are subsequentily merged in order. Strings are assumed to
177
+ # be messages.
178
+ def initialize(*args)
179
+ super nil
180
+ base_status = {
181
+ 'time' => Time.now.to_i,
182
+ 'status' => 'queued'
183
+ }
184
+ base_status['uuid'] = args.shift if args.length > 1
185
+ status_hash = args.inject(base_status) do |final, m|
186
+ m = {'message' => m} if m.is_a?(String)
187
+ final.merge(m || {})
188
+ end
189
+ self.replace(status_hash)
190
+ end
191
+
192
+ # calculate the % completion of the job based on <tt>status</tt>, <tt>num</tt>
193
+ # and <tt>total</tt>
194
+ def pct_complete
195
+ case status
196
+ when 'completed' then 100
197
+ when 'queued' then 0
198
+ else
199
+ t = (total == 0 || total.nil?) ? 1 : total
200
+ (((num || 0).to_f / t.to_f) * 100).to_i
201
+ end
202
+ end
203
+
204
+ # Return the time of the status initialization. If set returns a <tt>Time</tt>
205
+ # object, otherwise returns nil
206
+ def time
207
+ time? ? Time.at(self['time']) : nil
208
+ end
209
+
210
+ STATUSES.each do |status|
211
+ define_method("#{status}?") do
212
+ self['status'] === status
213
+ end
214
+ end
215
+
216
+ # Can the job be killed? 'failed', 'completed', and 'killed' jobs cant be killed
217
+ # (for pretty obvious reasons)
218
+ def killable?
219
+ !['failed', 'completed', 'killed'].include?(self.status)
220
+ end
221
+
222
+ unless method_defined?(:to_json)
223
+ def to_json(*args)
224
+ json
225
+ end
226
+ end
227
+
228
+ # Return a JSON representation of the current object.
229
+ def json
230
+ h = self.dup
231
+ h['pct_complete'] = pct_complete
232
+ self.class.encode(h)
233
+ end
234
+
235
+ def inspect
236
+ "#<Resque::Plugins::Status::Hash #{super}>"
237
+ end
238
+
239
+ end
240
+ end
241
+ end
242
+ end