sidekiq-status 0.6.0 → 3.0.3
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 +5 -5
- data/.github/workflows/ci.yaml +53 -0
- data/.gitignore +4 -1
- data/.gitlab-ci.yml +17 -0
- data/Appraisals +11 -0
- data/CHANGELOG.md +36 -18
- data/Gemfile +0 -4
- data/README.md +129 -50
- data/Rakefile +2 -0
- data/gemfiles/sidekiq_6.1.gemfile +7 -0
- data/gemfiles/sidekiq_6.x.gemfile +7 -0
- data/gemfiles/sidekiq_7.x.gemfile +7 -0
- data/lib/sidekiq-status/client_middleware.rb +46 -8
- data/lib/sidekiq-status/redis_adapter.rb +18 -0
- data/lib/sidekiq-status/redis_client_adapter.rb +14 -0
- data/lib/sidekiq-status/server_middleware.rb +76 -20
- data/lib/sidekiq-status/sidekiq_extensions.rb +7 -0
- data/lib/sidekiq-status/storage.rb +19 -9
- data/lib/sidekiq-status/testing/inline.rb +10 -0
- data/lib/sidekiq-status/version.rb +1 -1
- data/lib/sidekiq-status/web.rb +116 -25
- data/lib/sidekiq-status/worker.rb +12 -5
- data/lib/sidekiq-status.rb +40 -5
- data/sidekiq-status.gemspec +7 -4
- data/spec/environment.rb +1 -0
- data/spec/lib/sidekiq-status/client_middleware_spec.rb +15 -12
- data/spec/lib/sidekiq-status/server_middleware_spec.rb +66 -22
- data/spec/lib/sidekiq-status/web_spec.rb +62 -15
- data/spec/lib/sidekiq-status/worker_spec.rb +19 -1
- data/spec/lib/sidekiq-status_spec.rb +94 -21
- data/spec/spec_helper.rb +104 -26
- data/spec/support/test_jobs.rb +71 -6
- data/web/sidekiq-status-single-web.png +0 -0
- data/web/views/status.erb +118 -0
- data/web/views/status_not_found.erb +6 -0
- data/web/views/statuses.erb +108 -22
- metadata +74 -13
- data/.travis.yml +0 -16
- data/gemfiles/Gemfile.sidekiq-2 +0 -5
@@ -1,6 +1,13 @@
|
|
1
|
+
if Sidekiq.major_version >= 5
|
2
|
+
require 'sidekiq/job_retry'
|
3
|
+
end
|
4
|
+
|
1
5
|
module Sidekiq::Status
|
2
|
-
# Should be in the server middleware chain
|
6
|
+
# Should be in the server middleware chain
|
3
7
|
class ServerMiddleware
|
8
|
+
|
9
|
+
DEFAULT_MAX_RETRY_ATTEMPTS = Sidekiq.major_version >= 5 ? Sidekiq::JobRetry::DEFAULT_MAX_RETRY_ATTEMPTS : 25
|
10
|
+
|
4
11
|
include Storage
|
5
12
|
|
6
13
|
# Parameterized initialization, use it when adding middleware to server chain
|
@@ -22,28 +29,77 @@ module Sidekiq::Status
|
|
22
29
|
# @param [Array] msg job args, should have jid format
|
23
30
|
# @param [String] queue queue name
|
24
31
|
def call(worker, msg, queue)
|
25
|
-
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
|
33
|
+
# Initial assignment to prevent SystemExit & co. from excepting
|
34
|
+
expiry = @expiration
|
35
|
+
|
36
|
+
# Determine the actual job class
|
37
|
+
klass = msg["args"][0]["job_class"] || msg["class"] rescue msg["class"]
|
38
|
+
job_class = klass.is_a?(Class) ? klass : Module.const_get(klass)
|
39
|
+
|
40
|
+
# Bypass unless this is a Sidekiq::Status::Worker job
|
41
|
+
unless job_class.ancestors.include?(Sidekiq::Status::Worker)
|
42
|
+
yield
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
begin
|
47
|
+
# Determine job expiration
|
48
|
+
expiry = job_class.new.expiration || @expiration rescue @expiration
|
49
|
+
|
50
|
+
store_status worker.jid, :working, expiry
|
51
|
+
yield
|
52
|
+
store_status worker.jid, :complete, expiry
|
53
|
+
rescue Worker::Stopped
|
54
|
+
store_status worker.jid, :stopped, expiry
|
55
|
+
rescue SystemExit, Interrupt
|
56
|
+
store_status worker.jid, :interrupted, expiry
|
57
|
+
raise
|
58
|
+
rescue Exception
|
59
|
+
status = :failed
|
60
|
+
if msg['retry']
|
61
|
+
if retry_attempt_number(msg) < retry_attempts_from(msg['retry'], DEFAULT_MAX_RETRY_ATTEMPTS)
|
62
|
+
status = :retrying
|
63
|
+
end
|
33
64
|
end
|
65
|
+
store_status(worker.jid, status, expiry) if job_class && job_class.ancestors.include?(Sidekiq::Status::Worker)
|
66
|
+
raise
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def retry_attempt_number(msg)
|
73
|
+
if msg['retry_count']
|
74
|
+
msg['retry_count'] + sidekiq_version_dependent_retry_offset
|
75
|
+
else
|
76
|
+
0
|
34
77
|
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def retry_attempts_from(msg_retry, default)
|
81
|
+
msg_retry.is_a?(Integer) ? msg_retry : default
|
82
|
+
end
|
35
83
|
|
36
|
-
|
37
|
-
|
38
|
-
store_status worker.jid, :complete, @expiration
|
39
|
-
rescue Worker::Stopped
|
40
|
-
store_status worker.jid, :stopped, @expiration
|
41
|
-
rescue SystemExit, Interrupt
|
42
|
-
store_status worker.jid, :interrupted, @expiration
|
43
|
-
raise
|
44
|
-
rescue
|
45
|
-
store_status worker.jid, :failed, @expiration
|
46
|
-
raise
|
84
|
+
def sidekiq_version_dependent_retry_offset
|
85
|
+
Sidekiq.major_version >= 4 ? 1 : 0
|
47
86
|
end
|
48
87
|
end
|
88
|
+
|
89
|
+
# Helper method to easily configure sidekiq-status server middleware
|
90
|
+
# whatever the Sidekiq version is.
|
91
|
+
# @param [Sidekiq] sidekiq_config the Sidekiq config
|
92
|
+
# @param [Hash] server_middleware_options server middleware initialization options
|
93
|
+
# @option server_middleware_options [Fixnum] :expiration ttl for complete jobs
|
94
|
+
def self.configure_server_middleware(sidekiq_config, server_middleware_options = {})
|
95
|
+
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
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
49
105
|
end
|
@@ -12,11 +12,12 @@ module Sidekiq::Status::Storage
|
|
12
12
|
# @param [ConnectionPool] redis_pool optional redis connection pool
|
13
13
|
# @return [String] Redis operation status code
|
14
14
|
def store_for_id(id, status_updates, expiration = nil, redis_pool=nil)
|
15
|
+
status_updates.transform_values!(&:to_s)
|
15
16
|
redis_connection(redis_pool) do |conn|
|
16
|
-
conn.multi do
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
conn.multi do |pipeline|
|
18
|
+
pipeline.hset key(id), 'update_time', Time.now.to_i, *(status_updates.to_a.flatten(1))
|
19
|
+
pipeline.expire key(id), (expiration || Sidekiq::Status::DEFAULT_EXPIRY)
|
20
|
+
pipeline.publish "status_updates", id
|
20
21
|
end[0]
|
21
22
|
end
|
22
23
|
end
|
@@ -36,7 +37,7 @@ module Sidekiq::Status::Storage
|
|
36
37
|
# @param [String] id job id
|
37
38
|
# @param [Num] job_unix_time, unix timestamp for the scheduled job
|
38
39
|
def delete_and_unschedule(job_id, job_unix_time = nil)
|
39
|
-
Sidekiq.
|
40
|
+
Sidekiq::Status.redis_adapter do |conn|
|
40
41
|
scan_options = {offset: 0, conn: conn, start: (job_unix_time || '-inf'), end: (job_unix_time || '+inf')}
|
41
42
|
|
42
43
|
while not (jobs = schedule_batch(scan_options)).empty?
|
@@ -52,12 +53,21 @@ module Sidekiq::Status::Storage
|
|
52
53
|
false
|
53
54
|
end
|
54
55
|
|
56
|
+
# Deletes status hash info for given job id
|
57
|
+
# @param[String] job id
|
58
|
+
# @retrun [Integer] number of keys that were removed
|
59
|
+
def delete_status(id)
|
60
|
+
redis_connection do |conn|
|
61
|
+
conn.del(key(id))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
55
65
|
# Gets a single valued from job status hash
|
56
66
|
# @param [String] id job id
|
57
67
|
# @param [String] Symbol field fetched field name
|
58
68
|
# @return [String] Redis operation status code
|
59
69
|
def read_field_for_id(id, field)
|
60
|
-
Sidekiq.
|
70
|
+
Sidekiq::Status.redis_adapter do |conn|
|
61
71
|
conn.hget(key(id), field)
|
62
72
|
end
|
63
73
|
end
|
@@ -66,8 +76,8 @@ module Sidekiq::Status::Storage
|
|
66
76
|
# @param [String] id job id
|
67
77
|
# @return [Hash] Hash stored in redis
|
68
78
|
def read_hash_for_id(id)
|
69
|
-
Sidekiq.
|
70
|
-
conn.hgetall
|
79
|
+
Sidekiq::Status.redis_adapter do |conn|
|
80
|
+
conn.hgetall(key(id))
|
71
81
|
end
|
72
82
|
end
|
73
83
|
|
@@ -81,7 +91,7 @@ module Sidekiq::Status::Storage
|
|
81
91
|
# - end: end score (i.e. +inf or a unix timestamp)
|
82
92
|
# - offset: current progress through (all) jobs (e.g.: 100 if you want jobs from 100 to BATCH_LIMIT)
|
83
93
|
def schedule_batch(options)
|
84
|
-
options[:conn].
|
94
|
+
Sidekiq::Status.wrap_redis_connection(options[:conn]).schedule_batch("schedule", options.merge(limit: BATCH_LIMIT))
|
85
95
|
end
|
86
96
|
|
87
97
|
# Searches the jobs Array for the job_id
|
@@ -5,6 +5,16 @@ module Sidekiq
|
|
5
5
|
:complete
|
6
6
|
end
|
7
7
|
end
|
8
|
+
|
9
|
+
module Storage
|
10
|
+
def store_status(id, status, expiration = nil, redis_pool=nil)
|
11
|
+
'ok'
|
12
|
+
end
|
13
|
+
|
14
|
+
def store_for_id(id, status_updates, expiration = nil, redis_pool=nil)
|
15
|
+
'ok'
|
16
|
+
end
|
17
|
+
end
|
8
18
|
end
|
9
19
|
end
|
10
20
|
|
data/lib/sidekiq-status/web.rb
CHANGED
@@ -6,9 +6,40 @@ module Sidekiq::Status
|
|
6
6
|
# Location of Sidekiq::Status::Web view templates
|
7
7
|
VIEW_PATH = File.expand_path('../../../web/views', __FILE__)
|
8
8
|
|
9
|
+
DEFAULT_PER_PAGE_OPTS = [25, 50, 100].freeze
|
10
|
+
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
|
+
class << self
|
14
|
+
def per_page_opts= arr
|
15
|
+
@per_page_opts = arr
|
16
|
+
end
|
17
|
+
def per_page_opts
|
18
|
+
@per_page_opts || DEFAULT_PER_PAGE_OPTS
|
19
|
+
end
|
20
|
+
def default_per_page= val
|
21
|
+
@default_per_page = val
|
22
|
+
end
|
23
|
+
def default_per_page
|
24
|
+
@default_per_page || DEFAULT_PER_PAGE
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
9
28
|
# @param [Sidekiq::Web] app
|
10
29
|
def self.registered(app)
|
30
|
+
|
31
|
+
# Allow method overrides to support RESTful deletes
|
32
|
+
app.set :method_override, true
|
33
|
+
|
11
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
|
+
|
12
43
|
def sidekiq_status_template(name)
|
13
44
|
path = File.join(VIEW_PATH, name.to_s) + ".erb"
|
14
45
|
File.open(path).read
|
@@ -16,20 +47,40 @@ module Sidekiq::Status
|
|
16
47
|
|
17
48
|
def add_details_to_status(status)
|
18
49
|
status['label'] = status_label(status['status'])
|
19
|
-
status["pct_complete"]
|
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)
|
20
54
|
return status
|
21
55
|
end
|
22
56
|
|
57
|
+
def process_custom_data(hash)
|
58
|
+
hash.reject { |key, _| COMMON_STATUS_HASH_KEYS.include?(key) }
|
59
|
+
end
|
60
|
+
|
23
61
|
def pct_complete(status)
|
24
62
|
return 100 if status['status'] == 'complete'
|
25
|
-
Sidekiq::Status::pct_complete(status['jid'])
|
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'
|
26
77
|
end
|
27
78
|
|
28
79
|
def status_label(status)
|
29
80
|
case status
|
30
81
|
when 'complete'
|
31
82
|
'success'
|
32
|
-
when 'working'
|
83
|
+
when 'working', 'retrying'
|
33
84
|
'warning'
|
34
85
|
when 'queued'
|
35
86
|
'primary'
|
@@ -39,62 +90,102 @@ module Sidekiq::Status
|
|
39
90
|
end
|
40
91
|
|
41
92
|
def has_sort_by?(value)
|
42
|
-
["worker", "status", "update_time", "pct_complete", "message"].include?(value)
|
93
|
+
["worker", "status", "update_time", "pct_complete", "message", "args"].include?(value)
|
43
94
|
end
|
44
95
|
end
|
45
96
|
|
46
97
|
app.get '/statuses' do
|
47
|
-
|
48
|
-
jids =
|
98
|
+
|
99
|
+
jids = Sidekiq::Status.redis_adapter do |conn|
|
100
|
+
conn.scan(match: 'sidekiq:status:*', count: 100).map do |key|
|
101
|
+
key.split(':').last
|
102
|
+
end.uniq
|
103
|
+
end
|
49
104
|
@statuses = []
|
50
105
|
|
51
106
|
jids.each do |jid|
|
52
107
|
status = Sidekiq::Status::get_all jid
|
53
108
|
next if !status || status.count < 2
|
54
109
|
status = add_details_to_status(status)
|
55
|
-
@statuses <<
|
110
|
+
@statuses << status
|
56
111
|
end
|
57
112
|
|
58
113
|
sort_by = has_sort_by?(params[:sort_by]) ? params[:sort_by] : "update_time"
|
59
114
|
sort_dir = "asc"
|
60
115
|
|
61
116
|
if params[:sort_dir] == "asc"
|
62
|
-
@statuses = @statuses.sort { |x,y| x
|
117
|
+
@statuses = @statuses.sort { |x,y| (x[sort_by] <=> y[sort_by]) || -1 }
|
63
118
|
else
|
64
119
|
sort_dir = "desc"
|
65
|
-
@statuses = @statuses.sort { |y,x| x
|
120
|
+
@statuses = @statuses.sort { |y,x| (x[sort_by] <=> y[sort_by]) || 1 }
|
66
121
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
@statuses = working_jobs
|
71
|
-
else
|
72
|
-
@statuses = (@statuses.size >= 25) ? @statuses.take(25) : @statuses
|
122
|
+
|
123
|
+
if params[:status] && params[:status] != "all"
|
124
|
+
@statuses = @statuses.select {|job_status| job_status["status"] == params[:status] }
|
73
125
|
end
|
74
|
-
|
126
|
+
|
127
|
+
# Sidekiq pagination
|
128
|
+
@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
|
132
|
+
@statuses = @statuses.slice((@current_page - 1) * @count, @count)
|
133
|
+
|
75
134
|
@headers = [
|
76
|
-
{
|
77
|
-
{
|
78
|
-
{
|
79
|
-
{
|
80
|
-
{
|
135
|
+
{id: "worker", name: "Worker / JID", class: nil, url: nil},
|
136
|
+
{id: "args", name: "Arguments", class: nil, url: nil},
|
137
|
+
{id: "status", name: "Status", class: nil, url: nil},
|
138
|
+
{id: "update_time", name: "Last Updated", class: nil, url: nil},
|
139
|
+
{id: "pct_complete", name: "Progress", class: nil, url: nil},
|
140
|
+
{id: "elapsed", name: "Time Elapsed", class: nil, url: nil},
|
141
|
+
{id: "eta", name: "ETA", class: nil, url: nil},
|
81
142
|
]
|
82
143
|
|
83
144
|
@headers.each do |h|
|
84
|
-
|
85
|
-
params["sort_dir"] = (sort_by == h[:id] && sort_dir == "asc") ? "desc" : "asc"
|
86
|
-
h[:url] = "statuses?" + params.map {|k,v| "#{k}=#{v}" }.join("&")
|
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("&")
|
87
146
|
h[:class] = "sorted_#{sort_dir}" if sort_by == h[:id]
|
88
147
|
end
|
89
148
|
|
90
149
|
erb(sidekiq_status_template(:statuses))
|
91
150
|
end
|
151
|
+
|
152
|
+
app.get '/statuses/:jid' do
|
153
|
+
job = Sidekiq::Status::get_all params['jid']
|
154
|
+
|
155
|
+
if job.empty?
|
156
|
+
throw :halt, [404, {"Content-Type" => "text/html"}, [erb(sidekiq_status_template(:status_not_found))]]
|
157
|
+
else
|
158
|
+
@status = add_details_to_status(job)
|
159
|
+
erb(sidekiq_status_template(:status))
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Retries a failed job from the status list
|
164
|
+
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 }, []]
|
169
|
+
end
|
170
|
+
|
171
|
+
# Removes a completed job from the status list
|
172
|
+
app.delete '/statuses' do
|
173
|
+
Sidekiq::Status.delete(params[:jid])
|
174
|
+
throw :halt, [302, { "Location" => request.referer }, []]
|
175
|
+
end
|
92
176
|
end
|
93
177
|
end
|
94
178
|
end
|
95
179
|
|
96
|
-
|
180
|
+
unless defined?(Sidekiq::Web)
|
181
|
+
require 'delegate' # Needed for sidekiq 5.x
|
182
|
+
require 'sidekiq/web'
|
183
|
+
end
|
184
|
+
|
97
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
|
98
189
|
if Sidekiq::Web.tabs.is_a?(Array)
|
99
190
|
# For sidekiq < 2.5
|
100
191
|
Sidekiq::Web.tabs << "statuses"
|
@@ -11,14 +11,14 @@ module Sidekiq::Status::Worker
|
|
11
11
|
# @param [Hash] status_updates updated values
|
12
12
|
# @return [String] Redis operation status code
|
13
13
|
def store(hash)
|
14
|
-
store_for_id @jid, hash, @expiration
|
14
|
+
store_for_id @provider_job_id || @job_id || @jid || "", hash, @expiration
|
15
15
|
end
|
16
16
|
|
17
17
|
# Read value from job status hash
|
18
18
|
# @param String|Symbol hask key
|
19
19
|
# @return [String]
|
20
20
|
def retrieve(name)
|
21
|
-
read_field_for_id @jid, name
|
21
|
+
read_field_for_id @provider_job_id || @job_id || @jid || "", name
|
22
22
|
end
|
23
23
|
|
24
24
|
# Sets current task progress
|
@@ -27,15 +27,22 @@ module Sidekiq::Status::Worker
|
|
27
27
|
# @param String optional message
|
28
28
|
# @return [String]
|
29
29
|
def at(num, message = nil)
|
30
|
-
|
31
|
-
|
30
|
+
@_status_total = 100 if @_status_total.nil?
|
31
|
+
pct_complete = ((num / @_status_total.to_f) * 100).to_i rescue 0
|
32
|
+
store(at: num, total: @_status_total, pct_complete: pct_complete, message: message, working_at: working_at)
|
32
33
|
end
|
33
34
|
|
34
35
|
# Sets total number of tasks
|
35
36
|
# @param Fixnum total number of tasks
|
36
37
|
# @return [String]
|
37
38
|
def total(num)
|
38
|
-
|
39
|
+
@_status_total = num
|
40
|
+
store(total: num, working_at: working_at)
|
39
41
|
end
|
40
42
|
|
43
|
+
private
|
44
|
+
|
45
|
+
def working_at
|
46
|
+
@working_at ||= Time.now.to_i
|
47
|
+
end
|
41
48
|
end
|
data/lib/sidekiq-status.rb
CHANGED
@@ -1,14 +1,18 @@
|
|
1
|
-
require
|
1
|
+
require 'sidekiq-status/version'
|
2
|
+
require 'sidekiq-status/sidekiq_extensions'
|
2
3
|
require 'sidekiq-status/storage'
|
3
4
|
require 'sidekiq-status/worker'
|
5
|
+
require 'sidekiq-status/redis_client_adapter'
|
6
|
+
require 'sidekiq-status/redis_adapter'
|
4
7
|
require 'sidekiq-status/client_middleware'
|
5
8
|
require 'sidekiq-status/server_middleware'
|
6
9
|
require 'sidekiq-status/web' if defined?(Sidekiq::Web)
|
10
|
+
require 'chronic_duration'
|
7
11
|
|
8
12
|
module Sidekiq::Status
|
9
13
|
extend Storage
|
10
14
|
DEFAULT_EXPIRY = 60 * 30
|
11
|
-
STATUS = [ :queued, :working, :complete, :stopped, :failed, :interrupted ].freeze
|
15
|
+
STATUS = [ :queued, :working, :retrying, :complete, :stopped, :failed, :interrupted ].freeze
|
12
16
|
|
13
17
|
class << self
|
14
18
|
# Job status by id
|
@@ -21,8 +25,8 @@ module Sidekiq::Status
|
|
21
25
|
# Get all status fields for a job
|
22
26
|
# @params [String] id job id returned by async_perform
|
23
27
|
# @return [Hash] hash of all fields stored for the job
|
24
|
-
def get_all(
|
25
|
-
read_hash_for_id(
|
28
|
+
def get_all(job_id)
|
29
|
+
read_hash_for_id(job_id)
|
26
30
|
end
|
27
31
|
|
28
32
|
def status(job_id)
|
@@ -34,6 +38,10 @@ module Sidekiq::Status
|
|
34
38
|
delete_and_unschedule(job_id, job_unix_time)
|
35
39
|
end
|
36
40
|
|
41
|
+
def delete(job_id)
|
42
|
+
delete_status(job_id)
|
43
|
+
end
|
44
|
+
|
37
45
|
alias_method :unschedule, :cancel
|
38
46
|
|
39
47
|
STATUS.each do |name|
|
@@ -52,11 +60,38 @@ module Sidekiq::Status
|
|
52
60
|
end
|
53
61
|
|
54
62
|
def pct_complete(job_id)
|
55
|
-
(
|
63
|
+
get(job_id, :pct_complete).to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
def working_at(job_id)
|
67
|
+
(get(job_id, :working_at) || Time.now).to_i
|
68
|
+
end
|
69
|
+
|
70
|
+
def update_time(job_id)
|
71
|
+
(get(job_id, :update_time) || Time.now).to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
def eta(job_id)
|
75
|
+
at = at(job_id)
|
76
|
+
return nil if at.zero?
|
77
|
+
|
78
|
+
(Time.now.to_i - working_at(job_id)).to_f / at * (total(job_id) - at)
|
56
79
|
end
|
57
80
|
|
58
81
|
def message(job_id)
|
59
82
|
get(job_id, :message)
|
60
83
|
end
|
84
|
+
|
85
|
+
def wrap_redis_connection(conn)
|
86
|
+
if Sidekiq.major_version >= 7
|
87
|
+
conn.is_a?(RedisClientAdapter) ? conn : RedisClientAdapter.new(conn)
|
88
|
+
else
|
89
|
+
conn.is_a?(RedisAdapter) ? conn : RedisAdapter.new(conn)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def redis_adapter
|
94
|
+
Sidekiq.redis { |conn| yield wrap_redis_connection(conn) }
|
95
|
+
end
|
61
96
|
end
|
62
97
|
end
|
data/sidekiq-status.gemspec
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
require File.expand_path('../lib/sidekiq-status/version', __FILE__)
|
3
3
|
|
4
4
|
Gem::Specification.new do |gem|
|
5
|
-
gem.authors = ['Evgeniy Tsvigun']
|
6
|
-
gem.email = ['utgarda@gmail.com']
|
5
|
+
gem.authors = ['Evgeniy Tsvigun', 'Kenaniah Cerny']
|
6
|
+
gem.email = ['utgarda@gmail.com', 'kenaniah@gmail.com']
|
7
7
|
gem.summary = 'An extension to the sidekiq message processing to track your jobs'
|
8
|
-
gem.homepage = '
|
8
|
+
gem.homepage = 'https://github.com/kenaniah/sidekiq-status'
|
9
9
|
gem.license = 'MIT'
|
10
10
|
|
11
11
|
gem.files = `git ls-files`.split($\)
|
@@ -14,7 +14,10 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.require_paths = ['lib']
|
15
15
|
gem.version = Sidekiq::Status::VERSION
|
16
16
|
|
17
|
-
gem.add_dependency 'sidekiq', '>=
|
17
|
+
gem.add_dependency 'sidekiq', '>= 6.0', '< 8'
|
18
|
+
gem.add_dependency 'chronic_duration'
|
19
|
+
gem.add_development_dependency 'appraisal'
|
20
|
+
gem.add_development_dependency 'colorize'
|
18
21
|
gem.add_development_dependency 'rack-test'
|
19
22
|
gem.add_development_dependency 'rake'
|
20
23
|
gem.add_development_dependency 'rspec'
|
data/spec/environment.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# This file has been intentionally left blank
|
@@ -5,21 +5,20 @@ describe Sidekiq::Status::ClientMiddleware do
|
|
5
5
|
let!(:redis) { Sidekiq.redis { |conn| conn } }
|
6
6
|
let!(:job_id) { SecureRandom.hex(12) }
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
before do
|
9
|
+
allow(SecureRandom).to receive(:hex).once.and_return(job_id)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "without :expiration parameter" do
|
10
13
|
|
11
|
-
describe "#call" do
|
12
|
-
before { client_middleware }
|
13
14
|
it "sets queued status" do
|
14
|
-
|
15
|
-
expect(StubJob.perform_async(:arg1 => 'val1')).to eq(job_id)
|
15
|
+
expect(StubJob.perform_async 'arg1' => 'val1').to eq(job_id)
|
16
16
|
expect(redis.hget("sidekiq:status:#{job_id}", :status)).to eq('queued')
|
17
17
|
expect(Sidekiq::Status::queued?(job_id)).to be_truthy
|
18
18
|
end
|
19
19
|
|
20
20
|
it "sets status hash ttl" do
|
21
|
-
|
22
|
-
expect(StubJob.perform_async(:arg1 => 'val1')).to eq(job_id)
|
21
|
+
expect(StubJob.perform_async 'arg1' => 'val1').to eq(job_id)
|
23
22
|
expect(1..Sidekiq::Status::DEFAULT_EXPIRY).to cover redis.ttl("sidekiq:status:#{job_id}")
|
24
23
|
end
|
25
24
|
|
@@ -38,18 +37,22 @@ describe Sidekiq::Status::ClientMiddleware do
|
|
38
37
|
Sidekiq::Status::ClientMiddleware.new.call(StubJob, {'jid' => SecureRandom.hex}, :queued) do end
|
39
38
|
end
|
40
39
|
end
|
40
|
+
|
41
41
|
end
|
42
42
|
|
43
|
-
describe ":expiration parameter" do
|
43
|
+
describe "with :expiration parameter" do
|
44
|
+
|
44
45
|
let(:huge_expiration) { Sidekiq::Status::DEFAULT_EXPIRY * 100 }
|
46
|
+
|
47
|
+
# Ensure client middleware is loaded with an expiration parameter set
|
45
48
|
before do
|
46
|
-
|
49
|
+
client_middleware expiration: huge_expiration
|
47
50
|
end
|
48
51
|
|
49
52
|
it "overwrites default expiry value" do
|
50
|
-
|
51
|
-
StubJob.perform_async(:arg1 => 'val1')
|
53
|
+
StubJob.perform_async 'arg1' => 'val1'
|
52
54
|
expect((Sidekiq::Status::DEFAULT_EXPIRY+1)..huge_expiration).to cover redis.ttl("sidekiq:status:#{job_id}")
|
53
55
|
end
|
56
|
+
|
54
57
|
end
|
55
58
|
end
|