sidekiq-status 0.5.0 → 3.0.3

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