sidekiq 7.0.9 → 7.3.10

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +263 -0
  3. data/README.md +5 -5
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +41 -14
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  7. data/lib/generators/sidekiq/job_generator.rb +2 -0
  8. data/lib/sidekiq/api.rb +170 -52
  9. data/lib/sidekiq/capsule.rb +8 -3
  10. data/lib/sidekiq/cli.rb +6 -2
  11. data/lib/sidekiq/client.rb +58 -22
  12. data/lib/sidekiq/component.rb +23 -1
  13. data/lib/sidekiq/config.rb +52 -11
  14. data/lib/sidekiq/deploy.rb +4 -2
  15. data/lib/sidekiq/embedded.rb +2 -0
  16. data/lib/sidekiq/fetch.rb +2 -1
  17. data/lib/sidekiq/iterable_job.rb +55 -0
  18. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  19. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  20. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  21. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  22. data/lib/sidekiq/job/iterable.rb +294 -0
  23. data/lib/sidekiq/job.rb +15 -8
  24. data/lib/sidekiq/job_logger.rb +7 -6
  25. data/lib/sidekiq/job_retry.rb +30 -8
  26. data/lib/sidekiq/job_util.rb +6 -2
  27. data/lib/sidekiq/launcher.rb +8 -6
  28. data/lib/sidekiq/logger.rb +1 -1
  29. data/lib/sidekiq/metrics/query.rb +6 -1
  30. data/lib/sidekiq/metrics/shared.rb +16 -5
  31. data/lib/sidekiq/metrics/tracking.rb +20 -8
  32. data/lib/sidekiq/middleware/current_attributes.rb +88 -16
  33. data/lib/sidekiq/middleware/i18n.rb +2 -0
  34. data/lib/sidekiq/middleware/modules.rb +2 -0
  35. data/lib/sidekiq/monitor.rb +2 -1
  36. data/lib/sidekiq/paginator.rb +8 -2
  37. data/lib/sidekiq/processor.rb +44 -33
  38. data/lib/sidekiq/rails.rb +28 -7
  39. data/lib/sidekiq/redis_client_adapter.rb +30 -31
  40. data/lib/sidekiq/redis_connection.rb +49 -9
  41. data/lib/sidekiq/ring_buffer.rb +3 -0
  42. data/lib/sidekiq/scheduled.rb +3 -3
  43. data/lib/sidekiq/systemd.rb +2 -0
  44. data/lib/sidekiq/testing.rb +32 -13
  45. data/lib/sidekiq/transaction_aware_client.rb +20 -5
  46. data/lib/sidekiq/version.rb +5 -1
  47. data/lib/sidekiq/web/action.rb +29 -7
  48. data/lib/sidekiq/web/application.rb +64 -25
  49. data/lib/sidekiq/web/csrf_protection.rb +9 -6
  50. data/lib/sidekiq/web/helpers.rb +95 -35
  51. data/lib/sidekiq/web/router.rb +5 -2
  52. data/lib/sidekiq/web.rb +67 -3
  53. data/lib/sidekiq.rb +5 -3
  54. data/sidekiq.gemspec +5 -13
  55. data/web/assets/javascripts/application.js +27 -0
  56. data/web/assets/javascripts/dashboard-charts.js +40 -12
  57. data/web/assets/javascripts/dashboard.js +14 -10
  58. data/web/assets/javascripts/metrics.js +34 -0
  59. data/web/assets/stylesheets/application-rtl.css +10 -0
  60. data/web/assets/stylesheets/application.css +38 -3
  61. data/web/locales/en.yml +5 -1
  62. data/web/locales/fr.yml +13 -0
  63. data/web/locales/gd.yml +0 -1
  64. data/web/locales/it.yml +32 -1
  65. data/web/locales/ja.yml +0 -1
  66. data/web/locales/pt-br.yml +20 -1
  67. data/web/locales/tr.yml +100 -0
  68. data/web/locales/uk.yml +24 -1
  69. data/web/locales/zh-cn.yml +0 -1
  70. data/web/locales/zh-tw.yml +0 -1
  71. data/web/views/_footer.erb +12 -1
  72. data/web/views/_job_info.erb +1 -1
  73. data/web/views/_metrics_period_select.erb +1 -1
  74. data/web/views/_summary.erb +7 -7
  75. data/web/views/busy.erb +7 -7
  76. data/web/views/dashboard.erb +29 -36
  77. data/web/views/filtering.erb +6 -0
  78. data/web/views/layout.erb +6 -6
  79. data/web/views/metrics.erb +38 -30
  80. data/web/views/metrics_for_job.erb +30 -39
  81. data/web/views/morgue.erb +2 -2
  82. data/web/views/queue.erb +1 -1
  83. data/web/views/queues.erb +6 -2
  84. metadata +52 -21
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module QueueAdapters
5
+ # Explicitly remove the implementation existing in older rails'.
6
+ remove_const(:SidekiqAdapter) if const_defined?(:SidekiqAdapter)
7
+
8
+ # Sidekiq adapter for Active Job
9
+ #
10
+ # To use Sidekiq set the queue_adapter config to +:sidekiq+.
11
+ #
12
+ # Rails.application.config.active_job.queue_adapter = :sidekiq
13
+ class SidekiqAdapter
14
+ # Defines whether enqueuing should happen implicitly to after commit when called
15
+ # from inside a transaction.
16
+ # @api private
17
+ def enqueue_after_transaction_commit?
18
+ true
19
+ end
20
+
21
+ # @api private
22
+ def enqueue(job)
23
+ job.provider_job_id = JobWrapper.set(
24
+ wrapped: job.class,
25
+ queue: job.queue_name
26
+ ).perform_async(job.serialize)
27
+ end
28
+
29
+ # @api private
30
+ def enqueue_at(job, timestamp)
31
+ job.provider_job_id = JobWrapper.set(
32
+ wrapped: job.class,
33
+ queue: job.queue_name
34
+ ).perform_at(timestamp, job.serialize)
35
+ end
36
+
37
+ # @api private
38
+ def enqueue_all(jobs)
39
+ enqueued_count = 0
40
+ jobs.group_by(&:class).each do |job_class, same_class_jobs|
41
+ same_class_jobs.group_by(&:queue_name).each do |queue, same_class_and_queue_jobs|
42
+ immediate_jobs, scheduled_jobs = same_class_and_queue_jobs.partition { |job| job.scheduled_at.nil? }
43
+
44
+ if immediate_jobs.any?
45
+ jids = Sidekiq::Client.push_bulk(
46
+ "class" => JobWrapper,
47
+ "wrapped" => job_class,
48
+ "queue" => queue,
49
+ "args" => immediate_jobs.map { |job| [job.serialize] }
50
+ )
51
+ enqueued_count += jids.compact.size
52
+ end
53
+
54
+ if scheduled_jobs.any?
55
+ jids = Sidekiq::Client.push_bulk(
56
+ "class" => JobWrapper,
57
+ "wrapped" => job_class,
58
+ "queue" => queue,
59
+ "args" => scheduled_jobs.map { |job| [job.serialize] },
60
+ "at" => scheduled_jobs.map { |job| job.scheduled_at&.to_f }
61
+ )
62
+ enqueued_count += jids.compact.size
63
+ end
64
+ end
65
+ end
66
+ enqueued_count
67
+ end
68
+
69
+ # Defines a class alias for backwards compatibility with enqueued Active Job jobs.
70
+ # @api private
71
+ class JobWrapper < Sidekiq::ActiveJob::Wrapper
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails/generators/named_base"
2
4
 
3
5
  module Sidekiq
data/lib/sidekiq/api.rb CHANGED
@@ -4,7 +4,6 @@ require "sidekiq"
4
4
 
5
5
  require "zlib"
6
6
  require "set"
7
- require "base64"
8
7
 
9
8
  require "sidekiq/metrics/query"
10
9
 
@@ -92,11 +91,11 @@ module Sidekiq
92
91
  pipeline.zcard("retry")
93
92
  pipeline.zcard("dead")
94
93
  pipeline.scard("processes")
95
- pipeline.lrange("queue:default", -1, -1)
94
+ pipeline.lindex("queue:default", -1)
96
95
  end
97
96
  }
98
97
 
99
- default_queue_latency = if (entry = pipe1_res[6].first)
98
+ default_queue_latency = if (entry = pipe1_res[6])
100
99
  job = begin
101
100
  Sidekiq.load_json(entry)
102
101
  rescue
@@ -264,8 +263,8 @@ module Sidekiq
264
263
  # @return [Float] in seconds
265
264
  def latency
266
265
  entry = Sidekiq.redis { |conn|
267
- conn.lrange(@rname, -1, -1)
268
- }.first
266
+ conn.lindex(@rname, -1)
267
+ }
269
268
  return 0 unless entry
270
269
  job = Sidekiq.load_json(entry)
271
270
  now = Time.now.to_f
@@ -374,7 +373,7 @@ module Sidekiq
374
373
  def display_class
375
374
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
376
375
  @klass ||= self["display_class"] || begin
377
- if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
376
+ if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
378
377
  job_class = @item["wrapped"] || args[0]
379
378
  if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
380
379
  # MailerClass#mailer_method
@@ -390,14 +389,14 @@ module Sidekiq
390
389
 
391
390
  def display_args
392
391
  # 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"
394
- job_args = self["wrapped"] ? args[0]["arguments"] : []
392
+ @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
393
+ job_args = self["wrapped"] ? deserialize_argument(args[0]["arguments"]) : []
395
394
  if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
396
395
  # remove MailerClass, mailer_method and 'deliver_now'
397
396
  job_args.drop(3)
398
397
  elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
399
398
  # remove MailerClass, mailer_method and 'deliver_now'
400
- job_args.drop(3).first["args"]
399
+ job_args.drop(3).first.values_at("params", "args")
401
400
  else
402
401
  job_args
403
402
  end
@@ -467,9 +466,32 @@ module Sidekiq
467
466
 
468
467
  private
469
468
 
469
+ ACTIVE_JOB_PREFIX = "_aj_"
470
+ GLOBALID_KEY = "_aj_globalid"
471
+
472
+ def deserialize_argument(argument)
473
+ case argument
474
+ when Array
475
+ argument.map { |arg| deserialize_argument(arg) }
476
+ when Hash
477
+ if serialized_global_id?(argument)
478
+ argument[GLOBALID_KEY]
479
+ else
480
+ argument.transform_values { |v| deserialize_argument(v) }
481
+ .reject { |k, _| k.start_with?(ACTIVE_JOB_PREFIX) }
482
+ end
483
+ else
484
+ argument
485
+ end
486
+ end
487
+
488
+ def serialized_global_id?(hash)
489
+ hash.size == 1 && hash.include?(GLOBALID_KEY)
490
+ end
491
+
470
492
  def uncompress_backtrace(backtrace)
471
- decoded = Base64.decode64(backtrace)
472
- uncompressed = Zlib::Inflate.inflate(decoded)
493
+ strict_base64_decoded = backtrace.unpack1("m")
494
+ uncompressed = Zlib::Inflate.inflate(strict_base64_decoded)
473
495
  Sidekiq.load_json(uncompressed)
474
496
  end
475
497
  end
@@ -548,7 +570,7 @@ module Sidekiq
548
570
  def remove_job
549
571
  Sidekiq.redis do |conn|
550
572
  results = conn.multi { |transaction|
551
- transaction.zrangebyscore(parent.name, score, score)
573
+ transaction.zrange(parent.name, score, score, "BYSCORE")
552
574
  transaction.zremrangebyscore(parent.name, score, score)
553
575
  }.first
554
576
 
@@ -646,6 +668,41 @@ module Sidekiq
646
668
  end
647
669
  end
648
670
 
671
+ def pop_each
672
+ Sidekiq.redis do |c|
673
+ size.times do
674
+ data, score = c.zpopmin(name, 1)&.first
675
+ break unless data
676
+ yield data, score
677
+ end
678
+ end
679
+ end
680
+
681
+ def retry_all
682
+ c = Sidekiq::Client.new
683
+ pop_each do |msg, _|
684
+ job = Sidekiq.load_json(msg)
685
+ # Manual retries should not count against the retry limit.
686
+ job["retry_count"] -= 1 if job["retry_count"]
687
+ c.push(job)
688
+ end
689
+ end
690
+
691
+ # Move all jobs from this Set to the Dead Set.
692
+ # See DeadSet#kill
693
+ def kill_all(notify_failure: false, ex: nil)
694
+ ds = DeadSet.new
695
+ opts = {notify_failure: notify_failure, ex: ex, trim: false}
696
+
697
+ begin
698
+ pop_each do |msg, _|
699
+ ds.kill(msg, opts)
700
+ end
701
+ ensure
702
+ ds.trim
703
+ end
704
+ end
705
+
649
706
  def each
650
707
  initial_size = @_size
651
708
  offset_size = 0
@@ -656,7 +713,7 @@ module Sidekiq
656
713
  range_start = page * page_size + offset_size
657
714
  range_end = range_start + page_size - 1
658
715
  elements = Sidekiq.redis { |conn|
659
- conn.zrange name, range_start, range_end, withscores: true
716
+ conn.zrange name, range_start, range_end, "withscores"
660
717
  }
661
718
  break if elements.empty?
662
719
  page -= 1
@@ -683,7 +740,7 @@ module Sidekiq
683
740
  end
684
741
 
685
742
  elements = Sidekiq.redis { |conn|
686
- conn.zrangebyscore(name, begin_score, end_score, withscores: true)
743
+ conn.zrange(name, begin_score, end_score, "BYSCORE", "withscores")
687
744
  }
688
745
 
689
746
  elements.each_with_object([]) do |element, result|
@@ -724,7 +781,7 @@ module Sidekiq
724
781
  # @api private
725
782
  def delete_by_jid(score, jid)
726
783
  Sidekiq.redis do |conn|
727
- elements = conn.zrangebyscore(name, score, score)
784
+ elements = conn.zrange(name, score, score, "BYSCORE")
728
785
  elements.each do |element|
729
786
  if element.index(jid)
730
787
  message = Sidekiq.load_json(element)
@@ -743,39 +800,21 @@ module Sidekiq
743
800
 
744
801
  ##
745
802
  # The set of scheduled jobs within Sidekiq.
746
- # Based on this, you can search/filter for jobs. Here's an
747
- # example where I'm selecting jobs based on some complex logic
748
- # and deleting them from the scheduled set.
749
- #
750
803
  # See the API wiki page for usage notes and examples.
751
804
  #
752
805
  class ScheduledSet < JobSet
753
806
  def initialize
754
- super "schedule"
807
+ super("schedule")
755
808
  end
756
809
  end
757
810
 
758
811
  ##
759
812
  # The set of retries within Sidekiq.
760
- # Based on this, you can search/filter for jobs. Here's an
761
- # example where I'm selecting all jobs of a certain type
762
- # and deleting them from the retry queue.
763
- #
764
813
  # See the API wiki page for usage notes and examples.
765
814
  #
766
815
  class RetrySet < JobSet
767
816
  def initialize
768
- super "retry"
769
- end
770
-
771
- # Enqueues all jobs pending within the retry set.
772
- def retry_all
773
- each(&:retry) while size > 0
774
- end
775
-
776
- # Kills all jobs pending within the retry set.
777
- def kill_all
778
- each(&:kill) while size > 0
817
+ super("retry")
779
818
  end
780
819
  end
781
820
 
@@ -786,36 +825,48 @@ module Sidekiq
786
825
  #
787
826
  class DeadSet < JobSet
788
827
  def initialize
789
- super "dead"
828
+ super("dead")
829
+ end
830
+
831
+ # Trim dead jobs which are over our storage limits
832
+ def trim
833
+ hash = Sidekiq.default_configuration
834
+ now = Time.now.to_f
835
+ Sidekiq.redis do |conn|
836
+ conn.multi do |transaction|
837
+ transaction.zremrangebyscore(name, "-inf", now - hash[:dead_timeout_in_seconds])
838
+ transaction.zremrangebyrank(name, 0, - hash[:dead_max_jobs])
839
+ end
840
+ end
790
841
  end
791
842
 
792
843
  # Add the given job to the Dead set.
793
844
  # @param message [String] the job data as JSON
845
+ # @option opts [Boolean] :notify_failure (true) Whether death handlers should be called
846
+ # @option opts [Boolean] :trim (true) Whether Sidekiq should trim the structure to keep it within configuration
847
+ # @option opts [Exception] :ex (RuntimeError) An exception to pass to the death handlers
794
848
  def kill(message, opts = {})
795
849
  now = Time.now.to_f
796
850
  Sidekiq.redis do |conn|
797
- conn.multi do |transaction|
798
- transaction.zadd(name, now.to_s, message)
799
- transaction.zremrangebyscore(name, "-inf", now - Sidekiq::Config::DEFAULTS[:dead_timeout_in_seconds])
800
- transaction.zremrangebyrank(name, 0, - Sidekiq::Config::DEFAULTS[:dead_max_jobs])
801
- end
851
+ conn.zadd(name, now.to_s, message)
802
852
  end
803
853
 
854
+ trim if opts[:trim] != false
855
+
804
856
  if opts[:notify_failure] != false
805
857
  job = Sidekiq.load_json(message)
806
- r = RuntimeError.new("Job killed by API")
807
- r.set_backtrace(caller)
858
+ if opts[:ex]
859
+ ex = opts[:ex]
860
+ else
861
+ ex = RuntimeError.new("Job killed by API")
862
+ ex.set_backtrace(caller)
863
+ end
808
864
  Sidekiq.default_configuration.death_handlers.each do |handle|
809
- handle.call(job, r)
865
+ handle.call(job, ex)
810
866
  end
811
867
  end
812
868
  true
813
869
  end
814
-
815
- # Enqueue all dead jobs
816
- def retry_all
817
- each(&:retry) while size > 0
818
- end
819
870
  end
820
871
 
821
872
  ##
@@ -858,7 +909,7 @@ module Sidekiq
858
909
  # @api private
859
910
  def cleanup
860
911
  # dont run cleanup more than once per minute
861
- return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
912
+ return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", "NX", "EX", "60") }
862
913
 
863
914
  count = 0
864
915
  Sidekiq.redis do |conn|
@@ -1087,11 +1138,11 @@ module Sidekiq
1087
1138
 
1088
1139
  procs.zip(all_works).each do |key, workers|
1089
1140
  workers.each_pair do |tid, json|
1090
- results << [key, tid, Sidekiq.load_json(json)] unless json.empty?
1141
+ results << [key, tid, Sidekiq::Work.new(key, tid, Sidekiq.load_json(json))] unless json.empty?
1091
1142
  end
1092
1143
  end
1093
1144
 
1094
- results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
1145
+ results.sort_by { |(_, _, hsh)| hsh.raw("run_at") }.each(&block)
1095
1146
  end
1096
1147
 
1097
1148
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -1114,7 +1165,74 @@ module Sidekiq
1114
1165
  end
1115
1166
  end
1116
1167
  end
1168
+
1169
+ ##
1170
+ # Find the work which represents a job with the given JID.
1171
+ # *This is a slow O(n) operation*. Do not use for app logic.
1172
+ #
1173
+ # @param jid [String] the job identifier
1174
+ # @return [Sidekiq::Work] the work or nil
1175
+ def find_work_by_jid(jid)
1176
+ each do |_process_id, _thread_id, work|
1177
+ job = work.job
1178
+ return work if job.jid == jid
1179
+ end
1180
+ nil
1181
+ end
1117
1182
  end
1183
+
1184
+ # Sidekiq::Work represents a job which is currently executing.
1185
+ class Work
1186
+ attr_reader :process_id
1187
+ attr_reader :thread_id
1188
+
1189
+ def initialize(pid, tid, hsh)
1190
+ @process_id = pid
1191
+ @thread_id = tid
1192
+ @hsh = hsh
1193
+ @job = nil
1194
+ end
1195
+
1196
+ def queue
1197
+ @hsh["queue"]
1198
+ end
1199
+
1200
+ def run_at
1201
+ Time.at(@hsh["run_at"])
1202
+ end
1203
+
1204
+ def job
1205
+ @job ||= Sidekiq::JobRecord.new(@hsh["payload"])
1206
+ end
1207
+
1208
+ def payload
1209
+ @hsh["payload"]
1210
+ end
1211
+
1212
+ # deprecated
1213
+ def [](key)
1214
+ kwargs = {uplevel: 1}
1215
+ kwargs[:category] = :deprecated if RUBY_VERSION > "3.0" # TODO
1216
+ warn("Direct access to `Sidekiq::Work` attributes is deprecated, please use `#payload`, `#queue`, `#run_at` or `#job` instead", **kwargs)
1217
+
1218
+ @hsh[key]
1219
+ end
1220
+
1221
+ # :nodoc:
1222
+ # @api private
1223
+ def raw(name)
1224
+ @hsh[name]
1225
+ end
1226
+
1227
+ def method_missing(*all)
1228
+ @hsh.send(*all)
1229
+ end
1230
+
1231
+ def respond_to_missing?(name, *args)
1232
+ @hsh.respond_to?(name)
1233
+ end
1234
+ end
1235
+
1118
1236
  # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1119
1237
  # Is "worker" a process, a type of job, a thread? Undefined!
1120
1238
  # WorkSet better describes the data.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq/component"
2
4
 
3
5
  module Sidekiq
@@ -17,6 +19,7 @@ module Sidekiq
17
19
  # 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
31
+
27
32
  def initialize(name, config)
28
33
  @name = name
29
34
  @config = config
@@ -35,9 +40,9 @@ module Sidekiq
35
40
 
36
41
  def fetcher
37
42
  @fetcher ||= begin
38
- inst = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
39
- inst.setup(config[:fetch_setup]) if inst.respond_to?(:setup)
40
- inst
43
+ instance = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
44
+ instance.setup(config[:fetch_setup]) if instance.respond_to?(:setup)
45
+ instance
41
46
  end
42
47
  end
43
48
 
data/lib/sidekiq/cli.rb CHANGED
@@ -38,7 +38,7 @@ module Sidekiq # :nodoc:
38
38
  # Code within this method is not tested because it alters
39
39
  # global process state irreversibly. PRs which improve the
40
40
  # test coverage of Sidekiq::CLI are welcomed.
41
- def run(boot_app: true)
41
+ def run(boot_app: true, warmup: true)
42
42
  boot_application if boot_app
43
43
 
44
44
  if environment == "development" && $stdout.tty? && @config.logger.formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
@@ -101,6 +101,8 @@ module Sidekiq # :nodoc:
101
101
  # Touch middleware so it isn't lazy loaded by multiple threads, #3043
102
102
  @config.server_middleware
103
103
 
104
+ ::Process.warmup if warmup && ::Process.respond_to?(:warmup) && ENV["RUBY_DISABLE_WARMUP"] != "1"
105
+
104
106
  # Before this point, the process is initializing with just the main thread.
105
107
  # Starting here the process will now have multiple threads running.
106
108
  fire_event(:startup, reverse: false, reraise: true)
@@ -230,6 +232,7 @@ module Sidekiq # :nodoc:
230
232
  # Both Sinatra 2.0+ and Sidekiq support this term.
231
233
  # RAILS_ENV and RACK_ENV are there for legacy support.
232
234
  @environment = cli_env || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
235
+ config[:environment] = @environment
233
236
  end
234
237
 
235
238
  def symbolize_keys_deep!(hash)
@@ -396,7 +399,7 @@ module Sidekiq # :nodoc:
396
399
  end
397
400
 
398
401
  def parse_config(path)
399
- erb = ERB.new(File.read(path))
402
+ erb = ERB.new(File.read(path), trim_mode: "-")
400
403
  erb.filename = File.expand_path(path)
401
404
  opts = YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true) || {}
402
405
 
@@ -420,3 +423,4 @@ end
420
423
 
421
424
  require "sidekiq/systemd"
422
425
  require "sidekiq/metrics/tracking"
426
+ require "sidekiq/job/interrupt_handler"
@@ -58,6 +58,23 @@ 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)
71
+ # TODO When Redis 7.2 is required
72
+ # p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
73
+ end
74
+ end
75
+ result.to_i
76
+ end
77
+
61
78
  ##
62
79
  # The main method used to push a job to Redis. Accepts a number of options:
63
80
  #
@@ -66,6 +83,7 @@ module Sidekiq
66
83
  # args - an array of simple arguments to the perform method, must be JSON-serializable
67
84
  # at - timestamp to schedule the job (optional), must be Numeric (e.g. Time.now.to_f)
68
85
  # retry - whether to retry this job if it fails, default true or an integer number of retries
86
+ # retry_for - relative amount of time to retry this job if it fails, default nil
69
87
  # backtrace - whether to save any error backtrace, default false
70
88
  #
71
89
  # If class is set to the class name, the jobs' options will be based on Sidekiq's default
@@ -73,7 +91,7 @@ module Sidekiq
73
91
  #
74
92
  # Any options valid for a job class's sidekiq_options are also available here.
75
93
  #
76
- # All options must be strings, not symbols. NB: because we are serializing to JSON, all
94
+ # All keys must be strings, not symbols. NB: because we are serializing to JSON, all
77
95
  # symbols in 'args' will be converted to strings. Note that +backtrace: true+ can take quite a bit of
78
96
  # space in Redis; a large volume of failing jobs can start Redis swapping if you aren't careful.
79
97
  #
@@ -96,8 +114,9 @@ module Sidekiq
96
114
 
97
115
  ##
98
116
  # Push a large number of jobs to Redis. This method cuts out the redis
99
- # network round trip latency. I wouldn't recommend pushing more than
100
- # 1000 per call but YMMV based on network quality, size of job args, etc.
117
+ # network round trip latency. It pushes jobs in batches if more than
118
+ # `:batch_size` (1000 by default) of jobs are passed. I wouldn't recommend making `:batch_size`
119
+ # larger than 1000 but YMMV based on network quality, size of job args, etc.
101
120
  # A large number of jobs can cause a bit of Redis command processing latency.
102
121
  #
103
122
  # Takes the same arguments as #push except that args is expected to be
@@ -105,13 +124,15 @@ module Sidekiq
105
124
  # is run through the client middleware pipeline and each job gets its own Job ID
106
125
  # as normal.
107
126
  #
108
- # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
109
- # than the number given if the middleware stopped processing for one or more jobs.
127
+ # Returns an array of the of pushed jobs' jids, may contain nils if any client middleware
128
+ # prevented a job push.
129
+ #
130
+ # Example (pushing jobs in batches):
131
+ # push_bulk('class' => MyJob, 'args' => (1..100_000).to_a, batch_size: 1_000)
132
+ #
110
133
  def push_bulk(items)
134
+ batch_size = items.delete(:batch_size) || items.delete("batch_size") || 1_000
111
135
  args = items["args"]
112
- raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless args.is_a?(Array) && args.all?(Array)
113
- return [] if args.empty? # no jobs to push
114
-
115
136
  at = items.delete("at")
116
137
  raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all? { |entry| entry.is_a?(Numeric) })
117
138
  raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
@@ -120,18 +141,28 @@ module Sidekiq
120
141
  raise ArgumentError, "Explicitly passing 'jid' when pushing more than one job is not supported" if jid && args.size > 1
121
142
 
122
143
  normed = normalize_item(items)
123
- payloads = args.map.with_index { |job_args, index|
124
- copy = normed.merge("args" => job_args, "jid" => SecureRandom.hex(12))
125
- copy["at"] = (at.is_a?(Array) ? at[index] : at) if at
126
- result = middleware.invoke(items["class"], copy, copy["queue"], @redis_pool) do
127
- verify_json(copy)
128
- copy
129
- end
130
- result || nil
131
- }.compact
144
+ slice_index = 0
145
+ result = args.each_slice(batch_size).flat_map do |slice|
146
+ raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless slice.is_a?(Array) && slice.all?(Array)
147
+ break [] if slice.empty? # no jobs to push
148
+
149
+ payloads = slice.map.with_index { |job_args, index|
150
+ copy = normed.merge("args" => job_args, "jid" => SecureRandom.hex(12))
151
+ copy["at"] = (at.is_a?(Array) ? at[slice_index + index] : at) if at
152
+ result = middleware.invoke(items["class"], copy, copy["queue"], @redis_pool) do
153
+ verify_json(copy)
154
+ copy
155
+ end
156
+ result || nil
157
+ }
158
+ slice_index += batch_size
159
+
160
+ to_push = payloads.compact
161
+ raw_push(to_push) unless to_push.empty?
162
+ payloads.map { |payload| payload&.[]("jid") }
163
+ end
132
164
 
133
- raw_push(payloads) unless payloads.empty?
134
- payloads.collect { |payload| payload["jid"] }
165
+ result.is_a?(Enumerator::Lazy) ? result.force : result
135
166
  end
136
167
 
137
168
  # Allows sharding of jobs across any number of Redis instances. All jobs
@@ -160,8 +191,8 @@ module Sidekiq
160
191
  new.push(item)
161
192
  end
162
193
 
163
- def push_bulk(items)
164
- new.push_bulk(items)
194
+ def push_bulk(...)
195
+ new.push_bulk(...)
165
196
  end
166
197
 
167
198
  # Resque compatibility helpers. Note all helpers
@@ -234,7 +265,12 @@ module Sidekiq
234
265
  def atomic_push(conn, payloads)
235
266
  if payloads.first.key?("at")
236
267
  conn.zadd("schedule", payloads.flat_map { |hash|
237
- at = hash.delete("at").to_s
268
+ at = hash["at"].to_s
269
+ # ActiveJob sets this but the job has not been enqueued yet
270
+ hash.delete("enqueued_at")
271
+ # TODO: Use hash.except("at") when support for Ruby 2.7 is dropped
272
+ hash = hash.dup
273
+ hash.delete("at")
238
274
  [at, Sidekiq.dump_json(hash)]
239
275
  })
240
276
  else