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.
@@ -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
- # 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
33
- end
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
- store_status worker.jid, :working, @expiration
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, @expiration
55
+ store_status worker.jid, :complete, expiry
39
56
  rescue Worker::Stopped
40
- store_status worker.jid, :stopped, @expiration
57
+ store_status worker.jid, :stopped, expiry
41
58
  rescue SystemExit, Interrupt
42
- store_status worker.jid, :interrupted, @expiration
59
+ store_status worker.jid, :interrupted, expiry
43
60
  raise
44
- rescue
45
- store_status worker.jid, :failed, @expiration
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
@@ -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
@@ -11,6 +11,10 @@ module Sidekiq
11
11
  def store_status(id, status, expiration = nil, redis_pool=nil)
12
12
  'ok'
13
13
  end
14
+
15
+ def store_for_id(id, status_updates, expiration = nil, redis_pool=nil)
16
+ 'ok'
17
+ end
14
18
  end
15
19
  end
16
20
 
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module Status
3
- VERSION = '0.7.0'
3
+ VERSION = '0.8.0'
4
4
  end
5
5
  end
@@ -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"] = pct_complete(status)
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 << OpenStruct.new(status)
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.send(sort_by) <=> y.send(sort_by) }
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.send(sort_by) <=> y.send(sort_by) }
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
- { id: "worker", name: "Worker / JID", class: nil, url: nil},
79
- { id: "args", name: "Arguments", class: nil, url: nil},
80
- { id: "status", name: "Status", class: nil, url: nil},
81
- { id: "update_time", name: "Last Updated", class: nil, url: nil},
82
- { id: "pct_complete", name: "Progress", class: nil, url: nil},
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
- params["sort_by"] = h[:id]
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 = OpenStruct.new(add_details_to_status(job))
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
- 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)
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
 
@@ -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', '>= 2.7'
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
- describe "#call" do
9
- before { client_middleware }
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
- allow(SecureRandom).to receive(:hex).once.and_return(job_id)
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
- allow(SecureRandom).to receive(:hex).once.and_return(job_id)
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
- allow(SecureRandom).to receive(:hex).once.and_return(job_id)
49
+ client_middleware expiration: huge_expiration
44
50
  end
45
51
 
46
52
  it "overwrites default expiry value" do
47
- client_middleware(expiration: huge_expiration)
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 "#call" do
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
- expect(ConfirmationJob.perform_async(:arg1 => 'val1')).to eq(job_id)
14
- expect(thread.value).to eq([job_id, job_id,
15
- "while in #perform, status = working",
16
- job_id])
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(:arg1 => 'val1')).to eq(job_id)
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(:arg1 => 'val1')
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(:arg1 => 'val1')
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