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.
- checksums.yaml +7 -0
- data/.travis.yml +4 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +102 -0
- data/LICENSE +21 -0
- data/README.rdoc +208 -0
- data/Rakefile +48 -0
- data/examples/sleep_job.rb +36 -0
- data/init.rb +1 -0
- data/lib/resque/job_with_state.rb +5 -0
- data/lib/resque/plugins/state/hash.rb +326 -0
- data/lib/resque/plugins/state.rb +289 -0
- data/lib/resque/server/views/state.erb +91 -0
- data/lib/resque/server/views/state_styles.erb +104 -0
- data/lib/resque/server/views/statuses.erb +79 -0
- data/lib/resque/state.rb +8 -0
- data/lib/resque/state_server.rb +85 -0
- data/lib/resque-state.rb +1 -0
- data/resque-state.gemspec +65 -0
- data/test/test_helper.rb +100 -0
- data/test/test_resque_plugins_state.rb +426 -0
- data/test/test_resque_plugins_state_hash.rb +255 -0
- metadata +99 -0
@@ -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>
|