sidekiq-failures-discourse 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ module Sidekiq
2
+ class SortedEntry
3
+ def retry_failure
4
+ Sidekiq.redis do |conn|
5
+ results = conn.zrangebyscore(Sidekiq::Failures::LIST_KEY, score, score)
6
+ conn.zremrangebyscore(Sidekiq::Failures::LIST_KEY, score, score)
7
+ results.map do |message|
8
+ msg = Sidekiq.load_json(message)
9
+ Sidekiq::Client.push(msg)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module Sidekiq
2
+ module Failures
3
+ VERSION = "0.3.0"
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ <%= erb :_job_info, :locals => {:job => @failure, :type => :failure} %>
2
+
3
+ <h3><%= t('Error') %></h3>
4
+ <table class="error table table-bordered table-striped">
5
+ <tbody>
6
+ <tr>
7
+ <th><%= t('ErrorClass') %></th>
8
+ <td>
9
+ <code><%= @failure['error_class'] %></code>
10
+ </td>
11
+ </tr>
12
+ <tr>
13
+ <th><%= t('ErrorMessage') %></th>
14
+ <td><%= @failure['error_message'] %></td>
15
+ </tr>
16
+ <% if !@failure['error_backtrace'].nil? %>
17
+ <tr>
18
+ <th><%= t('ErrorBacktrace') %></th>
19
+ <td>
20
+ <code><%= @failure['error_backtrace'].join("<br/>") %></code>
21
+ </td>
22
+ </tr>
23
+ <% end %>
24
+ </tbody>
25
+ </table>
26
+ <form class="form-horizontal" action="<%= root_path %>failures/<%= job_params(@failure, @failure.score) %>" method="post">
27
+ <a class="btn" href="<%= root_path %>failures"><%= t('GoBack') %></a>
28
+ <input class="btn btn-primary" type="submit" name="retry" value="<%= t('RetryNow') %>" />
29
+ <input class="btn btn-danger" type="submit" name="delete" value="<%= t('Delete') %>" />
30
+ </form>
@@ -0,0 +1,75 @@
1
+ <header class="row">
2
+ <div class="col-sm-5">
3
+ <h3><%= t('FailedJobs') %></h3>
4
+ </div>
5
+ <% if @failures.size > 0 && @total_size > @count %>
6
+ <div class="col-sm-4">
7
+ <%= erb :_paging, :locals => { :url => "#{root_path}failures" } %>
8
+ </div>
9
+ <% end %>
10
+ <%= filtering('failures') %>
11
+ </header>
12
+
13
+ <% if @failures.size > 0 %>
14
+ <form action="<%= root_path %>failures" method="post">
15
+ <table class="table table-striped table-bordered table-white">
16
+ <thead>
17
+ <tr>
18
+ <th width="20px" class="table-checkbox">
19
+ <label>
20
+ <input type="checkbox" class="check_all" />
21
+ </label>
22
+ </th>
23
+ <th style="width: 20%"><%= t('Worker') %></th>
24
+ <th style="width: 10%"><%= t('Arguments') %></th>
25
+ <th style="width: 10%"><%= t('Queue') %></th>
26
+ <th style="width: 10%"><%= t('FailedAt') %></th>
27
+ <th style="width: 50%"><%= t('Error') %></th>
28
+ </tr>
29
+ </thead>
30
+ <% @failures.each do |entry| %>
31
+ <tr>
32
+ <td class="table-checkbox">
33
+ <label>
34
+ <input type='checkbox' name='key[]' value='<%= job_params(entry.item, entry.score) %>' />
35
+ </label>
36
+ </td>
37
+ <td><a href="<%= root_path %>failures/<%= job_params(entry.item, entry.score) %>"><%= entry.klass %></a></td>
38
+ <td>
39
+ <div class="args"><%= display_args(entry.args) %></div>
40
+ </td>
41
+ <td>
42
+ <a href="<%= root_path %>queues/<%= entry.queue %>"><%= entry.queue %></a>
43
+ </td>
44
+ <td><%= relative_time(Time.at(entry['failed_at'])) %></td>
45
+ <td style="overflow: auto; padding: 10px;">
46
+ <a class="backtrace" href="#" onclick="$(this).next().toggle(); return false">
47
+ <%= h entry['error_class'] %>: <%= h entry['error_message'] %>
48
+ </a>
49
+ <pre style="display: none; background: none; border: 0; width: 100%; max-height: 30em; font-size: 0.8em; white-space: nowrap;">
50
+ <%= entry['error_backtrace'].join("<br />") if entry['error_backtrace'] %>
51
+ </pre>
52
+ <p>
53
+ <span>Processor: <%= entry['processor'] %></span>
54
+ </p>
55
+ </td>
56
+ </tr>
57
+ <% end %>
58
+ </table>
59
+ <input class="btn btn-primary btn-xs pull-left" type="submit" name="retry" value="<%= t('RetryNow') %>" />
60
+ <input class="btn btn-danger btn-xs pull-left" type="submit" name="delete" value="<%= t('Delete') %>" />
61
+ </form>
62
+
63
+ <form action="<%= root_path %>failures/all/reset" method="post">
64
+ <input class="btn btn-danger btn-xs pull-right" type="submit" name="reset" value="<%= t('Reset Counter') %>" data-confirm="<%= t('AreYouSure') %>" />
65
+ </form>
66
+ <form action="<%= root_path %>failures/all/delete" method="post">
67
+ <input class="btn btn-danger btn-xs pull-right" type="submit" name="delete" value="<%= t('DeleteAll') %>" data-confirm="<%= t('AreYouSure') %>" />
68
+ </form>
69
+ <form action="<%= root_path %>failures/all/retry" method="post">
70
+ <input class="btn btn-danger btn-xs pull-right" type="submit" name="retry" value="<%= t('RetryAll') %>" data-confirm="<%= t('AreYouSure') %>" />
71
+ </form>
72
+
73
+ <% else %>
74
+ <div class="alert alert-success"><%= t('NoFailedJobsFound') %></div>
75
+ <% end %>
@@ -0,0 +1,72 @@
1
+ module Sidekiq
2
+ module Failures
3
+ module WebExtension
4
+
5
+ def self.registered(app)
6
+ view_path = File.join(File.expand_path("..", __FILE__), "views")
7
+
8
+ app.get "/failures" do
9
+ @count = (params[:count] || 25).to_i
10
+ (@current_page, @total_size, @failures) = page(LIST_KEY, params[:page], @count)
11
+ @failures = @failures.map {|msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
12
+
13
+ render(:erb, File.read(File.join(view_path, "failures.erb")))
14
+ end
15
+
16
+ app.get "/failures/:key" do
17
+ halt 404 unless params['key']
18
+
19
+ @failure = FailureSet.new.fetch(*parse_params(params['key'])).first
20
+ redirect "#{root_path}failures" if @failure.nil?
21
+ render(:erb, File.read(File.join(view_path, "failure.erb")))
22
+ end
23
+
24
+ app.post "/failures" do
25
+ halt 404 unless params['key']
26
+
27
+ params['key'].each do |key|
28
+ job = FailureSet.new.fetch(*parse_params(key)).first
29
+ next unless job
30
+
31
+ if params['retry']
32
+ job.retry_failure
33
+ elsif params['delete']
34
+ job.delete
35
+ end
36
+ end
37
+
38
+ redirect_with_query("#{root_path}failures")
39
+ end
40
+
41
+ app.post "/failures/:key" do
42
+ halt 404 unless params['key']
43
+
44
+ job = FailureSet.new.fetch(*parse_params(params['key'])).first
45
+ if job
46
+ if params['retry']
47
+ job.retry_failure
48
+ elsif params['delete']
49
+ job.delete
50
+ end
51
+ end
52
+ redirect_with_query("#{root_path}failures")
53
+ end
54
+
55
+ app.post "/failures/all/reset" do
56
+ Sidekiq::Failures.reset_failures
57
+ redirect "#{root_path}failures"
58
+ end
59
+
60
+ app.post "/failures/all/delete" do
61
+ FailureSet.new.clear
62
+ redirect "#{root_path}failures"
63
+ end
64
+
65
+ app.post "/failures/all/retry" do
66
+ FailureSet.new.retry_all_failures
67
+ redirect "#{root_path}failures"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/sidekiq/failures/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Marcelo Silveira"]
6
+ gem.email = ["marcelo@mhfs.com.br"]
7
+ gem.description = %q{Keep track of Sidekiq failed jobs}
8
+ gem.summary = %q{Keeps track of Sidekiq failed jobs and adds a tab to the Web UI to let you browse them. Makes use of Sidekiq's custom tabs and middleware chain.}
9
+ gem.homepage = "https://github.com/mhfs/sidekiq-failures/"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "sidekiq-failures-discourse"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Sidekiq::Failures::VERSION
17
+
18
+ gem.add_dependency "sidekiq", ">= 2.14.0"
19
+
20
+ gem.add_development_dependency "rake"
21
+ gem.add_development_dependency "rack-test"
22
+ gem.add_development_dependency "sprockets"
23
+ gem.add_development_dependency "sinatra"
24
+ end
@@ -0,0 +1,283 @@
1
+ require "test_helper"
2
+
3
+ module Sidekiq
4
+ module Failures
5
+ describe "Middleware" do
6
+ before do
7
+ Celluloid.boot
8
+ $invokes = 0
9
+ @boss = MiniTest::Mock.new
10
+ @processor = ::Sidekiq::Processor.new(@boss)
11
+ Sidekiq.server_middleware {|chain| chain.add Sidekiq::Failures::Middleware }
12
+ Sidekiq.redis = REDIS
13
+ Sidekiq.redis { |c| c.flushdb }
14
+ Sidekiq.instance_eval { @failures_default_mode = nil }
15
+ end
16
+
17
+ TestException = Class.new(Exception)
18
+ ShutdownException = Class.new(Sidekiq::Shutdown)
19
+
20
+ class MockWorker
21
+ include Sidekiq::Worker
22
+
23
+ def perform(args)
24
+ $invokes += 1
25
+ raise ShutdownException.new if args == "shutdown"
26
+ raise TestException.new("failed!")
27
+ end
28
+ end
29
+
30
+ it 'raises an error when failures_default_mode is configured incorrectly' do
31
+ assert_raises ArgumentError do
32
+ Sidekiq.failures_default_mode = 'exhaustion'
33
+ end
34
+ end
35
+
36
+ it 'defaults failures_default_mode to all' do
37
+ assert_equal :all, Sidekiq.failures_default_mode
38
+ end
39
+
40
+ it 'records all failures by default' do
41
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'])
42
+
43
+ assert_equal 0, failures_count
44
+
45
+ actor = MiniTest::Mock.new
46
+ actor.expect(:processor_done, nil, [@processor])
47
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
48
+ 2.times { @boss.expect(:async, actor, []) }
49
+
50
+ assert_raises TestException do
51
+ @processor.process(msg)
52
+ end
53
+
54
+ assert_equal 1, failures_count
55
+ assert_equal 1, $invokes
56
+ end
57
+
58
+ it 'records all failures if explicitly told to' do
59
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'failures' => true)
60
+
61
+ assert_equal 0, failures_count
62
+
63
+ actor = MiniTest::Mock.new
64
+ actor.expect(:processor_done, nil, [@processor])
65
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
66
+ 2.times { @boss.expect(:async, actor, []) }
67
+
68
+ assert_raises TestException do
69
+ @processor.process(msg)
70
+ end
71
+
72
+ assert_equal 1, failures_count
73
+ assert_equal 1, $invokes
74
+ end
75
+
76
+ it "doesn't record internal shutdown failure" do
77
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['shutdown'], 'failures' => true)
78
+
79
+ assert_equal 0, failures_count
80
+
81
+ actor = MiniTest::Mock.new
82
+ actor.expect(:processor_done, nil, [@processor])
83
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
84
+ 2.times { @boss.expect(:async, actor, []) }
85
+
86
+ @processor.process(msg)
87
+ @boss.verify
88
+
89
+ assert_equal 0, failures_count
90
+ assert_equal 1, $invokes
91
+ end
92
+
93
+ it "doesn't record failure if failures disabled" do
94
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'failures' => false)
95
+
96
+ assert_equal 0, failures_count
97
+
98
+ actor = MiniTest::Mock.new
99
+ actor.expect(:processor_done, nil, [@processor])
100
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
101
+ 2.times { @boss.expect(:async, actor, []) }
102
+
103
+ assert_raises TestException do
104
+ @processor.process(msg)
105
+ end
106
+
107
+ assert_equal 0, failures_count
108
+ assert_equal 1, $invokes
109
+ end
110
+
111
+ it "doesn't record failure if going to be retired again and configured to track exhaustion by default" do
112
+ Sidekiq.failures_default_mode = :exhausted
113
+
114
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry' => true )
115
+
116
+ assert_equal 0, failures_count
117
+
118
+ actor = MiniTest::Mock.new
119
+ actor.expect(:processor_done, nil, [@processor])
120
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
121
+ 2.times { @boss.expect(:async, actor, []) }
122
+
123
+ assert_raises TestException do
124
+ @processor.process(msg)
125
+ end
126
+
127
+ assert_equal 0, failures_count
128
+ assert_equal 1, $invokes
129
+ end
130
+
131
+
132
+ it "doesn't record failure if going to be retired again and configured to track exhaustion" do
133
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry' => true, 'failures' => 'exhausted')
134
+
135
+ assert_equal 0, failures_count
136
+
137
+ actor = MiniTest::Mock.new
138
+ actor.expect(:processor_done, nil, [@processor])
139
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
140
+ 2.times { @boss.expect(:async, actor, []) }
141
+
142
+ assert_raises TestException do
143
+ @processor.process(msg)
144
+ end
145
+
146
+ assert_equal 0, failures_count
147
+ assert_equal 1, $invokes
148
+ end
149
+
150
+ it "records failure if failing last retry and configured to track exhaustion" do
151
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry' => true, 'retry_count' => 25, 'failures' => 'exhausted')
152
+
153
+ assert_equal 0, failures_count
154
+
155
+ actor = MiniTest::Mock.new
156
+ actor.expect(:processor_done, nil, [@processor])
157
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
158
+ 2.times { @boss.expect(:async, actor, []) }
159
+
160
+ assert_raises TestException do
161
+ @processor.process(msg)
162
+ end
163
+
164
+ assert_equal 1, failures_count
165
+ assert_equal 1, $invokes
166
+ end
167
+
168
+ it "records failure if retry disabled and configured to track exhaustion" do
169
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry' => false, 'failures' => 'exhausted')
170
+
171
+ assert_equal 0, failures_count
172
+
173
+ actor = MiniTest::Mock.new
174
+ actor.expect(:processor_done, nil, [@processor])
175
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
176
+ 2.times { @boss.expect(:async, actor, []) }
177
+
178
+ assert_raises TestException do
179
+ @processor.process(msg)
180
+ end
181
+
182
+ assert_equal 1, failures_count
183
+ assert_equal 1, $invokes
184
+ end
185
+
186
+ it "records failure if retry disabled and configured to track exhaustion by default" do
187
+ Sidekiq.failures_default_mode = 'exhausted'
188
+
189
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry' => false)
190
+
191
+ assert_equal 0, failures_count
192
+
193
+ actor = MiniTest::Mock.new
194
+ actor.expect(:processor_done, nil, [@processor])
195
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
196
+ 2.times { @boss.expect(:async, actor, []) }
197
+
198
+ assert_raises TestException do
199
+ @processor.process(msg)
200
+ end
201
+
202
+ assert_equal 1, failures_count
203
+ assert_equal 1, $invokes
204
+ end
205
+
206
+ it "records failure if failing last retry and configured to track exhaustion by default" do
207
+ Sidekiq.failures_default_mode = 'exhausted'
208
+
209
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry' => true, 'retry_count' => 25)
210
+
211
+ assert_equal 0, failures_count
212
+
213
+ actor = MiniTest::Mock.new
214
+ actor.expect(:processor_done, nil, [@processor])
215
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
216
+ 2.times { @boss.expect(:async, actor, []) }
217
+
218
+ assert_raises TestException do
219
+ @processor.process(msg)
220
+ end
221
+
222
+ assert_equal 1, failures_count
223
+ assert_equal 1, $invokes
224
+ end
225
+
226
+ it "removes old failures when failures_max_count has been reached" do
227
+ assert_equal 1000, Sidekiq.failures_max_count
228
+ Sidekiq.failures_max_count = 2
229
+
230
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'])
231
+
232
+ assert_equal 0, failures_count
233
+
234
+ 3.times do
235
+ boss = MiniTest::Mock.new
236
+ processor = ::Sidekiq::Processor.new(boss)
237
+
238
+ actor = MiniTest::Mock.new
239
+ actor.expect(:processor_done, nil, [processor])
240
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
241
+ 2.times { boss.expect(:async, actor, []) }
242
+
243
+ assert_raises TestException do
244
+ processor.process(msg)
245
+ end
246
+ end
247
+
248
+ assert_equal 2, failures_count
249
+
250
+ Sidekiq.failures_max_count = false
251
+ assert Sidekiq.failures_max_count == false
252
+
253
+ Sidekiq.failures_max_count = nil
254
+ assert_equal 1000, Sidekiq.failures_max_count
255
+ end
256
+
257
+ it 'returns the total number of failed jobs in the queue' do
258
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'failures' => true)
259
+
260
+ assert_equal 0, Sidekiq::Failures.count
261
+
262
+ actor = MiniTest::Mock.new
263
+ actor.expect(:processor_done, nil, [@processor])
264
+ actor.expect(:real_thread, nil, [nil, Celluloid::Thread])
265
+ @boss.expect(:async, actor, [])
266
+
267
+ assert_raises TestException do
268
+ @processor.process(msg)
269
+ end
270
+
271
+ assert_equal 1, Sidekiq::Failures.count
272
+ end
273
+
274
+ def failures_count
275
+ Sidekiq.redis { |conn| conn.zcard(LIST_KEY) }
276
+ end
277
+
278
+ def create_work(msg)
279
+ Sidekiq::BasicFetch::UnitOfWork.new('default', Sidekiq.dump_json(msg))
280
+ end
281
+ end
282
+ end
283
+ end