sidekiq-undertaker 1.0.0.rc01

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +6 -0
  3. data/.gitignore +22 -0
  4. data/.rubocop.yml +5 -0
  5. data/.rubocop_todo.yml +23 -0
  6. data/.travis.yml +53 -0
  7. data/Demo_Filter.png +0 -0
  8. data/Demo_Job_Filter.png +0 -0
  9. data/Demo_Morgue_1_Job.png +0 -0
  10. data/Demo_Morgue_all.png +0 -0
  11. data/Gemfile +11 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +95 -0
  14. data/Rakefile +34 -0
  15. data/lib/sidekiq/undertaker.rb +16 -0
  16. data/lib/sidekiq/undertaker/bucket.rb +29 -0
  17. data/lib/sidekiq/undertaker/dead_job.rb +64 -0
  18. data/lib/sidekiq/undertaker/job_distributor.rb +64 -0
  19. data/lib/sidekiq/undertaker/job_filter.rb +46 -0
  20. data/lib/sidekiq/undertaker/version.rb +7 -0
  21. data/lib/sidekiq/undertaker/web_extension.rb +37 -0
  22. data/lib/sidekiq/undertaker/web_extension/api_helpers.rb +120 -0
  23. data/sidekiq-undertaker.gemspec +55 -0
  24. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_filter_page_is_called/behaves_like_a_page/the_displayed_page_is_correct.approved.txt +240 -0
  25. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_job_classbucket_page_is_called/behaves_like_a_page/the_displayed_page_is_correct.approved.txt +241 -0
  26. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_filter/when_job_classbucket_page_is_polled/behaves_like_a_page/the_displayed_page_is_correct.approved.txt +241 -0
  27. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_all_failures_and_errors/behaves_like_a_page/the_displayed_page_is_correct.approved.txt +316 -0
  28. data/spec/fixtures/approvals/sidekiq_undertaker_webextension/show_morgue/when_job_classerrorbucket_is_called/with_specific_job_class_and_a_specific_error/behaves_like_a_page/the_displayed_page_is_correct.approved.txt +274 -0
  29. data/spec/sidekiq/undertaker/bucket_spec.rb +26 -0
  30. data/spec/sidekiq/undertaker/dead_jobs_spec.rb +90 -0
  31. data/spec/sidekiq/undertaker/job_distributor_spec.rb +104 -0
  32. data/spec/sidekiq/undertaker/job_filter_spec.rb +105 -0
  33. data/spec/sidekiq/undertaker/web_extension_spec.rb +270 -0
  34. data/spec/spec_helper.rb +76 -0
  35. data/web/locales/en.yml +5 -0
  36. data/web/views/filter.erb +34 -0
  37. data/web/views/filter_job_class.erb +35 -0
  38. data/web/views/morgue.erb +75 -0
  39. metadata +378 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 11a061d4335309c54a17ef8f34fc207c49a36ad772f74a145b15482d2dc4446e
4
+ data.tar.gz: b82c5d0d70efd96d4e32e9e1ef6e402f2ccce912bfe50ad4b7ea1ddf97b1c463
5
+ SHA512:
6
+ metadata.gz: dba278326f094e4a3498f727791bc484e2e960be1d751f044a209a8f18583890e3cae95368692618eff8d4b315046806d94c24887a24b64b1b000d09f4f8f0d9
7
+ data.tar.gz: 48b3d817160a12606fec4a7f160c7b29062aca67bebcafc54ba8953a668c75ecf9b3cacf81f8e7315a2fd5abfa5a723d41e70e5b7b6fb50fd234bc7b5cb148fa
@@ -0,0 +1,6 @@
1
+ version: "2"
2
+ plugins:
3
+ git-legal:
4
+ enabled: true
5
+ # rubocop:
6
+ # enabled: true
@@ -0,0 +1,22 @@
1
+ _yardoc
2
+ .approvals
3
+ .bundle
4
+ .config
5
+ .idea/
6
+ .ruby-version
7
+ .rvmrc
8
+ .yardoc
9
+ *.gem
10
+ *.rbc
11
+ *.received.*
12
+ coverage
13
+ doc/
14
+ Gemfile.lock
15
+ InstalledFiles
16
+ lib/bundler/man
17
+ pkg
18
+ rdoc
19
+ spec/reports
20
+ test/tmp
21
+ test/version_tmp
22
+ tmp
@@ -0,0 +1,5 @@
1
+ require:
2
+ - rubocop-rspec
3
+ - rt_rubocop_defaults
4
+
5
+ inherit_from: .rubocop_todo.yml
@@ -0,0 +1,23 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2019-12-06 21:33:12 +0100 using RuboCop version 0.77.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 2
10
+ # Configuration parameters: Severity.
11
+ Metrics/AbcSize:
12
+ Max: 22
13
+
14
+ # Offense count: 3
15
+ # Configuration parameters: CountComments, ExcludedMethods, Severity.
16
+ Metrics/MethodLength:
17
+ Max: 19
18
+
19
+ # Offense count: 5
20
+ # Configuration parameters: .
21
+ # SupportedStyles: have_received, receive
22
+ RSpec/MessageSpies:
23
+ EnforcedStyle: receive
@@ -0,0 +1,53 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.4.9
5
+ - 2.5.7
6
+ - 2.6.5
7
+ - jruby-9.1.17.0
8
+ - jruby-9.2.9.0
9
+ jdk:
10
+ - oraclejdk11
11
+ env:
12
+ global:
13
+ - CC_TEST_REPORTER_ID=9fd6c503f8bb89483410c1fc0578e5845c3fe0d3e773a32eb2788713fb101a93
14
+ matrix:
15
+ - "JRUBY_OPTS='--dev --debug'"
16
+ - "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
17
+
18
+ matrix:
19
+ exclude:
20
+ - rvm: 2.4.9
21
+ jdk: oraclejdk8
22
+ env: "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
23
+ - rvm: 2.5.7
24
+ jdk: oraclejdk8
25
+ env: "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
26
+ - rvm: 2.6.5
27
+ jdk: oraclejdk8
28
+ env: "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
29
+ - rvm: truffleruby
30
+ jdk: oraclejdk8
31
+ env: "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
32
+ allow_failures:
33
+ - rvm: jruby-9.1.17.0
34
+ env: "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
35
+ - rvm: jruby-9.2.9.0
36
+ env: "JRUBY_OPTS='-Xcompile.invokedynamic=true --debug'"
37
+
38
+ services:
39
+ - redis-server
40
+
41
+ before_install:
42
+ - gem update --system
43
+ - gem install bundler -v "~> 1.0"
44
+ - gem install bundler -v "~> 2.0"
45
+
46
+ before_script:
47
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
48
+ - chmod +x ./cc-test-reporter
49
+ - ./cc-test-reporter before-build
50
+ script:
51
+ - COVERAGE=true bundle exec rspec
52
+ after_script:
53
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
Binary file
Binary file
Binary file
Binary file
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in sidekiq-undertaker.gemspec
6
+ gemspec
7
+
8
+ # Good for debuging on travis-ci.org
9
+ # group :development, :test do
10
+ # gem "approvals", git: "https://github.com/br/approvals", branch: "diff-preview"
11
+ # end
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Thomas Koppensteiner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,95 @@
1
+ # Sidekiq::Undertaker
2
+
3
+ [![Build Status](https://travis-ci.org/ThomasKoppensteiner/sidekiq-under.svg?branch=master)](https://travis-ci.org/ThomasKoppensteiner/sidekiq-undertaker)
4
+ [![Code Climate](https://codeclimate.com/github/ThomasKoppensteiner/sidekiq-undertaker.svg)](https://codeclimate.com/github/ThomasKoppensteiner/sidekiq-undertaker)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/9d75b8b7d3ff076e1ef1/test_coverage)](https://codeclimate.com/github/ThomasKoppensteiner/sidekiq-undertaker/test_coverage)
6
+
7
+ ## About
8
+
9
+ Sidekiq::Undertaker is a plugin for [Sidekiq](https://rubygems.org/gems/sidekiq).
10
+ It allows exploring, reviving (retrying) or burying (deleting) dead jobs.
11
+ For easy exploring the dead-jobs queue is broken down into time windows (buckets) of hours, days and weeks.
12
+
13
+ ## Installation
14
+
15
+ #### Install the Gem
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ````ruby
20
+ gem "sidekiq-undertaker"
21
+ ````
22
+
23
+ And then execute:
24
+ ````sh
25
+ $ bundle
26
+ ````
27
+
28
+ Or install it yourself as:
29
+
30
+ ````sh
31
+ $ gem install sidekiq-undertaker
32
+ ````
33
+
34
+ #### Install the Rubocop Pre-Commit Hook
35
+
36
+ ````sh
37
+ $ rake rubocop:install
38
+ ````
39
+
40
+ ## Impressions
41
+
42
+ #### Filter View
43
+
44
+ The filter page shows a table with time-buckets as columns and rows for each job class.
45
+
46
+ ![Sidekiq Undertaker](Demo_Filter.png)
47
+
48
+ #### Job Filter View
49
+
50
+ For each job class, you can drill down to view error distribution based on
51
+ error class.
52
+
53
+ ![Sidekiq Undertaker](Demo_Job_Filter.png)
54
+
55
+ #### Morgue View
56
+ Finally, click on the individual error counts to display details of the
57
+ errors in a list form.
58
+
59
+ ![Sidekiq Undertaker](Demo_Morgue_1_Job.png)
60
+
61
+ The morgue view can, for example, also show an error distribution over all job classes.
62
+
63
+ ![Sidekiq Undertaker](Demo_Morgue_all.png)
64
+
65
+ ## Contributing
66
+
67
+ 1. Fork it ( https://github.com/ThomasKoppensteiner/sidekiq-undertaker/fork )
68
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
69
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
70
+ 4. Push to the branch (`git push origin my-new-feature`)
71
+ 5. Create new Pull Request
72
+
73
+ ## Naming
74
+
75
+ As another gem with the name `sidekiq-cleaner` is already released on rubygems.org,
76
+ this fork was renamed to `sidekiq-undertaker`.
77
+
78
+ ## Thanks
79
+
80
+ The [Sidekiq-Cleaner](https://github.com/HackingHabits/sidekiq-cleaner) gem was originally created by [Madan Thangavelu](https://github.com/HackingHabits).
81
+ [Tout](https://github.com/Tout/sidekiq-cleaner) and [TheWudu](https://github.com/TheWudu/sidekiq-cleaner) also contributed to it.
82
+ For the complete list of network members have a look at the [fork overview](https://github.com/ThomasKoppensteiner/sidekiq-under/network/members).
83
+
84
+ ## Alternative Projects
85
+
86
+ * [sidekiq-cleaner](https://rubygems.org/gems/sidekiq-cleaner)
87
+ * [sidekiq_cleaner](https://rubygems.org/gems/sidekiq_cleaner)
88
+
89
+ ## Author
90
+
91
+ Thomas Koppensteiner | [Github](https://github.com/ThomasKoppensteiner) | [RubyGems](https://rubygems.org/profiles/thomaskoppensteiner) | [@koppensteiner_t](https://twitter.com/koppensteiner_t)
92
+
93
+ ## License
94
+
95
+ See the [License](https://github.com/ThomasKoppensteiner/sidekiq-under/blob/master/LICENSE.txt) file.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ $LOAD_PATH.unshift("lib")
6
+
7
+ desc "setup gem for development"
8
+ task :init do
9
+ Rake::Task["rubocop:install"].execute
10
+ end
11
+
12
+ #
13
+ # RubocopRunner
14
+ #
15
+ begin
16
+ require "rubocop_runner/rake_task"
17
+ RubocopRunner::RakeTask.new
18
+ rescue LoadError
19
+ puts "RubocopRunner not available!"
20
+ end
21
+
22
+ #
23
+ # RSpec
24
+ #
25
+ begin
26
+ require "rspec/core/rake_task"
27
+
28
+ RSpec::Core::RakeTask.new(:spec)
29
+
30
+ task default: :spec
31
+ task test: :spec
32
+ rescue LoadError
33
+ puts "RSpec not available!"
34
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/undertaker/version"
4
+ require "sidekiq/undertaker/web_extension"
5
+
6
+ begin
7
+ require "sidekiq/web"
8
+ rescue LoadError # rubocop:disable Lint/SuppressedException
9
+ # client-only usage
10
+ end
11
+
12
+ if defined?(Sidekiq::Web)
13
+ Sidekiq::Web.register Sidekiq::Undertaker::WebExtension
14
+ Sidekiq::Web.tabs["Undertaker"] = "undertaker/filter"
15
+ Sidekiq::Web.settings.locales << File.join(File.dirname(__FILE__), "../../web/locales")
16
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Undertaker
5
+ class Bucket
6
+ class << self
7
+ def bucket_names
8
+ %w[1_hour 3_hours 1_day 3_days 1_week older]
9
+ end
10
+
11
+ def for_elapsed_time(elapsed_time)
12
+ return "1_hour" if elapsed_time <= ONE_HOUR
13
+ return "3_hours" if elapsed_time <= THREE_HOURS
14
+ return "1_day" if elapsed_time <= ONE_DAY
15
+ return "3_days" if elapsed_time <= THREE_DAYS
16
+ return "1_week" if elapsed_time <= ONE_WEEK
17
+
18
+ "older"
19
+ end
20
+
21
+ ONE_HOUR = 60 * 60 * 1
22
+ THREE_HOURS = ONE_HOUR * 3
23
+ ONE_DAY = ONE_HOUR * 24
24
+ THREE_DAYS = ONE_DAY * 3
25
+ ONE_WEEK = ONE_DAY * 7
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/undertaker/bucket"
4
+
5
+ module Sidekiq
6
+ module Undertaker
7
+ class DeadJob
8
+ class << self
9
+ def for_each
10
+ Sidekiq::DeadSet.new.each do |job|
11
+ yield to_dead_job(job)
12
+ end
13
+ end
14
+
15
+ def to_dead_job(job)
16
+ job_failed_at = parsed_failed_at(job)
17
+ job_time_elapsed_since_failure = Time.now.to_i - job_failed_at.to_i
18
+ job_bucket_name = Bucket.for_elapsed_time(job_time_elapsed_since_failure)
19
+
20
+ new(job: job,
21
+ time_elapsed_since_failure: job_time_elapsed_since_failure,
22
+ bucket_name: job_bucket_name)
23
+ end
24
+
25
+ def parsed_failed_at(job)
26
+ job.item["failed_at"].is_a?(String) ? Time.parse(job.item["failed_at"]) : job.item["failed_at"]
27
+ end
28
+ end
29
+
30
+ attr_reader :job_class, :time_elapsed_since_failure, :error_class, :bucket_name, :job
31
+
32
+ def initialize(args)
33
+ @job = args.fetch(:job)
34
+ @time_elapsed_since_failure = args.fetch(:time_elapsed_since_failure)
35
+ @bucket_name = args.fetch(:bucket_name)
36
+ @job_class = args.fetch(:job_class, job.item["class"])
37
+ @error_class = args.fetch(:error_class, job.item["error_class"])
38
+ end
39
+
40
+ def ==(other)
41
+ job_class == other.job_class &&
42
+ time_elapsed_since_failure == other.time_elapsed_since_failure &&
43
+ error_class == other.error_class &&
44
+ bucket_name == other.bucket_name &&
45
+ job_eql?(other.job)
46
+ end
47
+ alias eql? ==
48
+
49
+ private
50
+
51
+ attr_writer :job_class, :time_elapsed_since_failure, :error_class, :bucket_name, :job
52
+
53
+ def job_eql?(other_job) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
54
+ job.jid == other_job.jid &&
55
+ job.item == other_job.item &&
56
+ job.args == other_job.args &&
57
+ job.queue == other_job.queue &&
58
+ job.score == other_job.score &&
59
+ job.parent.name == other_job.parent.name &&
60
+ job.parent.size == other_job.parent.size
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/undertaker/bucket"
4
+
5
+ module Sidekiq
6
+ module Undertaker
7
+ class JobDistributor
8
+ attr_reader :dead_jobs
9
+
10
+ def initialize(dead_jobs)
11
+ @dead_jobs = dead_jobs
12
+ end
13
+
14
+ def group_by_job_class
15
+ group_by(:job_class)
16
+ end
17
+
18
+ def group_by_error_class
19
+ group_by(:error_class)
20
+ end
21
+
22
+ private
23
+
24
+ def distribute
25
+ distribution = init_counters({}, "all")
26
+
27
+ dead_jobs.each do |dead_job|
28
+ bucket_name = dead_job.bucket_name
29
+
30
+ incr_counters(distribution, "all", bucket_name)
31
+ yield distribution, dead_job, bucket_name if block_given?
32
+ end
33
+
34
+ sort_by_total_dead(distribution)
35
+ end
36
+
37
+ def group_by(attribute)
38
+ distribute do |distribution, dead_job, bucket_name|
39
+ attr = dead_job.public_send(attribute)
40
+ init_counters(distribution, attr) unless distribution[attr]
41
+ incr_counters(distribution, attr, bucket_name)
42
+ end
43
+ end
44
+
45
+ def sort_by_total_dead(distribution)
46
+ distribution.map { |k, v| [k, v] }.sort do |x, y|
47
+ x[1]["total_dead"] <=> y[1]["total_dead"]
48
+ end.reverse
49
+ end
50
+
51
+ def init_counters(distribution, attr)
52
+ distribution[attr] = {}
53
+ Bucket.bucket_names.each { |bucket| distribution[attr][bucket] = 0 }
54
+ distribution[attr]["total_dead"] = 0
55
+ distribution
56
+ end
57
+
58
+ def incr_counters(distribution, attr, bucket_name, value = 1)
59
+ distribution[attr][bucket_name] += value
60
+ distribution[attr]["total_dead"] += value
61
+ end
62
+ end
63
+ end
64
+ end