sidekiq 7.1.4 → 8.0.9

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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +333 -0
  3. data/README.md +16 -13
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +31 -22
  6. data/bin/webload +69 -0
  7. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +121 -0
  8. data/lib/generators/sidekiq/job_generator.rb +2 -0
  9. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  10. data/lib/sidekiq/api.rb +260 -67
  11. data/lib/sidekiq/capsule.rb +17 -8
  12. data/lib/sidekiq/cli.rb +19 -20
  13. data/lib/sidekiq/client.rb +48 -15
  14. data/lib/sidekiq/component.rb +64 -3
  15. data/lib/sidekiq/config.rb +60 -18
  16. data/lib/sidekiq/deploy.rb +4 -2
  17. data/lib/sidekiq/embedded.rb +4 -1
  18. data/lib/sidekiq/fetch.rb +2 -1
  19. data/lib/sidekiq/iterable_job.rb +56 -0
  20. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  21. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  22. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  23. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  24. data/lib/sidekiq/job/iterable.rb +322 -0
  25. data/lib/sidekiq/job.rb +16 -5
  26. data/lib/sidekiq/job_logger.rb +15 -12
  27. data/lib/sidekiq/job_retry.rb +41 -13
  28. data/lib/sidekiq/job_util.rb +7 -1
  29. data/lib/sidekiq/launcher.rb +23 -11
  30. data/lib/sidekiq/loader.rb +57 -0
  31. data/lib/sidekiq/logger.rb +25 -69
  32. data/lib/sidekiq/manager.rb +0 -1
  33. data/lib/sidekiq/metrics/query.rb +76 -45
  34. data/lib/sidekiq/metrics/shared.rb +23 -9
  35. data/lib/sidekiq/metrics/tracking.rb +32 -15
  36. data/lib/sidekiq/middleware/current_attributes.rb +39 -14
  37. data/lib/sidekiq/middleware/i18n.rb +2 -0
  38. data/lib/sidekiq/middleware/modules.rb +2 -0
  39. data/lib/sidekiq/monitor.rb +6 -9
  40. data/lib/sidekiq/paginator.rb +16 -3
  41. data/lib/sidekiq/processor.rb +37 -20
  42. data/lib/sidekiq/profiler.rb +73 -0
  43. data/lib/sidekiq/rails.rb +47 -57
  44. data/lib/sidekiq/redis_client_adapter.rb +25 -8
  45. data/lib/sidekiq/redis_connection.rb +49 -9
  46. data/lib/sidekiq/ring_buffer.rb +3 -0
  47. data/lib/sidekiq/scheduled.rb +2 -2
  48. data/lib/sidekiq/systemd.rb +2 -0
  49. data/lib/sidekiq/testing.rb +34 -15
  50. data/lib/sidekiq/transaction_aware_client.rb +20 -5
  51. data/lib/sidekiq/version.rb +6 -2
  52. data/lib/sidekiq/web/action.rb +149 -64
  53. data/lib/sidekiq/web/application.rb +367 -297
  54. data/lib/sidekiq/web/config.rb +120 -0
  55. data/lib/sidekiq/web/csrf_protection.rb +8 -5
  56. data/lib/sidekiq/web/helpers.rb +146 -64
  57. data/lib/sidekiq/web/router.rb +61 -74
  58. data/lib/sidekiq/web.rb +53 -106
  59. data/lib/sidekiq.rb +11 -4
  60. data/sidekiq.gemspec +6 -5
  61. data/web/assets/images/logo.png +0 -0
  62. data/web/assets/images/status.png +0 -0
  63. data/web/assets/javascripts/application.js +66 -24
  64. data/web/assets/javascripts/base-charts.js +30 -16
  65. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  66. data/web/assets/javascripts/dashboard-charts.js +37 -11
  67. data/web/assets/javascripts/dashboard.js +15 -11
  68. data/web/assets/javascripts/metrics.js +50 -34
  69. data/web/assets/stylesheets/style.css +776 -0
  70. data/web/locales/ar.yml +2 -0
  71. data/web/locales/cs.yml +2 -0
  72. data/web/locales/da.yml +2 -0
  73. data/web/locales/de.yml +2 -0
  74. data/web/locales/el.yml +2 -0
  75. data/web/locales/en.yml +12 -1
  76. data/web/locales/es.yml +25 -2
  77. data/web/locales/fa.yml +2 -0
  78. data/web/locales/fr.yml +2 -1
  79. data/web/locales/gd.yml +2 -1
  80. data/web/locales/he.yml +2 -0
  81. data/web/locales/hi.yml +2 -0
  82. data/web/locales/it.yml +41 -1
  83. data/web/locales/ja.yml +2 -1
  84. data/web/locales/ko.yml +2 -0
  85. data/web/locales/lt.yml +2 -0
  86. data/web/locales/nb.yml +2 -0
  87. data/web/locales/nl.yml +2 -0
  88. data/web/locales/pl.yml +2 -0
  89. data/web/locales/{pt-br.yml → pt-BR.yml} +4 -3
  90. data/web/locales/pt.yml +2 -0
  91. data/web/locales/ru.yml +2 -0
  92. data/web/locales/sv.yml +2 -0
  93. data/web/locales/ta.yml +2 -0
  94. data/web/locales/tr.yml +102 -0
  95. data/web/locales/uk.yml +29 -4
  96. data/web/locales/ur.yml +2 -0
  97. data/web/locales/vi.yml +2 -0
  98. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -74
  99. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -2
  100. data/web/views/_footer.erb +31 -22
  101. data/web/views/_job_info.erb +91 -89
  102. data/web/views/_metrics_period_select.erb +13 -10
  103. data/web/views/_nav.erb +14 -21
  104. data/web/views/_paging.erb +22 -21
  105. data/web/views/_poll_link.erb +2 -2
  106. data/web/views/_summary.erb +23 -23
  107. data/web/views/busy.erb +123 -125
  108. data/web/views/dashboard.erb +71 -82
  109. data/web/views/dead.erb +31 -27
  110. data/web/views/filtering.erb +6 -0
  111. data/web/views/layout.erb +13 -29
  112. data/web/views/metrics.erb +70 -68
  113. data/web/views/metrics_for_job.erb +30 -40
  114. data/web/views/morgue.erb +65 -70
  115. data/web/views/profiles.erb +43 -0
  116. data/web/views/queue.erb +54 -52
  117. data/web/views/queues.erb +43 -37
  118. data/web/views/retries.erb +70 -75
  119. data/web/views/retry.erb +32 -27
  120. data/web/views/scheduled.erb +63 -55
  121. data/web/views/scheduled_job_info.erb +3 -3
  122. metadata +49 -27
  123. data/web/assets/stylesheets/application-dark.css +0 -147
  124. data/web/assets/stylesheets/application-rtl.css +0 -153
  125. data/web/assets/stylesheets/application.css +0 -724
  126. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  127. data/web/assets/stylesheets/bootstrap.css +0 -5
  128. data/web/views/_status.erb +0 -4
data/lib/sidekiq/api.rb CHANGED
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq"
4
-
5
3
  require "zlib"
6
- require "set"
7
- require "base64"
8
4
 
5
+ require "sidekiq"
9
6
  require "sidekiq/metrics/query"
10
7
 
11
8
  #
@@ -102,11 +99,22 @@ module Sidekiq
102
99
  rescue
103
100
  {}
104
101
  end
105
- now = Time.now.to_f
106
- thence = job["enqueued_at"] || now
107
- now - thence
102
+
103
+ enqueued_at = job["enqueued_at"]
104
+ if enqueued_at
105
+ if enqueued_at.is_a?(Float)
106
+ # old format
107
+ now = Time.now.to_f
108
+ now - enqueued_at
109
+ else
110
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
111
+ (now - enqueued_at) / 1000.0
112
+ end
113
+ else
114
+ 0.0
115
+ end
108
116
  else
109
- 0
117
+ 0.0
110
118
  end
111
119
 
112
120
  @stats = {
@@ -266,11 +274,22 @@ module Sidekiq
266
274
  entry = Sidekiq.redis { |conn|
267
275
  conn.lindex(@rname, -1)
268
276
  }
269
- return 0 unless entry
277
+ return 0.0 unless entry
278
+
270
279
  job = Sidekiq.load_json(entry)
271
- now = Time.now.to_f
272
- thence = job["enqueued_at"] || now
273
- now - thence
280
+ enqueued_at = job["enqueued_at"]
281
+ if enqueued_at
282
+ if enqueued_at.is_a?(Float)
283
+ # old format
284
+ now = Time.now.to_f
285
+ now - enqueued_at
286
+ else
287
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
288
+ (now - enqueued_at) / 1000.0
289
+ end
290
+ else
291
+ 0.0
292
+ end
274
293
  end
275
294
 
276
295
  def each
@@ -374,7 +393,7 @@ module Sidekiq
374
393
  def display_class
375
394
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
376
395
  @klass ||= self["display_class"] || begin
377
- if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
396
+ if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
378
397
  job_class = @item["wrapped"] || args[0]
379
398
  if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
380
399
  # MailerClass#mailer_method
@@ -390,7 +409,7 @@ module Sidekiq
390
409
 
391
410
  def display_args
392
411
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
393
- @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
412
+ @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
394
413
  job_args = self["wrapped"] ? deserialize_argument(args[0]["arguments"]) : []
395
414
  if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
396
415
  # remove MailerClass, mailer_method and 'deliver_now'
@@ -422,12 +441,26 @@ module Sidekiq
422
441
  self["bid"]
423
442
  end
424
443
 
444
+ def failed_at
445
+ if self["failed_at"]
446
+ time_from_timestamp(self["failed_at"])
447
+ end
448
+ end
449
+
450
+ def retried_at
451
+ if self["retried_at"]
452
+ time_from_timestamp(self["retried_at"])
453
+ end
454
+ end
455
+
425
456
  def enqueued_at
426
- self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
457
+ if self["enqueued_at"]
458
+ time_from_timestamp(self["enqueued_at"])
459
+ end
427
460
  end
428
461
 
429
462
  def created_at
430
- Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
463
+ time_from_timestamp(self["created_at"] || self["enqueued_at"] || 0)
431
464
  end
432
465
 
433
466
  def tags
@@ -445,8 +478,17 @@ module Sidekiq
445
478
  end
446
479
 
447
480
  def latency
448
- now = Time.now.to_f
449
- now - (@item["enqueued_at"] || @item["created_at"] || now)
481
+ timestamp = @item["enqueued_at"] || @item["created_at"]
482
+ if timestamp
483
+ if timestamp.is_a?(Float)
484
+ # old format
485
+ Time.now.to_f - timestamp
486
+ else
487
+ (::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) - timestamp) / 1000.0
488
+ end
489
+ else
490
+ 0.0
491
+ end
450
492
  end
451
493
 
452
494
  # Remove this job from the queue
@@ -491,10 +533,19 @@ module Sidekiq
491
533
  end
492
534
 
493
535
  def uncompress_backtrace(backtrace)
494
- decoded = Base64.decode64(backtrace)
495
- uncompressed = Zlib::Inflate.inflate(decoded)
536
+ strict_base64_decoded = backtrace.unpack1("m")
537
+ uncompressed = Zlib::Inflate.inflate(strict_base64_decoded)
496
538
  Sidekiq.load_json(uncompressed)
497
539
  end
540
+
541
+ def time_from_timestamp(timestamp)
542
+ if timestamp.is_a?(Float)
543
+ # old format, timestamps were stored as fractional seconds since the epoch
544
+ Time.at(timestamp).utc
545
+ else
546
+ Time.at(timestamp / 1000, timestamp % 1000, :millisecond)
547
+ end
548
+ end
498
549
  end
499
550
 
500
551
  # Represents a job within a Redis sorted set where the score
@@ -669,6 +720,41 @@ module Sidekiq
669
720
  end
670
721
  end
671
722
 
723
+ def pop_each
724
+ Sidekiq.redis do |c|
725
+ size.times do
726
+ data, score = c.zpopmin(name, 1)&.first
727
+ break unless data
728
+ yield data, score
729
+ end
730
+ end
731
+ end
732
+
733
+ def retry_all
734
+ c = Sidekiq::Client.new
735
+ pop_each do |msg, _|
736
+ job = Sidekiq.load_json(msg)
737
+ # Manual retries should not count against the retry limit.
738
+ job["retry_count"] -= 1 if job["retry_count"]
739
+ c.push(job)
740
+ end
741
+ end
742
+
743
+ # Move all jobs from this Set to the Dead Set.
744
+ # See DeadSet#kill
745
+ def kill_all(notify_failure: false, ex: nil)
746
+ ds = DeadSet.new
747
+ opts = {notify_failure: notify_failure, ex: ex, trim: false}
748
+
749
+ begin
750
+ pop_each do |msg, _|
751
+ ds.kill(msg, opts)
752
+ end
753
+ ensure
754
+ ds.trim
755
+ end
756
+ end
757
+
672
758
  def each
673
759
  initial_size = @_size
674
760
  offset_size = 0
@@ -679,7 +765,7 @@ module Sidekiq
679
765
  range_start = page * page_size + offset_size
680
766
  range_end = range_start + page_size - 1
681
767
  elements = Sidekiq.redis { |conn|
682
- conn.zrange name, range_start, range_end, withscores: true
768
+ conn.zrange name, range_start, range_end, "withscores"
683
769
  }
684
770
  break if elements.empty?
685
771
  page -= 1
@@ -706,7 +792,7 @@ module Sidekiq
706
792
  end
707
793
 
708
794
  elements = Sidekiq.redis { |conn|
709
- conn.zrange(name, begin_score, end_score, "BYSCORE", withscores: true)
795
+ conn.zrange(name, begin_score, end_score, "BYSCORE", "withscores")
710
796
  }
711
797
 
712
798
  elements.each_with_object([]) do |element, result|
@@ -766,39 +852,21 @@ module Sidekiq
766
852
 
767
853
  ##
768
854
  # The set of scheduled jobs within Sidekiq.
769
- # Based on this, you can search/filter for jobs. Here's an
770
- # example where I'm selecting jobs based on some complex logic
771
- # and deleting them from the scheduled set.
772
- #
773
855
  # See the API wiki page for usage notes and examples.
774
856
  #
775
857
  class ScheduledSet < JobSet
776
858
  def initialize
777
- super "schedule"
859
+ super("schedule")
778
860
  end
779
861
  end
780
862
 
781
863
  ##
782
864
  # The set of retries within Sidekiq.
783
- # Based on this, you can search/filter for jobs. Here's an
784
- # example where I'm selecting all jobs of a certain type
785
- # and deleting them from the retry queue.
786
- #
787
865
  # See the API wiki page for usage notes and examples.
788
866
  #
789
867
  class RetrySet < JobSet
790
868
  def initialize
791
- super "retry"
792
- end
793
-
794
- # Enqueues all jobs pending within the retry set.
795
- def retry_all
796
- each(&:retry) while size > 0
797
- end
798
-
799
- # Kills all jobs pending within the retry set.
800
- def kill_all
801
- each(&:kill) while size > 0
869
+ super("retry")
802
870
  end
803
871
  end
804
872
 
@@ -809,36 +877,48 @@ module Sidekiq
809
877
  #
810
878
  class DeadSet < JobSet
811
879
  def initialize
812
- super "dead"
880
+ super("dead")
881
+ end
882
+
883
+ # Trim dead jobs which are over our storage limits
884
+ def trim
885
+ hash = Sidekiq.default_configuration
886
+ now = Time.now.to_f
887
+ Sidekiq.redis do |conn|
888
+ conn.multi do |transaction|
889
+ transaction.zremrangebyscore(name, "-inf", now - hash[:dead_timeout_in_seconds])
890
+ transaction.zremrangebyrank(name, 0, - hash[:dead_max_jobs])
891
+ end
892
+ end
813
893
  end
814
894
 
815
895
  # Add the given job to the Dead set.
816
896
  # @param message [String] the job data as JSON
897
+ # @option opts [Boolean] :notify_failure (true) Whether death handlers should be called
898
+ # @option opts [Boolean] :trim (true) Whether Sidekiq should trim the structure to keep it within configuration
899
+ # @option opts [Exception] :ex (RuntimeError) An exception to pass to the death handlers
817
900
  def kill(message, opts = {})
818
901
  now = Time.now.to_f
819
902
  Sidekiq.redis do |conn|
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])
824
- end
903
+ conn.zadd(name, now.to_s, message)
825
904
  end
826
905
 
906
+ trim if opts[:trim] != false
907
+
827
908
  if opts[:notify_failure] != false
828
909
  job = Sidekiq.load_json(message)
829
- r = RuntimeError.new("Job killed by API")
830
- r.set_backtrace(caller)
910
+ if opts[:ex]
911
+ ex = opts[:ex]
912
+ else
913
+ ex = RuntimeError.new("Job killed by API")
914
+ ex.set_backtrace(caller)
915
+ end
831
916
  Sidekiq.default_configuration.death_handlers.each do |handle|
832
- handle.call(job, r)
917
+ handle.call(job, ex)
833
918
  end
834
919
  end
835
920
  true
836
921
  end
837
-
838
- # Enqueue all dead jobs
839
- def retry_all
840
- each(&:retry) while size > 0
841
- end
842
922
  end
843
923
 
844
924
  ##
@@ -881,7 +961,7 @@ module Sidekiq
881
961
  # @api private
882
962
  def cleanup
883
963
  # dont run cleanup more than once per minute
884
- return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
964
+ return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", "NX", "EX", "60") }
885
965
 
886
966
  count = 0
887
967
  Sidekiq.redis do |conn|
@@ -979,9 +1059,9 @@ module Sidekiq
979
1059
  # 'started_at' => <process start time>,
980
1060
  # 'pid' => 12345,
981
1061
  # 'tag' => 'myapp'
982
- # 'concurrency' => 25,
983
- # 'queues' => ['default', 'low'],
984
- # 'busy' => 10,
1062
+ # 'concurrency' => 5,
1063
+ # 'capsules' => {"default" => {"mode" => "weighted", "concurrency" => 5, "weights" => {"default" => 2, "low" => 1}}},
1064
+ # 'busy' => 3,
985
1065
  # 'beat' => <last heartbeat>,
986
1066
  # 'identity' => <unique string identifying the process>,
987
1067
  # 'embedded' => true,
@@ -1009,12 +1089,25 @@ module Sidekiq
1009
1089
  self["identity"]
1010
1090
  end
1011
1091
 
1092
+ # deprecated, use capsules below
1012
1093
  def queues
1013
- self["queues"]
1094
+ capsules.values.flat_map { |x| x["weights"].keys }.uniq
1014
1095
  end
1015
1096
 
1097
+ # deprecated, use capsules below
1016
1098
  def weights
1017
- self["weights"]
1099
+ hash = {}
1100
+ capsules.values.each do |cap|
1101
+ # Note: will lose data if two capsules are processing the same named queue
1102
+ cap["weights"].each_pair do |queue, weight|
1103
+ hash[queue] = weight
1104
+ end
1105
+ end
1106
+ hash
1107
+ end
1108
+
1109
+ def capsules
1110
+ self["capsules"]
1018
1111
  end
1019
1112
 
1020
1113
  def version
@@ -1086,9 +1179,8 @@ module Sidekiq
1086
1179
  # works.each do |process_id, thread_id, work|
1087
1180
  # # process_id is a unique identifier per Sidekiq process
1088
1181
  # # thread_id is a unique identifier per thread
1089
- # # work is a Hash which looks like:
1090
- # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
1091
- # # run_at is an epoch Integer.
1182
+ # # work is a `Sidekiq::Work` instance that has the following accessor methods.
1183
+ # # [work.queue, work.run_at, work.payload]
1092
1184
  # end
1093
1185
  #
1094
1186
  class WorkSet
@@ -1110,11 +1202,11 @@ module Sidekiq
1110
1202
 
1111
1203
  procs.zip(all_works).each do |key, workers|
1112
1204
  workers.each_pair do |tid, json|
1113
- results << [key, tid, Sidekiq.load_json(json)] unless json.empty?
1205
+ results << [key, tid, Sidekiq::Work.new(key, tid, Sidekiq.load_json(json))] unless json.empty?
1114
1206
  end
1115
1207
  end
1116
1208
 
1117
- results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
1209
+ results.sort_by { |(_, _, work)| work.run_at }.each(&block)
1118
1210
  end
1119
1211
 
1120
1212
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -1137,9 +1229,110 @@ module Sidekiq
1137
1229
  end
1138
1230
  end
1139
1231
  end
1232
+
1233
+ ##
1234
+ # Find the work which represents a job with the given JID.
1235
+ # *This is a slow O(n) operation*. Do not use for app logic.
1236
+ #
1237
+ # @param jid [String] the job identifier
1238
+ # @return [Sidekiq::Work] the work or nil
1239
+ def find_work(jid)
1240
+ each do |_process_id, _thread_id, work|
1241
+ job = work.job
1242
+ return work if job.jid == jid
1243
+ end
1244
+ nil
1245
+ end
1246
+ alias_method :find_work_by_jid, :find_work
1247
+ end
1248
+
1249
+ # Sidekiq::Work represents a job which is currently executing.
1250
+ class Work
1251
+ attr_reader :process_id
1252
+ attr_reader :thread_id
1253
+
1254
+ def initialize(pid, tid, hsh)
1255
+ @process_id = pid
1256
+ @thread_id = tid
1257
+ @hsh = hsh
1258
+ @job = nil
1259
+ end
1260
+
1261
+ def queue
1262
+ @hsh["queue"]
1263
+ end
1264
+
1265
+ def run_at
1266
+ Time.at(@hsh["run_at"])
1267
+ end
1268
+
1269
+ def job
1270
+ @job ||= Sidekiq::JobRecord.new(@hsh["payload"])
1271
+ end
1272
+
1273
+ def payload
1274
+ @hsh["payload"]
1275
+ end
1140
1276
  end
1277
+
1141
1278
  # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1142
1279
  # Is "worker" a process, a type of job, a thread? Undefined!
1143
1280
  # WorkSet better describes the data.
1144
1281
  Workers = WorkSet
1282
+
1283
+ class ProfileSet
1284
+ include Enumerable
1285
+
1286
+ # This is a point in time/snapshot API, you'll need to instantiate a new instance
1287
+ # if you want to fetch newer records.
1288
+ def initialize
1289
+ @records = Sidekiq.redis do |c|
1290
+ # This throws away expired profiles
1291
+ c.zremrangebyscore("profiles", "-inf", Time.now.to_f.to_s)
1292
+ # retreive records, newest to oldest
1293
+ c.zrange("profiles", "+inf", 0, "byscore", "rev")
1294
+ end
1295
+ end
1296
+
1297
+ def size
1298
+ @records.size
1299
+ end
1300
+
1301
+ def each(&block)
1302
+ fetch_keys = %w[started_at jid type token size elapsed].freeze
1303
+ arrays = Sidekiq.redis do |c|
1304
+ c.pipelined do |p|
1305
+ @records.each do |key|
1306
+ p.hmget(key, *fetch_keys)
1307
+ end
1308
+ end
1309
+ end
1310
+
1311
+ arrays.compact.map { |arr| ProfileRecord.new(arr) }.each(&block)
1312
+ end
1313
+ end
1314
+
1315
+ class ProfileRecord
1316
+ attr_reader :started_at, :jid, :type, :token, :size, :elapsed
1317
+
1318
+ def initialize(arr)
1319
+ # Must be same order as fetch_keys above
1320
+ @started_at = Time.at(Integer(arr[0]))
1321
+ @jid = arr[1]
1322
+ @type = arr[2]
1323
+ @token = arr[3]
1324
+ @size = Integer(arr[4])
1325
+ @elapsed = Float(arr[5])
1326
+ end
1327
+
1328
+ def key
1329
+ "#{token}-#{jid}"
1330
+ end
1331
+
1332
+ def data
1333
+ Sidekiq.redis { |c| c.hget(key, "data") }
1334
+ end
1335
+ end
1145
1336
  end
1337
+
1338
+ Sidekiq.loader.run_load_hooks(:api)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq/component"
2
4
 
3
5
  module Sidekiq
@@ -9,14 +11,15 @@ module Sidekiq
9
11
  # This capsule will pull jobs from the "single" queue and process
10
12
  # the jobs with one thread, meaning the jobs will be processed serially.
11
13
  #
12
- # Sidekiq.configure_server do |config|
13
- # config.capsule("single-threaded") do |cap|
14
- # cap.concurrency = 1
15
- # cap.queues = %w(single)
14
+ # Sidekiq.configure_server do |config|
15
+ # config.capsule("single-threaded") do |cap|
16
+ # cap.concurrency = 1
17
+ # cap.queues = %w(single)
18
+ # end
16
19
  # end
17
- # end
18
20
  class Capsule
19
21
  include Sidekiq::Component
22
+ extend Forwardable
20
23
 
21
24
  attr_reader :name
22
25
  attr_reader :queues
@@ -24,6 +27,8 @@ module Sidekiq
24
27
  attr_reader :mode
25
28
  attr_reader :weights
26
29
 
30
+ def_delegators :@config, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig, :thread_priority
31
+
27
32
  def initialize(name, config)
28
33
  @name = name
29
34
  @config = config
@@ -33,11 +38,15 @@ module Sidekiq
33
38
  @mode = :strict
34
39
  end
35
40
 
41
+ def to_h
42
+ {concurrency: concurrency, mode: mode, weights: weights}
43
+ end
44
+
36
45
  def fetcher
37
46
  @fetcher ||= begin
38
- inst = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
39
- inst.setup(config[:fetch_setup]) if inst.respond_to?(:setup)
40
- inst
47
+ instance = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
48
+ instance.setup(config[:fetch_setup]) if instance.respond_to?(:setup)
49
+ instance
41
50
  end
42
51
  end
43
52
 
data/lib/sidekiq/cli.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  $stdout.sync = true
4
4
 
5
5
  require "yaml"
6
- require "singleton"
7
6
  require "optparse"
8
7
  require "erb"
9
8
  require "fileutils"
@@ -17,7 +16,6 @@ require "sidekiq/launcher"
17
16
  module Sidekiq # :nodoc:
18
17
  class CLI
19
18
  include Sidekiq::Component
20
- include Singleton unless $TESTING
21
19
 
22
20
  attr_accessor :launcher
23
21
  attr_accessor :environment
@@ -31,6 +29,10 @@ module Sidekiq # :nodoc:
31
29
  validate!
32
30
  end
33
31
 
32
+ def self.instance
33
+ @instance ||= new
34
+ end
35
+
34
36
  def jruby?
35
37
  defined?(::JRUBY_VERSION)
36
38
  end
@@ -38,7 +40,7 @@ module Sidekiq # :nodoc:
38
40
  # Code within this method is not tested because it alters
39
41
  # global process state irreversibly. PRs which improve the
40
42
  # test coverage of Sidekiq::CLI are welcomed.
41
- def run(boot_app: true)
43
+ def run(boot_app: true, warmup: true)
42
44
  boot_application if boot_app
43
45
 
44
46
  if environment == "development" && $stdout.tty? && @config.logger.formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
@@ -74,7 +76,7 @@ module Sidekiq # :nodoc:
74
76
  # fire startup and start multithreading.
75
77
  info = @config.redis_info
76
78
  ver = Gem::Version.new(info["redis_version"])
77
- raise "You are connecting to Redis #{ver}, Sidekiq requires Redis 6.2.0 or greater" if ver < Gem::Version.new("6.2.0")
79
+ raise "You are connected to Redis #{ver}, Sidekiq requires Redis 7.0.0 or greater" if ver < Gem::Version.new("7.0.0")
78
80
 
79
81
  maxmemory_policy = info["maxmemory_policy"]
80
82
  if maxmemory_policy != "noeviction" && maxmemory_policy != ""
@@ -101,6 +103,8 @@ module Sidekiq # :nodoc:
101
103
  # Touch middleware so it isn't lazy loaded by multiple threads, #3043
102
104
  @config.server_middleware
103
105
 
106
+ ::Process.warmup if warmup && ::Process.respond_to?(:warmup) && ENV["RUBY_DISABLE_WARMUP"] != "1"
107
+
104
108
  # Before this point, the process is initializing with just the main thread.
105
109
  # Starting here the process will now have multiple threads running.
106
110
  fire_event(:startup, reverse: false, reraise: true)
@@ -296,29 +300,18 @@ module Sidekiq # :nodoc:
296
300
 
297
301
  if File.directory?(@config[:require])
298
302
  require "rails"
299
- if ::Rails::VERSION::MAJOR < 6
300
- warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 6+"
303
+ if ::Rails::VERSION::MAJOR < 7
304
+ warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 7+"
301
305
  end
302
306
  require "sidekiq/rails"
303
307
  require File.expand_path("#{@config[:require]}/config/environment.rb")
304
- @config[:tag] ||= default_tag
308
+ @config[:tag] ||= default_tag(::Rails.root)
305
309
  else
306
310
  require @config[:require]
311
+ @config[:tag] ||= default_tag
307
312
  end
308
313
  end
309
314
 
310
- def default_tag
311
- dir = ::Rails.root
312
- name = File.basename(dir)
313
- prevdir = File.dirname(dir) # Capistrano release directory?
314
- if name.to_i != 0 && prevdir
315
- if File.basename(prevdir) == "releases"
316
- return File.basename(File.dirname(prevdir))
317
- end
318
- end
319
- name
320
- end
321
-
322
315
  def validate!
323
316
  if !File.exist?(@config[:require]) ||
324
317
  (File.directory?(@config[:require]) && !File.exist?("#{@config[:require]}/config/application.rb"))
@@ -393,7 +386,12 @@ module Sidekiq # :nodoc:
393
386
  end
394
387
 
395
388
  def initialize_logger
396
- @config.logger.level = ::Logger::DEBUG if @config[:verbose]
389
+ if @config[:verbose] || ENV["DEBUG_INVOCATION"] == "1"
390
+ # DEBUG_INVOCATION is a systemd-ism triggered by
391
+ # RestartMode=debug. We turn on debugging when the
392
+ # sidekiq process crashes and is restarted with this flag.
393
+ @config.logger.level = ::Logger::DEBUG
394
+ end
397
395
  end
398
396
 
399
397
  def parse_config(path)
@@ -421,3 +419,4 @@ end
421
419
 
422
420
  require "sidekiq/systemd"
423
421
  require "sidekiq/metrics/tracking"
422
+ require "sidekiq/job/interrupt_handler"