resque-cleanerer 0.5.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/CHANGELOG.md +92 -0
- data/LICENSE +20 -0
- data/README.md +333 -0
- data/Rakefile +38 -0
- data/lib/resque-cleaner.rb +1 -0
- data/lib/resque_cleaner/server/views/_cleaner_styles.erb +63 -0
- data/lib/resque_cleaner/server/views/_limiter.erb +13 -0
- data/lib/resque_cleaner/server/views/_paginate.erb +54 -0
- data/lib/resque_cleaner/server/views/_stats.erb +44 -0
- data/lib/resque_cleaner/server/views/cleaner.erb +25 -0
- data/lib/resque_cleaner/server/views/cleaner_exec.erb +8 -0
- data/lib/resque_cleaner/server/views/cleaner_list.erb +179 -0
- data/lib/resque_cleaner/server.rb +258 -0
- data/lib/resque_cleaner.rb +309 -0
- data/test/redis-test.conf +115 -0
- data/test/resque_cleaner_test.rb +206 -0
- data/test/resque_web_test.rb +66 -0
- data/test/test_helper.rb +131 -0
- metadata +106 -0
@@ -0,0 +1,179 @@
|
|
1
|
+
<%= erb File.read(ResqueCleaner::Server.erb_path("_cleaner_styles.erb")) %>
|
2
|
+
|
3
|
+
<!-- Many code was copied from failed.erb of the original resque -->
|
4
|
+
<div class="cleaner">
|
5
|
+
<div class="title clearfix">
|
6
|
+
<h1>Failed Jobs</h1>
|
7
|
+
</div>
|
8
|
+
|
9
|
+
<div class="clearfix">
|
10
|
+
<div class="control_panel sub_header">
|
11
|
+
<form method="get">
|
12
|
+
<span class="class_filter">
|
13
|
+
Class: <%= class_filter("filter_class","c",@klasses,@klass)%>
|
14
|
+
</span>
|
15
|
+
<span class="exception_filter">
|
16
|
+
Exception: <%= exception_filter("filter_class","ex",@exceptions,@exception)%>
|
17
|
+
</span>
|
18
|
+
<span class="time_filter">
|
19
|
+
From: <%= time_filter("filter_from","f",@from)%>
|
20
|
+
</span>
|
21
|
+
<span class="time_filter">
|
22
|
+
To: <%= time_filter("filter_to","t",@to)%>
|
23
|
+
</span>
|
24
|
+
<span class="regex_filter">
|
25
|
+
Regex: <%= text_filter("filter_regex", "regex", Rack::Utils.escape_html(@regex))%>
|
26
|
+
</span>
|
27
|
+
<input type="submit" value="Filter" />
|
28
|
+
</form>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<% if @count > 0 %>
|
33
|
+
<div class="clearfix">
|
34
|
+
<div class="control_panel sub_header">
|
35
|
+
<form method="post" id="exec" action="cleaner_exec">
|
36
|
+
<input type="hidden" name="c" value="<%=@klass%>" />
|
37
|
+
<input type="hidden" name="f" value="<%=@from%>" />
|
38
|
+
<input type="hidden" name="t" value="<%=@to%>" />
|
39
|
+
<input type="hidden" name="p" value="<%=@paginate.page%>" />
|
40
|
+
<input type="hidden" name="ex" value="<%=@exception%>" />
|
41
|
+
<input type="hidden" name="regex" value="<%=Rack::Utils.escape_html(@regex)%>" />
|
42
|
+
<select id="form_action" name="action">
|
43
|
+
<option id="default_option" value="" selected="selected">-- Select Action --</option>
|
44
|
+
<option value="clear">Clear</option>
|
45
|
+
<option value="retry_and_clear">Retry and Clear</option>
|
46
|
+
<option value="retry">Retry</option>
|
47
|
+
</select>
|
48
|
+
<a href="#" id="select_all">select all</a>
|
49
|
+
<a href="#" id="reset_all">reset</a>
|
50
|
+
|
51
|
+
<% if @paginate.max_page > 1 %>
|
52
|
+
<input type="checkbox" name="select_all_pages" value="1" id="select_all_pages" />
|
53
|
+
<label for="select_all_pages">Select all <%=@count%> jobs</label>
|
54
|
+
<% end %>
|
55
|
+
<input type="hidden" name="sha1" id="sha1_list" />
|
56
|
+
|
57
|
+
</form>
|
58
|
+
</div>
|
59
|
+
</div>
|
60
|
+
<% end %>
|
61
|
+
|
62
|
+
<% start = 0 %>
|
63
|
+
<% failed = @paginate.paginated_jobs%>
|
64
|
+
<% index = 0 %>
|
65
|
+
<% date_format = "%Y/%m/%d %T %z" %>
|
66
|
+
|
67
|
+
<% if @paginate.max_page > 0 %>
|
68
|
+
<%= erb File.read(ResqueCleaner::Server.erb_path("_paginate.erb")) %>
|
69
|
+
<ul class='failed'>
|
70
|
+
<%for job in failed%>
|
71
|
+
<% index += 1 %>
|
72
|
+
<li>
|
73
|
+
<dl>
|
74
|
+
<% if job.nil? %>
|
75
|
+
<dt>Error</dt>
|
76
|
+
<dd>Job <%= index%> could not be parsed; perhaps it contains invalid JSON?</dd>
|
77
|
+
<% else %>
|
78
|
+
<dt>
|
79
|
+
<input type="checkbox" id="<%=Digest::SHA1.hexdigest job.to_json %>" />
|
80
|
+
</dt>
|
81
|
+
<dd> </dd>
|
82
|
+
<dt>Worker</dt>
|
83
|
+
<dd>
|
84
|
+
<a href="<%= u(:workers, job['worker']) %>"><%= job['worker'].split(':')[0...2].join(':') %></a> on <b class='queue-tag'><%= job['queue'] %></b > at <b><span class="time"><%= Time.parse(job['failed_at']).strftime(date_format) %></span></b>
|
85
|
+
<% if job['retried_at'] %>
|
86
|
+
<div class='retried'>
|
87
|
+
Retried <b><span class="time"><%= job['retried_at'] %></span></b>
|
88
|
+
</div>
|
89
|
+
<% end %>
|
90
|
+
</dd>
|
91
|
+
<dt>Class</dt>
|
92
|
+
<dd><code><%= job.klass_name %></code></dd>
|
93
|
+
<dt>Arguments</dt>
|
94
|
+
<dd><pre><%=h job['payload'] ? show_job_args(job['payload']['args']) : 'nil' %></pre></dd>
|
95
|
+
<dt>Exception</dt>
|
96
|
+
<dd><code><%= job['exception'] %></code></dd>
|
97
|
+
<dt>Error</dt>
|
98
|
+
<dd class='error'>
|
99
|
+
<% if job['backtrace'] %>
|
100
|
+
<a href="#" class="backtrace"><%= h(job['error']) %></a>
|
101
|
+
<pre style='display:none'><%=h job['backtrace'].join("\n") %></pre>
|
102
|
+
<% else %>
|
103
|
+
<%=h job['error'] %>
|
104
|
+
<% end %>
|
105
|
+
</dd>
|
106
|
+
<% end %>
|
107
|
+
</dl>
|
108
|
+
<div class='r'>
|
109
|
+
</div>
|
110
|
+
</li>
|
111
|
+
<%end%>
|
112
|
+
</ul>
|
113
|
+
<%= erb File.read(ResqueCleaner::Server.erb_path("_paginate.erb")) %>
|
114
|
+
<% else %>
|
115
|
+
Clean!
|
116
|
+
<% end %>
|
117
|
+
</div>
|
118
|
+
|
119
|
+
<script>
|
120
|
+
$(document).ready(function(){
|
121
|
+
$('#select_all_pages').click(function() {
|
122
|
+
updateCheckboxStaus();
|
123
|
+
});
|
124
|
+
|
125
|
+
$('#select_all').click(function() {
|
126
|
+
if (!$(this).hasClass('disabled')) {
|
127
|
+
$('.failed input').attr('checked','checked');
|
128
|
+
}
|
129
|
+
return false;
|
130
|
+
});
|
131
|
+
$('#reset_all').click(function() {
|
132
|
+
if (!$(this).hasClass('disabled')) {
|
133
|
+
$('.failed input').removeAttr('checked');
|
134
|
+
}
|
135
|
+
return false;
|
136
|
+
});
|
137
|
+
|
138
|
+
$('#form_action').change( function() {
|
139
|
+
if ($('#form_action option:selected').val()=='') return;
|
140
|
+
|
141
|
+
if ($('#select_all_pages:checked, .failed input:checked').length==0) {
|
142
|
+
alert('Please select jobs.');
|
143
|
+
$('#default_option').attr('selected','selected');
|
144
|
+
return false;
|
145
|
+
}
|
146
|
+
|
147
|
+
if (!confirm('Do you really want to proceed?')) {
|
148
|
+
$('#default_option').attr('selected','selected');
|
149
|
+
return false;
|
150
|
+
}
|
151
|
+
|
152
|
+
if ($('#select_all_pages:checked').length==0) {
|
153
|
+
setSha1();
|
154
|
+
}
|
155
|
+
$('#exec').submit();
|
156
|
+
});
|
157
|
+
});
|
158
|
+
|
159
|
+
function setSha1() {
|
160
|
+
var sha1 = "";
|
161
|
+
$('.failed input:checked').each( function() {
|
162
|
+
if (sha1.length>0) sha1 += ",";
|
163
|
+
sha1 += $(this).attr("id");
|
164
|
+
});
|
165
|
+
|
166
|
+
$('#sha1_list').val(sha1);
|
167
|
+
}
|
168
|
+
|
169
|
+
function updateCheckboxStaus() {
|
170
|
+
if ($('#select_all_pages:checked').length==1) {
|
171
|
+
$('#exec a').addClass('disabled');
|
172
|
+
$('.failed input').attr('disabled','disabled');
|
173
|
+
} else {
|
174
|
+
$('#exec a').removeClass('disabled');
|
175
|
+
$('.failed input').removeAttr('disabled');
|
176
|
+
}
|
177
|
+
};
|
178
|
+
|
179
|
+
</script>
|
@@ -0,0 +1,258 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# Extends Resque Web Based UI.
|
4
|
+
# Structure has been borrowed from ResqueScheduler.
|
5
|
+
module ResqueCleaner
|
6
|
+
module Server
|
7
|
+
|
8
|
+
def self.erb_path(filename)
|
9
|
+
File.join(File.dirname(__FILE__), 'server', 'views', filename)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Pagination helper for list page.
|
13
|
+
class Paginate
|
14
|
+
attr_accessor :page_size, :page, :jobs, :url
|
15
|
+
def initialize(jobs, url, page=1, page_size=20)
|
16
|
+
@jobs = jobs
|
17
|
+
@url = url
|
18
|
+
@page = (!page || page < 1) ? 1 : page
|
19
|
+
@page_size = 20
|
20
|
+
end
|
21
|
+
|
22
|
+
def first_index
|
23
|
+
@page_size * (@page-1)
|
24
|
+
end
|
25
|
+
|
26
|
+
def last_index
|
27
|
+
last = first_index + @page_size - 1
|
28
|
+
last > @jobs.size-1 ? @jobs.size-1 : last
|
29
|
+
end
|
30
|
+
|
31
|
+
def paginated_jobs
|
32
|
+
@jobs[first_index,@page_size]
|
33
|
+
end
|
34
|
+
|
35
|
+
def first_page?
|
36
|
+
@page <= 1
|
37
|
+
end
|
38
|
+
|
39
|
+
def last_page?
|
40
|
+
@page >= max_page
|
41
|
+
end
|
42
|
+
|
43
|
+
def page_url(page)
|
44
|
+
u = @url
|
45
|
+
u += @url.include?("?") ? "&" : "?"
|
46
|
+
if page.is_a?(Symbol)
|
47
|
+
page = @page - 1 if page==:prev
|
48
|
+
page = @page + 1 if page==:next
|
49
|
+
end
|
50
|
+
u += "p=#{page}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def total_size
|
54
|
+
@jobs.size
|
55
|
+
end
|
56
|
+
|
57
|
+
def max_page
|
58
|
+
((total_size-1) / @page_size) + 1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.included(base)
|
63
|
+
|
64
|
+
base.class_eval do
|
65
|
+
helpers do
|
66
|
+
def time_filter(id, name, value)
|
67
|
+
html = "<select id=\"#{id}\" name=\"#{name}\">"
|
68
|
+
html += "<option value=\"\">-</option>"
|
69
|
+
[1, 3, 6, 12, 24].each do |h|
|
70
|
+
selected = h.to_s == value ? 'selected="selected"' : ''
|
71
|
+
html += "<option #{selected} value=\"#{h}\">#{h} #{h==1 ? "hour" : "hours"} ago</option>"
|
72
|
+
end
|
73
|
+
[3, 7, 14, 28].each do |d|
|
74
|
+
selected = (d*24).to_s == value ? 'selected="selected"' : ''
|
75
|
+
html += "<option #{selected} value=\"#{d*24}\">#{d} days ago</option>"
|
76
|
+
end
|
77
|
+
html += "</select>"
|
78
|
+
end
|
79
|
+
|
80
|
+
def class_filter(id, name, klasses, value)
|
81
|
+
html = "<select id=\"#{id}\" name=\"#{name}\">"
|
82
|
+
html += "<option value=\"\">-</option>"
|
83
|
+
klasses.each do |k|
|
84
|
+
selected = k == value ? 'selected="selected"' : ''
|
85
|
+
html += "<option #{selected} value=\"#{k}\">#{k}</option>"
|
86
|
+
end
|
87
|
+
html += "</select>"
|
88
|
+
end
|
89
|
+
|
90
|
+
def exception_filter(id, name, exceptions, value)
|
91
|
+
html = "<select id=\"#{id}\" name=\"#{name}\">"
|
92
|
+
html += "<option value=\"\">-</option>"
|
93
|
+
exceptions.each do |ex|
|
94
|
+
selected = ex == value ? 'selected="selected"' : ''
|
95
|
+
html += "<option #{selected} value=\"#{ex}\">#{ex}</option>"
|
96
|
+
end
|
97
|
+
html += "</select>"
|
98
|
+
end
|
99
|
+
|
100
|
+
def show_job_args(args)
|
101
|
+
Array(args).map { |a| a.inspect }.join("\n")
|
102
|
+
end
|
103
|
+
|
104
|
+
def text_filter(id, name, value)
|
105
|
+
html = "<input id=\"#{id}\" type=\"text\" name=\"#{name}\" value=\"#{value}\">"
|
106
|
+
html += "</input>"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
mime_type :json, 'application/json'
|
111
|
+
|
112
|
+
get "/cleaner" do
|
113
|
+
load_library
|
114
|
+
load_cleaner_filter
|
115
|
+
|
116
|
+
@jobs = cleaner.select
|
117
|
+
@stats = { :klass => {}, :exception => {} }
|
118
|
+
@total = Hash.new(0)
|
119
|
+
@jobs.each do |job|
|
120
|
+
exception = job["exception"] || 'UNKNOWN'
|
121
|
+
failed_at = Time.parse job["failed_at"]
|
122
|
+
@stats[:klass][job.klass_name] ||= Hash.new(0)
|
123
|
+
@stats[:exception][exception] ||= Hash.new(0)
|
124
|
+
|
125
|
+
[
|
126
|
+
@stats[:klass][job.klass_name],
|
127
|
+
@stats[:exception][exception],
|
128
|
+
@total
|
129
|
+
].each do |stat|
|
130
|
+
stat[:total] += 1
|
131
|
+
stat[:h1] += 1 if failed_at >= hours_ago(1)
|
132
|
+
stat[:h3] += 1 if failed_at >= hours_ago(3)
|
133
|
+
stat[:d1] += 1 if failed_at >= hours_ago(24)
|
134
|
+
stat[:d3] += 1 if failed_at >= hours_ago(24*3)
|
135
|
+
stat[:d7] += 1 if failed_at >= hours_ago(24*7)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
erb File.read(ResqueCleaner::Server.erb_path('cleaner.erb'))
|
140
|
+
end
|
141
|
+
|
142
|
+
get "/cleaner_list" do
|
143
|
+
load_library
|
144
|
+
load_cleaner_filter
|
145
|
+
build_urls
|
146
|
+
|
147
|
+
block = filter_block
|
148
|
+
|
149
|
+
@failed = cleaner.select(&block).reverse
|
150
|
+
|
151
|
+
@paginate = Paginate.new(@failed, @list_url, params[:p].to_i)
|
152
|
+
|
153
|
+
@klasses = cleaner.stats_by_class.keys
|
154
|
+
@exceptions = cleaner.stats_by_exception.keys
|
155
|
+
@count = cleaner.select(&block).size
|
156
|
+
|
157
|
+
erb File.read(ResqueCleaner::Server.erb_path('cleaner_list.erb'))
|
158
|
+
end
|
159
|
+
|
160
|
+
post "/cleaner_exec" do
|
161
|
+
load_library
|
162
|
+
load_cleaner_filter
|
163
|
+
build_urls
|
164
|
+
|
165
|
+
if params[:select_all_pages]!="1"
|
166
|
+
@sha1 = {}
|
167
|
+
params[:sha1].split(",").each {|s| @sha1[s] = true }
|
168
|
+
end
|
169
|
+
|
170
|
+
block = filter_block
|
171
|
+
|
172
|
+
@count =
|
173
|
+
case params[:action]
|
174
|
+
when "clear" then cleaner.clear(&block)
|
175
|
+
when "retry_and_clear" then cleaner.requeue(true,&block)
|
176
|
+
when "retry" then cleaner.requeue(false,{},&block)
|
177
|
+
end
|
178
|
+
|
179
|
+
erb File.read(ResqueCleaner::Server.erb_path('cleaner_exec.erb'))
|
180
|
+
end
|
181
|
+
|
182
|
+
get "/cleaner_dump" do
|
183
|
+
load_library
|
184
|
+
load_cleaner_filter
|
185
|
+
|
186
|
+
block = filter_block
|
187
|
+
|
188
|
+
content_type :json
|
189
|
+
JSON.pretty_generate(cleaner.select(&block))
|
190
|
+
end
|
191
|
+
|
192
|
+
post "/cleaner_stale" do
|
193
|
+
load_library
|
194
|
+
cleaner.clear_stale
|
195
|
+
redirect url_path(:cleaner)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
def cleaner
|
202
|
+
@cleaner ||= Resque::Plugins::ResqueCleaner.new
|
203
|
+
@cleaner.print_message = false
|
204
|
+
@cleaner
|
205
|
+
end
|
206
|
+
|
207
|
+
def load_library
|
208
|
+
require 'digest/sha1'
|
209
|
+
begin
|
210
|
+
require 'yajl/json_gem' unless [].respond_to?(:to_json)
|
211
|
+
rescue Exception
|
212
|
+
require 'json'
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def load_cleaner_filter
|
217
|
+
@from = params[:f]=="" ? nil : params[:f]
|
218
|
+
@to = params[:t]=="" ? nil : params[:t]
|
219
|
+
@klass = params[:c]=="" ? nil : params[:c]
|
220
|
+
@exception = params[:ex]=="" ? nil : params[:ex]
|
221
|
+
@regex = params[:regex]=="" ? nil : params[:regex]
|
222
|
+
end
|
223
|
+
|
224
|
+
def build_urls
|
225
|
+
params = {
|
226
|
+
c: @klass,
|
227
|
+
ex: @exception,
|
228
|
+
f: @from,
|
229
|
+
t: @to,
|
230
|
+
regex: @regex
|
231
|
+
}.map {|key,value| "#{key}=#{CGI.escape(value.to_s)}"}.join("&")
|
232
|
+
|
233
|
+
@list_url = "cleaner_list?#{params}"
|
234
|
+
@dump_url = "cleaner_dump?#{params}"
|
235
|
+
end
|
236
|
+
|
237
|
+
def filter_block
|
238
|
+
block = lambda{|j|
|
239
|
+
(!@from || j.after?(hours_ago(@from))) &&
|
240
|
+
(!@to || j.before?(hours_ago(@to))) &&
|
241
|
+
(!@klass || j.klass?(@klass)) &&
|
242
|
+
(!@exception || j.exception?(@exception)) &&
|
243
|
+
(!@sha1 || @sha1[Digest::SHA1.hexdigest(j.to_json)]) &&
|
244
|
+
(!@regex || j.to_s =~ /#{@regex}/)
|
245
|
+
}
|
246
|
+
end
|
247
|
+
|
248
|
+
def hours_ago(h)
|
249
|
+
Time.now - h.to_i*60*60
|
250
|
+
end
|
251
|
+
Resque::Server.tabs << 'Cleaner'
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
Resque::Server.class_eval do
|
256
|
+
include ResqueCleaner::Server
|
257
|
+
end
|
258
|
+
|