sidekiq 5.2.8 → 6.2.2

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