sidekiq-failures-discourse 0.3.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,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