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 +5 -0
- data/.gitignore +23 -0
- data/LICENSE +20 -0
- data/README.rdoc +145 -0
- data/Rakefile +60 -0
- data/examples/sleep_job.rb +35 -0
- data/init.rb +1 -0
- data/lib/resque/job_with_status.rb +187 -0
- data/lib/resque/server/views/status.erb +57 -0
- data/lib/resque/server/views/status_styles.erb +98 -0
- data/lib/resque/server/views/statuses.erb +60 -0
- data/lib/resque/status.rb +206 -0
- data/lib/resque/status_server.rb +44 -0
- data/resque-status.gemspec +74 -0
- data/test/redis-test.conf +132 -0
- data/test/test_helper.rb +82 -0
- data/test/test_resque-job_with_status.rb +180 -0
- data/test/test_resque-status.rb +107 -0
- metadata +115 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
<%= status_view :status_styles, :layout => false %>
|
2
|
+
|
3
|
+
<h1 class='wi'>Statuses: <%= @status.uuid %>/<%= @status.name %></h1>
|
4
|
+
<p class='intro'>Viewing a specific job created with JobWithStatus. <a href="/statuses">Return to the list of statuses</a></p>
|
5
|
+
|
6
|
+
<div class="status-holder" rel="<%= @status.status %>" id="status_<%= @status.uuid %>">
|
7
|
+
<div class="status-progress">
|
8
|
+
<div class="status-progress-bar status-<%= @status.status %>" style="width: <%= @status.pct_complete %>%;"></div>
|
9
|
+
<p><%= @status.pct_complete %>%</p>
|
10
|
+
</div>
|
11
|
+
<div class="status-message"><%= @status.message %></div>
|
12
|
+
<div class="status-time"><%= @status.time? ? @status.time : 'Not started' %></div>
|
13
|
+
</div>
|
14
|
+
|
15
|
+
<script type="text/javascript" charset="utf-8">
|
16
|
+
jQuery(function($) {
|
17
|
+
|
18
|
+
// itterate over the holders
|
19
|
+
$('.status-holder').each(function() {
|
20
|
+
checkStatus($(this));
|
21
|
+
});
|
22
|
+
|
23
|
+
function checkStatus($status) {
|
24
|
+
var status_id = $status.attr('id').replace('status_', '');
|
25
|
+
$.getJSON('/statuses/' + status_id + '.js', function(json) {
|
26
|
+
if (json) {
|
27
|
+
var pct = "0%";
|
28
|
+
if (json.pct_complete) {
|
29
|
+
var pct = json.pct_complete + "%";
|
30
|
+
}
|
31
|
+
$status.find('.status-progress-bar').animate({width: pct});
|
32
|
+
$status.find('.status-progress p').text(pct)
|
33
|
+
if (json.message) {
|
34
|
+
$status.find('.status-message').html(json.message)
|
35
|
+
}
|
36
|
+
if (json.status) {
|
37
|
+
$status
|
38
|
+
.attr('rel', json.status)
|
39
|
+
.find('.status-progress-bar')
|
40
|
+
.attr('class', '')
|
41
|
+
.addClass('status-progress-bar status-' + json.status);
|
42
|
+
}
|
43
|
+
if (json.time) {
|
44
|
+
$status.find('.status-time').text(new Date(json.time * 1000).toString())
|
45
|
+
}
|
46
|
+
};
|
47
|
+
var status = $status.attr('rel');
|
48
|
+
if (status == 'working' || status == 'queued' || status == "") {
|
49
|
+
setTimeout(function() {
|
50
|
+
checkStatus($status)
|
51
|
+
}, 1500);
|
52
|
+
}
|
53
|
+
});
|
54
|
+
};
|
55
|
+
|
56
|
+
});
|
57
|
+
</script>
|
@@ -0,0 +1,98 @@
|
|
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
|
+
|
30
|
+
.status-holder {
|
31
|
+
background: #F7F7F7;
|
32
|
+
border: 1px solid #E5E5E5;
|
33
|
+
padding: 20px;
|
34
|
+
font-size: 110%;
|
35
|
+
margin-bottom: 40px;
|
36
|
+
}
|
37
|
+
.status-progress {
|
38
|
+
width: 100%;
|
39
|
+
height: 30px;
|
40
|
+
border: 1px solid #CCC;
|
41
|
+
background: #E5E5E5;
|
42
|
+
position:relative;
|
43
|
+
margin: 5px 0px;
|
44
|
+
}
|
45
|
+
.status-progress-bar {
|
46
|
+
position:absolute;
|
47
|
+
top: 0px;
|
48
|
+
left: 0px;
|
49
|
+
height: 30px;
|
50
|
+
background: #CCC;
|
51
|
+
}
|
52
|
+
.status-progress p {
|
53
|
+
position:absolute;
|
54
|
+
top: 5px;
|
55
|
+
left: 10px;
|
56
|
+
z-index: 15;
|
57
|
+
display:block;
|
58
|
+
color: #FFF;
|
59
|
+
padding: 0px;
|
60
|
+
font-weight: bold;
|
61
|
+
}
|
62
|
+
.status-message {
|
63
|
+
font-weight: bold;
|
64
|
+
}
|
65
|
+
.status-time {
|
66
|
+
font-size: 70%;
|
67
|
+
padding: 10px 0px;
|
68
|
+
color: #999;
|
69
|
+
}
|
70
|
+
.status-progress-bar.status-completed {
|
71
|
+
background:#61BF55;
|
72
|
+
}
|
73
|
+
.status-progress-bar.status-failed {
|
74
|
+
background: #E47E74;
|
75
|
+
}
|
76
|
+
.status-progress-bar.status-working {
|
77
|
+
background: #528499;
|
78
|
+
}
|
79
|
+
.status-progress-bar.status-killed {
|
80
|
+
background: #B84F16;
|
81
|
+
}
|
82
|
+
.status-completed {
|
83
|
+
color:#61BF55;
|
84
|
+
}
|
85
|
+
.status-failed {
|
86
|
+
color: #E47E74;
|
87
|
+
}
|
88
|
+
.status-working {
|
89
|
+
color: #528499;
|
90
|
+
}
|
91
|
+
.status-killed {
|
92
|
+
color: #B84F16;
|
93
|
+
}
|
94
|
+
#main a.kill:link, #main a.kill:visited {
|
95
|
+
color: #B84F16;
|
96
|
+
font-weight: bold;
|
97
|
+
}
|
98
|
+
</style>
|
@@ -0,0 +1,60 @@
|
|
1
|
+
<%= status_view :status_styles, :layout => false %>
|
2
|
+
|
3
|
+
<h1 class='wi'>Statuses</h1>
|
4
|
+
<p class='intro'>These are recent jobs created with the JobWithStatus class</p>
|
5
|
+
<table>
|
6
|
+
<tr>
|
7
|
+
<th>ID</th>
|
8
|
+
<th>Name</th>
|
9
|
+
<th>Status</th>
|
10
|
+
<th>Last Updated</th>
|
11
|
+
<th class="progress">% Complete</th>
|
12
|
+
<th>Message</th>
|
13
|
+
<th>Kill</th>
|
14
|
+
</tr>
|
15
|
+
<% unless @statuses.empty? %>
|
16
|
+
<% @statuses.each do |status| %>
|
17
|
+
<tr>
|
18
|
+
<td><a href="/statuses/<%= status.uuid %>"><%= status.uuid %></a></td>
|
19
|
+
<td><%= status.name %></td>
|
20
|
+
<td class="status status-<%= status.status %>"><%= status.status %></td>
|
21
|
+
<td class="time"><%= status.time %></td>
|
22
|
+
<td class="progress">
|
23
|
+
<div class="progress-bar" style="width:<%= status.pct_complete %>%"> </div>
|
24
|
+
<div class="progress-pct"><%= status.pct_complete ? "#{status.pct_complete}%" : '' %></div>
|
25
|
+
</td>
|
26
|
+
<td><%= status.message %></td>
|
27
|
+
<td><% if status.killable? %><a href="/statuses/<%= status.uuid %>/kill" class="kill">Kill</a><% end %></td>
|
28
|
+
</tr>
|
29
|
+
<% end %>
|
30
|
+
<% else %>
|
31
|
+
<tr>
|
32
|
+
<td colspan="7" class='no-data'>No Statuses right now...</td>
|
33
|
+
</tr>
|
34
|
+
<% end %>
|
35
|
+
</table>
|
36
|
+
|
37
|
+
<script type="text/javascript" charset="utf-8">
|
38
|
+
jQuery(function($) {
|
39
|
+
|
40
|
+
$('a.kill').click(function(e) {
|
41
|
+
e.preventDefault();
|
42
|
+
var $link = $(this),
|
43
|
+
url = $link.attr('href'),
|
44
|
+
confirmed = confirm("Are you sure you want to kill this job? There is no undo.");
|
45
|
+
if (confirmed) {
|
46
|
+
$link.animate({opacity: 0.5});
|
47
|
+
$.ajax({
|
48
|
+
url: url,
|
49
|
+
type: 'post',
|
50
|
+
success: function() {
|
51
|
+
$link.remove();
|
52
|
+
}
|
53
|
+
});
|
54
|
+
} else {
|
55
|
+
return false
|
56
|
+
}
|
57
|
+
});
|
58
|
+
|
59
|
+
});
|
60
|
+
</script>
|
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'resque'
|
2
|
+
require 'redisk'
|
3
|
+
require 'uuid'
|
4
|
+
|
5
|
+
module Resque
|
6
|
+
# Resque::Status 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 Status < Hash
|
10
|
+
VERSION = '0.1.0'
|
11
|
+
|
12
|
+
extend Resque::Helpers
|
13
|
+
|
14
|
+
# Create a status, generating a new UUID, passing the message to the status
|
15
|
+
# Returns the UUID of the new status.
|
16
|
+
def self.create(message = nil)
|
17
|
+
uuid = generate_uuid
|
18
|
+
set(uuid, message)
|
19
|
+
redis.zadd(set_key, Time.now.to_i, uuid)
|
20
|
+
redis.zremrangebyscore(set_key, 0, Time.now.to_i - @expire_in) if @expire_in
|
21
|
+
uuid
|
22
|
+
end
|
23
|
+
|
24
|
+
# Get a status by UUID. Returns a Resque::Status
|
25
|
+
def self.get(uuid)
|
26
|
+
val = redis.get(status_key(uuid))
|
27
|
+
val ? Resque::Status.new(uuid, decode(val)) : nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# set a status by UUID. <tt>messages</tt> can be any number of stirngs or hashes
|
31
|
+
# that are merged in order to create a single status.
|
32
|
+
def self.set(uuid, *messages)
|
33
|
+
val = Resque::Status.new(uuid, *messages)
|
34
|
+
redis.set(status_key(uuid), encode(val))
|
35
|
+
if expire_in
|
36
|
+
redis.expire(status_key(uuid), expire_in)
|
37
|
+
end
|
38
|
+
val
|
39
|
+
end
|
40
|
+
|
41
|
+
# returns a Redisk::Logger scoped to the UUID. Any options passed are passed
|
42
|
+
# to the logger initialization.
|
43
|
+
def self.logger(uuid, options = {})
|
44
|
+
Redisk::Logger.new(logger_key(uuid), options)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return <tt>num</tt> Resque::Status objects in reverse chronological order.
|
48
|
+
# By default returns the entire set.
|
49
|
+
def self.statuses(num = -1)
|
50
|
+
status_ids(num).collect do |id|
|
51
|
+
get(id)
|
52
|
+
end.compact
|
53
|
+
end
|
54
|
+
|
55
|
+
# Return the <tt>num</tt> most recent status/job UUIDs in reverse chronological order.
|
56
|
+
def self.status_ids(num = -1)
|
57
|
+
redis.zrevrange set_key, 0, num
|
58
|
+
end
|
59
|
+
|
60
|
+
# Kill the job at UUID on its next iteration this works by adding the UUID to a
|
61
|
+
# kill list (a.k.a. a list of jobs to be killed. Each iteration the job checks
|
62
|
+
# if it _should_ be killed by calling <tt>tick</tt> or <tt>at</tt>. If so, it raises
|
63
|
+
# a <tt>Resque::JobWithStatus::Killed</tt> error and sets the status to 'killed'.
|
64
|
+
def self.kill(uuid)
|
65
|
+
redis.sadd(kill_key, uuid)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Remove the job at UUID from the kill list
|
69
|
+
def self.killed(uuid)
|
70
|
+
redis.srem(kill_key, uuid)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return the UUIDs of the jobs on the kill list
|
74
|
+
def self.kill_ids
|
75
|
+
redis.smembers(kill_key)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Check whether a job with UUID is on the kill list
|
79
|
+
def self.should_kill?(uuid)
|
80
|
+
redis.sismember(kill_key, uuid)
|
81
|
+
end
|
82
|
+
|
83
|
+
# The time in seconds that jobs and statuses should expire from Redis (after
|
84
|
+
# the last time they are touched/updated)
|
85
|
+
def self.expire_in
|
86
|
+
@expire_in
|
87
|
+
end
|
88
|
+
|
89
|
+
# Set the <tt>expire_in</tt> time in seconds
|
90
|
+
def self.expire_in=(seconds)
|
91
|
+
@expire_in = seconds.nil? ? nil : seconds.to_i
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.status_key(uuid)
|
95
|
+
"status:#{uuid}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.set_key
|
99
|
+
"_statuses"
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.kill_key
|
103
|
+
"_kill"
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.logger_key(uuid)
|
107
|
+
"_log:#{uuid}"
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.generate_uuid
|
111
|
+
UUID.generate(:compact)
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.hash_accessor(name, options = {})
|
115
|
+
options[:default] ||= nil
|
116
|
+
coerce = options[:coerce] ? ".#{options[:coerce]}" : ""
|
117
|
+
module_eval <<-EOT
|
118
|
+
def #{name}
|
119
|
+
value = (self['#{name}'] ? self['#{name}']#{coerce} : #{options[:default].inspect})
|
120
|
+
yield value if block_given?
|
121
|
+
value
|
122
|
+
end
|
123
|
+
|
124
|
+
def #{name}=(value)
|
125
|
+
self['#{name}'] = value
|
126
|
+
end
|
127
|
+
|
128
|
+
def #{name}?
|
129
|
+
!!self['#{name}']
|
130
|
+
end
|
131
|
+
EOT
|
132
|
+
end
|
133
|
+
|
134
|
+
STATUSES = %w{queued working completed failed killed}.freeze
|
135
|
+
|
136
|
+
hash_accessor :uuid
|
137
|
+
hash_accessor :name
|
138
|
+
hash_accessor :status
|
139
|
+
hash_accessor :message
|
140
|
+
hash_accessor :time
|
141
|
+
|
142
|
+
hash_accessor :num
|
143
|
+
hash_accessor :total
|
144
|
+
|
145
|
+
# Create a new Resque::Status object. If multiple arguments are passed
|
146
|
+
# it is assumed the first argument is the UUID and the rest are status objects.
|
147
|
+
# All arguments are subsequentily merged in order. Strings are assumed to
|
148
|
+
# be messages.
|
149
|
+
def initialize(*args)
|
150
|
+
super nil
|
151
|
+
base_status = {
|
152
|
+
'time' => Time.now.to_i,
|
153
|
+
'status' => 'queued'
|
154
|
+
}
|
155
|
+
base_status['uuid'] = args.shift if args.length > 1
|
156
|
+
status_hash = args.inject(base_status) do |final, m|
|
157
|
+
m = {'message' => m} if m.is_a?(String)
|
158
|
+
final.merge(m || {})
|
159
|
+
end
|
160
|
+
self.replace(status_hash)
|
161
|
+
end
|
162
|
+
|
163
|
+
# calculate the % completion of the job based on <tt>status</tt>, <tt>num</tt>
|
164
|
+
# and <tt>total</tt>
|
165
|
+
def pct_complete
|
166
|
+
case status
|
167
|
+
when 'completed' then 100
|
168
|
+
when 'queued' then 0
|
169
|
+
when 'failed' then 100
|
170
|
+
else
|
171
|
+
t = (total == 0 || total.nil?) ? 1 : total
|
172
|
+
(((num || 0).to_f / t.to_f) * 100).to_i
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Return the time of the status initialization. If set returns a <tt>Time</tt>
|
177
|
+
# object, otherwise returns nil
|
178
|
+
def time
|
179
|
+
time? ? Time.at(self['time']) : nil
|
180
|
+
end
|
181
|
+
|
182
|
+
STATUSES.each do |status|
|
183
|
+
define_method("#{status}?") do
|
184
|
+
self['status'] === status
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Can the job be killed? 'failed', 'completed', and 'killed' jobs cant be killed
|
189
|
+
# (for pretty obvious reasons)
|
190
|
+
def killable?
|
191
|
+
!['failed', 'completed', 'killed'].include?(self.status)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Return a JSON representation of the current object.
|
195
|
+
def to_json
|
196
|
+
h = self.dup
|
197
|
+
h['pct_complete'] = pct_complete
|
198
|
+
self.class.encode(h)
|
199
|
+
end
|
200
|
+
|
201
|
+
def inspect
|
202
|
+
"#<Resque::Status #{super}>"
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'resque/status'
|
2
|
+
|
3
|
+
module Resque
|
4
|
+
module StatusServer
|
5
|
+
|
6
|
+
VIEW_PATH = File.join(File.dirname(__FILE__), 'server', 'views')
|
7
|
+
|
8
|
+
def self.registered(app)
|
9
|
+
|
10
|
+
app.get '/statuses' do
|
11
|
+
@statuses = Resque::Status.statuses
|
12
|
+
status_view(:statuses)
|
13
|
+
end
|
14
|
+
|
15
|
+
app.get '/statuses/:id.js' do
|
16
|
+
@status = Resque::Status.get(params[:id])
|
17
|
+
content_type :js
|
18
|
+
@status.to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
app.get '/statuses/:id' do
|
22
|
+
@status = Resque::Status.get(params[:id])
|
23
|
+
status_view(:status)
|
24
|
+
end
|
25
|
+
|
26
|
+
app.post '/statuses/:id/kill' do
|
27
|
+
Resque::Status.kill(params[:id])
|
28
|
+
redirect '/statuses'
|
29
|
+
end
|
30
|
+
|
31
|
+
app.helpers do
|
32
|
+
def status_view(filename, options = {}, locals = {})
|
33
|
+
erb(File.read(File.join(VIEW_PATH, "#{filename}.erb")), options, locals)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
app.tabs << "Statuses"
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Resque::Server.register Resque::StatusServer
|