resque-state 1.0.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.
@@ -0,0 +1,326 @@
1
+ require 'securerandom'
2
+
3
+ module Resque
4
+ module Plugins
5
+ module State
6
+ # Resque::Plugins::State::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
+ # Create a status, generating a new UUID, passing the message to the status
11
+ # Returns the UUID of the new status.
12
+ def self.create(uuid, *messages)
13
+ set(uuid, *messages)
14
+ redis.zadd(set_key, Time.now.to_i, uuid)
15
+ redis.zremrangebyscore(set_key, 0, Time.now.to_i - @expire_in) if @expire_in
16
+ uuid
17
+ end
18
+
19
+ # Get a status by UUID. Returns a Resque::Plugins::State::Hash
20
+ def self.get(uuid)
21
+ val = redis.get(status_key(uuid))
22
+ val ? Resque::Plugins::State::Hash.new(uuid, decode(val)) : nil
23
+ end
24
+
25
+ # Get multiple statuses by UUID. Returns array of Resque::Plugins::State::Hash
26
+ def self.mget(uuids)
27
+ return [] if uuids.empty?
28
+ status_keys = uuids.map { |u| status_key(u) }
29
+ vals = redis.mget(*status_keys)
30
+
31
+ uuids.zip(vals).map do |uuid, val|
32
+ val ? Resque::Plugins::State::Hash.new(uuid, decode(val)) : nil
33
+ end
34
+ end
35
+
36
+ # set a status by UUID. <tt>messages</tt> can be any number of strings or hashes
37
+ # that are merged in order to create a single status.
38
+ def self.set(uuid, *messages)
39
+ val = Resque::Plugins::State::Hash.new(uuid, *messages)
40
+ redis.set(status_key(uuid), encode(val))
41
+ redis.expire(status_key(uuid), expire_in) if expire_in
42
+ val
43
+ end
44
+
45
+ # clear statuses from redis passing an optional range. See `statuses` for info
46
+ # about ranges
47
+ def self.clear(range_start = nil, range_end = nil)
48
+ status_ids(range_start, range_end).each do |id|
49
+ remove(id)
50
+ end
51
+ end
52
+
53
+ def self.clear_completed(range_start = nil, range_end = nil)
54
+ status_ids(range_start, range_end).select do |id|
55
+ if get(id).completed?
56
+ remove(id)
57
+ true
58
+ else
59
+ false
60
+ end
61
+ end
62
+ end
63
+
64
+ def self.clear_failed(range_start = nil, range_end = nil)
65
+ status_ids(range_start, range_end).select do |id|
66
+ if get(id).failed?
67
+ remove(id)
68
+ true
69
+ else
70
+ false
71
+ end
72
+ end
73
+ end
74
+
75
+ def self.remove(uuid)
76
+ redis.del(status_key(uuid))
77
+ redis.zrem(set_key, uuid)
78
+ end
79
+
80
+ # Return <tt>num</tt> Resque::Plugins::State::Hash objects in reverse chronological order.
81
+ # By default returns the entire set.
82
+ # @param [Numeric] range_start The optional starting range
83
+ # @param [Numeric] range_end The optional ending range
84
+ # @example retuning the last 20 statuses
85
+ # Resque::Plugins::State::Hash.statuses(0, 20)
86
+ def self.statuses(range_start = nil, range_end = nil)
87
+ ids = status_ids(range_start, range_end)
88
+ mget(ids).compact || []
89
+ end
90
+
91
+ # Return the <tt>num</tt> most recent status/job UUIDs in reverse chronological order.
92
+ def self.status_ids(range_start = nil, range_end = nil)
93
+ if range_end && range_start
94
+ # Because we want a reverse chronological order, we need to get a range starting
95
+ # by the higest negative number. The ordering is transparent from the API user's
96
+ # perspective so we need to convert the passed params
97
+ (redis.zrevrange(set_key, range_start.abs, (range_end || 1).abs) || [])
98
+ else
99
+ # Because we want a reverse chronological order, we need to get a range starting
100
+ # by the higest negative number.
101
+ redis.zrevrange(set_key, 0, -1) || []
102
+ end
103
+ end
104
+
105
+ # Kill the job at UUID on its next iteration this works by adding the UUID to a
106
+ # kill list (a.k.a. a list of jobs to be killed. Each iteration the job checks
107
+ # if it _should_ be killed by calling <tt>tick</tt> or <tt>at</tt>. If so, it raises
108
+ # a <tt>Resque::Plugins::State::Killed</tt> error and sets the status to 'killed'.
109
+ def self.kill(uuid)
110
+ redis.sadd(kill_key, uuid)
111
+ end
112
+
113
+ # Remove the job at UUID from the kill list
114
+ def self.killed(uuid)
115
+ redis.srem(kill_key, uuid)
116
+ end
117
+
118
+ # Return the UUIDs of the jobs on the kill list
119
+ def self.kill_ids
120
+ redis.smembers(kill_key)
121
+ end
122
+
123
+ # Kills <tt>num</tt> jobs within range starting with the most recent first.
124
+ # By default kills all jobs.
125
+ # Note that the same conditions apply as <tt>kill</tt>, i.e. only jobs that check
126
+ # on each iteration by calling <tt>tick</tt> or <tt>at</tt> are eligible to killed.
127
+ # @param [Numeric] range_start The optional starting range
128
+ # @param [Numeric] range_end The optional ending range
129
+ # @example killing the last 20 submitted jobs
130
+ # Resque::Plugins::State::Hash.killall(0, 20)
131
+ def self.killall(range_start = nil, range_end = nil)
132
+ status_ids(range_start, range_end).collect do |id|
133
+ kill(id)
134
+ end
135
+ end
136
+
137
+ # Check whether a job with UUID is on the kill list
138
+ def self.should_kill?(uuid)
139
+ redis.sismember(kill_key, uuid)
140
+ end
141
+
142
+ # pause the job at UUID on its next iteration this works by adding the UUID to a
143
+ # pause list (a.k.a. a list of jobs to be pauseed. Each iteration the job checks
144
+ # if it _should_ be pauseed by calling <tt>tick</tt> or <tt>at</tt>. If so, it sleeps
145
+ # for 10 seconds before checking again if it should continue sleeping
146
+ def self.pause(uuid)
147
+ redis.sadd(pause_key, uuid)
148
+ end
149
+
150
+ # Remove the job at UUID from the pause list
151
+ def self.unpause(uuid)
152
+ redis.srem(pause_key, uuid)
153
+ end
154
+
155
+ # Return the UUIDs of the jobs on the pause list
156
+ def self.pause_ids
157
+ redis.smembers(pause_key)
158
+ end
159
+
160
+ # pauses <tt>num</tt> jobs within range starting with the most recent first.
161
+ # By default pauses all jobs.
162
+ # Note that the same conditions apply as <tt>pause</tt>, i.e. only jobs that check
163
+ # on each iteration by calling <tt>tick</tt> or <tt>at</tt> are eligible to pauseed.
164
+ # @param [Numeric] range_start The optional starting range
165
+ # @param [Numeric] range_end The optional ending range
166
+ # @example pauseing the last 20 submitted jobs
167
+ # Resque::Plugins::State::Hash.pauseall(0, 20)
168
+ def self.pauseall(range_start = nil, range_end = nil)
169
+ status_ids(range_start, range_end).collect do |id|
170
+ pause(id)
171
+ end
172
+ end
173
+
174
+ # Check whether a job with UUID is on the pause list
175
+ def self.should_pause?(uuid)
176
+ redis.sismember(pause_key, uuid)
177
+ end
178
+
179
+ # The time in seconds that jobs and statuses should expire from Redis (after
180
+ # the last time they are touched/updated)
181
+ class << self
182
+ attr_reader :expire_in
183
+ end
184
+
185
+ # Set the <tt>expire_in</tt> time in seconds
186
+ def self.expire_in=(seconds)
187
+ @expire_in = seconds.nil? ? nil : seconds.to_i
188
+ end
189
+
190
+ def self.status_key(uuid)
191
+ "status:#{uuid}"
192
+ end
193
+
194
+ def self.set_key
195
+ '_statuses'
196
+ end
197
+
198
+ def self.kill_key
199
+ '_kill'
200
+ end
201
+
202
+ def self.pause_key
203
+ '_pause'
204
+ end
205
+
206
+ def self.generate_uuid
207
+ SecureRandom.hex.to_s
208
+ end
209
+
210
+ def self.hash_accessor(name, options = {})
211
+ options[:default] ||= nil
212
+ coerce = options[:coerce] ? ".#{options[:coerce]}" : ''
213
+ module_eval <<-EOT
214
+ def #{name}
215
+ value = (self['#{name}'] ? self['#{name}']#{coerce} : #{options[:default].inspect})
216
+ yield value if block_given?
217
+ value
218
+ end
219
+
220
+ def #{name}=(value)
221
+ self['#{name}'] = value
222
+ end
223
+
224
+ def #{name}?
225
+ !!self['#{name}']
226
+ end
227
+ EOT
228
+ end
229
+
230
+ # Proxy deprecated methods directly back to Resque itself.
231
+ class << self
232
+ [:redis, :encode, :decode].each do |method|
233
+ define_method(method) { |*args| Resque.send(method, *args) }
234
+ end
235
+ end
236
+
237
+ hash_accessor :uuid
238
+ hash_accessor :name
239
+ hash_accessor :status
240
+ hash_accessor :message
241
+ hash_accessor :time
242
+ hash_accessor :options
243
+
244
+ hash_accessor :num
245
+ hash_accessor :total
246
+
247
+ # Create a new Resque::Plugins::State::Hash object. If multiple arguments are passed
248
+ # it is assumed the first argument is the UUID and the rest are status objects.
249
+ # All arguments are subsequentily merged in order. Strings are assumed to
250
+ # be messages.
251
+ def initialize(*args)
252
+ super nil
253
+ base_status = {
254
+ 'time' => Time.now.to_i,
255
+ 'status' => Resque::Plugins::State::STATUS_QUEUED
256
+ }
257
+ base_status['uuid'] = args.shift if args.length > 1
258
+ status_hash = args.inject(base_status) do |final, m|
259
+ m = { 'message' => m } if m.is_a?(String)
260
+ final.merge(m || {})
261
+ end
262
+ replace(status_hash)
263
+ end
264
+
265
+ # calculate the % completion of the job based on <tt>status</tt>, <tt>num</tt>
266
+ # and <tt>total</tt>
267
+ def pct_complete
268
+ if completed?
269
+ 100
270
+ elsif queued?
271
+ 0
272
+ elsif failed?
273
+ 0
274
+ else
275
+ if total.nil?
276
+ t = 1
277
+ else t = total
278
+ end
279
+ (((num || 0).to_f / t.to_f) * 100).to_i
280
+ end
281
+ end
282
+
283
+ # Return the time of the status initialization. If set returns a <tt>Time</tt>
284
+ # object, otherwise returns nil
285
+ def time
286
+ time? ? Time.at(self['time']) : nil
287
+ end
288
+
289
+ Resque::Plugins::State::STATUSES.each do |status|
290
+ define_method("#{status}?") do
291
+ self['status'] === status
292
+ end
293
+ end
294
+
295
+ # Can the job be killed? failed, completed, and killed jobs can't be
296
+ # killed, for obvious reasons
297
+ def killable?
298
+ !failed? && !completed? && !killed?
299
+ end
300
+
301
+ # Can the job be paused? failed, completed, paused, and killed jobs can't be
302
+ # paused, for obvious reasons
303
+ def pausable?
304
+ !failed? && !completed? && !killed? && !paused?
305
+ end
306
+
307
+ unless method_defined?(:to_json)
308
+ def to_json(*_args)
309
+ json
310
+ end
311
+ end
312
+
313
+ # Return a JSON representation of the current object.
314
+ def json
315
+ h = dup
316
+ h['pct_complete'] = pct_complete
317
+ self.class.encode(h)
318
+ end
319
+
320
+ def inspect
321
+ "#<Resque::Plugins::State::Hash #{super}>"
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
@@ -0,0 +1,289 @@
1
+ module Resque
2
+ module Plugins
3
+ # Resque::Plugins::State is a module your jobs will include.
4
+ # It provides helper methods for updating the status/etc from within an
5
+ # instance as well as class methods for creating and queuing the jobs.
6
+ #
7
+ # All you have to do to get this functionality is include
8
+ # Resque::Plugins::State and then implement a <tt>perform<tt> method.
9
+ #
10
+ # For example
11
+ #
12
+ # class ExampleJob
13
+ # include Resque::Plugins::State
14
+ #
15
+ # def perform
16
+ # num = options['num']
17
+ # i = 0
18
+ # while i < num
19
+ # i += 1
20
+ # at(i, num)
21
+ # end
22
+ # completed("Finished!")
23
+ # end
24
+ #
25
+ # end
26
+ #
27
+ # This job would iterate num times updating the status as it goes. At the
28
+ # end we update the status telling anyone listening to this job that its
29
+ # complete.
30
+ module State
31
+ VERSION = '1.0.0'.freeze
32
+
33
+ STATUS_QUEUED = 'queued'.freeze
34
+ STATUS_WORKING = 'working'.freeze
35
+ STATUS_COMPLETED = 'completed'.freeze
36
+ STATUS_FAILED = 'failed'.freeze
37
+ STATUS_KILLED = 'killed'.freeze
38
+ STATUS_PAUSED = 'paused'.freeze
39
+ STATUSES = [
40
+ STATUS_QUEUED,
41
+ STATUS_WORKING,
42
+ STATUS_COMPLETED,
43
+ STATUS_FAILED,
44
+ STATUS_KILLED,
45
+ STATUS_PAUSED
46
+ ].freeze
47
+
48
+ autoload :Hash, 'resque/plugins/state/hash'
49
+
50
+ # The error class raised when a job is killed
51
+ class Killed < RuntimeError; end
52
+ class NotANumber < RuntimeError; end
53
+
54
+ attr_reader :uuid, :options
55
+
56
+ def self.included(base)
57
+ base.extend(ClassMethods)
58
+ end
59
+
60
+ module ClassMethods
61
+ # The default queue is :statused, this can be ovveridden in the specific
62
+ # job class to put the jobs on a specific worker queue
63
+ def queue
64
+ :statused
65
+ end
66
+
67
+ # used when displaying the Job in the resque-web UI and identifiyng the
68
+ # job type by status. By default this is the name of the job class, but
69
+ # can be overidden in the specific job class to present a more user
70
+ # friendly job name
71
+ def name
72
+ to_s
73
+ end
74
+
75
+ # Create is the primary method for adding jobs to the queue. This would
76
+ # be called on the job class to create a job of that type. Any options
77
+ # passed are passed to the Job instance as a hash of options. It returns
78
+ # the UUID of the job.
79
+ #
80
+ # == Example:
81
+ #
82
+ # class ExampleJob
83
+ # include Resque::Plugins::State
84
+ #
85
+ # def perform
86
+ # job_status "Hey I'm a job num #{options['num']}"
87
+ # end
88
+ #
89
+ # end
90
+ #
91
+ # job_id = ExampleJob.create(:num => 100)
92
+ #
93
+ def create(options = {})
94
+ enqueue(self, options)
95
+ end
96
+
97
+ # Adds a job of type <tt>klass<tt> to the queue with <tt>options<tt>.
98
+ #
99
+ # Returns the UUID of the job if the job was queued, or nil if the job
100
+ # was rejected by a before_enqueue hook.
101
+ def enqueue(klass, options = {})
102
+ enqueue_to(Resque.queue_from_class(klass) || queue, klass, options)
103
+ end
104
+
105
+ # Adds a job of type <tt>klass<tt> to a specified queue with
106
+ # <tt>options<tt>.
107
+ #
108
+ # Returns the UUID of the job if the job was queued, or nil if the job
109
+ # was rejected by a before_enqueue hook.
110
+ def enqueue_to(queue, klass, options = {})
111
+ uuid = Resque::Plugins::State::Hash.generate_uuid
112
+ Resque::Plugins::State::Hash.create uuid, options: options
113
+
114
+ if Resque.enqueue_to(queue, klass, uuid, options)
115
+ uuid
116
+ else
117
+ Resque::Plugins::State::Hash.remove(uuid)
118
+ nil
119
+ end
120
+ end
121
+
122
+ # Removes a job of type <tt>klass<tt> from the queue.
123
+ #
124
+ # The initially given options are retrieved from the status hash.
125
+ # (Resque needs the options to find the correct queue entry)
126
+ def dequeue(klass, uuid)
127
+ status = Resque::Plugins::State::Hash.get(uuid)
128
+ Resque.dequeue(klass, uuid, status.options)
129
+ end
130
+
131
+ # This is the method called by Resque::Worker when processing jobs. It
132
+ # creates a new instance of the job class and populates it with the uuid
133
+ # and options.
134
+ #
135
+ # You should not override this method, rahter the <tt>perform</tt>
136
+ # instance method.
137
+ def perform(uuid = nil, options = {})
138
+ uuid ||= Resque::Plugins::State::Hash.generate_uuid
139
+ instance = new(uuid, options)
140
+ instance.safe_perform!
141
+ instance
142
+ end
143
+
144
+ # Wrapper API to forward a Resque::Job creation API call into a
145
+ # Resque::Plugins::State call.
146
+ # This is needed to be used with resque scheduler
147
+ # http://github.com/bvandenbos/resque-scheduler
148
+ def scheduled(queue, _klass, *args)
149
+ enqueue_to(queue, self, *args)
150
+ end
151
+ end
152
+
153
+ # Create a new instance with <tt>uuid</tt> and <tt>options</tt>
154
+ def initialize(uuid, options = {})
155
+ @uuid = uuid
156
+ @options = options
157
+ @logger = Resque.logger
158
+ end
159
+
160
+ # Run by the Resque::Worker when processing this job. It wraps the
161
+ # <tt>perform</tt> method ensuring that the final status of the job is set
162
+ # regardless of error. If an error occurs within the job's work, it will
163
+ # set the status as failed and re-raise the error.
164
+ def safe_perform!
165
+ job_status('status' => STATUS_WORKING)
166
+ messages = ['Job starting']
167
+ @logger.info("#{@uuid}: #{messages.join(' ')}")
168
+ perform
169
+ if status && status.failed?
170
+ on_failure(status.message) if respond_to?(:on_failure)
171
+ return
172
+ elsif status && !status.completed?
173
+ completed
174
+ end
175
+ on_success if respond_to?(:on_success)
176
+ rescue Killed
177
+ Resque::Plugins::State::Hash.killed(uuid)
178
+ on_killed if respond_to?(:on_killed)
179
+ rescue => e
180
+ failed("The task failed because of an error: #{e}")
181
+ raise e unless respond_to?(:on_failure)
182
+ on_failure(e)
183
+ end
184
+
185
+ # Set the jobs status. Can take an array of strings or hashes that are
186
+ # merged (in order) into a final status hash.
187
+ def status=(new_status)
188
+ Resque::Plugins::State::Hash.set(uuid, *new_status)
189
+ end
190
+
191
+ # get the Resque::Plugins::State::Hash object for the current uuid
192
+ def status
193
+ Resque::Plugins::State::Hash.get(uuid)
194
+ end
195
+
196
+ def name
197
+ "#{self.class.name}(#{options.inspect unless options.empty?})"
198
+ end
199
+
200
+ # Checks against the kill list if this specific job instance should be
201
+ # killed on the next iteration
202
+ def should_kill?
203
+ Resque::Plugins::State::Hash.should_kill?(uuid)
204
+ end
205
+
206
+ # Checks against the pause list if this specific job instance should be
207
+ # paused on the next iteration
208
+ def should_pause?
209
+ Resque::Plugins::State::Hash.should_pause?(uuid)
210
+ end
211
+
212
+ # set the status of the job for the current itteration. <tt>num</tt> and
213
+ # <tt>total</tt> are passed to the status as well as any messages.
214
+ # This will kill the job if it has been added to the kill list with
215
+ # <tt>Resque::Plugins::State::Hash.kill()</tt>
216
+ def at(num, total, *messages)
217
+ if total.to_f <= 0.0
218
+ raise(NotANumber,
219
+ "Called at() with total=#{total} which is not a number")
220
+ end
221
+ tick({
222
+ 'num' => num,
223
+ 'total' => total
224
+ }, *messages)
225
+ end
226
+
227
+ # sets the status of the job for the current itteration. You should use
228
+ # the <tt>at</tt> method if you have actual numbers to track the iteration
229
+ # count. This will kill or pause the job if it has been added to either
230
+ # list with <tt>Resque::Plugins::State::Hash.pause()</tt> or
231
+ # <tt>Resque::Plugins::State::Hash.kill()</tt> respectively
232
+ def tick(*messages)
233
+ kill! if should_kill?
234
+ if should_pause?
235
+ pause!
236
+ else
237
+ job_status({ 'status' => STATUS_WORKING }, *messages)
238
+ @logger.info("#{@uuid}: #{messages.join(' ')}")
239
+ end
240
+ end
241
+
242
+ # set the status to 'failed' passing along any additional messages
243
+ def failed(*messages)
244
+ job_status({ 'status' => STATUS_FAILED }, *messages)
245
+ @logger.error("#{@uuid}: #{messages.join(' ')}")
246
+ end
247
+
248
+ # set the status to 'completed' passing along any addional messages
249
+ def completed(*messages)
250
+ job_status({
251
+ 'status' => STATUS_COMPLETED,
252
+ 'message' => "Completed at #{Time.now}"
253
+ }, *messages)
254
+ @logger.info("#{@uuid}: #{messages.join(' ')}")
255
+ end
256
+
257
+ # kill the current job, setting the status to 'killed' and raising
258
+ # <tt>Killed</tt>
259
+ def kill!
260
+ messages = ["Killed at #{Time.now}"]
261
+ job_status('status' => STATUS_KILLED,
262
+ 'message' => messages[0])
263
+ @logger.error("#{@uuid}: #{messages.join(' ')}")
264
+ raise Killed
265
+ end
266
+
267
+ # pause the current job, setting the status to 'paused' and sleeping 10
268
+ # seconds
269
+ def pause!
270
+ Resque::Plugins::State::Hash.pause(uuid)
271
+ messages = ["Paused at #{Time.now}"]
272
+ job_status('status' => STATUS_PAUSED,
273
+ 'message' => messages[0])
274
+ raise Killed if @testing # Don't loop or complete during testing
275
+ @logger.info("#{@uuid}: #{messages.join(' ')}")
276
+ while should_pause?
277
+ kill! if should_kill?
278
+ sleep 10
279
+ end
280
+ end
281
+
282
+ private
283
+
284
+ def job_status(*args)
285
+ self.status = [status, { 'name' => name }, args].flatten
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,91 @@
1
+ <%= status_view :state_styles, :layout => false %>
2
+
3
+ <h1 class='wi'>State <%= @status.uuid %></h1>
4
+ <p class='intro'>Viewing a specific job created with Resque::Plugins::State. <a href="<%= u(:statuses) %>">Return to the list of statuses</a></p>
5
+
6
+ <div class="status-holder" rel="<%= @status.status %>" id="status_<%= @status.uuid %>">
7
+ <h2>Overview</h2>
8
+ <div class="status-progress">
9
+ <div class="status-progress-bar status-<%= @status.status %>" style="width: <%= @status.pct_complete %>%;"></div>
10
+ <p><%= @status.pct_complete %>%</p>
11
+ </div>
12
+ <div class="status-message"><%= @status.message %></div>
13
+ <div class="status-time"><%= @status.time? ? @status.time : 'Not started' %></div>
14
+
15
+ <h2>Details</h2>
16
+ <div class="status-details">
17
+ <table class="vertically-top">
18
+ <thead>
19
+ <tr>
20
+ <th>Key</th>
21
+ <th>Value</th>
22
+ </tr>
23
+ </thead>
24
+ <tbody class="status-details-body">
25
+ </tbody>
26
+ </table>
27
+ </div>
28
+ </div>
29
+
30
+ <script type="text/javascript" charset="utf-8">
31
+ jQuery(function($) {
32
+
33
+ // itterate over the holders
34
+ $('.status-holder').each(function() {
35
+ checkStatus($(this));
36
+ });
37
+
38
+ function checkStatus($status) {
39
+ var status_id = $status.attr('id').replace('status_', '');
40
+ $.getJSON('<%= u(:statuses) %>/' + status_id + '.js', function(json) {
41
+ if (json) {
42
+ var pct = "0%";
43
+ if (json.pct_complete) {
44
+ var pct = json.pct_complete + "%";
45
+ }
46
+ $status.find('.status-progress-bar').animate({width: pct});
47
+ $status.find('.status-progress p').text(pct)
48
+ if (json.message) {
49
+ $status.find('.status-message').html(json.message)
50
+ }
51
+ if (json.status) {
52
+ $status
53
+ .attr('rel', json.status)
54
+ .find('.status-progress-bar')
55
+ .attr('class', '')
56
+ .addClass('status-progress-bar status-' + json.status);
57
+ }
58
+ if (json.time) {
59
+ $status.find('.status-time').text(new Date(json.time * 1000).toString())
60
+ }
61
+
62
+ var $details = $status.find('.status-details-body');
63
+ $details.empty();
64
+
65
+ for (key in json) {
66
+ var $row = $("<tr>").appendTo($details);
67
+ $("<td>").text(key).appendTo($row);
68
+ $("<td>").text(printValue(key, json[key])).appendTo($row);
69
+ }
70
+ };
71
+ var status = $status.attr('rel');
72
+ if (status == '<%= Resque::Plugins::State::STATUS_WORKING %>' || status == '<%= Resque::Plugins::State::STATUS_QUEUED %>' || status == "") {
73
+ setTimeout(function() {
74
+ checkStatus($status)
75
+ }, 1500);
76
+ }
77
+ });
78
+ };
79
+
80
+ function printValue(key, value) {
81
+ if (/(^|_)time$/.test(key) && typeof value == 'number') {
82
+ var time = new Date();
83
+ time.setTime(value * 1000);
84
+ return time.toUTCString();
85
+ } else {
86
+ return JSON.stringify(value, null, " ");
87
+ }
88
+ }
89
+
90
+ });
91
+ </script>