sidekiq 5.2.9 → 6.4.1

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 +4 -4
  2. data/Changes.md +318 -1
  3. data/LICENSE +3 -3
  4. data/README.md +23 -34
  5. data/bin/sidekiq +27 -3
  6. data/bin/sidekiqload +67 -61
  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 +335 -267
  13. data/lib/sidekiq/cli.rb +164 -182
  14. data/lib/sidekiq/client.rb +58 -61
  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 +13 -22
  18. data/lib/sidekiq/extensions/active_record.rb +13 -10
  19. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  20. data/lib/sidekiq/extensions/generic_proxy.rb +6 -4
  21. data/lib/sidekiq/fetch.rb +40 -32
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +33 -7
  24. data/lib/sidekiq/job_retry.rb +70 -71
  25. data/lib/sidekiq/job_util.rb +65 -0
  26. data/lib/sidekiq/launcher.rb +161 -71
  27. data/lib/sidekiq/logger.rb +170 -0
  28. data/lib/sidekiq/manager.rb +17 -21
  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 +20 -16
  34. data/lib/sidekiq/processor.rb +71 -70
  35. data/lib/sidekiq/rails.rb +40 -37
  36. data/lib/sidekiq/redis_connection.rb +48 -48
  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 +88 -75
  46. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  47. data/lib/sidekiq/web/helpers.rb +109 -92
  48. data/lib/sidekiq/web/router.rb +23 -19
  49. data/lib/sidekiq/web.rb +61 -105
  50. data/lib/sidekiq/worker.rb +247 -105
  51. data/lib/sidekiq.rb +77 -44
  52. data/sidekiq.gemspec +23 -16
  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 +54 -73
  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 +45 -232
  59. data/web/locales/ar.yml +8 -2
  60. data/web/locales/de.yml +14 -2
  61. data/web/locales/en.yml +6 -1
  62. data/web/locales/es.yml +18 -2
  63. data/web/locales/fr.yml +10 -3
  64. data/web/locales/ja.yml +7 -1
  65. data/web/locales/lt.yml +83 -0
  66. data/web/locales/pl.yml +4 -4
  67. data/web/locales/ru.yml +4 -0
  68. data/web/locales/vi.yml +83 -0
  69. data/web/views/_footer.erb +1 -1
  70. data/web/views/_job_info.erb +3 -2
  71. data/web/views/_poll_link.erb +2 -5
  72. data/web/views/_summary.erb +7 -7
  73. data/web/views/busy.erb +54 -20
  74. data/web/views/dashboard.erb +22 -14
  75. data/web/views/dead.erb +3 -3
  76. data/web/views/layout.erb +3 -1
  77. data/web/views/morgue.erb +9 -6
  78. data/web/views/queue.erb +19 -10
  79. data/web/views/queues.erb +10 -2
  80. data/web/views/retries.erb +11 -8
  81. data/web/views/retry.erb +3 -3
  82. data/web/views/scheduled.erb +5 -2
  83. metadata +34 -64
  84. data/.circleci/config.yml +0 -61
  85. data/.github/contributing.md +0 -32
  86. data/.github/issue_template.md +0 -11
  87. data/.gitignore +0 -15
  88. data/.travis.yml +0 -11
  89. data/3.0-Upgrade.md +0 -70
  90. data/4.0-Upgrade.md +0 -53
  91. data/5.0-Upgrade.md +0 -56
  92. data/COMM-LICENSE +0 -97
  93. data/Ent-Changes.md +0 -238
  94. data/Gemfile +0 -23
  95. data/Pro-2.0-Upgrade.md +0 -138
  96. data/Pro-3.0-Upgrade.md +0 -44
  97. data/Pro-4.0-Upgrade.md +0 -35
  98. data/Pro-Changes.md +0 -759
  99. data/Rakefile +0 -9
  100. data/bin/sidekiqctl +0 -20
  101. data/code_of_conduct.md +0 -50
  102. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  103. data/lib/sidekiq/core_ext.rb +0 -1
  104. data/lib/sidekiq/ctl.rb +0 -221
  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|
68
- 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)
54
+ # O(1) redis calls
55
+ def fetch_stats_fast!
56
+ pipe1_res = Sidekiq.redis { |conn|
57
+ conn.pipelined do |pipeline|
58
+ pipeline.get("stat:processed")
59
+ pipeline.get("stat:failed")
60
+ pipeline.zcard("schedule")
61
+ pipeline.zcard("retry")
62
+ pipeline.zcard("dead")
63
+ pipeline.scard("processes")
64
+ pipeline.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|
88
- conn.pipelined do
89
- processes.each {|key| conn.hget(key, 'busy') }
90
- queues.each {|queue| conn.llen("queue:#{queue}") }
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|
104
+ conn.pipelined do |pipeline|
105
+ processes.each { |key| pipeline.hget(key, "busy") }
106
+ queues.each { |queue| pipeline.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 { |pipeline|
148
151
  queues.each do |queue|
149
- conn.llen("queue:#{queue}")
152
+ pipeline.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,16 +280,16 @@ 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
295
287
 
296
288
  def clear
297
289
  Sidekiq.redis do |conn|
298
- conn.multi do
299
- conn.del(@rname)
300
- conn.srem("queues", name)
290
+ conn.multi do |transaction|
291
+ transaction.unlink(@rname)
292
+ transaction.srem("queues", name)
301
293
  end
302
294
  end
303
295
  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,84 +326,105 @@ 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
361
355
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
362
356
  @display_args ||= case klass
363
357
  when /\ASidekiq::Extensions::Delayed/
364
- safe_load(args[0], args) do |_, _, arg|
365
- arg
358
+ safe_load(args[0], args) do |_, _, arg, kwarg|
359
+ if !kwarg || kwarg.empty?
360
+ arg
361
+ else
362
+ [arg, kwarg]
363
+ end
366
364
  end
367
365
  when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
368
- job_args = self['wrapped'] ? args[0]["arguments"] : []
369
- if 'ActionMailer::DeliveryJob' == (self['wrapped'] || args[0])
366
+ job_args = self["wrapped"] ? args[0]["arguments"] : []
367
+ if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
370
368
  # remove MailerClass, mailer_method and 'deliver_now'
371
369
  job_args.drop(3)
370
+ elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
371
+ # remove MailerClass, mailer_method and 'deliver_now'
372
+ job_args.drop(3).first["args"]
372
373
  else
373
374
  job_args
374
375
  end
375
376
  else
376
- if self['encrypt']
377
+ if self["encrypt"]
377
378
  # no point in showing 150+ bytes of random garbage
378
- args[-1] = '[encrypted data]'
379
+ args[-1] = "[encrypted data]"
379
380
  end
380
381
  args
381
- end
382
+ end
382
383
  end
383
384
 
384
385
  def args
385
- @args || @item['args']
386
+ @args || @item["args"]
386
387
  end
387
388
 
388
389
  def jid
389
- self['jid']
390
+ self["jid"]
390
391
  end
391
392
 
392
393
  def enqueued_at
393
- self['enqueued_at'] ? Time.at(self['enqueued_at']).utc : nil
394
+ self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
394
395
  end
395
396
 
396
397
  def created_at
397
- Time.at(self['created_at'] || self['enqueued_at'] || 0).utc
398
+ Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
399
+ end
400
+
401
+ def tags
402
+ self["tags"] || []
398
403
  end
399
404
 
400
- def queue
401
- @queue
405
+ def error_backtrace
406
+ # Cache nil values
407
+ if defined?(@error_backtrace)
408
+ @error_backtrace
409
+ else
410
+ value = self["error_backtrace"]
411
+ @error_backtrace = value && uncompress_backtrace(value)
412
+ end
402
413
  end
403
414
 
415
+ attr_reader :queue
416
+
404
417
  def latency
405
418
  now = Time.now.to_f
406
- now - (@item['enqueued_at'] || @item['created_at'] || now)
419
+ now - (@item["enqueued_at"] || @item["created_at"] || now)
407
420
  end
408
421
 
409
422
  ##
410
423
  # Remove this job from the queue.
411
424
  def delete
412
- count = Sidekiq.redis do |conn|
425
+ count = Sidekiq.redis { |conn|
413
426
  conn.lrem("queue:#{@queue}", 1, @value)
414
- end
427
+ }
415
428
  count != 0
416
429
  end
417
430
 
@@ -425,18 +438,33 @@ module Sidekiq
425
438
  private
426
439
 
427
440
  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
441
+ yield(*YAML.load(content))
442
+ rescue => ex
443
+ # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
444
+ # memory yet so the YAML can't be loaded.
445
+ Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
446
+ default
447
+ end
448
+
449
+ def uncompress_backtrace(backtrace)
450
+ if backtrace.is_a?(Array)
451
+ # Handle old jobs with raw Array backtrace format
452
+ backtrace
453
+ else
454
+ decoded = Base64.decode64(backtrace)
455
+ uncompressed = Zlib::Inflate.inflate(decoded)
456
+ begin
457
+ Sidekiq.load_json(uncompressed)
458
+ rescue
459
+ # Handle old jobs with marshalled backtrace format
460
+ # TODO Remove in 7.x
461
+ Marshal.load(uncompressed)
462
+ end
435
463
  end
436
464
  end
437
465
  end
438
466
 
439
- class SortedEntry < Job
467
+ class SortedEntry < JobRecord
440
468
  attr_reader :score
441
469
  attr_reader :parent
442
470
 
@@ -459,8 +487,9 @@ module Sidekiq
459
487
  end
460
488
 
461
489
  def reschedule(at)
462
- delete
463
- @parent.schedule(at, item)
490
+ Sidekiq.redis do |conn|
491
+ conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
492
+ end
464
493
  end
465
494
 
466
495
  def add_to_queue
@@ -473,7 +502,7 @@ module Sidekiq
473
502
  def retry
474
503
  remove_job do |message|
475
504
  msg = Sidekiq.load_json(message)
476
- msg['retry_count'] -= 1 if msg['retry_count']
505
+ msg["retry_count"] -= 1 if msg["retry_count"]
477
506
  Sidekiq::Client.push(msg)
478
507
  end
479
508
  end
@@ -487,45 +516,44 @@ module Sidekiq
487
516
  end
488
517
 
489
518
  def error?
490
- !!item['error_class']
519
+ !!item["error_class"]
491
520
  end
492
521
 
493
522
  private
494
523
 
495
524
  def remove_job
496
525
  Sidekiq.redis do |conn|
497
- results = conn.multi do
498
- conn.zrangebyscore(parent.name, score, score)
499
- conn.zremrangebyscore(parent.name, score, score)
500
- end.first
526
+ results = conn.multi { |transaction|
527
+ transaction.zrangebyscore(parent.name, score, score)
528
+ transaction.zremrangebyscore(parent.name, score, score)
529
+ }.first
501
530
 
502
531
  if results.size == 1
503
532
  yield results.first
504
533
  else
505
534
  # multiple jobs with the same score
506
535
  # find the one with the right JID and push it
507
- hash = results.group_by do |message|
536
+ matched, nonmatched = results.partition { |message|
508
537
  if message.index(jid)
509
538
  msg = Sidekiq.load_json(message)
510
- msg['jid'] == jid
539
+ msg["jid"] == jid
511
540
  else
512
541
  false
513
542
  end
514
- end
543
+ }
515
544
 
516
- msg = hash.fetch(true, []).first
545
+ msg = matched.first
517
546
  yield msg if msg
518
547
 
519
548
  # push the rest back onto the sorted set
520
- conn.multi do
521
- hash.fetch(false, []).each do |message|
522
- conn.zadd(parent.name, score.to_f.to_s, message)
549
+ conn.multi do |transaction|
550
+ nonmatched.each do |message|
551
+ transaction.zadd(parent.name, score.to_f.to_s, message)
523
552
  end
524
553
  end
525
554
  end
526
555
  end
527
556
  end
528
-
529
557
  end
530
558
 
531
559
  class SortedSet
@@ -542,16 +570,26 @@ module Sidekiq
542
570
  Sidekiq.redis { |c| c.zcard(name) }
543
571
  end
544
572
 
573
+ def scan(match, count = 100)
574
+ return to_enum(:scan, match, count) unless block_given?
575
+
576
+ match = "*#{match}*" unless match.include?("*")
577
+ Sidekiq.redis do |conn|
578
+ conn.zscan_each(name, match: match, count: count) do |entry, score|
579
+ yield SortedEntry.new(self, score, entry)
580
+ end
581
+ end
582
+ end
583
+
545
584
  def clear
546
585
  Sidekiq.redis do |conn|
547
- conn.del(name)
586
+ conn.unlink(name)
548
587
  end
549
588
  end
550
589
  alias_method :💣, :clear
551
590
  end
552
591
 
553
592
  class JobSet < SortedSet
554
-
555
593
  def schedule(timestamp, message)
556
594
  Sidekiq.redis do |conn|
557
595
  conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
@@ -564,44 +602,55 @@ module Sidekiq
564
602
  page = -1
565
603
  page_size = 50
566
604
 
567
- while true do
605
+ loop do
568
606
  range_start = page * page_size + offset_size
569
- range_end = range_start + page_size - 1
570
- elements = Sidekiq.redis do |conn|
607
+ range_end = range_start + page_size - 1
608
+ elements = Sidekiq.redis { |conn|
571
609
  conn.zrange name, range_start, range_end, with_scores: true
572
- end
610
+ }
573
611
  break if elements.empty?
574
612
  page -= 1
575
- elements.reverse.each do |element, score|
613
+ elements.reverse_each do |element, score|
576
614
  yield SortedEntry.new(self, score, element)
577
615
  end
578
616
  offset_size = initial_size - @_size
579
617
  end
580
618
  end
581
619
 
620
+ ##
621
+ # Fetch jobs that match a given time or Range. Job ID is an
622
+ # optional second argument.
582
623
  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
624
+ begin_score, end_score =
625
+ if score.is_a?(Range)
626
+ [score.first, score.last]
591
627
  else
592
- result << entry
628
+ [score, score]
593
629
  end
594
- result
630
+
631
+ elements = Sidekiq.redis { |conn|
632
+ conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
633
+ }
634
+
635
+ elements.each_with_object([]) do |element, result|
636
+ data, job_score = element
637
+ entry = SortedEntry.new(self, job_score, data)
638
+ result << entry if jid.nil? || entry.jid == jid
595
639
  end
596
640
  end
597
641
 
598
642
  ##
599
643
  # 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.
644
+ # This is a slower O(n) operation. Do not use for app logic.
603
645
  def find_job(jid)
604
- self.detect { |j| j.jid == jid }
646
+ Sidekiq.redis do |conn|
647
+ conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
648
+ job = JSON.parse(entry)
649
+ matched = job["jid"] == jid
650
+ return SortedEntry.new(self, score, entry) if matched
651
+ end
652
+ end
653
+ nil
605
654
  end
606
655
 
607
656
  def delete_by_value(name, value)
@@ -616,13 +665,14 @@ module Sidekiq
616
665
  Sidekiq.redis do |conn|
617
666
  elements = conn.zrangebyscore(name, score, score)
618
667
  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
668
+ if element.index(jid)
669
+ message = Sidekiq.load_json(element)
670
+ if message["jid"] == jid
671
+ ret = conn.zrem(name, element)
672
+ @_size -= 1 if ret
673
+ break ret
674
+ end
624
675
  end
625
- false
626
676
  end
627
677
  end
628
678
  end
@@ -644,7 +694,7 @@ module Sidekiq
644
694
  # end.map(&:delete)
645
695
  class ScheduledSet < JobSet
646
696
  def initialize
647
- super 'schedule'
697
+ super "schedule"
648
698
  end
649
699
  end
650
700
 
@@ -662,19 +712,15 @@ module Sidekiq
662
712
  # end.map(&:delete)
663
713
  class RetrySet < JobSet
664
714
  def initialize
665
- super 'retry'
715
+ super "retry"
666
716
  end
667
717
 
668
718
  def retry_all
669
- while size > 0
670
- each(&:retry)
671
- end
719
+ each(&:retry) while size > 0
672
720
  end
673
721
 
674
722
  def kill_all
675
- while size > 0
676
- each(&:kill)
677
- end
723
+ each(&:kill) while size > 0
678
724
  end
679
725
  end
680
726
 
@@ -683,16 +729,16 @@ module Sidekiq
683
729
  #
684
730
  class DeadSet < JobSet
685
731
  def initialize
686
- super 'dead'
732
+ super "dead"
687
733
  end
688
734
 
689
- def kill(message, opts={})
735
+ def kill(message, opts = {})
690
736
  now = Time.now.to_f
691
737
  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)
738
+ conn.multi do |transaction|
739
+ transaction.zadd(name, now.to_s, message)
740
+ transaction.zremrangebyscore(name, "-inf", now - self.class.timeout)
741
+ transaction.zremrangebyrank(name, 0, - self.class.max_jobs)
696
742
  end
697
743
  end
698
744
 
@@ -708,9 +754,7 @@ module Sidekiq
708
754
  end
709
755
 
710
756
  def retry_all
711
- while size > 0
712
- each(&:retry)
713
- end
757
+ each(&:retry) while size > 0
714
758
  end
715
759
 
716
760
  def self.max_jobs
@@ -724,16 +768,15 @@ module Sidekiq
724
768
 
725
769
  ##
726
770
  # Enumerates the set of Sidekiq processes which are actively working
727
- # right now. Each process send a heartbeat to Redis every 5 seconds
771
+ # right now. Each process sends a heartbeat to Redis every 5 seconds
728
772
  # so this set should be relatively accurate, barring network partitions.
729
773
  #
730
774
  # Yields a Sidekiq::Process.
731
775
  #
732
776
  class ProcessSet
733
777
  include Enumerable
734
- include RedisScanner
735
778
 
736
- def initialize(clean_plz=true)
779
+ def initialize(clean_plz = true)
737
780
  cleanup if clean_plz
738
781
  end
739
782
 
@@ -742,50 +785,51 @@ module Sidekiq
742
785
  def cleanup
743
786
  count = 0
744
787
  Sidekiq.redis do |conn|
745
- procs = sscan(conn, 'processes').sort
746
- heartbeats = conn.pipelined do
788
+ procs = conn.sscan_each("processes").to_a.sort
789
+ heartbeats = conn.pipelined { |pipeline|
747
790
  procs.each do |key|
748
- conn.hget(key, 'info')
791
+ pipeline.hget(key, "info")
749
792
  end
750
- end
793
+ }
751
794
 
752
795
  # the hash named key has an expiry of 60 seconds.
753
796
  # if it's not found, that means the process has not reported
754
797
  # in to Redis and probably died.
755
- to_prune = []
756
- heartbeats.each_with_index do |beat, i|
757
- to_prune << procs[i] if beat.nil?
758
- end
759
- count = conn.srem('processes', to_prune) unless to_prune.empty?
798
+ to_prune = procs.select.with_index { |proc, i|
799
+ heartbeats[i].nil?
800
+ }
801
+ count = conn.srem("processes", to_prune) unless to_prune.empty?
760
802
  end
761
803
  count
762
804
  end
763
805
 
764
806
  def each
765
- procs = Sidekiq.redis { |conn| sscan(conn, 'processes') }.sort
807
+ result = Sidekiq.redis { |conn|
808
+ procs = conn.sscan_each("processes").to_a.sort
766
809
 
767
- Sidekiq.redis do |conn|
768
810
  # We're making a tradeoff here between consuming more memory instead of
769
811
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
770
812
  # you'll be happier this way
771
- result = conn.pipelined do
813
+ conn.pipelined do |pipeline|
772
814
  procs.each do |key|
773
- conn.hmget(key, 'info', 'busy', 'beat', 'quiet')
815
+ pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
774
816
  end
775
817
  end
818
+ }
776
819
 
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?
820
+ result.each do |info, busy, at_s, quiet, rss, rtt|
821
+ # If a process is stopped between when we query Redis for `procs` and
822
+ # when we query for `result`, we will have an item in `result` that is
823
+ # composed of `nil` values.
824
+ next if info.nil?
782
825
 
783
- hash = Sidekiq.load_json(info)
784
- yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f, 'quiet' => quiet))
785
- end
826
+ hash = Sidekiq.load_json(info)
827
+ yield Process.new(hash.merge("busy" => busy.to_i,
828
+ "beat" => at_s.to_f,
829
+ "quiet" => quiet,
830
+ "rss" => rss.to_i,
831
+ "rtt_us" => rtt.to_i))
786
832
  end
787
-
788
- nil
789
833
  end
790
834
 
791
835
  # This method is not guaranteed accurate since it does not prune the set
@@ -793,17 +837,29 @@ module Sidekiq
793
837
  # contains Sidekiq processes which have sent a heartbeat within the last
794
838
  # 60 seconds.
795
839
  def size
796
- Sidekiq.redis { |conn| conn.scard('processes') }
840
+ Sidekiq.redis { |conn| conn.scard("processes") }
797
841
  end
798
842
 
843
+ # Total number of threads available to execute jobs.
844
+ # For Sidekiq Enterprise customers this number (in production) must be
845
+ # less than or equal to your licensed concurrency.
846
+ def total_concurrency
847
+ sum { |x| x["concurrency"].to_i }
848
+ end
849
+
850
+ def total_rss_in_kb
851
+ sum { |x| x["rss"].to_i }
852
+ end
853
+ alias_method :total_rss, :total_rss_in_kb
854
+
799
855
  # Returns the identity of the current cluster leader or "" if no leader.
800
856
  # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
801
857
  # or Sidekiq Pro.
802
858
  def leader
803
859
  @leader ||= begin
804
- x = Sidekiq.redis {|c| c.get("dear-leader") }
860
+ x = Sidekiq.redis { |c| c.get("dear-leader") }
805
861
  # need a non-falsy value so we can memoize
806
- x = "" unless x
862
+ x ||= ""
807
863
  x
808
864
  end
809
865
  end
@@ -830,11 +886,11 @@ module Sidekiq
830
886
  end
831
887
 
832
888
  def tag
833
- self['tag']
889
+ self["tag"]
834
890
  end
835
891
 
836
892
  def labels
837
- Array(self['labels'])
893
+ Array(self["labels"])
838
894
  end
839
895
 
840
896
  def [](key)
@@ -842,23 +898,27 @@ module Sidekiq
842
898
  end
843
899
 
844
900
  def identity
845
- self['identity']
901
+ self["identity"]
902
+ end
903
+
904
+ def queues
905
+ self["queues"]
846
906
  end
847
907
 
848
908
  def quiet!
849
- signal('TSTP')
909
+ signal("TSTP")
850
910
  end
851
911
 
852
912
  def stop!
853
- signal('TERM')
913
+ signal("TERM")
854
914
  end
855
915
 
856
916
  def dump_threads
857
- signal('TTIN')
917
+ signal("TTIN")
858
918
  end
859
919
 
860
920
  def stopping?
861
- self['quiet'] == 'true'
921
+ self["quiet"] == "true"
862
922
  end
863
923
 
864
924
  private
@@ -866,18 +926,17 @@ module Sidekiq
866
926
  def signal(sig)
867
927
  key = "#{identity}-signals"
868
928
  Sidekiq.redis do |c|
869
- c.multi do
870
- c.lpush(key, sig)
871
- c.expire(key, 60)
929
+ c.multi do |transaction|
930
+ transaction.lpush(key, sig)
931
+ transaction.expire(key, 60)
872
932
  end
873
933
  end
874
934
  end
875
-
876
935
  end
877
936
 
878
937
  ##
879
- # A worker is a thread that is currently processing a job.
880
- # Programmatic access to the current active worker set.
938
+ # The WorkSet stores the work being done by this Sidekiq cluster.
939
+ # It tracks the process and thread working on each job.
881
940
  #
882
941
  # WARNING WARNING WARNING
883
942
  #
@@ -885,34 +944,40 @@ module Sidekiq
885
944
  # If you call #size => 5 and then expect #each to be
886
945
  # called 5 times, you're going to have a bad time.
887
946
  #
888
- # workers = Sidekiq::Workers.new
889
- # workers.size => 2
890
- # workers.each do |process_id, thread_id, work|
947
+ # works = Sidekiq::WorkSet.new
948
+ # works.size => 2
949
+ # works.each do |process_id, thread_id, work|
891
950
  # # process_id is a unique identifier per Sidekiq process
892
951
  # # thread_id is a unique identifier per thread
893
952
  # # work is a Hash which looks like:
894
- # # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
953
+ # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
895
954
  # # run_at is an epoch Integer.
896
955
  # end
897
956
  #
898
- class Workers
957
+ class WorkSet
899
958
  include Enumerable
900
- include RedisScanner
901
959
 
902
- def each
960
+ def each(&block)
961
+ results = []
903
962
  Sidekiq.redis do |conn|
904
- procs = sscan(conn, 'processes')
963
+ procs = conn.sscan_each("processes").to_a
905
964
  procs.sort.each do |key|
906
- valid, workers = conn.pipelined do
907
- conn.exists(key)
908
- conn.hgetall("#{key}:workers")
909
- end
965
+ valid, workers = conn.pipelined { |pipeline|
966
+ pipeline.exists?(key)
967
+ pipeline.hgetall("#{key}:workers")
968
+ }
910
969
  next unless valid
911
970
  workers.each_pair do |tid, json|
912
- yield key, tid, Sidekiq.load_json(json)
971
+ hsh = Sidekiq.load_json(json)
972
+ p = hsh["payload"]
973
+ # avoid breaking API, this is a side effect of the JSON optimization in #4316
974
+ hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
975
+ results << [key, tid, hsh]
913
976
  end
914
977
  end
915
978
  end
979
+
980
+ results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
916
981
  end
917
982
 
918
983
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -923,18 +988,21 @@ module Sidekiq
923
988
  # which can easily get out of sync with crashy processes.
924
989
  def size
925
990
  Sidekiq.redis do |conn|
926
- procs = sscan(conn, 'processes')
991
+ procs = conn.sscan_each("processes").to_a
927
992
  if procs.empty?
928
993
  0
929
994
  else
930
- conn.pipelined do
995
+ conn.pipelined { |pipeline|
931
996
  procs.each do |key|
932
- conn.hget(key, 'busy')
997
+ pipeline.hget(key, "busy")
933
998
  end
934
- end.map(&:to_i).inject(:+)
999
+ }.sum(&:to_i)
935
1000
  end
936
1001
  end
937
1002
  end
938
1003
  end
939
-
1004
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1005
+ # Is "worker" a process, a type of job, a thread? Undefined!
1006
+ # WorkSet better describes the data.
1007
+ Workers = WorkSet
940
1008
  end