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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d9628945c34d8f9ed07942963bfa2af8205d62813380126efc72fdb2f357dac
4
- data.tar.gz: 9909e3f78cf4974de23878094751200f7d23b795637524d469b40d26237cf8c0
3
+ metadata.gz: ba0a7d8ecb1ed1f847be0918723281d6c12d7f40a4780c089340cc8e687183f3
4
+ data.tar.gz: 10aca014769f8883c9de557bded7ac01287276924ad314ec1006d6d0e1c332bd
5
5
  SHA512:
6
- metadata.gz: df5f041943257ee40566a7eb740abd0f61c69d8a03e15f0b8a4727bc1a37a72115a6173684f22a11a095c7478c7f4c325e7e73fce1094fe72a147213443384c5
7
- data.tar.gz: b7265f98b1ba526c14d3c2a28a5d17a85dc8fa9231d7233623c964784cb903557518729398db13e07605a6d2367cc143b6084900fb68faff029c6339c0fbe923
6
+ metadata.gz: 1bc85386b596a67b27e00b6c2efed3377f8377bb89dd4eb128336b0f1bf4150acd361c3be1f1ed8dce233710031f3bd9b17d09d4c85b7ee7673da67ba7649d89
7
+ data.tar.gz: 236381eb70f14564b3dd43fc715ad8ac3f5578973b4c20fb95f8f6cfcedadb82db922c1cb8eafed13e6c5189d5218ec0c959dbe559592413b727da2ef18fed25
@@ -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@v3
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 dependecy
61
- * Escape exception info when outputing to html
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 Expception#message instead of Exception#to_s (@supaspoida)
85
- * Removed web depencies (@LongMan)
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 reseting Sidekiq Failure stored failed jobs programmatically.
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.reset_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
- results = conn.zrangebyscore(Sidekiq::Failures::LIST_KEY, score, score)
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,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module Failures
3
- VERSION = "1.0.3"
3
+ VERSION = "1.1.0"
4
4
  end
5
5
  end
@@ -1,31 +1,39 @@
1
1
  <%= erb :_job_info, :locals => {:job => @failure, :type => :failure} %>
2
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
- <%= 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>
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
- <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">
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
- </div>
9
- <% end %>
10
- <%= filtering('failures') if respond_to?(:filtering) %>
11
- </header>
9
+ <% end %>
10
+ <%= filtering('failures') if respond_to?(:filtering) %>
11
+ </header>
12
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>
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
- <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>
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
- <% if @failures.count > 0 && @total_size > @count %>
76
- <div class="col-sm-4">
77
- <%= erb :_paging, :locals => { :url => "#{root_path}failures" } %>
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
- <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>
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
- <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>
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
- app.helpers do
9
- def safe_relative_time(time)
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
- relative_time(time)
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
- @count = (params[:count] || 25).to_i
22
- (@current_page, @total_size, @failures) = page(LIST_KEY, params[:page], @count, :reverse => true)
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(File.join(view_path, "failures.erb")))
97
+ render(:erb, File.read(failures_view_path))
26
98
  end
27
99
 
28
100
  app.get "/failures/:key" do
29
- halt 404 unless params['key']
30
-
31
- @failure = FailureSet.new.fetch(*parse_params(params['key'])).first
32
- redirect "#{root_path}failures" if @failure.nil?
33
- render(:erb, File.read(File.join(view_path, "failure.erb")))
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
- halt 404 unless params['key']
113
+ key_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures", "key").call(self)
114
+ halt 404 unless key_param
38
115
 
39
- params['key'].each do |key|
40
- job = FailureSet.new.fetch(*parse_params(key)).first
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
- if params['retry']
120
+ retry_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures", "retry").call(self)
121
+ if retry_param
44
122
  job.retry_failure
45
- elsif params['delete']
46
- job.delete
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
- halt 404 unless params['key']
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(*parse_params(params['key'])).first
138
+ job = FailureSet.new.fetch(*parse_helper.call(self, key_param)).first
57
139
  if job
58
- if params['retry']
140
+ retry_param = Sidekiq::Failures::WebExtension.fetch_param_value("/failures/:key", "retry").call(self)
141
+ if retry_param
59
142
  job.retry_failure
60
- elsif params['delete']
61
- job.delete
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.reset_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
- @failures = Sidekiq::Failures::FailureSet.new.scan("*#{params[:substr]}*")
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(File.join(view_path, "failures.erb")))
176
+ @count = @total_size = @failures.count
177
+ render(:erb, File.read(failures_view_path))
91
178
  end
92
179
  end
93
180
  end
@@ -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
- Sidekiq::Web.register Sidekiq::Failures::WebExtension
91
- Sidekiq::Web.tabs["Failures"] = "failures"
92
- Sidekiq::Web.settings.locales << File.join(File.dirname(__FILE__), "failures/locales")
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
@@ -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::VERISON' do
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
@@ -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('6.4.0')
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 = MiniTest::Mock.new
65
+ @boss = Minitest::Mock.new
57
66
  @provider = new_provider
58
67
  @processor = @provider.new_processor(@boss)
59
68
 
60
- Sidekiq.server_middleware {|chain| chain.add Sidekiq::Failures::Middleware }
61
- Sidekiq.redis = REDIS
62
- Sidekiq.redis { |c| c.flushdb }
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(msg)
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(msg)
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(msg)
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(msg)
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(msg)
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(msg)
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(msg)
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(msg)
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(msg)
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(msg)
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 = MiniTest::Mock.new
248
+ boss = Minitest::Mock.new
238
249
  processor = @provider.new_processor(boss)
239
250
 
240
251
  assert_raises TestException do
241
- processor.process(msg)
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(msg)
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://localhost/15")
20
+ REDIS = Sidekiq::RedisConnection.create(url: "redis://127.0.0.1:6379/15") # Use DB 15 for testing
@@ -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 = 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 => [failure_score], :delete => 'Delete' }
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 => [failure_score], :retry => 'Retry Now' }
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/#{failure_score}"
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/#{failure_score}", :delete => 'Delete'
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/#{failure_score}"
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/#{failure_score}", :retry => 'Retry Now'
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/#{failure_score}"
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
- before do
205
- create_sample_failure(args: ['<h1>omg</h1>'], error_message: '<p>wow</p>')
206
- get '/failures'
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(/&quot;&lt;h1&gt;omg&lt;\/h1&gt;&quot;/)
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(/&lt;script&gt;alert\(&quot;xss&quot;\)&lt;\/script&gt;ArgumentError: &lt;p&gt;wow&lt;\/p&gt;/)
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
- it 'can escape arguments' do
210
- _(last_response.body).must_match(/&quot;&lt;h1&gt;omg&lt;&#x2F;h1&gt;&quot;/)
211
- end
212
-
213
- it 'can escape error message' do
214
- _(last_response.body).must_match(/ArgumentError: &lt;p&gt;wow&lt;&#x2F;p&gt;/)
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(/&lt;script&gt;alert\(&quot;xss&quot;\)&lt;\/script&gt;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(/&lt;p&gt;wow&lt;\/p&gt;/)
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(/&quot;&lt;h1&gt;omg&lt;\/h1&gt;&quot;/)
245
+ # _(last_response.body).must_match(/&quot;&lt;script&gt;alert\(&quot;xss2&quot;\)&lt;\/script&gt;&quot;/)
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 => 1,
249
- :enqueued_at => Time.now.utc.to_f,
250
- :failed_at => Time.now.utc.to_f,
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, failure_score, Sidekiq.dump_json(data))
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.at(1).to_f
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.3
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: 2022-08-13 00:00:00.000000000 Z
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.3.7
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.