sidekiq 6.3.1 → 7.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +285 -11
  3. data/LICENSE.txt +9 -0
  4. data/README.md +47 -34
  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 +333 -190
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +86 -80
  15. data/lib/sidekiq/client.rb +104 -95
  16. data/lib/sidekiq/{util.rb → component.rb} +14 -41
  17. data/lib/sidekiq/config.rb +282 -0
  18. data/lib/sidekiq/deploy.rb +62 -0
  19. data/lib/sidekiq/embedded.rb +61 -0
  20. data/lib/sidekiq/fetch.rb +23 -24
  21. data/lib/sidekiq/job.rb +371 -10
  22. data/lib/sidekiq/job_logger.rb +16 -28
  23. data/lib/sidekiq/job_retry.rb +99 -58
  24. data/lib/sidekiq/job_util.rb +107 -0
  25. data/lib/sidekiq/launcher.rb +103 -95
  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 +59 -16
  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 +81 -80
  38. data/lib/sidekiq/rails.rb +21 -14
  39. data/lib/sidekiq/redis_client_adapter.rb +95 -0
  40. data/lib/sidekiq/redis_connection.rb +14 -82
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +76 -38
  43. data/lib/sidekiq/testing/inline.rb +4 -4
  44. data/lib/sidekiq/testing.rb +42 -69
  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 +95 -11
  49. data/lib/sidekiq/web/csrf_protection.rb +4 -4
  50. data/lib/sidekiq/web/helpers.rb +58 -30
  51. data/lib/sidekiq/web.rb +22 -17
  52. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  53. data/lib/sidekiq.rb +85 -202
  54. data/sidekiq.gemspec +12 -10
  55. data/web/assets/javascripts/application.js +77 -26
  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 +168 -0
  60. data/web/assets/javascripts/dashboard.js +3 -240
  61. data/web/assets/javascripts/metrics.js +264 -0
  62. data/web/assets/stylesheets/application-dark.css +17 -17
  63. data/web/assets/stylesheets/application-rtl.css +2 -91
  64. data/web/assets/stylesheets/application.css +69 -302
  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 +84 -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 +83 -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 +5 -2
  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 +1 -1
  100. data/web/views/_summary.erb +1 -1
  101. data/web/views/busy.erb +44 -28
  102. data/web/views/dashboard.erb +36 -4
  103. data/web/views/filtering.erb +7 -0
  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 +15 -15
  108. data/web/views/queues.erb +3 -1
  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 -311
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,24 +67,36 @@ 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.lindex("queue:default", -1)
65
96
  end
66
97
  }
67
98
 
68
- default_queue_latency = if (entry = pipe1_res[6].first)
99
+ default_queue_latency = if (entry = pipe1_res[6])
69
100
  job = begin
70
101
  Sidekiq.load_json(entry)
71
102
  rescue
@@ -91,36 +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
116
148
  @stats
117
149
  end
118
150
 
151
+ # @api private
119
152
  def fetch_stats!
120
153
  fetch_stats_fast!
121
154
  fetch_stats_slow!
122
155
  end
123
156
 
157
+ # @api private
124
158
  def reset(*stats)
125
159
  all = %w[failed processed]
126
160
  stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
@@ -142,25 +176,10 @@ module Sidekiq
142
176
  @stats[s] || raise(ArgumentError, "Unknown stat #{s}")
143
177
  end
144
178
 
145
- class Queues
146
- def lengths
147
- Sidekiq.redis do |conn|
148
- queues = conn.sscan_each("queues").to_a
149
-
150
- lengths = conn.pipelined {
151
- queues.each do |queue|
152
- conn.llen("queue:#{queue}")
153
- end
154
- }
155
-
156
- array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
157
- array_of_arrays.to_h
158
- end
159
- end
160
- end
161
-
162
179
  class History
163
- 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)
164
183
  @days_previous = days_previous
165
184
  @start_date = start_date || Time.now.utc.to_date
166
185
  end
@@ -183,15 +202,10 @@ module Sidekiq
183
202
 
184
203
  keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
185
204
 
186
- begin
187
- Sidekiq.redis do |conn|
188
- conn.mget(keys).each_with_index do |value, idx|
189
- stat_hash[dates[idx]] = value ? value.to_i : 0
190
- 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
191
208
  end
192
- rescue Redis::CommandError
193
- # mget will trigger a CROSSSLOT error when run against a Cluster
194
- # TODO Someone want to add Cluster support?
195
209
  end
196
210
 
197
211
  stat_hash
@@ -200,9 +214,10 @@ module Sidekiq
200
214
  end
201
215
 
202
216
  ##
203
- # Encapsulates a queue within Sidekiq.
217
+ # Represents a queue within Sidekiq.
204
218
  # Allows enumeration of all jobs within the queue
205
- # 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.
206
221
  #
207
222
  # queue = Sidekiq::Queue.new("mailer")
208
223
  # queue.each do |job|
@@ -210,29 +225,34 @@ module Sidekiq
210
225
  # job.args # => [1, 2, 3]
211
226
  # job.delete if job.jid == 'abcdef1234567890'
212
227
  # end
213
- #
214
228
  class Queue
215
229
  include Enumerable
216
230
 
217
231
  ##
218
- # Return all known queues within Redis.
232
+ # Fetch all known queues within Redis.
219
233
  #
234
+ # @return [Array<Sidekiq::Queue>]
220
235
  def self.all
221
- 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) }
222
237
  end
223
238
 
224
239
  attr_reader :name
225
240
 
241
+ # @param name [String] the name of the queue
226
242
  def initialize(name = "default")
227
243
  @name = name.to_s
228
244
  @rname = "queue:#{name}"
229
245
  end
230
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
231
251
  def size
232
252
  Sidekiq.redis { |con| con.llen(@rname) }
233
253
  end
234
254
 
235
- # Sidekiq Pro overrides this
255
+ # @return [Boolean] if the queue is currently paused
236
256
  def paused?
237
257
  false
238
258
  end
@@ -241,11 +261,11 @@ module Sidekiq
241
261
  # Calculates this queue's latency, the difference in seconds since the oldest
242
262
  # job in the queue was enqueued.
243
263
  #
244
- # @return Float
264
+ # @return [Float] in seconds
245
265
  def latency
246
266
  entry = Sidekiq.redis { |conn|
247
- conn.lrange(@rname, -1, -1)
248
- }.first
267
+ conn.lindex(@rname, -1)
268
+ }
249
269
  return 0 unless entry
250
270
  job = Sidekiq.load_json(entry)
251
271
  now = Time.now.to_f
@@ -277,34 +297,54 @@ module Sidekiq
277
297
  ##
278
298
  # Find the job with the given JID within this queue.
279
299
  #
280
- # This is a slow, inefficient operation. Do not use under
300
+ # This is a *slow, inefficient* operation. Do not use under
281
301
  # normal conditions.
302
+ #
303
+ # @param jid [String] the job_id to look for
304
+ # @return [Sidekiq::JobRecord]
305
+ # @return [nil] if not found
282
306
  def find_job(jid)
283
307
  detect { |j| j.jid == jid }
284
308
  end
285
309
 
310
+ # delete all jobs within this queue
311
+ # @return [Boolean] true
286
312
  def clear
287
313
  Sidekiq.redis do |conn|
288
- conn.multi do
289
- conn.unlink(@rname)
290
- conn.srem("queues", name)
314
+ conn.multi do |transaction|
315
+ transaction.unlink(@rname)
316
+ transaction.srem("queues", [name])
291
317
  end
292
318
  end
319
+ true
293
320
  end
294
321
  alias_method :💣, :clear
322
+
323
+ # :nodoc:
324
+ # @api private
325
+ def as_json(options = nil)
326
+ {name: name} # 5336
327
+ end
295
328
  end
296
329
 
297
330
  ##
298
- # Encapsulates a pending job within a Sidekiq queue or
299
- # sorted set.
331
+ # Represents a pending job within a Sidekiq queue.
300
332
  #
301
333
  # The job should be considered immutable but may be
302
334
  # removed from the queue via JobRecord#delete.
303
- #
304
335
  class JobRecord
336
+ # the parsed Hash of job data
337
+ # @!attribute [r] Item
305
338
  attr_reader :item
339
+ # the underlying String in Redis
340
+ # @!attribute [r] Value
306
341
  attr_reader :value
342
+ # the queue associated with this job
343
+ # @!attribute [r] Queue
344
+ attr_reader :queue
307
345
 
346
+ # :nodoc:
347
+ # @api private
308
348
  def initialize(item, queue_name = nil)
309
349
  @args = nil
310
350
  @value = item
@@ -312,6 +352,8 @@ module Sidekiq
312
352
  @queue = queue_name || @item["queue"]
313
353
  end
314
354
 
355
+ # :nodoc:
356
+ # @api private
315
357
  def parse(item)
316
358
  Sidekiq.load_json(item)
317
359
  rescue JSON::ParserError
@@ -323,6 +365,8 @@ module Sidekiq
323
365
  {}
324
366
  end
325
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.
326
370
  def klass
327
371
  self["class"]
328
372
  end
@@ -330,12 +374,7 @@ module Sidekiq
330
374
  def display_class
331
375
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
332
376
  @klass ||= self["display_class"] || begin
333
- case klass
334
- when /\ASidekiq::Extensions::Delayed/
335
- safe_load(args[0], klass) do |target, method, _|
336
- "#{target}.#{method}"
337
- end
338
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
377
+ if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
339
378
  job_class = @item["wrapped"] || args[0]
340
379
  if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
341
380
  # MailerClass#mailer_method
@@ -351,28 +390,23 @@ module Sidekiq
351
390
 
352
391
  def display_args
353
392
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
354
- @display_args ||= case klass
355
- when /\ASidekiq::Extensions::Delayed/
356
- safe_load(args[0], args) do |_, _, arg|
357
- arg
358
- end
359
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
360
- job_args = self["wrapped"] ? args[0]["arguments"] : []
361
- if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
362
- # remove MailerClass, mailer_method and 'deliver_now'
363
- job_args.drop(3)
364
- elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
365
- # remove MailerClass, mailer_method and 'deliver_now'
366
- job_args.drop(3).first["args"]
367
- else
368
- job_args
369
- end
370
- else
371
- if self["encrypt"]
372
- # no point in showing 150+ bytes of random garbage
373
- args[-1] = "[encrypted data]"
374
- end
375
- 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
376
410
  end
377
411
  end
378
412
 
@@ -384,6 +418,10 @@ module Sidekiq
384
418
  self["jid"]
385
419
  end
386
420
 
421
+ def bid
422
+ self["bid"]
423
+ end
424
+
387
425
  def enqueued_at
388
426
  self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
389
427
  end
@@ -406,15 +444,12 @@ module Sidekiq
406
444
  end
407
445
  end
408
446
 
409
- attr_reader :queue
410
-
411
447
  def latency
412
448
  now = Time.now.to_f
413
449
  now - (@item["enqueued_at"] || @item["created_at"] || now)
414
450
  end
415
451
 
416
- ##
417
- # Remove this job from the queue.
452
+ # Remove this job from the queue
418
453
  def delete
419
454
  count = Sidekiq.redis { |conn|
420
455
  conn.lrem("queue:#{@queue}", 1, @value)
@@ -422,6 +457,7 @@ module Sidekiq
422
457
  count != 0
423
458
  end
424
459
 
460
+ # Access arbitrary attributes within the job hash
425
461
  def [](name)
426
462
  # nil will happen if the JSON fails to parse.
427
463
  # We don't guarantee Sidekiq will work with bad job JSON but we should
@@ -431,47 +467,58 @@ module Sidekiq
431
467
 
432
468
  private
433
469
 
434
- def safe_load(content, default)
435
- yield(*YAML.load(content))
436
- rescue => ex
437
- # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
438
- # memory yet so the YAML can't be loaded.
439
- Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
440
- default
441
- end
470
+ ACTIVE_JOB_PREFIX = "_aj_"
471
+ GLOBALID_KEY = "_aj_globalid"
442
472
 
443
- def uncompress_backtrace(backtrace)
444
- if backtrace.is_a?(Array)
445
- # Handle old jobs with raw Array backtrace format
446
- backtrace
447
- else
448
- decoded = Base64.decode64(backtrace)
449
- uncompressed = Zlib::Inflate.inflate(decoded)
450
- begin
451
- Sidekiq.load_json(uncompressed)
452
- rescue
453
- # Handle old jobs with marshalled backtrace format
454
- # TODO Remove in 7.x
455
- 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) }
456
483
  end
484
+ else
485
+ argument
457
486
  end
458
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
459
498
  end
460
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).
461
504
  class SortedEntry < JobRecord
462
505
  attr_reader :score
463
506
  attr_reader :parent
464
507
 
508
+ # :nodoc:
509
+ # @api private
465
510
  def initialize(parent, score, item)
466
511
  super(item)
467
- @score = score
512
+ @score = Float(score)
468
513
  @parent = parent
469
514
  end
470
515
 
516
+ # The timestamp associated with this entry
471
517
  def at
472
518
  Time.at(score).utc
473
519
  end
474
520
 
521
+ # remove this entry from the sorted set
475
522
  def delete
476
523
  if @value
477
524
  @parent.delete_by_value(@parent.name, @value)
@@ -480,12 +527,17 @@ module Sidekiq
480
527
  end
481
528
  end
482
529
 
530
+ # Change the scheduled time for this job.
531
+ #
532
+ # @param at [Time] the new timestamp for this job
483
533
  def reschedule(at)
484
534
  Sidekiq.redis do |conn|
485
535
  conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
486
536
  end
487
537
  end
488
538
 
539
+ # Enqueue this job from the scheduled or dead set so it will
540
+ # be executed at some point in the near future.
489
541
  def add_to_queue
490
542
  remove_job do |message|
491
543
  msg = Sidekiq.load_json(message)
@@ -493,6 +545,8 @@ module Sidekiq
493
545
  end
494
546
  end
495
547
 
548
+ # enqueue this job from the retry set so it will be executed
549
+ # at some point in the near future.
496
550
  def retry
497
551
  remove_job do |message|
498
552
  msg = Sidekiq.load_json(message)
@@ -501,8 +555,7 @@ module Sidekiq
501
555
  end
502
556
  end
503
557
 
504
- ##
505
- # Place job in the dead set
558
+ # Move this job from its current set into the Dead set.
506
559
  def kill
507
560
  remove_job do |message|
508
561
  DeadSet.new.kill(message)
@@ -517,9 +570,9 @@ module Sidekiq
517
570
 
518
571
  def remove_job
519
572
  Sidekiq.redis do |conn|
520
- results = conn.multi {
521
- conn.zrangebyscore(parent.name, score, score)
522
- 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)
523
576
  }.first
524
577
 
525
578
  if results.size == 1
@@ -540,9 +593,9 @@ module Sidekiq
540
593
  yield msg if msg
541
594
 
542
595
  # push the rest back onto the sorted set
543
- conn.multi do
596
+ conn.multi do |transaction|
544
597
  nonmatched.each do |message|
545
- conn.zadd(parent.name, score.to_f.to_s, message)
598
+ transaction.zadd(parent.name, score.to_f.to_s, message)
546
599
  end
547
600
  end
548
601
  end
@@ -550,43 +603,69 @@ module Sidekiq
550
603
  end
551
604
  end
552
605
 
606
+ # Base class for all sorted sets within Sidekiq.
553
607
  class SortedSet
554
608
  include Enumerable
555
609
 
610
+ # Redis key of the set
611
+ # @!attribute [r] Name
556
612
  attr_reader :name
557
613
 
614
+ # :nodoc:
615
+ # @api private
558
616
  def initialize(name)
559
617
  @name = name
560
618
  @_size = size
561
619
  end
562
620
 
621
+ # real-time size of the set, will change
563
622
  def size
564
623
  Sidekiq.redis { |c| c.zcard(name) }
565
624
  end
566
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
567
632
  def scan(match, count = 100)
568
633
  return to_enum(:scan, match, count) unless block_given?
569
634
 
570
635
  match = "*#{match}*" unless match.include?("*")
571
636
  Sidekiq.redis do |conn|
572
- conn.zscan_each(name, match: match, count: count) do |entry, score|
637
+ conn.zscan(name, match: match, count: count) do |entry, score|
573
638
  yield SortedEntry.new(self, score, entry)
574
639
  end
575
640
  end
576
641
  end
577
642
 
643
+ # @return [Boolean] always true
578
644
  def clear
579
645
  Sidekiq.redis do |conn|
580
646
  conn.unlink(name)
581
647
  end
648
+ true
582
649
  end
583
650
  alias_method :💣, :clear
651
+
652
+ # :nodoc:
653
+ # @api private
654
+ def as_json(options = nil)
655
+ {name: name} # 5336
656
+ end
584
657
  end
585
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.
586
662
  class JobSet < SortedSet
587
- 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)
588
667
  Sidekiq.redis do |conn|
589
- 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))
590
669
  end
591
670
  end
592
671
 
@@ -600,7 +679,7 @@ module Sidekiq
600
679
  range_start = page * page_size + offset_size
601
680
  range_end = range_start + page_size - 1
602
681
  elements = Sidekiq.redis { |conn|
603
- conn.zrange name, range_start, range_end, with_scores: true
682
+ conn.zrange name, range_start, range_end, withscores: true
604
683
  }
605
684
  break if elements.empty?
606
685
  page -= 1
@@ -614,6 +693,10 @@ module Sidekiq
614
693
  ##
615
694
  # Fetch jobs that match a given time or Range. Job ID is an
616
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
617
700
  def fetch(score, jid = nil)
618
701
  begin_score, end_score =
619
702
  if score.is_a?(Range)
@@ -623,7 +706,7 @@ module Sidekiq
623
706
  end
624
707
 
625
708
  elements = Sidekiq.redis { |conn|
626
- conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
709
+ conn.zrange(name, begin_score, end_score, "BYSCORE", withscores: true)
627
710
  }
628
711
 
629
712
  elements.each_with_object([]) do |element, result|
@@ -635,11 +718,14 @@ module Sidekiq
635
718
 
636
719
  ##
637
720
  # Find the job with the given JID within this sorted set.
638
- # 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
639
725
  def find_job(jid)
640
726
  Sidekiq.redis do |conn|
641
- conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
642
- job = JSON.parse(entry)
727
+ conn.zscan(name, match: "*#{jid}*", count: 100) do |entry, score|
728
+ job = Sidekiq.load_json(entry)
643
729
  matched = job["jid"] == jid
644
730
  return SortedEntry.new(self, score, entry) if matched
645
731
  end
@@ -647,6 +733,8 @@ module Sidekiq
647
733
  nil
648
734
  end
649
735
 
736
+ # :nodoc:
737
+ # @api private
650
738
  def delete_by_value(name, value)
651
739
  Sidekiq.redis do |conn|
652
740
  ret = conn.zrem(name, value)
@@ -655,9 +743,11 @@ module Sidekiq
655
743
  end
656
744
  end
657
745
 
746
+ # :nodoc:
747
+ # @api private
658
748
  def delete_by_jid(score, jid)
659
749
  Sidekiq.redis do |conn|
660
- elements = conn.zrangebyscore(name, score, score)
750
+ elements = conn.zrange(name, score, score, "BYSCORE")
661
751
  elements.each do |element|
662
752
  if element.index(jid)
663
753
  message = Sidekiq.load_json(element)
@@ -675,17 +765,13 @@ module Sidekiq
675
765
  end
676
766
 
677
767
  ##
678
- # Allows enumeration of scheduled jobs within Sidekiq.
768
+ # The set of scheduled jobs within Sidekiq.
679
769
  # Based on this, you can search/filter for jobs. Here's an
680
- # example where I'm selecting all jobs of a certain type
681
- # 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.
682
774
  #
683
- # r = Sidekiq::ScheduledSet.new
684
- # r.select do |scheduled|
685
- # scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
686
- # scheduled.args[0] == 'User' &&
687
- # scheduled.args[1] == 'setup_new_subscriber'
688
- # end.map(&:delete)
689
775
  class ScheduledSet < JobSet
690
776
  def initialize
691
777
  super "schedule"
@@ -693,46 +779,48 @@ module Sidekiq
693
779
  end
694
780
 
695
781
  ##
696
- # Allows enumeration of retries within Sidekiq.
782
+ # The set of retries within Sidekiq.
697
783
  # Based on this, you can search/filter for jobs. Here's an
698
784
  # example where I'm selecting all jobs of a certain type
699
785
  # and deleting them from the retry queue.
700
786
  #
701
- # r = Sidekiq::RetrySet.new
702
- # r.select do |retri|
703
- # retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
704
- # retri.args[0] == 'User' &&
705
- # retri.args[1] == 'setup_new_subscriber'
706
- # end.map(&:delete)
787
+ # See the API wiki page for usage notes and examples.
788
+ #
707
789
  class RetrySet < JobSet
708
790
  def initialize
709
791
  super "retry"
710
792
  end
711
793
 
794
+ # Enqueues all jobs pending within the retry set.
712
795
  def retry_all
713
796
  each(&:retry) while size > 0
714
797
  end
715
798
 
799
+ # Kills all jobs pending within the retry set.
716
800
  def kill_all
717
801
  each(&:kill) while size > 0
718
802
  end
719
803
  end
720
804
 
721
805
  ##
722
- # 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.
723
809
  #
724
810
  class DeadSet < JobSet
725
811
  def initialize
726
812
  super "dead"
727
813
  end
728
814
 
815
+ # Add the given job to the Dead set.
816
+ # @param message [String] the job data as JSON
729
817
  def kill(message, opts = {})
730
818
  now = Time.now.to_f
731
819
  Sidekiq.redis do |conn|
732
- conn.multi do
733
- conn.zadd(name, now.to_s, message)
734
- conn.zremrangebyscore(name, "-inf", now - self.class.timeout)
735
- 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])
736
824
  end
737
825
  end
738
826
 
@@ -740,24 +828,17 @@ module Sidekiq
740
828
  job = Sidekiq.load_json(message)
741
829
  r = RuntimeError.new("Job killed by API")
742
830
  r.set_backtrace(caller)
743
- Sidekiq.death_handlers.each do |handle|
831
+ Sidekiq.default_configuration.death_handlers.each do |handle|
744
832
  handle.call(job, r)
745
833
  end
746
834
  end
747
835
  true
748
836
  end
749
837
 
838
+ # Enqueue all dead jobs
750
839
  def retry_all
751
840
  each(&:retry) while size > 0
752
841
  end
753
-
754
- def self.max_jobs
755
- Sidekiq.options[:dead_max_jobs]
756
- end
757
-
758
- def self.timeout
759
- Sidekiq.options[:dead_timeout_in_seconds]
760
- end
761
842
  end
762
843
 
763
844
  ##
@@ -765,24 +846,49 @@ module Sidekiq
765
846
  # right now. Each process sends a heartbeat to Redis every 5 seconds
766
847
  # so this set should be relatively accurate, barring network partitions.
767
848
  #
768
- # Yields a Sidekiq::Process.
849
+ # @yieldparam [Sidekiq::Process]
769
850
  #
770
851
  class ProcessSet
771
852
  include Enumerable
772
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
773
874
  def initialize(clean_plz = true)
774
875
  cleanup if clean_plz
775
876
  end
776
877
 
777
878
  # Cleans up dead processes recorded in Redis.
778
879
  # Returns the number of processes cleaned.
880
+ # :nodoc:
881
+ # @api private
779
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
+
780
886
  count = 0
781
887
  Sidekiq.redis do |conn|
782
- procs = conn.sscan_each("processes").to_a.sort
783
- heartbeats = conn.pipelined {
888
+ procs = conn.sscan("processes").to_a
889
+ heartbeats = conn.pipelined { |pipeline|
784
890
  procs.each do |key|
785
- conn.hget(key, "info")
891
+ pipeline.hget(key, "info")
786
892
  end
787
893
  }
788
894
 
@@ -799,19 +905,19 @@ module Sidekiq
799
905
 
800
906
  def each
801
907
  result = Sidekiq.redis { |conn|
802
- procs = conn.sscan_each("processes").to_a.sort
908
+ procs = conn.sscan("processes").to_a.sort
803
909
 
804
910
  # We're making a tradeoff here between consuming more memory instead of
805
911
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
806
912
  # you'll be happier this way
807
- conn.pipelined do
913
+ conn.pipelined do |pipeline|
808
914
  procs.each do |key|
809
- conn.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
915
+ pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
810
916
  end
811
917
  end
812
918
  }
813
919
 
814
- result.each do |info, busy, at_s, quiet, rss, rtt|
920
+ result.each do |info, busy, beat, quiet, rss, rtt_us|
815
921
  # If a process is stopped between when we query Redis for `procs` and
816
922
  # when we query for `result`, we will have an item in `result` that is
817
923
  # composed of `nil` values.
@@ -819,10 +925,10 @@ module Sidekiq
819
925
 
820
926
  hash = Sidekiq.load_json(info)
821
927
  yield Process.new(hash.merge("busy" => busy.to_i,
822
- "beat" => at_s.to_f,
823
- "quiet" => quiet,
824
- "rss" => rss.to_i,
825
- "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))
826
932
  end
827
933
  end
828
934
 
@@ -830,6 +936,7 @@ module Sidekiq
830
936
  # based on current heartbeat. #each does that and ensures the set only
831
937
  # contains Sidekiq processes which have sent a heartbeat within the last
832
938
  # 60 seconds.
939
+ # @return [Integer] current number of registered Sidekiq processes
833
940
  def size
834
941
  Sidekiq.redis { |conn| conn.scard("processes") }
835
942
  end
@@ -837,10 +944,12 @@ module Sidekiq
837
944
  # Total number of threads available to execute jobs.
838
945
  # For Sidekiq Enterprise customers this number (in production) must be
839
946
  # less than or equal to your licensed concurrency.
947
+ # @return [Integer] the sum of process concurrency
840
948
  def total_concurrency
841
949
  sum { |x| x["concurrency"].to_i }
842
950
  end
843
951
 
952
+ # @return [Integer] total amount of RSS memory consumed by Sidekiq processes
844
953
  def total_rss_in_kb
845
954
  sum { |x| x["rss"].to_i }
846
955
  end
@@ -849,6 +958,8 @@ module Sidekiq
849
958
  # Returns the identity of the current cluster leader or "" if no leader.
850
959
  # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
851
960
  # or Sidekiq Pro.
961
+ # @return [String] Identity of cluster leader
962
+ # @return [String] empty string if no leader
852
963
  def leader
853
964
  @leader ||= begin
854
965
  x = Sidekiq.redis { |c| c.get("dear-leader") }
@@ -873,8 +984,11 @@ module Sidekiq
873
984
  # 'busy' => 10,
874
985
  # 'beat' => <last heartbeat>,
875
986
  # 'identity' => <unique string identifying the process>,
987
+ # 'embedded' => true,
876
988
  # }
877
989
  class Process
990
+ # :nodoc:
991
+ # @api private
878
992
  def initialize(hash)
879
993
  @attribs = hash
880
994
  end
@@ -884,7 +998,7 @@ module Sidekiq
884
998
  end
885
999
 
886
1000
  def labels
887
- Array(self["labels"])
1001
+ self["labels"].to_a
888
1002
  end
889
1003
 
890
1004
  def [](key)
@@ -899,18 +1013,47 @@ module Sidekiq
899
1013
  self["queues"]
900
1014
  end
901
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.
902
1032
  def quiet!
1033
+ raise "Can't quiet an embedded process" if embedded?
1034
+
903
1035
  signal("TSTP")
904
1036
  end
905
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.
906
1042
  def stop!
1043
+ raise "Can't stop an embedded process" if embedded?
1044
+
907
1045
  signal("TERM")
908
1046
  end
909
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.
910
1052
  def dump_threads
911
1053
  signal("TTIN")
912
1054
  end
913
1055
 
1056
+ # @return [Boolean] true if this process is quiet or shutting down
914
1057
  def stopping?
915
1058
  self["quiet"] == "true"
916
1059
  end
@@ -920,9 +1063,9 @@ module Sidekiq
920
1063
  def signal(sig)
921
1064
  key = "#{identity}-signals"
922
1065
  Sidekiq.redis do |c|
923
- c.multi do
924
- c.lpush(key, sig)
925
- c.expire(key, 60)
1066
+ c.multi do |transaction|
1067
+ transaction.lpush(key, sig)
1068
+ transaction.expire(key, 60)
926
1069
  end
927
1070
  end
928
1071
  end
@@ -953,24 +1096,24 @@ module Sidekiq
953
1096
 
954
1097
  def each(&block)
955
1098
  results = []
1099
+ procs = nil
1100
+ all_works = nil
1101
+
956
1102
  Sidekiq.redis do |conn|
957
- procs = conn.sscan_each("processes").to_a
958
- procs.sort.each do |key|
959
- valid, workers = conn.pipelined {
960
- conn.exists?(key)
961
- conn.hgetall("#{key}:workers")
962
- }
963
- next unless valid
964
- workers.each_pair do |tid, json|
965
- hsh = Sidekiq.load_json(json)
966
- p = hsh["payload"]
967
- # avoid breaking API, this is a side effect of the JSON optimization in #4316
968
- hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
969
- 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")
970
1107
  end
971
1108
  end
972
1109
  end
973
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
+
974
1117
  results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
975
1118
  end
976
1119
 
@@ -982,13 +1125,13 @@ module Sidekiq
982
1125
  # which can easily get out of sync with crashy processes.
983
1126
  def size
984
1127
  Sidekiq.redis do |conn|
985
- procs = conn.sscan_each("processes").to_a
1128
+ procs = conn.sscan("processes").to_a
986
1129
  if procs.empty?
987
1130
  0
988
1131
  else
989
- conn.pipelined {
1132
+ conn.pipelined { |pipeline|
990
1133
  procs.each do |key|
991
- conn.hget(key, "busy")
1134
+ pipeline.hget(key, "busy")
992
1135
  end
993
1136
  }.sum(&:to_i)
994
1137
  end