sidekiq-failures 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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