sidekiq 7.3.0 → 8.0.5

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +158 -0
  3. data/README.md +16 -13
  4. data/bin/sidekiqload +31 -22
  5. data/bin/webload +69 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +120 -0
  7. data/lib/generators/sidekiq/job_generator.rb +2 -0
  8. data/lib/sidekiq/api.rb +184 -71
  9. data/lib/sidekiq/capsule.rb +11 -9
  10. data/lib/sidekiq/cli.rb +16 -20
  11. data/lib/sidekiq/client.rb +28 -11
  12. data/lib/sidekiq/component.rb +62 -2
  13. data/lib/sidekiq/config.rb +42 -18
  14. data/lib/sidekiq/deploy.rb +2 -0
  15. data/lib/sidekiq/embedded.rb +4 -1
  16. data/lib/sidekiq/iterable_job.rb +3 -0
  17. data/lib/sidekiq/job/interrupt_handler.rb +2 -0
  18. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +3 -3
  19. data/lib/sidekiq/job/iterable.rb +82 -7
  20. data/lib/sidekiq/job_logger.rb +15 -27
  21. data/lib/sidekiq/job_retry.rb +17 -5
  22. data/lib/sidekiq/job_util.rb +7 -1
  23. data/lib/sidekiq/launcher.rb +3 -2
  24. data/lib/sidekiq/logger.rb +19 -70
  25. data/lib/sidekiq/manager.rb +0 -1
  26. data/lib/sidekiq/metrics/query.rb +73 -45
  27. data/lib/sidekiq/metrics/shared.rb +23 -9
  28. data/lib/sidekiq/metrics/tracking.rb +22 -12
  29. data/lib/sidekiq/middleware/current_attributes.rb +12 -4
  30. data/lib/sidekiq/middleware/modules.rb +2 -0
  31. data/lib/sidekiq/monitor.rb +2 -1
  32. data/lib/sidekiq/paginator.rb +14 -1
  33. data/lib/sidekiq/processor.rb +26 -19
  34. data/lib/sidekiq/profiler.rb +72 -0
  35. data/lib/sidekiq/rails.rb +44 -55
  36. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  37. data/lib/sidekiq/redis_connection.rb +22 -4
  38. data/lib/sidekiq/ring_buffer.rb +2 -0
  39. data/lib/sidekiq/systemd.rb +2 -0
  40. data/lib/sidekiq/testing.rb +7 -7
  41. data/lib/sidekiq/version.rb +6 -2
  42. data/lib/sidekiq/web/action.rb +124 -69
  43. data/lib/sidekiq/web/application.rb +355 -377
  44. data/lib/sidekiq/web/config.rb +120 -0
  45. data/lib/sidekiq/web/helpers.rb +64 -33
  46. data/lib/sidekiq/web/router.rb +61 -74
  47. data/lib/sidekiq/web.rb +52 -150
  48. data/lib/sidekiq.rb +5 -4
  49. data/sidekiq.gemspec +6 -6
  50. data/web/assets/javascripts/application.js +6 -13
  51. data/web/assets/javascripts/base-charts.js +30 -16
  52. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  53. data/web/assets/javascripts/dashboard-charts.js +2 -0
  54. data/web/assets/javascripts/dashboard.js +7 -1
  55. data/web/assets/javascripts/metrics.js +16 -34
  56. data/web/assets/stylesheets/style.css +766 -0
  57. data/web/locales/ar.yml +1 -0
  58. data/web/locales/cs.yml +1 -0
  59. data/web/locales/da.yml +1 -0
  60. data/web/locales/de.yml +1 -0
  61. data/web/locales/el.yml +1 -0
  62. data/web/locales/en.yml +9 -1
  63. data/web/locales/es.yml +24 -2
  64. data/web/locales/fa.yml +1 -0
  65. data/web/locales/fr.yml +1 -1
  66. data/web/locales/gd.yml +1 -1
  67. data/web/locales/he.yml +1 -0
  68. data/web/locales/hi.yml +1 -0
  69. data/web/locales/it.yml +40 -1
  70. data/web/locales/ja.yml +1 -1
  71. data/web/locales/ko.yml +1 -0
  72. data/web/locales/lt.yml +1 -0
  73. data/web/locales/nb.yml +1 -0
  74. data/web/locales/nl.yml +1 -0
  75. data/web/locales/pl.yml +1 -0
  76. data/web/locales/{pt-br.yml → pt-BR.yml} +3 -3
  77. data/web/locales/pt.yml +1 -0
  78. data/web/locales/ru.yml +1 -0
  79. data/web/locales/sv.yml +1 -0
  80. data/web/locales/ta.yml +1 -0
  81. data/web/locales/tr.yml +2 -2
  82. data/web/locales/uk.yml +25 -1
  83. data/web/locales/ur.yml +1 -0
  84. data/web/locales/vi.yml +1 -0
  85. data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -74
  86. data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -2
  87. data/web/views/_footer.erb +31 -34
  88. data/web/views/_job_info.erb +91 -89
  89. data/web/views/_metrics_period_select.erb +13 -10
  90. data/web/views/_nav.erb +14 -21
  91. data/web/views/_paging.erb +23 -21
  92. data/web/views/_poll_link.erb +2 -2
  93. data/web/views/_summary.erb +16 -16
  94. data/web/views/busy.erb +124 -122
  95. data/web/views/dashboard.erb +63 -64
  96. data/web/views/dead.erb +31 -27
  97. data/web/views/filtering.erb +3 -4
  98. data/web/views/layout.erb +13 -29
  99. data/web/views/metrics.erb +75 -82
  100. data/web/views/metrics_for_job.erb +45 -46
  101. data/web/views/morgue.erb +61 -70
  102. data/web/views/profiles.erb +43 -0
  103. data/web/views/queue.erb +54 -52
  104. data/web/views/queues.erb +43 -41
  105. data/web/views/retries.erb +66 -75
  106. data/web/views/retry.erb +32 -27
  107. data/web/views/scheduled.erb +59 -55
  108. data/web/views/scheduled_job_info.erb +1 -1
  109. metadata +27 -29
  110. data/web/assets/stylesheets/application-dark.css +0 -147
  111. data/web/assets/stylesheets/application-rtl.css +0 -163
  112. data/web/assets/stylesheets/application.css +0 -758
  113. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  114. data/web/assets/stylesheets/bootstrap.css +0 -5
  115. data/web/views/_status.erb +0 -4
data/lib/sidekiq/api.rb CHANGED
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq"
4
-
5
3
  require "zlib"
6
- require "set"
7
4
 
5
+ require "sidekiq"
8
6
  require "sidekiq/metrics/query"
9
7
 
10
8
  #
@@ -101,11 +99,22 @@ module Sidekiq
101
99
  rescue
102
100
  {}
103
101
  end
104
- now = Time.now.to_f
105
- thence = job["enqueued_at"] || now
106
- 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
107
116
  else
108
- 0
117
+ 0.0
109
118
  end
110
119
 
111
120
  @stats = {
@@ -265,11 +274,22 @@ module Sidekiq
265
274
  entry = Sidekiq.redis { |conn|
266
275
  conn.lindex(@rname, -1)
267
276
  }
268
- return 0 unless entry
277
+ return 0.0 unless entry
278
+
269
279
  job = Sidekiq.load_json(entry)
270
- now = Time.now.to_f
271
- thence = job["enqueued_at"] || now
272
- 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
273
293
  end
274
294
 
275
295
  def each
@@ -373,7 +393,7 @@ module Sidekiq
373
393
  def display_class
374
394
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
375
395
  @klass ||= self["display_class"] || begin
376
- if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
396
+ if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
377
397
  job_class = @item["wrapped"] || args[0]
378
398
  if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
379
399
  # MailerClass#mailer_method
@@ -389,7 +409,7 @@ module Sidekiq
389
409
 
390
410
  def display_args
391
411
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
392
- @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
412
+ @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
393
413
  job_args = self["wrapped"] ? deserialize_argument(args[0]["arguments"]) : []
394
414
  if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
395
415
  # remove MailerClass, mailer_method and 'deliver_now'
@@ -421,12 +441,26 @@ module Sidekiq
421
441
  self["bid"]
422
442
  end
423
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
+
424
456
  def enqueued_at
425
- self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
457
+ if self["enqueued_at"]
458
+ time_from_timestamp(self["enqueued_at"])
459
+ end
426
460
  end
427
461
 
428
462
  def created_at
429
- Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
463
+ time_from_timestamp(self["created_at"] || self["enqueued_at"] || 0)
430
464
  end
431
465
 
432
466
  def tags
@@ -444,8 +478,17 @@ module Sidekiq
444
478
  end
445
479
 
446
480
  def latency
447
- now = Time.now.to_f
448
- 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
449
492
  end
450
493
 
451
494
  # Remove this job from the queue
@@ -494,6 +537,15 @@ module Sidekiq
494
537
  uncompressed = Zlib::Inflate.inflate(strict_base64_decoded)
495
538
  Sidekiq.load_json(uncompressed)
496
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
497
549
  end
498
550
 
499
551
  # Represents a job within a Redis sorted set where the score
@@ -668,6 +720,41 @@ module Sidekiq
668
720
  end
669
721
  end
670
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
+
671
758
  def each
672
759
  initial_size = @_size
673
760
  offset_size = 0
@@ -765,10 +852,6 @@ module Sidekiq
765
852
 
766
853
  ##
767
854
  # The set of scheduled jobs within Sidekiq.
768
- # Based on this, you can search/filter for jobs. Here's an
769
- # example where I'm selecting jobs based on some complex logic
770
- # and deleting them from the scheduled set.
771
- #
772
855
  # See the API wiki page for usage notes and examples.
773
856
  #
774
857
  class ScheduledSet < JobSet
@@ -779,26 +862,12 @@ module Sidekiq
779
862
 
780
863
  ##
781
864
  # The set of retries within Sidekiq.
782
- # Based on this, you can search/filter for jobs. Here's an
783
- # example where I'm selecting all jobs of a certain type
784
- # and deleting them from the retry queue.
785
- #
786
865
  # See the API wiki page for usage notes and examples.
787
866
  #
788
867
  class RetrySet < JobSet
789
868
  def initialize
790
869
  super("retry")
791
870
  end
792
-
793
- # Enqueues all jobs pending within the retry set.
794
- def retry_all
795
- each(&:retry) while size > 0
796
- end
797
-
798
- # Kills all jobs pending within the retry set.
799
- def kill_all
800
- each(&:kill) while size > 0
801
- end
802
871
  end
803
872
 
804
873
  ##
@@ -811,33 +880,45 @@ module Sidekiq
811
880
  super("dead")
812
881
  end
813
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
893
+ end
894
+
814
895
  # Add the given job to the Dead set.
815
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
816
900
  def kill(message, opts = {})
817
901
  now = Time.now.to_f
818
902
  Sidekiq.redis do |conn|
819
- conn.multi do |transaction|
820
- transaction.zadd(name, now.to_s, message)
821
- transaction.zremrangebyscore(name, "-inf", now - Sidekiq::Config::DEFAULTS[:dead_timeout_in_seconds])
822
- transaction.zremrangebyrank(name, 0, - Sidekiq::Config::DEFAULTS[:dead_max_jobs])
823
- end
903
+ conn.zadd(name, now.to_s, message)
824
904
  end
825
905
 
906
+ trim if opts[:trim] != false
907
+
826
908
  if opts[:notify_failure] != false
827
909
  job = Sidekiq.load_json(message)
828
- r = RuntimeError.new("Job killed by API")
829
- 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
830
916
  Sidekiq.default_configuration.death_handlers.each do |handle|
831
- handle.call(job, r)
917
+ handle.call(job, ex)
832
918
  end
833
919
  end
834
920
  true
835
921
  end
836
-
837
- # Enqueue all dead jobs
838
- def retry_all
839
- each(&:retry) while size > 0
840
- end
841
922
  end
842
923
 
843
924
  ##
@@ -1085,8 +1166,8 @@ module Sidekiq
1085
1166
  # works.each do |process_id, thread_id, work|
1086
1167
  # # process_id is a unique identifier per Sidekiq process
1087
1168
  # # thread_id is a unique identifier per thread
1088
- # # work is a Hash which looks like:
1089
- # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
1169
+ # # work is a `Sidekiq::Work` instance that has the following accessor methods.
1170
+ # # [work.queue, work.run_at, work.payload]
1090
1171
  # # run_at is an epoch Integer.
1091
1172
  # end
1092
1173
  #
@@ -1113,7 +1194,7 @@ module Sidekiq
1113
1194
  end
1114
1195
  end
1115
1196
 
1116
- results.sort_by { |(_, _, hsh)| hsh.raw("run_at") }.each(&block)
1197
+ results.sort_by { |(_, _, work)| work.run_at }.each(&block)
1117
1198
  end
1118
1199
 
1119
1200
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -1143,13 +1224,14 @@ module Sidekiq
1143
1224
  #
1144
1225
  # @param jid [String] the job identifier
1145
1226
  # @return [Sidekiq::Work] the work or nil
1146
- def find_work_by_jid(jid)
1227
+ def find_work(jid)
1147
1228
  each do |_process_id, _thread_id, work|
1148
1229
  job = work.job
1149
1230
  return work if job.jid == jid
1150
1231
  end
1151
1232
  nil
1152
1233
  end
1234
+ alias_method :find_work_by_jid, :find_work
1153
1235
  end
1154
1236
 
1155
1237
  # Sidekiq::Work represents a job which is currently executing.
@@ -1179,33 +1261,64 @@ module Sidekiq
1179
1261
  def payload
1180
1262
  @hsh["payload"]
1181
1263
  end
1264
+ end
1182
1265
 
1183
- # deprecated
1184
- def [](key)
1185
- kwargs = {uplevel: 1}
1186
- kwargs[:category] = :deprecated if RUBY_VERSION > "3.0" # TODO
1187
- warn("Direct access to `Sidekiq::Work` attributes is deprecated, please use `#payload`, `#queue`, `#run_at` or `#job` instead", **kwargs)
1266
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1267
+ # Is "worker" a process, a type of job, a thread? Undefined!
1268
+ # WorkSet better describes the data.
1269
+ Workers = WorkSet
1188
1270
 
1189
- @hsh[key]
1190
- end
1271
+ class ProfileSet
1272
+ include Enumerable
1191
1273
 
1192
- # :nodoc:
1193
- # @api private
1194
- def raw(name)
1195
- @hsh[name]
1274
+ # This is a point in time/snapshot API, you'll need to instantiate a new instance
1275
+ # if you want to fetch newer records.
1276
+ def initialize
1277
+ @records = Sidekiq.redis do |c|
1278
+ # This throws away expired profiles
1279
+ c.zremrangebyscore("profiles", "-inf", Time.now.to_f.to_s)
1280
+ # retreive records, newest to oldest
1281
+ c.zrange("profiles", "+inf", 0, "byscore", "rev")
1282
+ end
1196
1283
  end
1197
1284
 
1198
- def method_missing(*all)
1199
- @hsh.send(*all)
1285
+ def size
1286
+ @records.size
1200
1287
  end
1201
1288
 
1202
- def respond_to_missing?(name, *args)
1203
- @hsh.respond_to?(name)
1289
+ def each(&block)
1290
+ fetch_keys = %w[started_at jid type token size elapsed].freeze
1291
+ arrays = Sidekiq.redis do |c|
1292
+ c.pipelined do |p|
1293
+ @records.each do |key|
1294
+ p.hmget(key, *fetch_keys)
1295
+ end
1296
+ end
1297
+ end
1298
+
1299
+ arrays.compact.map { |arr| ProfileRecord.new(arr) }.each(&block)
1204
1300
  end
1205
1301
  end
1206
1302
 
1207
- # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1208
- # Is "worker" a process, a type of job, a thread? Undefined!
1209
- # WorkSet better describes the data.
1210
- Workers = WorkSet
1303
+ class ProfileRecord
1304
+ attr_reader :started_at, :jid, :type, :token, :size, :elapsed
1305
+
1306
+ def initialize(arr)
1307
+ # Must be same order as fetch_keys above
1308
+ @started_at = Time.at(Integer(arr[0]))
1309
+ @jid = arr[1]
1310
+ @type = arr[2]
1311
+ @token = arr[3]
1312
+ @size = Integer(arr[4])
1313
+ @elapsed = Float(arr[5])
1314
+ end
1315
+
1316
+ def key
1317
+ "#{token}-#{jid}"
1318
+ end
1319
+
1320
+ def data
1321
+ Sidekiq.redis { |c| c.hget(key, "data") }
1322
+ end
1323
+ end
1211
1324
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq/component"
2
4
 
3
5
  module Sidekiq
@@ -9,12 +11,12 @@ 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
20
22
  extend Forwardable
@@ -25,7 +27,7 @@ module Sidekiq
25
27
  attr_reader :mode
26
28
  attr_reader :weights
27
29
 
28
- def_delegators :@config, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
30
+ def_delegators :@config, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig, :thread_priority
29
31
 
30
32
  def initialize(name, config)
31
33
  @name = name
@@ -38,9 +40,9 @@ module Sidekiq
38
40
 
39
41
  def fetcher
40
42
  @fetcher ||= begin
41
- inst = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
42
- inst.setup(config[:fetch_setup]) if inst.respond_to?(:setup)
43
- inst
43
+ instance = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
44
+ instance.setup(config[:fetch_setup]) if instance.respond_to?(:setup)
45
+ instance
44
46
  end
45
47
  end
46
48
 
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
@@ -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,7 +103,7 @@ module Sidekiq # :nodoc:
101
103
  # Touch middleware so it isn't lazy loaded by multiple threads, #3043
102
104
  @config.server_middleware
103
105
 
104
- ::Process.warmup if warmup && ::Process.respond_to?(:warmup)
106
+ ::Process.warmup if warmup && ::Process.respond_to?(:warmup) && ENV["RUBY_DISABLE_WARMUP"] != "1"
105
107
 
106
108
  # Before this point, the process is initializing with just the main thread.
107
109
  # Starting here the process will now have multiple threads running.
@@ -298,29 +300,18 @@ module Sidekiq # :nodoc:
298
300
 
299
301
  if File.directory?(@config[:require])
300
302
  require "rails"
301
- if ::Rails::VERSION::MAJOR < 6
302
- warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 6+"
303
+ if ::Rails::VERSION::MAJOR < 7
304
+ warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 7+"
303
305
  end
304
306
  require "sidekiq/rails"
305
307
  require File.expand_path("#{@config[:require]}/config/environment.rb")
306
- @config[:tag] ||= default_tag
308
+ @config[:tag] ||= default_tag(::Rails.root)
307
309
  else
308
310
  require @config[:require]
311
+ @config[:tag] ||= default_tag
309
312
  end
310
313
  end
311
314
 
312
- def default_tag
313
- dir = ::Rails.root
314
- name = File.basename(dir)
315
- prevdir = File.dirname(dir) # Capistrano release directory?
316
- if name.to_i != 0 && prevdir
317
- if File.basename(prevdir) == "releases"
318
- return File.basename(File.dirname(prevdir))
319
- end
320
- end
321
- name
322
- end
323
-
324
315
  def validate!
325
316
  if !File.exist?(@config[:require]) ||
326
317
  (File.directory?(@config[:require]) && !File.exist?("#{@config[:require]}/config/application.rb"))
@@ -395,7 +386,12 @@ module Sidekiq # :nodoc:
395
386
  end
396
387
 
397
388
  def initialize_logger
398
- @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
399
395
  end
400
396
 
401
397
  def parse_config(path)
@@ -58,6 +58,21 @@ module Sidekiq
58
58
  end
59
59
  end
60
60
 
61
+ # Cancel the IterableJob with the given JID.
62
+ # **NB: Cancellation is asynchronous.** Iteration checks every
63
+ # five seconds so this will not immediately stop the given job.
64
+ def cancel!(jid)
65
+ key = "it-#{jid}"
66
+ _, result, _ = Sidekiq.redis do |c|
67
+ c.pipelined do |p|
68
+ p.hsetnx(key, "cancelled", Time.now.to_i)
69
+ p.hget(key, "cancelled")
70
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
71
+ end
72
+ end
73
+ result.to_i
74
+ end
75
+
61
76
  ##
62
77
  # The main method used to push a job to Redis. Accepts a number of options:
63
78
  #
@@ -249,19 +264,21 @@ module Sidekiq
249
264
  if payloads.first.key?("at")
250
265
  conn.zadd("schedule", payloads.flat_map { |hash|
251
266
  at = hash["at"].to_s
252
- # ActiveJob sets this but the job has not been enqueued yet
253
- hash.delete("enqueued_at")
254
- [at, Sidekiq.dump_json(hash.except("at"))]
267
+ # ActiveJob sets enqueued_at but the job has not been enqueued yet
268
+ hash = hash.except("enqueued_at", "at")
269
+ [at, Sidekiq.dump_json(hash)]
255
270
  })
256
271
  else
257
- queue = payloads.first["queue"]
258
- now = Time.now.to_f
259
- to_push = payloads.map { |entry|
260
- entry["enqueued_at"] = now
261
- Sidekiq.dump_json(entry)
262
- }
263
- conn.sadd("queues", [queue])
264
- conn.lpush("queue:#{queue}", to_push)
272
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) # milliseconds since the epoch
273
+ grouped_queues = payloads.group_by { |job| job["queue"] }
274
+ conn.sadd("queues", grouped_queues.keys)
275
+ grouped_queues.each do |queue, grouped_payloads|
276
+ to_push = grouped_payloads.map { |entry|
277
+ entry["enqueued_at"] = now
278
+ Sidekiq.dump_json(entry)
279
+ }
280
+ conn.lpush("queue:#{queue}", to_push)
281
+ end
265
282
  end
266
283
  end
267
284
  end
@@ -1,11 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
+ # Ruby's default thread priority is 0, which uses 100ms time slices.
5
+ # This can lead to some surprising thread starvation; if using a lot of
6
+ # CPU-heavy concurrency, it may take several seconds before a Thread gets
7
+ # on the CPU.
8
+ #
9
+ # Negative priorities lower the timeslice by half, so -1 = 50ms, -2 = 25ms, etc.
10
+ # With more frequent timeslices, we reduce the risk of unintentional timeouts
11
+ # and starvation.
12
+ #
13
+ # Customize like so:
14
+ #
15
+ # Sidekiq.configure_server do |cfg|
16
+ # cfg.thread_priority = 0
17
+ # end
18
+ #
19
+ DEFAULT_THREAD_PRIORITY = -1
20
+
4
21
  ##
5
22
  # Sidekiq::Component assumes a config instance is available at @config
6
23
  module Component # :nodoc:
7
24
  attr_reader :config
8
25
 
26
+ # This is epoch milliseconds, appropriate for persistence
27
+ def real_ms
28
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
29
+ end
30
+
31
+ # used for time difference and relative comparisons, not persistence.
32
+ def mono_ms
33
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
34
+ end
35
+
9
36
  def watchdog(last_words)
10
37
  yield
11
38
  rescue Exception => ex
@@ -13,11 +40,11 @@ module Sidekiq
13
40
  raise ex
14
41
  end
15
42
 
16
- def safe_thread(name, &block)
43
+ def safe_thread(name, priority: nil, &block)
17
44
  Thread.new do
18
45
  Thread.current.name = "sidekiq.#{name}"
19
46
  watchdog(name, &block)
20
- end
47
+ end.tap { |t| t.priority = (priority || config.thread_priority || DEFAULT_THREAD_PRIORITY) }
21
48
  end
22
49
 
23
50
  def logger
@@ -64,5 +91,38 @@ module Sidekiq
64
91
  end
65
92
  arr.clear if oneshot # once we've fired an event, we never fire it again
66
93
  end
94
+
95
+ # When you have a large tree of components, the `inspect` output
96
+ # can get out of hand, especially with lots of Sidekiq::Config
97
+ # references everywhere. We avoid calling `inspect` on more complex
98
+ # state and use `to_s` instead to keep output manageable, #6553
99
+ def inspect
100
+ "#<#{self.class.name} #{
101
+ instance_variables.map do |name|
102
+ value = instance_variable_get(name)
103
+ case value
104
+ when Proc
105
+ "#{name}=#{value}"
106
+ when Sidekiq::Config
107
+ "#{name}=#{value}"
108
+ when Sidekiq::Component
109
+ "#{name}=#{value}"
110
+ else
111
+ "#{name}=#{value.inspect}"
112
+ end
113
+ end.join(", ")
114
+ }>"
115
+ end
116
+
117
+ def default_tag(dir = Dir.pwd)
118
+ name = File.basename(dir)
119
+ prevdir = File.dirname(dir) # Capistrano release directory?
120
+ if name.to_i != 0 && prevdir
121
+ if File.basename(prevdir) == "releases"
122
+ return File.basename(File.dirname(prevdir))
123
+ end
124
+ end
125
+ name
126
+ end
67
127
  end
68
128
  end