sidekiq 6.4.0 → 6.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +119 -1
  3. data/README.md +6 -1
  4. data/bin/sidekiq +3 -3
  5. data/bin/sidekiqload +70 -66
  6. data/bin/sidekiqmon +1 -1
  7. data/lib/sidekiq/api.rb +255 -100
  8. data/lib/sidekiq/cli.rb +60 -38
  9. data/lib/sidekiq/client.rb +44 -30
  10. data/lib/sidekiq/component.rb +65 -0
  11. data/lib/sidekiq/delay.rb +2 -2
  12. data/lib/sidekiq/extensions/action_mailer.rb +2 -2
  13. data/lib/sidekiq/extensions/active_record.rb +2 -2
  14. data/lib/sidekiq/extensions/class_methods.rb +2 -2
  15. data/lib/sidekiq/extensions/generic_proxy.rb +3 -3
  16. data/lib/sidekiq/fetch.rb +20 -18
  17. data/lib/sidekiq/job_logger.rb +15 -27
  18. data/lib/sidekiq/job_retry.rb +73 -52
  19. data/lib/sidekiq/job_util.rb +15 -9
  20. data/lib/sidekiq/launcher.rb +58 -54
  21. data/lib/sidekiq/logger.rb +8 -18
  22. data/lib/sidekiq/manager.rb +28 -25
  23. data/lib/sidekiq/metrics/deploy.rb +47 -0
  24. data/lib/sidekiq/metrics/query.rb +153 -0
  25. data/lib/sidekiq/metrics/shared.rb +94 -0
  26. data/lib/sidekiq/metrics/tracking.rb +134 -0
  27. data/lib/sidekiq/middleware/chain.rb +82 -38
  28. data/lib/sidekiq/middleware/current_attributes.rb +18 -12
  29. data/lib/sidekiq/middleware/i18n.rb +6 -4
  30. data/lib/sidekiq/middleware/modules.rb +21 -0
  31. data/lib/sidekiq/monitor.rb +2 -2
  32. data/lib/sidekiq/paginator.rb +17 -9
  33. data/lib/sidekiq/processor.rb +47 -41
  34. data/lib/sidekiq/rails.rb +19 -13
  35. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  36. data/lib/sidekiq/redis_connection.rb +80 -49
  37. data/lib/sidekiq/ring_buffer.rb +29 -0
  38. data/lib/sidekiq/scheduled.rb +53 -24
  39. data/lib/sidekiq/testing/inline.rb +4 -4
  40. data/lib/sidekiq/testing.rb +37 -36
  41. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  42. data/lib/sidekiq/version.rb +1 -1
  43. data/lib/sidekiq/web/action.rb +3 -3
  44. data/lib/sidekiq/web/application.rb +21 -5
  45. data/lib/sidekiq/web/csrf_protection.rb +2 -2
  46. data/lib/sidekiq/web/helpers.rb +21 -8
  47. data/lib/sidekiq/web.rb +8 -4
  48. data/lib/sidekiq/worker.rb +26 -20
  49. data/lib/sidekiq.rb +107 -31
  50. data/sidekiq.gemspec +2 -2
  51. data/web/assets/javascripts/application.js +59 -26
  52. data/web/assets/javascripts/chart.min.js +13 -0
  53. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  54. data/web/assets/javascripts/dashboard.js +0 -17
  55. data/web/assets/javascripts/graph.js +16 -0
  56. data/web/assets/javascripts/metrics.js +262 -0
  57. data/web/assets/stylesheets/application.css +45 -1
  58. data/web/locales/el.yml +43 -19
  59. data/web/locales/en.yml +7 -0
  60. data/web/locales/ja.yml +7 -0
  61. data/web/locales/pt-br.yml +27 -9
  62. data/web/locales/zh-cn.yml +36 -11
  63. data/web/locales/zh-tw.yml +32 -7
  64. data/web/views/_nav.erb +1 -1
  65. data/web/views/_summary.erb +1 -1
  66. data/web/views/busy.erb +9 -4
  67. data/web/views/dashboard.erb +1 -0
  68. data/web/views/metrics.erb +69 -0
  69. data/web/views/metrics_for_job.erb +87 -0
  70. data/web/views/queue.erb +5 -1
  71. metadata +34 -9
  72. data/lib/sidekiq/exception_handler.rb +0 -27
  73. data/lib/sidekiq/util.rb +0 -108
data/lib/sidekiq/api.rb CHANGED
@@ -3,9 +3,31 @@
3
3
  require "sidekiq"
4
4
 
5
5
  require "zlib"
6
+ require "set"
6
7
  require "base64"
7
8
 
9
+ if ENV["SIDEKIQ_METRICS_BETA"]
10
+ require "sidekiq/metrics/deploy"
11
+ require "sidekiq/metrics/query"
12
+ end
13
+
14
+ #
15
+ # Sidekiq's Data API provides a Ruby object model on top
16
+ # of Sidekiq's runtime data in Redis. This API should never
17
+ # be used within application code for business logic.
18
+ #
19
+ # The Sidekiq server process never uses this API: all data
20
+ # manipulation is done directly for performance reasons to
21
+ # ensure we are using Redis as efficiently as possible at
22
+ # every callsite.
23
+ #
24
+
8
25
  module Sidekiq
26
+ # Retrieve runtime statistics from Redis regarding
27
+ # this Sidekiq cluster.
28
+ #
29
+ # stat = Sidekiq::Stats.new
30
+ # stat.processed
9
31
  class Stats
10
32
  def initialize
11
33
  fetch_stats_fast!
@@ -52,16 +74,17 @@ module Sidekiq
52
74
  end
53
75
 
54
76
  # O(1) redis calls
77
+ # @api private
55
78
  def fetch_stats_fast!
56
79
  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)
80
+ conn.pipelined do |pipeline|
81
+ pipeline.get("stat:processed")
82
+ pipeline.get("stat:failed")
83
+ pipeline.zcard("schedule")
84
+ pipeline.zcard("retry")
85
+ pipeline.zcard("dead")
86
+ pipeline.scard("processes")
87
+ pipeline.lrange("queue:default", -1, -1)
65
88
  end
66
89
  }
67
90
 
@@ -91,6 +114,7 @@ module Sidekiq
91
114
  end
92
115
 
93
116
  # O(number of processes + number of queues) redis calls
117
+ # @api private
94
118
  def fetch_stats_slow!
95
119
  processes = Sidekiq.redis { |conn|
96
120
  conn.sscan_each("processes").to_a
@@ -101,9 +125,9 @@ module Sidekiq
101
125
  }
102
126
 
103
127
  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}") }
128
+ conn.pipelined do |pipeline|
129
+ processes.each { |key| pipeline.hget(key, "busy") }
130
+ queues.each { |queue| pipeline.llen("queue:#{queue}") }
107
131
  end
108
132
  }
109
133
 
@@ -116,11 +140,13 @@ module Sidekiq
116
140
  @stats
117
141
  end
118
142
 
143
+ # @api private
119
144
  def fetch_stats!
120
145
  fetch_stats_fast!
121
146
  fetch_stats_slow!
122
147
  end
123
148
 
149
+ # @api private
124
150
  def reset(*stats)
125
151
  all = %w[failed processed]
126
152
  stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
@@ -147,9 +173,9 @@ module Sidekiq
147
173
  Sidekiq.redis do |conn|
148
174
  queues = conn.sscan_each("queues").to_a
149
175
 
150
- lengths = conn.pipelined {
176
+ lengths = conn.pipelined { |pipeline|
151
177
  queues.each do |queue|
152
- conn.llen("queue:#{queue}")
178
+ pipeline.llen("queue:#{queue}")
153
179
  end
154
180
  }
155
181
 
@@ -191,7 +217,7 @@ module Sidekiq
191
217
  stat_hash[dates[idx]] = value ? value.to_i : 0
192
218
  end
193
219
  end
194
- rescue Redis::CommandError
220
+ rescue RedisConnection.adapter::CommandError
195
221
  # mget will trigger a CROSSSLOT error when run against a Cluster
196
222
  # TODO Someone want to add Cluster support?
197
223
  end
@@ -202,9 +228,10 @@ module Sidekiq
202
228
  end
203
229
 
204
230
  ##
205
- # Encapsulates a queue within Sidekiq.
231
+ # Represents a queue within Sidekiq.
206
232
  # Allows enumeration of all jobs within the queue
207
- # and deletion of jobs.
233
+ # and deletion of jobs. NB: this queue data is real-time
234
+ # and is changing within Redis moment by moment.
208
235
  #
209
236
  # queue = Sidekiq::Queue.new("mailer")
210
237
  # queue.each do |job|
@@ -212,29 +239,34 @@ module Sidekiq
212
239
  # job.args # => [1, 2, 3]
213
240
  # job.delete if job.jid == 'abcdef1234567890'
214
241
  # end
215
- #
216
242
  class Queue
217
243
  include Enumerable
218
244
 
219
245
  ##
220
- # Return all known queues within Redis.
246
+ # Fetch all known queues within Redis.
221
247
  #
248
+ # @return [Array<Sidekiq::Queue>]
222
249
  def self.all
223
250
  Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
224
251
  end
225
252
 
226
253
  attr_reader :name
227
254
 
255
+ # @param name [String] the name of the queue
228
256
  def initialize(name = "default")
229
257
  @name = name.to_s
230
258
  @rname = "queue:#{name}"
231
259
  end
232
260
 
261
+ # The current size of the queue within Redis.
262
+ # This value is real-time and can change between calls.
263
+ #
264
+ # @return [Integer] the size
233
265
  def size
234
266
  Sidekiq.redis { |con| con.llen(@rname) }
235
267
  end
236
268
 
237
- # Sidekiq Pro overrides this
269
+ # @return [Boolean] if the queue is currently paused
238
270
  def paused?
239
271
  false
240
272
  end
@@ -243,7 +275,7 @@ module Sidekiq
243
275
  # Calculates this queue's latency, the difference in seconds since the oldest
244
276
  # job in the queue was enqueued.
245
277
  #
246
- # @return Float
278
+ # @return [Float] in seconds
247
279
  def latency
248
280
  entry = Sidekiq.redis { |conn|
249
281
  conn.lrange(@rname, -1, -1)
@@ -279,34 +311,54 @@ module Sidekiq
279
311
  ##
280
312
  # Find the job with the given JID within this queue.
281
313
  #
282
- # This is a slow, inefficient operation. Do not use under
314
+ # This is a *slow, inefficient* operation. Do not use under
283
315
  # normal conditions.
316
+ #
317
+ # @param jid [String] the job_id to look for
318
+ # @return [Sidekiq::JobRecord]
319
+ # @return [nil] if not found
284
320
  def find_job(jid)
285
321
  detect { |j| j.jid == jid }
286
322
  end
287
323
 
324
+ # delete all jobs within this queue
325
+ # @return [Boolean] true
288
326
  def clear
289
327
  Sidekiq.redis do |conn|
290
- conn.multi do
291
- conn.unlink(@rname)
292
- conn.srem("queues", name)
328
+ conn.multi do |transaction|
329
+ transaction.unlink(@rname)
330
+ transaction.srem("queues", [name])
293
331
  end
294
332
  end
333
+ true
295
334
  end
296
335
  alias_method :💣, :clear
336
+
337
+ # :nodoc:
338
+ # @api private
339
+ def as_json(options = nil)
340
+ {name: name} # 5336
341
+ end
297
342
  end
298
343
 
299
344
  ##
300
- # Encapsulates a pending job within a Sidekiq queue or
301
- # sorted set.
345
+ # Represents a pending job within a Sidekiq queue.
302
346
  #
303
347
  # The job should be considered immutable but may be
304
348
  # removed from the queue via JobRecord#delete.
305
- #
306
349
  class JobRecord
350
+ # the parsed Hash of job data
351
+ # @!attribute [r] Item
307
352
  attr_reader :item
353
+ # the underlying String in Redis
354
+ # @!attribute [r] Value
308
355
  attr_reader :value
356
+ # the queue associated with this job
357
+ # @!attribute [r] Queue
358
+ attr_reader :queue
309
359
 
360
+ # :nodoc:
361
+ # @api private
310
362
  def initialize(item, queue_name = nil)
311
363
  @args = nil
312
364
  @value = item
@@ -314,6 +366,8 @@ module Sidekiq
314
366
  @queue = queue_name || @item["queue"]
315
367
  end
316
368
 
369
+ # :nodoc:
370
+ # @api private
317
371
  def parse(item)
318
372
  Sidekiq.load_json(item)
319
373
  rescue JSON::ParserError
@@ -325,6 +379,8 @@ module Sidekiq
325
379
  {}
326
380
  end
327
381
 
382
+ # This is the job class which Sidekiq will execute. If using ActiveJob,
383
+ # this class will be the ActiveJob adapter class rather than a specific job.
328
384
  def klass
329
385
  self["class"]
330
386
  end
@@ -354,27 +410,31 @@ module Sidekiq
354
410
  def display_args
355
411
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
356
412
  @display_args ||= case klass
357
- when /\ASidekiq::Extensions::Delayed/
358
- safe_load(args[0], args) do |_, _, arg|
359
- arg
360
- end
361
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
362
- job_args = self["wrapped"] ? args[0]["arguments"] : []
363
- if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
364
- # remove MailerClass, mailer_method and 'deliver_now'
365
- job_args.drop(3)
366
- elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
367
- # remove MailerClass, mailer_method and 'deliver_now'
368
- job_args.drop(3).first["args"]
369
- else
370
- job_args
371
- end
372
- else
373
- if self["encrypt"]
374
- # no point in showing 150+ bytes of random garbage
375
- args[-1] = "[encrypted data]"
376
- end
377
- args
413
+ when /\ASidekiq::Extensions::Delayed/
414
+ safe_load(args[0], args) do |_, _, arg, kwarg|
415
+ if !kwarg || kwarg.empty?
416
+ arg
417
+ else
418
+ [arg, kwarg]
419
+ end
420
+ end
421
+ when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
422
+ job_args = self["wrapped"] ? args[0]["arguments"] : []
423
+ if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
424
+ # remove MailerClass, mailer_method and 'deliver_now'
425
+ job_args.drop(3)
426
+ elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
427
+ # remove MailerClass, mailer_method and 'deliver_now'
428
+ job_args.drop(3).first["args"]
429
+ else
430
+ job_args
431
+ end
432
+ else
433
+ if self["encrypt"]
434
+ # no point in showing 150+ bytes of random garbage
435
+ args[-1] = "[encrypted data]"
436
+ end
437
+ args
378
438
  end
379
439
  end
380
440
 
@@ -408,15 +468,12 @@ module Sidekiq
408
468
  end
409
469
  end
410
470
 
411
- attr_reader :queue
412
-
413
471
  def latency
414
472
  now = Time.now.to_f
415
473
  now - (@item["enqueued_at"] || @item["created_at"] || now)
416
474
  end
417
475
 
418
- ##
419
- # Remove this job from the queue.
476
+ # Remove this job from the queue
420
477
  def delete
421
478
  count = Sidekiq.redis { |conn|
422
479
  conn.lrem("queue:#{@queue}", 1, @value)
@@ -424,6 +481,7 @@ module Sidekiq
424
481
  count != 0
425
482
  end
426
483
 
484
+ # Access arbitrary attributes within the job hash
427
485
  def [](name)
428
486
  # nil will happen if the JSON fails to parse.
429
487
  # We don't guarantee Sidekiq will work with bad job JSON but we should
@@ -438,6 +496,7 @@ module Sidekiq
438
496
  rescue => ex
439
497
  # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
440
498
  # memory yet so the YAML can't be loaded.
499
+ # TODO is this still necessary? Zeitwerk reloader should handle?
441
500
  Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
442
501
  default
443
502
  end
@@ -460,20 +519,28 @@ module Sidekiq
460
519
  end
461
520
  end
462
521
 
522
+ # Represents a job within a Redis sorted set where the score
523
+ # represents a timestamp associated with the job. This timestamp
524
+ # could be the scheduled time for it to run (e.g. scheduled set),
525
+ # or the expiration date after which the entry should be deleted (e.g. dead set).
463
526
  class SortedEntry < JobRecord
464
527
  attr_reader :score
465
528
  attr_reader :parent
466
529
 
530
+ # :nodoc:
531
+ # @api private
467
532
  def initialize(parent, score, item)
468
533
  super(item)
469
- @score = score
534
+ @score = Float(score)
470
535
  @parent = parent
471
536
  end
472
537
 
538
+ # The timestamp associated with this entry
473
539
  def at
474
540
  Time.at(score).utc
475
541
  end
476
542
 
543
+ # remove this entry from the sorted set
477
544
  def delete
478
545
  if @value
479
546
  @parent.delete_by_value(@parent.name, @value)
@@ -482,12 +549,17 @@ module Sidekiq
482
549
  end
483
550
  end
484
551
 
552
+ # Change the scheduled time for this job.
553
+ #
554
+ # @param at [Time] the new timestamp for this job
485
555
  def reschedule(at)
486
556
  Sidekiq.redis do |conn|
487
557
  conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
488
558
  end
489
559
  end
490
560
 
561
+ # Enqueue this job from the scheduled or dead set so it will
562
+ # be executed at some point in the near future.
491
563
  def add_to_queue
492
564
  remove_job do |message|
493
565
  msg = Sidekiq.load_json(message)
@@ -495,6 +567,8 @@ module Sidekiq
495
567
  end
496
568
  end
497
569
 
570
+ # enqueue this job from the retry set so it will be executed
571
+ # at some point in the near future.
498
572
  def retry
499
573
  remove_job do |message|
500
574
  msg = Sidekiq.load_json(message)
@@ -503,8 +577,7 @@ module Sidekiq
503
577
  end
504
578
  end
505
579
 
506
- ##
507
- # Place job in the dead set
580
+ # Move this job from its current set into the Dead set.
508
581
  def kill
509
582
  remove_job do |message|
510
583
  DeadSet.new.kill(message)
@@ -519,9 +592,9 @@ module Sidekiq
519
592
 
520
593
  def remove_job
521
594
  Sidekiq.redis do |conn|
522
- results = conn.multi {
523
- conn.zrangebyscore(parent.name, score, score)
524
- conn.zremrangebyscore(parent.name, score, score)
595
+ results = conn.multi { |transaction|
596
+ transaction.zrangebyscore(parent.name, score, score)
597
+ transaction.zremrangebyscore(parent.name, score, score)
525
598
  }.first
526
599
 
527
600
  if results.size == 1
@@ -542,9 +615,9 @@ module Sidekiq
542
615
  yield msg if msg
543
616
 
544
617
  # push the rest back onto the sorted set
545
- conn.multi do
618
+ conn.multi do |transaction|
546
619
  nonmatched.each do |message|
547
- conn.zadd(parent.name, score.to_f.to_s, message)
620
+ transaction.zadd(parent.name, score.to_f.to_s, message)
548
621
  end
549
622
  end
550
623
  end
@@ -552,20 +625,32 @@ module Sidekiq
552
625
  end
553
626
  end
554
627
 
628
+ # Base class for all sorted sets within Sidekiq.
555
629
  class SortedSet
556
630
  include Enumerable
557
631
 
632
+ # Redis key of the set
633
+ # @!attribute [r] Name
558
634
  attr_reader :name
559
635
 
636
+ # :nodoc:
637
+ # @api private
560
638
  def initialize(name)
561
639
  @name = name
562
640
  @_size = size
563
641
  end
564
642
 
643
+ # real-time size of the set, will change
565
644
  def size
566
645
  Sidekiq.redis { |c| c.zcard(name) }
567
646
  end
568
647
 
648
+ # Scan through each element of the sorted set, yielding each to the supplied block.
649
+ # Please see Redis's <a href="https://redis.io/commands/scan/">SCAN documentation</a> for implementation details.
650
+ #
651
+ # @param match [String] a snippet or regexp to filter matches.
652
+ # @param count [Integer] number of elements to retrieve at a time, default 100
653
+ # @yieldparam [Sidekiq::SortedEntry] each entry
569
654
  def scan(match, count = 100)
570
655
  return to_enum(:scan, match, count) unless block_given?
571
656
 
@@ -577,18 +662,32 @@ module Sidekiq
577
662
  end
578
663
  end
579
664
 
665
+ # @return [Boolean] always true
580
666
  def clear
581
667
  Sidekiq.redis do |conn|
582
668
  conn.unlink(name)
583
669
  end
670
+ true
584
671
  end
585
672
  alias_method :💣, :clear
673
+
674
+ # :nodoc:
675
+ # @api private
676
+ def as_json(options = nil)
677
+ {name: name} # 5336
678
+ end
586
679
  end
587
680
 
681
+ # Base class for all sorted sets which contain jobs, e.g. scheduled, retry and dead.
682
+ # Sidekiq Pro and Enterprise add additional sorted sets which do not contain job data,
683
+ # e.g. Batches.
588
684
  class JobSet < SortedSet
589
- def schedule(timestamp, message)
685
+ # Add a job with the associated timestamp to this set.
686
+ # @param timestamp [Time] the score for the job
687
+ # @param job [Hash] the job data
688
+ def schedule(timestamp, job)
590
689
  Sidekiq.redis do |conn|
591
- conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
690
+ conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(job))
592
691
  end
593
692
  end
594
693
 
@@ -602,7 +701,7 @@ module Sidekiq
602
701
  range_start = page * page_size + offset_size
603
702
  range_end = range_start + page_size - 1
604
703
  elements = Sidekiq.redis { |conn|
605
- conn.zrange name, range_start, range_end, with_scores: true
704
+ conn.zrange name, range_start, range_end, withscores: true
606
705
  }
607
706
  break if elements.empty?
608
707
  page -= 1
@@ -616,6 +715,10 @@ module Sidekiq
616
715
  ##
617
716
  # Fetch jobs that match a given time or Range. Job ID is an
618
717
  # optional second argument.
718
+ #
719
+ # @param score [Time,Range] a specific timestamp or range
720
+ # @param jid [String, optional] find a specific JID within the score
721
+ # @return [Array<SortedEntry>] any results found, can be empty
619
722
  def fetch(score, jid = nil)
620
723
  begin_score, end_score =
621
724
  if score.is_a?(Range)
@@ -625,7 +728,7 @@ module Sidekiq
625
728
  end
626
729
 
627
730
  elements = Sidekiq.redis { |conn|
628
- conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
731
+ conn.zrangebyscore(name, begin_score, end_score, withscores: true)
629
732
  }
630
733
 
631
734
  elements.each_with_object([]) do |element, result|
@@ -637,7 +740,10 @@ module Sidekiq
637
740
 
638
741
  ##
639
742
  # Find the job with the given JID within this sorted set.
640
- # This is a slower O(n) operation. Do not use for app logic.
743
+ # *This is a slow O(n) operation*. Do not use for app logic.
744
+ #
745
+ # @param jid [String] the job identifier
746
+ # @return [SortedEntry] the record or nil
641
747
  def find_job(jid)
642
748
  Sidekiq.redis do |conn|
643
749
  conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
@@ -649,6 +755,8 @@ module Sidekiq
649
755
  nil
650
756
  end
651
757
 
758
+ # :nodoc:
759
+ # @api private
652
760
  def delete_by_value(name, value)
653
761
  Sidekiq.redis do |conn|
654
762
  ret = conn.zrem(name, value)
@@ -657,6 +765,8 @@ module Sidekiq
657
765
  end
658
766
  end
659
767
 
768
+ # :nodoc:
769
+ # @api private
660
770
  def delete_by_jid(score, jid)
661
771
  Sidekiq.redis do |conn|
662
772
  elements = conn.zrangebyscore(name, score, score)
@@ -677,10 +787,10 @@ module Sidekiq
677
787
  end
678
788
 
679
789
  ##
680
- # Allows enumeration of scheduled jobs within Sidekiq.
790
+ # The set of scheduled jobs within Sidekiq.
681
791
  # Based on this, you can search/filter for jobs. Here's an
682
- # example where I'm selecting all jobs of a certain type
683
- # and deleting them from the schedule queue.
792
+ # example where I'm selecting jobs based on some complex logic
793
+ # and deleting them from the scheduled set.
684
794
  #
685
795
  # r = Sidekiq::ScheduledSet.new
686
796
  # r.select do |scheduled|
@@ -695,7 +805,7 @@ module Sidekiq
695
805
  end
696
806
 
697
807
  ##
698
- # Allows enumeration of retries within Sidekiq.
808
+ # The set of retries within Sidekiq.
699
809
  # Based on this, you can search/filter for jobs. Here's an
700
810
  # example where I'm selecting all jobs of a certain type
701
811
  # and deleting them from the retry queue.
@@ -711,30 +821,36 @@ module Sidekiq
711
821
  super "retry"
712
822
  end
713
823
 
824
+ # Enqueues all jobs pending within the retry set.
714
825
  def retry_all
715
826
  each(&:retry) while size > 0
716
827
  end
717
828
 
829
+ # Kills all jobs pending within the retry set.
718
830
  def kill_all
719
831
  each(&:kill) while size > 0
720
832
  end
721
833
  end
722
834
 
723
835
  ##
724
- # Allows enumeration of dead jobs within Sidekiq.
836
+ # The set of dead jobs within Sidekiq. Dead jobs have failed all of
837
+ # their retries and are helding in this set pending some sort of manual
838
+ # fix. They will be removed after 6 months (dead_timeout) if not.
725
839
  #
726
840
  class DeadSet < JobSet
727
841
  def initialize
728
842
  super "dead"
729
843
  end
730
844
 
845
+ # Add the given job to the Dead set.
846
+ # @param message [String] the job data as JSON
731
847
  def kill(message, opts = {})
732
848
  now = Time.now.to_f
733
849
  Sidekiq.redis do |conn|
734
- conn.multi do
735
- conn.zadd(name, now.to_s, message)
736
- conn.zremrangebyscore(name, "-inf", now - self.class.timeout)
737
- conn.zremrangebyrank(name, 0, - self.class.max_jobs)
850
+ conn.multi do |transaction|
851
+ transaction.zadd(name, now.to_s, message)
852
+ transaction.zremrangebyscore(name, "-inf", now - self.class.timeout)
853
+ transaction.zremrangebyrank(name, 0, - self.class.max_jobs)
738
854
  end
739
855
  end
740
856
 
@@ -749,16 +865,21 @@ module Sidekiq
749
865
  true
750
866
  end
751
867
 
868
+ # Enqueue all dead jobs
752
869
  def retry_all
753
870
  each(&:retry) while size > 0
754
871
  end
755
872
 
873
+ # The maximum size of the Dead set. Older entries will be trimmed
874
+ # to stay within this limit. Default value is 10,000.
756
875
  def self.max_jobs
757
- Sidekiq.options[:dead_max_jobs]
876
+ Sidekiq[:dead_max_jobs]
758
877
  end
759
878
 
879
+ # The time limit for entries within the Dead set. Older entries will be thrown away.
880
+ # Default value is six months.
760
881
  def self.timeout
761
- Sidekiq.options[:dead_timeout_in_seconds]
882
+ Sidekiq[:dead_timeout_in_seconds]
762
883
  end
763
884
  end
764
885
 
@@ -767,24 +888,31 @@ module Sidekiq
767
888
  # right now. Each process sends a heartbeat to Redis every 5 seconds
768
889
  # so this set should be relatively accurate, barring network partitions.
769
890
  #
770
- # Yields a Sidekiq::Process.
891
+ # @yieldparam [Sidekiq::Process]
771
892
  #
772
893
  class ProcessSet
773
894
  include Enumerable
774
895
 
896
+ # :nodoc:
897
+ # @api private
775
898
  def initialize(clean_plz = true)
776
899
  cleanup if clean_plz
777
900
  end
778
901
 
779
902
  # Cleans up dead processes recorded in Redis.
780
903
  # Returns the number of processes cleaned.
904
+ # :nodoc:
905
+ # @api private
781
906
  def cleanup
907
+ # dont run cleanup more than once per minute
908
+ return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
909
+
782
910
  count = 0
783
911
  Sidekiq.redis do |conn|
784
- procs = conn.sscan_each("processes").to_a.sort
785
- heartbeats = conn.pipelined {
912
+ procs = conn.sscan_each("processes").to_a
913
+ heartbeats = conn.pipelined { |pipeline|
786
914
  procs.each do |key|
787
- conn.hget(key, "info")
915
+ pipeline.hget(key, "info")
788
916
  end
789
917
  }
790
918
 
@@ -806,9 +934,9 @@ module Sidekiq
806
934
  # We're making a tradeoff here between consuming more memory instead of
807
935
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
808
936
  # you'll be happier this way
809
- conn.pipelined do
937
+ conn.pipelined do |pipeline|
810
938
  procs.each do |key|
811
- conn.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
939
+ pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
812
940
  end
813
941
  end
814
942
  }
@@ -832,6 +960,7 @@ module Sidekiq
832
960
  # based on current heartbeat. #each does that and ensures the set only
833
961
  # contains Sidekiq processes which have sent a heartbeat within the last
834
962
  # 60 seconds.
963
+ # @return [Integer] current number of registered Sidekiq processes
835
964
  def size
836
965
  Sidekiq.redis { |conn| conn.scard("processes") }
837
966
  end
@@ -839,10 +968,12 @@ module Sidekiq
839
968
  # Total number of threads available to execute jobs.
840
969
  # For Sidekiq Enterprise customers this number (in production) must be
841
970
  # less than or equal to your licensed concurrency.
971
+ # @return [Integer] the sum of process concurrency
842
972
  def total_concurrency
843
973
  sum { |x| x["concurrency"].to_i }
844
974
  end
845
975
 
976
+ # @return [Integer] total amount of RSS memory consumed by Sidekiq processes
846
977
  def total_rss_in_kb
847
978
  sum { |x| x["rss"].to_i }
848
979
  end
@@ -851,6 +982,8 @@ module Sidekiq
851
982
  # Returns the identity of the current cluster leader or "" if no leader.
852
983
  # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
853
984
  # or Sidekiq Pro.
985
+ # @return [String] Identity of cluster leader
986
+ # @return [String] empty string if no leader
854
987
  def leader
855
988
  @leader ||= begin
856
989
  x = Sidekiq.redis { |c| c.get("dear-leader") }
@@ -877,6 +1010,8 @@ module Sidekiq
877
1010
  # 'identity' => <unique string identifying the process>,
878
1011
  # }
879
1012
  class Process
1013
+ # :nodoc:
1014
+ # @api private
880
1015
  def initialize(hash)
881
1016
  @attribs = hash
882
1017
  end
@@ -901,18 +1036,31 @@ module Sidekiq
901
1036
  self["queues"]
902
1037
  end
903
1038
 
1039
+ # Signal this process to stop processing new jobs.
1040
+ # It will continue to execute jobs it has already fetched.
1041
+ # This method is *asynchronous* and it can take 5-10
1042
+ # seconds for the process to quiet.
904
1043
  def quiet!
905
1044
  signal("TSTP")
906
1045
  end
907
1046
 
1047
+ # Signal this process to shutdown.
1048
+ # It will shutdown within its configured :timeout value, default 25 seconds.
1049
+ # This method is *asynchronous* and it can take 5-10
1050
+ # seconds for the process to start shutting down.
908
1051
  def stop!
909
1052
  signal("TERM")
910
1053
  end
911
1054
 
1055
+ # Signal this process to log backtraces for all threads.
1056
+ # Useful if you have a frozen or deadlocked process which is
1057
+ # still sending a heartbeat.
1058
+ # This method is *asynchronous* and it can take 5-10 seconds.
912
1059
  def dump_threads
913
1060
  signal("TTIN")
914
1061
  end
915
1062
 
1063
+ # @return [Boolean] true if this process is quiet or shutting down
916
1064
  def stopping?
917
1065
  self["quiet"] == "true"
918
1066
  end
@@ -922,9 +1070,9 @@ module Sidekiq
922
1070
  def signal(sig)
923
1071
  key = "#{identity}-signals"
924
1072
  Sidekiq.redis do |c|
925
- c.multi do
926
- c.lpush(key, sig)
927
- c.expire(key, 60)
1073
+ c.multi do |transaction|
1074
+ transaction.lpush(key, sig)
1075
+ transaction.expire(key, 60)
928
1076
  end
929
1077
  end
930
1078
  end
@@ -955,24 +1103,31 @@ module Sidekiq
955
1103
 
956
1104
  def each(&block)
957
1105
  results = []
1106
+ procs = nil
1107
+ all_works = nil
1108
+
958
1109
  Sidekiq.redis do |conn|
959
- procs = conn.sscan_each("processes").to_a
960
- procs.sort.each do |key|
961
- valid, workers = conn.pipelined {
962
- conn.exists?(key)
963
- conn.hgetall("#{key}:workers")
964
- }
965
- next unless valid
966
- workers.each_pair do |tid, json|
967
- hsh = Sidekiq.load_json(json)
968
- p = hsh["payload"]
969
- # avoid breaking API, this is a side effect of the JSON optimization in #4316
970
- hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
971
- results << [key, tid, hsh]
1110
+ procs = conn.sscan_each("processes").to_a.sort
1111
+
1112
+ all_works = conn.pipelined do |pipeline|
1113
+ procs.each do |key|
1114
+ pipeline.hgetall("#{key}:work")
972
1115
  end
973
1116
  end
974
1117
  end
975
1118
 
1119
+ procs.zip(all_works).each do |key, workers|
1120
+ workers.each_pair do |tid, json|
1121
+ next if json.empty?
1122
+
1123
+ hsh = Sidekiq.load_json(json)
1124
+ p = hsh["payload"]
1125
+ # avoid breaking API, this is a side effect of the JSON optimization in #4316
1126
+ hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
1127
+ results << [key, tid, hsh]
1128
+ end
1129
+ end
1130
+
976
1131
  results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
977
1132
  end
978
1133
 
@@ -988,9 +1143,9 @@ module Sidekiq
988
1143
  if procs.empty?
989
1144
  0
990
1145
  else
991
- conn.pipelined {
1146
+ conn.pipelined { |pipeline|
992
1147
  procs.each do |key|
993
- conn.hget(key, "busy")
1148
+ pipeline.hget(key, "busy")
994
1149
  end
995
1150
  }.sum(&:to_i)
996
1151
  end