sidekiq 3.5.4 → 5.2.7

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 (175) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +61 -0
  3. data/{Contributing.md → .github/contributing.md} +0 -0
  4. data/.github/issue_template.md +11 -0
  5. data/.gitignore +3 -0
  6. data/.travis.yml +5 -10
  7. data/4.0-Upgrade.md +53 -0
  8. data/5.0-Upgrade.md +56 -0
  9. data/COMM-LICENSE +13 -11
  10. data/Changes.md +376 -1
  11. data/Ent-Changes.md +201 -2
  12. data/Gemfile +14 -18
  13. data/LICENSE +1 -1
  14. data/Pro-3.0-Upgrade.md +44 -0
  15. data/Pro-4.0-Upgrade.md +35 -0
  16. data/Pro-Changes.md +307 -2
  17. data/README.md +34 -22
  18. data/Rakefile +3 -3
  19. data/bin/sidekiq +0 -1
  20. data/bin/sidekiqctl +13 -86
  21. data/bin/sidekiqload +23 -27
  22. data/code_of_conduct.md +50 -0
  23. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +3 -3
  24. data/lib/generators/sidekiq/templates/worker_test.rb.erb +6 -6
  25. data/lib/sidekiq.rb +72 -25
  26. data/lib/sidekiq/api.rb +206 -73
  27. data/lib/sidekiq/cli.rb +145 -101
  28. data/lib/sidekiq/client.rb +42 -36
  29. data/lib/sidekiq/core_ext.rb +1 -105
  30. data/lib/sidekiq/ctl.rb +221 -0
  31. data/lib/sidekiq/delay.rb +42 -0
  32. data/lib/sidekiq/exception_handler.rb +4 -5
  33. data/lib/sidekiq/extensions/action_mailer.rb +1 -0
  34. data/lib/sidekiq/extensions/active_record.rb +1 -0
  35. data/lib/sidekiq/extensions/class_methods.rb +1 -0
  36. data/lib/sidekiq/extensions/generic_proxy.rb +8 -1
  37. data/lib/sidekiq/fetch.rb +36 -111
  38. data/lib/sidekiq/job_logger.rb +25 -0
  39. data/lib/sidekiq/job_retry.rb +262 -0
  40. data/lib/sidekiq/launcher.rb +129 -55
  41. data/lib/sidekiq/logging.rb +21 -3
  42. data/lib/sidekiq/manager.rb +83 -182
  43. data/lib/sidekiq/middleware/chain.rb +1 -0
  44. data/lib/sidekiq/middleware/i18n.rb +1 -0
  45. data/lib/sidekiq/middleware/server/active_record.rb +10 -0
  46. data/lib/sidekiq/paginator.rb +1 -0
  47. data/lib/sidekiq/processor.rb +221 -103
  48. data/lib/sidekiq/rails.rb +47 -27
  49. data/lib/sidekiq/redis_connection.rb +74 -7
  50. data/lib/sidekiq/scheduled.rb +87 -28
  51. data/lib/sidekiq/testing.rb +150 -19
  52. data/lib/sidekiq/testing/inline.rb +1 -0
  53. data/lib/sidekiq/util.rb +15 -17
  54. data/lib/sidekiq/version.rb +2 -1
  55. data/lib/sidekiq/web.rb +120 -184
  56. data/lib/sidekiq/web/action.rb +89 -0
  57. data/lib/sidekiq/web/application.rb +353 -0
  58. data/lib/sidekiq/{web_helpers.rb → web/helpers.rb} +123 -47
  59. data/lib/sidekiq/web/router.rb +100 -0
  60. data/lib/sidekiq/worker.rb +135 -18
  61. data/sidekiq.gemspec +8 -14
  62. data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
  63. data/web/assets/javascripts/application.js +24 -20
  64. data/web/assets/javascripts/dashboard.js +33 -18
  65. data/web/assets/stylesheets/application-rtl.css +246 -0
  66. data/web/assets/stylesheets/application.css +401 -7
  67. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  68. data/web/assets/stylesheets/bootstrap.css +4 -8
  69. data/web/locales/ar.yml +81 -0
  70. data/web/locales/cs.yml +11 -1
  71. data/web/locales/de.yml +1 -1
  72. data/web/locales/en.yml +4 -0
  73. data/web/locales/es.yml +4 -3
  74. data/web/locales/fa.yml +80 -0
  75. data/web/locales/fr.yml +21 -12
  76. data/web/locales/he.yml +79 -0
  77. data/web/locales/ja.yml +24 -13
  78. data/web/locales/ru.yml +3 -0
  79. data/web/locales/ur.yml +80 -0
  80. data/web/views/_footer.erb +7 -9
  81. data/web/views/_job_info.erb +5 -1
  82. data/web/views/_nav.erb +5 -19
  83. data/web/views/_paging.erb +1 -1
  84. data/web/views/busy.erb +18 -9
  85. data/web/views/dashboard.erb +5 -5
  86. data/web/views/dead.erb +1 -1
  87. data/web/views/layout.erb +13 -5
  88. data/web/views/morgue.erb +16 -12
  89. data/web/views/queue.erb +12 -11
  90. data/web/views/queues.erb +5 -3
  91. data/web/views/retries.erb +19 -13
  92. data/web/views/retry.erb +2 -2
  93. data/web/views/scheduled.erb +4 -4
  94. data/web/views/scheduled_job_info.erb +1 -1
  95. metadata +45 -227
  96. data/lib/sidekiq/actor.rb +0 -39
  97. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  98. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -206
  99. data/test/config.yml +0 -9
  100. data/test/env_based_config.yml +0 -11
  101. data/test/fake_env.rb +0 -0
  102. data/test/fixtures/en.yml +0 -2
  103. data/test/helper.rb +0 -49
  104. data/test/test_api.rb +0 -493
  105. data/test/test_cli.rb +0 -335
  106. data/test/test_client.rb +0 -194
  107. data/test/test_exception_handler.rb +0 -55
  108. data/test/test_extensions.rb +0 -126
  109. data/test/test_fetch.rb +0 -104
  110. data/test/test_logging.rb +0 -34
  111. data/test/test_manager.rb +0 -168
  112. data/test/test_middleware.rb +0 -159
  113. data/test/test_processor.rb +0 -237
  114. data/test/test_rails.rb +0 -21
  115. data/test/test_redis_connection.rb +0 -126
  116. data/test/test_retry.rb +0 -325
  117. data/test/test_scheduled.rb +0 -114
  118. data/test/test_scheduling.rb +0 -49
  119. data/test/test_sidekiq.rb +0 -99
  120. data/test/test_testing.rb +0 -142
  121. data/test/test_testing_fake.rb +0 -268
  122. data/test/test_testing_inline.rb +0 -93
  123. data/test/test_util.rb +0 -16
  124. data/test/test_web.rb +0 -608
  125. data/test/test_web_helpers.rb +0 -53
  126. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  127. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  128. data/web/assets/images/status/active.png +0 -0
  129. data/web/assets/images/status/idle.png +0 -0
  130. data/web/assets/javascripts/locales/README.md +0 -27
  131. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  132. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  133. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  134. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  135. data/web/assets/javascripts/locales/jquery.timeago.cs.js +0 -18
  136. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  137. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  138. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  139. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  140. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  141. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  142. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  143. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  144. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  145. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  146. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  147. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  148. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  149. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  150. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  151. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  152. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  153. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  154. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  155. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  156. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  157. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  158. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  159. data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
  160. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  161. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  162. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  163. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  164. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  165. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  166. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  167. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  168. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  169. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  170. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  171. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  172. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  173. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +0 -20
  174. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +0 -20
  175. data/web/views/_poll_js.erb +0 -5
@@ -1,8 +1,24 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
  require 'sidekiq'
3
3
 
4
4
  module Sidekiq
5
+
6
+ module RedisScanner
7
+ def sscan(conn, key)
8
+ cursor = '0'
9
+ result = []
10
+ loop do
11
+ cursor, values = conn.sscan(key, cursor)
12
+ result.push(*values)
13
+ break if cursor == '0'
14
+ end
15
+ result
16
+ end
17
+ end
18
+
5
19
  class Stats
20
+ include RedisScanner
21
+
6
22
  def initialize
7
23
  fetch_stats!
8
24
  end
@@ -50,31 +66,40 @@ module Sidekiq
50
66
  def fetch_stats!
51
67
  pipe1_res = Sidekiq.redis do |conn|
52
68
  conn.pipelined do
53
- conn.get('stat:processed'.freeze)
54
- conn.get('stat:failed'.freeze)
55
- conn.zcard('schedule'.freeze)
56
- conn.zcard('retry'.freeze)
57
- conn.zcard('dead'.freeze)
58
- conn.scard('processes'.freeze)
59
- conn.lrange('queue:default'.freeze, -1, -1)
60
- conn.smembers('processes'.freeze)
61
- conn.smembers('queues'.freeze)
69
+ conn.get('stat:processed')
70
+ conn.get('stat:failed')
71
+ conn.zcard('schedule')
72
+ conn.zcard('retry')
73
+ conn.zcard('dead')
74
+ conn.scard('processes')
75
+ conn.lrange('queue:default', -1, -1)
62
76
  end
63
77
  end
64
78
 
79
+ processes = Sidekiq.redis do |conn|
80
+ sscan(conn, 'processes')
81
+ end
82
+
83
+ queues = Sidekiq.redis do |conn|
84
+ sscan(conn, 'queues')
85
+ end
86
+
65
87
  pipe2_res = Sidekiq.redis do |conn|
66
88
  conn.pipelined do
67
- pipe1_res[7].each {|key| conn.hget(key, 'busy'.freeze) }
68
- pipe1_res[8].each {|queue| conn.llen("queue:#{queue}") }
89
+ processes.each {|key| conn.hget(key, 'busy') }
90
+ queues.each {|queue| conn.llen("queue:#{queue}") }
69
91
  end
70
92
  end
71
93
 
72
- s = pipe1_res[7].size
94
+ s = processes.size
73
95
  workers_size = pipe2_res[0...s].map(&:to_i).inject(0, &:+)
74
96
  enqueued = pipe2_res[s..-1].map(&:to_i).inject(0, &:+)
75
97
 
76
98
  default_queue_latency = if (entry = pipe1_res[6].first)
77
- Time.now.to_f - Sidekiq.load_json(entry)['enqueued_at'.freeze]
99
+ job = Sidekiq.load_json(entry) rescue {}
100
+ now = Time.now.to_f
101
+ thence = job['enqueued_at'] || now
102
+ now - thence
78
103
  else
79
104
  0
80
105
  end
@@ -113,9 +138,11 @@ module Sidekiq
113
138
  end
114
139
 
115
140
  class Queues
141
+ include RedisScanner
142
+
116
143
  def lengths
117
144
  Sidekiq.redis do |conn|
118
- queues = conn.smembers('queues'.freeze)
145
+ queues = sscan(conn, 'queues')
119
146
 
120
147
  lengths = conn.pipelined do
121
148
  queues.each do |queue|
@@ -142,11 +169,11 @@ module Sidekiq
142
169
  end
143
170
 
144
171
  def processed
145
- date_stat_hash("processed")
172
+ @processed ||= date_stat_hash("processed")
146
173
  end
147
174
 
148
175
  def failed
149
- date_stat_hash("failed")
176
+ @failed ||= date_stat_hash("failed")
150
177
  end
151
178
 
152
179
  private
@@ -159,16 +186,21 @@ module Sidekiq
159
186
 
160
187
  while i < @days_previous
161
188
  date = @start_date - i
162
- datestr = date.strftime("%Y-%m-%d".freeze)
189
+ datestr = date.strftime("%Y-%m-%d")
163
190
  keys << "stat:#{stat}:#{datestr}"
164
191
  dates << datestr
165
192
  i += 1
166
193
  end
167
194
 
168
- Sidekiq.redis do |conn|
169
- conn.mget(keys).each_with_index do |value, idx|
170
- stat_hash[dates[idx]] = value ? value.to_i : 0
195
+ begin
196
+ Sidekiq.redis do |conn|
197
+ conn.mget(keys).each_with_index do |value, idx|
198
+ stat_hash[dates[idx]] = value ? value.to_i : 0
199
+ end
171
200
  end
201
+ rescue Redis::CommandError
202
+ # mget will trigger a CROSSSLOT error when run against a Cluster
203
+ # TODO Someone want to add Cluster support?
172
204
  end
173
205
 
174
206
  stat_hash
@@ -190,15 +222,19 @@ module Sidekiq
190
222
  #
191
223
  class Queue
192
224
  include Enumerable
225
+ extend RedisScanner
193
226
 
227
+ ##
228
+ # Return all known queues within Redis.
229
+ #
194
230
  def self.all
195
- Sidekiq.redis {|c| c.smembers('queues'.freeze) }.sort.map {|q| Sidekiq::Queue.new(q) }
231
+ Sidekiq.redis { |c| sscan(c, 'queues') }.sort.map { |q| Sidekiq::Queue.new(q) }
196
232
  end
197
233
 
198
234
  attr_reader :name
199
235
 
200
236
  def initialize(name="default")
201
- @name = name
237
+ @name = name.to_s
202
238
  @rname = "queue:#{name}"
203
239
  end
204
240
 
@@ -211,12 +247,20 @@ module Sidekiq
211
247
  false
212
248
  end
213
249
 
250
+ ##
251
+ # Calculates this queue's latency, the difference in seconds since the oldest
252
+ # job in the queue was enqueued.
253
+ #
254
+ # @return Float
214
255
  def latency
215
256
  entry = Sidekiq.redis do |conn|
216
257
  conn.lrange(@rname, -1, -1)
217
258
  end.first
218
259
  return 0 unless entry
219
- Time.now.to_f - Sidekiq.load_json(entry)['enqueued_at']
260
+ job = Sidekiq.load_json(entry)
261
+ now = Time.now.to_f
262
+ thence = job['enqueued_at'] || now
263
+ now - thence
220
264
  end
221
265
 
222
266
  def each
@@ -225,9 +269,9 @@ module Sidekiq
225
269
  page = 0
226
270
  page_size = 50
227
271
 
228
- loop do
272
+ while true do
229
273
  range_start = page * page_size - deleted_size
230
- range_end = page * page_size - deleted_size + (page_size - 1)
274
+ range_end = range_start + page_size - 1
231
275
  entries = Sidekiq.redis do |conn|
232
276
  conn.lrange @rname, range_start, range_end
233
277
  end
@@ -240,6 +284,11 @@ module Sidekiq
240
284
  end
241
285
  end
242
286
 
287
+ ##
288
+ # Find the job with the given JID within this queue.
289
+ #
290
+ # This is a slow, inefficient operation. Do not use under
291
+ # normal conditions. Sidekiq Pro contains a faster version.
243
292
  def find_job(jid)
244
293
  detect { |j| j.jid == jid }
245
294
  end
@@ -248,7 +297,7 @@ module Sidekiq
248
297
  Sidekiq.redis do |conn|
249
298
  conn.multi do
250
299
  conn.del(@rname)
251
- conn.srem("queues".freeze, name)
300
+ conn.srem("queues", name)
252
301
  end
253
302
  end
254
303
  end
@@ -264,15 +313,28 @@ module Sidekiq
264
313
  #
265
314
  class Job
266
315
  attr_reader :item
316
+ attr_reader :value
267
317
 
268
318
  def initialize(item, queue_name=nil)
319
+ @args = nil
269
320
  @value = item
270
- @item = item.is_a?(Hash) ? item : Sidekiq.load_json(item)
321
+ @item = item.is_a?(Hash) ? item : parse(item)
271
322
  @queue = queue_name || @item['queue']
272
323
  end
273
324
 
325
+ def parse(item)
326
+ Sidekiq.load_json(item)
327
+ rescue JSON::ParserError
328
+ # If the job payload in Redis is invalid JSON, we'll load
329
+ # the item as an empty hash and store the invalid JSON as
330
+ # the job 'args' for display in the Web UI.
331
+ @invalid = true
332
+ @args = [item]
333
+ {}
334
+ end
335
+
274
336
  def klass
275
- @item['class']
337
+ self['class']
276
338
  end
277
339
 
278
340
  def display_class
@@ -283,7 +345,13 @@ module Sidekiq
283
345
  "#{target}.#{method}"
284
346
  end
285
347
  when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
286
- @item['wrapped'] || args[0]
348
+ job_class = @item['wrapped'] || args[0]
349
+ if 'ActionMailer::DeliveryJob' == job_class
350
+ # MailerClass#mailer_method
351
+ args[0]['arguments'][0..1].join('#')
352
+ else
353
+ job_class
354
+ end
287
355
  else
288
356
  klass
289
357
  end
@@ -291,32 +359,42 @@ module Sidekiq
291
359
 
292
360
  def display_args
293
361
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
294
- @args ||= case klass
362
+ @display_args ||= case klass
295
363
  when /\ASidekiq::Extensions::Delayed/
296
364
  safe_load(args[0], args) do |_, _, arg|
297
365
  arg
298
366
  end
299
367
  when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
300
- @item['wrapped'] ? args[0]["arguments"] : []
368
+ job_args = self['wrapped'] ? args[0]["arguments"] : []
369
+ if 'ActionMailer::DeliveryJob' == (self['wrapped'] || args[0])
370
+ # remove MailerClass, mailer_method and 'deliver_now'
371
+ job_args.drop(3)
372
+ else
373
+ job_args
374
+ end
301
375
  else
376
+ if self['encrypt']
377
+ # no point in showing 150+ bytes of random garbage
378
+ args[-1] = '[encrypted data]'
379
+ end
302
380
  args
303
381
  end
304
382
  end
305
383
 
306
384
  def args
307
- @item['args']
385
+ @args || @item['args']
308
386
  end
309
387
 
310
388
  def jid
311
- @item['jid']
389
+ self['jid']
312
390
  end
313
391
 
314
392
  def enqueued_at
315
- @item['enqueued_at'] ? Time.at(@item['enqueued_at']).utc : nil
393
+ self['enqueued_at'] ? Time.at(self['enqueued_at']).utc : nil
316
394
  end
317
395
 
318
396
  def created_at
319
- Time.at(@item['created_at'] || @item['enqueued_at'] || 0).utc
397
+ Time.at(self['created_at'] || self['enqueued_at'] || 0).utc
320
398
  end
321
399
 
322
400
  def queue
@@ -324,7 +402,8 @@ module Sidekiq
324
402
  end
325
403
 
326
404
  def latency
327
- Time.now.to_f - (@item['enqueued_at'] || @item['created_at'])
405
+ now = Time.now.to_f
406
+ now - (@item['enqueued_at'] || @item['created_at'] || now)
328
407
  end
329
408
 
330
409
  ##
@@ -337,7 +416,10 @@ module Sidekiq
337
416
  end
338
417
 
339
418
  def [](name)
340
- @item.__send__(:[], name)
419
+ # nil will happen if the JSON fails to parse.
420
+ # We don't guarantee Sidekiq will work with bad job JSON but we should
421
+ # make a best effort to minimize the damage.
422
+ @item ? @item[name] : nil
341
423
  end
342
424
 
343
425
  private
@@ -389,10 +471,9 @@ module Sidekiq
389
471
  end
390
472
 
391
473
  def retry
392
- raise "Retry not available on jobs which have not failed" unless item["failed_at"]
393
474
  remove_job do |message|
394
475
  msg = Sidekiq.load_json(message)
395
- msg['retry_count'] -= 1
476
+ msg['retry_count'] -= 1 if msg['retry_count']
396
477
  Sidekiq::Client.push(msg)
397
478
  end
398
479
  end
@@ -400,20 +481,15 @@ module Sidekiq
400
481
  ##
401
482
  # Place job in the dead set
402
483
  def kill
403
- raise 'Kill not available on jobs which have not failed' unless item['failed_at']
404
484
  remove_job do |message|
405
- Sidekiq.logger.info { "Killing job #{message['jid']}" }
406
- now = Time.now.to_f
407
- Sidekiq.redis do |conn|
408
- conn.multi do
409
- conn.zadd('dead', now, message)
410
- conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
411
- conn.zremrangebyrank('dead', 0, - DeadSet.max_jobs)
412
- end
413
- end
485
+ DeadSet.new.kill(message)
414
486
  end
415
487
  end
416
488
 
489
+ def error?
490
+ !!item['error_class']
491
+ end
492
+
417
493
  private
418
494
 
419
495
  def remove_job
@@ -488,15 +564,15 @@ module Sidekiq
488
564
  page = -1
489
565
  page_size = 50
490
566
 
491
- loop do
567
+ while true do
492
568
  range_start = page * page_size + offset_size
493
- range_end = page * page_size + offset_size + (page_size - 1)
569
+ range_end = range_start + page_size - 1
494
570
  elements = Sidekiq.redis do |conn|
495
571
  conn.zrange name, range_start, range_end, with_scores: true
496
572
  end
497
573
  break if elements.empty?
498
574
  page -= 1
499
- elements.each do |element, score|
575
+ elements.reverse.each do |element, score|
500
576
  yield SortedEntry.new(self, score, element)
501
577
  end
502
578
  offset_size = initial_size - @_size
@@ -519,6 +595,11 @@ module Sidekiq
519
595
  end
520
596
  end
521
597
 
598
+ ##
599
+ # Find the job with the given JID within this sorted set.
600
+ #
601
+ # This is a slow, inefficient operation. Do not use under
602
+ # normal conditions. Sidekiq Pro contains a faster version.
522
603
  def find_job(jid)
523
604
  self.detect { |j| j.jid == jid }
524
605
  end
@@ -553,13 +634,13 @@ module Sidekiq
553
634
  # Allows enumeration of scheduled jobs within Sidekiq.
554
635
  # Based on this, you can search/filter for jobs. Here's an
555
636
  # example where I'm selecting all jobs of a certain type
556
- # and deleting them from the retry queue.
637
+ # and deleting them from the schedule queue.
557
638
  #
558
639
  # r = Sidekiq::ScheduledSet.new
559
- # r.select do |retri|
560
- # retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
561
- # retri.args[0] == 'User' &&
562
- # retri.args[1] == 'setup_new_subscriber'
640
+ # r.select do |scheduled|
641
+ # scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
642
+ # scheduled.args[0] == 'User' &&
643
+ # scheduled.args[1] == 'setup_new_subscriber'
563
644
  # end.map(&:delete)
564
645
  class ScheduledSet < JobSet
565
646
  def initialize
@@ -589,6 +670,12 @@ module Sidekiq
589
670
  each(&:retry)
590
671
  end
591
672
  end
673
+
674
+ def kill_all
675
+ while size > 0
676
+ each(&:kill)
677
+ end
678
+ end
592
679
  end
593
680
 
594
681
  ##
@@ -599,6 +686,27 @@ module Sidekiq
599
686
  super 'dead'
600
687
  end
601
688
 
689
+ def kill(message, opts={})
690
+ now = Time.now.to_f
691
+ Sidekiq.redis do |conn|
692
+ conn.multi do
693
+ conn.zadd(name, now.to_s, message)
694
+ conn.zremrangebyscore(name, '-inf', now - self.class.timeout)
695
+ conn.zremrangebyrank(name, 0, - self.class.max_jobs)
696
+ end
697
+ end
698
+
699
+ if opts[:notify_failure] != false
700
+ job = Sidekiq.load_json(message)
701
+ r = RuntimeError.new("Job killed by API")
702
+ r.set_backtrace(caller)
703
+ Sidekiq.death_handlers.each do |handle|
704
+ handle.call(job, r)
705
+ end
706
+ end
707
+ true
708
+ end
709
+
602
710
  def retry_all
603
711
  while size > 0
604
712
  each(&:retry)
@@ -621,20 +729,20 @@ module Sidekiq
621
729
  #
622
730
  # Yields a Sidekiq::Process.
623
731
  #
624
-
625
732
  class ProcessSet
626
733
  include Enumerable
734
+ include RedisScanner
627
735
 
628
736
  def initialize(clean_plz=true)
629
- self.class.cleanup if clean_plz
737
+ cleanup if clean_plz
630
738
  end
631
739
 
632
740
  # Cleans up dead processes recorded in Redis.
633
741
  # Returns the number of processes cleaned.
634
- def self.cleanup
742
+ def cleanup
635
743
  count = 0
636
744
  Sidekiq.redis do |conn|
637
- procs = conn.smembers('processes').sort
745
+ procs = sscan(conn, 'processes').sort
638
746
  heartbeats = conn.pipelined do
639
747
  procs.each do |key|
640
748
  conn.hget(key, 'info')
@@ -654,7 +762,7 @@ module Sidekiq
654
762
  end
655
763
 
656
764
  def each
657
- procs = Sidekiq.redis { |conn| conn.smembers('processes') }.sort
765
+ procs = Sidekiq.redis { |conn| sscan(conn, 'processes') }.sort
658
766
 
659
767
  Sidekiq.redis do |conn|
660
768
  # We're making a tradeoff here between consuming more memory instead of
@@ -662,13 +770,18 @@ module Sidekiq
662
770
  # you'll be happier this way
663
771
  result = conn.pipelined do
664
772
  procs.each do |key|
665
- conn.hmget(key, 'info', 'busy', 'beat')
773
+ conn.hmget(key, 'info', 'busy', 'beat', 'quiet')
666
774
  end
667
775
  end
668
776
 
669
- result.each do |info, busy, at_s|
777
+ result.each do |info, busy, at_s, quiet|
778
+ # If a process is stopped between when we query Redis for `procs` and
779
+ # when we query for `result`, we will have an item in `result` that is
780
+ # composed of `nil` values.
781
+ next if info.nil?
782
+
670
783
  hash = Sidekiq.load_json(info)
671
- yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f))
784
+ yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f, 'quiet' => quiet))
672
785
  end
673
786
  end
674
787
 
@@ -682,10 +795,23 @@ module Sidekiq
682
795
  def size
683
796
  Sidekiq.redis { |conn| conn.scard('processes') }
684
797
  end
798
+
799
+ # Returns the identity of the current cluster leader or "" if no leader.
800
+ # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
801
+ # or Sidekiq Pro.
802
+ def leader
803
+ @leader ||= begin
804
+ x = Sidekiq.redis {|c| c.get("dear-leader") }
805
+ # need a non-falsy value so we can memoize
806
+ x = "" unless x
807
+ x
808
+ end
809
+ end
685
810
  end
686
811
 
687
812
  #
688
- # Sidekiq::Process has a set of attributes which look like this:
813
+ # Sidekiq::Process represents an active Sidekiq process talking with Redis.
814
+ # Each process has a set of attributes which look like this:
689
815
  #
690
816
  # {
691
817
  # 'hostname' => 'app-1.example.com',
@@ -715,8 +841,12 @@ module Sidekiq
715
841
  @attribs[key]
716
842
  end
717
843
 
844
+ def identity
845
+ self['identity']
846
+ end
847
+
718
848
  def quiet!
719
- signal('USR1')
849
+ signal('TSTP')
720
850
  end
721
851
 
722
852
  def stop!
@@ -727,6 +857,10 @@ module Sidekiq
727
857
  signal('TTIN')
728
858
  end
729
859
 
860
+ def stopping?
861
+ self['quiet'] == 'true'
862
+ end
863
+
730
864
  private
731
865
 
732
866
  def signal(sig)
@@ -739,12 +873,10 @@ module Sidekiq
739
873
  end
740
874
  end
741
875
 
742
- def identity
743
- self['identity']
744
- end
745
876
  end
746
877
 
747
878
  ##
879
+ # A worker is a thread that is currently processing a job.
748
880
  # Programmatic access to the current active worker set.
749
881
  #
750
882
  # WARNING WARNING WARNING
@@ -765,10 +897,11 @@ module Sidekiq
765
897
  #
766
898
  class Workers
767
899
  include Enumerable
900
+ include RedisScanner
768
901
 
769
902
  def each
770
903
  Sidekiq.redis do |conn|
771
- procs = conn.smembers('processes')
904
+ procs = sscan(conn, 'processes')
772
905
  procs.sort.each do |key|
773
906
  valid, workers = conn.pipelined do
774
907
  conn.exists(key)
@@ -790,7 +923,7 @@ module Sidekiq
790
923
  # which can easily get out of sync with crashy processes.
791
924
  def size
792
925
  Sidekiq.redis do |conn|
793
- procs = conn.smembers('processes')
926
+ procs = sscan(conn, 'processes')
794
927
  if procs.empty?
795
928
  0
796
929
  else