sidekiq-status 0.5.0 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
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
data/spec/spec_helper.rb CHANGED
@@ -1,69 +1,149 @@
1
1
  require "rspec"
2
-
3
- require 'celluloid'
2
+ require 'colorize'
4
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
+
5
9
  require 'sidekiq/processor'
6
10
  require 'sidekiq/manager'
7
11
  require 'sidekiq-status'
8
12
 
13
+ # Clears jobs before every test
14
+ RSpec.configure do |config|
15
+ config.before(:each) do
16
+ Sidekiq.redis { |conn| conn.flushall }
17
+ client_middleware
18
+ sleep 0.05
19
+ end
20
+ end
9
21
 
10
22
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
11
23
 
12
- Sidekiq.configure_client do |config|
13
- config.client_middleware do |chain|
14
- chain.add Sidekiq::Status::ClientMiddleware
24
+ # Configures client middleware
25
+ def client_middleware client_middleware_options = {}
26
+ Sidekiq.configure_client do |config|
27
+ Sidekiq::Status.configure_client_middleware config, client_middleware_options
15
28
  end
16
29
  end
17
30
 
18
- def confirmations_thread(messages_limit, *channels)
31
+ def redis_thread messages_limit, *channels
32
+
19
33
  parent = Thread.current
20
34
  thread = Thread.new {
21
- confirmations = []
35
+ messages = []
22
36
  Sidekiq.redis do |conn|
23
- 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 60, *channels do |on|
24
39
  on.subscribe do |ch, subscriptions|
40
+ puts "Subscribed to #{ch}".cyan if ENV['DEBUG']
25
41
  if subscriptions == channels.size
26
42
  sleep 0.1 while parent.status != "sleep"
27
43
  parent.run
28
44
  end
29
45
  end
30
46
  on.message do |ch, msg|
31
- confirmations << msg
32
- 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
33
50
  end
34
51
  end
35
52
  end
36
- confirmations
53
+ puts "Returing from thread".cyan if ENV['DEBUG']
54
+ messages
37
55
  }
56
+
38
57
  Thread.stop
39
58
  yield if block_given?
40
59
  thread
60
+
61
+ end
62
+
63
+ def redis_client_thread message_limit, *channels
64
+ thread = Thread.new {
65
+ messages = []
66
+ Sidekiq.redis do |conn|
67
+ puts "Subscribing to #{channels} for #{message_limit.to_s.bold} messages".cyan if ENV['DEBUG']
68
+ pubsub = conn.pubsub
69
+ pubsub.call("SUBSCRIBE", *channels)
70
+
71
+ timeouts = 0
72
+ loop do
73
+ type, ch, msg = pubsub.next_event(2)
74
+ next if type == "subscribe"
75
+ if msg
76
+ puts "Message received: #{ch} -> #{msg}".white if ENV['DEBUG']
77
+ messages << msg
78
+ break if messages.length >= message_limit
79
+ else
80
+ # no new message was received in the allocated timeout
81
+ timeouts += 1
82
+ break if timeouts >= 30
83
+ end
84
+ end
85
+ end
86
+ puts "Returing from thread".cyan if ENV['DEBUG']
87
+ messages
88
+ }
89
+ sleep 0.1
90
+ yield if block_given?
91
+ thread.join
92
+ end
93
+
94
+ def branched_redis_thread n, *channels, &block
95
+ if Sidekiq.major_version < 7
96
+ redis_thread(n, *channels, &block)
97
+ else
98
+ redis_client_thread(n, *channels, &block)
99
+ end
41
100
  end
42
101
 
43
- def capture_status_updates(n, &block)
44
- confirmations_thread(n, "status_updates", &block).value
102
+ def capture_status_updates n, &block
103
+ branched_redis_thread(n, "status_updates", &block).value
45
104
  end
46
105
 
47
- def start_server(server_middleware_options={})
106
+ # Configures server middleware and launches a sidekiq server
107
+ def start_server server_middleware_options = {}
108
+
109
+ # Creates a process for the Sidekiq server
48
110
  pid = Process.fork do
49
- $stdout.reopen File::NULL, 'w'
50
- $stderr.reopen File::NULL, 'w'
111
+
112
+ # Redirect the server's outputs
113
+ $stdout.reopen File::NULL, 'w' unless ENV['DEBUG']
114
+ $stderr.reopen File::NULL, 'w' unless ENV['DEBUG']
115
+
116
+ # Load and configure server options
51
117
  require 'sidekiq/cli'
52
- Sidekiq.options[:queues] << 'default'
118
+
119
+ # Add the server middleware
53
120
  Sidekiq.configure_server do |config|
54
- config.redis = Sidekiq::RedisConnection.create
55
- config.server_middleware do |chain|
56
- chain.add Sidekiq::Status::ServerMiddleware, server_middleware_options
57
- end
121
+ config.concurrency = 5
122
+ config.redis = Sidekiq::RedisConnection.create if Sidekiq.major_version < 7
123
+ Sidekiq::Status.configure_server_middleware config, server_middleware_options
58
124
  end
59
- Sidekiq::CLI.instance.run
125
+
126
+ # Launch
127
+ puts "Server starting".yellow if ENV['DEBUG']
128
+ instance = Sidekiq::CLI.instance
129
+ instance.parse(['-r', File.expand_path('environment.rb', File.dirname(__FILE__))])
130
+ instance.run
131
+
60
132
  end
61
133
 
134
+ # Run the client-side code
62
135
  yield
63
136
 
64
- sleep 0.1
137
+ # Pause to ensure all jobs are picked up & started before TERM is sent
138
+ sleep 0.2
139
+
140
+ # Attempt to shut down the server normally
65
141
  Process.kill 'TERM', pid
66
- Timeout::timeout(10) { Process.wait pid } rescue Timeout::Error
142
+ Process.wait pid
143
+
67
144
  ensure
145
+
146
+ # Ensure the server is actually dead
68
147
  Process.kill 'KILL', pid rescue "OK" # it's OK if the process is gone already
148
+
69
149
  end
@@ -1,16 +1,34 @@
1
+ require 'sidekiq-status'
2
+
1
3
  class StubJob
2
4
  include Sidekiq::Worker
3
5
  include Sidekiq::Status::Worker
4
6
 
5
- sidekiq_options 'retry' => 'false'
7
+ sidekiq_options 'retry' => false
8
+
9
+ def perform(*args)
10
+ end
11
+ end
12
+
13
+ class StubNoStatusJob
14
+ include Sidekiq::Worker
15
+
16
+ sidekiq_options 'retry' => false
6
17
 
7
18
  def perform(*args)
8
19
  end
9
20
  end
10
21
 
22
+
23
+ class ExpiryJob < StubJob
24
+ def expiration
25
+ 15
26
+ end
27
+ end
28
+
11
29
  class LongJob < StubJob
12
30
  def perform(*args)
13
- sleep args[0] || 1
31
+ sleep args[0] || 0.25
14
32
  end
15
33
  end
16
34
 
@@ -23,6 +41,13 @@ class DataJob < StubJob
23
41
  end
24
42
  end
25
43
 
44
+ class CustomDataJob < StubJob
45
+ def perform
46
+ store({mister_cat: 'meow'})
47
+ sleep 0.5
48
+ end
49
+ end
50
+
26
51
  class ProgressJob < StubJob
27
52
  def perform
28
53
  total 500
@@ -34,7 +59,7 @@ end
34
59
  class ConfirmationJob < StubJob
35
60
  def perform(*args)
36
61
  Sidekiq.redis do |conn|
37
- conn.publish "job_messages_#{jid}", "while in #perform, status = #{conn.hget jid, :status}"
62
+ conn.publish "job_messages_#{jid}", "while in #perform, status = #{conn.hget "sidekiq:status:#{jid}", :status}"
38
63
  end
39
64
  end
40
65
  end
@@ -54,13 +79,65 @@ class FailingJob < StubJob
54
79
  end
55
80
  end
56
81
 
82
+ class FailingNoStatusJob < StubNoStatusJob
83
+ def perform
84
+ raise StandardError
85
+ end
86
+ end
87
+
88
+ class RetryAndFailJob < StubJob
89
+ sidekiq_options retry: 1
90
+
91
+ def perform
92
+ raise StandardError
93
+ end
94
+ end
95
+
96
+ class FailingHardJob < StubJob
97
+ def perform
98
+ raise Exception
99
+ end
100
+ end
101
+
102
+ class FailingHardNoStatusJob < StubNoStatusJob
103
+ def perform
104
+ raise Exception
105
+ end
106
+ end
107
+
108
+ class ExitedJob < StubJob
109
+ def perform
110
+ raise SystemExit
111
+ end
112
+ end
113
+
114
+ class ExitedNoStatusJob < StubNoStatusJob
115
+ def perform
116
+ raise SystemExit
117
+ end
118
+ end
119
+
120
+ class InterruptedJob < StubJob
121
+ def perform
122
+ raise Interrupt
123
+ end
124
+ end
125
+
126
+ class InterruptedNoStatusJob < StubNoStatusJob
127
+ def perform
128
+ raise Interrupt
129
+ end
130
+ end
131
+
57
132
  class RetriedJob < StubJob
58
- sidekiq_options 'retry' => 'true'
59
- def perform()
133
+
134
+ sidekiq_options retry: true
135
+ sidekiq_retry_in do |count| 3 end # 3 second delay > job timeout in test suite
136
+
137
+ def perform
60
138
  Sidekiq.redis do |conn|
61
139
  key = "RetriedJob_#{jid}"
62
- sleep 1
63
- unless conn.exists key
140
+ if [0, false].include? conn.exists(key)
64
141
  conn.set key, 'tried'
65
142
  raise StandardError
66
143
  end
Binary file
Binary file
@@ -0,0 +1,118 @@
1
+ <% require 'cgi'; def h(v); CGI.escape_html(v.to_s); end %>
2
+ <style>
3
+ .progress {
4
+ background-color: #C8E1ED;
5
+ }
6
+ .progress-percentage {
7
+ margin: 5px;
8
+ padding-left: 4px;
9
+ color: #333;
10
+ text-align: left;
11
+ text-shadow: 0 0 5px white;
12
+ font-weight: bold;
13
+ font-size: 14px;
14
+ }
15
+ </style>
16
+
17
+ <h3>
18
+ Job Status: <%= h @status["jid"] %>
19
+ <span class='label label-<%= h @status["label"] %>'>
20
+ <%= h @status["status"] %>
21
+ </span>
22
+ </h3>
23
+
24
+ <div class="progress" style="height: 30px;">
25
+ <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 %>%">
26
+ <div class="progress-percentage">
27
+ <%= @status["pct_complete"].to_i %>%
28
+ </div>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="panel panel-default" style="background-color: inherit">
33
+ <div class="panel-body">
34
+ <h4><%= h @status["worker"] %></h4>
35
+
36
+ <div class="row">
37
+ <div class="col-sm-2">
38
+ <strong>Arguments</strong>
39
+ </div>
40
+ <div class="col-sm-10">
41
+ <p><%= @status["args"].empty? ? "<i>none</i>" : h(@status["args"]) %></p>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="row">
46
+ <div class="col-sm-2">
47
+ <strong>Message</strong>
48
+ </div>
49
+ <div class="col-sm-10">
50
+ <p><%= h(@status["message"]) || "<i>none</i>" %></p>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="row">
55
+ <div class="col-sm-2">
56
+ <strong>Last Updated</strong>
57
+ </div>
58
+ <div class="col-sm-10">
59
+ <p>
60
+ <% secs = Time.now.to_i - @status["update_time"].to_i %>
61
+ <% if secs > 0 %>
62
+ <%= ChronicDuration.output(secs, :weeks => true, :units => 2) %> ago
63
+ <% else %>
64
+ Now
65
+ <% end %>
66
+ </p>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="row">
71
+ <div class="col-sm-2">
72
+ <strong>Elapsed Time</strong>
73
+ </div>
74
+ <div class="col-sm-10">
75
+ <p>
76
+ <% if @status["elapsed"] %>
77
+ <%= ChronicDuration.output(@status["elapsed"].to_i, :weeks => true, :units => 2) %>
78
+ <% end %>
79
+ </p>
80
+ </div>
81
+ </div>
82
+
83
+ <div class="row">
84
+ <div class="col-sm-2">
85
+ <strong>ETA</strong>
86
+ </div>
87
+ <div class="col-sm-10">
88
+ <p>
89
+ <% if @status["eta"] %>
90
+ <%= ChronicDuration.output(@status["eta"].to_i, :weeks => true, :units => 2) %>
91
+ <% end %>
92
+ </p>
93
+ </div>
94
+ </div>
95
+
96
+ <% if @status["custom"].any? %>
97
+ <hr>
98
+ <% @status["custom"].each do |key, val| %>
99
+ <div class="row">
100
+ <div class="col-sm-2">
101
+ <strong><%= key %></strong>
102
+ </div>
103
+ <div class="col-sm-10">
104
+ <% if val && val.include?("\n") %>
105
+ <pre><%= h val %></pre>
106
+ <% else %>
107
+ <p><%= h(val) || "<i>none</i>" %></p>
108
+ <% end %>
109
+ </div>
110
+ </div>
111
+ <% end %>
112
+ <% end %>
113
+ </div>
114
+ </div>
115
+
116
+ <a href="<%= root_path %>statuses">
117
+ <small>&larr; back to all statuses</small>
118
+ </a>
@@ -0,0 +1,6 @@
1
+ <% require 'cgi'; def h(v); CGI.escape_html(v.to_s); end %>
2
+ <h3>Job Status: <%= h params[:jid] %></h3>
3
+
4
+ <div role="alert">
5
+ <strong>Uh oh!</strong> That job can't be found. It may have expired already.
6
+ </div>
@@ -1,31 +1,149 @@
1
- <h3 class="wi">Recent job statuses</h3>
1
+ <% require 'cgi'; def h(v); CGI.escape_html(v.to_s); end %>
2
+ <style>
3
+ .progress {
4
+ background-color: #C8E1ED;
5
+ }
6
+ .bar {
7
+ background-color: #2897cb;
8
+ color: white;
9
+ text-shadow: 0 0 0;
10
+ }
11
+ .message {
12
+ text-shadow: 0 0 5px white;
13
+ font-weight: bold; padding-left: 4px;
14
+ color: #333;
15
+ }
16
+ .actions {
17
+ text-align: center;
18
+ }
19
+ .header {
20
+ text-align: center;
21
+ }
22
+ .header_update_time {
23
+ width: 10%;
24
+ }
25
+ .header_pct_complete {
26
+ width: 45%;
27
+ }
28
+ .btn-warning {
29
+ background-image: linear-gradient(#f0ad4e, #eea236)
30
+ }
31
+ .nav-container {
32
+ display: flex;
33
+ line-height: 45px;
34
+ }
35
+ .nav-container .pull-right {
36
+ float: none !important;
37
+ }
38
+ .nav-container .pagination {
39
+ display: flex;
40
+ align-items: center;
41
+ }
42
+ .nav-container .per-page, .filter-status {
43
+ display: flex;
44
+ align-items: center;
45
+ margin: 20px 0 20px 10px;
46
+ white-space: nowrap;
47
+ }
48
+ .nav-container .per-page SELECT {
49
+ margin: 0 0 0 5px;
50
+ }
51
+ </style>
52
+ <script>
53
+ function setPerPage(select){
54
+ window.location = select.options[select.selectedIndex].getAttribute('data-url')
55
+ }
56
+ </script>
57
+ <div style="display: flex; justify-content: space-between;">
58
+ <h3 class="wi">Recent job statuses</h3>
59
+ <div class="nav-container">
60
+ <%= erb :_paging, locals: { url: "#{root_path}statuses" } %>
61
+ <div class="filter-status">
62
+ Filter Status:
63
+ <select class="form-control" onchange="setPerPage(this)">
64
+ <% (['all', 'complete', 'failed', 'interrupted', 'queued', 'retrying', 'stopped', 'working']).each do |status| %>
65
+ <option data-url="?<%= qparams(status: status)%>" value="<%= status %>" <%= 'selected="selected"' if status == (params[:status]) %>><%= status %></option>
66
+ <% end %>
67
+ </select>
68
+ </div>
2
69
 
3
-
4
- <table class="table table-striped table-bordered">
70
+ <div class="per-page">
71
+ Per page:
72
+ <select class="form-control" onchange="setPerPage(this)">
73
+ <% (Sidekiq::Status::Web.per_page_opts + ['all']).each do |num| %>
74
+ <option data-url="?<%= qparams(page: 1, per_page: num)%>" value="<%= num %>" <%= 'selected="selected"' if num.to_s == (params[:per_page] || @count) %>><%= num %></option>
75
+ <% end %>
76
+ </select>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ <table class="table table-hover table-bordered table-striped">
5
81
  <tr>
6
- <th>Worker/jid</th>
7
- <th>Status</th>
8
- <th>Last Updated ↆ</th>
9
- <th>Progress</th>
10
- <th>Message</th>
82
+ <% @headers.each do |hdr| %>
83
+ <th class="header <%= h hdr[:class] %> header_<%= h hdr[:id] %>">
84
+ <a href="<%= h hdr[:url] %>"><%= h hdr[:name] %></a>
85
+ </th>
86
+ <% end %>
87
+ <th class="header">
88
+ Actions
89
+ </th>
11
90
  </tr>
12
91
  <% @statuses.each do |container| %>
13
92
  <tr>
14
93
  <td>
15
- <%= container.worker %>
16
- <br />
17
- <%= container.jid %>
94
+ <div title='<%= h container["jid"] %>'><a href="<%= root_path %>statuses/<%= h container["jid"] %>"><%= h container["worker"] %></a></div>
95
+ </td>
96
+ <td>
97
+ <div class='args' title='<%= h container["jid"] %>'><%= h container["args"] %></div>
98
+ </td>
99
+ <td style='text-align: center;'>
100
+ <div class='label label-<%= h container["label"] %>'><%= h container["status"] %></div>
101
+ </td>
102
+ <% secs = Time.now.to_i - container["update_time"].to_i %>
103
+ <td style='text-align: center; white-space: nowrap;' title="<%= Time.at(container["update_time"].to_i) %>">
104
+ <% if secs > 0 %>
105
+ <%= ChronicDuration.output(secs, :weeks => true, :units => 2) %> ago
106
+ <% else %>
107
+ Now
108
+ <% end %>
18
109
  </td>
19
- <td><%= container.status %></td>
20
- <td><%= Time.at(container.update_time.to_i) %></td>
21
110
  <td>
22
111
  <div class="progress progress-striped" style="margin-bottom: 0">
23
- <div class="bar" style="width: <%= container.pct_complete %>%; text-shadow: 1px 1px 1px black; background-color: #AD003D;
24
- color: white;">
25
- <%= container.pct_complete %>%
112
+ <div class='message' style='text-align:right; padding-right:0.5em; background-color: transparent; float:right;'>
113
+ <%= h container["message"] %>
26
114
  </div>
115
+ <% if container["pct_complete"].to_i > 0 %>
116
+ <div class="bar message" style="width: <%= h container["pct_complete"] %>%;">
117
+ <%= h container["pct_complete"] %>%
118
+ </div>
119
+ <% end %>
27
120
  </div>
28
- <td><%= container.message %></td>
121
+ </td>
122
+ <td style='text-align: center; white-space: nowrap;'>
123
+ <% if container["elapsed"] %>
124
+ <%= ChronicDuration.output(container["elapsed"].to_i, :weeks => true, :units => 2) %>
125
+ <% end %>
126
+ </td>
127
+ <td style='text-align: center; white-space: nowrap;'>
128
+ <% if container["eta"] %>
129
+ <%= ChronicDuration.output(container["eta"].to_i, :weeks => true, :units => 2) %>
130
+ <% end %>
131
+ </td>
132
+ <td>
133
+ <div class="actions">
134
+ <form action="statuses" method="post">
135
+ <input type="hidden" name="jid" value="<%= h container["jid"] %>" />
136
+ <%= csrf_tag %>
137
+ <% if container["status"] == "complete" %>
138
+ <input type="hidden" name="_method" value="delete" />
139
+ <input type="submit" class="btn btn-danger btn-xs" value="Remove" />
140
+ <% elsif container["status"] == "failed" %>
141
+ <input type="hidden" name="_method" value="put" />
142
+ <input type="submit" class="btn btn-warning btn-xs" value="Retry Now" />
143
+ <% end %>
144
+ </form>
145
+ </div>
146
+ </td>
29
147
  </tr>
30
148
  <% end %>
31
149
  <% if @statuses.empty? %>