sidekiq 8.1.0 → 8.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63658318932ac6b7045211590b07f84ed9e1e6398f908ace790a0df20d5ac1f0
4
- data.tar.gz: 0c3f5e69ada7529bdac7392acb1c56ef2a19b8ed13210be8c36aac19858c6df4
3
+ metadata.gz: 0d21db4cf06c0b4d0b5fb770162e289403a76ed996d6c99613cf45083f0e671d
4
+ data.tar.gz: 07cf848cd5de112f3153ecd0014991cbf2abf9e66bc0847d2b7845ec7a726329
5
5
  SHA512:
6
- metadata.gz: 9a5e95a2faccbbf42f8e651c4882c9495ad72b0500bb8fe77fa2a93fb4aa6f8cb96fc820621cd1910fb4b00accf35d75a99d548b3cc8ccdd41a18b5620a9ad73
7
- data.tar.gz: 1bc66490ee07c548c71f680fb280e876efa7eeb8c6f234277df73211ce05c5ae29f5c9bc13064972da945568d1df6634252a6c22a7c5298bdd436b7266246c53
6
+ metadata.gz: 395d346f8b5227480e4d830b3c1f219a35bb0e2527283f9c00a6a4043c1ebe271998874ae53240b6e7dabca89ce8ad939d0a3969da2b33c3e4155fccbebae9ba
7
+ data.tar.gz: 20f44288fd8990544569af7f2a87d90d4ce898b2ce760cc95dd2f30c5a0c4ffc90bcc087cbfa22dbc188a904cc2e817756a83b5b3ffd648f75dc0413d3bec3ff
data/Changes.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  [Sidekiq Changes](https://github.com/sidekiq/sidekiq/blob/main/Changes.md) | [Sidekiq Pro Changes](https://github.com/sidekiq/sidekiq/blob/main/Pro-Changes.md) | [Sidekiq Enterprise Changes](https://github.com/sidekiq/sidekiq/blob/main/Ent-Changes.md)
4
4
 
5
+ 8.1.2
6
+ ----------
7
+
8
+ - Initial release for `kiq`, Sidekiq's official terminal UI:
9
+ ```
10
+ bundle exec kiq
11
+ ```
12
+ Use REDIS_URL or REDIS_PROVIDER to point `kiq` to Redis.
13
+ - Mutation during iteration in `SortedSet#each` caused it to miss half of the jobs [#6936]
14
+ - Fix edge case resulting in nil crash on /busy page [#6954]
15
+
16
+ 8.1.1
17
+ ----------
18
+
19
+ - **DEPRECATION** `require 'sidekiq/testing'` and
20
+ `require 'sidekiq/testing/inline'`.
21
+ Add new `Sidekiq.testing!(mode)` API [#6931]
22
+ Requiring code should not enable process-wide changes.
23
+ ```ruby
24
+ # Old, implicit
25
+ require "sidekiq/testing"
26
+ require "sidekiq/testing/inline"
27
+ # New, more explicit
28
+ Sidekiq.testing!(:fake)
29
+ Sidekiq.testing!(:inline)
30
+ ```
31
+ - Fix race condition with Stop button in UI [#6935]
32
+ - Fix javascript error handler [#6893]
33
+
5
34
  8.1.0
6
35
  ----------
7
36
 
data/README.md CHANGED
@@ -90,7 +90,7 @@ Useful resources:
90
90
  * The [Sidekiq tag](https://stackoverflow.com/questions/tagged/sidekiq) on Stack Overflow has lots of useful Q & A.
91
91
 
92
92
  Every Thursday morning is Sidekiq Office Hour: I video chat and answer questions.
93
- See the [Sidekiq support page](https://sidekiq.org/support.html) for details.
93
+ See the [Sidekiq support page](https://sidekiq.org/support/) for details.
94
94
 
95
95
  Contributing
96
96
  -----------------
data/bin/kiq ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This requires the default gemset so Sidekiq Pro
4
+ # and Sidekiq Enterprise can load any code extensions.
5
+ Bundler.require(:default, :tui)
6
+
7
+ require_relative "../lib/sidekiq/tui"
8
+
9
+ # Run any load hooks registered during Bundler.require
10
+ Sidekiq.loader.run_load_hooks(:tui)
11
+
12
+ tt = Sidekiq::TUI.new(Sidekiq.default_configuration)
13
+
14
+ RatatuiRuby.run do |tui|
15
+ tt.prepare(tui)
16
+ tt.run_loop
17
+ end
@@ -61,18 +61,21 @@ begin
61
61
 
62
62
  # @api private
63
63
  def enqueue(job)
64
- job.provider_job_id = Sidekiq::ActiveJob::Wrapper.set(
65
- wrapped: job.class,
66
- queue: job.queue_name
67
- ).perform_async(job.serialize)
64
+ # NB: Active Job only serializes keys it recognizes. We
65
+ # cannot set arbitrary key/values here.
66
+ options = {wrapped: job.class, queue: job.queue_name}
67
+ options[:profile] = job.profile if job.respond_to?(:profile) && !job.profile.nil?
68
+
69
+ wrapper = Sidekiq::ActiveJob::Wrapper.set(options)
70
+ job.provider_job_id = wrapper.perform_async(job.serialize)
68
71
  end
69
72
 
70
73
  # @api private
71
74
  def enqueue_at(job, timestamp)
72
- job.provider_job_id = Sidekiq::ActiveJob::Wrapper.set(
73
- wrapped: job.class,
74
- queue: job.queue_name
75
- ).perform_at(timestamp, job.serialize)
75
+ options = {wrapped: job.class, queue: job.queue_name}
76
+ options[:profile] = job.profile if job.respond_to?(:profile) && !job.profile.nil?
77
+
78
+ job.provider_job_id = Sidekiq::ActiveJob::Wrapper.set(options).perform_at(timestamp, job.serialize)
76
79
  end
77
80
 
78
81
  # @api private
@@ -14,7 +14,10 @@ module Sidekiq
14
14
  end
15
15
 
16
16
  def create_job_file
17
- template "job.rb.erb", File.join("app/sidekiq", class_path, "#{file_name}_job.rb")
17
+ template(
18
+ "job.rb.erb",
19
+ File.join(jobs_directory, class_path, "#{file_name}_job.rb")
20
+ )
18
21
  end
19
22
 
20
23
  def create_test_file
@@ -31,7 +34,8 @@ module Sidekiq
31
34
 
32
35
  def create_job_spec
33
36
  template_file = File.join(
34
- "spec/sidekiq",
37
+ "spec",
38
+ jobs_directory.gsub("app/", ""),
35
39
  class_path,
36
40
  "#{file_name}_job_spec.rb"
37
41
  )
@@ -40,7 +44,8 @@ module Sidekiq
40
44
 
41
45
  def create_job_test
42
46
  template_file = File.join(
43
- "test/sidekiq",
47
+ "test",
48
+ jobs_directory.gsub("app/", ""),
44
49
  class_path,
45
50
  "#{file_name}_job_test.rb"
46
51
  )
@@ -54,6 +59,13 @@ module Sidekiq
54
59
  def test_framework
55
60
  ::Rails.application.config.generators.options[:rails][:test_framework]
56
61
  end
62
+
63
+ # Can be set in an initializer or in application configuration
64
+ # with Rails.application.config.generators.options[:sidekiq][:jobs_directory] = "app/jobs"
65
+ # to change the directory that the job files are generated in to.
66
+ def jobs_directory
67
+ ::Rails.application.config.generators.options[:sidekiq].fetch(:jobs_directory, "app/sidekiq")
68
+ end
57
69
  end
58
70
  end
59
71
  end
data/lib/sidekiq/api.rb CHANGED
@@ -17,12 +17,37 @@ require "sidekiq/metrics/query"
17
17
  #
18
18
 
19
19
  module Sidekiq
20
+ module ApiUtils
21
+ # @api private
22
+ # Calculate the latency in seconds for a job based on its enqueued timestamp
23
+ # @param job [Hash] the job hash
24
+ # @return [Float] latency in seconds
25
+ def calculate_latency(job)
26
+ timestamp = job["enqueued_at"] || job["created_at"]
27
+ return 0.0 unless timestamp
28
+
29
+ if timestamp.is_a?(Float)
30
+ # old format
31
+ Time.now.to_f - timestamp
32
+ else
33
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
34
+ (now - timestamp) / 1000.0
35
+ end
36
+ end
37
+ end
38
+
20
39
  # Retrieve runtime statistics from Redis regarding
21
40
  # this Sidekiq cluster.
22
41
  #
23
42
  # stat = Sidekiq::Stats.new
24
43
  # stat.processed
25
44
  class Stats
45
+ QueueSummary = Data.define(:name, :size, :latency, :paused) do
46
+ alias_method :paused?, :paused
47
+ end
48
+
49
+ include ApiUtils
50
+
26
51
  def initialize
27
52
  fetch_stats_fast!
28
53
  end
@@ -63,6 +88,7 @@ module Sidekiq
63
88
  stat :default_queue_latency
64
89
  end
65
90
 
91
+ # @return [Hash{String => Integer}] a hash of queue names to their lengths
66
92
  def queues
67
93
  Sidekiq.redis do |conn|
68
94
  queues = conn.sscan("queues").to_a
@@ -78,6 +104,41 @@ module Sidekiq
78
104
  end
79
105
  end
80
106
 
107
+ # More detailed information about each queue: name, size, latency, paused status
108
+ # @return [Array<QueueSummary>]
109
+ def queue_summaries
110
+ Sidekiq.redis do |conn|
111
+ queues = conn.sscan("queues").to_a
112
+ return [] if queues.empty?
113
+
114
+ results = conn.pipelined { |pipeline|
115
+ queues.each do |queue|
116
+ pipeline.llen("queue:#{queue}")
117
+ pipeline.lindex("queue:#{queue}", -1)
118
+ pipeline.sismember("paused", queue)
119
+ end
120
+ }
121
+
122
+ queue_summaries = []
123
+ queues.each_with_index do |name, idx|
124
+ size = results[idx * 3]
125
+ last_item = results[idx * 3 + 1]
126
+ paused = results[idx * 3 + 2] > 0
127
+
128
+ latency = if last_item
129
+ job = Sidekiq.load_json(last_item)
130
+ calculate_latency(job)
131
+ else
132
+ 0.0
133
+ end
134
+
135
+ queue_summaries << QueueSummary.new(name:, size:, latency:, paused:)
136
+ end
137
+
138
+ queue_summaries.sort_by { |qd| -qd.size }
139
+ end
140
+ end
141
+
81
142
  # O(1) redis calls
82
143
  # @api private
83
144
  def fetch_stats_fast!
@@ -100,19 +161,7 @@ module Sidekiq
100
161
  {}
101
162
  end
102
163
 
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
164
+ calculate_latency(job)
116
165
  else
117
166
  0.0
118
167
  end
@@ -235,6 +284,7 @@ module Sidekiq
235
284
  # end
236
285
  class Queue
237
286
  include Enumerable
287
+ include ApiUtils
238
288
 
239
289
  ##
240
290
  # Fetch all known queues within Redis.
@@ -245,6 +295,7 @@ module Sidekiq
245
295
  end
246
296
 
247
297
  attr_reader :name
298
+ alias_method :id, :name
248
299
 
249
300
  # @param name [String] the name of the queue
250
301
  def initialize(name = "default")
@@ -277,19 +328,7 @@ module Sidekiq
277
328
  return 0.0 unless entry
278
329
 
279
330
  job = Sidekiq.load_json(entry)
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
331
+ calculate_latency(job)
293
332
  end
294
333
 
295
334
  def each
@@ -352,6 +391,8 @@ module Sidekiq
352
391
  # The job should be considered immutable but may be
353
392
  # removed from the queue via JobRecord#delete.
354
393
  class JobRecord
394
+ include ApiUtils
395
+
355
396
  # the parsed Hash of job data
356
397
  # @!attribute [r] Item
357
398
  attr_reader :item
@@ -478,17 +519,7 @@ module Sidekiq
478
519
  end
479
520
 
480
521
  def latency
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
522
+ calculate_latency(@item)
492
523
  end
493
524
 
494
525
  # Remove this job from the queue
@@ -553,17 +584,24 @@ module Sidekiq
553
584
  # could be the scheduled time for it to run (e.g. scheduled set),
554
585
  # or the expiration date after which the entry should be deleted (e.g. dead set).
555
586
  class SortedEntry < JobRecord
556
- attr_reader :score
557
587
  attr_reader :parent
558
588
 
559
589
  # :nodoc:
560
590
  # @api private
561
591
  def initialize(parent, score, item)
562
592
  super(item)
563
- @score = Float(score)
593
+ @score = score
564
594
  @parent = parent
565
595
  end
566
596
 
597
+ def score
598
+ Float(@score)
599
+ end
600
+
601
+ def id
602
+ "#{@score}|#{item["jid"]}"
603
+ end
604
+
567
605
  # The timestamp associated with this entry
568
606
  def at
569
607
  Time.at(score).utc
@@ -574,7 +612,7 @@ module Sidekiq
574
612
  if @value
575
613
  @parent.delete_by_value(@parent.name, @value)
576
614
  else
577
- @parent.delete_by_jid(score, jid)
615
+ @parent.delete_by_jid(@score, jid)
578
616
  end
579
617
  end
580
618
 
@@ -583,7 +621,7 @@ module Sidekiq
583
621
  # @param at [Time] the new timestamp for this job
584
622
  def reschedule(at)
585
623
  Sidekiq.redis do |conn|
586
- conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
624
+ conn.zincrby(@parent.name, at.to_f - score, Sidekiq.dump_json(@item))
587
625
  end
588
626
  end
589
627
 
@@ -619,38 +657,8 @@ module Sidekiq
619
657
 
620
658
  private
621
659
 
622
- def remove_job
623
- Sidekiq.redis do |conn|
624
- results = conn.multi { |transaction|
625
- transaction.zrange(parent.name, score, score, "BYSCORE")
626
- transaction.zremrangebyscore(parent.name, score, score)
627
- }.first
628
-
629
- if results.size == 1
630
- yield results.first
631
- else
632
- # multiple jobs with the same score
633
- # find the one with the right JID and push it
634
- matched, nonmatched = results.partition { |message|
635
- if message.index(jid)
636
- msg = Sidekiq.load_json(message)
637
- msg["jid"] == jid
638
- else
639
- false
640
- end
641
- }
642
-
643
- msg = matched.first
644
- yield msg if msg
645
-
646
- # push the rest back onto the sorted set
647
- conn.multi do |transaction|
648
- nonmatched.each do |message|
649
- transaction.zadd(parent.name, score.to_f.to_s, message)
650
- end
651
- end
652
- end
653
- end
660
+ def remove_job(&)
661
+ parent.remove_job(self, &)
654
662
  end
655
663
  end
656
664
 
@@ -819,6 +827,46 @@ module Sidekiq
819
827
  nil
820
828
  end
821
829
 
830
+ def remove_job(entry)
831
+ score = entry.score
832
+ jid = entry.jid
833
+ Sidekiq.redis do |conn|
834
+ results = conn.multi { |transaction|
835
+ transaction.zrange(name, score, score, "BYSCORE")
836
+ transaction.zremrangebyscore(name, score, score)
837
+ }.first
838
+
839
+ if results.size == 1
840
+ yield results.first
841
+ @_size -= 1
842
+ else
843
+ # multiple jobs with the same score
844
+ # find the one with the right JID and push it
845
+ matched, nonmatched = results.partition { |message|
846
+ if message.index(jid)
847
+ msg = Sidekiq.load_json(message)
848
+ msg["jid"] == jid
849
+ else
850
+ false
851
+ end
852
+ }
853
+
854
+ msg = matched.first
855
+ if msg
856
+ yield msg
857
+ @_size -= 1
858
+ end
859
+
860
+ # push the rest back onto the sorted set
861
+ conn.multi do |transaction|
862
+ nonmatched.each do |message|
863
+ transaction.zadd(name, score.to_f.to_s, message)
864
+ end
865
+ end
866
+ end
867
+ end
868
+ end
869
+
822
870
  # :nodoc:
823
871
  # @api private
824
872
  def delete_by_value(name, value)
@@ -992,19 +1040,20 @@ module Sidekiq
992
1040
  # you'll be happier this way
993
1041
  conn.pipelined do |pipeline|
994
1042
  procs.each do |key|
995
- pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
1043
+ pipeline.hmget(key, "info", "concurrency", "busy", "beat", "quiet", "rss", "rtt_us")
996
1044
  end
997
1045
  end
998
1046
  }
999
1047
 
1000
- result.each do |info, busy, beat, quiet, rss, rtt_us|
1048
+ result.each do |info, concurrency, busy, beat, quiet, rss, rtt_us|
1001
1049
  # If a process is stopped between when we query Redis for `procs` and
1002
1050
  # when we query for `result`, we will have an item in `result` that is
1003
1051
  # composed of `nil` values.
1004
1052
  next if info.nil?
1005
1053
 
1006
1054
  hash = Sidekiq.load_json(info)
1007
- yield Process.new(hash.merge("busy" => busy.to_i,
1055
+ yield Process.new(hash.merge("concurrency" => concurrency.to_i,
1056
+ "busy" => busy.to_i,
1008
1057
  "beat" => beat.to_f,
1009
1058
  "quiet" => quiet,
1010
1059
  "rss" => rss.to_i,
@@ -1088,6 +1137,7 @@ module Sidekiq
1088
1137
  def identity
1089
1138
  self["identity"]
1090
1139
  end
1140
+ alias_method :id, :identity
1091
1141
 
1092
1142
  # deprecated, use capsules below
1093
1143
  def queues
@@ -1161,6 +1211,10 @@ module Sidekiq
1161
1211
  self["quiet"] == "true"
1162
1212
  end
1163
1213
 
1214
+ def leader?
1215
+ Sidekiq.redis { |c| c.get("dear-leader") == identity }
1216
+ end
1217
+
1164
1218
  private
1165
1219
 
1166
1220
  def signal(sig)
@@ -132,12 +132,14 @@ module Sidekiq
132
132
  # push_bulk('class' => MyJob, 'args' => (1..100_000).to_a, batch_size: 1_000)
133
133
  #
134
134
  def push_bulk(items)
135
- batch_size = items.delete(:batch_size) || items.delete("batch_size") || 1_000
136
135
  args = items["args"]
137
136
  at = items.delete("at") || items.delete(:at)
138
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) })
139
138
  raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
140
139
 
140
+ # Use a smaller batch size by default for scheduled jobs since adding to sorted sets is more costly.
141
+ batch_size = items.delete(:batch_size) || items.delete("batch_size") || (at ? 100 : 1_000)
142
+
141
143
  jid = items.delete("jid")
142
144
  raise ArgumentError, "Explicitly passing 'jid' when pushing more than one job is not supported" if jid && args.size > 1
143
145
 
@@ -57,6 +57,9 @@ module Sidekiq
57
57
  end
58
58
 
59
59
  def tid
60
+ # We XOR with PID to ensure Thread IDs changes after fork.
61
+ # I'm unclear why we don't multiply the two values to better guarantee
62
+ # a unique value but it's been this way for quite a while now. #3685
60
63
  Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
61
64
  end
62
65
 
@@ -176,6 +176,7 @@ module Sidekiq
176
176
  transaction.sadd("processes", [key])
177
177
  transaction.exists(key)
178
178
  transaction.hset(key, "info", to_json,
179
+ "concurrency", @config.total_concurrency,
179
180
  "busy", curstate.size,
180
181
  "beat", Time.now.to_f,
181
182
  "rtt_us", rtt,
@@ -257,7 +258,6 @@ module Sidekiq
257
258
  "started_at" => Time.now.to_f,
258
259
  "pid" => ::Process.pid,
259
260
  "tag" => @config[:tag] || "",
260
- "concurrency" => @config.total_concurrency,
261
261
  "capsules" => @config.capsules.each_with_object({}) { |(name, cap), memo|
262
262
  memo[name] = cap.to_h
263
263
  },
@@ -69,7 +69,7 @@ module Sidekiq
69
69
  def processor_result(processor, reason = nil)
70
70
  @plock.synchronize do
71
71
  @workers.delete(processor)
72
- unless @done
72
+ if !@done && @count > @workers.size
73
73
  p = Processor.new(@config, &method(:processor_result))
74
74
  @workers << p
75
75
  p.start
@@ -62,7 +62,12 @@ module Sidekiq
62
62
  pageidx = current_page - 1
63
63
  starting = pageidx * page_size
64
64
  items = items.to_a
65
- [current_page, items.size, items[starting, page_size]]
65
+ total_size = items.size
66
+ if starting > total_size
67
+ starting = 0
68
+ current_page = 1
69
+ end
70
+ [current_page, total_size, items[starting, page_size]]
66
71
  end
67
72
  end
68
73
  end
@@ -21,7 +21,7 @@ module Sidekiq
21
21
  return yield unless job["profile"]
22
22
 
23
23
  token = job["profile"]
24
- type = job["class"]
24
+ type = job["wrapped"] || job["class"]
25
25
  jid = job["jid"]
26
26
  started_at = Time.now
27
27