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.
@@ -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? %>