sidekiq-status 0.5.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.
Files changed (39) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yaml +53 -0
  3. data/.gitignore +4 -1
  4. data/.gitlab-ci.yml +17 -0
  5. data/Appraisals +11 -0
  6. data/CHANGELOG.md +41 -0
  7. data/README.md +148 -41
  8. data/Rakefile +2 -0
  9. data/gemfiles/sidekiq_6.1.gemfile +7 -0
  10. data/gemfiles/sidekiq_6.x.gemfile +7 -0
  11. data/gemfiles/sidekiq_7.x.gemfile +7 -0
  12. data/lib/sidekiq-status/client_middleware.rb +54 -1
  13. data/lib/sidekiq-status/redis_adapter.rb +18 -0
  14. data/lib/sidekiq-status/redis_client_adapter.rb +14 -0
  15. data/lib/sidekiq-status/server_middleware.rb +76 -17
  16. data/lib/sidekiq-status/sidekiq_extensions.rb +7 -0
  17. data/lib/sidekiq-status/storage.rb +26 -13
  18. data/lib/sidekiq-status/testing/inline.rb +10 -0
  19. data/lib/sidekiq-status/version.rb +1 -1
  20. data/lib/sidekiq-status/web.rb +158 -10
  21. data/lib/sidekiq-status/worker.rb +12 -5
  22. data/lib/sidekiq-status.rb +43 -10
  23. data/sidekiq-status.gemspec +9 -4
  24. data/spec/environment.rb +1 -0
  25. data/spec/lib/sidekiq-status/client_middleware_spec.rb +30 -13
  26. data/spec/lib/sidekiq-status/server_middleware_spec.rb +102 -30
  27. data/spec/lib/sidekiq-status/web_spec.rb +84 -0
  28. data/spec/lib/sidekiq-status/worker_spec.rb +21 -2
  29. data/spec/lib/sidekiq-status_spec.rb +158 -77
  30. data/spec/spec_helper.rb +104 -24
  31. data/spec/support/test_jobs.rb +84 -7
  32. data/web/sidekiq-status-single-web.png +0 -0
  33. data/web/sidekiq-status-web.png +0 -0
  34. data/web/views/status.erb +118 -0
  35. data/web/views/status_not_found.erb +6 -0
  36. data/web/views/statuses.erb +135 -17
  37. metadata +102 -16
  38. data/.travis.yml +0 -2
  39. data/CHANGELOG +0 -2
@@ -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,25 +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
- # a way of overriding default expiration time,
26
- # so worker wouldn't lose its data
27
- # and it allows also to overwrite global expiration time on worker basis
28
- if worker.respond_to? :expiration
29
- if !worker.expiration && worker.respond_to?(:expiration=)
30
- worker.expiration = @expiration
31
- else
32
- @expiration = worker.expiration
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
- store_status worker.jid, :working, @expiration
37
- yield
38
- store_status worker.jid, :complete, @expiration
39
- rescue Worker::Stopped
40
- store_status worker.jid, :stopped, @expiration
41
- rescue
42
- store_status worker.jid, :failed, @expiration
43
- raise
84
+ def sidekiq_version_dependent_retry_offset
85
+ Sidekiq.major_version >= 4 ? 1 : 0
44
86
  end
45
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
46
105
  end
@@ -0,0 +1,7 @@
1
+ require 'sidekiq/version'
2
+
3
+ module Sidekiq
4
+ def self.major_version
5
+ VERSION.split('.').first.to_i
6
+ end
7
+ 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
- conn.hmset id, 'update_time', Time.now.to_i, *(status_updates.to_a.flatten(1))
18
- conn.expire id, (expiration || Sidekiq::Status::DEFAULT_EXPIRY)
19
- conn.publish "status_updates", id
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,14 +37,14 @@ 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.redis do |conn|
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?
43
44
  match = scan_scheduled_jobs_for_jid jobs, job_id
44
45
  unless match.nil?
45
46
  conn.zrem "schedule", match
46
- conn.del job_id
47
+ conn.del key(job_id)
47
48
  return true # Done
48
49
  end
49
50
  scan_options[:offset] += BATCH_LIMIT
@@ -52,13 +53,22 @@ 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.redis do |conn|
61
- conn.hmget(id, field)[0]
70
+ Sidekiq::Status.redis_adapter do |conn|
71
+ conn.hget(key(id), field)
62
72
  end
63
73
  end
64
74
 
@@ -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.redis do |conn|
70
- conn.hgetall id
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].zrangebyscore "schedule", options[:start], options[:end], {limit: [options[:offset], BATCH_LIMIT]}
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
@@ -91,8 +101,7 @@ module Sidekiq::Status::Storage
91
101
  # A Little skecthy, I know, but the structure of these internal JSON
92
102
  # is predefined in such a way where this will not catch unintentional elements,
93
103
  # and this is notably faster than performing JSON.parse() for every listing:
94
- scheduled_jobs.each { |job_listing| (return job_listing) if job_listing.include?("\"jid\":\"#{job_id}") }
95
- nil
104
+ scheduled_jobs.select { |job_listing| job_listing.match(/\"jid\":\"#{job_id}\"/) }[0]
96
105
  end
97
106
 
98
107
  # Yields redis connection. Uses redis pool if available.
@@ -108,4 +117,8 @@ module Sidekiq::Status::Storage
108
117
  end
109
118
  end
110
119
  end
120
+
121
+ def key(id)
122
+ "sidekiq:status:#{id}"
123
+ end
111
124
  end
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module Status
3
- VERSION = "0.5.0"
3
+ VERSION = '3.0.3'
4
4
  end
5
5
  end
@@ -6,38 +6,186 @@ 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
15
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
16
95
  end
17
96
 
18
97
  app.get '/statuses' do
19
- queue = Sidekiq::Workers.new
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
20
104
  @statuses = []
21
105
 
22
- queue.each do |process, name, work, started_at|
23
- job = Struct.new(:jid, :klass, :args).new(work["payload"]["jid"], work["payload"]["class"], work["payload"]["args"])
24
- status = Sidekiq::Status::get_all job.jid
106
+ jids.each do |jid|
107
+ status = Sidekiq::Status::get_all jid
25
108
  next if !status || status.count < 2
26
- status["worker"] = job.klass
27
- status["args"] = job.args
28
- status["jid"] = job.jid
29
- status["pct_complete"] = ((status["at"].to_f / status["total"].to_f) * 100).to_i if status["total"].to_f > 0
30
- @statuses << OpenStruct.new(status)
109
+ status = add_details_to_status(status)
110
+ @statuses << status
111
+ end
112
+
113
+ sort_by = has_sort_by?(params[:sort_by]) ? params[:sort_by] : "update_time"
114
+ sort_dir = "asc"
115
+
116
+ if params[:sort_dir] == "asc"
117
+ @statuses = @statuses.sort { |x,y| (x[sort_by] <=> y[sort_by]) || -1 }
118
+ else
119
+ sort_dir = "desc"
120
+ @statuses = @statuses.sort { |y,x| (x[sort_by] <=> y[sort_by]) || 1 }
121
+ end
122
+
123
+ if params[:status] && params[:status] != "all"
124
+ @statuses = @statuses.select {|job_status| job_status["status"] == params[:status] }
125
+ end
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
+
134
+ @headers = [
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},
142
+ ]
143
+
144
+ @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("&")
146
+ h[:class] = "sorted_#{sort_dir}" if sort_by == h[:id]
31
147
  end
32
148
 
33
149
  erb(sidekiq_status_template(:statuses))
34
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
35
176
  end
36
177
  end
37
178
  end
38
179
 
39
- require 'sidekiq/web' unless defined?(Sidekiq::Web)
180
+ unless defined?(Sidekiq::Web)
181
+ require 'delegate' # Needed for sidekiq 5.x
182
+ require 'sidekiq/web'
183
+ end
184
+
40
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
41
189
  if Sidekiq::Web.tabs.is_a?(Array)
42
190
  # For sidekiq < 2.5
43
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
- total(100) if retrieve(:total).nil?
31
- store(at: num, message: message)
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
- store(total: num)
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
@@ -1,14 +1,18 @@
1
- require "sidekiq-status/version"
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 = %w(queued working complete stopped failed).map(&:to_sym).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(id)
25
- read_hash_for_id(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,14 +38,16 @@ 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|
40
- class_eval(<<-END, __FILE__, __LINE__)
41
- def #{name}?(job_id)
42
- status(job_id) == :#{name}
43
- end
44
- END
48
+ define_method("#{name}?") do |job_id|
49
+ status(job_id) == name
50
+ end
45
51
  end
46
52
 
47
53
  # Methods for retrieving job completion
@@ -54,11 +60,38 @@ module Sidekiq::Status
54
60
  end
55
61
 
56
62
  def pct_complete(job_id)
57
- (at(job_id).to_f / total(job_id)) * 100
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)
58
79
  end
59
80
 
60
81
  def message(job_id)
61
82
  get(job_id, :message)
62
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
63
96
  end
64
97
  end
@@ -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 = 'http://github.com/utgarda/sidekiq-status'
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,12 @@ 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', '>= 2.7', '< 3.1'
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'
21
+ gem.add_development_dependency 'rack-test'
18
22
  gem.add_development_dependency 'rake'
19
23
  gem.add_development_dependency 'rspec'
24
+ gem.add_development_dependency 'sinatra'
20
25
  end
@@ -0,0 +1 @@
1
+ # This file has been intentionally left blank
@@ -5,37 +5,54 @@ describe Sidekiq::Status::ClientMiddleware do
5
5
  let!(:redis) { Sidekiq.redis { |conn| conn } }
6
6
  let!(:job_id) { SecureRandom.hex(12) }
7
7
 
8
- # Clean Redis before each test
9
- before { redis.flushall }
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
14
  it "sets queued status" do
13
- SecureRandom.should_receive(:hex).once.and_return(job_id)
14
- StubJob.perform_async(:arg1 => 'val1').should == job_id
15
- redis.hget(job_id, :status).should == 'queued'
16
- Sidekiq::Status::queued?(job_id).should be_true
15
+ expect(StubJob.perform_async 'arg1' => 'val1').to eq(job_id)
16
+ expect(redis.hget("sidekiq:status:#{job_id}", :status)).to eq('queued')
17
+ expect(Sidekiq::Status::queued?(job_id)).to be_truthy
17
18
  end
18
19
 
19
20
  it "sets status hash ttl" do
20
- SecureRandom.should_receive(:hex).once.and_return(job_id)
21
- StubJob.perform_async(:arg1 => 'val1').should == job_id
22
- (1..Sidekiq::Status::DEFAULT_EXPIRY).should cover redis.ttl(job_id)
21
+ expect(StubJob.perform_async 'arg1' => 'val1').to eq(job_id)
22
+ expect(1..Sidekiq::Status::DEFAULT_EXPIRY).to cover redis.ttl("sidekiq:status:#{job_id}")
23
23
  end
24
24
 
25
25
  context "when redis_pool passed" do
26
26
  it "uses redis_pool" do
27
27
  redis_pool = double(:redis_pool)
28
- redis_pool.should_receive(:with)
29
- Sidekiq.should_not_receive(:redis)
28
+ allow(redis_pool).to receive(:with)
29
+ expect(Sidekiq).to_not receive(:redis)
30
30
  Sidekiq::Status::ClientMiddleware.new.call(StubJob, {'jid' => SecureRandom.hex}, :queued, redis_pool) do end
31
31
  end
32
32
  end
33
33
 
34
34
  context "when redis_pool is not passed" do
35
35
  it "uses Sidekiq.redis" do
36
- Sidekiq.should_receive(:redis)
36
+ allow(Sidekiq).to receive(:redis)
37
37
  Sidekiq::Status::ClientMiddleware.new.call(StubJob, {'jid' => SecureRandom.hex}, :queued) do end
38
38
  end
39
39
  end
40
+
41
+ end
42
+
43
+ describe "with :expiration parameter" do
44
+
45
+ let(:huge_expiration) { Sidekiq::Status::DEFAULT_EXPIRY * 100 }
46
+
47
+ # Ensure client middleware is loaded with an expiration parameter set
48
+ before do
49
+ client_middleware expiration: huge_expiration
50
+ end
51
+
52
+ it "overwrites default expiry value" do
53
+ StubJob.perform_async 'arg1' => 'val1'
54
+ expect((Sidekiq::Status::DEFAULT_EXPIRY+1)..huge_expiration).to cover redis.ttl("sidekiq:status:#{job_id}")
55
+ end
56
+
40
57
  end
41
58
  end