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.
@@ -21,7 +21,7 @@ describe 'sidekiq status web' do
21
21
 
22
22
  it 'shows the list of jobs in progress' do
23
23
  capture_status_updates(2) do
24
- expect(LongJob.perform_async(1)).to eq(job_id)
24
+ expect(LongJob.perform_async(0.5)).to eq(job_id)
25
25
  end
26
26
 
27
27
  get '/statuses'
@@ -13,7 +13,7 @@ describe Sidekiq::Status::Worker do
13
13
 
14
14
  describe ".expiration" do
15
15
  subject { StubJob.new }
16
-
16
+
17
17
  it "allows to set/get expiration" do
18
18
  expect(subject.expiration).to be_nil
19
19
  subject.expiration = :val
@@ -15,11 +15,12 @@ describe Sidekiq::Status do
15
15
 
16
16
  start_server do
17
17
  expect(capture_status_updates(2) {
18
- expect(LongJob.perform_async(1)).to eq(job_id)
18
+ expect(LongJob.perform_async(0.5)).to eq(job_id)
19
19
  }).to eq([job_id]*2)
20
20
  expect(Sidekiq::Status.status(job_id)).to eq(:working)
21
21
  expect(Sidekiq::Status.working?(job_id)).to be_truthy
22
22
  expect(Sidekiq::Status::queued?(job_id)).to be_falsey
23
+ expect(Sidekiq::Status::retrying?(job_id)).to be_falsey
23
24
  expect(Sidekiq::Status::failed?(job_id)).to be_falsey
24
25
  expect(Sidekiq::Status::complete?(job_id)).to be_falsey
25
26
  expect(Sidekiq::Status::stopped?(job_id)).to be_falsey
@@ -49,9 +50,9 @@ describe Sidekiq::Status do
49
50
  allow(SecureRandom).to receive(:hex).once.and_return(job_id)
50
51
 
51
52
  start_server do
52
- expect(capture_status_updates(3) {
53
+ expect(capture_status_updates(4) {
53
54
  expect(ProgressJob.perform_async).to eq(job_id)
54
- }).to eq([job_id]*3)
55
+ }).to eq([job_id]*4)
55
56
  end
56
57
  expect(Sidekiq::Status.at(job_id)).to be(100)
57
58
  expect(Sidekiq::Status.total(job_id)).to be(500)
@@ -67,7 +68,7 @@ describe Sidekiq::Status do
67
68
 
68
69
  start_server do
69
70
  expect(capture_status_updates(2) {
70
- expect(LongJob.perform_async(1)).to eq(job_id)
71
+ expect(LongJob.perform_async(0.5)).to eq(job_id)
71
72
  }).to eq([job_id]*2)
72
73
  expect(hash = Sidekiq::Status.get_all(job_id)).to include 'status' => 'working'
73
74
  expect(hash).to include 'update_time'
@@ -82,7 +83,7 @@ describe Sidekiq::Status do
82
83
  allow(SecureRandom).to receive(:hex).once.and_return(job_id)
83
84
  start_server do
84
85
  expect(capture_status_updates(2) {
85
- expect(LongJob.perform_async(1)).to eq(job_id)
86
+ expect(LongJob.perform_async(0.5)).to eq(job_id)
86
87
  }).to eq([job_id]*2)
87
88
  end
88
89
  expect(Sidekiq::Status.delete(job_id)).to eq(1)
@@ -147,13 +148,30 @@ describe Sidekiq::Status do
147
148
  end
148
149
 
149
150
  it "retries failed jobs" do
150
- allow(SecureRandom).to receive(:hex).once.and_return(retried_job_id)
151
+ allow(SecureRandom).to receive(:hex).and_return(retried_job_id)
151
152
  start_server do
152
- expect(capture_status_updates(5) {
153
+ expect(capture_status_updates(3) {
153
154
  expect(RetriedJob.perform_async()).to eq(retried_job_id)
154
- }).to eq([retried_job_id] * 5)
155
+ }).to eq([retried_job_id] * 3)
156
+ expect(Sidekiq::Status.status(retried_job_id)).to eq(:retrying)
157
+ expect(Sidekiq::Status.working?(retried_job_id)).to be_falsey
158
+ expect(Sidekiq::Status::queued?(retried_job_id)).to be_falsey
159
+ expect(Sidekiq::Status::retrying?(retried_job_id)).to be_truthy
160
+ expect(Sidekiq::Status::failed?(retried_job_id)).to be_falsey
161
+ expect(Sidekiq::Status::complete?(retried_job_id)).to be_falsey
162
+ expect(Sidekiq::Status::stopped?(retried_job_id)).to be_falsey
163
+ expect(Sidekiq::Status::interrupted?(retried_job_id)).to be_falsey
164
+ end
165
+ expect(Sidekiq::Status.status(retried_job_id)).to eq(:retrying)
166
+ expect(Sidekiq::Status::retrying?(retried_job_id)).to be_truthy
167
+
168
+ # restarting and waiting for the job to complete
169
+ start_server do
170
+ expect(capture_status_updates(3) {}).to eq([retried_job_id] * 3)
171
+ expect(Sidekiq::Status.status(retried_job_id)).to eq(:complete)
172
+ expect(Sidekiq::Status.complete?(retried_job_id)).to be_truthy
173
+ expect(Sidekiq::Status::retrying?(retried_job_id)).to be_falsey
155
174
  end
156
- expect(Sidekiq::Status.status(retried_job_id)).to eq(:complete)
157
175
  end
158
176
 
159
177
  context ":expiration param" do
@@ -166,7 +184,7 @@ describe Sidekiq::Status do
166
184
  expect_2_jobs_ttl_covers (Sidekiq::Status::DEFAULT_EXPIRY+1)..expiration_param
167
185
  end
168
186
 
169
- it "allow to overwrite :expiration parameter by .expiration method from worker" do
187
+ it "allow to overwrite :expiration parameter by #expiration method from worker" do
170
188
  overwritten_expiration = expiration_param * 100
171
189
  allow_any_instance_of(NoStatusConfirmationJob).to receive(:expiration).
172
190
  and_return(overwritten_expiration)
@@ -176,6 +194,16 @@ describe Sidekiq::Status do
176
194
  expect_2_jobs_are_done_and_status_eq :complete
177
195
  expect_2_jobs_ttl_covers (expiration_param+1)..overwritten_expiration
178
196
  end
197
+
198
+ it "reads #expiration from a method when defined" do
199
+ allow(SecureRandom).to receive(:hex).once.and_return(job_id, job_id_1)
200
+ start_server do
201
+ expect(StubJob.perform_async).to eq(job_id)
202
+ expect(ExpiryJob.perform_async).to eq(job_id_1)
203
+ expect(redis.ttl("sidekiq:status:#{job_id}")).to eq(30 * 60)
204
+ expect(redis.ttl("sidekiq:status:#{job_id_1}")).to eq(15)
205
+ end
206
+ end
179
207
  end
180
208
 
181
209
  def seed_secure_random_with_job_ids
@@ -185,12 +213,12 @@ describe Sidekiq::Status do
185
213
 
186
214
  def run_2_jobs!
187
215
  start_server(:expiration => expiration_param) do
188
- expect(capture_status_updates(12) {
216
+ expect(capture_status_updates(6) {
189
217
  expect(StubJob.perform_async).to eq(plain_sidekiq_job_id)
190
218
  NoStatusConfirmationJob.perform_async(1)
191
219
  expect(StubJob.perform_async).to eq(job_id_1)
192
220
  NoStatusConfirmationJob.perform_async(2)
193
- }).to match_array([plain_sidekiq_job_id, job_id_1] * 6)
221
+ }).to match_array([plain_sidekiq_job_id, job_id_1] * 3)
194
222
  end
195
223
  end
196
224
 
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  require "rspec"
2
-
2
+ require 'colorize'
3
3
  require 'sidekiq'
4
+
5
+ # Celluloid should only be manually required before Sidekiq versions 4.+
6
+ require 'sidekiq/version'
7
+ require 'celluloid' if Gem::Version.new(Sidekiq::VERSION) < Gem::Version.new('4.0')
8
+
4
9
  require 'sidekiq/processor'
5
10
  require 'sidekiq/manager'
6
11
  require 'sidekiq-status'
@@ -9,71 +14,98 @@ require 'sidekiq-status'
9
14
  RSpec.configure do |config|
10
15
  config.before(:each) do
11
16
  Sidekiq.redis { |conn| conn.flushall }
17
+ client_middleware
12
18
  sleep 0.05
13
19
  end
14
20
  end
15
21
 
16
22
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
17
23
 
18
- def client_middleware(client_middleware_options={})
24
+ # Configures client middleware
25
+ def client_middleware client_middleware_options = {}
19
26
  Sidekiq.configure_client do |config|
20
- config.client_middleware do |chain|
21
- chain.add Sidekiq::Status::ClientMiddleware, client_middleware_options
22
- end
27
+ Sidekiq::Status.configure_client_middleware config, client_middleware_options
23
28
  end
24
29
  end
25
30
 
26
- def confirmations_thread(messages_limit, *channels)
31
+ def redis_thread messages_limit, *channels
32
+
27
33
  parent = Thread.current
28
34
  thread = Thread.new {
29
- confirmations = []
35
+ messages = []
30
36
  Sidekiq.redis do |conn|
31
- conn.subscribe *channels do |on|
37
+ puts "Subscribing to #{channels} for #{messages_limit.to_s.bold} messages".cyan if ENV['DEBUG']
38
+ conn.subscribe_with_timeout 30, *channels do |on|
32
39
  on.subscribe do |ch, subscriptions|
40
+ puts "Subscribed to #{ch}".cyan if ENV['DEBUG']
33
41
  if subscriptions == channels.size
34
42
  sleep 0.1 while parent.status != "sleep"
35
43
  parent.run
36
44
  end
37
45
  end
38
46
  on.message do |ch, msg|
39
- confirmations << msg
40
- conn.unsubscribe if confirmations.length >= messages_limit
47
+ puts "Message received: #{ch} -> #{msg}".white if ENV['DEBUG']
48
+ messages << msg
49
+ conn.unsubscribe if messages.length >= messages_limit
41
50
  end
42
51
  end
43
52
  end
44
- confirmations
53
+ puts "Returing from thread".cyan if ENV['DEBUG']
54
+ messages
45
55
  }
56
+
46
57
  Thread.stop
47
58
  yield if block_given?
48
59
  thread
60
+
49
61
  end
50
62
 
51
- def capture_status_updates(n, &block)
52
- confirmations_thread(n, "status_updates", &block).value
63
+ def capture_status_updates n, &block
64
+ redis_thread(n, "status_updates", &block).value
53
65
  end
54
66
 
55
- def start_server(server_middleware_options={})
67
+ # Configures server middleware and launches a sidekiq server
68
+ def start_server server_middleware_options = {}
69
+
70
+ # Creates a process for the Sidekiq server
56
71
  pid = Process.fork do
57
- $stdout.reopen File::NULL, 'w'
58
- $stderr.reopen File::NULL, 'w'
72
+
73
+ # Redirect the server's outputs
74
+ $stdout.reopen File::NULL, 'w' unless ENV['DEBUG']
75
+ $stderr.reopen File::NULL, 'w' unless ENV['DEBUG']
76
+
77
+ # Load and configure server options
59
78
  require 'sidekiq/cli'
60
79
  Sidekiq.options[:queues] << 'default'
61
- Sidekiq.options[:require] = File.expand_path('environment.rb', File.dirname(__FILE__))
80
+ Sidekiq.options[:require] = File.expand_path 'environment.rb', File.dirname(__FILE__)
62
81
  Sidekiq.options[:timeout] = 1
63
82
  Sidekiq.options[:concurrency] = 5
83
+
84
+ # Add the server middleware
64
85
  Sidekiq.configure_server do |config|
65
86
  config.redis = Sidekiq::RedisConnection.create
66
- config.server_middleware do |chain|
67
- chain.add Sidekiq::Status::ServerMiddleware, server_middleware_options
68
- end
87
+ Sidekiq::Status.configure_server_middleware config, server_middleware_options
69
88
  end
89
+
90
+ # Launch
91
+ puts "Server starting".yellow if ENV['DEBUG']
70
92
  Sidekiq::CLI.instance.run
93
+
71
94
  end
72
95
 
96
+ # Run the client-side code
73
97
  yield
74
- sleep 0.1
98
+
99
+ # Pause to ensure all jobs are picked up & started before TERM is sent
100
+ sleep 0.2
101
+
102
+ # Attempt to shut down the server normally
75
103
  Process.kill 'TERM', pid
76
- Timeout::timeout(5) { Process.wait pid } rescue Timeout::Error
104
+ Process.wait pid
105
+
77
106
  ensure
107
+
108
+ # Ensure the server is actually dead
78
109
  Process.kill 'KILL', pid rescue "OK" # it's OK if the process is gone already
110
+
79
111
  end
@@ -4,12 +4,18 @@ class StubJob
4
4
  include Sidekiq::Worker
5
5
  include Sidekiq::Status::Worker
6
6
 
7
- sidekiq_options 'retry' => 'false'
7
+ sidekiq_options 'retry' => false
8
8
 
9
9
  def perform(*args)
10
10
  end
11
11
  end
12
12
 
13
+ class ExpiryJob < StubJob
14
+ def expiration
15
+ 15
16
+ end
17
+ end
18
+
13
19
  class LongJob < StubJob
14
20
  def perform(*args)
15
21
  sleep args[0] || 0.25
@@ -56,6 +62,12 @@ class FailingJob < StubJob
56
62
  end
57
63
  end
58
64
 
65
+ class FailingHardJob < StubJob
66
+ def perform
67
+ raise Exception
68
+ end
69
+ end
70
+
59
71
  class ExitedJob < StubJob
60
72
  def perform
61
73
  raise SystemExit
@@ -69,11 +81,13 @@ class InterruptedJob < StubJob
69
81
  end
70
82
 
71
83
  class RetriedJob < StubJob
72
- sidekiq_options 'retry' => 'true'
84
+
85
+ sidekiq_options 'retry' => true
86
+ sidekiq_retry_in do |count| 3 end # 3 second delay > job timeout in test suite
87
+
73
88
  def perform()
74
89
  Sidekiq.redis do |conn|
75
90
  key = "RetriedJob_#{jid}"
76
- sleep 1
77
91
  unless conn.exists key
78
92
  conn.set key, 'tried'
79
93
  raise StandardError
data/web/views/status.erb CHANGED
@@ -14,30 +14,30 @@
14
14
  </style>
15
15
 
16
16
  <h3>
17
- Job Status: <%= @status.jid %>
18
- <span class='label label-<%= @status.label %>'>
19
- <%= @status.status %>
17
+ Job Status: <%= @status["jid"] %>
18
+ <span class='label label-<%= @status["label"] %>'>
19
+ <%= @status["status"] %>
20
20
  </span>
21
21
  </h3>
22
22
 
23
23
  <div class="progress" style="height: 30px;">
24
- <div class="progress-bar" role="progressbar" aria-valuenow="<%= @status.pct_complete.to_i %>" aria-valuemin="0" aria-valuemax="100" style="width: <%= @status.pct_complete.to_i %>%">
24
+ <div class="progress-bar" role="progressbar" aria-valuenow="<%= @status["pct_complete"].to_i %>" aria-valuemin="0" aria-valuemax="100" style="width: <%= @status["pct_complete"].to_i %>%">
25
25
  <div class="progress-percentage">
26
- <%= @status.pct_complete.to_i %>%
26
+ <%= @status["pct_complete"].to_i %>%
27
27
  </div>
28
28
  </div>
29
29
  </div>
30
30
 
31
31
  <div class="panel panel-default">
32
32
  <div class="panel-body">
33
- <h4><%= @status.worker %></h4>
33
+ <h4><%= @status["worker"] %></h4>
34
34
 
35
35
  <div class="row">
36
36
  <div class="col-sm-2">
37
37
  <strong>Arguments</strong>
38
38
  </div>
39
39
  <div class="col-sm-10">
40
- <p><%= @status.args.empty? ? "<i>none</i>" : @status.args %></p>
40
+ <p><%= @status["args"].empty? ? "<i>none</i>" : @status["args"] %></p>
41
41
  </div>
42
42
  </div>
43
43
 
@@ -46,7 +46,7 @@
46
46
  <strong>Message</strong>
47
47
  </div>
48
48
  <div class="col-sm-10">
49
- <p><%= @status.message || "<i>none</i>" %></p>
49
+ <p><%= @status["message"] || "<i>none</i>" %></p>
50
50
  </div>
51
51
  </div>
52
52
 
@@ -56,9 +56,9 @@
56
56
  </div>
57
57
  <div class="col-sm-10">
58
58
  <p>
59
- <% secs = Time.now.to_i - @status.update_time.to_i %>
59
+ <% secs = Time.now.to_i - @status["update_time"].to_i %>
60
60
  <% if secs > 0 %>
61
- <%= secs %> sec<%= secs == 1 ? '' : 's' %> ago
61
+ <%= ChronicDuration.output(secs, :weeks => true, :units => 2) %> ago
62
62
  <% else %>
63
63
  Now
64
64
  <% end %>
@@ -1,4 +1,3 @@
1
-
2
1
  <style>
3
2
  .progress {
4
3
  background-color: #C8E1ED;
@@ -13,19 +12,61 @@
13
12
  font-weight: bold; padding-left: 4px;
14
13
  color: #333;
15
14
  }
16
- .header{
15
+ .actions {
17
16
  text-align: center;
18
17
  }
19
- .header_update_time{
18
+ .header {
19
+ text-align: center;
20
+ }
21
+ .header_update_time {
20
22
  width: 10%;
21
23
  }
22
- .header_pct_complete{
24
+ .header_pct_complete {
23
25
  width: 45%;
24
26
  }
25
-
27
+ .btn-warning {
28
+ background-image: linear-gradient(#f0ad4e, #eea236)
29
+ }
30
+ .nav-container {
31
+ display: flex;
32
+ line-height: 45px;
33
+ }
34
+ .nav-container .pull-right {
35
+ float: none !important;
36
+ }
37
+ .nav-container .pagination {
38
+ display: flex;
39
+ align-items: center;
40
+ }
41
+ .nav-container .per-page {
42
+ display: flex;
43
+ align-items: center;
44
+ margin: 20px 0 20px 10px;
45
+ white-space: nowrap;
46
+ }
47
+ .nav-container .per-page SELECT {
48
+ margin: 0 0 0 5px;
49
+ }
26
50
  </style>
27
-
28
- <h3 class="wi">Recent job statuses</h3>
51
+ <script>
52
+ function setPerPage(select){
53
+ window.location = select.options[select.selectedIndex].getAttribute('data-url')
54
+ }
55
+ </script>
56
+ <div style="display: flex; justify-content: space-between;">
57
+ <h3 class="wi">Recent job statuses</h3>
58
+ <div class="nav-container">
59
+ <%= erb :_paging, locals: { url: "#{root_path}statuses" } %>
60
+ <div class="per-page">
61
+ Per page:
62
+ <select class="form-control" onchange="setPerPage(this)">
63
+ <% (Sidekiq::Status::Web.per_page_opts + ['all']).each do |num| %>
64
+ <option data-url="?<%= qparams(page: 1, per_page: num)%>" value="<%= num %>" <%= 'selected="selected"' if num.to_s == (params[:per_page] || @count) %>><%= num %></option>
65
+ <% end %>
66
+ </select>
67
+ </div>
68
+ </div>
69
+ </div>
29
70
  <table class="table table-hover table-bordered table-striped table-white">
30
71
  <tr>
31
72
  <% @headers.each do |h| %>
@@ -33,20 +74,23 @@
33
74
  <a href="<%= h[:url] %>"><%= h[:name] %></a>
34
75
  </th>
35
76
  <% end %>
77
+ <th class="header">
78
+ Actions
79
+ </th>
36
80
  </tr>
37
81
  <% @statuses.each do |container| %>
38
82
  <tr>
39
83
  <td>
40
- <div title='<%= container.jid %>'><a href="<%= root_path %>statuses/<%= container.jid %>"><%= container.worker %></a></div>
84
+ <div title='<%= container["jid"] %>'><a href="<%= root_path %>statuses/<%= container["jid"] %>"><%= container["worker"] %></a></div>
41
85
  </td>
42
86
  <td>
43
- <div class='args' title='<%= container.jid %>'><%= container.args %></div>
87
+ <div class='args' title='<%= container["jid"] %>'><%= container["args"] %></div>
44
88
  </td>
45
89
  <td style='text-align: center;'>
46
- <div class='label label-<%= container.label %>'><%= container.status %></div>
90
+ <div class='label label-<%= container["label"] %>'><%= container["status"] %></div>
47
91
  </td>
48
- <% secs = Time.now.to_i - container.update_time.to_i %>
49
- <td style='text-align: center; white-space: nowrap;' title="<%= Time.at(container.update_time.to_i) %>">
92
+ <% secs = Time.now.to_i - container["update_time"].to_i %>
93
+ <td style='text-align: center; white-space: nowrap;' title="<%= Time.at(container["update_time"].to_i) %>">
50
94
  <% if secs > 0 %>
51
95
  <%= ChronicDuration.output(secs, :weeks => true, :units => 2) %> ago
52
96
  <% else %>
@@ -56,15 +100,30 @@
56
100
  <td>
57
101
  <div class="progress progress-striped" style="margin-bottom: 0">
58
102
  <div class='message' style='text-align:right; padding-right:0.5em; background-color: transparent; float:right;'>
59
- <%= container.message %>
103
+ <%= container["message"] %>
60
104
  </div>
61
- <% if container.pct_complete.to_i > 0 %>
62
- <div class="bar message" style="width: <%= container.pct_complete %>%;">
63
- <%= container.pct_complete %>%
105
+ <% if container["pct_complete"].to_i > 0 %>
106
+ <div class="bar message" style="width: <%= container["pct_complete"] %>%;">
107
+ <%= container["pct_complete"] %>%
64
108
  </div>
65
109
  <% end %>
66
110
  </div>
67
111
  </td>
112
+ <td>
113
+ <div class="actions">
114
+ <form action="statuses" method="post">
115
+ <input type="hidden" name="jid" value="<%= container["jid"] %>" />
116
+ <%= csrf_tag %>
117
+ <% if container["status"] == "complete" %>
118
+ <input type="hidden" name="_method" value="delete" />
119
+ <input type="submit" class="btn btn-danger btn-xs" value="Remove" />
120
+ <% elsif container["status"] == "failed" %>
121
+ <input type="hidden" name="_method" value="put" />
122
+ <input type="submit" class="btn btn-warning btn-xs" value="Retry Now" />
123
+ <% end %>
124
+ </form>
125
+ </div>
126
+ </td>
68
127
  </tr>
69
128
  <% end %>
70
129
  <% if @statuses.empty? %>