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.
- checksums.yaml +4 -4
- data/Changes.md +158 -0
- data/README.md +16 -13
- data/bin/sidekiqload +31 -22
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +120 -0
- data/lib/generators/sidekiq/job_generator.rb +2 -0
- data/lib/sidekiq/api.rb +184 -71
- data/lib/sidekiq/capsule.rb +11 -9
- data/lib/sidekiq/cli.rb +16 -20
- data/lib/sidekiq/client.rb +28 -11
- data/lib/sidekiq/component.rb +62 -2
- data/lib/sidekiq/config.rb +42 -18
- data/lib/sidekiq/deploy.rb +2 -0
- data/lib/sidekiq/embedded.rb +4 -1
- data/lib/sidekiq/iterable_job.rb +3 -0
- data/lib/sidekiq/job/interrupt_handler.rb +2 -0
- data/lib/sidekiq/job/iterable/active_record_enumerator.rb +3 -3
- data/lib/sidekiq/job/iterable.rb +82 -7
- data/lib/sidekiq/job_logger.rb +15 -27
- data/lib/sidekiq/job_retry.rb +17 -5
- data/lib/sidekiq/job_util.rb +7 -1
- data/lib/sidekiq/launcher.rb +3 -2
- data/lib/sidekiq/logger.rb +19 -70
- data/lib/sidekiq/manager.rb +0 -1
- data/lib/sidekiq/metrics/query.rb +73 -45
- data/lib/sidekiq/metrics/shared.rb +23 -9
- data/lib/sidekiq/metrics/tracking.rb +22 -12
- data/lib/sidekiq/middleware/current_attributes.rb +12 -4
- data/lib/sidekiq/middleware/modules.rb +2 -0
- data/lib/sidekiq/monitor.rb +2 -1
- data/lib/sidekiq/paginator.rb +14 -1
- data/lib/sidekiq/processor.rb +26 -19
- data/lib/sidekiq/profiler.rb +72 -0
- data/lib/sidekiq/rails.rb +44 -55
- data/lib/sidekiq/redis_client_adapter.rb +0 -1
- data/lib/sidekiq/redis_connection.rb +22 -4
- data/lib/sidekiq/ring_buffer.rb +2 -0
- data/lib/sidekiq/systemd.rb +2 -0
- data/lib/sidekiq/testing.rb +7 -7
- data/lib/sidekiq/version.rb +6 -2
- data/lib/sidekiq/web/action.rb +124 -69
- data/lib/sidekiq/web/application.rb +355 -377
- data/lib/sidekiq/web/config.rb +120 -0
- data/lib/sidekiq/web/helpers.rb +64 -33
- data/lib/sidekiq/web/router.rb +61 -74
- data/lib/sidekiq/web.rb +52 -150
- data/lib/sidekiq.rb +5 -4
- data/sidekiq.gemspec +6 -6
- data/web/assets/javascripts/application.js +6 -13
- data/web/assets/javascripts/base-charts.js +30 -16
- data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/web/assets/javascripts/dashboard-charts.js +2 -0
- data/web/assets/javascripts/dashboard.js +7 -1
- data/web/assets/javascripts/metrics.js +16 -34
- data/web/assets/stylesheets/style.css +766 -0
- data/web/locales/ar.yml +1 -0
- data/web/locales/cs.yml +1 -0
- data/web/locales/da.yml +1 -0
- data/web/locales/de.yml +1 -0
- data/web/locales/el.yml +1 -0
- data/web/locales/en.yml +9 -1
- data/web/locales/es.yml +24 -2
- data/web/locales/fa.yml +1 -0
- data/web/locales/fr.yml +1 -1
- data/web/locales/gd.yml +1 -1
- data/web/locales/he.yml +1 -0
- data/web/locales/hi.yml +1 -0
- data/web/locales/it.yml +40 -1
- data/web/locales/ja.yml +1 -1
- data/web/locales/ko.yml +1 -0
- data/web/locales/lt.yml +1 -0
- data/web/locales/nb.yml +1 -0
- data/web/locales/nl.yml +1 -0
- data/web/locales/pl.yml +1 -0
- data/web/locales/{pt-br.yml → pt-BR.yml} +3 -3
- data/web/locales/pt.yml +1 -0
- data/web/locales/ru.yml +1 -0
- data/web/locales/sv.yml +1 -0
- data/web/locales/ta.yml +1 -0
- data/web/locales/tr.yml +2 -2
- data/web/locales/uk.yml +25 -1
- data/web/locales/ur.yml +1 -0
- data/web/locales/vi.yml +1 -0
- data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -74
- data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -2
- data/web/views/_footer.erb +31 -34
- data/web/views/_job_info.erb +91 -89
- data/web/views/_metrics_period_select.erb +13 -10
- data/web/views/_nav.erb +14 -21
- data/web/views/_paging.erb +23 -21
- data/web/views/_poll_link.erb +2 -2
- data/web/views/_summary.erb +16 -16
- data/web/views/busy.erb +124 -122
- data/web/views/dashboard.erb +63 -64
- data/web/views/dead.erb +31 -27
- data/web/views/filtering.erb +3 -4
- data/web/views/layout.erb +13 -29
- data/web/views/metrics.erb +75 -82
- data/web/views/metrics_for_job.erb +45 -46
- data/web/views/morgue.erb +61 -70
- data/web/views/profiles.erb +43 -0
- data/web/views/queue.erb +54 -52
- data/web/views/queues.erb +43 -41
- data/web/views/retries.erb +66 -75
- data/web/views/retry.erb +32 -27
- data/web/views/scheduled.erb +59 -55
- data/web/views/scheduled_job_info.erb +1 -1
- metadata +27 -29
- data/web/assets/stylesheets/application-dark.css +0 -147
- data/web/assets/stylesheets/application-rtl.css +0 -163
- data/web/assets/stylesheets/application.css +0 -758
- data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
- data/web/assets/stylesheets/bootstrap.css +0 -5
- 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
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
271
|
-
|
272
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
448
|
-
|
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.
|
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
|
-
|
829
|
-
|
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,
|
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
|
1089
|
-
# #
|
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 { |(_, _,
|
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
|
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
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
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
|
-
|
1190
|
-
|
1271
|
+
class ProfileSet
|
1272
|
+
include Enumerable
|
1191
1273
|
|
1192
|
-
#
|
1193
|
-
#
|
1194
|
-
def
|
1195
|
-
@
|
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
|
1199
|
-
@
|
1285
|
+
def size
|
1286
|
+
@records.size
|
1200
1287
|
end
|
1201
1288
|
|
1202
|
-
def
|
1203
|
-
|
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
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
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
|
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
|
@@ -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
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
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
|
-
|
42
|
-
|
43
|
-
|
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
|
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 <
|
302
|
-
warn "Sidekiq #{Sidekiq::VERSION} only supports Rails
|
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
|
-
|
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)
|
data/lib/sidekiq/client.rb
CHANGED
@@ -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
|
253
|
-
hash.
|
254
|
-
[at, Sidekiq.dump_json(hash
|
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
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
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
|
data/lib/sidekiq/component.rb
CHANGED
@@ -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
|