sidekiq 6.4.1 → 6.5.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +107 -1
  3. data/README.md +1 -1
  4. data/bin/sidekiqload +18 -12
  5. data/lib/sidekiq/api.rb +222 -71
  6. data/lib/sidekiq/cli.rb +51 -37
  7. data/lib/sidekiq/client.rb +27 -28
  8. data/lib/sidekiq/component.rb +65 -0
  9. data/lib/sidekiq/delay.rb +1 -1
  10. data/lib/sidekiq/extensions/generic_proxy.rb +1 -1
  11. data/lib/sidekiq/fetch.rb +18 -16
  12. data/lib/sidekiq/job_retry.rb +73 -52
  13. data/lib/sidekiq/job_util.rb +15 -9
  14. data/lib/sidekiq/launcher.rb +37 -33
  15. data/lib/sidekiq/logger.rb +5 -19
  16. data/lib/sidekiq/manager.rb +28 -25
  17. data/lib/sidekiq/metrics/deploy.rb +47 -0
  18. data/lib/sidekiq/metrics/query.rb +153 -0
  19. data/lib/sidekiq/metrics/shared.rb +94 -0
  20. data/lib/sidekiq/metrics/tracking.rb +134 -0
  21. data/lib/sidekiq/middleware/chain.rb +82 -38
  22. data/lib/sidekiq/middleware/current_attributes.rb +18 -12
  23. data/lib/sidekiq/middleware/i18n.rb +6 -4
  24. data/lib/sidekiq/middleware/modules.rb +21 -0
  25. data/lib/sidekiq/monitor.rb +2 -2
  26. data/lib/sidekiq/paginator.rb +11 -3
  27. data/lib/sidekiq/processor.rb +47 -41
  28. data/lib/sidekiq/rails.rb +25 -8
  29. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  30. data/lib/sidekiq/redis_connection.rb +80 -49
  31. data/lib/sidekiq/ring_buffer.rb +29 -0
  32. data/lib/sidekiq/scheduled.rb +53 -24
  33. data/lib/sidekiq/testing/inline.rb +4 -4
  34. data/lib/sidekiq/testing.rb +37 -36
  35. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  36. data/lib/sidekiq/version.rb +1 -1
  37. data/lib/sidekiq/web/action.rb +3 -3
  38. data/lib/sidekiq/web/application.rb +21 -5
  39. data/lib/sidekiq/web/csrf_protection.rb +2 -2
  40. data/lib/sidekiq/web/helpers.rb +20 -7
  41. data/lib/sidekiq/web.rb +5 -1
  42. data/lib/sidekiq/worker.rb +24 -16
  43. data/lib/sidekiq.rb +106 -31
  44. data/sidekiq.gemspec +2 -2
  45. data/web/assets/javascripts/application.js +59 -26
  46. data/web/assets/javascripts/chart.min.js +13 -0
  47. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  48. data/web/assets/javascripts/dashboard.js +0 -17
  49. data/web/assets/javascripts/graph.js +16 -0
  50. data/web/assets/javascripts/metrics.js +262 -0
  51. data/web/assets/stylesheets/application.css +45 -3
  52. data/web/locales/el.yml +43 -19
  53. data/web/locales/en.yml +7 -0
  54. data/web/locales/ja.yml +7 -0
  55. data/web/locales/pt-br.yml +27 -9
  56. data/web/locales/zh-cn.yml +36 -11
  57. data/web/locales/zh-tw.yml +32 -7
  58. data/web/views/_nav.erb +1 -1
  59. data/web/views/_summary.erb +1 -1
  60. data/web/views/busy.erb +9 -4
  61. data/web/views/dashboard.erb +1 -0
  62. data/web/views/metrics.erb +69 -0
  63. data/web/views/metrics_for_job.erb +87 -0
  64. data/web/views/queue.erb +5 -1
  65. metadata +34 -9
  66. data/lib/sidekiq/exception_handler.rb +0 -27
  67. 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,6 +74,7 @@ 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
80
  conn.pipelined do |pipeline|
@@ -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
@@ -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)
@@ -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
328
  conn.multi do |transaction|
291
329
  transaction.unlink(@rname)
292
- transaction.srem("queues", name)
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,31 +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, kwarg|
359
- if !kwarg || kwarg.empty?
360
- arg
361
- else
362
- [arg, kwarg]
363
- end
364
- end
365
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
366
- job_args = self["wrapped"] ? args[0]["arguments"] : []
367
- if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
368
- # remove MailerClass, mailer_method and 'deliver_now'
369
- job_args.drop(3)
370
- elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
371
- # remove MailerClass, mailer_method and 'deliver_now'
372
- job_args.drop(3).first["args"]
373
- else
374
- job_args
375
- end
376
- else
377
- if self["encrypt"]
378
- # no point in showing 150+ bytes of random garbage
379
- args[-1] = "[encrypted data]"
380
- end
381
- 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
382
438
  end
383
439
  end
384
440
 
@@ -412,15 +468,12 @@ module Sidekiq
412
468
  end
413
469
  end
414
470
 
415
- attr_reader :queue
416
-
417
471
  def latency
418
472
  now = Time.now.to_f
419
473
  now - (@item["enqueued_at"] || @item["created_at"] || now)
420
474
  end
421
475
 
422
- ##
423
- # Remove this job from the queue.
476
+ # Remove this job from the queue
424
477
  def delete
425
478
  count = Sidekiq.redis { |conn|
426
479
  conn.lrem("queue:#{@queue}", 1, @value)
@@ -428,6 +481,7 @@ module Sidekiq
428
481
  count != 0
429
482
  end
430
483
 
484
+ # Access arbitrary attributes within the job hash
431
485
  def [](name)
432
486
  # nil will happen if the JSON fails to parse.
433
487
  # We don't guarantee Sidekiq will work with bad job JSON but we should
@@ -442,6 +496,7 @@ module Sidekiq
442
496
  rescue => ex
443
497
  # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
444
498
  # memory yet so the YAML can't be loaded.
499
+ # TODO is this still necessary? Zeitwerk reloader should handle?
445
500
  Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
446
501
  default
447
502
  end
@@ -464,20 +519,28 @@ module Sidekiq
464
519
  end
465
520
  end
466
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).
467
526
  class SortedEntry < JobRecord
468
527
  attr_reader :score
469
528
  attr_reader :parent
470
529
 
530
+ # :nodoc:
531
+ # @api private
471
532
  def initialize(parent, score, item)
472
533
  super(item)
473
- @score = score
534
+ @score = Float(score)
474
535
  @parent = parent
475
536
  end
476
537
 
538
+ # The timestamp associated with this entry
477
539
  def at
478
540
  Time.at(score).utc
479
541
  end
480
542
 
543
+ # remove this entry from the sorted set
481
544
  def delete
482
545
  if @value
483
546
  @parent.delete_by_value(@parent.name, @value)
@@ -486,12 +549,17 @@ module Sidekiq
486
549
  end
487
550
  end
488
551
 
552
+ # Change the scheduled time for this job.
553
+ #
554
+ # @param at [Time] the new timestamp for this job
489
555
  def reschedule(at)
490
556
  Sidekiq.redis do |conn|
491
557
  conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
492
558
  end
493
559
  end
494
560
 
561
+ # Enqueue this job from the scheduled or dead set so it will
562
+ # be executed at some point in the near future.
495
563
  def add_to_queue
496
564
  remove_job do |message|
497
565
  msg = Sidekiq.load_json(message)
@@ -499,6 +567,8 @@ module Sidekiq
499
567
  end
500
568
  end
501
569
 
570
+ # enqueue this job from the retry set so it will be executed
571
+ # at some point in the near future.
502
572
  def retry
503
573
  remove_job do |message|
504
574
  msg = Sidekiq.load_json(message)
@@ -507,8 +577,7 @@ module Sidekiq
507
577
  end
508
578
  end
509
579
 
510
- ##
511
- # Place job in the dead set
580
+ # Move this job from its current set into the Dead set.
512
581
  def kill
513
582
  remove_job do |message|
514
583
  DeadSet.new.kill(message)
@@ -556,20 +625,32 @@ module Sidekiq
556
625
  end
557
626
  end
558
627
 
628
+ # Base class for all sorted sets within Sidekiq.
559
629
  class SortedSet
560
630
  include Enumerable
561
631
 
632
+ # Redis key of the set
633
+ # @!attribute [r] Name
562
634
  attr_reader :name
563
635
 
636
+ # :nodoc:
637
+ # @api private
564
638
  def initialize(name)
565
639
  @name = name
566
640
  @_size = size
567
641
  end
568
642
 
643
+ # real-time size of the set, will change
569
644
  def size
570
645
  Sidekiq.redis { |c| c.zcard(name) }
571
646
  end
572
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
573
654
  def scan(match, count = 100)
574
655
  return to_enum(:scan, match, count) unless block_given?
575
656
 
@@ -581,18 +662,32 @@ module Sidekiq
581
662
  end
582
663
  end
583
664
 
665
+ # @return [Boolean] always true
584
666
  def clear
585
667
  Sidekiq.redis do |conn|
586
668
  conn.unlink(name)
587
669
  end
670
+ true
588
671
  end
589
672
  alias_method :💣, :clear
673
+
674
+ # :nodoc:
675
+ # @api private
676
+ def as_json(options = nil)
677
+ {name: name} # 5336
678
+ end
590
679
  end
591
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.
592
684
  class JobSet < SortedSet
593
- 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)
594
689
  Sidekiq.redis do |conn|
595
- 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))
596
691
  end
597
692
  end
598
693
 
@@ -606,7 +701,7 @@ module Sidekiq
606
701
  range_start = page * page_size + offset_size
607
702
  range_end = range_start + page_size - 1
608
703
  elements = Sidekiq.redis { |conn|
609
- conn.zrange name, range_start, range_end, with_scores: true
704
+ conn.zrange name, range_start, range_end, withscores: true
610
705
  }
611
706
  break if elements.empty?
612
707
  page -= 1
@@ -620,6 +715,10 @@ module Sidekiq
620
715
  ##
621
716
  # Fetch jobs that match a given time or Range. Job ID is an
622
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
623
722
  def fetch(score, jid = nil)
624
723
  begin_score, end_score =
625
724
  if score.is_a?(Range)
@@ -629,7 +728,7 @@ module Sidekiq
629
728
  end
630
729
 
631
730
  elements = Sidekiq.redis { |conn|
632
- conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
731
+ conn.zrangebyscore(name, begin_score, end_score, withscores: true)
633
732
  }
634
733
 
635
734
  elements.each_with_object([]) do |element, result|
@@ -641,7 +740,10 @@ module Sidekiq
641
740
 
642
741
  ##
643
742
  # Find the job with the given JID within this sorted set.
644
- # 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
645
747
  def find_job(jid)
646
748
  Sidekiq.redis do |conn|
647
749
  conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
@@ -653,6 +755,8 @@ module Sidekiq
653
755
  nil
654
756
  end
655
757
 
758
+ # :nodoc:
759
+ # @api private
656
760
  def delete_by_value(name, value)
657
761
  Sidekiq.redis do |conn|
658
762
  ret = conn.zrem(name, value)
@@ -661,6 +765,8 @@ module Sidekiq
661
765
  end
662
766
  end
663
767
 
768
+ # :nodoc:
769
+ # @api private
664
770
  def delete_by_jid(score, jid)
665
771
  Sidekiq.redis do |conn|
666
772
  elements = conn.zrangebyscore(name, score, score)
@@ -681,10 +787,10 @@ module Sidekiq
681
787
  end
682
788
 
683
789
  ##
684
- # Allows enumeration of scheduled jobs within Sidekiq.
790
+ # The set of scheduled jobs within Sidekiq.
685
791
  # Based on this, you can search/filter for jobs. Here's an
686
- # example where I'm selecting all jobs of a certain type
687
- # 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.
688
794
  #
689
795
  # r = Sidekiq::ScheduledSet.new
690
796
  # r.select do |scheduled|
@@ -699,7 +805,7 @@ module Sidekiq
699
805
  end
700
806
 
701
807
  ##
702
- # Allows enumeration of retries within Sidekiq.
808
+ # The set of retries within Sidekiq.
703
809
  # Based on this, you can search/filter for jobs. Here's an
704
810
  # example where I'm selecting all jobs of a certain type
705
811
  # and deleting them from the retry queue.
@@ -715,23 +821,29 @@ module Sidekiq
715
821
  super "retry"
716
822
  end
717
823
 
824
+ # Enqueues all jobs pending within the retry set.
718
825
  def retry_all
719
826
  each(&:retry) while size > 0
720
827
  end
721
828
 
829
+ # Kills all jobs pending within the retry set.
722
830
  def kill_all
723
831
  each(&:kill) while size > 0
724
832
  end
725
833
  end
726
834
 
727
835
  ##
728
- # 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.
729
839
  #
730
840
  class DeadSet < JobSet
731
841
  def initialize
732
842
  super "dead"
733
843
  end
734
844
 
845
+ # Add the given job to the Dead set.
846
+ # @param message [String] the job data as JSON
735
847
  def kill(message, opts = {})
736
848
  now = Time.now.to_f
737
849
  Sidekiq.redis do |conn|
@@ -753,16 +865,21 @@ module Sidekiq
753
865
  true
754
866
  end
755
867
 
868
+ # Enqueue all dead jobs
756
869
  def retry_all
757
870
  each(&:retry) while size > 0
758
871
  end
759
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.
760
875
  def self.max_jobs
761
- Sidekiq.options[:dead_max_jobs]
876
+ Sidekiq[:dead_max_jobs]
762
877
  end
763
878
 
879
+ # The time limit for entries within the Dead set. Older entries will be thrown away.
880
+ # Default value is six months.
764
881
  def self.timeout
765
- Sidekiq.options[:dead_timeout_in_seconds]
882
+ Sidekiq[:dead_timeout_in_seconds]
766
883
  end
767
884
  end
768
885
 
@@ -771,21 +888,28 @@ module Sidekiq
771
888
  # right now. Each process sends a heartbeat to Redis every 5 seconds
772
889
  # so this set should be relatively accurate, barring network partitions.
773
890
  #
774
- # Yields a Sidekiq::Process.
891
+ # @yieldparam [Sidekiq::Process]
775
892
  #
776
893
  class ProcessSet
777
894
  include Enumerable
778
895
 
896
+ # :nodoc:
897
+ # @api private
779
898
  def initialize(clean_plz = true)
780
899
  cleanup if clean_plz
781
900
  end
782
901
 
783
902
  # Cleans up dead processes recorded in Redis.
784
903
  # Returns the number of processes cleaned.
904
+ # :nodoc:
905
+ # @api private
785
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
+
786
910
  count = 0
787
911
  Sidekiq.redis do |conn|
788
- procs = conn.sscan_each("processes").to_a.sort
912
+ procs = conn.sscan_each("processes").to_a
789
913
  heartbeats = conn.pipelined { |pipeline|
790
914
  procs.each do |key|
791
915
  pipeline.hget(key, "info")
@@ -836,6 +960,7 @@ module Sidekiq
836
960
  # based on current heartbeat. #each does that and ensures the set only
837
961
  # contains Sidekiq processes which have sent a heartbeat within the last
838
962
  # 60 seconds.
963
+ # @return [Integer] current number of registered Sidekiq processes
839
964
  def size
840
965
  Sidekiq.redis { |conn| conn.scard("processes") }
841
966
  end
@@ -843,10 +968,12 @@ module Sidekiq
843
968
  # Total number of threads available to execute jobs.
844
969
  # For Sidekiq Enterprise customers this number (in production) must be
845
970
  # less than or equal to your licensed concurrency.
971
+ # @return [Integer] the sum of process concurrency
846
972
  def total_concurrency
847
973
  sum { |x| x["concurrency"].to_i }
848
974
  end
849
975
 
976
+ # @return [Integer] total amount of RSS memory consumed by Sidekiq processes
850
977
  def total_rss_in_kb
851
978
  sum { |x| x["rss"].to_i }
852
979
  end
@@ -855,6 +982,8 @@ module Sidekiq
855
982
  # Returns the identity of the current cluster leader or "" if no leader.
856
983
  # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
857
984
  # or Sidekiq Pro.
985
+ # @return [String] Identity of cluster leader
986
+ # @return [String] empty string if no leader
858
987
  def leader
859
988
  @leader ||= begin
860
989
  x = Sidekiq.redis { |c| c.get("dear-leader") }
@@ -881,6 +1010,8 @@ module Sidekiq
881
1010
  # 'identity' => <unique string identifying the process>,
882
1011
  # }
883
1012
  class Process
1013
+ # :nodoc:
1014
+ # @api private
884
1015
  def initialize(hash)
885
1016
  @attribs = hash
886
1017
  end
@@ -905,18 +1036,31 @@ module Sidekiq
905
1036
  self["queues"]
906
1037
  end
907
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.
908
1043
  def quiet!
909
1044
  signal("TSTP")
910
1045
  end
911
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.
912
1051
  def stop!
913
1052
  signal("TERM")
914
1053
  end
915
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.
916
1059
  def dump_threads
917
1060
  signal("TTIN")
918
1061
  end
919
1062
 
1063
+ # @return [Boolean] true if this process is quiet or shutting down
920
1064
  def stopping?
921
1065
  self["quiet"] == "true"
922
1066
  end
@@ -959,24 +1103,31 @@ module Sidekiq
959
1103
 
960
1104
  def each(&block)
961
1105
  results = []
1106
+ procs = nil
1107
+ all_works = nil
1108
+
962
1109
  Sidekiq.redis do |conn|
963
- procs = conn.sscan_each("processes").to_a
964
- procs.sort.each do |key|
965
- valid, workers = conn.pipelined { |pipeline|
966
- pipeline.exists?(key)
967
- pipeline.hgetall("#{key}:workers")
968
- }
969
- next unless valid
970
- workers.each_pair do |tid, json|
971
- hsh = Sidekiq.load_json(json)
972
- p = hsh["payload"]
973
- # avoid breaking API, this is a side effect of the JSON optimization in #4316
974
- hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
975
- results << [key, tid, hsh]
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")
976
1115
  end
977
1116
  end
978
1117
  end
979
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
+
980
1131
  results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
981
1132
  end
982
1133