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.
- checksums.yaml +4 -4
- data/.devcontainer/Dockerfile +2 -0
- data/.devcontainer/README.md +57 -0
- data/.devcontainer/devcontainer.json +55 -0
- data/.devcontainer/docker-compose.yml +19 -0
- data/.github/workflows/ci.yaml +9 -6
- data/Appraisals +14 -6
- data/CHANGELOG.md +12 -0
- data/Dockerfile +5 -0
- data/README.md +756 -41
- data/Rakefile +153 -0
- data/docker-compose.yml +15 -0
- data/gemfiles/{sidekiq_6.1.gemfile → sidekiq_7.0.gemfile} +1 -1
- data/gemfiles/sidekiq_7.3.gemfile +7 -0
- data/gemfiles/sidekiq_8.0.gemfile +7 -0
- data/gemfiles/{sidekiq_6.x.gemfile → sidekiq_8.x.gemfile} +1 -1
- data/lib/sidekiq-status/client_middleware.rb +4 -3
- data/lib/sidekiq-status/helpers.rb +94 -0
- data/lib/sidekiq-status/server_middleware.rb +6 -21
- data/lib/sidekiq-status/storage.rb +12 -3
- data/lib/sidekiq-status/version.rb +1 -1
- data/lib/sidekiq-status/web.rb +67 -93
- data/lib/sidekiq-status/worker.rb +6 -10
- data/lib/sidekiq-status.rb +21 -5
- data/sidekiq-status.gemspec +7 -1
- data/spec/environment.rb +12 -1
- data/spec/lib/sidekiq-status/client_middleware_spec.rb +8 -0
- data/spec/lib/sidekiq-status/server_middleware_spec.rb +13 -0
- data/spec/lib/sidekiq-status/web_spec.rb +72 -3
- data/spec/lib/sidekiq-status/worker_spec.rb +3 -3
- data/spec/lib/sidekiq-status_spec.rb +20 -3
- data/spec/spec_helper.rb +3 -8
- data/spec/support/test_jobs.rb +11 -0
- data/spec/test_environment.rb +1 -0
- data/web/assets/statuses.css +124 -0
- data/web/assets/statuses.js +24 -0
- data/web/views/status.erb +131 -93
- data/web/views/status_not_found.erb +1 -1
- data/web/views/statuses.erb +23 -79
- 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
|
data/docker-compose.yml
ADDED
@@ -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
|
@@ -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
|
-
|
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'] +
|
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
|
-
|
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
|
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), '
|
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
|
-
|
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
|
data/lib/sidekiq-status/web.rb
CHANGED
@@ -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
|
7
|
-
|
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
|
-
|
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?(
|
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
|
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
|
124
|
-
@statuses = @statuses.select {|job_status| job_status["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 =
|
130
|
-
@count = @total_size if
|
131
|
-
@current_page =
|
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: "
|
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?" +
|
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
|
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
|
-
|
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
|
-
|
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
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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' }]]
|