sidekiq 2.15.1 → 4.2.10

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (187) hide show
  1. checksums.yaml +7 -0
  2. data/.github/contributing.md +32 -0
  3. data/.github/issue_template.md +9 -0
  4. data/.gitignore +1 -0
  5. data/.travis.yml +16 -17
  6. data/3.0-Upgrade.md +70 -0
  7. data/4.0-Upgrade.md +53 -0
  8. data/COMM-LICENSE +56 -44
  9. data/Changes.md +644 -1
  10. data/Ent-Changes.md +173 -0
  11. data/Gemfile +27 -0
  12. data/LICENSE +1 -1
  13. data/Pro-2.0-Upgrade.md +138 -0
  14. data/Pro-3.0-Upgrade.md +44 -0
  15. data/Pro-Changes.md +457 -3
  16. data/README.md +46 -29
  17. data/Rakefile +6 -3
  18. data/bin/sidekiq +4 -0
  19. data/bin/sidekiqctl +41 -20
  20. data/bin/sidekiqload +154 -0
  21. data/code_of_conduct.md +50 -0
  22. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  23. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  24. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  25. data/lib/generators/sidekiq/worker_generator.rb +49 -0
  26. data/lib/sidekiq.rb +141 -29
  27. data/lib/sidekiq/api.rb +540 -106
  28. data/lib/sidekiq/cli.rb +131 -71
  29. data/lib/sidekiq/client.rb +168 -96
  30. data/lib/sidekiq/core_ext.rb +36 -8
  31. data/lib/sidekiq/exception_handler.rb +20 -28
  32. data/lib/sidekiq/extensions/action_mailer.rb +25 -5
  33. data/lib/sidekiq/extensions/active_record.rb +8 -4
  34. data/lib/sidekiq/extensions/class_methods.rb +9 -5
  35. data/lib/sidekiq/extensions/generic_proxy.rb +1 -0
  36. data/lib/sidekiq/fetch.rb +45 -101
  37. data/lib/sidekiq/launcher.rb +144 -30
  38. data/lib/sidekiq/logging.rb +69 -12
  39. data/lib/sidekiq/manager.rb +90 -140
  40. data/lib/sidekiq/middleware/chain.rb +18 -5
  41. data/lib/sidekiq/middleware/i18n.rb +9 -2
  42. data/lib/sidekiq/middleware/server/active_record.rb +1 -1
  43. data/lib/sidekiq/middleware/server/logging.rb +11 -11
  44. data/lib/sidekiq/middleware/server/retry_jobs.rb +98 -44
  45. data/lib/sidekiq/paginator.rb +20 -8
  46. data/lib/sidekiq/processor.rb +157 -96
  47. data/lib/sidekiq/rails.rb +109 -5
  48. data/lib/sidekiq/redis_connection.rb +70 -24
  49. data/lib/sidekiq/scheduled.rb +122 -50
  50. data/lib/sidekiq/testing.rb +171 -31
  51. data/lib/sidekiq/testing/inline.rb +1 -0
  52. data/lib/sidekiq/util.rb +31 -5
  53. data/lib/sidekiq/version.rb +2 -1
  54. data/lib/sidekiq/web.rb +136 -263
  55. data/lib/sidekiq/web/action.rb +93 -0
  56. data/lib/sidekiq/web/application.rb +336 -0
  57. data/lib/sidekiq/web/helpers.rb +278 -0
  58. data/lib/sidekiq/web/router.rb +100 -0
  59. data/lib/sidekiq/worker.rb +40 -7
  60. data/sidekiq.gemspec +18 -14
  61. data/web/assets/images/favicon.ico +0 -0
  62. data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
  63. data/web/assets/javascripts/application.js +67 -19
  64. data/web/assets/javascripts/dashboard.js +138 -29
  65. data/web/assets/stylesheets/application.css +267 -406
  66. data/web/assets/stylesheets/bootstrap.css +4 -8
  67. data/web/locales/cs.yml +78 -0
  68. data/web/locales/da.yml +9 -1
  69. data/web/locales/de.yml +18 -9
  70. data/web/locales/el.yml +68 -0
  71. data/web/locales/en.yml +19 -4
  72. data/web/locales/es.yml +10 -1
  73. data/web/locales/fa.yml +79 -0
  74. data/web/locales/fr.yml +50 -32
  75. data/web/locales/hi.yml +75 -0
  76. data/web/locales/it.yml +27 -18
  77. data/web/locales/ja.yml +27 -12
  78. data/web/locales/ko.yml +8 -3
  79. data/web/locales/{no.yml → nb.yml} +19 -5
  80. data/web/locales/nl.yml +8 -3
  81. data/web/locales/pl.yml +0 -1
  82. data/web/locales/pt-br.yml +11 -4
  83. data/web/locales/pt.yml +8 -1
  84. data/web/locales/ru.yml +39 -21
  85. data/web/locales/sv.yml +68 -0
  86. data/web/locales/ta.yml +75 -0
  87. data/web/locales/uk.yml +76 -0
  88. data/web/locales/zh-cn.yml +68 -0
  89. data/web/locales/zh-tw.yml +68 -0
  90. data/web/views/_footer.erb +17 -0
  91. data/web/views/_job_info.erb +72 -60
  92. data/web/views/_nav.erb +58 -25
  93. data/web/views/_paging.erb +5 -5
  94. data/web/views/_poll_link.erb +7 -0
  95. data/web/views/_summary.erb +20 -14
  96. data/web/views/busy.erb +94 -0
  97. data/web/views/dashboard.erb +34 -21
  98. data/web/views/dead.erb +34 -0
  99. data/web/views/layout.erb +8 -30
  100. data/web/views/morgue.erb +75 -0
  101. data/web/views/queue.erb +37 -30
  102. data/web/views/queues.erb +26 -20
  103. data/web/views/retries.erb +60 -47
  104. data/web/views/retry.erb +23 -19
  105. data/web/views/scheduled.erb +39 -35
  106. data/web/views/scheduled_job_info.erb +2 -1
  107. metadata +152 -195
  108. data/Contributing.md +0 -29
  109. data/config.ru +0 -18
  110. data/lib/sidekiq/actor.rb +0 -7
  111. data/lib/sidekiq/capistrano.rb +0 -54
  112. data/lib/sidekiq/yaml_patch.rb +0 -21
  113. data/test/config.yml +0 -11
  114. data/test/env_based_config.yml +0 -11
  115. data/test/fake_env.rb +0 -0
  116. data/test/helper.rb +0 -42
  117. data/test/test_api.rb +0 -341
  118. data/test/test_cli.rb +0 -326
  119. data/test/test_client.rb +0 -211
  120. data/test/test_exception_handler.rb +0 -124
  121. data/test/test_extensions.rb +0 -105
  122. data/test/test_fetch.rb +0 -44
  123. data/test/test_manager.rb +0 -83
  124. data/test/test_middleware.rb +0 -135
  125. data/test/test_processor.rb +0 -160
  126. data/test/test_redis_connection.rb +0 -97
  127. data/test/test_retry.rb +0 -306
  128. data/test/test_scheduled.rb +0 -86
  129. data/test/test_scheduling.rb +0 -47
  130. data/test/test_sidekiq.rb +0 -37
  131. data/test/test_testing.rb +0 -82
  132. data/test/test_testing_fake.rb +0 -265
  133. data/test/test_testing_inline.rb +0 -92
  134. data/test/test_util.rb +0 -18
  135. data/test/test_web.rb +0 -372
  136. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  137. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  138. data/web/assets/images/status/active.png +0 -0
  139. data/web/assets/images/status/idle.png +0 -0
  140. data/web/assets/javascripts/locales/README.md +0 -27
  141. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  142. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  143. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  144. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  145. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  146. data/web/assets/javascripts/locales/jquery.timeago.cz.js +0 -18
  147. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  148. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  149. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  150. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  151. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  152. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  153. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  154. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  155. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  156. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  157. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  158. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  159. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  160. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  161. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  162. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  163. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  164. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  165. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  166. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  167. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  168. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  169. data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
  170. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  171. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  172. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  173. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  174. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  175. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  176. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  177. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  178. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  179. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  180. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  181. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  182. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  183. data/web/assets/javascripts/locales/jquery.timeago.zh-CN.js +0 -20
  184. data/web/assets/javascripts/locales/jquery.timeago.zh-TW.js +0 -20
  185. data/web/views/_poll.erb +0 -14
  186. data/web/views/_workers.erb +0 -29
  187. data/web/views/index.erb +0 -16
@@ -0,0 +1,9 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>Worker
3
+ include Sidekiq::Worker
4
+
5
+ def perform(*args)
6
+ # Do something
7
+ end
8
+ end
9
+ <% end -%>
@@ -0,0 +1,6 @@
1
+ require 'rails_helper'
2
+ <% module_namespacing do -%>
3
+ RSpec.describe <%= class_name %>Worker, type: :worker do
4
+ pending "add some examples to (or delete) #{__FILE__}"
5
+ end
6
+ <% end -%>
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+ <% module_namespacing do -%>
3
+ class <%= class_name %>WorkerTest < <% if defined? Minitest::Test %>Minitest::Test<% else %>MiniTest::Unit::TestCase<% end %>
4
+ def test_example
5
+ skip "add some examples to (or delete) #{__FILE__}"
6
+ end
7
+ end
8
+ <% end -%>
@@ -0,0 +1,49 @@
1
+ require 'rails/generators/named_base'
2
+
3
+ module Sidekiq
4
+ module Generators # :nodoc:
5
+ class WorkerGenerator < ::Rails::Generators::NamedBase # :nodoc:
6
+ desc 'This generator creates a Sidekiq Worker in app/workers and a corresponding test'
7
+
8
+ check_class_collision suffix: 'Worker'
9
+
10
+ def self.default_generator_root
11
+ File.dirname(__FILE__)
12
+ end
13
+
14
+ def create_worker_file
15
+ template 'worker.rb.erb', File.join('app/workers', class_path, "#{file_name}_worker.rb")
16
+ end
17
+
18
+ def create_test_file
19
+ if defined?(RSpec)
20
+ create_worker_spec
21
+ else
22
+ create_worker_test
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def create_worker_spec
29
+ template_file = File.join(
30
+ 'spec/workers',
31
+ class_path,
32
+ "#{file_name}_worker_spec.rb"
33
+ )
34
+ template 'worker_spec.rb.erb', template_file
35
+ end
36
+
37
+ def create_worker_test
38
+ template_file = File.join(
39
+ 'test/workers',
40
+ class_path,
41
+ "#{file_name}_worker_test.rb"
42
+ )
43
+ template 'worker_test.rb.erb', template_file
44
+ end
45
+
46
+
47
+ end
48
+ end
49
+ end
@@ -1,35 +1,61 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
  require 'sidekiq/version'
4
+ fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.0.0." if RUBY_PLATFORM != 'java' && RUBY_VERSION < '2.0.0'
5
+
3
6
  require 'sidekiq/logging'
4
7
  require 'sidekiq/client'
5
8
  require 'sidekiq/worker'
6
9
  require 'sidekiq/redis_connection'
7
- require 'sidekiq/util'
8
- require 'sidekiq/api'
9
10
 
10
11
  require 'json'
11
12
 
12
13
  module Sidekiq
13
- NAME = "Sidekiq"
14
+ NAME = 'Sidekiq'
14
15
  LICENSE = 'See LICENSE and the LGPL-3.0 for licensing details.'
15
16
 
16
17
  DEFAULTS = {
17
- :queues => [],
18
- :concurrency => 25,
19
- :require => '.',
20
- :environment => nil,
21
- :timeout => 8,
22
- :profile => false,
18
+ queues: [],
19
+ labels: [],
20
+ concurrency: 25,
21
+ require: '.',
22
+ environment: nil,
23
+ timeout: 8,
24
+ poll_interval_average: nil,
25
+ average_scheduled_poll_interval: 15,
26
+ error_handlers: [],
27
+ lifecycle_events: {
28
+ startup: [],
29
+ quiet: [],
30
+ shutdown: [],
31
+ heartbeat: [],
32
+ },
33
+ dead_max_jobs: 10_000,
34
+ dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
35
+ reloader: proc { |&block| block.call },
36
+ executor: proc { |&block| block.call },
37
+ }
38
+
39
+ DEFAULT_WORKER_OPTIONS = {
40
+ 'retry' => true,
41
+ 'queue' => 'default'
23
42
  }
24
43
 
44
+ FAKE_INFO = {
45
+ "redis_version" => "9.9.9",
46
+ "uptime_in_days" => "9999",
47
+ "connected_clients" => "9999",
48
+ "used_memory_human" => "9P",
49
+ "used_memory_peak_human" => "9P"
50
+ }.freeze
51
+
25
52
  def self.❨╯°□°❩╯︵┻━┻
26
- puts "Calm down, bro"
53
+ puts "Calm down, yo."
27
54
  end
28
55
 
29
56
  def self.options
30
57
  @options ||= DEFAULTS.dup
31
58
  end
32
-
33
59
  def self.options=(opts)
34
60
  @options = opts
35
61
  end
@@ -61,46 +87,95 @@ module Sidekiq
61
87
  defined?(Sidekiq::CLI)
62
88
  end
63
89
 
64
- def self.redis(&block)
65
- raise ArgumentError, "requires a block" if !block
66
- @redis ||= Sidekiq::RedisConnection.create(@hash || {})
67
- @redis.with(&block)
90
+ def self.redis
91
+ raise ArgumentError, "requires a block" unless block_given?
92
+ redis_pool.with do |conn|
93
+ retryable = true
94
+ begin
95
+ yield conn
96
+ rescue Redis::CommandError => ex
97
+ #2550 Failover can cause the server to become a slave, need
98
+ # to disconnect and reopen the socket to get back to the master.
99
+ (conn.disconnect!; retryable = false; retry) if retryable && ex.message =~ /READONLY/
100
+ raise
101
+ end
102
+ end
68
103
  end
69
104
 
70
- def self.redis=(hash)
71
- return @redis = hash if hash.is_a?(ConnectionPool)
105
+ def self.redis_info
106
+ redis do |conn|
107
+ begin
108
+ # admin commands can't go through redis-namespace starting
109
+ # in redis-namespace 2.0
110
+ if conn.respond_to?(:namespace)
111
+ conn.redis.info
112
+ else
113
+ conn.info
114
+ end
115
+ rescue Redis::CommandError => ex
116
+ #2850 return fake version when INFO command has (probably) been renamed
117
+ raise unless ex.message =~ /unknown command/
118
+ FAKE_INFO
119
+ end
120
+ end
121
+ end
72
122
 
73
- if hash.is_a?(Hash)
74
- @hash = hash
123
+ def self.redis_pool
124
+ @redis ||= Sidekiq::RedisConnection.create
125
+ end
126
+
127
+ def self.redis=(hash)
128
+ @redis = if hash.is_a?(ConnectionPool)
129
+ hash
75
130
  else
76
- raise ArgumentError, "redis= requires a Hash or ConnectionPool"
131
+ Sidekiq::RedisConnection.create(hash)
77
132
  end
78
133
  end
79
134
 
80
135
  def self.client_middleware
81
- @client_chain ||= Client.default_middleware
136
+ @client_chain ||= Middleware::Chain.new
82
137
  yield @client_chain if block_given?
83
138
  @client_chain
84
139
  end
85
140
 
86
141
  def self.server_middleware
87
- @server_chain ||= Processor.default_middleware
142
+ @server_chain ||= default_server_middleware
88
143
  yield @server_chain if block_given?
89
144
  @server_chain
90
145
  end
91
146
 
92
- def self.default_worker_options=(hash)
93
- @default_worker_options = default_worker_options.merge(hash)
147
+ def self.default_server_middleware
148
+ require 'sidekiq/middleware/server/retry_jobs'
149
+ require 'sidekiq/middleware/server/logging'
150
+
151
+ Middleware::Chain.new do |m|
152
+ m.add Middleware::Server::RetryJobs
153
+ m.add Middleware::Server::Logging
154
+ end
94
155
  end
95
156
 
157
+ def self.default_worker_options=(hash)
158
+ @default_worker_options = default_worker_options.merge(hash.stringify_keys)
159
+ end
96
160
  def self.default_worker_options
97
- @default_worker_options || { 'retry' => true, 'queue' => 'default' }
161
+ defined?(@default_worker_options) ? @default_worker_options : DEFAULT_WORKER_OPTIONS
162
+ end
163
+
164
+ # Sidekiq.configure_server do |config|
165
+ # config.default_retries_exhausted = -> (job, ex) do
166
+ # end
167
+ # end
168
+ def self.default_retries_exhausted=(prok)
169
+ @default_retries_exhausted = prok
170
+ end
171
+ @default_retries_exhausted = ->(job, ex) { }
172
+ def self.default_retries_exhausted
173
+ @default_retries_exhausted
98
174
  end
99
175
 
100
176
  def self.load_json(string)
101
177
  JSON.parse(string)
102
178
  end
103
-
104
179
  def self.dump_json(object)
105
180
  JSON.generate(object)
106
181
  end
@@ -108,15 +183,52 @@ module Sidekiq
108
183
  def self.logger
109
184
  Sidekiq::Logging.logger
110
185
  end
111
-
112
186
  def self.logger=(log)
113
187
  Sidekiq::Logging.logger = log
114
188
  end
115
189
 
116
- def self.poll_interval=(interval)
117
- self.options[:poll_interval] = interval
190
+ # How frequently Redis should be checked by a random Sidekiq process for
191
+ # scheduled and retriable jobs. Each individual process will take turns by
192
+ # waiting some multiple of this value.
193
+ #
194
+ # See sidekiq/scheduled.rb for an in-depth explanation of this value
195
+ def self.average_scheduled_poll_interval=(interval)
196
+ self.options[:average_scheduled_poll_interval] = interval
118
197
  end
119
198
 
199
+ # Register a proc to handle any error which occurs within the Sidekiq process.
200
+ #
201
+ # Sidekiq.configure_server do |config|
202
+ # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
203
+ # end
204
+ #
205
+ # The default error handler logs errors to Sidekiq.logger.
206
+ def self.error_handlers
207
+ self.options[:error_handlers]
208
+ end
209
+
210
+ # Register a block to run at a point in the Sidekiq lifecycle.
211
+ # :startup, :quiet or :shutdown are valid events.
212
+ #
213
+ # Sidekiq.configure_server do |config|
214
+ # config.on(:shutdown) do
215
+ # puts "Goodbye cruel world!"
216
+ # end
217
+ # end
218
+ def self.on(event, &block)
219
+ raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
220
+ raise ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
221
+ options[:lifecycle_events][event] << block
222
+ end
223
+
224
+ # We are shutting down Sidekiq but what about workers that
225
+ # are working on some long job? This error is
226
+ # raised in workers that have not finished within the hard
227
+ # timeout limit. This is needed to rollback db transactions,
228
+ # otherwise Ruby's Thread#kill will commit. See #377.
229
+ # DO NOT RESCUE THIS ERROR IN YOUR WORKERS
230
+ class Shutdown < Interrupt; end
231
+
120
232
  end
121
233
 
122
234
  require 'sidekiq/extensions/class_methods'
@@ -1,45 +1,142 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
1
3
  require 'sidekiq'
2
4
 
3
5
  module Sidekiq
4
6
  class Stats
7
+ def initialize
8
+ fetch_stats!
9
+ end
10
+
5
11
  def processed
6
- Sidekiq.redis { |conn| conn.get("stat:processed") }.to_i
12
+ stat :processed
7
13
  end
8
14
 
9
15
  def failed
10
- Sidekiq.redis { |conn| conn.get("stat:failed") }.to_i
16
+ stat :failed
11
17
  end
12
18
 
13
- def reset
14
- Sidekiq.redis do |conn|
15
- conn.set("stat:failed", 0)
16
- conn.set("stat:processed", 0)
17
- end
19
+ def scheduled_size
20
+ stat :scheduled_size
21
+ end
22
+
23
+ def retry_size
24
+ stat :retry_size
25
+ end
26
+
27
+ def dead_size
28
+ stat :dead_size
29
+ end
30
+
31
+ def enqueued
32
+ stat :enqueued
33
+ end
34
+
35
+ def processes_size
36
+ stat :processes_size
37
+ end
38
+
39
+ def workers_size
40
+ stat :workers_size
41
+ end
42
+
43
+ def default_queue_latency
44
+ stat :default_queue_latency
18
45
  end
19
46
 
20
47
  def queues
21
- Sidekiq.redis do |conn|
22
- queues = conn.smembers('queues')
48
+ Sidekiq::Stats::Queues.new.lengths
49
+ end
50
+
51
+ def fetch_stats!
52
+ pipe1_res = Sidekiq.redis do |conn|
53
+ conn.pipelined do
54
+ conn.get('stat:processed'.freeze)
55
+ conn.get('stat:failed'.freeze)
56
+ conn.zcard('schedule'.freeze)
57
+ conn.zcard('retry'.freeze)
58
+ conn.zcard('dead'.freeze)
59
+ conn.scard('processes'.freeze)
60
+ conn.lrange('queue:default'.freeze, -1, -1)
61
+ conn.smembers('processes'.freeze)
62
+ conn.smembers('queues'.freeze)
63
+ end
64
+ end
23
65
 
24
- array_of_arrays = queues.inject({}) do |memo, queue|
25
- memo[queue] = conn.llen("queue:#{queue}")
26
- memo
27
- end.sort_by { |_, size| size }
66
+ pipe2_res = Sidekiq.redis do |conn|
67
+ conn.pipelined do
68
+ pipe1_res[7].each {|key| conn.hget(key, 'busy'.freeze) }
69
+ pipe1_res[8].each {|queue| conn.llen("queue:#{queue}") }
70
+ end
71
+ end
28
72
 
29
- Hash[array_of_arrays.reverse]
73
+ s = pipe1_res[7].size
74
+ workers_size = pipe2_res[0...s].map(&:to_i).inject(0, &:+)
75
+ enqueued = pipe2_res[s..-1].map(&:to_i).inject(0, &:+)
76
+
77
+ default_queue_latency = if (entry = pipe1_res[6].first)
78
+ job = Sidekiq.load_json(entry)
79
+ now = Time.now.to_f
80
+ thence = job['enqueued_at'.freeze] || now
81
+ now - thence
82
+ else
83
+ 0
84
+ end
85
+ @stats = {
86
+ processed: pipe1_res[0].to_i,
87
+ failed: pipe1_res[1].to_i,
88
+ scheduled_size: pipe1_res[2],
89
+ retry_size: pipe1_res[3],
90
+ dead_size: pipe1_res[4],
91
+ processes_size: pipe1_res[5],
92
+
93
+ default_queue_latency: default_queue_latency,
94
+ workers_size: workers_size,
95
+ enqueued: enqueued
96
+ }
97
+ end
98
+
99
+ def reset(*stats)
100
+ all = %w(failed processed)
101
+ stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
102
+
103
+ mset_args = []
104
+ stats.each do |stat|
105
+ mset_args << "stat:#{stat}"
106
+ mset_args << 0
107
+ end
108
+ Sidekiq.redis do |conn|
109
+ conn.mset(*mset_args)
30
110
  end
31
111
  end
32
112
 
33
- def enqueued
34
- queues.values.inject(&:+) || 0
35
- end
113
+ private
36
114
 
37
- def scheduled_size
38
- Sidekiq.redis {|c| c.zcard('schedule') }
115
+ def stat(s)
116
+ @stats[s]
39
117
  end
40
118
 
41
- def retry_size
42
- Sidekiq.redis {|c| c.zcard('retry') }
119
+ class Queues
120
+ def lengths
121
+ Sidekiq.redis do |conn|
122
+ queues = conn.smembers('queues'.freeze)
123
+
124
+ lengths = conn.pipelined do
125
+ queues.each do |queue|
126
+ conn.llen("queue:#{queue}")
127
+ end
128
+ end
129
+
130
+ i = 0
131
+ array_of_arrays = queues.inject({}) do |memo, queue|
132
+ memo[queue] = lengths[i]
133
+ i += 1
134
+ memo
135
+ end.sort_by { |_, size| size }
136
+
137
+ Hash[array_of_arrays.reverse]
138
+ end
139
+ end
43
140
  end
44
141
 
45
142
  class History
@@ -61,15 +158,20 @@ module Sidekiq
61
158
  def date_stat_hash(stat)
62
159
  i = 0
63
160
  stat_hash = {}
161
+ keys = []
162
+ dates = []
163
+
164
+ while i < @days_previous
165
+ date = @start_date - i
166
+ datestr = date.strftime("%Y-%m-%d".freeze)
167
+ keys << "stat:#{stat}:#{datestr}"
168
+ dates << datestr
169
+ i += 1
170
+ end
64
171
 
65
172
  Sidekiq.redis do |conn|
66
- while i < @days_previous
67
- date = @start_date - i
68
- value = conn.get("stat:#{stat}:#{date}")
69
-
70
- stat_hash[date.to_s] = value ? value.to_i : 0
71
-
72
- i += 1
173
+ conn.mget(keys).each_with_index do |value, idx|
174
+ stat_hash[dates[idx]] = value ? value.to_i : 0
73
175
  end
74
176
  end
75
177
 
@@ -93,8 +195,11 @@ module Sidekiq
93
195
  class Queue
94
196
  include Enumerable
95
197
 
198
+ ##
199
+ # Return all known queues within Redis.
200
+ #
96
201
  def self.all
97
- Sidekiq.redis {|c| c.smembers('queues') }.map {|q| Sidekiq::Queue.new(q) }
202
+ Sidekiq.redis { |c| c.smembers('queues'.freeze) }.sort.map { |q| Sidekiq::Queue.new(q) }
98
203
  end
99
204
 
100
205
  attr_reader :name
@@ -108,42 +213,66 @@ module Sidekiq
108
213
  Sidekiq.redis { |con| con.llen(@rname) }
109
214
  end
110
215
 
216
+ # Sidekiq Pro overrides this
217
+ def paused?
218
+ false
219
+ end
220
+
221
+ ##
222
+ # Calculates this queue's latency, the difference in seconds since the oldest
223
+ # job in the queue was enqueued.
224
+ #
225
+ # @return Float
111
226
  def latency
112
227
  entry = Sidekiq.redis do |conn|
113
228
  conn.lrange(@rname, -1, -1)
114
229
  end.first
115
230
  return 0 unless entry
116
- Time.now.to_f - Sidekiq.load_json(entry)['enqueued_at']
231
+ job = Sidekiq.load_json(entry)
232
+ now = Time.now.to_f
233
+ thence = job['enqueued_at'] || now
234
+ now - thence
117
235
  end
118
236
 
119
- def each(&block)
237
+ def each
238
+ initial_size = size
239
+ deleted_size = 0
120
240
  page = 0
121
241
  page_size = 50
122
242
 
123
- loop do
243
+ while true do
244
+ range_start = page * page_size - deleted_size
245
+ range_end = range_start + page_size - 1
124
246
  entries = Sidekiq.redis do |conn|
125
- conn.lrange @rname, page * page_size, (page * page_size) + page_size - 1
247
+ conn.lrange @rname, range_start, range_end
126
248
  end
127
249
  break if entries.empty?
128
250
  page += 1
129
251
  entries.each do |entry|
130
- block.call Job.new(entry, @name)
252
+ yield Job.new(entry, @name)
131
253
  end
254
+ deleted_size = initial_size - size
132
255
  end
133
256
  end
134
257
 
258
+ ##
259
+ # Find the job with the given JID within this queue.
260
+ #
261
+ # This is a slow, inefficient operation. Do not use under
262
+ # normal conditions. Sidekiq Pro contains a faster version.
135
263
  def find_job(jid)
136
- self.detect { |j| j.jid == jid }
264
+ detect { |j| j.jid == jid }
137
265
  end
138
266
 
139
267
  def clear
140
268
  Sidekiq.redis do |conn|
141
269
  conn.multi do
142
270
  conn.del(@rname)
143
- conn.srem("queues", name)
271
+ conn.srem("queues".freeze, name)
144
272
  end
145
273
  end
146
274
  end
275
+ alias_method :💣, :clear
147
276
  end
148
277
 
149
278
  ##
@@ -155,10 +284,11 @@ module Sidekiq
155
284
  #
156
285
  class Job
157
286
  attr_reader :item
287
+ attr_reader :value
158
288
 
159
289
  def initialize(item, queue_name=nil)
160
290
  @value = item
161
- @item = Sidekiq.load_json(item)
291
+ @item = item.is_a?(Hash) ? item : Sidekiq.load_json(item)
162
292
  @queue = queue_name || @item['queue']
163
293
  end
164
294
 
@@ -166,6 +296,46 @@ module Sidekiq
166
296
  @item['class']
167
297
  end
168
298
 
299
+ def display_class
300
+ # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
301
+ @klass ||= case klass
302
+ when /\ASidekiq::Extensions::Delayed/
303
+ safe_load(args[0], klass) do |target, method, _|
304
+ "#{target}.#{method}"
305
+ end
306
+ when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
307
+ job_class = @item['wrapped'] || args[0]
308
+ if 'ActionMailer::DeliveryJob' == job_class
309
+ # MailerClass#mailer_method
310
+ args[0]['arguments'][0..1].join('#')
311
+ else
312
+ job_class
313
+ end
314
+ else
315
+ klass
316
+ end
317
+ end
318
+
319
+ def display_args
320
+ # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
321
+ @args ||= case klass
322
+ when /\ASidekiq::Extensions::Delayed/
323
+ safe_load(args[0], args) do |_, _, arg|
324
+ arg
325
+ end
326
+ when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
327
+ job_args = @item['wrapped'] ? args[0]["arguments"] : []
328
+ if 'ActionMailer::DeliveryJob' == (@item['wrapped'] || args[0])
329
+ # remove MailerClass, mailer_method and 'deliver_now'
330
+ job_args.drop(3)
331
+ else
332
+ job_args
333
+ end
334
+ else
335
+ args
336
+ end
337
+ end
338
+
169
339
  def args
170
340
  @item['args']
171
341
  end
@@ -175,7 +345,11 @@ module Sidekiq
175
345
  end
176
346
 
177
347
  def enqueued_at
178
- Time.at(@item['enqueued_at'] || 0).utc
348
+ @item['enqueued_at'] ? Time.at(@item['enqueued_at']).utc : nil
349
+ end
350
+
351
+ def created_at
352
+ Time.at(@item['created_at'] || @item['enqueued_at'] || 0).utc
179
353
  end
180
354
 
181
355
  def queue
@@ -183,25 +357,40 @@ module Sidekiq
183
357
  end
184
358
 
185
359
  def latency
186
- Time.now.to_f - @item['enqueued_at']
360
+ now = Time.now.to_f
361
+ now - (@item['enqueued_at'] || @item['created_at'] || now)
187
362
  end
188
363
 
189
364
  ##
190
365
  # Remove this job from the queue.
191
366
  def delete
192
367
  count = Sidekiq.redis do |conn|
193
- conn.lrem("queue:#{@queue}", 0, @value)
368
+ conn.lrem("queue:#{@queue}", 1, @value)
194
369
  end
195
370
  count != 0
196
371
  end
197
372
 
198
373
  def [](name)
199
- @item.send(:[], name)
374
+ @item[name]
375
+ end
376
+
377
+ private
378
+
379
+ def safe_load(content, default)
380
+ begin
381
+ yield(*YAML.load(content))
382
+ rescue => ex
383
+ # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
384
+ # memory yet so the YAML can't be loaded.
385
+ Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == 'development'
386
+ default
387
+ end
200
388
  end
201
389
  end
202
390
 
203
391
  class SortedEntry < Job
204
392
  attr_reader :score
393
+ attr_reader :parent
205
394
 
206
395
  def initialize(parent, score, item)
207
396
  super(item)
@@ -214,76 +403,144 @@ module Sidekiq
214
403
  end
215
404
 
216
405
  def delete
217
- @parent.delete(score, jid)
406
+ if @value
407
+ @parent.delete_by_value(@parent.name, @value)
408
+ else
409
+ @parent.delete_by_jid(score, jid)
410
+ end
218
411
  end
219
412
 
220
413
  def reschedule(at)
221
- @parent.delete(score, jid)
414
+ delete
222
415
  @parent.schedule(at, item)
223
416
  end
224
417
 
225
418
  def add_to_queue
226
- Sidekiq.redis do |conn|
227
- results = conn.zrangebyscore('schedule', score, score)
228
- conn.zremrangebyscore('schedule', score, score)
229
- results.map do |message|
230
- msg = Sidekiq.load_json(message)
231
- Sidekiq::Client.push(msg)
232
- end
419
+ remove_job do |message|
420
+ msg = Sidekiq.load_json(message)
421
+ Sidekiq::Client.push(msg)
233
422
  end
234
423
  end
235
424
 
236
425
  def retry
237
- raise "Retry not available on jobs not in the Retry queue." unless item["failed_at"]
426
+ remove_job do |message|
427
+ msg = Sidekiq.load_json(message)
428
+ msg['retry_count'] -= 1 if msg['retry_count']
429
+ Sidekiq::Client.push(msg)
430
+ end
431
+ end
432
+
433
+ ##
434
+ # Place job in the dead set
435
+ def kill
436
+ remove_job do |message|
437
+ now = Time.now.to_f
438
+ Sidekiq.redis do |conn|
439
+ conn.multi do
440
+ conn.zadd('dead', now, message)
441
+ conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
442
+ conn.zremrangebyrank('dead', 0, - DeadSet.max_jobs)
443
+ end
444
+ end
445
+ end
446
+ end
447
+
448
+ def error?
449
+ !!item['error_class']
450
+ end
451
+
452
+ private
453
+
454
+ def remove_job
238
455
  Sidekiq.redis do |conn|
239
- results = conn.zrangebyscore('retry', score, score)
240
- conn.zremrangebyscore('retry', score, score)
241
- results.map do |message|
242
- msg = Sidekiq.load_json(message)
243
- msg['retry_count'] = msg['retry_count'] - 1
244
- Sidekiq::Client.push(msg)
456
+ results = conn.multi do
457
+ conn.zrangebyscore(parent.name, score, score)
458
+ conn.zremrangebyscore(parent.name, score, score)
459
+ end.first
460
+
461
+ if results.size == 1
462
+ yield results.first
463
+ else
464
+ # multiple jobs with the same score
465
+ # find the one with the right JID and push it
466
+ hash = results.group_by do |message|
467
+ if message.index(jid)
468
+ msg = Sidekiq.load_json(message)
469
+ msg['jid'] == jid
470
+ else
471
+ false
472
+ end
473
+ end
474
+
475
+ msg = hash.fetch(true, []).first
476
+ yield msg if msg
477
+
478
+ # push the rest back onto the sorted set
479
+ conn.multi do
480
+ hash.fetch(false, []).each do |message|
481
+ conn.zadd(parent.name, score.to_f.to_s, message)
482
+ end
483
+ end
245
484
  end
246
485
  end
247
486
  end
487
+
248
488
  end
249
489
 
250
490
  class SortedSet
251
491
  include Enumerable
252
492
 
493
+ attr_reader :name
494
+
253
495
  def initialize(name)
254
- @zset = name
496
+ @name = name
497
+ @_size = size
255
498
  end
256
499
 
257
500
  def size
258
- Sidekiq.redis {|c| c.zcard(@zset) }
501
+ Sidekiq.redis { |c| c.zcard(name) }
259
502
  end
260
503
 
504
+ def clear
505
+ Sidekiq.redis do |conn|
506
+ conn.del(name)
507
+ end
508
+ end
509
+ alias_method :💣, :clear
510
+ end
511
+
512
+ class JobSet < SortedSet
513
+
261
514
  def schedule(timestamp, message)
262
515
  Sidekiq.redis do |conn|
263
- conn.zadd(@zset, timestamp.to_f.to_s, Sidekiq.dump_json(message))
516
+ conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
264
517
  end
265
518
  end
266
519
 
267
- def each(&block)
268
- # page thru the sorted set backwards so deleting entries doesn't screw up indexing
520
+ def each
521
+ initial_size = @_size
522
+ offset_size = 0
269
523
  page = -1
270
524
  page_size = 50
271
525
 
272
- loop do
526
+ while true do
527
+ range_start = page * page_size + offset_size
528
+ range_end = range_start + page_size - 1
273
529
  elements = Sidekiq.redis do |conn|
274
- conn.zrange @zset, page * page_size, (page * page_size) + (page_size - 1), :with_scores => true
530
+ conn.zrange name, range_start, range_end, with_scores: true
275
531
  end
276
532
  break if elements.empty?
277
533
  page -= 1
278
534
  elements.each do |element, score|
279
- block.call SortedEntry.new(self, score, element)
535
+ yield SortedEntry.new(self, score, element)
280
536
  end
537
+ offset_size = initial_size - @_size
281
538
  end
282
539
  end
283
540
 
284
541
  def fetch(score, jid = nil)
285
542
  elements = Sidekiq.redis do |conn|
286
- conn.zrangebyscore(@zset, score, score)
543
+ conn.zrangebyscore(name, score, score)
287
544
  end
288
545
 
289
546
  elements.inject([]) do |result, element|
@@ -297,52 +554,54 @@ module Sidekiq
297
554
  end
298
555
  end
299
556
 
557
+ ##
558
+ # Find the job with the given JID within this sorted set.
559
+ #
560
+ # This is a slow, inefficient operation. Do not use under
561
+ # normal conditions. Sidekiq Pro contains a faster version.
300
562
  def find_job(jid)
301
563
  self.detect { |j| j.jid == jid }
302
564
  end
303
565
 
304
- def delete(score, jid = nil)
305
- if jid
306
- elements = Sidekiq.redis do |conn|
307
- conn.zrangebyscore(@zset, score, score)
308
- end
566
+ def delete_by_value(name, value)
567
+ Sidekiq.redis do |conn|
568
+ ret = conn.zrem(name, value)
569
+ @_size -= 1 if ret
570
+ ret
571
+ end
572
+ end
309
573
 
310
- elements_with_jid = elements.map do |element|
574
+ def delete_by_jid(score, jid)
575
+ Sidekiq.redis do |conn|
576
+ elements = conn.zrangebyscore(name, score, score)
577
+ elements.each do |element|
311
578
  message = Sidekiq.load_json(element)
312
-
313
579
  if message["jid"] == jid
314
- Sidekiq.redis { |conn| conn.zrem(@zset, element) }
580
+ ret = conn.zrem(name, element)
581
+ @_size -= 1 if ret
582
+ break ret
315
583
  end
584
+ false
316
585
  end
317
- elements_with_jid.count != 0
318
- else
319
- count = Sidekiq.redis do |conn|
320
- conn.zremrangebyscore(@zset, score, score)
321
- end
322
- count != 0
323
586
  end
324
587
  end
325
588
 
326
- def clear
327
- Sidekiq.redis do |conn|
328
- conn.del(@zset)
329
- end
330
- end
589
+ alias_method :delete, :delete_by_jid
331
590
  end
332
591
 
333
592
  ##
334
593
  # Allows enumeration of scheduled jobs within Sidekiq.
335
594
  # Based on this, you can search/filter for jobs. Here's an
336
595
  # example where I'm selecting all jobs of a certain type
337
- # and deleting them from the retry queue.
596
+ # and deleting them from the schedule queue.
338
597
  #
339
598
  # r = Sidekiq::ScheduledSet.new
340
- # r.select do |retri|
341
- # retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
342
- # retri.args[0] == 'User' &&
343
- # retri.args[1] == 'setup_new_subscriber'
599
+ # r.select do |scheduled|
600
+ # scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
601
+ # scheduled.args[0] == 'User' &&
602
+ # scheduled.args[1] == 'setup_new_subscriber'
344
603
  # end.map(&:delete)
345
- class ScheduledSet < SortedSet
604
+ class ScheduledSet < JobSet
346
605
  def initialize
347
606
  super 'schedule'
348
607
  end
@@ -360,7 +619,7 @@ module Sidekiq
360
619
  # retri.args[0] == 'User' &&
361
620
  # retri.args[1] == 'setup_new_subscriber'
362
621
  # end.map(&:delete)
363
- class RetrySet < SortedSet
622
+ class RetrySet < JobSet
364
623
  def initialize
365
624
  super 'retry'
366
625
  end
@@ -372,8 +631,165 @@ module Sidekiq
372
631
  end
373
632
  end
374
633
 
634
+ ##
635
+ # Allows enumeration of dead jobs within Sidekiq.
636
+ #
637
+ class DeadSet < JobSet
638
+ def initialize
639
+ super 'dead'
640
+ end
641
+
642
+ def retry_all
643
+ while size > 0
644
+ each(&:retry)
645
+ end
646
+ end
647
+
648
+ def self.max_jobs
649
+ Sidekiq.options[:dead_max_jobs]
650
+ end
651
+
652
+ def self.timeout
653
+ Sidekiq.options[:dead_timeout_in_seconds]
654
+ end
655
+ end
375
656
 
376
657
  ##
658
+ # Enumerates the set of Sidekiq processes which are actively working
659
+ # right now. Each process send a heartbeat to Redis every 5 seconds
660
+ # so this set should be relatively accurate, barring network partitions.
661
+ #
662
+ # Yields a Sidekiq::Process.
663
+ #
664
+ class ProcessSet
665
+ include Enumerable
666
+
667
+ def initialize(clean_plz=true)
668
+ self.class.cleanup if clean_plz
669
+ end
670
+
671
+ # Cleans up dead processes recorded in Redis.
672
+ # Returns the number of processes cleaned.
673
+ def self.cleanup
674
+ count = 0
675
+ Sidekiq.redis do |conn|
676
+ procs = conn.smembers('processes').sort
677
+ heartbeats = conn.pipelined do
678
+ procs.each do |key|
679
+ conn.hget(key, 'info')
680
+ end
681
+ end
682
+
683
+ # the hash named key has an expiry of 60 seconds.
684
+ # if it's not found, that means the process has not reported
685
+ # in to Redis and probably died.
686
+ to_prune = []
687
+ heartbeats.each_with_index do |beat, i|
688
+ to_prune << procs[i] if beat.nil?
689
+ end
690
+ count = conn.srem('processes', to_prune) unless to_prune.empty?
691
+ end
692
+ count
693
+ end
694
+
695
+ def each
696
+ procs = Sidekiq.redis { |conn| conn.smembers('processes') }.sort
697
+
698
+ Sidekiq.redis do |conn|
699
+ # We're making a tradeoff here between consuming more memory instead of
700
+ # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
701
+ # you'll be happier this way
702
+ result = conn.pipelined do
703
+ procs.each do |key|
704
+ conn.hmget(key, 'info', 'busy', 'beat', 'quiet')
705
+ end
706
+ end
707
+
708
+ result.each do |info, busy, at_s, quiet|
709
+ hash = Sidekiq.load_json(info)
710
+ yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f, 'quiet' => quiet))
711
+ end
712
+ end
713
+
714
+ nil
715
+ end
716
+
717
+ # This method is not guaranteed accurate since it does not prune the set
718
+ # based on current heartbeat. #each does that and ensures the set only
719
+ # contains Sidekiq processes which have sent a heartbeat within the last
720
+ # 60 seconds.
721
+ def size
722
+ Sidekiq.redis { |conn| conn.scard('processes') }
723
+ end
724
+ end
725
+
726
+ #
727
+ # Sidekiq::Process represents an active Sidekiq process talking with Redis.
728
+ # Each process has a set of attributes which look like this:
729
+ #
730
+ # {
731
+ # 'hostname' => 'app-1.example.com',
732
+ # 'started_at' => <process start time>,
733
+ # 'pid' => 12345,
734
+ # 'tag' => 'myapp'
735
+ # 'concurrency' => 25,
736
+ # 'queues' => ['default', 'low'],
737
+ # 'busy' => 10,
738
+ # 'beat' => <last heartbeat>,
739
+ # 'identity' => <unique string identifying the process>,
740
+ # }
741
+ class Process
742
+ def initialize(hash)
743
+ @attribs = hash
744
+ end
745
+
746
+ def tag
747
+ self['tag']
748
+ end
749
+
750
+ def labels
751
+ Array(self['labels'])
752
+ end
753
+
754
+ def [](key)
755
+ @attribs[key]
756
+ end
757
+
758
+ def quiet!
759
+ signal('USR1')
760
+ end
761
+
762
+ def stop!
763
+ signal('TERM')
764
+ end
765
+
766
+ def dump_threads
767
+ signal('TTIN')
768
+ end
769
+
770
+ def stopping?
771
+ self['quiet'] == 'true'
772
+ end
773
+
774
+ private
775
+
776
+ def signal(sig)
777
+ key = "#{identity}-signals"
778
+ Sidekiq.redis do |c|
779
+ c.multi do
780
+ c.lpush(key, sig)
781
+ c.expire(key, 60)
782
+ end
783
+ end
784
+ end
785
+
786
+ def identity
787
+ self['identity']
788
+ end
789
+ end
790
+
791
+ ##
792
+ # A worker is a thread that is currently processing a job.
377
793
  # Programmatic access to the current active worker set.
378
794
  #
379
795
  # WARNING WARNING WARNING
@@ -384,34 +800,52 @@ module Sidekiq
384
800
  #
385
801
  # workers = Sidekiq::Workers.new
386
802
  # workers.size => 2
387
- # workers.each do |name, work, started_at|
388
- # # name is a unique identifier per worker
803
+ # workers.each do |process_id, thread_id, work|
804
+ # # process_id is a unique identifier per Sidekiq process
805
+ # # thread_id is a unique identifier per thread
389
806
  # # work is a Hash which looks like:
390
807
  # # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
391
- # # started_at is a String rep of the time when the worker started working on the job
808
+ # # run_at is an epoch Integer.
392
809
  # end
393
-
810
+ #
394
811
  class Workers
395
812
  include Enumerable
396
813
 
397
- def each(&block)
814
+ def each
398
815
  Sidekiq.redis do |conn|
399
- workers = conn.smembers("workers")
400
- workers.each do |w|
401
- msg, time = conn.multi do
402
- conn.get("worker:#{w}")
403
- conn.get("worker:#{w}:started")
816
+ procs = conn.smembers('processes')
817
+ procs.sort.each do |key|
818
+ valid, workers = conn.pipelined do
819
+ conn.exists(key)
820
+ conn.hgetall("#{key}:workers")
821
+ end
822
+ next unless valid
823
+ workers.each_pair do |tid, json|
824
+ yield key, tid, Sidekiq.load_json(json)
404
825
  end
405
- next unless msg
406
- block.call(w, Sidekiq.load_json(msg), time)
407
826
  end
408
827
  end
409
828
  end
410
829
 
830
+ # Note that #size is only as accurate as Sidekiq's heartbeat,
831
+ # which happens every 5 seconds. It is NOT real-time.
832
+ #
833
+ # Not very efficient if you have lots of Sidekiq
834
+ # processes but the alternative is a global counter
835
+ # which can easily get out of sync with crashy processes.
411
836
  def size
412
837
  Sidekiq.redis do |conn|
413
- conn.scard("workers")
414
- end.to_i
838
+ procs = conn.smembers('processes')
839
+ if procs.empty?
840
+ 0
841
+ else
842
+ conn.pipelined do
843
+ procs.each do |key|
844
+ conn.hget(key, 'busy')
845
+ end
846
+ end.map(&:to_i).inject(:+)
847
+ end
848
+ end
415
849
  end
416
850
  end
417
851