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.
- checksums.yaml +4 -4
- data/Changes.md +263 -0
- data/README.md +5 -5
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiqload +41 -14
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
- data/lib/generators/sidekiq/job_generator.rb +2 -0
- data/lib/sidekiq/api.rb +170 -52
- data/lib/sidekiq/capsule.rb +8 -3
- data/lib/sidekiq/cli.rb +6 -2
- data/lib/sidekiq/client.rb +58 -22
- data/lib/sidekiq/component.rb +23 -1
- data/lib/sidekiq/config.rb +52 -11
- data/lib/sidekiq/deploy.rb +4 -2
- data/lib/sidekiq/embedded.rb +2 -0
- data/lib/sidekiq/fetch.rb +2 -1
- data/lib/sidekiq/iterable_job.rb +55 -0
- data/lib/sidekiq/job/interrupt_handler.rb +24 -0
- data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
- data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
- data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
- data/lib/sidekiq/job/iterable.rb +294 -0
- data/lib/sidekiq/job.rb +15 -8
- data/lib/sidekiq/job_logger.rb +7 -6
- data/lib/sidekiq/job_retry.rb +30 -8
- data/lib/sidekiq/job_util.rb +6 -2
- data/lib/sidekiq/launcher.rb +8 -6
- data/lib/sidekiq/logger.rb +1 -1
- data/lib/sidekiq/metrics/query.rb +6 -1
- data/lib/sidekiq/metrics/shared.rb +16 -5
- data/lib/sidekiq/metrics/tracking.rb +20 -8
- data/lib/sidekiq/middleware/current_attributes.rb +88 -16
- data/lib/sidekiq/middleware/i18n.rb +2 -0
- data/lib/sidekiq/middleware/modules.rb +2 -0
- data/lib/sidekiq/monitor.rb +2 -1
- data/lib/sidekiq/paginator.rb +8 -2
- data/lib/sidekiq/processor.rb +44 -33
- data/lib/sidekiq/rails.rb +28 -7
- data/lib/sidekiq/redis_client_adapter.rb +30 -31
- data/lib/sidekiq/redis_connection.rb +49 -9
- data/lib/sidekiq/ring_buffer.rb +3 -0
- data/lib/sidekiq/scheduled.rb +3 -3
- data/lib/sidekiq/systemd.rb +2 -0
- data/lib/sidekiq/testing.rb +32 -13
- data/lib/sidekiq/transaction_aware_client.rb +20 -5
- data/lib/sidekiq/version.rb +5 -1
- data/lib/sidekiq/web/action.rb +29 -7
- data/lib/sidekiq/web/application.rb +64 -25
- data/lib/sidekiq/web/csrf_protection.rb +9 -6
- data/lib/sidekiq/web/helpers.rb +95 -35
- data/lib/sidekiq/web/router.rb +5 -2
- data/lib/sidekiq/web.rb +67 -3
- data/lib/sidekiq.rb +5 -3
- data/sidekiq.gemspec +5 -13
- data/web/assets/javascripts/application.js +27 -0
- data/web/assets/javascripts/dashboard-charts.js +40 -12
- data/web/assets/javascripts/dashboard.js +14 -10
- data/web/assets/javascripts/metrics.js +34 -0
- data/web/assets/stylesheets/application-rtl.css +10 -0
- data/web/assets/stylesheets/application.css +38 -3
- data/web/locales/en.yml +5 -1
- data/web/locales/fr.yml +13 -0
- data/web/locales/gd.yml +0 -1
- data/web/locales/it.yml +32 -1
- data/web/locales/ja.yml +0 -1
- data/web/locales/pt-br.yml +20 -1
- data/web/locales/tr.yml +100 -0
- data/web/locales/uk.yml +24 -1
- data/web/locales/zh-cn.yml +0 -1
- data/web/locales/zh-tw.yml +0 -1
- data/web/views/_footer.erb +12 -1
- data/web/views/_job_info.erb +1 -1
- data/web/views/_metrics_period_select.erb +1 -1
- data/web/views/_summary.erb +7 -7
- data/web/views/busy.erb +7 -7
- data/web/views/dashboard.erb +29 -36
- data/web/views/filtering.erb +6 -0
- data/web/views/layout.erb +6 -6
- data/web/views/metrics.erb +38 -30
- data/web/views/metrics_for_job.erb +30 -39
- data/web/views/morgue.erb +2 -2
- data/web/views/queue.erb +1 -1
- data/web/views/queues.erb +6 -2
- 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
|
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.
|
|
94
|
+
pipeline.lindex("queue:default", -1)
|
|
96
95
|
end
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
default_queue_latency = if (entry = pipe1_res[6]
|
|
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.
|
|
268
|
-
}
|
|
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
|
|
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
|
-
|
|
472
|
-
uncompressed = Zlib::Inflate.inflate(
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
807
|
-
|
|
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,
|
|
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",
|
|
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
|
|
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.
|
data/lib/sidekiq/capsule.rb
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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"
|
data/lib/sidekiq/client.rb
CHANGED
|
@@ -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
|
|
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.
|
|
100
|
-
# 1000
|
|
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
|
|
109
|
-
#
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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(
|
|
164
|
-
new.push_bulk(
|
|
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
|
|
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
|