sidekiq-failures 1.0.3 → 1.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +3 -3
- data/CHANGELOG.md +13 -4
- data/README.md +9 -4
- data/lib/sidekiq/failures/sorted_entry.rb +8 -1
- data/lib/sidekiq/failures/version.rb +1 -1
- data/lib/sidekiq/failures/views/failure.erb +37 -29
- data/lib/sidekiq/failures/views/failure_legacy.erb +31 -0
- data/lib/sidekiq/failures/views/failures.erb +106 -98
- data/lib/sidekiq/failures/views/failures_legacy.erb +105 -0
- data/lib/sidekiq/failures/web_extension.rb +118 -31
- data/lib/sidekiq/failures.rb +20 -3
- data/test/failures_test.rb +1 -1
- data/test/middleware_test.rb +32 -21
- data/test/test_helper.rb +3 -1
- data/test/web_extension_test.rb +79 -33
- metadata +5 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba0a7d8ecb1ed1f847be0918723281d6c12d7f40a4780c089340cc8e687183f3
|
4
|
+
data.tar.gz: 10aca014769f8883c9de557bded7ac01287276924ad314ec1006d6d0e1c332bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1bc85386b596a67b27e00b6c2efed3377f8377bb89dd4eb128336b0f1bf4150acd361c3be1f1ed8dce233710031f3bd9b17d09d4c85b7ee7673da67ba7649d89
|
7
|
+
data.tar.gz: 236381eb70f14564b3dd43fc715ad8ac3f5578973b4c20fb95f8f6cfcedadb82db922c1cb8eafed13e6c5189d5218ec0c959dbe559592413b727da2ef18fed25
|
data/.github/workflows/ci.yml
CHANGED
@@ -12,8 +12,8 @@ jobs:
|
|
12
12
|
strategy:
|
13
13
|
fail-fast: false
|
14
14
|
matrix:
|
15
|
-
ruby: [2.6, 2.7, '3.0', 3.1]
|
16
|
-
sidekiq: [4.2, 5.2, 6.2]
|
15
|
+
ruby: [2.6, 2.7, '3.0', 3.1, 3.4.3]
|
16
|
+
sidekiq: [4.2, 5.2, 6.2, 8.0.3]
|
17
17
|
services:
|
18
18
|
redis:
|
19
19
|
image: redis
|
@@ -27,7 +27,7 @@ jobs:
|
|
27
27
|
env:
|
28
28
|
SIDEKIQ_VERSION: ~> ${{ matrix.sidekiq }}
|
29
29
|
steps:
|
30
|
-
- uses: actions/checkout@
|
30
|
+
- uses: actions/checkout@v4
|
31
31
|
- name: Set up Ruby
|
32
32
|
uses: ruby/setup-ruby@v1
|
33
33
|
with:
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
## Unreleased
|
2
2
|
|
3
|
+
## 1.1.0
|
4
|
+
|
5
|
+
* Add support for Sidekiq 8.x (#145 @navidemad)
|
6
|
+
* Fix nil exception in safe_relative_time (#152 @leedrum)
|
7
|
+
|
8
|
+
## 1.0.4
|
9
|
+
|
10
|
+
* Sanitize failure text on the failure details page (#144 @mcasper)
|
11
|
+
|
3
12
|
## 1.0.3
|
4
13
|
|
5
14
|
* Expand failure descriptions with pure JS (#141 @icyleaf)
|
@@ -57,8 +66,8 @@
|
|
57
66
|
## 0.3.0
|
58
67
|
|
59
68
|
* Bump sidekiq dependency to sidekiq >= 2.14.0
|
60
|
-
* Remove slim templates and
|
61
|
-
* Escape exception info when
|
69
|
+
* Remove slim templates and dependency
|
70
|
+
* Escape exception info when outputting to html
|
62
71
|
* Add `Sidekiq::Failures.reset_failures` helper method
|
63
72
|
* Add `Sidekiq::Failures.count` helper method (@zanker)
|
64
73
|
* Adhere to sidekiq approach of showing UTC times
|
@@ -81,8 +90,8 @@
|
|
81
90
|
* Added processor identity to failure data (@krasnoukhov)
|
82
91
|
* Handle Sidekiq::Shutdown exceptions (@krasnoukhov)
|
83
92
|
* Add failures maximum count option (@mathieulaporte)
|
84
|
-
* User
|
85
|
-
* Removed web
|
93
|
+
* User Exception#message instead of Exception#to_s (@supaspoida)
|
94
|
+
* Removed web dependencies (@LongMan)
|
86
95
|
* Stop overloading find_template (@zquestz)
|
87
96
|
|
88
97
|
## 0.1.0
|
data/README.md
CHANGED
@@ -127,13 +127,18 @@ influenced by `failures_max_count`.
|
|
127
127
|
Sidekiq::Failures.count
|
128
128
|
```
|
129
129
|
|
130
|
-
### Reset Failures
|
130
|
+
### Reset and clear Failures
|
131
131
|
|
132
|
-
Gives a convenient way of
|
133
|
-
Takes an options hash and if the `counter` key is present also resets Sidekiq own failed stats.
|
132
|
+
Gives a convenient way of resetting Sidekiq Failure stored failed jobs programmatically.
|
134
133
|
|
135
134
|
```ruby
|
136
|
-
Sidekiq::Failures.
|
135
|
+
Sidekiq::Failures.clear_failures
|
136
|
+
```
|
137
|
+
|
138
|
+
To reset Sidekiq own failed *stats*.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
Sidekiq::Failures.reset_failure_count
|
137
142
|
```
|
138
143
|
|
139
144
|
## Dependencies
|
@@ -11,7 +11,14 @@ module Sidekiq
|
|
11
11
|
|
12
12
|
def retry_failure
|
13
13
|
Sidekiq.redis do |conn|
|
14
|
-
|
14
|
+
# from Redis v6.2.0, zrangebyscore is deprecated and zrange with BYSCORE is used
|
15
|
+
# option byscore is available from redis-rb v4.6.0
|
16
|
+
results = if Gem::Version.new(conn.info["redis_version"].to_s) >= Gem::Version.new('6.2.0') &&
|
17
|
+
(Gem.loaded_specs['redis'].nil? || Gem.loaded_specs['redis'].version >= Gem::Version.new('4.6.0'))
|
18
|
+
conn.zrange(Sidekiq::Failures::LIST_KEY, score.to_i, score.to_i, byscore: true)
|
19
|
+
else
|
20
|
+
conn.zrangebyscore(Sidekiq::Failures::LIST_KEY, score, score)
|
21
|
+
end
|
15
22
|
conn.zremrangebyscore(Sidekiq::Failures::LIST_KEY, score, score)
|
16
23
|
results.map do |message|
|
17
24
|
msg = Sidekiq.load_json(message)
|
@@ -1,31 +1,39 @@
|
|
1
1
|
<%= erb :_job_info, :locals => {:job => @failure, :type => :failure} %>
|
2
2
|
|
3
|
-
<
|
4
|
-
<
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
</
|
3
|
+
<section>
|
4
|
+
<header>
|
5
|
+
<h2><%= t('Error') %></h2>
|
6
|
+
</header>
|
7
|
+
|
8
|
+
<div class="table_container">
|
9
|
+
<table>
|
10
|
+
<tbody>
|
11
|
+
<tr>
|
12
|
+
<th><%= t('ErrorClass') %></th>
|
13
|
+
<td>
|
14
|
+
<code><%= h @failure['error_class'] %></code>
|
15
|
+
</td>
|
16
|
+
</tr>
|
17
|
+
<tr>
|
18
|
+
<th><%= t('ErrorMessage') %></th>
|
19
|
+
<td><%= h @failure['error_message'] %></td>
|
20
|
+
</tr>
|
21
|
+
<% if !@failure['error_backtrace'].nil? %>
|
22
|
+
<tr>
|
23
|
+
<th><%= t('ErrorBacktrace') %></th>
|
24
|
+
<td>
|
25
|
+
<code><%= @failure['error_backtrace'].join("<br/>") %></code>
|
26
|
+
</td>
|
27
|
+
</tr>
|
28
|
+
<% end %>
|
29
|
+
</tbody>
|
30
|
+
</table>
|
31
|
+
</div>
|
32
|
+
|
33
|
+
<form action="<%= root_path %>failures/<%= job_params(@failure, @failure.score) %>" method="post">
|
34
|
+
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
35
|
+
<a class="btn btn-default" href="<%= root_path %>failures"><%= t('GoBack') %></a>
|
36
|
+
<input class="btn btn-primary" type="submit" name="retry" value="<%= t('RetryNow') %>" />
|
37
|
+
<input class="btn btn-danger" type="submit" name="delete" value="<%= t('Delete') %>" />
|
38
|
+
</form>
|
39
|
+
</section>
|
@@ -0,0 +1,31 @@
|
|
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><%= h @failure['error_class'] %></code>
|
10
|
+
</td>
|
11
|
+
</tr>
|
12
|
+
<tr>
|
13
|
+
<th><%= t('ErrorMessage') %></th>
|
14
|
+
<td><%= h @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
|
+
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
28
|
+
<a class="btn" href="<%= root_path %>failures"><%= t('GoBack') %></a>
|
29
|
+
<input class="btn btn-primary" type="submit" name="retry" value="<%= t('RetryNow') %>" />
|
30
|
+
<input class="btn btn-danger" type="submit" name="delete" value="<%= t('Delete') %>" />
|
31
|
+
</form>
|
@@ -1,105 +1,113 @@
|
|
1
|
-
<
|
2
|
-
<
|
3
|
-
<
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
<section>
|
2
|
+
<header>
|
3
|
+
<h1>
|
4
|
+
<span class="title"><%= t('FailedJobs') %></span>
|
5
|
+
<span class="badge badge-secondary"><%= number_with_delimiter(@failures.count) %></span>
|
6
|
+
</h1>
|
7
|
+
<% if @failures.count > 0 && @total_size > @count %>
|
7
8
|
<%= erb :_paging, :locals => { :url => "#{root_path}failures" } %>
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
</header>
|
9
|
+
<% end %>
|
10
|
+
<%= filtering('failures') if respond_to?(:filtering) %>
|
11
|
+
</header>
|
12
12
|
|
13
|
-
<% if @failures.count > 0 %>
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
<span>Processor: <%= entry['processor'] %></span>
|
57
|
-
</p>
|
58
|
-
</td>
|
59
|
-
</tr>
|
60
|
-
<% end %>
|
61
|
-
</table>
|
62
|
-
<input class="btn btn-primary btn-xs pull-left" type="submit" name="retry" value="<%= t('RetryNow') %>" />
|
63
|
-
<input class="btn btn-danger btn-xs pull-left" type="submit" name="delete" value="<%= t('Delete') %>" />
|
64
|
-
</form>
|
13
|
+
<% if @failures.count > 0 %>
|
14
|
+
<form action="<%= root_path %>failures" method="post">
|
15
|
+
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
16
|
+
<div class="table_container">
|
17
|
+
<table>
|
18
|
+
<thead>
|
19
|
+
<tr>
|
20
|
+
<th width="20px" class="table-checkbox">
|
21
|
+
<label>
|
22
|
+
<input type="checkbox" class="check_all" />
|
23
|
+
</label>
|
24
|
+
</th>
|
25
|
+
<th><%= t('FailedAt') %></th>
|
26
|
+
<th><%= t('Queue') %></th>
|
27
|
+
<th><%= t('Worker') %></th>
|
28
|
+
<th><%= t('Arguments') %></th>
|
29
|
+
<th><%= t('Error') %></th>
|
30
|
+
</tr>
|
31
|
+
</thead>
|
32
|
+
<% @failures.each do |entry| %>
|
33
|
+
<tr>
|
34
|
+
<td class="table-checkbox">
|
35
|
+
<label>
|
36
|
+
<input type='checkbox' name='key[]' value='<%= job_params(entry.item, entry.score) %>' />
|
37
|
+
</label>
|
38
|
+
</td>
|
39
|
+
<td>
|
40
|
+
<a href="<%= root_path %>failures/<%= job_params(entry.item, entry.score) %>"><%= safe_relative_time(entry['failed_at']) %></a>
|
41
|
+
</td>
|
42
|
+
<td>
|
43
|
+
<a href="<%= root_path %>queues/<%= entry.queue %>"><%= entry.queue %></a>
|
44
|
+
</td>
|
45
|
+
<td><%= entry.respond_to?(:display_class) ? entry.display_class : entry.klass %></td>
|
46
|
+
<td>
|
47
|
+
<code>
|
48
|
+
<div class="args"><%= display_args(entry.respond_to?(:display_args) ? entry.display_args : entry.args) %></div>
|
49
|
+
</code>
|
50
|
+
</td>
|
51
|
+
<td>
|
52
|
+
<% error_class = entry['error_class'].to_s %>
|
53
|
+
<% error_message = entry['error_message'].to_s %>
|
54
|
+
<% error_text = "#{h(error_class)}: #{h(error_message)}" %>
|
55
|
+
<% truncated_error = truncate(error_text, 200) %>
|
65
56
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
57
|
+
<% if entry['error_backtrace'] %>
|
58
|
+
<details>
|
59
|
+
<summary>
|
60
|
+
<%= truncated_error %>
|
61
|
+
</summary>
|
62
|
+
<% if entry['error_backtrace'] %>
|
63
|
+
<code>
|
64
|
+
<%= entry['error_backtrace'].join("<br/>") %>
|
65
|
+
</code>
|
66
|
+
<% end %>
|
67
|
+
</details>
|
68
|
+
<% else %>
|
69
|
+
<div>
|
70
|
+
<%= truncated_error %>
|
71
|
+
</div>
|
72
|
+
<% end %>
|
73
|
+
<div>
|
74
|
+
<span>Processor: <%= entry['processor'] %></span>
|
75
|
+
</div>
|
76
|
+
</td>
|
77
|
+
</tr>
|
78
|
+
<% end %>
|
79
|
+
</table>
|
80
|
+
</div>
|
81
|
+
<div class="buttons-row">
|
82
|
+
<input class="btn btn-primary btn-xs pull-left" type="submit" name="retry" value="<%= t('RetryNow') %>" />
|
83
|
+
<input class="btn btn-danger btn-xs pull-left" type="submit" name="delete" value="<%= t('Delete') %>" />
|
84
|
+
</div>
|
85
|
+
</form>
|
74
86
|
|
75
|
-
|
76
|
-
|
77
|
-
|
87
|
+
<div class="buttons-row">
|
88
|
+
<form action="<%= root_path %>failures/all/delete" method="post">
|
89
|
+
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
90
|
+
<input class="btn btn-danger btn-xs pull-right" type="submit" name="delete" value="<%= t('DeleteAll') %>" data-confirm="<%= t('AreYouSure') %>" />
|
91
|
+
</form>
|
92
|
+
<form action="<%= root_path %>failures/all/retry" method="post">
|
93
|
+
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
94
|
+
<input class="btn btn-danger btn-xs pull-right" type="submit" name="retry" value="<%= t('RetryAll') %>" data-confirm="<%= t('AreYouSure') %>" />
|
95
|
+
</form>
|
78
96
|
</div>
|
79
|
-
<% end %>
|
80
97
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
}
|
90
|
-
</script>
|
98
|
+
<% if @failures.count > 0 && @total_size > @count %>
|
99
|
+
<div>
|
100
|
+
<%= erb :_paging, :locals => { :url => "#{root_path}failures" } %>
|
101
|
+
</div>
|
102
|
+
<% end %>
|
103
|
+
<% else %>
|
104
|
+
<div class="alert alert-success"><%= t('NoFailedJobsFound') %></div>
|
105
|
+
<% end %>
|
91
106
|
|
92
|
-
<
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
<% else %>
|
100
|
-
<div class="alert alert-success"><%= t('NoFailedJobsFound') %></div>
|
101
|
-
<% end %>
|
102
|
-
<form action="<%= root_path %>failures/all/reset" method="post">
|
103
|
-
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
104
|
-
<input class="btn btn-danger btn-xs pull-right" type="submit" name="reset" value="<%= t('ResetCounter') %>" data-confirm="<%= t('AreYouSure') %>" />
|
105
|
-
</form>
|
107
|
+
<div class="buttons-row">
|
108
|
+
<form action="<%= root_path %>failures/all/reset" method="post">
|
109
|
+
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
110
|
+
<input class="btn btn-danger btn-xs pull-right" type="submit" name="reset" value="<%= t('ResetCounter') %>" data-confirm="<%= t('AreYouSure') %>" />
|
111
|
+
</form>
|
112
|
+
</div>
|
113
|
+
</section>
|
@@ -0,0 +1,105 @@
|
|
1
|
+
<header class="row">
|
2
|
+
<div class="col-sm-5">
|
3
|
+
<h3><%= t('FailedJobs') %></h3>
|
4
|
+
</div>
|
5
|
+
<% if @failures.count > 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') if respond_to?(:filtering) %>
|
11
|
+
</header>
|
12
|
+
|
13
|
+
<% if @failures.count > 0 %>
|
14
|
+
<form action="<%= root_path %>failures" method="post">
|
15
|
+
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
16
|
+
<table class="table table-striped table-bordered table-white">
|
17
|
+
<thead>
|
18
|
+
<tr>
|
19
|
+
<th width="20px" class="table-checkbox">
|
20
|
+
<label>
|
21
|
+
<input type="checkbox" class="check_all" />
|
22
|
+
</label>
|
23
|
+
</th>
|
24
|
+
<th><%= t('FailedAt') %></th>
|
25
|
+
<th><%= t('Queue') %></th>
|
26
|
+
<th><%= t('Worker') %></th>
|
27
|
+
<th><%= t('Arguments') %></th>
|
28
|
+
<th><%= t('Error') %></th>
|
29
|
+
</tr>
|
30
|
+
</thead>
|
31
|
+
<% @failures.each do |entry| %>
|
32
|
+
<tr>
|
33
|
+
<td class="table-checkbox">
|
34
|
+
<label>
|
35
|
+
<input type='checkbox' name='key[]' value='<%= job_params(entry.item, entry.score) %>' />
|
36
|
+
</label>
|
37
|
+
</td>
|
38
|
+
<td>
|
39
|
+
<a href="<%= root_path %>failures/<%= job_params(entry.item, entry.score) %>"><%= safe_relative_time(entry['failed_at']) %></a>
|
40
|
+
</td>
|
41
|
+
<td>
|
42
|
+
<a href="<%= root_path %>queues/<%= entry.queue %>"><%= entry.queue %></a>
|
43
|
+
</td>
|
44
|
+
<td><%= entry.respond_to?(:display_class) ? entry.display_class : entry.klass %></td>
|
45
|
+
<td>
|
46
|
+
<div class="args"><%= display_args(entry.respond_to?(:display_args) ? entry.display_args : entry.args) %></div>
|
47
|
+
</td>
|
48
|
+
<td style="overflow: auto; padding: 10px;">
|
49
|
+
<a class="backtrace" href="javascript:void(0)", onclick="toggle_error_backtrace(this)">
|
50
|
+
<%= h entry['error_class'] %>: <%= h entry['error_message'].to_s.size > 500 ? entry['error_message'][0..500] + '...' : entry['error_message'] %>
|
51
|
+
</a>
|
52
|
+
<pre style="display: none; background: none; border: 0; width: 100%; max-height: 30em; font-size: 0.8em; white-space: nowrap; overflow: auto;">
|
53
|
+
<%= entry['error_backtrace'].join("<br />") if entry['error_backtrace'] %>
|
54
|
+
</pre>
|
55
|
+
<p>
|
56
|
+
<span>Processor: <%= entry['processor'] %></span>
|
57
|
+
</p>
|
58
|
+
</td>
|
59
|
+
</tr>
|
60
|
+
<% end %>
|
61
|
+
</table>
|
62
|
+
<input class="btn btn-primary btn-xs pull-left" type="submit" name="retry" value="<%= t('RetryNow') %>" />
|
63
|
+
<input class="btn btn-danger btn-xs pull-left" type="submit" name="delete" value="<%= t('Delete') %>" />
|
64
|
+
</form>
|
65
|
+
|
66
|
+
<form action="<%= root_path %>failures/all/delete" method="post">
|
67
|
+
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
68
|
+
<input class="btn btn-danger btn-xs pull-right" type="submit" name="delete" value="<%= t('DeleteAll') %>" data-confirm="<%= t('AreYouSure') %>" />
|
69
|
+
</form>
|
70
|
+
<form action="<%= root_path %>failures/all/retry" method="post">
|
71
|
+
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
72
|
+
<input class="btn btn-danger btn-xs pull-right" type="submit" name="retry" value="<%= t('RetryAll') %>" data-confirm="<%= t('AreYouSure') %>" />
|
73
|
+
</form>
|
74
|
+
|
75
|
+
<% if @failures.count > 0 && @total_size > @count %>
|
76
|
+
<div class="col-sm-4">
|
77
|
+
<%= erb :_paging, :locals => { :url => "#{root_path}failures" } %>
|
78
|
+
</div>
|
79
|
+
<% end %>
|
80
|
+
|
81
|
+
<script>
|
82
|
+
function toggle_error_backtrace(e) {
|
83
|
+
let x = e.nextElementSibling;
|
84
|
+
if (x.style.display === "none") {
|
85
|
+
x.style.display = "block";
|
86
|
+
} else {
|
87
|
+
x.style.display = "none";
|
88
|
+
}
|
89
|
+
}
|
90
|
+
</script>
|
91
|
+
|
92
|
+
<style type="text/css">
|
93
|
+
@media only screen and (prefers-color-scheme: dark) {
|
94
|
+
pre {
|
95
|
+
color: white
|
96
|
+
}
|
97
|
+
}
|
98
|
+
</style>
|
99
|
+
<% else %>
|
100
|
+
<div class="alert alert-success"><%= t('NoFailedJobsFound') %></div>
|
101
|
+
<% end %>
|
102
|
+
<form action="<%= root_path %>failures/all/reset" method="post">
|
103
|
+
<%= csrf_tag if respond_to?(:csrf_tag) %>
|
104
|
+
<input class="btn btn-danger btn-xs pull-right" type="submit" name="reset" value="<%= t('ResetCounter') %>" data-confirm="<%= t('AreYouSure') %>" />
|
105
|
+
</form>
|
@@ -1,49 +1,130 @@
|
|
1
1
|
module Sidekiq
|
2
2
|
module Failures
|
3
3
|
module WebExtension
|
4
|
+
LEGACY_SIDEKIQ_VERSION = Gem::Version.new("7.3.9")
|
5
|
+
|
6
|
+
# Helper method to check Sidekiq version
|
7
|
+
def self.legacy_sidekiq?
|
8
|
+
Gem::Version.new(Sidekiq::VERSION) <= LEGACY_SIDEKIQ_VERSION
|
9
|
+
end
|
10
|
+
|
11
|
+
# Helper method to get parameters based on path and parameter name
|
12
|
+
def self.fetch_param_value(path, param_name)
|
13
|
+
if legacy_sidekiq?
|
14
|
+
# For newer Sidekiq, use route_params or url_params based on path
|
15
|
+
lambda { |env| env.params[param_name] }
|
16
|
+
else
|
17
|
+
# For legacy Sidekiq, just use params
|
18
|
+
if path.include?(":#{param_name}")
|
19
|
+
lambda { |env| env.route_params(param_name.to_sym) }
|
20
|
+
else
|
21
|
+
lambda { |env| env.url_params(param_name.to_s) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Helper method to handle parse_params vs parse_key compatibility
|
27
|
+
def self.parse_key_or_params
|
28
|
+
lambda do |env, key|
|
29
|
+
if env.respond_to?(:parse_key)
|
30
|
+
env.parse_key(key)
|
31
|
+
else
|
32
|
+
env.parse_params(key)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Define the helper method implementation that will be used in both versions
|
38
|
+
# Instead of a static lambda, we'll return a method that needs to be evaluated in context
|
39
|
+
def self.safe_relative_time_implementation
|
40
|
+
lambda do |time, context|
|
41
|
+
return unless time
|
42
|
+
|
43
|
+
time = if time.is_a?(Numeric)
|
44
|
+
Time.at(time)
|
45
|
+
else
|
46
|
+
Time.parse(time)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Use the context to call relative_time
|
50
|
+
context.relative_time(time)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Define the helpers module for Sidekiq 8.0+
|
55
|
+
module FailuresHelpers
|
56
|
+
def safe_relative_time(time)
|
57
|
+
# Pass self (the context with relative_time) to the implementation
|
58
|
+
WebExtension.safe_relative_time_implementation.call(time, self)
|
59
|
+
end
|
60
|
+
end
|
4
61
|
|
5
62
|
def self.registered(app)
|
6
63
|
view_path = File.join(File.expand_path("..", __FILE__), "views")
|
64
|
+
if legacy_sidekiq?
|
65
|
+
failures_view_path = File.join(view_path, "failures_legacy.erb")
|
66
|
+
failure_view_path = File.join(view_path, "failure_legacy.erb")
|
67
|
+
else
|
68
|
+
failures_view_path = File.join(view_path, "failures.erb")
|
69
|
+
failure_view_path = File.join(view_path, "failure.erb")
|
70
|
+
end
|
7
71
|
|
8
|
-
|
9
|
-
|
10
|
-
time = if time.is_a?(Numeric)
|
11
|
-
Time.at(time)
|
12
|
-
else
|
13
|
-
Time.parse(time)
|
14
|
-
end
|
72
|
+
# Create a parse helper for use in routes
|
73
|
+
parse_helper = parse_key_or_params
|
15
74
|
|
16
|
-
|
75
|
+
# Use appropriate helpers implementation based on Sidekiq version
|
76
|
+
if legacy_sidekiq?
|
77
|
+
# Original implementation for older Sidekiq versions
|
78
|
+
app.helpers do
|
79
|
+
define_method(:safe_relative_time) do |time|
|
80
|
+
# Pass self (the context with relative_time) to the implementation
|
81
|
+
WebExtension.safe_relative_time_implementation.call(time, self)
|
82
|
+
end
|
17
83
|
end
|
84
|
+
else
|
85
|
+
# New implementation for Sidekiq 8.0+
|
86
|
+
app.helpers(FailuresHelpers)
|
18
87
|
end
|
19
88
|
|
20
89
|
app.get "/failures" do
|
21
|
-
|
22
|
-
|
90
|
+
page_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures", "page").call(self)
|
91
|
+
count_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures", "count").call(self)
|
92
|
+
@count = (count_param || 25).to_i
|
93
|
+
|
94
|
+
(@current_page, @total_size, @failures) = page(LIST_KEY, page_param, @count, :reverse => true)
|
23
95
|
@failures = @failures.map {|msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
|
24
96
|
|
25
|
-
render(:erb, File.read(
|
97
|
+
render(:erb, File.read(failures_view_path))
|
26
98
|
end
|
27
99
|
|
28
100
|
app.get "/failures/:key" do
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
101
|
+
key_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures/:key", "key").call(self)
|
102
|
+
halt 404 unless key_param
|
103
|
+
|
104
|
+
@failure = FailureSet.new.fetch(*parse_helper.call(self, key_param)).first
|
105
|
+
if @failure.nil?
|
106
|
+
redirect "#{root_path}failures"
|
107
|
+
else
|
108
|
+
render(:erb, File.read(failure_view_path))
|
109
|
+
end
|
34
110
|
end
|
35
111
|
|
36
112
|
app.post "/failures" do
|
37
|
-
|
113
|
+
key_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures", "key").call(self)
|
114
|
+
halt 404 unless key_param
|
38
115
|
|
39
|
-
|
40
|
-
job = FailureSet.new.fetch(*
|
116
|
+
key_param.each do |key|
|
117
|
+
job = FailureSet.new.fetch(*parse_helper.call(self, key)).first
|
41
118
|
next unless job
|
42
119
|
|
43
|
-
|
120
|
+
retry_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures", "retry").call(self)
|
121
|
+
if retry_param
|
44
122
|
job.retry_failure
|
45
|
-
|
46
|
-
|
123
|
+
else
|
124
|
+
delete_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures", "delete").call(self)
|
125
|
+
if delete_param
|
126
|
+
job.delete
|
127
|
+
end
|
47
128
|
end
|
48
129
|
end
|
49
130
|
|
@@ -51,21 +132,26 @@ module Sidekiq
|
|
51
132
|
end
|
52
133
|
|
53
134
|
app.post "/failures/:key" do
|
54
|
-
|
135
|
+
key_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures/:key", "key").call(self)
|
136
|
+
halt 404 unless key_param
|
55
137
|
|
56
|
-
job = FailureSet.new.fetch(*
|
138
|
+
job = FailureSet.new.fetch(*parse_helper.call(self, key_param)).first
|
57
139
|
if job
|
58
|
-
|
140
|
+
retry_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures/:key", "retry").call(self)
|
141
|
+
if retry_param
|
59
142
|
job.retry_failure
|
60
|
-
|
61
|
-
|
143
|
+
else
|
144
|
+
delete_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures/:key", "delete").call(self)
|
145
|
+
if delete_param
|
146
|
+
job.delete
|
147
|
+
end
|
62
148
|
end
|
63
149
|
end
|
64
150
|
redirect_with_query("#{root_path}failures")
|
65
151
|
end
|
66
152
|
|
67
153
|
app.post "/failures/all/reset" do
|
68
|
-
Sidekiq::Failures.
|
154
|
+
Sidekiq::Failures.reset_failure_count
|
69
155
|
redirect "#{root_path}failures"
|
70
156
|
end
|
71
157
|
|
@@ -84,10 +170,11 @@ module Sidekiq
|
|
84
170
|
end
|
85
171
|
|
86
172
|
app.post '/filter/failures' do
|
87
|
-
|
173
|
+
substr_param = Sidekiq::Failures::WebExtension.fetch_param_value("/filter/failures", "substr").call(self)
|
174
|
+
@failures = Sidekiq::Failures::FailureSet.new.scan("*#{substr_param}*")
|
88
175
|
@current_page = 1
|
89
|
-
@count = @total_size = @failures.count
|
90
|
-
render(:erb, File.read(
|
176
|
+
@count = @total_size = @failures.count
|
177
|
+
render(:erb, File.read(failures_view_path))
|
91
178
|
end
|
92
179
|
end
|
93
180
|
end
|
data/lib/sidekiq/failures.rb
CHANGED
@@ -59,9 +59,19 @@ module Sidekiq
|
|
59
59
|
LIST_KEY = :failed
|
60
60
|
|
61
61
|
def self.reset_failures
|
62
|
+
warn "NOTE: Sidekiq::Failures.reset_failures is deprecated; use Sidekiq::Failures.reset_failure_count instead."
|
63
|
+
|
64
|
+
reset_failure_count
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.reset_failure_count
|
62
68
|
Sidekiq.redis { |c| c.set("stat:failed", 0) }
|
63
69
|
end
|
64
70
|
|
71
|
+
def self.clear_failures
|
72
|
+
FailureSet.new.clear
|
73
|
+
end
|
74
|
+
|
65
75
|
def self.count
|
66
76
|
Sidekiq.redis {|r| r.zcard(LIST_KEY) }
|
67
77
|
end
|
@@ -87,7 +97,14 @@ Sidekiq.configure_server do |config|
|
|
87
97
|
end
|
88
98
|
|
89
99
|
if defined?(Sidekiq::Web)
|
90
|
-
|
91
|
-
|
92
|
-
|
100
|
+
if Sidekiq::Failures::WebExtension.legacy_sidekiq?
|
101
|
+
Sidekiq::Web.register Sidekiq::Failures::WebExtension
|
102
|
+
Sidekiq::Web.tabs["Failures"] = "failures"
|
103
|
+
Sidekiq::Web.settings.locales << File.join(File.dirname(__FILE__), "failures/locales")
|
104
|
+
else
|
105
|
+
Sidekiq::Web.configure do |config|
|
106
|
+
config.locales << File.join(File.dirname(__FILE__), "failures/locales")
|
107
|
+
config.register(Sidekiq::Failures::WebExtension, name: "failures", tab: ["Failures"], index: ["failures"])
|
108
|
+
end
|
109
|
+
end
|
93
110
|
end
|
data/test/failures_test.rb
CHANGED
@@ -3,7 +3,7 @@ require "test_helper"
|
|
3
3
|
module Sidekiq
|
4
4
|
describe Failures do
|
5
5
|
describe '.retry_middleware_class' do
|
6
|
-
it 'returns based on Sidekiq::
|
6
|
+
it 'returns based on Sidekiq::VERSION' do
|
7
7
|
case Sidekiq::VERSION[0]
|
8
8
|
when '5'
|
9
9
|
assert_equal Failures.retry_middleware_class, Sidekiq::JobRetry
|
data/test/middleware_test.rb
CHANGED
@@ -29,20 +29,29 @@ end
|
|
29
29
|
|
30
30
|
class SidekiqPost63
|
31
31
|
def new_processor(boss)
|
32
|
-
config = Sidekiq
|
32
|
+
config = ::Sidekiq
|
33
33
|
config[:queues] = ['default']
|
34
|
-
config[:fetch] = Sidekiq::BasicFetch.new(config)
|
34
|
+
config[:fetch] = ::Sidekiq::BasicFetch.new(config)
|
35
35
|
config[:error_handlers] << Sidekiq.method(:default_error_handler)
|
36
36
|
::Sidekiq::Processor.new(config) { |processor, reason = nil| }
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
+
class SidekiqPost7
|
41
|
+
def new_processor(boss)
|
42
|
+
config = ::Sidekiq.default_configuration
|
43
|
+
::Sidekiq::Processor.new(config.default_capsule) { |*args| }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
40
47
|
module Sidekiq
|
41
48
|
module Failures
|
42
49
|
describe "Middleware" do
|
43
50
|
def new_provider
|
44
51
|
version = Gem::Version.new(Sidekiq::VERSION)
|
45
|
-
if version >= Gem::Version.new('
|
52
|
+
if version >= Gem::Version.new('7')
|
53
|
+
SidekiqPost7
|
54
|
+
elsif version >= Gem::Version.new('6.4.0')
|
46
55
|
SidekiqPost63
|
47
56
|
elsif version >= Gem::Version.new('6.0')
|
48
57
|
SidekiqPre63
|
@@ -53,13 +62,16 @@ module Sidekiq
|
|
53
62
|
|
54
63
|
before do
|
55
64
|
$invokes = 0
|
56
|
-
@boss =
|
65
|
+
@boss = Minitest::Mock.new
|
57
66
|
@provider = new_provider
|
58
67
|
@processor = @provider.new_processor(@boss)
|
59
68
|
|
60
|
-
Sidekiq.
|
61
|
-
|
62
|
-
|
69
|
+
Sidekiq.configure_server do |config|
|
70
|
+
config.server_middleware do |chain|
|
71
|
+
chain.add Sidekiq::Failures::Middleware
|
72
|
+
end
|
73
|
+
end
|
74
|
+
Sidekiq.redis(&:flushdb)
|
63
75
|
Sidekiq.instance_eval { @failures_default_mode = nil }
|
64
76
|
end
|
65
77
|
|
@@ -96,7 +108,7 @@ module Sidekiq
|
|
96
108
|
assert_equal 0, failures_count
|
97
109
|
|
98
110
|
assert_raises TestException do
|
99
|
-
@processor.process
|
111
|
+
@processor.send(:process, msg)
|
100
112
|
end
|
101
113
|
|
102
114
|
assert_equal 1, failures_count
|
@@ -109,7 +121,7 @@ module Sidekiq
|
|
109
121
|
assert_equal 0, failures_count
|
110
122
|
|
111
123
|
assert_raises TestException do
|
112
|
-
@processor.process
|
124
|
+
@processor.send(:process, msg)
|
113
125
|
end
|
114
126
|
|
115
127
|
assert_equal 1, failures_count
|
@@ -121,7 +133,7 @@ module Sidekiq
|
|
121
133
|
|
122
134
|
assert_equal 0, failures_count
|
123
135
|
|
124
|
-
@processor.process
|
136
|
+
@processor.send(:process, msg)
|
125
137
|
|
126
138
|
assert_equal 0, failures_count
|
127
139
|
assert_equal 1, $invokes
|
@@ -133,7 +145,7 @@ module Sidekiq
|
|
133
145
|
assert_equal 0, failures_count
|
134
146
|
|
135
147
|
assert_raises TestException do
|
136
|
-
@processor.process
|
148
|
+
@processor.send(:process, msg)
|
137
149
|
end
|
138
150
|
|
139
151
|
assert_equal 0, failures_count
|
@@ -148,21 +160,20 @@ module Sidekiq
|
|
148
160
|
assert_equal 0, failures_count
|
149
161
|
|
150
162
|
assert_raises TestException do
|
151
|
-
@processor.process
|
163
|
+
@processor.send(:process, msg)
|
152
164
|
end
|
153
165
|
|
154
166
|
assert_equal 0, failures_count
|
155
167
|
assert_equal 1, $invokes
|
156
168
|
end
|
157
169
|
|
158
|
-
|
159
170
|
it "doesn't record failure if going to be retired again and configured to track exhaustion" do
|
160
171
|
msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry' => true, 'failures' => 'exhausted')
|
161
172
|
|
162
173
|
assert_equal 0, failures_count
|
163
174
|
|
164
175
|
assert_raises TestException do
|
165
|
-
@processor.process
|
176
|
+
@processor.send(:process, msg)
|
166
177
|
end
|
167
178
|
|
168
179
|
assert_equal 0, failures_count
|
@@ -175,7 +186,7 @@ module Sidekiq
|
|
175
186
|
assert_equal 0, failures_count
|
176
187
|
|
177
188
|
assert_raises TestException do
|
178
|
-
@processor.process
|
189
|
+
@processor.send(:process, msg)
|
179
190
|
end
|
180
191
|
|
181
192
|
assert_equal 1, failures_count
|
@@ -188,7 +199,7 @@ module Sidekiq
|
|
188
199
|
assert_equal 0, failures_count
|
189
200
|
|
190
201
|
assert_raises TestException do
|
191
|
-
@processor.process
|
202
|
+
@processor.send(:process, msg)
|
192
203
|
end
|
193
204
|
|
194
205
|
assert_equal 1, failures_count
|
@@ -203,7 +214,7 @@ module Sidekiq
|
|
203
214
|
assert_equal 0, failures_count
|
204
215
|
|
205
216
|
assert_raises TestException do
|
206
|
-
@processor.process
|
217
|
+
@processor.send(:process, msg)
|
207
218
|
end
|
208
219
|
|
209
220
|
assert_equal 1, failures_count
|
@@ -218,7 +229,7 @@ module Sidekiq
|
|
218
229
|
assert_equal 0, failures_count
|
219
230
|
|
220
231
|
assert_raises TestException do
|
221
|
-
@processor.process
|
232
|
+
@processor.send(:process, msg)
|
222
233
|
end
|
223
234
|
|
224
235
|
assert_equal 1, failures_count
|
@@ -234,11 +245,11 @@ module Sidekiq
|
|
234
245
|
assert_equal 0, failures_count
|
235
246
|
|
236
247
|
3.times do
|
237
|
-
boss =
|
248
|
+
boss = Minitest::Mock.new
|
238
249
|
processor = @provider.new_processor(boss)
|
239
250
|
|
240
251
|
assert_raises TestException do
|
241
|
-
processor.process
|
252
|
+
processor.send(:process, msg)
|
242
253
|
end
|
243
254
|
|
244
255
|
boss.verify
|
@@ -259,7 +270,7 @@ module Sidekiq
|
|
259
270
|
assert_equal 0, Sidekiq::Failures.count
|
260
271
|
|
261
272
|
assert_raises TestException do
|
262
|
-
@processor.process
|
273
|
+
@processor.send(:process, msg)
|
263
274
|
end
|
264
275
|
|
265
276
|
assert_equal 1, Sidekiq::Failures.count
|
data/test/test_helper.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
$TESTING = true
|
2
2
|
|
3
|
+
ENV["MT_CPU"] = "1" # Disable parallel testing to avoid flaky tests, force a single CPU for minitest
|
4
|
+
|
3
5
|
require "minitest/autorun"
|
4
6
|
require "minitest/spec"
|
5
7
|
require "minitest/mock"
|
@@ -15,4 +17,4 @@ require "sidekiq/cli"
|
|
15
17
|
|
16
18
|
Sidekiq.logger.level = Logger::ERROR
|
17
19
|
|
18
|
-
REDIS = Sidekiq::RedisConnection.create(url: "redis://
|
20
|
+
REDIS = Sidekiq::RedisConnection.create(url: "redis://127.0.0.1:6379/15") # Use DB 15 for testing
|
data/test/web_extension_test.rb
CHANGED
@@ -5,7 +5,7 @@ module Sidekiq
|
|
5
5
|
describe "WebExtension" do
|
6
6
|
include Rack::Test::Methods
|
7
7
|
|
8
|
-
TOKEN = SecureRandom.base64(32).freeze
|
8
|
+
TOKEN = SecureRandom.base64(defined?(Sidekiq::Web::TOKEN_LENGTH) ? Sidekiq::Web::TOKEN_LENGTH : 32).freeze
|
9
9
|
|
10
10
|
def app
|
11
11
|
Sidekiq::Web
|
@@ -14,8 +14,7 @@ module Sidekiq
|
|
14
14
|
before do
|
15
15
|
env 'rack.session', { csrf: TOKEN }
|
16
16
|
env 'HTTP_X_CSRF_TOKEN', TOKEN
|
17
|
-
Sidekiq.redis
|
18
|
-
Sidekiq.redis {|c| c.flushdb }
|
17
|
+
Sidekiq.redis(&:flushdb)
|
19
18
|
end
|
20
19
|
|
21
20
|
it 'can display home with failures tab' do
|
@@ -41,7 +40,7 @@ module Sidekiq
|
|
41
40
|
|
42
41
|
describe 'when there are failures' do
|
43
42
|
before do
|
44
|
-
create_sample_failure
|
43
|
+
@failure = create_sample_failure
|
45
44
|
get '/failures'
|
46
45
|
end
|
47
46
|
|
@@ -53,15 +52,14 @@ module Sidekiq
|
|
53
52
|
_(last_response.body).must_match(/Failed Jobs/)
|
54
53
|
_(last_response.body).must_match(/HardWorker/)
|
55
54
|
_(last_response.body).must_match(/ArgumentError/)
|
56
|
-
_(last_response.body).wont_match(/No failed jobs found/)
|
57
55
|
end
|
58
56
|
|
59
57
|
it 'can reset counter' do
|
60
58
|
assert_equal failed_count, "1"
|
61
59
|
|
62
60
|
_(last_response.body).must_match(/HardWorker/)
|
61
|
+
post '/failures/all/reset', { authenticity_token: TOKEN }
|
63
62
|
|
64
|
-
post '/failures/all/reset'
|
65
63
|
_(last_response.status).must_equal 302
|
66
64
|
_(last_response.location).must_match(/failures$/)
|
67
65
|
|
@@ -82,7 +80,7 @@ module Sidekiq
|
|
82
80
|
|
83
81
|
_(last_response.body).must_match(/HardWorker/)
|
84
82
|
|
85
|
-
post '/failures/all/delete'
|
83
|
+
post '/failures/all/delete', { authenticity_token: TOKEN }
|
86
84
|
_(last_response.status).must_equal 302
|
87
85
|
_(last_response.location).must_match(/failures$/)
|
88
86
|
|
@@ -102,7 +100,7 @@ module Sidekiq
|
|
102
100
|
assert_equal failed_count, "1"
|
103
101
|
|
104
102
|
_(last_response.body).must_match(/HardWorker/)
|
105
|
-
post '/failures/all/retry'
|
103
|
+
post '/failures/all/retry', { authenticity_token: TOKEN }
|
106
104
|
_(last_response.status).must_equal 302
|
107
105
|
_(last_response.location).must_match(/failures/)
|
108
106
|
|
@@ -116,7 +114,7 @@ module Sidekiq
|
|
116
114
|
|
117
115
|
_(last_response.body).must_match(/HardWorker/)
|
118
116
|
|
119
|
-
post '/failures', { :key => [
|
117
|
+
post '/failures', { authenticity_token: TOKEN, :key => [build_param_key(@failure)], :delete => 'Delete' }
|
120
118
|
_(last_response.status).must_equal 302
|
121
119
|
_(last_response.location).must_match(/failures/)
|
122
120
|
|
@@ -130,7 +128,7 @@ module Sidekiq
|
|
130
128
|
|
131
129
|
_(last_response.body).must_match(/HardWorker/)
|
132
130
|
|
133
|
-
post '/failures', { :key => [
|
131
|
+
post '/failures', { authenticity_token: TOKEN, :key => [build_param_key(@failure)], :retry => 'Retry Now' }
|
134
132
|
_(last_response.status).must_equal 302
|
135
133
|
_(last_response.location).must_match(/failures/)
|
136
134
|
|
@@ -150,8 +148,8 @@ module Sidekiq
|
|
150
148
|
|
151
149
|
describe 'when there is failure' do
|
152
150
|
before do
|
153
|
-
create_sample_failure
|
154
|
-
get "/failures/#{
|
151
|
+
@failure = create_sample_failure
|
152
|
+
get "/failures/#{build_param_key(@failure)}"
|
155
153
|
end
|
156
154
|
|
157
155
|
it 'should be successful' do
|
@@ -168,11 +166,11 @@ module Sidekiq
|
|
168
166
|
it 'can delete failure' do
|
169
167
|
_(last_response.body).must_match(/HardWorker/)
|
170
168
|
|
171
|
-
post "/failures/#{
|
169
|
+
post "/failures/#{build_param_key(@failure)}", { authenticity_token: TOKEN, :delete => 'Delete' }
|
172
170
|
_(last_response.status).must_equal 302
|
173
171
|
_(last_response.location).must_match(/failures/)
|
174
172
|
|
175
|
-
get "/failures/#{
|
173
|
+
get "/failures/#{build_param_key(@failure)}"
|
176
174
|
_(last_response.status).must_equal 302
|
177
175
|
_(last_response.location).must_match(/failures/)
|
178
176
|
end
|
@@ -180,11 +178,11 @@ module Sidekiq
|
|
180
178
|
it 'can retry failure' do
|
181
179
|
_(last_response.body).must_match(/HardWorker/)
|
182
180
|
|
183
|
-
post "/failures/#{
|
181
|
+
post "/failures/#{build_param_key(@failure)}", { authenticity_token: TOKEN, :retry => 'Retry Now' }
|
184
182
|
_(last_response.status).must_equal 302
|
185
183
|
_(last_response.location).must_match(/failures/)
|
186
184
|
|
187
|
-
get "/failures/#{
|
185
|
+
get "/failures/#{build_param_key(@failure)}"
|
188
186
|
_(last_response.status).must_equal 302
|
189
187
|
_(last_response.location).must_match(/failures/)
|
190
188
|
end
|
@@ -192,7 +190,7 @@ module Sidekiq
|
|
192
190
|
if defined? Sidekiq::Pro
|
193
191
|
it 'can filter failure' do
|
194
192
|
create_sample_failure
|
195
|
-
post '/filter/failures', substr: 'new'
|
193
|
+
post '/filter/failures', { authenticity_token: TOKEN, substr: 'new' }
|
196
194
|
|
197
195
|
_(last_response.status).must_equal 200
|
198
196
|
end
|
@@ -201,17 +199,53 @@ module Sidekiq
|
|
201
199
|
|
202
200
|
describe 'when there is specific failure' do
|
203
201
|
describe 'with unescaped data' do
|
204
|
-
|
205
|
-
|
206
|
-
|
202
|
+
describe 'the index page' do
|
203
|
+
before do
|
204
|
+
create_sample_failure(args: ['<h1>omg</h1>'], error_message: '<p>wow</p>', error_class: '<script>alert("xss")</script>ArgumentError')
|
205
|
+
get '/failures'
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'can escape arguments' do
|
209
|
+
_(last_response.body).must_match(/"<h1>omg<\/h1>"/)
|
210
|
+
_(last_response.body).wont_match(/<h1>omg<\/h1>/)
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'can escape error message' do
|
214
|
+
_(last_response.body).must_match(/<script>alert\("xss"\)<\/script>ArgumentError: <p>wow<\/p>/)
|
215
|
+
_(last_response.body).wont_match(/<script>alert\("xss"\)<\/script>ArgumentError/)
|
216
|
+
_(last_response.body).wont_match(/<p>wow<\/p>/)
|
217
|
+
end
|
207
218
|
end
|
208
219
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
220
|
+
describe 'the details page' do
|
221
|
+
before do
|
222
|
+
@failure = create_sample_failure(
|
223
|
+
args: ['<h1>omg</h1>', '<script>alert("xss2")</script>'],
|
224
|
+
error_message: '<p>wow</p>',
|
225
|
+
error_class: '<script>alert("xss")</script>ArgumentError'
|
226
|
+
)
|
227
|
+
get "/failures/#{build_param_key(@failure)}"
|
228
|
+
end
|
229
|
+
|
230
|
+
it 'can escape error class' do
|
231
|
+
_(last_response.status).must_equal 200
|
232
|
+
_(last_response.body).must_match(/<script>alert\("xss"\)<\/script>ArgumentError/)
|
233
|
+
_(last_response.body).wont_match(/<script>alert\("xss"\)<\/script>ArgumentError/)
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'can escape error message' do
|
237
|
+
_(last_response.status).must_equal 200
|
238
|
+
_(last_response.body).must_match(/<p>wow<\/p>/)
|
239
|
+
_(last_response.body).wont_match(/<p>wow<\/p>/)
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'can escape arguments' do
|
243
|
+
_(last_response.status).must_equal 200
|
244
|
+
_(last_response.body).must_match(/"<h1>omg<\/h1>"/)
|
245
|
+
# _(last_response.body).must_match(/"<script>alert\("xss2"\)<\/script>"/)
|
246
|
+
_(last_response.body).wont_match(/<h1>omg<\/h1>/)
|
247
|
+
_(last_response.body).wont_match(/<script>alert\("xss2"\)<\/script>/)
|
248
|
+
end
|
215
249
|
end
|
216
250
|
end
|
217
251
|
|
@@ -223,7 +257,6 @@ module Sidekiq
|
|
223
257
|
|
224
258
|
it 'should be successful' do
|
225
259
|
_(last_response.status).must_equal 200
|
226
|
-
_(last_response.body).wont_match(/No failed jobs found/)
|
227
260
|
end
|
228
261
|
end
|
229
262
|
|
@@ -235,30 +268,43 @@ module Sidekiq
|
|
235
268
|
|
236
269
|
it 'should be successful' do
|
237
270
|
_(last_response.status).must_equal 200
|
238
|
-
_(last_response.body).wont_match(/No failed jobs found/)
|
239
271
|
end
|
240
272
|
end
|
241
273
|
end
|
242
274
|
|
243
275
|
def create_sample_failure(data = {})
|
276
|
+
failed_at = Time.now.utc.to_i - 10000
|
277
|
+
enqueued_at = failed_at - 1000
|
278
|
+
|
244
279
|
data = {
|
245
280
|
:queue => 'default',
|
246
281
|
:class => 'HardWorker',
|
247
282
|
:args => ['bob', 5],
|
248
|
-
:jid =>
|
249
|
-
:enqueued_at =>
|
250
|
-
:failed_at =>
|
283
|
+
:jid => SecureRandom.hex(12),
|
284
|
+
:enqueued_at => failed_at.to_f,
|
285
|
+
:failed_at => enqueued_at.to_f,
|
251
286
|
:error_class => 'ArgumentError',
|
252
287
|
:error_message => 'Some new message',
|
253
288
|
:error_backtrace => ["path/file1.rb", "path/file2.rb"]
|
254
289
|
}.merge(data)
|
255
290
|
|
291
|
+
# Store the score so we can use it for retrieving the failure later
|
292
|
+
score = failure_score
|
293
|
+
|
256
294
|
Sidekiq.redis do |c|
|
257
295
|
c.multi do
|
258
|
-
c.zadd(Sidekiq::Failures::LIST_KEY,
|
296
|
+
c.zadd(Sidekiq::Failures::LIST_KEY, score, Sidekiq.dump_json(data))
|
259
297
|
c.set("stat:failed", 1)
|
260
298
|
end
|
261
299
|
end
|
300
|
+
|
301
|
+
# Update data with the score used so tests can reference it
|
302
|
+
data[:score] = score
|
303
|
+
data
|
304
|
+
end
|
305
|
+
|
306
|
+
def build_param_key(failure)
|
307
|
+
"#{failure[:score]}-#{failure[:jid]}"
|
262
308
|
end
|
263
309
|
|
264
310
|
def failed_count
|
@@ -266,7 +312,7 @@ module Sidekiq
|
|
266
312
|
end
|
267
313
|
|
268
314
|
def failure_score
|
269
|
-
Time.
|
315
|
+
Time.now.to_f
|
270
316
|
end
|
271
317
|
end
|
272
318
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sidekiq-failures
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marcelo Silveira
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-08-21 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: sidekiq
|
@@ -120,7 +119,9 @@ files:
|
|
120
119
|
- lib/sidekiq/failures/sorted_entry.rb
|
121
120
|
- lib/sidekiq/failures/version.rb
|
122
121
|
- lib/sidekiq/failures/views/failure.erb
|
122
|
+
- lib/sidekiq/failures/views/failure_legacy.erb
|
123
123
|
- lib/sidekiq/failures/views/failures.erb
|
124
|
+
- lib/sidekiq/failures/views/failures_legacy.erb
|
124
125
|
- lib/sidekiq/failures/web_extension.rb
|
125
126
|
- sidekiq-failures.gemspec
|
126
127
|
- test/failures_test.rb
|
@@ -131,7 +132,6 @@ homepage: https://github.com/mhfs/sidekiq-failures/
|
|
131
132
|
licenses:
|
132
133
|
- MIT
|
133
134
|
metadata: {}
|
134
|
-
post_install_message:
|
135
135
|
rdoc_options: []
|
136
136
|
require_paths:
|
137
137
|
- lib
|
@@ -146,8 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
146
146
|
- !ruby/object:Gem::Version
|
147
147
|
version: '0'
|
148
148
|
requirements: []
|
149
|
-
rubygems_version: 3.
|
150
|
-
signing_key:
|
149
|
+
rubygems_version: 3.6.2
|
151
150
|
specification_version: 4
|
152
151
|
summary: Keeps track of Sidekiq failed jobs and adds a tab to the Web UI to let you
|
153
152
|
browse them. Makes use of Sidekiq's custom tabs and middleware chain.
|