sidekiq-failures 0.3.0 → 0.4.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
  SHA1:
3
- metadata.gz: 708163dea57e9c34fbaf279980cd23c56420415f
4
- data.tar.gz: 1a8967ec4c7efc057fa48e2481601fc42980edf0
3
+ metadata.gz: 1e3a32d193a70834983a509cda264943a04cf76d
4
+ data.tar.gz: 946c4d36eab67ffd4b16b1e4e5dece3699be0f5a
5
5
  SHA512:
6
- metadata.gz: d8bca52118ae7b792ac8387c25aad0662680b2797ab8fe0bd0b668986fe4643f26ddefc2200d33cbf4c662a85d16d1279f2ff33097ebaa502eddbd1da5413beb
7
- data.tar.gz: 5ac567f23aa5f0e9f27769ca5223da6c4d08f4e6de981c398c87e6a3978706d543302370539d413f9023a7c504e0015b200f641a76f433fbc1ca7c058242c2d8
6
+ metadata.gz: 322bccce2ceacf773accdc94f9f693fc4c74286308aa2470cde98b731fedace3497514114161851194b074c44aa09179c36cc7824523297ad7b31da0480b2a05
7
+ data.tar.gz: ef0caebd8929b2f98bb1a7956bfea0b9806198bb72f29d3ee971e7381d44b68b8fea6766aba82c8c3813ec0ad41d9b4ea05b5da773aea4d9d60bd913922c082a
data/.gitignore CHANGED
@@ -14,3 +14,4 @@ spec/reports
14
14
  test/tmp
15
15
  test/version_tmp
16
16
  tmp
17
+ Gemfile.lock
data/.travis.yml CHANGED
@@ -5,3 +5,10 @@ rvm:
5
5
  - 1.9.3
6
6
  - jruby-19mode
7
7
  - 2.0.0
8
+ - 2.1.0
9
+ - 2.1.1
10
+ env:
11
+ matrix:
12
+ - SIDEKIQ_VERSION="~> 2.16.0"
13
+ - SIDEKIQ_VERSION="~> 2.17.0"
14
+ - SIDEKIQ_VERSION="~> 3.0.0"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 0.4.0
4
+ * Bump sidekiq dependency to sidekiq >= 2.16.0
5
+ * Introduce delete(all) / retry(all) (@spectator)
6
+ * Fix Sidekiq 3 compatibility (@petergoldstein)
7
+ * Sidekiq 3 compatibility cleanup (@spectator)
8
+ * Explicitly require sidekiq/api (@krasnoukhov)
9
+
3
10
  ## 0.3.0
4
11
  * Bump sidekiq dependency to sidekiq >= 2.14.0
5
12
  * Remove slim templates and dependecy
@@ -8,7 +15,7 @@
8
15
  * Add `Sidekiq::Failures.count` helper method (@zanker)
9
16
  * Adhere to sidekiq approach of showing UTC times
10
17
  * Catch all exceptions, not just those that inherit from StandardError (@tylerkovacs)
11
- * Fix pricate method call (@bwthomas)
18
+ * Fix private method call (@bwthomas)
12
19
 
13
20
  ## 0.2.2
14
21
  * Support ERB for sidekiq >= 2.14.0 (@tobiassvn)
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in sidekiq-failures.gemspec
4
4
  gemspec
5
+
6
+ gem 'sidekiq', ENV['SIDEKIQ_VERSION'] if ENV['SIDEKIQ_VERSION']
@@ -4,7 +4,10 @@ rescue LoadError
4
4
  # client-only usage
5
5
  end
6
6
 
7
+ require "sidekiq/api"
7
8
  require "sidekiq/failures/version"
9
+ require "sidekiq/failures/sorted_entry"
10
+ require "sidekiq/failures/failure_set"
8
11
  require "sidekiq/failures/middleware"
9
12
  require "sidekiq/failures/web_extension"
10
13
 
@@ -50,37 +53,27 @@ module Sidekiq
50
53
  end
51
54
 
52
55
  module Failures
53
-
54
56
  LIST_KEY = :failed
55
57
 
56
- def self.reset_failures(options = {})
57
- Sidekiq.redis { |c|
58
- c.multi do
59
- c.del(LIST_KEY)
60
- c.set("stat:failed", 0) if options[:counter] || options["counter"]
61
- end
62
- }
58
+ def self.reset_failures
59
+ Sidekiq.redis { |c| c.set("stat:failed", 0) }
63
60
  end
64
61
 
65
62
  def self.count
66
- Sidekiq.redis {|r| r.llen(LIST_KEY) }
63
+ Sidekiq.redis {|r| r.zcard(LIST_KEY) }
67
64
  end
68
65
  end
69
66
  end
70
67
 
71
68
  Sidekiq.configure_server do |config|
72
69
  config.server_middleware do |chain|
73
- chain.add Sidekiq::Failures::Middleware
70
+ chain.insert_before Sidekiq::Middleware::Server::RetryJobs,
71
+ Sidekiq::Failures::Middleware
74
72
  end
75
73
  end
76
74
 
77
75
  if defined?(Sidekiq::Web)
78
76
  Sidekiq::Web.register Sidekiq::Failures::WebExtension
79
-
80
- if Sidekiq::Web.tabs.is_a?(Array)
81
- # For sidekiq < 2.5
82
- Sidekiq::Web.tabs << "failures"
83
- else
84
- Sidekiq::Web.tabs["Failures"] = "failures"
85
- end
77
+ Sidekiq::Web.tabs["Failures"] = "failures"
78
+ Sidekiq::Web.settings.locales << File.join(File.dirname(__FILE__), "failures/locales")
86
79
  end
@@ -0,0 +1,22 @@
1
+ module Sidekiq
2
+ module Failures
3
+ Superclass =
4
+ if defined?(Sidekiq::JobSet)
5
+ Sidekiq::JobSet
6
+ else
7
+ Sidekiq::SortedSet
8
+ end
9
+
10
+ class FailureSet < Superclass
11
+ def initialize
12
+ super LIST_KEY
13
+ end
14
+
15
+ def retry_all_failures
16
+ while size > 0
17
+ each(&:retry_failure)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ en:
2
+ FailedJobs: Failed Jobs
3
+ FailedAt: Failed At
4
+ ResetCounter: Reset Counter
5
+ NoFailedJobsFound: No failed jobs found
@@ -2,7 +2,6 @@ module Sidekiq
2
2
  module Failures
3
3
 
4
4
  class Middleware
5
- include Sidekiq::Util
6
5
  attr_accessor :msg
7
6
 
8
7
  def call(worker, msg, queue)
@@ -13,21 +12,24 @@ module Sidekiq
13
12
  rescue Exception => e
14
13
  raise e if skip_failure?
15
14
 
16
- data = {
17
- :failed_at => Time.now.utc,
18
- :payload => msg,
19
- :exception => e.class.to_s,
20
- :error => e.message,
21
- :backtrace => e.backtrace,
22
- :worker => msg['class'],
23
- :processor => "#{hostname}:#{process_id}-#{Thread.current.object_id}",
24
- :queue => queue
25
- }
15
+ msg['error_message'] = e.message
16
+ msg['error_class'] = e.class.name
17
+ msg['processor'] = identity
18
+ msg['failed_at'] = Time.now.utc.to_f
26
19
 
20
+ if msg['backtrace'] == true
21
+ msg['error_backtrace'] = e.backtrace
22
+ elsif msg['backtrace'] == false
23
+ # do nothing
24
+ elsif msg['backtrace'].to_i != 0
25
+ msg['error_backtrace'] = e.backtrace[0..msg['backtrace'].to_i]
26
+ end
27
+
28
+ payload = Sidekiq.dump_json(msg)
27
29
  Sidekiq.redis do |conn|
28
- conn.lpush(LIST_KEY, Sidekiq.dump_json(data))
30
+ conn.zadd(LIST_KEY, Time.now.utc.to_f, payload)
29
31
  unless Sidekiq.failures_max_count == false
30
- conn.ltrim(LIST_KEY, 0, Sidekiq.failures_max_count - 1)
32
+ conn.zremrangebyrank(LIST_KEY, 0, -(Sidekiq.failures_max_count + 1))
31
33
  end
32
34
  end
33
35
 
@@ -36,12 +38,16 @@ module Sidekiq
36
38
 
37
39
  private
38
40
 
39
- def skip_failure?
40
- failure_mode == :off || not_exhausted?
41
+ def failure_mode_off?
42
+ failure_mode == :off
41
43
  end
42
44
 
43
- def not_exhausted?
44
- failure_mode == :exhausted && !last_try?
45
+ def failure_mode_exhausted?
46
+ failure_mode == :exhausted
47
+ end
48
+
49
+ def skip_failure?
50
+ failure_mode_off? || failure_mode_exhausted? && !exhausted?
45
51
  end
46
52
 
47
53
  def failure_mode
@@ -57,8 +63,12 @@ module Sidekiq
57
63
  end
58
64
  end
59
65
 
60
- def last_try?
61
- ! msg['retry'] || retry_count == max_retries - 1
66
+ def exhausted?
67
+ !retriable? || retry_count >= max_retries
68
+ end
69
+
70
+ def retriable?
71
+ msg['retry']
62
72
  end
63
73
 
64
74
  def retry_count
@@ -76,6 +86,14 @@ module Sidekiq
76
86
  def default_max_retries
77
87
  Sidekiq::Middleware::Server::RetryJobs::DEFAULT_MAX_RETRY_ATTEMPTS
78
88
  end
89
+
90
+ def hostname
91
+ Socket.gethostname
92
+ end
93
+
94
+ def identity
95
+ @@identity ||= "#{hostname}:#{$$}"
96
+ end
79
97
  end
80
98
  end
81
99
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module Failures
3
- VERSION = "0.3.0"
3
+ VERSION = "0.4.0"
4
4
  end
5
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>
@@ -1,58 +1,75 @@
1
1
  <header class="row">
2
- <div class="span5">
3
- <h3>Failed Jobs</h3>
4
- </div>
5
- <div class="span4">
6
- <% if @messages.size > 0 %>
7
- <%= erb :_paging, :locals => { :url => "#{root_path}failures#@name" } %>
8
- <% end %>
2
+ <div class="col-sm-5">
3
+ <h3><%= t('FailedJobs') %></h3>
9
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') if respond_to?(:filtering) %>
10
11
  </header>
11
12
 
12
- <% if @messages.size > 0 %>
13
- <table class="table table-striped table-bordered table-white" style="width: 100%; margin: 0; table-layout:fixed;">
14
- <thead>
15
- <th style="width: 25%">Worker, Args</th>
16
- <th style="width: 10%">Queue</th>
17
- <th style="width: 15%">Failed At</th>
18
- <th style="width: 50%">Exception</th>
19
- </thead>
20
- <% @messages.each do |msg| %>
21
- <tr>
22
- <td style="overflow: hidden; text-overflow: ellipsis;">
23
- <%= msg['worker'] %>
24
- <br />
25
- <%= msg['payload']['args'].inspect[0..100] %>
26
- </td>
27
- <td><%= msg['queue'] %></td>
28
- <td>
29
- <%= relative_time(Time.parse(msg['failed_at'])) %>
30
- </td>
31
- <td style="overflow: auto; padding: 10px;">
32
- <a class="backtrace" href="#" onclick="$(this).next().toggle(); return false">
33
- <%= h msg['exception'] %>: <%= h msg['error'] %>
34
- </a>
35
- <pre style="display: none; background: none; border: 0; width: 100%; max-height: 30em; font-size: 0.8em; white-space: nowrap;">
36
- <%= msg['backtrace'].join("<br />") %>
37
- </pre>
38
- <p>
39
- <span>Processor: <%= msg['processor'] %></span>
40
- </p>
41
- </td>
42
- </tr>
43
- <% end %>
44
- </table>
45
- <div class="row">
46
- <div class="span5">
47
- <form class="form-inline" action="<%= "#{root_path}failures/remove" %>" method="post" style="margin: 20px 0">
48
- <input class="btn btn-danger btn-small" type="submit" name="delete" value="Clear All" />
49
- <label class="checkbox">
50
- <input type="checkbox" name="counter" value="true" />
51
- reset failed counter
52
- </label>
53
- </form>
54
- </div>
55
- </div>
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
+
56
73
  <% else %>
57
- <div class="alert alert-success">No failed jobs found.</div>
74
+ <div class="alert alert-success"><%= t('NoFailedJobsFound') %></div>
58
75
  <% end %>
@@ -3,19 +3,67 @@ module Sidekiq
3
3
  module WebExtension
4
4
 
5
5
  def self.registered(app)
6
- app.get "/failures" do
7
- view_path = File.join(File.expand_path("..", __FILE__), "views")
6
+ view_path = File.join(File.expand_path("..", __FILE__), "views")
8
7
 
8
+ app.get "/failures" do
9
9
  @count = (params[:count] || 25).to_i
10
- (@current_page, @total_size, @messages) = page("failed", params[:page], @count)
11
- @messages = @messages.map { |msg| Sidekiq.load_json(msg) }
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
12
 
13
13
  render(:erb, File.read(File.join(view_path, "failures.erb")))
14
14
  end
15
15
 
16
- app.post "/failures/remove" do
17
- Sidekiq::Failures.reset_failures(counter: params["counter"])
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
18
64
 
65
+ app.post "/failures/all/retry" do
66
+ FailureSet.new.retry_all_failures
19
67
  redirect "#{root_path}failures"
20
68
  end
21
69
  end
@@ -15,7 +15,7 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Sidekiq::Failures::VERSION
17
17
 
18
- gem.add_dependency "sidekiq", ">= 2.14.0"
18
+ gem.add_dependency "sidekiq", ">= 2.16.0"
19
19
 
20
20
  gem.add_development_dependency "rake"
21
21
  gem.add_development_dependency "rack-test"
@@ -148,7 +148,7 @@ module Sidekiq
148
148
  end
149
149
 
150
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' => 24, 'failures' => 'exhausted')
151
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry' => true, 'retry_count' => 25, 'failures' => 'exhausted')
152
152
 
153
153
  assert_equal 0, failures_count
154
154
 
@@ -206,7 +206,7 @@ module Sidekiq
206
206
  it "records failure if failing last retry and configured to track exhaustion by default" do
207
207
  Sidekiq.failures_default_mode = 'exhausted'
208
208
 
209
- msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry' => true, 'retry_count' => 24)
209
+ msg = create_work('class' => MockWorker.to_s, 'args' => ['myarg'], 'retry' => true, 'retry_count' => 25)
210
210
 
211
211
  assert_equal 0, failures_count
212
212
 
@@ -272,7 +272,7 @@ module Sidekiq
272
272
  end
273
273
 
274
274
  def failures_count
275
- Sidekiq.redis { |conn|conn.llen('failed') } || 0
275
+ Sidekiq.redis { |conn| conn.zcard(LIST_KEY) }
276
276
  end
277
277
 
278
278
  def create_work(msg)
@@ -46,19 +46,38 @@ module Sidekiq
46
46
  last_response.body.wont_match /No failed jobs found/
47
47
  end
48
48
 
49
- it 'has the clear all form and action' do
50
- last_response.body.must_match /failures\/remove/
51
- last_response.body.must_match /method=\"post/
52
- last_response.body.must_match /Clear All/
53
- last_response.body.must_match /reset failed counter/
49
+ it 'has the reset counter form and action' do
50
+ last_response.body.must_match /failures\/all\/reset/
51
+ last_response.body.must_match /Reset Counter/
54
52
  end
55
53
 
56
- it 'can remove all failures without clearing counter' do
54
+ it 'can reset counter' do
57
55
  assert_equal failed_count, "1"
58
56
 
59
57
  last_response.body.must_match /HardWorker/
60
58
 
61
- post '/failures/remove'
59
+ post '/failures/all/reset'
60
+ last_response.status.must_equal 302
61
+ last_response.location.must_match /failures$/
62
+
63
+ get '/failures'
64
+ last_response.status.must_equal 200
65
+ last_response.body.must_match /HardWorker/
66
+
67
+ assert_equal failed_count, "0"
68
+ end
69
+
70
+ it 'has the delete all form and action' do
71
+ last_response.body.must_match /failures\/all\/delete/
72
+ last_response.body.must_match /Delete All/
73
+ end
74
+
75
+ it 'can delete all failures' do
76
+ assert_equal failed_count, "1"
77
+
78
+ last_response.body.must_match /HardWorker/
79
+
80
+ post '/failures/all/delete'
62
81
  last_response.status.must_equal 302
63
82
  last_response.location.must_match /failures$/
64
83
 
@@ -69,37 +88,111 @@ module Sidekiq
69
88
  assert_equal failed_count, "1"
70
89
  end
71
90
 
72
- it 'can remove all failures and clear counter' do
91
+ it 'has the retry all form and action' do
92
+ last_response.body.must_match /failures\/all\/retry/
93
+ last_response.body.must_match /Retry All/
94
+ end
95
+
96
+ it 'can retry all failures' do
97
+ assert_equal failed_count, "1"
98
+
99
+ last_response.body.must_match /HardWorker/
100
+ post '/failures/all/retry'
101
+ last_response.status.must_equal 302
102
+ last_response.location.must_match /failures/
103
+
104
+ get '/failures'
105
+ last_response.status.must_equal 200
106
+ last_response.body.must_match(/No failed jobs found/)
107
+ end
108
+
109
+ it 'can delete failure from the list' do
73
110
  assert_equal failed_count, "1"
74
111
 
75
112
  last_response.body.must_match /HardWorker/
76
113
 
77
- post '/failures/remove', counter: "true"
114
+ post '/failures', { :key => [failure_score], :delete => 'Delete' }
78
115
  last_response.status.must_equal 302
79
- last_response.location.must_match /failures$/
116
+ last_response.location.must_match /failures/
80
117
 
81
118
  get '/failures'
82
119
  last_response.status.must_equal 200
83
120
  last_response.body.must_match /No failed jobs found/
121
+ end
84
122
 
85
- assert_equal failed_count, "0"
123
+ it 'can retry failure from the list' do
124
+ assert_equal failed_count, "1"
125
+
126
+ last_response.body.must_match /HardWorker/
127
+
128
+ post '/failures', { :key => [failure_score], :retry => 'Retry Now' }
129
+ last_response.status.must_equal 302
130
+ last_response.location.must_match /failures/
131
+
132
+ get '/failures'
133
+ last_response.status.must_equal 200
134
+ last_response.body.must_match /No failed jobs found/
135
+ end
136
+ end
137
+
138
+ describe 'when there is failure' do
139
+ before do
140
+ create_sample_failure
141
+ get "/failures/#{failure_score}"
142
+ end
143
+
144
+ it 'should be successful' do
145
+ last_response.status.must_equal 200
146
+ end
147
+
148
+ it 'can display failure page' do
149
+ last_response.body.must_match /Job/
150
+ last_response.body.must_match /HardWorker/
151
+ last_response.body.must_match /ArgumentError/
152
+ last_response.body.must_match /file1/
153
+ end
154
+
155
+ it 'can delete failure' do
156
+ last_response.body.must_match /HardWorker/
157
+
158
+ post "/failures/#{failure_score}", :delete => 'Delete'
159
+ last_response.status.must_equal 302
160
+ last_response.location.must_match /failures/
161
+
162
+ get "/failures/#{failure_score}"
163
+ last_response.status.must_equal 302
164
+ last_response.location.must_match /failures/
165
+ end
166
+
167
+ it 'can retry failure' do
168
+ last_response.body.must_match /HardWorker/
169
+
170
+ post "/failures/#{failure_score}", :retry => 'Retry Now'
171
+ last_response.status.must_equal 302
172
+ last_response.location.must_match /failures/
173
+
174
+ get "/failures/#{failure_score}"
175
+ last_response.status.must_equal 302
176
+ last_response.location.must_match /failures/
86
177
  end
87
178
  end
88
179
 
89
180
  def create_sample_failure
90
181
  data = {
91
- :failed_at => Time.now.strftime("%Y/%m/%d %H:%M:%S %Z"),
92
- :payload => { :args => ["bob", 5] },
93
- :exception => "ArgumentError",
94
- :error => "Some new message",
95
- :backtrace => ["path/file1.rb", "path/file2.rb"],
96
- :worker => 'HardWorker',
97
- :queue => 'default'
182
+ :queue => 'default',
183
+ :class => 'HardWorker',
184
+ :args => ['bob', 5],
185
+ :jid => 1,
186
+ :enqueued_at => Time.now.utc.to_f,
187
+ :failed_at => Time.now.utc.to_f,
188
+ :error_class => 'ArgumentError',
189
+ :error_message => 'Some new message',
190
+ :error_backtrace => ["path/file1.rb", "path/file2.rb"]
98
191
  }
99
192
 
100
193
  Sidekiq.redis do |c|
101
194
  c.multi do
102
- c.rpush("failed", Sidekiq.dump_json(data))
195
+ c.zadd(Sidekiq::Failures::LIST_KEY, failure_score, Sidekiq.dump_json(data))
103
196
  c.set("stat:failed", 1)
104
197
  end
105
198
  end
@@ -108,5 +201,9 @@ module Sidekiq
108
201
  def failed_count
109
202
  Sidekiq.redis { |c| c.get("stat:failed") }
110
203
  end
204
+
205
+ def failure_score
206
+ Time.at(1).to_f
207
+ end
111
208
  end
112
209
  end
metadata CHANGED
@@ -1,83 +1,83 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-failures
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcelo Silveira
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-12-28 00:00:00.000000000 Z
11
+ date: 2014-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 2.14.0
19
+ version: 2.16.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 2.14.0
26
+ version: 2.16.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rack-test
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '>='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: sprockets
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - '>='
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - '>='
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: sinatra
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - '>='
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - '>='
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  description: Keep track of Sidekiq failed jobs
@@ -87,18 +87,21 @@ executables: []
87
87
  extensions: []
88
88
  extra_rdoc_files: []
89
89
  files:
90
- - .gitignore
91
- - .travis.yml
90
+ - ".gitignore"
91
+ - ".travis.yml"
92
92
  - CHANGELOG.md
93
93
  - Gemfile
94
- - Gemfile.lock
95
94
  - LICENSE
96
95
  - README.md
97
96
  - Rakefile
98
97
  - lib/sidekiq-failures.rb
99
98
  - lib/sidekiq/failures.rb
99
+ - lib/sidekiq/failures/failure_set.rb
100
+ - lib/sidekiq/failures/locales/en.yml
100
101
  - lib/sidekiq/failures/middleware.rb
102
+ - lib/sidekiq/failures/sorted_entry.rb
101
103
  - lib/sidekiq/failures/version.rb
104
+ - lib/sidekiq/failures/views/failure.erb
102
105
  - lib/sidekiq/failures/views/failures.erb
103
106
  - lib/sidekiq/failures/web_extension.rb
104
107
  - sidekiq-failures.gemspec
@@ -114,17 +117,17 @@ require_paths:
114
117
  - lib
115
118
  required_ruby_version: !ruby/object:Gem::Requirement
116
119
  requirements:
117
- - - '>='
120
+ - - ">="
118
121
  - !ruby/object:Gem::Version
119
122
  version: '0'
120
123
  required_rubygems_version: !ruby/object:Gem::Requirement
121
124
  requirements:
122
- - - '>='
125
+ - - ">="
123
126
  - !ruby/object:Gem::Version
124
127
  version: '0'
125
128
  requirements: []
126
129
  rubyforge_project:
127
- rubygems_version: 2.0.3
130
+ rubygems_version: 2.2.2
128
131
  signing_key:
129
132
  specification_version: 4
130
133
  summary: Keeps track of Sidekiq failed jobs and adds a tab to the Web UI to let you
data/Gemfile.lock DELETED
@@ -1,51 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- sidekiq-failures (0.3.0)
5
- sidekiq (>= 2.14.0)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- celluloid (0.15.2)
11
- timers (~> 1.1.0)
12
- connection_pool (1.2.0)
13
- hike (1.2.1)
14
- json (1.8.1)
15
- multi_json (1.7.3)
16
- rack (1.4.1)
17
- rack-protection (1.2.0)
18
- rack
19
- rack-test (0.6.2)
20
- rack (>= 1.0)
21
- rake (0.9.2.2)
22
- redis (3.0.6)
23
- redis-namespace (1.3.2)
24
- redis (~> 3.0.4)
25
- sidekiq (2.17.0)
26
- celluloid (>= 0.15.2)
27
- connection_pool (>= 1.0.0)
28
- json
29
- redis (>= 3.0.4)
30
- redis-namespace (>= 1.3.1)
31
- sinatra (1.3.3)
32
- rack (~> 1.3, >= 1.3.6)
33
- rack-protection (~> 1.2)
34
- tilt (~> 1.3, >= 1.3.3)
35
- sprockets (2.8.1)
36
- hike (~> 1.2)
37
- multi_json (~> 1.0)
38
- rack (~> 1.0)
39
- tilt (~> 1.1, != 1.3.0)
40
- tilt (1.3.3)
41
- timers (1.1.0)
42
-
43
- PLATFORMS
44
- ruby
45
-
46
- DEPENDENCIES
47
- rack-test
48
- rake
49
- sidekiq-failures!
50
- sinatra
51
- sprockets