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.
@@ -0,0 +1,8 @@
1
+ <%= erb File.read(ResqueCleaner::Server.erb_path("_cleaner_styles.erb")) %>
2
+
3
+ <div class="cleaner">
4
+ <p class="message">Processed <%= @count %> jobs.</p>
5
+ <p class="back_to_list">
6
+ <a href="<%=@list_url%>">Back to List</a>
7
+ </p>
8
+ </div>
@@ -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>&nbsp;</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
+