sidekiq-status 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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