powerhome-resque-status 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +85 -0
- data/LICENSE +20 -0
- data/README.rdoc +185 -0
- data/Rakefile +48 -0
- data/examples/sleep_job.rb +36 -0
- data/init.rb +1 -0
- data/lib/resque-status.rb +1 -0
- data/lib/resque/job_with_status.rb +5 -0
- data/lib/resque/plugins/status.rb +254 -0
- data/lib/resque/plugins/status/hash.rb +283 -0
- data/lib/resque/server/views/status.erb +91 -0
- data/lib/resque/server/views/status_styles.erb +104 -0
- data/lib/resque/server/views/statuses.erb +79 -0
- data/lib/resque/status.rb +8 -0
- data/lib/resque/status_server.rb +88 -0
- data/powerhome-resque-status.gemspec +62 -0
- data/resque-status.gemspec +64 -0
- data/test/redis-test.conf +125 -0
- data/test/test_helper.rb +98 -0
- data/test/test_resque_plugins_status.rb +358 -0
- data/test/test_resque_plugins_status_hash.rb +222 -0
- metadata +98 -0
@@ -0,0 +1,283 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module Plugins
|
5
|
+
module Status
|
6
|
+
|
7
|
+
# Resque::Plugins::Status::Hash is a Hash object that has helper methods for dealing with
|
8
|
+
# the common status attributes. It also has a number of class methods for
|
9
|
+
# creating/updating/retrieving status objects from Redis
|
10
|
+
class Hash < ::Hash
|
11
|
+
|
12
|
+
# Create a status, generating a new UUID, passing the message to the status
|
13
|
+
# Returns the UUID of the new status.
|
14
|
+
def self.create(uuid, *messages)
|
15
|
+
set(uuid, *messages)
|
16
|
+
redis.zadd(set_key, Time.now.to_i, uuid)
|
17
|
+
redis.zremrangebyscore(set_key, 0, Time.now.to_i - @expire_in) if @expire_in
|
18
|
+
uuid
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get a status by UUID. Returns a Resque::Plugins::Status::Hash
|
22
|
+
def self.get(uuid)
|
23
|
+
val = redis.get(status_key(uuid))
|
24
|
+
val ? Resque::Plugins::Status::Hash.new(uuid, decode(val)) : nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get multiple statuses by UUID. Returns array of Resque::Plugins::Status::Hash
|
28
|
+
def self.mget(uuids)
|
29
|
+
return [] if uuids.empty?
|
30
|
+
status_keys = uuids.map{|u| status_key(u)}
|
31
|
+
vals = redis.mget(*status_keys)
|
32
|
+
|
33
|
+
uuids.zip(vals).map do |uuid, val|
|
34
|
+
val ? Resque::Plugins::Status::Hash.new(uuid, decode(val)) : nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# set a status by UUID. <tt>messages</tt> can be any number of strings or hashes
|
39
|
+
# that are merged in order to create a single status.
|
40
|
+
def self.set(uuid, *messages)
|
41
|
+
val = Resque::Plugins::Status::Hash.new(uuid, *messages)
|
42
|
+
redis.set(status_key(uuid), encode(val))
|
43
|
+
if expire_in
|
44
|
+
redis.expire(status_key(uuid), expire_in)
|
45
|
+
end
|
46
|
+
val
|
47
|
+
end
|
48
|
+
|
49
|
+
# clear statuses from redis passing an optional range. See `statuses` for info
|
50
|
+
# about ranges
|
51
|
+
def self.clear(range_start = nil, range_end = nil)
|
52
|
+
status_ids(range_start, range_end).each do |id|
|
53
|
+
remove(id)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.clear_completed(range_start = nil, range_end = nil)
|
58
|
+
status_ids(range_start, range_end).select do |id|
|
59
|
+
if get(id).completed?
|
60
|
+
remove(id)
|
61
|
+
true
|
62
|
+
else
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.clear_failed(range_start = nil, range_end = nil)
|
69
|
+
status_ids(range_start, range_end).select do |id|
|
70
|
+
if get(id).failed?
|
71
|
+
remove(id)
|
72
|
+
true
|
73
|
+
else
|
74
|
+
false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.remove(uuid)
|
80
|
+
redis.del(status_key(uuid))
|
81
|
+
redis.zrem(set_key, uuid)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.count
|
85
|
+
redis.zcard(set_key)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return <tt>num</tt> Resque::Plugins::Status::Hash objects in reverse chronological order.
|
89
|
+
# By default returns the entire set.
|
90
|
+
# @param [Numeric] range_start The optional starting range
|
91
|
+
# @param [Numeric] range_end The optional ending range
|
92
|
+
# @example retuning the last 20 statuses
|
93
|
+
# Resque::Plugins::Status::Hash.statuses(0, 20)
|
94
|
+
def self.statuses(range_start = nil, range_end = nil)
|
95
|
+
ids = status_ids(range_start, range_end)
|
96
|
+
mget(ids).compact || []
|
97
|
+
end
|
98
|
+
|
99
|
+
# Return the <tt>num</tt> most recent status/job UUIDs in reverse chronological order.
|
100
|
+
def self.status_ids(range_start = nil, range_end = nil)
|
101
|
+
if range_end && range_start
|
102
|
+
# Because we want a reverse chronological order, we need to get a range starting
|
103
|
+
# by the higest negative number. The ordering is transparent from the API user's
|
104
|
+
# perspective so we need to convert the passed params
|
105
|
+
(redis.zrevrange(set_key, (range_start.abs), ((range_end || 1).abs)) || [])
|
106
|
+
else
|
107
|
+
# Because we want a reverse chronological order, we need to get a range starting
|
108
|
+
# by the higest negative number.
|
109
|
+
redis.zrevrange(set_key, 0, -1) || []
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Kill the job at UUID on its next iteration this works by adding the UUID to a
|
114
|
+
# kill list (a.k.a. a list of jobs to be killed. Each iteration the job checks
|
115
|
+
# if it _should_ be killed by calling <tt>tick</tt> or <tt>at</tt>. If so, it raises
|
116
|
+
# a <tt>Resque::Plugins::Status::Killed</tt> error and sets the status to 'killed'.
|
117
|
+
def self.kill(uuid)
|
118
|
+
redis.sadd(kill_key, uuid)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Remove the job at UUID from the kill list
|
122
|
+
def self.killed(uuid)
|
123
|
+
redis.srem(kill_key, uuid)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Return the UUIDs of the jobs on the kill list
|
127
|
+
def self.kill_ids
|
128
|
+
redis.smembers(kill_key)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Kills <tt>num</tt> jobs within range starting with the most recent first.
|
132
|
+
# By default kills all jobs.
|
133
|
+
# Note that the same conditions apply as <tt>kill</tt>, i.e. only jobs that check
|
134
|
+
# on each iteration by calling <tt>tick</tt> or <tt>at</tt> are eligible to killed.
|
135
|
+
# @param [Numeric] range_start The optional starting range
|
136
|
+
# @param [Numeric] range_end The optional ending range
|
137
|
+
# @example killing the last 20 submitted jobs
|
138
|
+
# Resque::Plugins::Status::Hash.killall(0, 20)
|
139
|
+
def self.killall(range_start = nil, range_end = nil)
|
140
|
+
status_ids(range_start, range_end).collect do |id|
|
141
|
+
kill(id)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Check whether a job with UUID is on the kill list
|
146
|
+
def self.should_kill?(uuid)
|
147
|
+
redis.sismember(kill_key, uuid)
|
148
|
+
end
|
149
|
+
|
150
|
+
# The time in seconds that jobs and statuses should expire from Redis (after
|
151
|
+
# the last time they are touched/updated)
|
152
|
+
def self.expire_in
|
153
|
+
@expire_in
|
154
|
+
end
|
155
|
+
|
156
|
+
# Set the <tt>expire_in</tt> time in seconds
|
157
|
+
def self.expire_in=(seconds)
|
158
|
+
@expire_in = seconds.nil? ? nil : seconds.to_i
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.status_key(uuid)
|
162
|
+
"status:#{uuid}"
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.set_key
|
166
|
+
"_statuses"
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.kill_key
|
170
|
+
"_kill"
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.generate_uuid
|
174
|
+
SecureRandom.hex.to_s
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.hash_accessor(name, options = {})
|
178
|
+
options[:default] ||= nil
|
179
|
+
coerce = options[:coerce] ? ".#{options[:coerce]}" : ""
|
180
|
+
module_eval <<-EOT
|
181
|
+
def #{name}
|
182
|
+
value = (self['#{name}'] ? self['#{name}']#{coerce} : #{options[:default].inspect})
|
183
|
+
yield value if block_given?
|
184
|
+
value
|
185
|
+
end
|
186
|
+
|
187
|
+
def #{name}=(value)
|
188
|
+
self['#{name}'] = value
|
189
|
+
end
|
190
|
+
|
191
|
+
def #{name}?
|
192
|
+
!!self['#{name}']
|
193
|
+
end
|
194
|
+
EOT
|
195
|
+
end
|
196
|
+
|
197
|
+
# Proxy deprecated methods directly back to Resque itself.
|
198
|
+
class << self
|
199
|
+
[:redis, :encode, :decode].each do |method|
|
200
|
+
define_method(method) { |*args| Resque.send(method, *args) }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
hash_accessor :uuid
|
205
|
+
hash_accessor :name
|
206
|
+
hash_accessor :status
|
207
|
+
hash_accessor :message
|
208
|
+
hash_accessor :time
|
209
|
+
hash_accessor :options
|
210
|
+
|
211
|
+
hash_accessor :num
|
212
|
+
hash_accessor :total
|
213
|
+
|
214
|
+
# Create a new Resque::Plugins::Status::Hash object. If multiple arguments are passed
|
215
|
+
# it is assumed the first argument is the UUID and the rest are status objects.
|
216
|
+
# All arguments are subsequentily merged in order. Strings are assumed to
|
217
|
+
# be messages.
|
218
|
+
def initialize(*args)
|
219
|
+
super nil
|
220
|
+
base_status = {
|
221
|
+
'time' => Time.now.to_i,
|
222
|
+
'status' => Resque::Plugins::Status::STATUS_QUEUED
|
223
|
+
}
|
224
|
+
base_status['uuid'] = args.shift if args.length > 1
|
225
|
+
status_hash = args.inject(base_status) do |final, m|
|
226
|
+
m = {'message' => m} if m.is_a?(String)
|
227
|
+
final.merge(m || {})
|
228
|
+
end
|
229
|
+
self.replace(status_hash)
|
230
|
+
end
|
231
|
+
|
232
|
+
# calculate the % completion of the job based on <tt>status</tt>, <tt>num</tt>
|
233
|
+
# and <tt>total</tt>
|
234
|
+
def pct_complete
|
235
|
+
if completed?
|
236
|
+
100
|
237
|
+
elsif queued?
|
238
|
+
0
|
239
|
+
else
|
240
|
+
t = (total == 0 || total.nil?) ? 1 : total
|
241
|
+
(((num || 0).to_f / t.to_f) * 100).to_i
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Return the time of the status initialization. If set returns a <tt>Time</tt>
|
246
|
+
# object, otherwise returns nil
|
247
|
+
def time
|
248
|
+
time? ? Time.at(self['time']) : nil
|
249
|
+
end
|
250
|
+
|
251
|
+
Resque::Plugins::Status::STATUSES.each do |status|
|
252
|
+
define_method("#{status}?") do
|
253
|
+
self['status'] === status
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# Can the job be killed? failed, completed, and killed jobs can't be
|
258
|
+
# killed, for obvious reasons
|
259
|
+
def killable?
|
260
|
+
!failed? && !completed? && !killed?
|
261
|
+
end
|
262
|
+
|
263
|
+
unless method_defined?(:to_json)
|
264
|
+
def to_json(*args)
|
265
|
+
json
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Return a JSON representation of the current object.
|
270
|
+
def json
|
271
|
+
h = self.dup
|
272
|
+
h['pct_complete'] = pct_complete
|
273
|
+
self.class.encode(h)
|
274
|
+
end
|
275
|
+
|
276
|
+
def inspect
|
277
|
+
"#<Resque::Plugins::Status::Hash #{super}>"
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
<%= status_view :status_styles, :layout => false %>
|
2
|
+
|
3
|
+
<h1 class='wi'>Status <%= @status.uuid %></h1>
|
4
|
+
<p class='intro'>Viewing a specific job created with Resque::Plugins::Status. <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::Status::STATUS_WORKING %>' || status == '<%= Resque::Plugins::Status::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>
|
@@ -0,0 +1,104 @@
|
|
1
|
+
<style type="text/css" media="screen">
|
2
|
+
th.progress {
|
3
|
+
width: 100px;
|
4
|
+
}
|
5
|
+
td.progress {
|
6
|
+
position: relative;
|
7
|
+
width: 100px;
|
8
|
+
display:block;
|
9
|
+
}
|
10
|
+
td.status {
|
11
|
+
font-weight: bold;
|
12
|
+
}
|
13
|
+
td.progress .progress-bar {
|
14
|
+
position: absolute;
|
15
|
+
top: 0px;
|
16
|
+
left: 0px;
|
17
|
+
background: #999;
|
18
|
+
display:block;
|
19
|
+
height: 100%;
|
20
|
+
z-index: 0;
|
21
|
+
opacity: 0.5;
|
22
|
+
-moz-opacity: 0.5;
|
23
|
+
-webkit-opacity: 0.5;
|
24
|
+
}
|
25
|
+
td.progress .progress-pct {
|
26
|
+
z-index: 10;
|
27
|
+
color: #333;
|
28
|
+
}
|
29
|
+
table.vertically-top td {
|
30
|
+
vertical-align: text-top;
|
31
|
+
}
|
32
|
+
table.vertically-top tr {
|
33
|
+
border: 1px solid #ccc;
|
34
|
+
}
|
35
|
+
|
36
|
+
.status-holder {
|
37
|
+
background: #F7F7F7;
|
38
|
+
border: 1px solid #E5E5E5;
|
39
|
+
padding: 20px;
|
40
|
+
font-size: 110%;
|
41
|
+
margin-bottom: 40px;
|
42
|
+
}
|
43
|
+
.status-progress {
|
44
|
+
width: 100%;
|
45
|
+
height: 30px;
|
46
|
+
border: 1px solid #CCC;
|
47
|
+
background: #E5E5E5;
|
48
|
+
position:relative;
|
49
|
+
margin: 5px 0px;
|
50
|
+
}
|
51
|
+
.status-progress-bar {
|
52
|
+
position:absolute;
|
53
|
+
top: 0px;
|
54
|
+
left: 0px;
|
55
|
+
height: 30px;
|
56
|
+
background: #CCC;
|
57
|
+
}
|
58
|
+
.status-progress p {
|
59
|
+
position:absolute;
|
60
|
+
top: 5px;
|
61
|
+
left: 10px;
|
62
|
+
z-index: 15;
|
63
|
+
display:block;
|
64
|
+
color: #FFF;
|
65
|
+
padding: 0px;
|
66
|
+
font-weight: bold;
|
67
|
+
}
|
68
|
+
.status-message {
|
69
|
+
font-weight: bold;
|
70
|
+
}
|
71
|
+
.status-time {
|
72
|
+
font-size: 70%;
|
73
|
+
padding: 10px 0px;
|
74
|
+
color: #999;
|
75
|
+
}
|
76
|
+
.status-progress-bar.status-completed {
|
77
|
+
background:#61BF55;
|
78
|
+
}
|
79
|
+
.status-progress-bar.status-failed {
|
80
|
+
background: #E47E74;
|
81
|
+
}
|
82
|
+
.status-progress-bar.status-working {
|
83
|
+
background: #528499;
|
84
|
+
}
|
85
|
+
.status-progress-bar.status-killed {
|
86
|
+
background: #B84F16;
|
87
|
+
}
|
88
|
+
.status-completed {
|
89
|
+
color:#61BF55;
|
90
|
+
}
|
91
|
+
.status-failed {
|
92
|
+
color: #E47E74;
|
93
|
+
}
|
94
|
+
.status-working {
|
95
|
+
color: #528499;
|
96
|
+
}
|
97
|
+
.status-killed {
|
98
|
+
color: #B84F16;
|
99
|
+
}
|
100
|
+
#main a.kill:link, #main a.kill:visited {
|
101
|
+
color: #B84F16;
|
102
|
+
font-weight: bold;
|
103
|
+
}
|
104
|
+
</style>
|