sidekiq-status 3.0.3 → 4.0.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +2 -0
  3. data/.devcontainer/README.md +57 -0
  4. data/.devcontainer/devcontainer.json +55 -0
  5. data/.devcontainer/docker-compose.yml +19 -0
  6. data/.github/workflows/ci.yaml +9 -6
  7. data/Appraisals +14 -6
  8. data/CHANGELOG.md +12 -0
  9. data/Dockerfile +5 -0
  10. data/README.md +756 -41
  11. data/Rakefile +153 -0
  12. data/docker-compose.yml +15 -0
  13. data/gemfiles/{sidekiq_6.1.gemfile → sidekiq_7.0.gemfile} +1 -1
  14. data/gemfiles/sidekiq_7.3.gemfile +7 -0
  15. data/gemfiles/sidekiq_8.0.gemfile +7 -0
  16. data/gemfiles/{sidekiq_6.x.gemfile → sidekiq_8.x.gemfile} +1 -1
  17. data/lib/sidekiq-status/client_middleware.rb +4 -3
  18. data/lib/sidekiq-status/helpers.rb +94 -0
  19. data/lib/sidekiq-status/server_middleware.rb +6 -21
  20. data/lib/sidekiq-status/storage.rb +12 -3
  21. data/lib/sidekiq-status/version.rb +1 -1
  22. data/lib/sidekiq-status/web.rb +67 -93
  23. data/lib/sidekiq-status/worker.rb +6 -10
  24. data/lib/sidekiq-status.rb +21 -5
  25. data/sidekiq-status.gemspec +7 -1
  26. data/spec/environment.rb +12 -1
  27. data/spec/lib/sidekiq-status/client_middleware_spec.rb +8 -0
  28. data/spec/lib/sidekiq-status/server_middleware_spec.rb +13 -0
  29. data/spec/lib/sidekiq-status/web_spec.rb +72 -3
  30. data/spec/lib/sidekiq-status/worker_spec.rb +3 -3
  31. data/spec/lib/sidekiq-status_spec.rb +20 -3
  32. data/spec/spec_helper.rb +3 -8
  33. data/spec/support/test_jobs.rb +11 -0
  34. data/spec/test_environment.rb +1 -0
  35. data/web/assets/statuses.css +124 -0
  36. data/web/assets/statuses.js +24 -0
  37. data/web/views/status.erb +131 -93
  38. data/web/views/status_not_found.erb +1 -1
  39. data/web/views/statuses.erb +23 -79
  40. metadata +93 -14
data/Rakefile CHANGED
@@ -9,3 +9,156 @@ RSpec::Core::RakeTask.new(:spec)
9
9
  task :test => :spec
10
10
 
11
11
  task :default => :spec
12
+
13
+ desc "Launch a minimal server with Sidekiq UI at /sidekiq"
14
+ task :server do
15
+ require 'webrick'
16
+ require 'rack'
17
+ require 'rack/session'
18
+ require 'stringio'
19
+ require 'sidekiq'
20
+ require 'sidekiq/web'
21
+ require 'sidekiq-status'
22
+
23
+ # Create a Rack application
24
+ app = Rack::Builder.new do
25
+ # Add session middleware for Sidekiq::Web CSRF protection
26
+ use Rack::Session::Cookie,
27
+ secret: ENV['SESSION_SECRET'] || 'development_secret_key_that_is_definitely_long_enough_for_rack_session_cookie_middleware',
28
+ same_site: true,
29
+ max_age: 86400
30
+
31
+ map "/sidekiq" do
32
+ run Sidekiq::Web
33
+ end
34
+
35
+ map "/" do
36
+ run lambda { |env|
37
+ [
38
+ 200,
39
+ { 'Content-Type' => 'text/html' },
40
+ [<<~HTML
41
+ <!DOCTYPE html>
42
+ <html>
43
+ <head>
44
+ <title>Sidekiq Status Server</title>
45
+ </head>
46
+ <body>
47
+ <h1>Sidekiq Status Server</h1>
48
+ <p>The Sidekiq web interface is available at <a href="/sidekiq">/sidekiq</a></p>
49
+ </body>
50
+ </html>
51
+ HTML
52
+ ]
53
+ ]
54
+ }
55
+ end
56
+ end
57
+
58
+ puts "Starting server on http://localhost:9292"
59
+ puts "Sidekiq web interface available at http://localhost:9292/sidekiq"
60
+ puts "Press Ctrl+C to stop the server"
61
+
62
+ # Use WEBrick with a proper Rack handler
63
+ server = WEBrick::HTTPServer.new(Port: 9292, Host: '0.0.0.0')
64
+
65
+ # Mount the Rack app properly
66
+ server.mount_proc '/' do |req, res|
67
+ begin
68
+ # Construct proper Rack environment
69
+ env = {
70
+ 'REQUEST_METHOD' => req.request_method,
71
+ 'PATH_INFO' => req.path_info || req.path,
72
+ 'QUERY_STRING' => req.query_string || '',
73
+ 'REQUEST_URI' => req.request_uri.to_s,
74
+ 'HTTP_HOST' => req.host,
75
+ 'SERVER_NAME' => req.host,
76
+ 'SERVER_PORT' => req.port.to_s,
77
+ 'SCRIPT_NAME' => '',
78
+ 'rack.input' => StringIO.new(req.body || ''),
79
+ 'rack.errors' => $stderr,
80
+ 'rack.version' => [1, 3],
81
+ 'rack.url_scheme' => 'http',
82
+ 'rack.multithread' => true,
83
+ 'rack.multiprocess' => false,
84
+ 'rack.run_once' => false
85
+ }
86
+
87
+ # Add request headers to environment
88
+ req.header.each do |key, values|
89
+ env_key = key.upcase.tr('-', '_')
90
+ env_key = "HTTP_#{env_key}" unless %w[CONTENT_TYPE CONTENT_LENGTH].include?(env_key)
91
+ env[env_key] = values.first if values.any?
92
+ end
93
+
94
+ # Call the Rack app
95
+ status, headers, body = app.call(env)
96
+
97
+ # Set response
98
+ res.status = status
99
+ headers.each { |k, v| res[k] = v } if headers
100
+
101
+ # Handle response body
102
+ if body.respond_to?(:each)
103
+ body_content = ""
104
+ body.each { |chunk| body_content << chunk.to_s }
105
+ res.body = body_content
106
+ else
107
+ res.body = body.to_s
108
+ end
109
+
110
+ rescue => e
111
+ res.status = 500
112
+ res['Content-Type'] = 'text/plain'
113
+ res.body = "Internal Server Error: #{e.message}"
114
+ puts "Error: #{e.message}\n#{e.backtrace.join("\n")}"
115
+ end
116
+ end
117
+
118
+ trap('INT') { server.shutdown }
119
+
120
+ begin
121
+ server.start
122
+ rescue Interrupt
123
+ puts "\nServer stopped."
124
+ end
125
+ end
126
+
127
+ desc "Starts an IRB session with Sidekiq, Sidekiq::Status, and the testing jobs loaded"
128
+ task :irb do
129
+ require 'irb'
130
+ require 'sidekiq-status'
131
+ require_relative 'spec/support/test_jobs'
132
+
133
+ Sidekiq.configure_server do |config|
134
+ Sidekiq::Status.configure_server_middleware config
135
+ end
136
+
137
+ # Configure Sidekiq if needed
138
+ Sidekiq.configure_client do |config|
139
+ Sidekiq::Status.configure_client_middleware config
140
+ config.redis = { url: ENV['REDIS_URL'] || 'redis://localhost:6379' }
141
+ end
142
+
143
+ puts "="*60
144
+ puts "IRB Session with Sidekiq Status"
145
+ puts ""
146
+ puts "To launch a sidekiq worker, run:"
147
+ puts " bundle exec sidekiq -r ./spec/environment.rb"
148
+ puts ""
149
+ puts "="*60
150
+ puts "Available job classes:"
151
+ puts " StubJob, LongJob, DataJob, ProgressJob,"
152
+ puts " FailingJob, ExpiryJob, etc."
153
+ puts ""
154
+ puts "Example usage:"
155
+ puts " job_id = StubJob.perform_async"
156
+ puts " job_id = LongJob.perform_async(0.5)"
157
+ puts " Sidekiq::Status.status(job_id)"
158
+ puts " Sidekiq::Status.get_all"
159
+ puts "="*60
160
+ puts ""
161
+
162
+ ARGV.clear # Clear ARGV to prevent IRB from trying to parse them
163
+ IRB.start
164
+ end
@@ -0,0 +1,15 @@
1
+ # Run the test suite with docker compose
2
+ services:
3
+ sidekiq-status:
4
+ build: .
5
+ environment:
6
+ - REDIS_URL=redis://redis
7
+ volumes:
8
+ - .:/app
9
+ working_dir: /app
10
+ command: bundle exec appraisal rake
11
+ depends_on:
12
+ - redis
13
+
14
+ redis:
15
+ image: redis:7.4.0
@@ -2,6 +2,6 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "sidekiq", "~> 6.1"
5
+ gem "sidekiq", "~> 7.0.0"
6
6
 
7
7
  gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sidekiq", "~> 7.3.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sidekiq", "~> 8.0.0"
6
+
7
+ gemspec path: "../"
@@ -2,6 +2,6 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "sidekiq", "~> 6"
5
+ gem "sidekiq", "~> 8"
6
6
 
7
7
  gemspec path: "../"
@@ -22,7 +22,7 @@ module Sidekiq::Status
22
22
  def call(worker_class, msg, queue, redis_pool=nil)
23
23
 
24
24
  # Determine the actual job class
25
- klass = msg["args"][0]["job_class"] || worker_class rescue worker_class
25
+ klass = (!msg["args"][0].is_a?(String) && msg["args"][0]["job_class"]) || worker_class rescue worker_class
26
26
  job_class = if klass.is_a?(Class)
27
27
  klass
28
28
  elsif Module.const_defined?(klass)
@@ -37,7 +37,8 @@ module Sidekiq::Status
37
37
  jid: msg['jid'],
38
38
  status: :queued,
39
39
  worker: JOB_CLASS.new(msg, queue).display_class,
40
- args: display_args(msg, queue)
40
+ args: display_args(msg, queue),
41
+ enqueued_at: Time.now.to_i
41
42
  }
42
43
  store_for_id msg['jid'], initial_metadata, job_class.new.expiration || @expiration, redis_pool
43
44
  end
@@ -57,7 +58,7 @@ module Sidekiq::Status
57
58
 
58
59
  # Helper method to easily configure sidekiq-status client middleware
59
60
  # whatever the Sidekiq version is.
60
- # @param [Sidekiq] sidekiq_config the Sidekiq config
61
+ # @param [Sidekiq::Config] sidekiq_config the Sidekiq config
61
62
  # @param [Hash] client_middleware_options client middleware initialization options
62
63
  # @option client_middleware_options [Fixnum] :expiration ttl for complete jobs
63
64
  def self.configure_client_middleware(sidekiq_config, client_middleware_options = {})
@@ -0,0 +1,94 @@
1
+ module Sidekiq::Status
2
+ module Web
3
+ module Helpers
4
+ COMMON_STATUS_HASH_KEYS = %w(enqueued_at started_at updated_at ended_at jid status worker args label pct_complete total at message elapsed eta)
5
+
6
+ def safe_url_params(key)
7
+ return url_params(key) if Sidekiq.major_version >= 8
8
+ request.params[key.to_s]
9
+ end
10
+
11
+ def safe_route_params(key)
12
+ return route_params(key) if Sidekiq.major_version >= 8
13
+ env["rack.route_params"][key.to_sym]
14
+ end
15
+
16
+ def csrf_tag
17
+ "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
18
+ end
19
+
20
+ def poll_path
21
+ "?#{request.query_string}" if safe_url_params("poll")
22
+ end
23
+
24
+ def sidekiq_status_template(name)
25
+ path = File.join(VIEW_PATH, name.to_s) + ".erb"
26
+ File.open(path).read
27
+ end
28
+
29
+ def add_details_to_status(status)
30
+ status['label'] = status_label(status['status'])
31
+ status["pct_complete"] ||= pct_complete(status)
32
+ status["elapsed"] ||= elapsed(status).to_s
33
+ status["eta"] ||= eta(status).to_s
34
+ status["custom"] = process_custom_data(status)
35
+ return status
36
+ end
37
+
38
+ def process_custom_data(hash)
39
+ hash.reject { |key, _| COMMON_STATUS_HASH_KEYS.include?(key) }
40
+ end
41
+
42
+ def pct_complete(status)
43
+ return 100 if status['status'] == 'complete'
44
+ Sidekiq::Status::pct_complete(status['jid']) || 0
45
+ end
46
+
47
+ def elapsed(status)
48
+ started = Sidekiq::Status.started_at(status['jid'])
49
+ return nil unless started
50
+ case status['status']
51
+ when 'complete', 'failed', 'stopped', 'interrupted'
52
+ ended = Sidekiq::Status.ended_at(status['jid'])
53
+ return nil unless ended
54
+ ended - started
55
+ when 'working', 'retrying'
56
+ Time.now.to_i - started
57
+ end
58
+ end
59
+
60
+ def eta(status)
61
+ Sidekiq::Status.eta(status['jid']) if status['status'] == 'working'
62
+ end
63
+
64
+ def status_label(status)
65
+ case status
66
+ when 'complete'
67
+ 'success'
68
+ when 'working', 'retrying'
69
+ 'warning'
70
+ when 'queued'
71
+ 'primary'
72
+ else
73
+ 'danger'
74
+ end
75
+ end
76
+
77
+ def has_sort_by?(value)
78
+ ["worker", "status", "updated_at", "pct_complete", "message", "args", "elapsed"].include?(value)
79
+ end
80
+
81
+ def retry_job_action
82
+ job = Sidekiq::RetrySet.new.find_job(safe_url_params("jid"))
83
+ job ||= Sidekiq::DeadSet.new.find_job(safe_url_params("jid"))
84
+ job.retry if job
85
+ throw :halt, [302, { "Location" => request.referer }, []]
86
+ end
87
+
88
+ def delete_job_action
89
+ Sidekiq::Status.delete(safe_url_params("jid"))
90
+ throw :halt, [302, { "Location" => request.referer }, []]
91
+ end
92
+ end
93
+ end
94
+ end
@@ -1,13 +1,8 @@
1
- if Sidekiq.major_version >= 5
2
- require 'sidekiq/job_retry'
3
- end
1
+ require 'sidekiq/job_retry'
4
2
 
5
3
  module Sidekiq::Status
6
4
  # Should be in the server middleware chain
7
5
  class ServerMiddleware
8
-
9
- DEFAULT_MAX_RETRY_ATTEMPTS = Sidekiq.major_version >= 5 ? Sidekiq::JobRetry::DEFAULT_MAX_RETRY_ATTEMPTS : 25
10
-
11
6
  include Storage
12
7
 
13
8
  # Parameterized initialization, use it when adding middleware to server chain
@@ -34,7 +29,7 @@ module Sidekiq::Status
34
29
  expiry = @expiration
35
30
 
36
31
  # Determine the actual job class
37
- klass = msg["args"][0]["job_class"] || msg["class"] rescue msg["class"]
32
+ klass = (!msg["args"][0].is_a?(String) && msg["args"][0]["job_class"]) || msg["class"] rescue msg["class"]
38
33
  job_class = klass.is_a?(Class) ? klass : Module.const_get(klass)
39
34
 
40
35
  # Bypass unless this is a Sidekiq::Status::Worker job
@@ -58,7 +53,7 @@ module Sidekiq::Status
58
53
  rescue Exception
59
54
  status = :failed
60
55
  if msg['retry']
61
- if retry_attempt_number(msg) < retry_attempts_from(msg['retry'], DEFAULT_MAX_RETRY_ATTEMPTS)
56
+ if retry_attempt_number(msg) < retry_attempts_from(msg['retry'], Sidekiq::JobRetry::DEFAULT_MAX_RETRY_ATTEMPTS)
62
57
  status = :retrying
63
58
  end
64
59
  end
@@ -71,7 +66,7 @@ module Sidekiq::Status
71
66
 
72
67
  def retry_attempt_number(msg)
73
68
  if msg['retry_count']
74
- msg['retry_count'] + sidekiq_version_dependent_retry_offset
69
+ msg['retry_count'] + 1
75
70
  else
76
71
  0
77
72
  end
@@ -80,26 +75,16 @@ module Sidekiq::Status
80
75
  def retry_attempts_from(msg_retry, default)
81
76
  msg_retry.is_a?(Integer) ? msg_retry : default
82
77
  end
83
-
84
- def sidekiq_version_dependent_retry_offset
85
- Sidekiq.major_version >= 4 ? 1 : 0
86
- end
87
78
  end
88
79
 
89
80
  # Helper method to easily configure sidekiq-status server middleware
90
81
  # whatever the Sidekiq version is.
91
- # @param [Sidekiq] sidekiq_config the Sidekiq config
82
+ # @param [Sidekiq::Config] sidekiq_config the Sidekiq config
92
83
  # @param [Hash] server_middleware_options server middleware initialization options
93
84
  # @option server_middleware_options [Fixnum] :expiration ttl for complete jobs
94
85
  def self.configure_server_middleware(sidekiq_config, server_middleware_options = {})
95
86
  sidekiq_config.server_middleware do |chain|
96
- if Sidekiq.major_version < 5
97
- chain.insert_after Sidekiq::Middleware::Server::Logging,
98
- Sidekiq::Status::ServerMiddleware, server_middleware_options
99
- else
100
- chain.add Sidekiq::Status::ServerMiddleware, server_middleware_options
101
- end
87
+ chain.add Sidekiq::Status::ServerMiddleware, server_middleware_options
102
88
  end
103
-
104
89
  end
105
90
  end
@@ -1,5 +1,5 @@
1
1
  module Sidekiq::Status::Storage
2
- RESERVED_FIELDS=%w(status stop update_time).freeze
2
+ RESERVED_FIELDS=%w(status stop enqueued_at started_at updated_at ended_at).freeze
3
3
  BATCH_LIMIT = 500
4
4
 
5
5
  protected
@@ -15,7 +15,7 @@ module Sidekiq::Status::Storage
15
15
  status_updates.transform_values!(&:to_s)
16
16
  redis_connection(redis_pool) do |conn|
17
17
  conn.multi do |pipeline|
18
- pipeline.hset key(id), 'update_time', Time.now.to_i, *(status_updates.to_a.flatten(1))
18
+ pipeline.hset key(id), 'updated_at', Time.now.to_i, *(status_updates.to_a.flatten(1))
19
19
  pipeline.expire key(id), (expiration || Sidekiq::Status::DEFAULT_EXPIRY)
20
20
  pipeline.publish "status_updates", id
21
21
  end[0]
@@ -30,7 +30,16 @@ module Sidekiq::Status::Storage
30
30
  # @param [ConnectionPool] redis_pool optional redis connection pool
31
31
  # @return [String] Redis operation status code
32
32
  def store_status(id, status, expiration = nil, redis_pool=nil)
33
- store_for_id id, {status: status}, expiration, redis_pool
33
+ updates = {status: status}
34
+ case status.to_sym
35
+ when :failed, :stopped, :interrupted, :complete
36
+ updates[:ended_at] = Time.now.to_i
37
+ when :working
38
+ updates[:started_at] = Time.now.to_i
39
+ when :queued
40
+ updates[:enqueued_at] = Time.now.to_i
41
+ end
42
+ store_for_id id, updates, expiration, redis_pool
34
43
  end
35
44
 
36
45
  # Unschedules the job and deletes the Status
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module Status
3
- VERSION = '3.0.3'
3
+ VERSION = '4.0.0'
4
4
  end
5
5
  end
@@ -1,14 +1,15 @@
1
1
  # adapted from https://github.com/cryo28/sidekiq_status
2
+ require_relative 'helpers'
2
3
 
3
4
  module Sidekiq::Status
4
5
  # Hook into *Sidekiq::Web* Sinatra app which adds a new "/statuses" page
5
6
  module Web
6
- # Location of Sidekiq::Status::Web view templates
7
- VIEW_PATH = File.expand_path('../../../web/views', __FILE__)
7
+ # Location of Sidekiq::Status::Web static assets and templates
8
+ ROOT = File.expand_path("../../web", File.dirname(__FILE__))
9
+ VIEW_PATH = File.expand_path("views", ROOT)
8
10
 
9
11
  DEFAULT_PER_PAGE_OPTS = [25, 50, 100].freeze
10
12
  DEFAULT_PER_PAGE = 25
11
- COMMON_STATUS_HASH_KEYS = %w(update_time jid status worker args label pct_complete total at message working_at elapsed eta)
12
13
 
13
14
  class << self
14
15
  def per_page_opts= arr
@@ -25,74 +26,9 @@ module Sidekiq::Status
25
26
  end
26
27
  end
27
28
 
28
- # @param [Sidekiq::Web] app
29
29
  def self.registered(app)
30
30
 
31
- # Allow method overrides to support RESTful deletes
32
- app.set :method_override, true
33
-
34
- app.helpers do
35
- def csrf_tag
36
- "<input type='hidden' name='authenticity_token' value='#{session[:csrf]}'/>"
37
- end
38
-
39
- def poll_path
40
- "?#{request.query_string}" if params[:poll]
41
- end
42
-
43
- def sidekiq_status_template(name)
44
- path = File.join(VIEW_PATH, name.to_s) + ".erb"
45
- File.open(path).read
46
- end
47
-
48
- def add_details_to_status(status)
49
- status['label'] = status_label(status['status'])
50
- status["pct_complete"] ||= pct_complete(status)
51
- status["elapsed"] ||= elapsed(status).to_s
52
- status["eta"] ||= eta(status).to_s
53
- status["custom"] = process_custom_data(status)
54
- return status
55
- end
56
-
57
- def process_custom_data(hash)
58
- hash.reject { |key, _| COMMON_STATUS_HASH_KEYS.include?(key) }
59
- end
60
-
61
- def pct_complete(status)
62
- return 100 if status['status'] == 'complete'
63
- Sidekiq::Status::pct_complete(status['jid']) || 0
64
- end
65
-
66
- def elapsed(status)
67
- case status['status']
68
- when 'complete'
69
- Sidekiq::Status.update_time(status['jid']) - Sidekiq::Status.working_at(status['jid'])
70
- when 'working', 'retrying'
71
- Time.now.to_i - Sidekiq::Status.working_at(status['jid'])
72
- end
73
- end
74
-
75
- def eta(status)
76
- Sidekiq::Status.eta(status['jid']) if status['status'] == 'working'
77
- end
78
-
79
- def status_label(status)
80
- case status
81
- when 'complete'
82
- 'success'
83
- when 'working', 'retrying'
84
- 'warning'
85
- when 'queued'
86
- 'primary'
87
- else
88
- 'danger'
89
- end
90
- end
91
-
92
- def has_sort_by?(value)
93
- ["worker", "status", "update_time", "pct_complete", "message", "args"].include?(value)
94
- end
95
- end
31
+ app.helpers Helpers
96
32
 
97
33
  app.get '/statuses' do
98
34
 
@@ -110,39 +46,41 @@ module Sidekiq::Status
110
46
  @statuses << status
111
47
  end
112
48
 
113
- sort_by = has_sort_by?(params[:sort_by]) ? params[:sort_by] : "update_time"
49
+ sort_by = has_sort_by?(safe_url_params("sort_by")) ? safe_url_params("sort_by") : "updated_at"
114
50
  sort_dir = "asc"
115
51
 
116
- if params[:sort_dir] == "asc"
52
+ if safe_url_params("sort_dir") == "asc"
117
53
  @statuses = @statuses.sort { |x,y| (x[sort_by] <=> y[sort_by]) || -1 }
118
54
  else
119
55
  sort_dir = "desc"
120
56
  @statuses = @statuses.sort { |y,x| (x[sort_by] <=> y[sort_by]) || 1 }
121
57
  end
122
58
 
123
- if params[:status] && params[:status] != "all"
124
- @statuses = @statuses.select {|job_status| job_status["status"] == params[:status] }
59
+ if safe_url_params("status") && safe_url_params("status") != "all"
60
+ @statuses = @statuses.select {|job_status| job_status["status"] == safe_url_params("status") }
125
61
  end
126
62
 
127
63
  # Sidekiq pagination
128
64
  @total_size = @statuses.count
129
- @count = params[:per_page] ? params[:per_page].to_i : Sidekiq::Status::Web.default_per_page
130
- @count = @total_size if params[:per_page] == 'all'
131
- @current_page = params[:page].to_i < 1 ? 1 : params[:page].to_i
65
+ @count = safe_url_params("per_page") ? safe_url_params("per_page").to_i : Sidekiq::Status::Web.default_per_page
66
+ @count = @total_size if safe_url_params("per_page") == 'all'
67
+ @current_page = safe_url_params("page").to_i < 1 ? 1 : safe_url_params("page").to_i
132
68
  @statuses = @statuses.slice((@current_page - 1) * @count, @count)
133
69
 
134
70
  @headers = [
135
71
  {id: "worker", name: "Worker / JID", class: nil, url: nil},
136
72
  {id: "args", name: "Arguments", class: nil, url: nil},
137
73
  {id: "status", name: "Status", class: nil, url: nil},
138
- {id: "update_time", name: "Last Updated", class: nil, url: nil},
74
+ {id: "updated_at", name: "Last Updated", class: nil, url: nil},
139
75
  {id: "pct_complete", name: "Progress", class: nil, url: nil},
140
76
  {id: "elapsed", name: "Time Elapsed", class: nil, url: nil},
141
77
  {id: "eta", name: "ETA", class: nil, url: nil},
142
78
  ]
143
79
 
80
+ args = request.params
81
+
144
82
  @headers.each do |h|
145
- h[:url] = "statuses?" + params.merge("sort_by" => h[:id], "sort_dir" => (sort_by == h[:id] && sort_dir == "asc") ? "desc" : "asc").map{|k, v| "#{k}=#{CGI.escape v.to_s}"}.join("&")
83
+ h[:url] = "statuses?" + args.merge("sort_by" => h[:id], "sort_dir" => (sort_by == h[:id] && sort_dir == "asc") ? "desc" : "asc").map{|k, v| "#{k}=#{CGI.escape v.to_s}"}.join("&")
146
84
  h[:class] = "sorted_#{sort_dir}" if sort_by == h[:id]
147
85
  end
148
86
 
@@ -150,7 +88,7 @@ module Sidekiq::Status
150
88
  end
151
89
 
152
90
  app.get '/statuses/:jid' do
153
- job = Sidekiq::Status::get_all params['jid']
91
+ job = Sidekiq::Status::get_all safe_route_params(:jid)
154
92
 
155
93
  if job.empty?
156
94
  throw :halt, [404, {"Content-Type" => "text/html"}, [erb(sidekiq_status_template(:status_not_found))]]
@@ -160,35 +98,71 @@ module Sidekiq::Status
160
98
  end
161
99
  end
162
100
 
101
+ # Handles POST requests with method override for statuses
102
+ app.post '/statuses' do
103
+ case safe_url_params("_method")
104
+ when 'put'
105
+ # Retries a failed job from the status list
106
+ retry_job_action
107
+ when 'delete'
108
+ # Removes a completed job from the status list
109
+ delete_job_action
110
+ else
111
+ throw :halt, [405, {"Content-Type" => "text/html"}, ["Method not allowed"]]
112
+ end
113
+ end
114
+
163
115
  # Retries a failed job from the status list
164
116
  app.put '/statuses' do
165
- job = Sidekiq::RetrySet.new.find_job(params[:jid])
166
- job ||= Sidekiq::DeadSet.new.find_job(params[:jid])
167
- job.retry if job
168
- throw :halt, [302, { "Location" => request.referer }, []]
117
+ retry_job_action
169
118
  end
170
119
 
171
120
  # Removes a completed job from the status list
172
121
  app.delete '/statuses' do
173
- Sidekiq::Status.delete(params[:jid])
174
- throw :halt, [302, { "Location" => request.referer }, []]
122
+ delete_job_action
175
123
  end
176
124
  end
177
125
  end
178
126
  end
179
127
 
180
128
  unless defined?(Sidekiq::Web)
181
- require 'delegate' # Needed for sidekiq 5.x
182
129
  require 'sidekiq/web'
183
130
  end
184
131
 
185
- Sidekiq::Web.register(Sidekiq::Status::Web)
186
- ["per_page", "sort_by", "sort_dir", "status"].each do |key|
187
- Sidekiq::WebHelpers::SAFE_QPARAMS.push(key)
188
- end
189
- if Sidekiq::Web.tabs.is_a?(Array)
190
- # For sidekiq < 2.5
191
- Sidekiq::Web.tabs << "statuses"
132
+ if Sidekiq.major_version >= 8
133
+ Sidekiq::Web.configure do |config|
134
+ config.register_extension(
135
+ Sidekiq::Status::Web,
136
+ name: 'statuses',
137
+ tab: ['Statuses'],
138
+ index: ['statuses'],
139
+ root_dir: Sidekiq::Status::Web::ROOT,
140
+ asset_paths: ['javascripts', 'stylesheets']
141
+ )
142
+ end
143
+ elsif Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('7.3.0')
144
+ Sidekiq::Web.configure do |config|
145
+ config.register(
146
+ Sidekiq::Status::Web,
147
+ name: 'statuses',
148
+ tab: ['Statuses'],
149
+ index: 'statuses'
150
+ )
151
+ end
192
152
  else
153
+ Sidekiq::Web.register(Sidekiq::Status::Web)
193
154
  Sidekiq::Web.tabs["Statuses"] = "statuses"
194
155
  end
156
+
157
+ ["per_page", "sort_by", "sort_dir", "status"].each do |key|
158
+ Sidekiq::WebHelpers::SAFE_QPARAMS.push(key)
159
+ end
160
+
161
+ # Register custom JavaScript and CSS assets
162
+ ASSETS_PATH = File.expand_path('../../../web', __FILE__)
163
+
164
+ Sidekiq::Web.use Rack::Static,
165
+ urls: ['/assets'],
166
+ root: ASSETS_PATH,
167
+ cascade: true,
168
+ header_rules: [[:all, { 'cache-control' => 'private, max-age=86400' }]]