sidekiq 6.2.2 → 7.1.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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +279 -11
  3. data/LICENSE.txt +9 -0
  4. data/README.md +45 -32
  5. data/bin/sidekiq +4 -9
  6. data/bin/sidekiqload +207 -117
  7. data/bin/sidekiqmon +4 -1
  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 +331 -187
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +94 -81
  15. data/lib/sidekiq/client.rb +100 -96
  16. data/lib/sidekiq/{util.rb → component.rb} +14 -41
  17. data/lib/sidekiq/config.rb +274 -0
  18. data/lib/sidekiq/deploy.rb +62 -0
  19. data/lib/sidekiq/embedded.rb +61 -0
  20. data/lib/sidekiq/fetch.rb +26 -26
  21. data/lib/sidekiq/job.rb +371 -5
  22. data/lib/sidekiq/job_logger.rb +16 -28
  23. data/lib/sidekiq/job_retry.rb +85 -59
  24. data/lib/sidekiq/job_util.rb +105 -0
  25. data/lib/sidekiq/launcher.rb +106 -94
  26. data/lib/sidekiq/logger.rb +9 -44
  27. data/lib/sidekiq/manager.rb +40 -41
  28. data/lib/sidekiq/metrics/query.rb +153 -0
  29. data/lib/sidekiq/metrics/shared.rb +95 -0
  30. data/lib/sidekiq/metrics/tracking.rb +136 -0
  31. data/lib/sidekiq/middleware/chain.rb +96 -51
  32. data/lib/sidekiq/middleware/current_attributes.rb +56 -0
  33. data/lib/sidekiq/middleware/i18n.rb +6 -4
  34. data/lib/sidekiq/middleware/modules.rb +21 -0
  35. data/lib/sidekiq/monitor.rb +17 -4
  36. data/lib/sidekiq/paginator.rb +17 -9
  37. data/lib/sidekiq/processor.rb +60 -60
  38. data/lib/sidekiq/rails.rb +25 -6
  39. data/lib/sidekiq/redis_client_adapter.rb +96 -0
  40. data/lib/sidekiq/redis_connection.rb +17 -88
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +101 -44
  43. data/lib/sidekiq/testing/inline.rb +4 -4
  44. data/lib/sidekiq/testing.rb +41 -68
  45. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  46. data/lib/sidekiq/version.rb +2 -1
  47. data/lib/sidekiq/web/action.rb +3 -3
  48. data/lib/sidekiq/web/application.rb +47 -13
  49. data/lib/sidekiq/web/csrf_protection.rb +3 -3
  50. data/lib/sidekiq/web/helpers.rb +36 -33
  51. data/lib/sidekiq/web.rb +10 -17
  52. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  53. data/lib/sidekiq.rb +86 -201
  54. data/sidekiq.gemspec +12 -10
  55. data/web/assets/javascripts/application.js +131 -60
  56. data/web/assets/javascripts/base-charts.js +106 -0
  57. data/web/assets/javascripts/chart.min.js +13 -0
  58. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  59. data/web/assets/javascripts/dashboard-charts.js +166 -0
  60. data/web/assets/javascripts/dashboard.js +36 -273
  61. data/web/assets/javascripts/metrics.js +264 -0
  62. data/web/assets/stylesheets/application-dark.css +23 -23
  63. data/web/assets/stylesheets/application-rtl.css +2 -95
  64. data/web/assets/stylesheets/application.css +73 -402
  65. data/web/locales/ar.yml +70 -70
  66. data/web/locales/cs.yml +62 -62
  67. data/web/locales/da.yml +60 -53
  68. data/web/locales/de.yml +65 -65
  69. data/web/locales/el.yml +43 -24
  70. data/web/locales/en.yml +82 -69
  71. data/web/locales/es.yml +68 -68
  72. data/web/locales/fa.yml +65 -65
  73. data/web/locales/fr.yml +81 -67
  74. data/web/locales/gd.yml +99 -0
  75. data/web/locales/he.yml +65 -64
  76. data/web/locales/hi.yml +59 -59
  77. data/web/locales/it.yml +53 -53
  78. data/web/locales/ja.yml +73 -68
  79. data/web/locales/ko.yml +52 -52
  80. data/web/locales/lt.yml +66 -66
  81. data/web/locales/nb.yml +61 -61
  82. data/web/locales/nl.yml +52 -52
  83. data/web/locales/pl.yml +45 -45
  84. data/web/locales/pt-br.yml +63 -55
  85. data/web/locales/pt.yml +51 -51
  86. data/web/locales/ru.yml +67 -66
  87. data/web/locales/sv.yml +53 -53
  88. data/web/locales/ta.yml +60 -60
  89. data/web/locales/uk.yml +62 -61
  90. data/web/locales/ur.yml +64 -64
  91. data/web/locales/vi.yml +67 -67
  92. data/web/locales/zh-cn.yml +43 -16
  93. data/web/locales/zh-tw.yml +42 -8
  94. data/web/views/_footer.erb +6 -3
  95. data/web/views/_job_info.erb +18 -2
  96. data/web/views/_metrics_period_select.erb +12 -0
  97. data/web/views/_nav.erb +1 -1
  98. data/web/views/_paging.erb +2 -0
  99. data/web/views/_poll_link.erb +3 -6
  100. data/web/views/_summary.erb +7 -7
  101. data/web/views/busy.erb +44 -28
  102. data/web/views/dashboard.erb +44 -12
  103. data/web/views/layout.erb +1 -1
  104. data/web/views/metrics.erb +82 -0
  105. data/web/views/metrics_for_job.erb +68 -0
  106. data/web/views/morgue.erb +5 -9
  107. data/web/views/queue.erb +24 -24
  108. data/web/views/queues.erb +4 -2
  109. data/web/views/retries.erb +5 -9
  110. data/web/views/scheduled.erb +12 -13
  111. metadata +62 -31
  112. data/LICENSE +0 -9
  113. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  114. data/lib/sidekiq/delay.rb +0 -41
  115. data/lib/sidekiq/exception_handler.rb +0 -27
  116. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  117. data/lib/sidekiq/extensions/active_record.rb +0 -43
  118. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  119. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  120. data/lib/sidekiq/worker.rb +0 -244
data/lib/sidekiq/api.rb CHANGED
@@ -3,9 +3,28 @@
3
3
  require "sidekiq"
4
4
 
5
5
  require "zlib"
6
+ require "set"
6
7
  require "base64"
7
8
 
9
+ require "sidekiq/metrics/query"
10
+
11
+ #
12
+ # Sidekiq's Data API provides a Ruby object model on top
13
+ # of Sidekiq's runtime data in Redis. This API should never
14
+ # be used within application code for business logic.
15
+ #
16
+ # The Sidekiq server process never uses this API: all data
17
+ # manipulation is done directly for performance reasons to
18
+ # ensure we are using Redis as efficiently as possible at
19
+ # every callsite.
20
+ #
21
+
8
22
  module Sidekiq
23
+ # Retrieve runtime statistics from Redis regarding
24
+ # this Sidekiq cluster.
25
+ #
26
+ # stat = Sidekiq::Stats.new
27
+ # stat.processed
9
28
  class Stats
10
29
  def initialize
11
30
  fetch_stats_fast!
@@ -48,20 +67,32 @@ module Sidekiq
48
67
  end
49
68
 
50
69
  def queues
51
- Sidekiq::Stats::Queues.new.lengths
70
+ Sidekiq.redis do |conn|
71
+ queues = conn.sscan("queues").to_a
72
+
73
+ lengths = conn.pipelined { |pipeline|
74
+ queues.each do |queue|
75
+ pipeline.llen("queue:#{queue}")
76
+ end
77
+ }
78
+
79
+ array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
80
+ array_of_arrays.to_h
81
+ end
52
82
  end
53
83
 
54
84
  # O(1) redis calls
85
+ # @api private
55
86
  def fetch_stats_fast!
56
87
  pipe1_res = Sidekiq.redis { |conn|
57
- conn.pipelined do
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)
88
+ conn.pipelined do |pipeline|
89
+ pipeline.get("stat:processed")
90
+ pipeline.get("stat:failed")
91
+ pipeline.zcard("schedule")
92
+ pipeline.zcard("retry")
93
+ pipeline.zcard("dead")
94
+ pipeline.scard("processes")
95
+ pipeline.lrange("queue:default", -1, -1)
65
96
  end
66
97
  }
67
98
 
@@ -91,35 +122,39 @@ module Sidekiq
91
122
  end
92
123
 
93
124
  # O(number of processes + number of queues) redis calls
125
+ # @api private
94
126
  def fetch_stats_slow!
95
127
  processes = Sidekiq.redis { |conn|
96
- conn.sscan_each("processes").to_a
128
+ conn.sscan("processes").to_a
97
129
  }
98
130
 
99
131
  queues = Sidekiq.redis { |conn|
100
- conn.sscan_each("queues").to_a
132
+ conn.sscan("queues").to_a
101
133
  }
102
134
 
103
135
  pipe2_res = Sidekiq.redis { |conn|
104
- conn.pipelined do
105
- processes.each { |key| conn.hget(key, "busy") }
106
- queues.each { |queue| conn.llen("queue:#{queue}") }
136
+ conn.pipelined do |pipeline|
137
+ processes.each { |key| pipeline.hget(key, "busy") }
138
+ queues.each { |queue| pipeline.llen("queue:#{queue}") }
107
139
  end
108
140
  }
109
141
 
110
142
  s = processes.size
111
143
  workers_size = pipe2_res[0...s].sum(&:to_i)
112
- enqueued = pipe2_res[s..-1].sum(&:to_i)
144
+ enqueued = pipe2_res[s..].sum(&:to_i)
113
145
 
114
146
  @stats[:workers_size] = workers_size
115
147
  @stats[:enqueued] = enqueued
148
+ @stats
116
149
  end
117
150
 
151
+ # @api private
118
152
  def fetch_stats!
119
153
  fetch_stats_fast!
120
154
  fetch_stats_slow!
121
155
  end
122
156
 
157
+ # @api private
123
158
  def reset(*stats)
124
159
  all = %w[failed processed]
125
160
  stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
@@ -141,25 +176,10 @@ module Sidekiq
141
176
  @stats[s] || raise(ArgumentError, "Unknown stat #{s}")
142
177
  end
143
178
 
144
- class Queues
145
- def lengths
146
- Sidekiq.redis do |conn|
147
- queues = conn.sscan_each("queues").to_a
148
-
149
- lengths = conn.pipelined {
150
- queues.each do |queue|
151
- conn.llen("queue:#{queue}")
152
- end
153
- }
154
-
155
- array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
156
- array_of_arrays.to_h
157
- end
158
- end
159
- end
160
-
161
179
  class History
162
- def initialize(days_previous, start_date = nil)
180
+ def initialize(days_previous, start_date = nil, pool: nil)
181
+ # we only store five years of data in Redis
182
+ raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
163
183
  @days_previous = days_previous
164
184
  @start_date = start_date || Time.now.utc.to_date
165
185
  end
@@ -182,15 +202,10 @@ module Sidekiq
182
202
 
183
203
  keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
184
204
 
185
- begin
186
- Sidekiq.redis do |conn|
187
- conn.mget(keys).each_with_index do |value, idx|
188
- stat_hash[dates[idx]] = value ? value.to_i : 0
189
- end
205
+ Sidekiq.redis do |conn|
206
+ conn.mget(keys).each_with_index do |value, idx|
207
+ stat_hash[dates[idx]] = value ? value.to_i : 0
190
208
  end
191
- rescue Redis::CommandError
192
- # mget will trigger a CROSSSLOT error when run against a Cluster
193
- # TODO Someone want to add Cluster support?
194
209
  end
195
210
 
196
211
  stat_hash
@@ -199,9 +214,10 @@ module Sidekiq
199
214
  end
200
215
 
201
216
  ##
202
- # Encapsulates a queue within Sidekiq.
217
+ # Represents a queue within Sidekiq.
203
218
  # Allows enumeration of all jobs within the queue
204
- # and deletion of jobs.
219
+ # and deletion of jobs. NB: this queue data is real-time
220
+ # and is changing within Redis moment by moment.
205
221
  #
206
222
  # queue = Sidekiq::Queue.new("mailer")
207
223
  # queue.each do |job|
@@ -209,29 +225,34 @@ module Sidekiq
209
225
  # job.args # => [1, 2, 3]
210
226
  # job.delete if job.jid == 'abcdef1234567890'
211
227
  # end
212
- #
213
228
  class Queue
214
229
  include Enumerable
215
230
 
216
231
  ##
217
- # Return all known queues within Redis.
232
+ # Fetch all known queues within Redis.
218
233
  #
234
+ # @return [Array<Sidekiq::Queue>]
219
235
  def self.all
220
- Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
236
+ Sidekiq.redis { |c| c.sscan("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
221
237
  end
222
238
 
223
239
  attr_reader :name
224
240
 
241
+ # @param name [String] the name of the queue
225
242
  def initialize(name = "default")
226
243
  @name = name.to_s
227
244
  @rname = "queue:#{name}"
228
245
  end
229
246
 
247
+ # The current size of the queue within Redis.
248
+ # This value is real-time and can change between calls.
249
+ #
250
+ # @return [Integer] the size
230
251
  def size
231
252
  Sidekiq.redis { |con| con.llen(@rname) }
232
253
  end
233
254
 
234
- # Sidekiq Pro overrides this
255
+ # @return [Boolean] if the queue is currently paused
235
256
  def paused?
236
257
  false
237
258
  end
@@ -240,7 +261,7 @@ module Sidekiq
240
261
  # Calculates this queue's latency, the difference in seconds since the oldest
241
262
  # job in the queue was enqueued.
242
263
  #
243
- # @return Float
264
+ # @return [Float] in seconds
244
265
  def latency
245
266
  entry = Sidekiq.redis { |conn|
246
267
  conn.lrange(@rname, -1, -1)
@@ -276,34 +297,54 @@ module Sidekiq
276
297
  ##
277
298
  # Find the job with the given JID within this queue.
278
299
  #
279
- # This is a slow, inefficient operation. Do not use under
300
+ # This is a *slow, inefficient* operation. Do not use under
280
301
  # normal conditions.
302
+ #
303
+ # @param jid [String] the job_id to look for
304
+ # @return [Sidekiq::JobRecord]
305
+ # @return [nil] if not found
281
306
  def find_job(jid)
282
307
  detect { |j| j.jid == jid }
283
308
  end
284
309
 
310
+ # delete all jobs within this queue
311
+ # @return [Boolean] true
285
312
  def clear
286
313
  Sidekiq.redis do |conn|
287
- conn.multi do
288
- conn.unlink(@rname)
289
- conn.srem("queues", name)
314
+ conn.multi do |transaction|
315
+ transaction.unlink(@rname)
316
+ transaction.srem("queues", [name])
290
317
  end
291
318
  end
319
+ true
292
320
  end
293
321
  alias_method :💣, :clear
322
+
323
+ # :nodoc:
324
+ # @api private
325
+ def as_json(options = nil)
326
+ {name: name} # 5336
327
+ end
294
328
  end
295
329
 
296
330
  ##
297
- # Encapsulates a pending job within a Sidekiq queue or
298
- # sorted set.
331
+ # Represents a pending job within a Sidekiq queue.
299
332
  #
300
333
  # The job should be considered immutable but may be
301
334
  # removed from the queue via JobRecord#delete.
302
- #
303
335
  class JobRecord
336
+ # the parsed Hash of job data
337
+ # @!attribute [r] Item
304
338
  attr_reader :item
339
+ # the underlying String in Redis
340
+ # @!attribute [r] Value
305
341
  attr_reader :value
342
+ # the queue associated with this job
343
+ # @!attribute [r] Queue
344
+ attr_reader :queue
306
345
 
346
+ # :nodoc:
347
+ # @api private
307
348
  def initialize(item, queue_name = nil)
308
349
  @args = nil
309
350
  @value = item
@@ -311,6 +352,8 @@ module Sidekiq
311
352
  @queue = queue_name || @item["queue"]
312
353
  end
313
354
 
355
+ # :nodoc:
356
+ # @api private
314
357
  def parse(item)
315
358
  Sidekiq.load_json(item)
316
359
  rescue JSON::ParserError
@@ -322,6 +365,8 @@ module Sidekiq
322
365
  {}
323
366
  end
324
367
 
368
+ # This is the job class which Sidekiq will execute. If using ActiveJob,
369
+ # this class will be the ActiveJob adapter class rather than a specific job.
325
370
  def klass
326
371
  self["class"]
327
372
  end
@@ -329,12 +374,7 @@ module Sidekiq
329
374
  def display_class
330
375
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
331
376
  @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"
377
+ if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
338
378
  job_class = @item["wrapped"] || args[0]
339
379
  if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
340
380
  # MailerClass#mailer_method
@@ -350,28 +390,23 @@ module Sidekiq
350
390
 
351
391
  def display_args
352
392
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
353
- @display_args ||= case klass
354
- when /\ASidekiq::Extensions::Delayed/
355
- safe_load(args[0], args) do |_, _, arg|
356
- arg
357
- end
358
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
359
- job_args = self["wrapped"] ? args[0]["arguments"] : []
360
- if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
361
- # remove MailerClass, mailer_method and 'deliver_now'
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"]
366
- else
367
- job_args
368
- end
369
- else
370
- if self["encrypt"]
371
- # no point in showing 150+ bytes of random garbage
372
- args[-1] = "[encrypted data]"
373
- end
374
- args
393
+ @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
394
+ job_args = self["wrapped"] ? deserialize_argument(args[0]["arguments"]) : []
395
+ if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
396
+ # remove MailerClass, mailer_method and 'deliver_now'
397
+ job_args.drop(3)
398
+ elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
399
+ # remove MailerClass, mailer_method and 'deliver_now'
400
+ job_args.drop(3).first.values_at("params", "args")
401
+ else
402
+ job_args
403
+ end
404
+ else
405
+ if self["encrypt"]
406
+ # no point in showing 150+ bytes of random garbage
407
+ args[-1] = "[encrypted data]"
408
+ end
409
+ args
375
410
  end
376
411
  end
377
412
 
@@ -383,6 +418,10 @@ module Sidekiq
383
418
  self["jid"]
384
419
  end
385
420
 
421
+ def bid
422
+ self["bid"]
423
+ end
424
+
386
425
  def enqueued_at
387
426
  self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
388
427
  end
@@ -405,15 +444,12 @@ module Sidekiq
405
444
  end
406
445
  end
407
446
 
408
- attr_reader :queue
409
-
410
447
  def latency
411
448
  now = Time.now.to_f
412
449
  now - (@item["enqueued_at"] || @item["created_at"] || now)
413
450
  end
414
451
 
415
- ##
416
- # Remove this job from the queue.
452
+ # Remove this job from the queue
417
453
  def delete
418
454
  count = Sidekiq.redis { |conn|
419
455
  conn.lrem("queue:#{@queue}", 1, @value)
@@ -421,6 +457,7 @@ module Sidekiq
421
457
  count != 0
422
458
  end
423
459
 
460
+ # Access arbitrary attributes within the job hash
424
461
  def [](name)
425
462
  # nil will happen if the JSON fails to parse.
426
463
  # We don't guarantee Sidekiq will work with bad job JSON but we should
@@ -430,47 +467,58 @@ module Sidekiq
430
467
 
431
468
  private
432
469
 
433
- def safe_load(content, 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
470
+ ACTIVE_JOB_PREFIX = "_aj_"
471
+ GLOBALID_KEY = "_aj_globalid"
441
472
 
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)
473
+ def deserialize_argument(argument)
474
+ case argument
475
+ when Array
476
+ argument.map { |arg| deserialize_argument(arg) }
477
+ when Hash
478
+ if serialized_global_id?(argument)
479
+ argument[GLOBALID_KEY]
480
+ else
481
+ argument.transform_values { |v| deserialize_argument(v) }
482
+ .reject { |k, _| k.start_with?(ACTIVE_JOB_PREFIX) }
455
483
  end
484
+ else
485
+ argument
456
486
  end
457
487
  end
488
+
489
+ def serialized_global_id?(hash)
490
+ hash.size == 1 && hash.include?(GLOBALID_KEY)
491
+ end
492
+
493
+ def uncompress_backtrace(backtrace)
494
+ decoded = Base64.decode64(backtrace)
495
+ uncompressed = Zlib::Inflate.inflate(decoded)
496
+ Sidekiq.load_json(uncompressed)
497
+ end
458
498
  end
459
499
 
500
+ # Represents a job within a Redis sorted set where the score
501
+ # represents a timestamp associated with the job. This timestamp
502
+ # could be the scheduled time for it to run (e.g. scheduled set),
503
+ # or the expiration date after which the entry should be deleted (e.g. dead set).
460
504
  class SortedEntry < JobRecord
461
505
  attr_reader :score
462
506
  attr_reader :parent
463
507
 
508
+ # :nodoc:
509
+ # @api private
464
510
  def initialize(parent, score, item)
465
511
  super(item)
466
- @score = score
512
+ @score = Float(score)
467
513
  @parent = parent
468
514
  end
469
515
 
516
+ # The timestamp associated with this entry
470
517
  def at
471
518
  Time.at(score).utc
472
519
  end
473
520
 
521
+ # remove this entry from the sorted set
474
522
  def delete
475
523
  if @value
476
524
  @parent.delete_by_value(@parent.name, @value)
@@ -479,12 +527,17 @@ module Sidekiq
479
527
  end
480
528
  end
481
529
 
530
+ # Change the scheduled time for this job.
531
+ #
532
+ # @param at [Time] the new timestamp for this job
482
533
  def reschedule(at)
483
534
  Sidekiq.redis do |conn|
484
535
  conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
485
536
  end
486
537
  end
487
538
 
539
+ # Enqueue this job from the scheduled or dead set so it will
540
+ # be executed at some point in the near future.
488
541
  def add_to_queue
489
542
  remove_job do |message|
490
543
  msg = Sidekiq.load_json(message)
@@ -492,6 +545,8 @@ module Sidekiq
492
545
  end
493
546
  end
494
547
 
548
+ # enqueue this job from the retry set so it will be executed
549
+ # at some point in the near future.
495
550
  def retry
496
551
  remove_job do |message|
497
552
  msg = Sidekiq.load_json(message)
@@ -500,8 +555,7 @@ module Sidekiq
500
555
  end
501
556
  end
502
557
 
503
- ##
504
- # Place job in the dead set
558
+ # Move this job from its current set into the Dead set.
505
559
  def kill
506
560
  remove_job do |message|
507
561
  DeadSet.new.kill(message)
@@ -516,9 +570,9 @@ module Sidekiq
516
570
 
517
571
  def remove_job
518
572
  Sidekiq.redis do |conn|
519
- results = conn.multi {
520
- conn.zrangebyscore(parent.name, score, score)
521
- conn.zremrangebyscore(parent.name, score, score)
573
+ results = conn.multi { |transaction|
574
+ transaction.zrange(parent.name, score, score, "BYSCORE")
575
+ transaction.zremrangebyscore(parent.name, score, score)
522
576
  }.first
523
577
 
524
578
  if results.size == 1
@@ -539,9 +593,9 @@ module Sidekiq
539
593
  yield msg if msg
540
594
 
541
595
  # push the rest back onto the sorted set
542
- conn.multi do
596
+ conn.multi do |transaction|
543
597
  nonmatched.each do |message|
544
- conn.zadd(parent.name, score.to_f.to_s, message)
598
+ transaction.zadd(parent.name, score.to_f.to_s, message)
545
599
  end
546
600
  end
547
601
  end
@@ -549,43 +603,69 @@ module Sidekiq
549
603
  end
550
604
  end
551
605
 
606
+ # Base class for all sorted sets within Sidekiq.
552
607
  class SortedSet
553
608
  include Enumerable
554
609
 
610
+ # Redis key of the set
611
+ # @!attribute [r] Name
555
612
  attr_reader :name
556
613
 
614
+ # :nodoc:
615
+ # @api private
557
616
  def initialize(name)
558
617
  @name = name
559
618
  @_size = size
560
619
  end
561
620
 
621
+ # real-time size of the set, will change
562
622
  def size
563
623
  Sidekiq.redis { |c| c.zcard(name) }
564
624
  end
565
625
 
626
+ # Scan through each element of the sorted set, yielding each to the supplied block.
627
+ # Please see Redis's <a href="https://redis.io/commands/scan/">SCAN documentation</a> for implementation details.
628
+ #
629
+ # @param match [String] a snippet or regexp to filter matches.
630
+ # @param count [Integer] number of elements to retrieve at a time, default 100
631
+ # @yieldparam [Sidekiq::SortedEntry] each entry
566
632
  def scan(match, count = 100)
567
633
  return to_enum(:scan, match, count) unless block_given?
568
634
 
569
635
  match = "*#{match}*" unless match.include?("*")
570
636
  Sidekiq.redis do |conn|
571
- conn.zscan_each(name, match: match, count: count) do |entry, score|
637
+ conn.zscan(name, match: match, count: count) do |entry, score|
572
638
  yield SortedEntry.new(self, score, entry)
573
639
  end
574
640
  end
575
641
  end
576
642
 
643
+ # @return [Boolean] always true
577
644
  def clear
578
645
  Sidekiq.redis do |conn|
579
646
  conn.unlink(name)
580
647
  end
648
+ true
581
649
  end
582
650
  alias_method :💣, :clear
651
+
652
+ # :nodoc:
653
+ # @api private
654
+ def as_json(options = nil)
655
+ {name: name} # 5336
656
+ end
583
657
  end
584
658
 
659
+ # Base class for all sorted sets which contain jobs, e.g. scheduled, retry and dead.
660
+ # Sidekiq Pro and Enterprise add additional sorted sets which do not contain job data,
661
+ # e.g. Batches.
585
662
  class JobSet < SortedSet
586
- def schedule(timestamp, message)
663
+ # Add a job with the associated timestamp to this set.
664
+ # @param timestamp [Time] the score for the job
665
+ # @param job [Hash] the job data
666
+ def schedule(timestamp, job)
587
667
  Sidekiq.redis do |conn|
588
- conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
668
+ conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(job))
589
669
  end
590
670
  end
591
671
 
@@ -599,7 +679,7 @@ module Sidekiq
599
679
  range_start = page * page_size + offset_size
600
680
  range_end = range_start + page_size - 1
601
681
  elements = Sidekiq.redis { |conn|
602
- conn.zrange name, range_start, range_end, with_scores: true
682
+ conn.zrange name, range_start, range_end, withscores: true
603
683
  }
604
684
  break if elements.empty?
605
685
  page -= 1
@@ -613,6 +693,10 @@ module Sidekiq
613
693
  ##
614
694
  # Fetch jobs that match a given time or Range. Job ID is an
615
695
  # optional second argument.
696
+ #
697
+ # @param score [Time,Range] a specific timestamp or range
698
+ # @param jid [String, optional] find a specific JID within the score
699
+ # @return [Array<SortedEntry>] any results found, can be empty
616
700
  def fetch(score, jid = nil)
617
701
  begin_score, end_score =
618
702
  if score.is_a?(Range)
@@ -622,7 +706,7 @@ module Sidekiq
622
706
  end
623
707
 
624
708
  elements = Sidekiq.redis { |conn|
625
- conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
709
+ conn.zrange(name, begin_score, end_score, "BYSCORE", withscores: true)
626
710
  }
627
711
 
628
712
  elements.each_with_object([]) do |element, result|
@@ -634,11 +718,14 @@ module Sidekiq
634
718
 
635
719
  ##
636
720
  # Find the job with the given JID within this sorted set.
637
- # This is a slower O(n) operation. Do not use for app logic.
721
+ # *This is a slow O(n) operation*. Do not use for app logic.
722
+ #
723
+ # @param jid [String] the job identifier
724
+ # @return [SortedEntry] the record or nil
638
725
  def find_job(jid)
639
726
  Sidekiq.redis do |conn|
640
- conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
641
- job = JSON.parse(entry)
727
+ conn.zscan(name, match: "*#{jid}*", count: 100) do |entry, score|
728
+ job = Sidekiq.load_json(entry)
642
729
  matched = job["jid"] == jid
643
730
  return SortedEntry.new(self, score, entry) if matched
644
731
  end
@@ -646,6 +733,8 @@ module Sidekiq
646
733
  nil
647
734
  end
648
735
 
736
+ # :nodoc:
737
+ # @api private
649
738
  def delete_by_value(name, value)
650
739
  Sidekiq.redis do |conn|
651
740
  ret = conn.zrem(name, value)
@@ -654,9 +743,11 @@ module Sidekiq
654
743
  end
655
744
  end
656
745
 
746
+ # :nodoc:
747
+ # @api private
657
748
  def delete_by_jid(score, jid)
658
749
  Sidekiq.redis do |conn|
659
- elements = conn.zrangebyscore(name, score, score)
750
+ elements = conn.zrange(name, score, score, "BYSCORE")
660
751
  elements.each do |element|
661
752
  if element.index(jid)
662
753
  message = Sidekiq.load_json(element)
@@ -674,17 +765,13 @@ module Sidekiq
674
765
  end
675
766
 
676
767
  ##
677
- # Allows enumeration of scheduled jobs within Sidekiq.
768
+ # The set of scheduled jobs within Sidekiq.
678
769
  # Based on this, you can search/filter for jobs. Here's an
679
- # example where I'm selecting all jobs of a certain type
680
- # and deleting them from the schedule queue.
770
+ # example where I'm selecting jobs based on some complex logic
771
+ # and deleting them from the scheduled set.
772
+ #
773
+ # See the API wiki page for usage notes and examples.
681
774
  #
682
- # r = Sidekiq::ScheduledSet.new
683
- # r.select do |scheduled|
684
- # scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
685
- # scheduled.args[0] == 'User' &&
686
- # scheduled.args[1] == 'setup_new_subscriber'
687
- # end.map(&:delete)
688
775
  class ScheduledSet < JobSet
689
776
  def initialize
690
777
  super "schedule"
@@ -692,46 +779,48 @@ module Sidekiq
692
779
  end
693
780
 
694
781
  ##
695
- # Allows enumeration of retries within Sidekiq.
782
+ # The set of retries within Sidekiq.
696
783
  # Based on this, you can search/filter for jobs. Here's an
697
784
  # example where I'm selecting all jobs of a certain type
698
785
  # and deleting them from the retry queue.
699
786
  #
700
- # r = Sidekiq::RetrySet.new
701
- # r.select do |retri|
702
- # retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
703
- # retri.args[0] == 'User' &&
704
- # retri.args[1] == 'setup_new_subscriber'
705
- # end.map(&:delete)
787
+ # See the API wiki page for usage notes and examples.
788
+ #
706
789
  class RetrySet < JobSet
707
790
  def initialize
708
791
  super "retry"
709
792
  end
710
793
 
794
+ # Enqueues all jobs pending within the retry set.
711
795
  def retry_all
712
796
  each(&:retry) while size > 0
713
797
  end
714
798
 
799
+ # Kills all jobs pending within the retry set.
715
800
  def kill_all
716
801
  each(&:kill) while size > 0
717
802
  end
718
803
  end
719
804
 
720
805
  ##
721
- # Allows enumeration of dead jobs within Sidekiq.
806
+ # The set of dead jobs within Sidekiq. Dead jobs have failed all of
807
+ # their retries and are helding in this set pending some sort of manual
808
+ # fix. They will be removed after 6 months (dead_timeout) if not.
722
809
  #
723
810
  class DeadSet < JobSet
724
811
  def initialize
725
812
  super "dead"
726
813
  end
727
814
 
815
+ # Add the given job to the Dead set.
816
+ # @param message [String] the job data as JSON
728
817
  def kill(message, opts = {})
729
818
  now = Time.now.to_f
730
819
  Sidekiq.redis do |conn|
731
- conn.multi do
732
- conn.zadd(name, now.to_s, message)
733
- conn.zremrangebyscore(name, "-inf", now - self.class.timeout)
734
- conn.zremrangebyrank(name, 0, - self.class.max_jobs)
820
+ conn.multi do |transaction|
821
+ transaction.zadd(name, now.to_s, message)
822
+ transaction.zremrangebyscore(name, "-inf", now - Sidekiq::Config::DEFAULTS[:dead_timeout_in_seconds])
823
+ transaction.zremrangebyrank(name, 0, - Sidekiq::Config::DEFAULTS[:dead_max_jobs])
735
824
  end
736
825
  end
737
826
 
@@ -739,24 +828,17 @@ module Sidekiq
739
828
  job = Sidekiq.load_json(message)
740
829
  r = RuntimeError.new("Job killed by API")
741
830
  r.set_backtrace(caller)
742
- Sidekiq.death_handlers.each do |handle|
831
+ Sidekiq.default_configuration.death_handlers.each do |handle|
743
832
  handle.call(job, r)
744
833
  end
745
834
  end
746
835
  true
747
836
  end
748
837
 
838
+ # Enqueue all dead jobs
749
839
  def retry_all
750
840
  each(&:retry) while size > 0
751
841
  end
752
-
753
- def self.max_jobs
754
- Sidekiq.options[:dead_max_jobs]
755
- end
756
-
757
- def self.timeout
758
- Sidekiq.options[:dead_timeout_in_seconds]
759
- end
760
842
  end
761
843
 
762
844
  ##
@@ -764,24 +846,49 @@ module Sidekiq
764
846
  # right now. Each process sends a heartbeat to Redis every 5 seconds
765
847
  # so this set should be relatively accurate, barring network partitions.
766
848
  #
767
- # Yields a Sidekiq::Process.
849
+ # @yieldparam [Sidekiq::Process]
768
850
  #
769
851
  class ProcessSet
770
852
  include Enumerable
771
853
 
854
+ def self.[](identity)
855
+ exists, (info, busy, beat, quiet, rss, rtt_us) = Sidekiq.redis { |conn|
856
+ conn.multi { |transaction|
857
+ transaction.sismember("processes", identity)
858
+ transaction.hmget(identity, "info", "busy", "beat", "quiet", "rss", "rtt_us")
859
+ }
860
+ }
861
+
862
+ return nil if exists == 0 || info.nil?
863
+
864
+ hash = Sidekiq.load_json(info)
865
+ Process.new(hash.merge("busy" => busy.to_i,
866
+ "beat" => beat.to_f,
867
+ "quiet" => quiet,
868
+ "rss" => rss.to_i,
869
+ "rtt_us" => rtt_us.to_i))
870
+ end
871
+
872
+ # :nodoc:
873
+ # @api private
772
874
  def initialize(clean_plz = true)
773
875
  cleanup if clean_plz
774
876
  end
775
877
 
776
878
  # Cleans up dead processes recorded in Redis.
777
879
  # Returns the number of processes cleaned.
880
+ # :nodoc:
881
+ # @api private
778
882
  def cleanup
883
+ # dont run cleanup more than once per minute
884
+ return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
885
+
779
886
  count = 0
780
887
  Sidekiq.redis do |conn|
781
- procs = conn.sscan_each("processes").to_a.sort
782
- heartbeats = conn.pipelined {
888
+ procs = conn.sscan("processes").to_a
889
+ heartbeats = conn.pipelined { |pipeline|
783
890
  procs.each do |key|
784
- conn.hget(key, "info")
891
+ pipeline.hget(key, "info")
785
892
  end
786
893
  }
787
894
 
@@ -798,19 +905,19 @@ module Sidekiq
798
905
 
799
906
  def each
800
907
  result = Sidekiq.redis { |conn|
801
- procs = conn.sscan_each("processes").to_a.sort
908
+ procs = conn.sscan("processes").to_a.sort
802
909
 
803
910
  # We're making a tradeoff here between consuming more memory instead of
804
911
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
805
912
  # you'll be happier this way
806
- conn.pipelined do
913
+ conn.pipelined do |pipeline|
807
914
  procs.each do |key|
808
- conn.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
915
+ pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
809
916
  end
810
917
  end
811
918
  }
812
919
 
813
- result.each do |info, busy, at_s, quiet, rss, rtt|
920
+ result.each do |info, busy, beat, quiet, rss, rtt_us|
814
921
  # If a process is stopped between when we query Redis for `procs` and
815
922
  # when we query for `result`, we will have an item in `result` that is
816
923
  # composed of `nil` values.
@@ -818,10 +925,10 @@ module Sidekiq
818
925
 
819
926
  hash = Sidekiq.load_json(info)
820
927
  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))
928
+ "beat" => beat.to_f,
929
+ "quiet" => quiet,
930
+ "rss" => rss.to_i,
931
+ "rtt_us" => rtt_us.to_i))
825
932
  end
826
933
  end
827
934
 
@@ -829,6 +936,7 @@ module Sidekiq
829
936
  # based on current heartbeat. #each does that and ensures the set only
830
937
  # contains Sidekiq processes which have sent a heartbeat within the last
831
938
  # 60 seconds.
939
+ # @return [Integer] current number of registered Sidekiq processes
832
940
  def size
833
941
  Sidekiq.redis { |conn| conn.scard("processes") }
834
942
  end
@@ -836,10 +944,12 @@ module Sidekiq
836
944
  # Total number of threads available to execute jobs.
837
945
  # For Sidekiq Enterprise customers this number (in production) must be
838
946
  # less than or equal to your licensed concurrency.
947
+ # @return [Integer] the sum of process concurrency
839
948
  def total_concurrency
840
949
  sum { |x| x["concurrency"].to_i }
841
950
  end
842
951
 
952
+ # @return [Integer] total amount of RSS memory consumed by Sidekiq processes
843
953
  def total_rss_in_kb
844
954
  sum { |x| x["rss"].to_i }
845
955
  end
@@ -848,6 +958,8 @@ module Sidekiq
848
958
  # Returns the identity of the current cluster leader or "" if no leader.
849
959
  # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
850
960
  # or Sidekiq Pro.
961
+ # @return [String] Identity of cluster leader
962
+ # @return [String] empty string if no leader
851
963
  def leader
852
964
  @leader ||= begin
853
965
  x = Sidekiq.redis { |c| c.get("dear-leader") }
@@ -872,8 +984,11 @@ module Sidekiq
872
984
  # 'busy' => 10,
873
985
  # 'beat' => <last heartbeat>,
874
986
  # 'identity' => <unique string identifying the process>,
987
+ # 'embedded' => true,
875
988
  # }
876
989
  class Process
990
+ # :nodoc:
991
+ # @api private
877
992
  def initialize(hash)
878
993
  @attribs = hash
879
994
  end
@@ -883,7 +998,7 @@ module Sidekiq
883
998
  end
884
999
 
885
1000
  def labels
886
- Array(self["labels"])
1001
+ self["labels"].to_a
887
1002
  end
888
1003
 
889
1004
  def [](key)
@@ -898,18 +1013,47 @@ module Sidekiq
898
1013
  self["queues"]
899
1014
  end
900
1015
 
1016
+ def weights
1017
+ self["weights"]
1018
+ end
1019
+
1020
+ def version
1021
+ self["version"]
1022
+ end
1023
+
1024
+ def embedded?
1025
+ self["embedded"]
1026
+ end
1027
+
1028
+ # Signal this process to stop processing new jobs.
1029
+ # It will continue to execute jobs it has already fetched.
1030
+ # This method is *asynchronous* and it can take 5-10
1031
+ # seconds for the process to quiet.
901
1032
  def quiet!
1033
+ raise "Can't quiet an embedded process" if embedded?
1034
+
902
1035
  signal("TSTP")
903
1036
  end
904
1037
 
1038
+ # Signal this process to shutdown.
1039
+ # It will shutdown within its configured :timeout value, default 25 seconds.
1040
+ # This method is *asynchronous* and it can take 5-10
1041
+ # seconds for the process to start shutting down.
905
1042
  def stop!
1043
+ raise "Can't stop an embedded process" if embedded?
1044
+
906
1045
  signal("TERM")
907
1046
  end
908
1047
 
1048
+ # Signal this process to log backtraces for all threads.
1049
+ # Useful if you have a frozen or deadlocked process which is
1050
+ # still sending a heartbeat.
1051
+ # This method is *asynchronous* and it can take 5-10 seconds.
909
1052
  def dump_threads
910
1053
  signal("TTIN")
911
1054
  end
912
1055
 
1056
+ # @return [Boolean] true if this process is quiet or shutting down
913
1057
  def stopping?
914
1058
  self["quiet"] == "true"
915
1059
  end
@@ -919,9 +1063,9 @@ module Sidekiq
919
1063
  def signal(sig)
920
1064
  key = "#{identity}-signals"
921
1065
  Sidekiq.redis do |c|
922
- c.multi do
923
- c.lpush(key, sig)
924
- c.expire(key, 60)
1066
+ c.multi do |transaction|
1067
+ transaction.lpush(key, sig)
1068
+ transaction.expire(key, 60)
925
1069
  end
926
1070
  end
927
1071
  end
@@ -952,24 +1096,24 @@ module Sidekiq
952
1096
 
953
1097
  def each(&block)
954
1098
  results = []
1099
+ procs = nil
1100
+ all_works = nil
1101
+
955
1102
  Sidekiq.redis do |conn|
956
- procs = conn.sscan_each("processes").to_a
957
- procs.sort.each do |key|
958
- valid, workers = conn.pipelined {
959
- conn.exists?(key)
960
- conn.hgetall("#{key}:workers")
961
- }
962
- next unless valid
963
- workers.each_pair do |tid, 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]
1103
+ procs = conn.sscan("processes").to_a.sort
1104
+ all_works = conn.pipelined do |pipeline|
1105
+ procs.each do |key|
1106
+ pipeline.hgetall("#{key}:work")
969
1107
  end
970
1108
  end
971
1109
  end
972
1110
 
1111
+ procs.zip(all_works).each do |key, workers|
1112
+ workers.each_pair do |tid, json|
1113
+ results << [key, tid, Sidekiq.load_json(json)] unless json.empty?
1114
+ end
1115
+ end
1116
+
973
1117
  results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
974
1118
  end
975
1119
 
@@ -981,13 +1125,13 @@ module Sidekiq
981
1125
  # which can easily get out of sync with crashy processes.
982
1126
  def size
983
1127
  Sidekiq.redis do |conn|
984
- procs = conn.sscan_each("processes").to_a
1128
+ procs = conn.sscan("processes").to_a
985
1129
  if procs.empty?
986
1130
  0
987
1131
  else
988
- conn.pipelined {
1132
+ conn.pipelined { |pipeline|
989
1133
  procs.each do |key|
990
- conn.hget(key, "busy")
1134
+ pipeline.hget(key, "busy")
991
1135
  end
992
1136
  }.sum(&:to_i)
993
1137
  end