sidekiq 5.2.1 → 6.4.0

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 (106) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +368 -1
  3. data/LICENSE +3 -3
  4. data/README.md +21 -37
  5. data/bin/sidekiq +26 -2
  6. data/bin/sidekiqload +33 -25
  7. data/bin/sidekiqmon +8 -0
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +316 -246
  13. data/lib/sidekiq/cli.rb +195 -221
  14. data/lib/sidekiq/client.rb +42 -60
  15. data/lib/sidekiq/delay.rb +7 -6
  16. data/lib/sidekiq/exception_handler.rb +10 -12
  17. data/lib/sidekiq/extensions/action_mailer.rb +15 -24
  18. data/lib/sidekiq/extensions/active_record.rb +15 -12
  19. data/lib/sidekiq/extensions/class_methods.rb +16 -13
  20. data/lib/sidekiq/extensions/generic_proxy.rb +8 -6
  21. data/lib/sidekiq/fetch.rb +39 -31
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +47 -9
  24. data/lib/sidekiq/job_retry.rb +88 -68
  25. data/lib/sidekiq/job_util.rb +65 -0
  26. data/lib/sidekiq/launcher.rb +151 -61
  27. data/lib/sidekiq/logger.rb +166 -0
  28. data/lib/sidekiq/manager.rb +18 -22
  29. data/lib/sidekiq/middleware/chain.rb +20 -8
  30. data/lib/sidekiq/middleware/current_attributes.rb +57 -0
  31. data/lib/sidekiq/middleware/i18n.rb +5 -7
  32. data/lib/sidekiq/monitor.rb +133 -0
  33. data/lib/sidekiq/paginator.rb +18 -14
  34. data/lib/sidekiq/processor.rb +116 -82
  35. data/lib/sidekiq/rails.rb +42 -38
  36. data/lib/sidekiq/redis_connection.rb +49 -30
  37. data/lib/sidekiq/scheduled.rb +62 -28
  38. data/lib/sidekiq/sd_notify.rb +149 -0
  39. data/lib/sidekiq/systemd.rb +24 -0
  40. data/lib/sidekiq/testing/inline.rb +2 -1
  41. data/lib/sidekiq/testing.rb +36 -27
  42. data/lib/sidekiq/util.rb +57 -15
  43. data/lib/sidekiq/version.rb +2 -1
  44. data/lib/sidekiq/web/action.rb +15 -11
  45. data/lib/sidekiq/web/application.rb +95 -76
  46. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  47. data/lib/sidekiq/web/helpers.rb +115 -91
  48. data/lib/sidekiq/web/router.rb +23 -19
  49. data/lib/sidekiq/web.rb +61 -105
  50. data/lib/sidekiq/worker.rb +259 -99
  51. data/lib/sidekiq.rb +79 -45
  52. data/sidekiq.gemspec +23 -18
  53. data/web/assets/images/apple-touch-icon.png +0 -0
  54. data/web/assets/javascripts/application.js +83 -64
  55. data/web/assets/javascripts/dashboard.js +66 -75
  56. data/web/assets/stylesheets/application-dark.css +143 -0
  57. data/web/assets/stylesheets/application-rtl.css +0 -4
  58. data/web/assets/stylesheets/application.css +75 -231
  59. data/web/assets/stylesheets/bootstrap.css +1 -1
  60. data/web/locales/ar.yml +9 -2
  61. data/web/locales/de.yml +14 -2
  62. data/web/locales/en.yml +7 -1
  63. data/web/locales/es.yml +18 -2
  64. data/web/locales/fr.yml +10 -3
  65. data/web/locales/ja.yml +7 -1
  66. data/web/locales/lt.yml +83 -0
  67. data/web/locales/pl.yml +4 -4
  68. data/web/locales/ru.yml +4 -0
  69. data/web/locales/vi.yml +83 -0
  70. data/web/views/_footer.erb +1 -1
  71. data/web/views/_job_info.erb +3 -2
  72. data/web/views/_nav.erb +3 -17
  73. data/web/views/_poll_link.erb +2 -5
  74. data/web/views/_summary.erb +7 -7
  75. data/web/views/busy.erb +54 -20
  76. data/web/views/dashboard.erb +22 -14
  77. data/web/views/dead.erb +3 -3
  78. data/web/views/layout.erb +3 -1
  79. data/web/views/morgue.erb +9 -6
  80. data/web/views/queue.erb +20 -10
  81. data/web/views/queues.erb +11 -3
  82. data/web/views/retries.erb +14 -7
  83. data/web/views/retry.erb +3 -3
  84. data/web/views/scheduled.erb +5 -2
  85. metadata +39 -54
  86. data/.github/contributing.md +0 -32
  87. data/.github/issue_template.md +0 -11
  88. data/.gitignore +0 -13
  89. data/.travis.yml +0 -14
  90. data/3.0-Upgrade.md +0 -70
  91. data/4.0-Upgrade.md +0 -53
  92. data/5.0-Upgrade.md +0 -56
  93. data/COMM-LICENSE +0 -95
  94. data/Ent-Changes.md +0 -221
  95. data/Gemfile +0 -14
  96. data/Pro-2.0-Upgrade.md +0 -138
  97. data/Pro-3.0-Upgrade.md +0 -44
  98. data/Pro-4.0-Upgrade.md +0 -35
  99. data/Pro-Changes.md +0 -739
  100. data/Rakefile +0 -8
  101. data/bin/sidekiqctl +0 -99
  102. data/code_of_conduct.md +0 -50
  103. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  104. data/lib/sidekiq/core_ext.rb +0 -1
  105. data/lib/sidekiq/logging.rb +0 -122
  106. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
data/lib/sidekiq/api.rb CHANGED
@@ -1,26 +1,14 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq'
3
2
 
4
- module Sidekiq
3
+ require "sidekiq"
5
4
 
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
5
+ require "zlib"
6
+ require "base64"
18
7
 
8
+ module Sidekiq
19
9
  class Stats
20
- include RedisScanner
21
-
22
10
  def initialize
23
- fetch_stats!
11
+ fetch_stats_fast!
24
12
  end
25
13
 
26
14
  def processed
@@ -63,62 +51,78 @@ module Sidekiq
63
51
  Sidekiq::Stats::Queues.new.lengths
64
52
  end
65
53
 
66
- def fetch_stats!
67
- pipe1_res = Sidekiq.redis do |conn|
54
+ # O(1) redis calls
55
+ def fetch_stats_fast!
56
+ pipe1_res = Sidekiq.redis { |conn|
68
57
  conn.pipelined do
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)
58
+ conn.get("stat:processed")
59
+ conn.get("stat:failed")
60
+ conn.zcard("schedule")
61
+ conn.zcard("retry")
62
+ conn.zcard("dead")
63
+ conn.scard("processes")
64
+ conn.lrange("queue:default", -1, -1)
76
65
  end
77
- end
66
+ }
78
67
 
79
- processes = Sidekiq.redis do |conn|
80
- sscan(conn, 'processes')
68
+ default_queue_latency = if (entry = pipe1_res[6].first)
69
+ job = begin
70
+ Sidekiq.load_json(entry)
71
+ rescue
72
+ {}
73
+ end
74
+ now = Time.now.to_f
75
+ thence = job["enqueued_at"] || now
76
+ now - thence
77
+ else
78
+ 0
81
79
  end
82
80
 
83
- queues = Sidekiq.redis do |conn|
84
- sscan(conn, 'queues')
85
- end
81
+ @stats = {
82
+ processed: pipe1_res[0].to_i,
83
+ failed: pipe1_res[1].to_i,
84
+ scheduled_size: pipe1_res[2],
85
+ retry_size: pipe1_res[3],
86
+ dead_size: pipe1_res[4],
87
+ processes_size: pipe1_res[5],
88
+
89
+ default_queue_latency: default_queue_latency
90
+ }
91
+ end
86
92
 
87
- pipe2_res = Sidekiq.redis do |conn|
93
+ # O(number of processes + number of queues) redis calls
94
+ def fetch_stats_slow!
95
+ processes = Sidekiq.redis { |conn|
96
+ conn.sscan_each("processes").to_a
97
+ }
98
+
99
+ queues = Sidekiq.redis { |conn|
100
+ conn.sscan_each("queues").to_a
101
+ }
102
+
103
+ pipe2_res = Sidekiq.redis { |conn|
88
104
  conn.pipelined do
89
- processes.each {|key| conn.hget(key, 'busy') }
90
- queues.each {|queue| conn.llen("queue:#{queue}") }
105
+ processes.each { |key| conn.hget(key, "busy") }
106
+ queues.each { |queue| conn.llen("queue:#{queue}") }
91
107
  end
92
- end
108
+ }
93
109
 
94
110
  s = processes.size
95
- workers_size = pipe2_res[0...s].map(&:to_i).inject(0, &:+)
96
- enqueued = pipe2_res[s..-1].map(&:to_i).inject(0, &:+)
111
+ workers_size = pipe2_res[0...s].sum(&:to_i)
112
+ enqueued = pipe2_res[s..-1].sum(&:to_i)
97
113
 
98
- default_queue_latency = if (entry = pipe1_res[6].first)
99
- job = Sidekiq.load_json(entry) rescue {}
100
- now = Time.now.to_f
101
- thence = job['enqueued_at'] || now
102
- now - thence
103
- else
104
- 0
105
- end
106
- @stats = {
107
- processed: pipe1_res[0].to_i,
108
- failed: pipe1_res[1].to_i,
109
- scheduled_size: pipe1_res[2],
110
- retry_size: pipe1_res[3],
111
- dead_size: pipe1_res[4],
112
- processes_size: pipe1_res[5],
113
-
114
- default_queue_latency: default_queue_latency,
115
- workers_size: workers_size,
116
- enqueued: enqueued
117
- }
114
+ @stats[:workers_size] = workers_size
115
+ @stats[:enqueued] = enqueued
116
+ @stats
117
+ end
118
+
119
+ def fetch_stats!
120
+ fetch_stats_fast!
121
+ fetch_stats_slow!
118
122
  end
119
123
 
120
124
  def reset(*stats)
121
- all = %w(failed processed)
125
+ all = %w[failed processed]
122
126
  stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
123
127
 
124
128
  mset_args = []
@@ -134,36 +138,31 @@ module Sidekiq
134
138
  private
135
139
 
136
140
  def stat(s)
137
- @stats[s]
141
+ fetch_stats_slow! if @stats[s].nil?
142
+ @stats[s] || raise(ArgumentError, "Unknown stat #{s}")
138
143
  end
139
144
 
140
145
  class Queues
141
- include RedisScanner
142
-
143
146
  def lengths
144
147
  Sidekiq.redis do |conn|
145
- queues = sscan(conn, 'queues')
148
+ queues = conn.sscan_each("queues").to_a
146
149
 
147
- lengths = conn.pipelined do
150
+ lengths = conn.pipelined {
148
151
  queues.each do |queue|
149
152
  conn.llen("queue:#{queue}")
150
153
  end
151
- end
152
-
153
- i = 0
154
- array_of_arrays = queues.inject({}) do |memo, queue|
155
- memo[queue] = lengths[i]
156
- i += 1
157
- memo
158
- end.sort_by { |_, size| size }
154
+ }
159
155
 
160
- Hash[array_of_arrays.reverse]
156
+ array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
157
+ array_of_arrays.to_h
161
158
  end
162
159
  end
163
160
  end
164
161
 
165
162
  class History
166
163
  def initialize(days_previous, start_date = nil)
164
+ # we only store five years of data in Redis
165
+ raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
167
166
  @days_previous = days_previous
168
167
  @start_date = start_date || Time.now.utc.to_date
169
168
  end
@@ -179,18 +178,12 @@ module Sidekiq
179
178
  private
180
179
 
181
180
  def date_stat_hash(stat)
182
- i = 0
183
181
  stat_hash = {}
184
- keys = []
185
- dates = []
186
-
187
- while i < @days_previous
188
- date = @start_date - i
189
- datestr = date.strftime("%Y-%m-%d")
190
- keys << "stat:#{stat}:#{datestr}"
191
- dates << datestr
192
- i += 1
193
- end
182
+ dates = @start_date.downto(@start_date - @days_previous + 1).map { |date|
183
+ date.strftime("%Y-%m-%d")
184
+ }
185
+
186
+ keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
194
187
 
195
188
  begin
196
189
  Sidekiq.redis do |conn|
@@ -222,18 +215,17 @@ module Sidekiq
222
215
  #
223
216
  class Queue
224
217
  include Enumerable
225
- extend RedisScanner
226
218
 
227
219
  ##
228
220
  # Return all known queues within Redis.
229
221
  #
230
222
  def self.all
231
- Sidekiq.redis { |c| sscan(c, 'queues') }.sort.map { |q| Sidekiq::Queue.new(q) }
223
+ Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
232
224
  end
233
225
 
234
226
  attr_reader :name
235
227
 
236
- def initialize(name="default")
228
+ def initialize(name = "default")
237
229
  @name = name.to_s
238
230
  @rname = "queue:#{name}"
239
231
  end
@@ -253,13 +245,13 @@ module Sidekiq
253
245
  #
254
246
  # @return Float
255
247
  def latency
256
- entry = Sidekiq.redis do |conn|
248
+ entry = Sidekiq.redis { |conn|
257
249
  conn.lrange(@rname, -1, -1)
258
- end.first
250
+ }.first
259
251
  return 0 unless entry
260
252
  job = Sidekiq.load_json(entry)
261
253
  now = Time.now.to_f
262
- thence = job['enqueued_at'] || now
254
+ thence = job["enqueued_at"] || now
263
255
  now - thence
264
256
  end
265
257
 
@@ -269,16 +261,16 @@ module Sidekiq
269
261
  page = 0
270
262
  page_size = 50
271
263
 
272
- while true do
264
+ loop do
273
265
  range_start = page * page_size - deleted_size
274
- range_end = range_start + page_size - 1
275
- entries = Sidekiq.redis do |conn|
266
+ range_end = range_start + page_size - 1
267
+ entries = Sidekiq.redis { |conn|
276
268
  conn.lrange @rname, range_start, range_end
277
- end
269
+ }
278
270
  break if entries.empty?
279
271
  page += 1
280
272
  entries.each do |entry|
281
- yield Job.new(entry, @name)
273
+ yield JobRecord.new(entry, @name)
282
274
  end
283
275
  deleted_size = initial_size - size
284
276
  end
@@ -288,7 +280,7 @@ module Sidekiq
288
280
  # Find the job with the given JID within this queue.
289
281
  #
290
282
  # This is a slow, inefficient operation. Do not use under
291
- # normal conditions. Sidekiq Pro contains a faster version.
283
+ # normal conditions.
292
284
  def find_job(jid)
293
285
  detect { |j| j.jid == jid }
294
286
  end
@@ -296,7 +288,7 @@ module Sidekiq
296
288
  def clear
297
289
  Sidekiq.redis do |conn|
298
290
  conn.multi do
299
- conn.del(@rname)
291
+ conn.unlink(@rname)
300
292
  conn.srem("queues", name)
301
293
  end
302
294
  end
@@ -309,17 +301,17 @@ module Sidekiq
309
301
  # sorted set.
310
302
  #
311
303
  # The job should be considered immutable but may be
312
- # removed from the queue via Job#delete.
304
+ # removed from the queue via JobRecord#delete.
313
305
  #
314
- class Job
306
+ class JobRecord
315
307
  attr_reader :item
316
308
  attr_reader :value
317
309
 
318
- def initialize(item, queue_name=nil)
310
+ def initialize(item, queue_name = nil)
319
311
  @args = nil
320
312
  @value = item
321
313
  @item = item.is_a?(Hash) ? item : parse(item)
322
- @queue = queue_name || @item['queue']
314
+ @queue = queue_name || @item["queue"]
323
315
  end
324
316
 
325
317
  def parse(item)
@@ -334,27 +326,29 @@ module Sidekiq
334
326
  end
335
327
 
336
328
  def klass
337
- self['class']
329
+ self["class"]
338
330
  end
339
331
 
340
332
  def display_class
341
333
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
342
- @klass ||= case klass
343
- when /\ASidekiq::Extensions::Delayed/
344
- safe_load(args[0], klass) do |target, method, _|
345
- "#{target}.#{method}"
346
- end
347
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
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
355
- else
356
- klass
357
- end
334
+ @klass ||= self["display_class"] || begin
335
+ case klass
336
+ when /\ASidekiq::Extensions::Delayed/
337
+ safe_load(args[0], klass) do |target, method, _|
338
+ "#{target}.#{method}"
339
+ end
340
+ when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
341
+ job_class = @item["wrapped"] || args[0]
342
+ if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
343
+ # MailerClass#mailer_method
344
+ args[0]["arguments"][0..1].join("#")
345
+ else
346
+ job_class
347
+ end
348
+ else
349
+ klass
350
+ end
351
+ end
358
352
  end
359
353
 
360
354
  def display_args
@@ -365,53 +359,68 @@ module Sidekiq
365
359
  arg
366
360
  end
367
361
  when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
368
- job_args = self['wrapped'] ? args[0]["arguments"] : []
369
- if 'ActionMailer::DeliveryJob' == (self['wrapped'] || args[0])
362
+ job_args = self["wrapped"] ? args[0]["arguments"] : []
363
+ if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
370
364
  # remove MailerClass, mailer_method and 'deliver_now'
371
365
  job_args.drop(3)
366
+ elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
367
+ # remove MailerClass, mailer_method and 'deliver_now'
368
+ job_args.drop(3).first["args"]
372
369
  else
373
370
  job_args
374
371
  end
375
372
  else
376
- if self['encrypt']
373
+ if self["encrypt"]
377
374
  # no point in showing 150+ bytes of random garbage
378
- args[-1] = '[encrypted data]'
375
+ args[-1] = "[encrypted data]"
379
376
  end
380
377
  args
381
- end
378
+ end
382
379
  end
383
380
 
384
381
  def args
385
- @args || @item['args']
382
+ @args || @item["args"]
386
383
  end
387
384
 
388
385
  def jid
389
- self['jid']
386
+ self["jid"]
390
387
  end
391
388
 
392
389
  def enqueued_at
393
- self['enqueued_at'] ? Time.at(self['enqueued_at']).utc : nil
390
+ self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
394
391
  end
395
392
 
396
393
  def created_at
397
- Time.at(self['created_at'] || self['enqueued_at'] || 0).utc
394
+ Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
398
395
  end
399
396
 
400
- def queue
401
- @queue
397
+ def tags
398
+ self["tags"] || []
402
399
  end
403
400
 
401
+ def error_backtrace
402
+ # Cache nil values
403
+ if defined?(@error_backtrace)
404
+ @error_backtrace
405
+ else
406
+ value = self["error_backtrace"]
407
+ @error_backtrace = value && uncompress_backtrace(value)
408
+ end
409
+ end
410
+
411
+ attr_reader :queue
412
+
404
413
  def latency
405
414
  now = Time.now.to_f
406
- now - (@item['enqueued_at'] || @item['created_at'] || now)
415
+ now - (@item["enqueued_at"] || @item["created_at"] || now)
407
416
  end
408
417
 
409
418
  ##
410
419
  # Remove this job from the queue.
411
420
  def delete
412
- count = Sidekiq.redis do |conn|
421
+ count = Sidekiq.redis { |conn|
413
422
  conn.lrem("queue:#{@queue}", 1, @value)
414
- end
423
+ }
415
424
  count != 0
416
425
  end
417
426
 
@@ -425,18 +434,33 @@ module Sidekiq
425
434
  private
426
435
 
427
436
  def safe_load(content, default)
428
- begin
429
- yield(*YAML.load(content))
430
- rescue => ex
431
- # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
432
- # memory yet so the YAML can't be loaded.
433
- Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == 'development'
434
- default
437
+ yield(*YAML.load(content))
438
+ rescue => ex
439
+ # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
440
+ # memory yet so the YAML can't be loaded.
441
+ Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
442
+ default
443
+ end
444
+
445
+ def uncompress_backtrace(backtrace)
446
+ if backtrace.is_a?(Array)
447
+ # Handle old jobs with raw Array backtrace format
448
+ backtrace
449
+ else
450
+ decoded = Base64.decode64(backtrace)
451
+ uncompressed = Zlib::Inflate.inflate(decoded)
452
+ begin
453
+ Sidekiq.load_json(uncompressed)
454
+ rescue
455
+ # Handle old jobs with marshalled backtrace format
456
+ # TODO Remove in 7.x
457
+ Marshal.load(uncompressed)
458
+ end
435
459
  end
436
460
  end
437
461
  end
438
462
 
439
- class SortedEntry < Job
463
+ class SortedEntry < JobRecord
440
464
  attr_reader :score
441
465
  attr_reader :parent
442
466
 
@@ -459,8 +483,9 @@ module Sidekiq
459
483
  end
460
484
 
461
485
  def reschedule(at)
462
- delete
463
- @parent.schedule(at, item)
486
+ Sidekiq.redis do |conn|
487
+ conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
488
+ end
464
489
  end
465
490
 
466
491
  def add_to_queue
@@ -473,7 +498,7 @@ module Sidekiq
473
498
  def retry
474
499
  remove_job do |message|
475
500
  msg = Sidekiq.load_json(message)
476
- msg['retry_count'] -= 1 if msg['retry_count']
501
+ msg["retry_count"] -= 1 if msg["retry_count"]
477
502
  Sidekiq::Client.push(msg)
478
503
  end
479
504
  end
@@ -487,45 +512,44 @@ module Sidekiq
487
512
  end
488
513
 
489
514
  def error?
490
- !!item['error_class']
515
+ !!item["error_class"]
491
516
  end
492
517
 
493
518
  private
494
519
 
495
520
  def remove_job
496
521
  Sidekiq.redis do |conn|
497
- results = conn.multi do
522
+ results = conn.multi {
498
523
  conn.zrangebyscore(parent.name, score, score)
499
524
  conn.zremrangebyscore(parent.name, score, score)
500
- end.first
525
+ }.first
501
526
 
502
527
  if results.size == 1
503
528
  yield results.first
504
529
  else
505
530
  # multiple jobs with the same score
506
531
  # find the one with the right JID and push it
507
- hash = results.group_by do |message|
532
+ matched, nonmatched = results.partition { |message|
508
533
  if message.index(jid)
509
534
  msg = Sidekiq.load_json(message)
510
- msg['jid'] == jid
535
+ msg["jid"] == jid
511
536
  else
512
537
  false
513
538
  end
514
- end
539
+ }
515
540
 
516
- msg = hash.fetch(true, []).first
541
+ msg = matched.first
517
542
  yield msg if msg
518
543
 
519
544
  # push the rest back onto the sorted set
520
545
  conn.multi do
521
- hash.fetch(false, []).each do |message|
546
+ nonmatched.each do |message|
522
547
  conn.zadd(parent.name, score.to_f.to_s, message)
523
548
  end
524
549
  end
525
550
  end
526
551
  end
527
552
  end
528
-
529
553
  end
530
554
 
531
555
  class SortedSet
@@ -542,16 +566,26 @@ module Sidekiq
542
566
  Sidekiq.redis { |c| c.zcard(name) }
543
567
  end
544
568
 
569
+ def scan(match, count = 100)
570
+ return to_enum(:scan, match, count) unless block_given?
571
+
572
+ match = "*#{match}*" unless match.include?("*")
573
+ Sidekiq.redis do |conn|
574
+ conn.zscan_each(name, match: match, count: count) do |entry, score|
575
+ yield SortedEntry.new(self, score, entry)
576
+ end
577
+ end
578
+ end
579
+
545
580
  def clear
546
581
  Sidekiq.redis do |conn|
547
- conn.del(name)
582
+ conn.unlink(name)
548
583
  end
549
584
  end
550
585
  alias_method :💣, :clear
551
586
  end
552
587
 
553
588
  class JobSet < SortedSet
554
-
555
589
  def schedule(timestamp, message)
556
590
  Sidekiq.redis do |conn|
557
591
  conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
@@ -564,44 +598,55 @@ module Sidekiq
564
598
  page = -1
565
599
  page_size = 50
566
600
 
567
- while true do
601
+ loop do
568
602
  range_start = page * page_size + offset_size
569
- range_end = range_start + page_size - 1
570
- elements = Sidekiq.redis do |conn|
603
+ range_end = range_start + page_size - 1
604
+ elements = Sidekiq.redis { |conn|
571
605
  conn.zrange name, range_start, range_end, with_scores: true
572
- end
606
+ }
573
607
  break if elements.empty?
574
608
  page -= 1
575
- elements.reverse.each do |element, score|
609
+ elements.reverse_each do |element, score|
576
610
  yield SortedEntry.new(self, score, element)
577
611
  end
578
612
  offset_size = initial_size - @_size
579
613
  end
580
614
  end
581
615
 
616
+ ##
617
+ # Fetch jobs that match a given time or Range. Job ID is an
618
+ # optional second argument.
582
619
  def fetch(score, jid = nil)
583
- elements = Sidekiq.redis do |conn|
584
- conn.zrangebyscore(name, score, score)
585
- end
586
-
587
- elements.inject([]) do |result, element|
588
- entry = SortedEntry.new(self, score, element)
589
- if jid
590
- result << entry if entry.jid == jid
620
+ begin_score, end_score =
621
+ if score.is_a?(Range)
622
+ [score.first, score.last]
591
623
  else
592
- result << entry
624
+ [score, score]
593
625
  end
594
- result
626
+
627
+ elements = Sidekiq.redis { |conn|
628
+ conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
629
+ }
630
+
631
+ elements.each_with_object([]) do |element, result|
632
+ data, job_score = element
633
+ entry = SortedEntry.new(self, job_score, data)
634
+ result << entry if jid.nil? || entry.jid == jid
595
635
  end
596
636
  end
597
637
 
598
638
  ##
599
639
  # 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.
640
+ # This is a slower O(n) operation. Do not use for app logic.
603
641
  def find_job(jid)
604
- self.detect { |j| j.jid == jid }
642
+ Sidekiq.redis do |conn|
643
+ conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
644
+ job = JSON.parse(entry)
645
+ matched = job["jid"] == jid
646
+ return SortedEntry.new(self, score, entry) if matched
647
+ end
648
+ end
649
+ nil
605
650
  end
606
651
 
607
652
  def delete_by_value(name, value)
@@ -616,13 +661,14 @@ module Sidekiq
616
661
  Sidekiq.redis do |conn|
617
662
  elements = conn.zrangebyscore(name, score, score)
618
663
  elements.each do |element|
619
- message = Sidekiq.load_json(element)
620
- if message["jid"] == jid
621
- ret = conn.zrem(name, element)
622
- @_size -= 1 if ret
623
- break ret
664
+ if element.index(jid)
665
+ message = Sidekiq.load_json(element)
666
+ if message["jid"] == jid
667
+ ret = conn.zrem(name, element)
668
+ @_size -= 1 if ret
669
+ break ret
670
+ end
624
671
  end
625
- false
626
672
  end
627
673
  end
628
674
  end
@@ -644,7 +690,7 @@ module Sidekiq
644
690
  # end.map(&:delete)
645
691
  class ScheduledSet < JobSet
646
692
  def initialize
647
- super 'schedule'
693
+ super "schedule"
648
694
  end
649
695
  end
650
696
 
@@ -662,13 +708,15 @@ module Sidekiq
662
708
  # end.map(&:delete)
663
709
  class RetrySet < JobSet
664
710
  def initialize
665
- super 'retry'
711
+ super "retry"
666
712
  end
667
713
 
668
714
  def retry_all
669
- while size > 0
670
- each(&:retry)
671
- end
715
+ each(&:retry) while size > 0
716
+ end
717
+
718
+ def kill_all
719
+ each(&:kill) while size > 0
672
720
  end
673
721
  end
674
722
 
@@ -677,15 +725,15 @@ module Sidekiq
677
725
  #
678
726
  class DeadSet < JobSet
679
727
  def initialize
680
- super 'dead'
728
+ super "dead"
681
729
  end
682
730
 
683
- def kill(message, opts={})
731
+ def kill(message, opts = {})
684
732
  now = Time.now.to_f
685
733
  Sidekiq.redis do |conn|
686
734
  conn.multi do
687
735
  conn.zadd(name, now.to_s, message)
688
- conn.zremrangebyscore(name, '-inf', now - self.class.timeout)
736
+ conn.zremrangebyscore(name, "-inf", now - self.class.timeout)
689
737
  conn.zremrangebyrank(name, 0, - self.class.max_jobs)
690
738
  end
691
739
  end
@@ -702,9 +750,7 @@ module Sidekiq
702
750
  end
703
751
 
704
752
  def retry_all
705
- while size > 0
706
- each(&:retry)
707
- end
753
+ each(&:retry) while size > 0
708
754
  end
709
755
 
710
756
  def self.max_jobs
@@ -718,16 +764,15 @@ module Sidekiq
718
764
 
719
765
  ##
720
766
  # Enumerates the set of Sidekiq processes which are actively working
721
- # right now. Each process send a heartbeat to Redis every 5 seconds
767
+ # right now. Each process sends a heartbeat to Redis every 5 seconds
722
768
  # so this set should be relatively accurate, barring network partitions.
723
769
  #
724
770
  # Yields a Sidekiq::Process.
725
771
  #
726
772
  class ProcessSet
727
773
  include Enumerable
728
- include RedisScanner
729
774
 
730
- def initialize(clean_plz=true)
775
+ def initialize(clean_plz = true)
731
776
  cleanup if clean_plz
732
777
  end
733
778
 
@@ -736,50 +781,51 @@ module Sidekiq
736
781
  def cleanup
737
782
  count = 0
738
783
  Sidekiq.redis do |conn|
739
- procs = sscan(conn, 'processes').sort
740
- heartbeats = conn.pipelined do
784
+ procs = conn.sscan_each("processes").to_a.sort
785
+ heartbeats = conn.pipelined {
741
786
  procs.each do |key|
742
- conn.hget(key, 'info')
787
+ conn.hget(key, "info")
743
788
  end
744
- end
789
+ }
745
790
 
746
791
  # the hash named key has an expiry of 60 seconds.
747
792
  # if it's not found, that means the process has not reported
748
793
  # in to Redis and probably died.
749
- to_prune = []
750
- heartbeats.each_with_index do |beat, i|
751
- to_prune << procs[i] if beat.nil?
752
- end
753
- count = conn.srem('processes', to_prune) unless to_prune.empty?
794
+ to_prune = procs.select.with_index { |proc, i|
795
+ heartbeats[i].nil?
796
+ }
797
+ count = conn.srem("processes", to_prune) unless to_prune.empty?
754
798
  end
755
799
  count
756
800
  end
757
801
 
758
802
  def each
759
- procs = Sidekiq.redis { |conn| sscan(conn, 'processes') }.sort
803
+ result = Sidekiq.redis { |conn|
804
+ procs = conn.sscan_each("processes").to_a.sort
760
805
 
761
- Sidekiq.redis do |conn|
762
806
  # We're making a tradeoff here between consuming more memory instead of
763
807
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
764
808
  # you'll be happier this way
765
- result = conn.pipelined do
809
+ conn.pipelined do
766
810
  procs.each do |key|
767
- conn.hmget(key, 'info', 'busy', 'beat', 'quiet')
811
+ conn.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
768
812
  end
769
813
  end
814
+ }
770
815
 
771
- result.each do |info, busy, at_s, quiet|
772
- # If a process is stopped between when we query Redis for `procs` and
773
- # when we query for `result`, we will have an item in `result` that is
774
- # composed of `nil` values.
775
- next if info.nil?
816
+ result.each do |info, busy, at_s, quiet, rss, rtt|
817
+ # If a process is stopped between when we query Redis for `procs` and
818
+ # when we query for `result`, we will have an item in `result` that is
819
+ # composed of `nil` values.
820
+ next if info.nil?
776
821
 
777
- hash = Sidekiq.load_json(info)
778
- yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f, 'quiet' => quiet))
779
- end
822
+ hash = Sidekiq.load_json(info)
823
+ yield Process.new(hash.merge("busy" => busy.to_i,
824
+ "beat" => at_s.to_f,
825
+ "quiet" => quiet,
826
+ "rss" => rss.to_i,
827
+ "rtt_us" => rtt.to_i))
780
828
  end
781
-
782
- nil
783
829
  end
784
830
 
785
831
  # This method is not guaranteed accurate since it does not prune the set
@@ -787,17 +833,29 @@ module Sidekiq
787
833
  # contains Sidekiq processes which have sent a heartbeat within the last
788
834
  # 60 seconds.
789
835
  def size
790
- Sidekiq.redis { |conn| conn.scard('processes') }
836
+ Sidekiq.redis { |conn| conn.scard("processes") }
791
837
  end
792
838
 
839
+ # Total number of threads available to execute jobs.
840
+ # For Sidekiq Enterprise customers this number (in production) must be
841
+ # less than or equal to your licensed concurrency.
842
+ def total_concurrency
843
+ sum { |x| x["concurrency"].to_i }
844
+ end
845
+
846
+ def total_rss_in_kb
847
+ sum { |x| x["rss"].to_i }
848
+ end
849
+ alias_method :total_rss, :total_rss_in_kb
850
+
793
851
  # Returns the identity of the current cluster leader or "" if no leader.
794
852
  # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
795
853
  # or Sidekiq Pro.
796
854
  def leader
797
855
  @leader ||= begin
798
- x = Sidekiq.redis {|c| c.get("dear-leader") }
856
+ x = Sidekiq.redis { |c| c.get("dear-leader") }
799
857
  # need a non-falsy value so we can memoize
800
- x = "" unless x
858
+ x ||= ""
801
859
  x
802
860
  end
803
861
  end
@@ -824,11 +882,11 @@ module Sidekiq
824
882
  end
825
883
 
826
884
  def tag
827
- self['tag']
885
+ self["tag"]
828
886
  end
829
887
 
830
888
  def labels
831
- Array(self['labels'])
889
+ Array(self["labels"])
832
890
  end
833
891
 
834
892
  def [](key)
@@ -836,23 +894,27 @@ module Sidekiq
836
894
  end
837
895
 
838
896
  def identity
839
- self['identity']
897
+ self["identity"]
898
+ end
899
+
900
+ def queues
901
+ self["queues"]
840
902
  end
841
903
 
842
904
  def quiet!
843
- signal('TSTP')
905
+ signal("TSTP")
844
906
  end
845
907
 
846
908
  def stop!
847
- signal('TERM')
909
+ signal("TERM")
848
910
  end
849
911
 
850
912
  def dump_threads
851
- signal('TTIN')
913
+ signal("TTIN")
852
914
  end
853
915
 
854
916
  def stopping?
855
- self['quiet'] == 'true'
917
+ self["quiet"] == "true"
856
918
  end
857
919
 
858
920
  private
@@ -866,12 +928,11 @@ module Sidekiq
866
928
  end
867
929
  end
868
930
  end
869
-
870
931
  end
871
932
 
872
933
  ##
873
- # A worker is a thread that is currently processing a job.
874
- # Programmatic access to the current active worker set.
934
+ # The WorkSet stores the work being done by this Sidekiq cluster.
935
+ # It tracks the process and thread working on each job.
875
936
  #
876
937
  # WARNING WARNING WARNING
877
938
  #
@@ -879,34 +940,40 @@ module Sidekiq
879
940
  # If you call #size => 5 and then expect #each to be
880
941
  # called 5 times, you're going to have a bad time.
881
942
  #
882
- # workers = Sidekiq::Workers.new
883
- # workers.size => 2
884
- # workers.each do |process_id, thread_id, work|
943
+ # works = Sidekiq::WorkSet.new
944
+ # works.size => 2
945
+ # works.each do |process_id, thread_id, work|
885
946
  # # process_id is a unique identifier per Sidekiq process
886
947
  # # thread_id is a unique identifier per thread
887
948
  # # work is a Hash which looks like:
888
- # # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
949
+ # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
889
950
  # # run_at is an epoch Integer.
890
951
  # end
891
952
  #
892
- class Workers
953
+ class WorkSet
893
954
  include Enumerable
894
- include RedisScanner
895
955
 
896
- def each
956
+ def each(&block)
957
+ results = []
897
958
  Sidekiq.redis do |conn|
898
- procs = sscan(conn, 'processes')
959
+ procs = conn.sscan_each("processes").to_a
899
960
  procs.sort.each do |key|
900
- valid, workers = conn.pipelined do
901
- conn.exists(key)
961
+ valid, workers = conn.pipelined {
962
+ conn.exists?(key)
902
963
  conn.hgetall("#{key}:workers")
903
- end
964
+ }
904
965
  next unless valid
905
966
  workers.each_pair do |tid, json|
906
- yield key, tid, Sidekiq.load_json(json)
967
+ hsh = Sidekiq.load_json(json)
968
+ p = hsh["payload"]
969
+ # avoid breaking API, this is a side effect of the JSON optimization in #4316
970
+ hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
971
+ results << [key, tid, hsh]
907
972
  end
908
973
  end
909
974
  end
975
+
976
+ results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
910
977
  end
911
978
 
912
979
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -917,18 +984,21 @@ module Sidekiq
917
984
  # which can easily get out of sync with crashy processes.
918
985
  def size
919
986
  Sidekiq.redis do |conn|
920
- procs = sscan(conn, 'processes')
987
+ procs = conn.sscan_each("processes").to_a
921
988
  if procs.empty?
922
989
  0
923
990
  else
924
- conn.pipelined do
991
+ conn.pipelined {
925
992
  procs.each do |key|
926
- conn.hget(key, 'busy')
993
+ conn.hget(key, "busy")
927
994
  end
928
- end.map(&:to_i).inject(:+)
995
+ }.sum(&:to_i)
929
996
  end
930
997
  end
931
998
  end
932
999
  end
933
-
1000
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1001
+ # Is "worker" a process, a type of job, a thread? Undefined!
1002
+ # WorkSet better describes the data.
1003
+ Workers = WorkSet
934
1004
  end