sidekiq-status 0.7.0 → 0.8.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/.gitignore +3 -1
- data/.travis.yml +4 -1
- data/Appraisals +11 -0
- data/CHANGELOG.md +18 -7
- data/README.md +99 -44
- data/Rakefile +2 -0
- data/gemfiles/sidekiq_3.x.gemfile +7 -0
- data/gemfiles/sidekiq_4.x.gemfile +7 -0
- data/gemfiles/sidekiq_5.x.gemfile +7 -0
- data/lib/sidekiq-status.rb +4 -3
- data/lib/sidekiq-status/client_middleware.rb +28 -7
- data/lib/sidekiq-status/server_middleware.rb +63 -16
- data/lib/sidekiq-status/sidekiq_extensions.rb +7 -0
- data/lib/sidekiq-status/testing/inline.rb +4 -0
- data/lib/sidekiq-status/version.rb +1 -1
- data/lib/sidekiq-status/web.rb +68 -24
- data/lib/sidekiq-status/worker.rb +4 -2
- data/sidekiq-status.gemspec +5 -3
- data/spec/lib/sidekiq-status/client_middleware_spec.rb +16 -10
- data/spec/lib/sidekiq-status/server_middleware_spec.rb +24 -10
- data/spec/lib/sidekiq-status/web_spec.rb +1 -1
- data/spec/lib/sidekiq-status/worker_spec.rb +1 -1
- data/spec/lib/sidekiq-status_spec.rb +40 -12
- data/spec/spec_helper.rb +54 -22
- data/spec/support/test_jobs.rb +17 -3
- data/web/views/status.erb +10 -10
- data/web/views/statuses.erb +75 -16
- metadata +40 -5
@@ -1,6 +1,17 @@
|
|
1
|
+
if Sidekiq.major_version < 5
|
2
|
+
require 'sidekiq/middleware/server/retry_jobs'
|
3
|
+
else
|
4
|
+
require 'sidekiq/job_retry'
|
5
|
+
end
|
6
|
+
|
1
7
|
module Sidekiq::Status
|
2
|
-
# Should be in the server middleware chain
|
8
|
+
# Should be in the server middleware chain
|
3
9
|
class ServerMiddleware
|
10
|
+
|
11
|
+
DEFAULT_MAX_RETRY_ATTEMPTS = Sidekiq.major_version < 5 ?
|
12
|
+
Sidekiq::Middleware::Server::RetryJobs::DEFAULT_MAX_RETRY_ATTEMPTS :
|
13
|
+
Sidekiq::JobRetry::DEFAULT_MAX_RETRY_ATTEMPTS
|
14
|
+
|
4
15
|
include Storage
|
5
16
|
|
6
17
|
# Parameterized initialization, use it when adding middleware to server chain
|
@@ -22,28 +33,64 @@ module Sidekiq::Status
|
|
22
33
|
# @param [Array] msg job args, should have jid format
|
23
34
|
# @param [String] queue queue name
|
24
35
|
def call(worker, msg, queue)
|
25
|
-
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
36
|
+
|
37
|
+
# Initial assignment to prevent SystemExit & co. from excepting
|
38
|
+
expiry = @expiration
|
39
|
+
|
40
|
+
# Determine the actual job class
|
41
|
+
klass = msg["args"][0]["job_class"] || msg["class"] rescue msg["class"]
|
42
|
+
job_class = klass.is_a?(Class) ? klass : Module.const_get(klass)
|
43
|
+
|
44
|
+
# Bypass unless this is a Sidekiq::Status::Worker job
|
45
|
+
unless job_class.ancestors.include?(Sidekiq::Status::Worker)
|
46
|
+
yield
|
47
|
+
return
|
34
48
|
end
|
35
49
|
|
36
|
-
|
50
|
+
# Determine job expiration
|
51
|
+
expiry = job_class.new.expiration || @expiration rescue @expiration
|
52
|
+
|
53
|
+
store_status worker.jid, :working, expiry
|
37
54
|
yield
|
38
|
-
store_status worker.jid, :complete,
|
55
|
+
store_status worker.jid, :complete, expiry
|
39
56
|
rescue Worker::Stopped
|
40
|
-
store_status worker.jid, :stopped,
|
57
|
+
store_status worker.jid, :stopped, expiry
|
41
58
|
rescue SystemExit, Interrupt
|
42
|
-
store_status worker.jid, :interrupted,
|
59
|
+
store_status worker.jid, :interrupted, expiry
|
43
60
|
raise
|
44
|
-
rescue
|
45
|
-
|
61
|
+
rescue Exception
|
62
|
+
status = :failed
|
63
|
+
if msg['retry']
|
64
|
+
retry_count = msg['retry_count'] || 0
|
65
|
+
if retry_count < retry_attempts_from(msg['retry'], DEFAULT_MAX_RETRY_ATTEMPTS)
|
66
|
+
status = :retrying
|
67
|
+
end
|
68
|
+
end
|
69
|
+
store_status worker.jid, status, expiry
|
46
70
|
raise
|
47
71
|
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def retry_attempts_from(msg_retry, default)
|
76
|
+
msg_retry.is_a?(Integer) ? msg_retry : default
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Helper method to easily configure sidekiq-status server middleware
|
81
|
+
# whatever the Sidekiq version is.
|
82
|
+
# @param [Sidekiq] sidekiq_config the Sidekiq config
|
83
|
+
# @param [Hash] server_middleware_options server middleware initialization options
|
84
|
+
# @option server_middleware_options [Fixnum] :expiration ttl for complete jobs
|
85
|
+
def self.configure_server_middleware(sidekiq_config, server_middleware_options = {})
|
86
|
+
sidekiq_config.server_middleware do |chain|
|
87
|
+
if Sidekiq.major_version < 5
|
88
|
+
chain.insert_after Sidekiq::Middleware::Server::Logging,
|
89
|
+
Sidekiq::Status::ServerMiddleware, server_middleware_options
|
90
|
+
else
|
91
|
+
chain.add Sidekiq::Status::ServerMiddleware, server_middleware_options
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
48
95
|
end
|
49
96
|
end
|
data/lib/sidekiq-status/web.rb
CHANGED
@@ -6,9 +6,39 @@ 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
|
+
|
12
|
+
class << self
|
13
|
+
def per_page_opts= arr
|
14
|
+
@per_page_opts = arr
|
15
|
+
end
|
16
|
+
def per_page_opts
|
17
|
+
@per_page_opts || DEFAULT_PER_PAGE_OPTS
|
18
|
+
end
|
19
|
+
def default_per_page= val
|
20
|
+
@default_per_page = val
|
21
|
+
end
|
22
|
+
def default_per_page
|
23
|
+
@default_per_page || DEFAULT_PER_PAGE
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
9
27
|
# @param [Sidekiq::Web] app
|
10
28
|
def self.registered(app)
|
29
|
+
|
30
|
+
# Allow method overrides to support RESTful deletes
|
31
|
+
app.set :method_override, true
|
32
|
+
|
11
33
|
app.helpers do
|
34
|
+
def csrf_tag
|
35
|
+
"<input type='hidden' name='authenticity_token' value='#{session[:csrf]}'/>"
|
36
|
+
end
|
37
|
+
|
38
|
+
def poll_path
|
39
|
+
"?#{request.query_string}" if params[:poll]
|
40
|
+
end
|
41
|
+
|
12
42
|
def sidekiq_status_template(name)
|
13
43
|
path = File.join(VIEW_PATH, name.to_s) + ".erb"
|
14
44
|
File.open(path).read
|
@@ -16,7 +46,7 @@ module Sidekiq::Status
|
|
16
46
|
|
17
47
|
def add_details_to_status(status)
|
18
48
|
status['label'] = status_label(status['status'])
|
19
|
-
status["pct_complete"]
|
49
|
+
status["pct_complete"] ||= pct_complete(status)
|
20
50
|
return status
|
21
51
|
end
|
22
52
|
|
@@ -29,7 +59,7 @@ module Sidekiq::Status
|
|
29
59
|
case status
|
30
60
|
when 'complete'
|
31
61
|
'success'
|
32
|
-
when 'working'
|
62
|
+
when 'working', 'retrying'
|
33
63
|
'warning'
|
34
64
|
when 'queued'
|
35
65
|
'primary'
|
@@ -39,53 +69,50 @@ module Sidekiq::Status
|
|
39
69
|
end
|
40
70
|
|
41
71
|
def has_sort_by?(value)
|
42
|
-
["worker", "status", "update_time", "pct_complete", "message"].include?(value)
|
72
|
+
["worker", "status", "update_time", "pct_complete", "message", "args"].include?(value)
|
43
73
|
end
|
44
74
|
end
|
45
75
|
|
46
76
|
app.get '/statuses' do
|
77
|
+
|
47
78
|
namespace_jids = Sidekiq.redis{ |conn| conn.keys('sidekiq:status:*') }
|
48
|
-
jids = namespace_jids.map{|id_namespace| id_namespace.split(':').last }
|
79
|
+
jids = namespace_jids.map{ |id_namespace| id_namespace.split(':').last }
|
49
80
|
@statuses = []
|
50
81
|
|
51
82
|
jids.each do |jid|
|
52
83
|
status = Sidekiq::Status::get_all jid
|
53
84
|
next if !status || status.count < 2
|
54
85
|
status = add_details_to_status(status)
|
55
|
-
@statuses <<
|
86
|
+
@statuses << status
|
56
87
|
end
|
57
88
|
|
58
89
|
sort_by = has_sort_by?(params[:sort_by]) ? params[:sort_by] : "update_time"
|
59
90
|
sort_dir = "asc"
|
60
91
|
|
61
92
|
if params[:sort_dir] == "asc"
|
62
|
-
@statuses = @statuses.sort { |x,y| x
|
93
|
+
@statuses = @statuses.sort { |x,y| (x[sort_by] <=> y[sort_by]) || -1 }
|
63
94
|
else
|
64
95
|
sort_dir = "desc"
|
65
|
-
@statuses = @statuses.sort { |y,x| x
|
66
|
-
end
|
67
|
-
|
68
|
-
working_jobs = @statuses.select{|job| job.status == "working"}
|
69
|
-
size = params[:size] ? params[:size].to_i : 25
|
70
|
-
if working_jobs.size >= size
|
71
|
-
@statuses = working_jobs
|
72
|
-
else
|
73
|
-
@statuses = (@statuses.size >= size) ? @statuses.take(size) : @statuses
|
96
|
+
@statuses = @statuses.sort { |y,x| (x[sort_by] <=> y[sort_by]) || 1 }
|
74
97
|
end
|
75
98
|
|
99
|
+
# Sidekiq pagination
|
100
|
+
@total_size = @statuses.count
|
101
|
+
@count = params[:per_page] ? params[:per_page].to_i : Sidekiq::Status::Web.default_per_page
|
102
|
+
@count = @total_size if params[:per_page] == 'all'
|
103
|
+
@current_page = params[:page].to_i < 1 ? 1 : params[:page].to_i
|
104
|
+
@statuses = @statuses.slice((@current_page - 1) * @count, @count)
|
76
105
|
|
77
106
|
@headers = [
|
78
|
-
{
|
79
|
-
{
|
80
|
-
{
|
81
|
-
{
|
82
|
-
{
|
107
|
+
{id: "worker", name: "Worker / JID", class: nil, url: nil},
|
108
|
+
{id: "args", name: "Arguments", class: nil, url: nil},
|
109
|
+
{id: "status", name: "Status", class: nil, url: nil},
|
110
|
+
{id: "update_time", name: "Last Updated", class: nil, url: nil},
|
111
|
+
{id: "pct_complete", name: "Progress", class: nil, url: nil},
|
83
112
|
]
|
84
113
|
|
85
114
|
@headers.each do |h|
|
86
|
-
|
87
|
-
params["sort_dir"] = (sort_by == h[:id] && sort_dir == "asc") ? "desc" : "asc"
|
88
|
-
h[:url] = "statuses?" + params.map {|k,v| "#{k}=#{v}" }.join("&")
|
115
|
+
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("&")
|
89
116
|
h[:class] = "sorted_#{sort_dir}" if sort_by == h[:id]
|
90
117
|
end
|
91
118
|
|
@@ -98,16 +125,33 @@ module Sidekiq::Status
|
|
98
125
|
if job.empty?
|
99
126
|
halt [404, {"Content-Type" => "text/html"}, [erb(sidekiq_status_template(:status_not_found))]]
|
100
127
|
else
|
101
|
-
@status =
|
128
|
+
@status = add_details_to_status(job)
|
102
129
|
erb(sidekiq_status_template(:status))
|
103
130
|
end
|
104
131
|
end
|
132
|
+
|
133
|
+
# Retries a failed job from the status list
|
134
|
+
app.put '/statuses' do
|
135
|
+
job = Sidekiq::RetrySet.new.find_job(params[:jid])
|
136
|
+
job ||= Sidekiq::DeadSet.new.find_job(params[:jid])
|
137
|
+
job.retry if job
|
138
|
+
halt [302, { "Location" => request.referer }, []]
|
139
|
+
end
|
140
|
+
|
141
|
+
# Removes a completed job from the status list
|
142
|
+
app.delete '/statuses' do
|
143
|
+
Sidekiq::Status.delete(params[:jid])
|
144
|
+
halt [302, { "Location" => request.referer }, []]
|
145
|
+
end
|
105
146
|
end
|
106
147
|
end
|
107
148
|
end
|
108
149
|
|
109
150
|
require 'sidekiq/web' unless defined?(Sidekiq::Web)
|
110
151
|
Sidekiq::Web.register(Sidekiq::Status::Web)
|
152
|
+
["per_page", "sort_by", "sort_dir"].each do |key|
|
153
|
+
Sidekiq::WebHelpers::SAFE_QPARAMS.push(key)
|
154
|
+
end
|
111
155
|
if Sidekiq::Web.tabs.is_a?(Array)
|
112
156
|
# For sidekiq < 2.5
|
113
157
|
Sidekiq::Web.tabs << "statuses"
|
@@ -27,14 +27,16 @@ 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)
|
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)
|
39
|
+
@_status_total = num
|
38
40
|
store(total: num)
|
39
41
|
end
|
40
42
|
|
data/sidekiq-status.gemspec
CHANGED
@@ -2,8 +2,8 @@
|
|
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
8
|
gem.homepage = 'http://github.com/utgarda/sidekiq-status'
|
9
9
|
gem.license = 'MIT'
|
@@ -14,8 +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', '>= 3.0'
|
18
18
|
gem.add_dependency 'chronic_duration'
|
19
|
+
gem.add_development_dependency 'appraisal'
|
20
|
+
gem.add_development_dependency 'colorize'
|
19
21
|
gem.add_development_dependency 'rack-test'
|
20
22
|
gem.add_development_dependency 'rake'
|
21
23
|
gem.add_development_dependency 'rspec'
|
@@ -5,18 +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
|
13
|
+
|
10
14
|
it "sets queued status" do
|
11
|
-
|
12
|
-
expect(StubJob.perform_async(:arg1 => 'val1')).to eq(job_id)
|
15
|
+
expect(StubJob.perform_async arg1: 'val1').to eq(job_id)
|
13
16
|
expect(redis.hget("sidekiq:status:#{job_id}", :status)).to eq('queued')
|
14
17
|
expect(Sidekiq::Status::queued?(job_id)).to be_truthy
|
15
18
|
end
|
16
19
|
|
17
20
|
it "sets status hash ttl" do
|
18
|
-
|
19
|
-
expect(StubJob.perform_async(:arg1 => 'val1')).to eq(job_id)
|
21
|
+
expect(StubJob.perform_async arg1: 'val1').to eq(job_id)
|
20
22
|
expect(1..Sidekiq::Status::DEFAULT_EXPIRY).to cover redis.ttl("sidekiq:status:#{job_id}")
|
21
23
|
end
|
22
24
|
|
@@ -35,18 +37,22 @@ describe Sidekiq::Status::ClientMiddleware do
|
|
35
37
|
Sidekiq::Status::ClientMiddleware.new.call(StubJob, {'jid' => SecureRandom.hex}, :queued) do end
|
36
38
|
end
|
37
39
|
end
|
40
|
+
|
38
41
|
end
|
39
42
|
|
40
|
-
describe ":expiration parameter" do
|
43
|
+
describe "with :expiration parameter" do
|
44
|
+
|
41
45
|
let(:huge_expiration) { Sidekiq::Status::DEFAULT_EXPIRY * 100 }
|
46
|
+
|
47
|
+
# Ensure client middleware is loaded with an expiration parameter set
|
42
48
|
before do
|
43
|
-
|
49
|
+
client_middleware expiration: huge_expiration
|
44
50
|
end
|
45
51
|
|
46
52
|
it "overwrites default expiry value" do
|
47
|
-
|
48
|
-
StubJob.perform_async(:arg1 => 'val1')
|
53
|
+
StubJob.perform_async arg1: 'val1'
|
49
54
|
expect((Sidekiq::Status::DEFAULT_EXPIRY+1)..huge_expiration).to cover redis.ttl("sidekiq:status:#{job_id}")
|
50
55
|
end
|
56
|
+
|
51
57
|
end
|
52
58
|
end
|
@@ -5,15 +5,18 @@ describe Sidekiq::Status::ServerMiddleware do
|
|
5
5
|
let!(:redis) { Sidekiq.redis { |conn| conn } }
|
6
6
|
let!(:job_id) { SecureRandom.hex(12) }
|
7
7
|
|
8
|
-
describe "
|
8
|
+
describe "without :expiration parameter" do
|
9
9
|
it "sets working/complete status" do
|
10
|
-
thread = confirmations_thread 4, "status_updates", "job_messages_#{job_id}"
|
11
10
|
allow(SecureRandom).to receive(:hex).once.and_return(job_id)
|
12
11
|
start_server do
|
13
|
-
|
14
|
-
expect(
|
15
|
-
|
16
|
-
|
12
|
+
thread = redis_thread 4, "status_updates", "job_messages_#{job_id}"
|
13
|
+
expect(ConfirmationJob.perform_async arg1: 'val1').to eq(job_id)
|
14
|
+
expect(thread.value).to eq([
|
15
|
+
job_id,
|
16
|
+
job_id,
|
17
|
+
"while in #perform, status = working",
|
18
|
+
job_id
|
19
|
+
])
|
17
20
|
end
|
18
21
|
expect(redis.hget("sidekiq:status:#{job_id}", :status)).to eq('complete')
|
19
22
|
expect(Sidekiq::Status::complete?(job_id)).to be_truthy
|
@@ -30,6 +33,17 @@ describe Sidekiq::Status::ServerMiddleware do
|
|
30
33
|
expect(Sidekiq::Status::failed?(job_id)).to be_truthy
|
31
34
|
end
|
32
35
|
|
36
|
+
it "sets failed status when Exception raised" do
|
37
|
+
allow(SecureRandom).to receive(:hex).once.and_return(job_id)
|
38
|
+
start_server do
|
39
|
+
expect(capture_status_updates(3) {
|
40
|
+
expect(FailingHardJob.perform_async).to eq(job_id)
|
41
|
+
}).to eq([job_id]*3)
|
42
|
+
end
|
43
|
+
expect(redis.hget("sidekiq:status:#{job_id}", :status)).to eq('failed')
|
44
|
+
expect(Sidekiq::Status::failed?(job_id)).to be_truthy
|
45
|
+
end
|
46
|
+
|
33
47
|
context "sets interrupted status" do
|
34
48
|
it "on system exit signal" do
|
35
49
|
allow(SecureRandom).to receive(:hex).once.and_return(job_id)
|
@@ -58,13 +72,13 @@ describe Sidekiq::Status::ServerMiddleware do
|
|
58
72
|
it "sets status hash ttl" do
|
59
73
|
allow(SecureRandom).to receive(:hex).once.and_return(job_id)
|
60
74
|
start_server do
|
61
|
-
expect(StubJob.perform_async
|
75
|
+
expect(StubJob.perform_async arg1: 'val1').to eq(job_id)
|
62
76
|
end
|
63
77
|
expect(1..Sidekiq::Status::DEFAULT_EXPIRY).to cover redis.ttl("sidekiq:status:#{job_id}")
|
64
78
|
end
|
65
79
|
end
|
66
80
|
|
67
|
-
describe ":expiration parameter" do
|
81
|
+
describe "with :expiration parameter" do
|
68
82
|
let(:huge_expiration) { Sidekiq::Status::DEFAULT_EXPIRY * 100 }
|
69
83
|
before do
|
70
84
|
allow(SecureRandom).to receive(:hex).once.and_return(job_id)
|
@@ -72,7 +86,7 @@ describe Sidekiq::Status::ServerMiddleware do
|
|
72
86
|
|
73
87
|
it "overwrites default expiry value" do
|
74
88
|
start_server(:expiration => huge_expiration) do
|
75
|
-
StubJob.perform_async
|
89
|
+
StubJob.perform_async arg1: 'val1'
|
76
90
|
end
|
77
91
|
expect((Sidekiq::Status::DEFAULT_EXPIRY-1)..huge_expiration).to cover redis.ttl("sidekiq:status:#{job_id}")
|
78
92
|
end
|
@@ -81,7 +95,7 @@ describe Sidekiq::Status::ServerMiddleware do
|
|
81
95
|
overwritten_expiration = huge_expiration * 100
|
82
96
|
allow_any_instance_of(StubJob).to receive(:expiration).and_return(overwritten_expiration)
|
83
97
|
start_server(:expiration => huge_expiration) do
|
84
|
-
StubJob.perform_async
|
98
|
+
StubJob.perform_async arg1: 'val1'
|
85
99
|
end
|
86
100
|
expect((huge_expiration+1)..overwritten_expiration).to cover redis.ttl("sidekiq:status:#{job_id}")
|
87
101
|
end
|