sidekiq 7.3.9 → 8.1.7

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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +225 -0
  3. data/README.md +34 -14
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/bin/sidekiqload +12 -10
  7. data/bin/webload +69 -0
  8. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +107 -58
  9. data/lib/generators/sidekiq/job_generator.rb +15 -3
  10. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  11. data/lib/sidekiq/api.rb +310 -83
  12. data/lib/sidekiq/capsule.rb +10 -7
  13. data/lib/sidekiq/cli.rb +31 -24
  14. data/lib/sidekiq/client.rb +36 -18
  15. data/lib/sidekiq/component.rb +45 -3
  16. data/lib/sidekiq/config.rb +35 -30
  17. data/lib/sidekiq/embedded.rb +2 -1
  18. data/lib/sidekiq/fetch.rb +1 -0
  19. data/lib/sidekiq/iterable_job.rb +1 -0
  20. data/lib/sidekiq/job/iterable.rb +46 -18
  21. data/lib/sidekiq/job.rb +18 -3
  22. data/lib/sidekiq/job_logger.rb +8 -6
  23. data/lib/sidekiq/job_retry.rb +45 -18
  24. data/lib/sidekiq/job_util.rb +5 -1
  25. data/lib/sidekiq/launcher.rb +22 -12
  26. data/lib/sidekiq/loader.rb +57 -0
  27. data/lib/sidekiq/logger.rb +25 -69
  28. data/lib/sidekiq/manager.rb +2 -2
  29. data/lib/sidekiq/metrics/query.rb +77 -46
  30. data/lib/sidekiq/metrics/shared.rb +8 -5
  31. data/lib/sidekiq/metrics/tracking.rb +12 -7
  32. data/lib/sidekiq/middleware/current_attributes.rb +11 -19
  33. data/lib/sidekiq/middleware/i18n.rb +2 -0
  34. data/lib/sidekiq/monitor.rb +4 -8
  35. data/lib/sidekiq/paginator.rb +18 -2
  36. data/lib/sidekiq/processor.rb +26 -18
  37. data/lib/sidekiq/profiler.rb +73 -0
  38. data/lib/sidekiq/rails.rb +46 -67
  39. data/lib/sidekiq/redis_client_adapter.rb +6 -3
  40. data/lib/sidekiq/redis_connection.rb +16 -5
  41. data/lib/sidekiq/ring_buffer.rb +1 -0
  42. data/lib/sidekiq/scheduled.rb +6 -7
  43. data/lib/sidekiq/test_api.rb +331 -0
  44. data/lib/sidekiq/testing/inline.rb +2 -30
  45. data/lib/sidekiq/testing.rb +2 -333
  46. data/lib/sidekiq/transaction_aware_client.rb +13 -5
  47. data/lib/sidekiq/tui/controls.rb +53 -0
  48. data/lib/sidekiq/tui/filtering.rb +53 -0
  49. data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
  50. data/lib/sidekiq/tui/tabs/busy.rb +104 -0
  51. data/lib/sidekiq/tui/tabs/dead.rb +14 -0
  52. data/lib/sidekiq/tui/tabs/home.rb +115 -0
  53. data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
  54. data/lib/sidekiq/tui/tabs/queues.rb +83 -0
  55. data/lib/sidekiq/tui/tabs/retries.rb +14 -0
  56. data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
  57. data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
  58. data/lib/sidekiq/tui/tabs.rb +15 -0
  59. data/lib/sidekiq/tui.rb +390 -0
  60. data/lib/sidekiq/version.rb +2 -2
  61. data/lib/sidekiq/web/action.rb +146 -83
  62. data/lib/sidekiq/web/application.rb +372 -328
  63. data/lib/sidekiq/web/config.rb +117 -0
  64. data/lib/sidekiq/web/helpers.rb +102 -44
  65. data/lib/sidekiq/web/router.rb +61 -77
  66. data/lib/sidekiq/web.rb +65 -151
  67. data/lib/sidekiq.rb +13 -1
  68. data/sidekiq.gemspec +9 -8
  69. data/web/assets/images/logo.png +0 -0
  70. data/web/assets/images/status.png +0 -0
  71. data/web/assets/javascripts/application.js +43 -27
  72. data/web/assets/javascripts/base-charts.js +30 -16
  73. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  74. data/web/assets/javascripts/dashboard.js +1 -1
  75. data/web/assets/javascripts/metrics.js +16 -34
  76. data/web/assets/stylesheets/style.css +776 -0
  77. data/web/locales/ar.yml +3 -1
  78. data/web/locales/cs.yml +2 -0
  79. data/web/locales/da.yml +2 -0
  80. data/web/locales/de.yml +2 -0
  81. data/web/locales/el.yml +2 -0
  82. data/web/locales/en.yml +8 -0
  83. data/web/locales/es.yml +25 -2
  84. data/web/locales/fa.yml +3 -1
  85. data/web/locales/fr.yml +3 -1
  86. data/web/locales/gd.yml +13 -1
  87. data/web/locales/he.yml +3 -1
  88. data/web/locales/hi.yml +36 -0
  89. data/web/locales/it.yml +9 -0
  90. data/web/locales/ja.yml +2 -0
  91. data/web/locales/ko.yml +2 -0
  92. data/web/locales/lt.yml +2 -0
  93. data/web/locales/nb.yml +2 -0
  94. data/web/locales/nl.yml +2 -0
  95. data/web/locales/pl.yml +2 -0
  96. data/web/locales/{pt-br.yml → pt-BR.yml} +3 -1
  97. data/web/locales/pt.yml +2 -0
  98. data/web/locales/ru.yml +2 -0
  99. data/web/locales/sv.yml +2 -0
  100. data/web/locales/ta.yml +83 -47
  101. data/web/locales/tr.yml +2 -0
  102. data/web/locales/uk.yml +7 -5
  103. data/web/locales/ur.yml +3 -1
  104. data/web/locales/vi.yml +2 -0
  105. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -73
  106. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -1
  107. data/web/views/_footer.html.erb +32 -0
  108. data/web/views/_job_info.html.erb +115 -0
  109. data/web/views/_metrics_period_select.html.erb +15 -0
  110. data/web/views/_nav.html.erb +45 -0
  111. data/web/views/_paging.html.erb +26 -0
  112. data/web/views/_poll_link.html.erb +4 -0
  113. data/web/views/{_summary.erb → _summary.html.erb} +16 -16
  114. data/web/views/busy.html.erb +155 -0
  115. data/web/views/dashboard.html.erb +104 -0
  116. data/web/views/dead.html.erb +38 -0
  117. data/web/views/filtering.html.erb +6 -0
  118. data/web/views/layout.html.erb +26 -0
  119. data/web/views/metrics.html.erb +85 -0
  120. data/web/views/metrics_for_job.html.erb +58 -0
  121. data/web/views/morgue.html.erb +69 -0
  122. data/web/views/profiles.html.erb +43 -0
  123. data/web/views/queue.html.erb +57 -0
  124. data/web/views/queues.html.erb +46 -0
  125. data/web/views/retries.html.erb +77 -0
  126. data/web/views/retry.html.erb +39 -0
  127. data/web/views/scheduled.html.erb +64 -0
  128. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  129. metadata +65 -47
  130. data/lib/sidekiq/web/csrf_protection.rb +0 -183
  131. data/web/assets/stylesheets/application-dark.css +0 -147
  132. data/web/assets/stylesheets/application-rtl.css +0 -163
  133. data/web/assets/stylesheets/application.css +0 -759
  134. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  135. data/web/assets/stylesheets/bootstrap.css +0 -5
  136. data/web/views/_footer.erb +0 -34
  137. data/web/views/_job_info.erb +0 -105
  138. data/web/views/_metrics_period_select.erb +0 -12
  139. data/web/views/_nav.erb +0 -52
  140. data/web/views/_paging.erb +0 -25
  141. data/web/views/_poll_link.erb +0 -4
  142. data/web/views/_status.erb +0 -4
  143. data/web/views/busy.erb +0 -148
  144. data/web/views/dashboard.erb +0 -108
  145. data/web/views/dead.erb +0 -34
  146. data/web/views/filtering.erb +0 -6
  147. data/web/views/layout.erb +0 -42
  148. data/web/views/metrics.erb +0 -90
  149. data/web/views/metrics_for_job.erb +0 -59
  150. data/web/views/morgue.erb +0 -74
  151. data/web/views/queue.erb +0 -55
  152. data/web/views/queues.erb +0 -44
  153. data/web/views/retries.erb +0 -79
  154. data/web/views/retry.erb +0 -34
  155. data/web/views/scheduled.erb +0 -56
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
  #
@@ -19,12 +17,37 @@ require "sidekiq/metrics/query"
19
17
  #
20
18
 
21
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
+
22
39
  # Retrieve runtime statistics from Redis regarding
23
40
  # this Sidekiq cluster.
24
41
  #
25
42
  # stat = Sidekiq::Stats.new
26
43
  # stat.processed
27
44
  class Stats
45
+ QueueSummary = Data.define(:name, :size, :latency, :paused) do
46
+ alias_method :paused?, :paused
47
+ end
48
+
49
+ include ApiUtils
50
+
28
51
  def initialize
29
52
  fetch_stats_fast!
30
53
  end
@@ -65,6 +88,7 @@ module Sidekiq
65
88
  stat :default_queue_latency
66
89
  end
67
90
 
91
+ # @return [Hash{String => Integer}] a hash of queue names to their lengths
68
92
  def queues
69
93
  Sidekiq.redis do |conn|
70
94
  queues = conn.sscan("queues").to_a
@@ -80,6 +104,41 @@ module Sidekiq
80
104
  end
81
105
  end
82
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
+
83
142
  # O(1) redis calls
84
143
  # @api private
85
144
  def fetch_stats_fast!
@@ -101,11 +160,10 @@ module Sidekiq
101
160
  rescue
102
161
  {}
103
162
  end
104
- now = Time.now.to_f
105
- thence = job["enqueued_at"] || now
106
- now - thence
163
+
164
+ calculate_latency(job)
107
165
  else
108
- 0
166
+ 0.0
109
167
  end
110
168
 
111
169
  @stats = {
@@ -176,7 +234,7 @@ module Sidekiq
176
234
  end
177
235
 
178
236
  class History
179
- def initialize(days_previous, start_date = nil, pool: nil)
237
+ def initialize(days_previous, start_date = nil)
180
238
  # we only store five years of data in Redis
181
239
  raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
182
240
  @days_previous = days_previous
@@ -226,6 +284,7 @@ module Sidekiq
226
284
  # end
227
285
  class Queue
228
286
  include Enumerable
287
+ include ApiUtils
229
288
 
230
289
  ##
231
290
  # Fetch all known queues within Redis.
@@ -236,6 +295,7 @@ module Sidekiq
236
295
  end
237
296
 
238
297
  attr_reader :name
298
+ alias_method :id, :name
239
299
 
240
300
  # @param name [String] the name of the queue
241
301
  def initialize(name = "default")
@@ -265,11 +325,10 @@ module Sidekiq
265
325
  entry = Sidekiq.redis { |conn|
266
326
  conn.lindex(@rname, -1)
267
327
  }
268
- return 0 unless entry
328
+ return 0.0 unless entry
329
+
269
330
  job = Sidekiq.load_json(entry)
270
- now = Time.now.to_f
271
- thence = job["enqueued_at"] || now
272
- now - thence
331
+ calculate_latency(job)
273
332
  end
274
333
 
275
334
  def each
@@ -332,6 +391,8 @@ module Sidekiq
332
391
  # The job should be considered immutable but may be
333
392
  # removed from the queue via JobRecord#delete.
334
393
  class JobRecord
394
+ include ApiUtils
395
+
335
396
  # the parsed Hash of job data
336
397
  # @!attribute [r] Item
337
398
  attr_reader :item
@@ -417,16 +478,34 @@ module Sidekiq
417
478
  self["jid"]
418
479
  end
419
480
 
481
+ def iterable_state
482
+ @iterable_state ||= Sidekiq::IterableJobQuery.new(jid)[jid]
483
+ end
484
+
420
485
  def bid
421
486
  self["bid"]
422
487
  end
423
488
 
489
+ def failed_at
490
+ if self["failed_at"]
491
+ time_from_timestamp(self["failed_at"])
492
+ end
493
+ end
494
+
495
+ def retried_at
496
+ if self["retried_at"]
497
+ time_from_timestamp(self["retried_at"])
498
+ end
499
+ end
500
+
424
501
  def enqueued_at
425
- self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
502
+ if self["enqueued_at"]
503
+ time_from_timestamp(self["enqueued_at"])
504
+ end
426
505
  end
427
506
 
428
507
  def created_at
429
- Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
508
+ time_from_timestamp(self["created_at"] || self["enqueued_at"] || 0)
430
509
  end
431
510
 
432
511
  def tags
@@ -444,8 +523,7 @@ module Sidekiq
444
523
  end
445
524
 
446
525
  def latency
447
- now = Time.now.to_f
448
- now - (@item["enqueued_at"] || @item["created_at"] || now)
526
+ calculate_latency(@item)
449
527
  end
450
528
 
451
529
  # Remove this job from the queue
@@ -494,6 +572,15 @@ module Sidekiq
494
572
  uncompressed = Zlib::Inflate.inflate(strict_base64_decoded)
495
573
  Sidekiq.load_json(uncompressed)
496
574
  end
575
+
576
+ def time_from_timestamp(timestamp)
577
+ if timestamp.is_a?(Float)
578
+ # old format, timestamps were stored as fractional seconds since the epoch
579
+ Time.at(timestamp).utc
580
+ else
581
+ Time.at(timestamp / 1000, timestamp % 1000, :millisecond)
582
+ end
583
+ end
497
584
  end
498
585
 
499
586
  # Represents a job within a Redis sorted set where the score
@@ -501,17 +588,24 @@ module Sidekiq
501
588
  # could be the scheduled time for it to run (e.g. scheduled set),
502
589
  # or the expiration date after which the entry should be deleted (e.g. dead set).
503
590
  class SortedEntry < JobRecord
504
- attr_reader :score
505
591
  attr_reader :parent
506
592
 
507
593
  # :nodoc:
508
594
  # @api private
509
595
  def initialize(parent, score, item)
510
596
  super(item)
511
- @score = Float(score)
597
+ @score = score
512
598
  @parent = parent
513
599
  end
514
600
 
601
+ def score
602
+ Float(@score)
603
+ end
604
+
605
+ def id
606
+ "#{@score}|#{item["jid"]}"
607
+ end
608
+
515
609
  # The timestamp associated with this entry
516
610
  def at
517
611
  Time.at(score).utc
@@ -522,7 +616,7 @@ module Sidekiq
522
616
  if @value
523
617
  @parent.delete_by_value(@parent.name, @value)
524
618
  else
525
- @parent.delete_by_jid(score, jid)
619
+ @parent.delete_by_jid(@score, jid)
526
620
  end
527
621
  end
528
622
 
@@ -531,8 +625,9 @@ module Sidekiq
531
625
  # @param at [Time] the new timestamp for this job
532
626
  def reschedule(at)
533
627
  Sidekiq.redis do |conn|
534
- conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
628
+ conn.zincrby(@parent.name, at.to_f - score, Sidekiq.dump_json(@item))
535
629
  end
630
+ @score = at.to_f
536
631
  end
537
632
 
538
633
  # Enqueue this job from the scheduled or dead set so it will
@@ -567,38 +662,8 @@ module Sidekiq
567
662
 
568
663
  private
569
664
 
570
- def remove_job
571
- Sidekiq.redis do |conn|
572
- results = conn.multi { |transaction|
573
- transaction.zrange(parent.name, score, score, "BYSCORE")
574
- transaction.zremrangebyscore(parent.name, score, score)
575
- }.first
576
-
577
- if results.size == 1
578
- yield results.first
579
- else
580
- # multiple jobs with the same score
581
- # find the one with the right JID and push it
582
- matched, nonmatched = results.partition { |message|
583
- if message.index(jid)
584
- msg = Sidekiq.load_json(message)
585
- msg["jid"] == jid
586
- else
587
- false
588
- end
589
- }
590
-
591
- msg = matched.first
592
- yield msg if msg
593
-
594
- # push the rest back onto the sorted set
595
- conn.multi do |transaction|
596
- nonmatched.each do |message|
597
- transaction.zadd(parent.name, score.to_f.to_s, message)
598
- end
599
- end
600
- end
601
- end
665
+ def remove_job(&)
666
+ parent.remove_job(self, &)
602
667
  end
603
668
  end
604
669
 
@@ -767,6 +832,46 @@ module Sidekiq
767
832
  nil
768
833
  end
769
834
 
835
+ def remove_job(entry)
836
+ score = entry.score
837
+ jid = entry.jid
838
+ Sidekiq.redis do |conn|
839
+ results = conn.multi { |transaction|
840
+ transaction.zrange(name, score, score, "BYSCORE")
841
+ transaction.zremrangebyscore(name, score, score)
842
+ }.first
843
+
844
+ if results.size == 1
845
+ yield results.first
846
+ @_size -= 1
847
+ else
848
+ # multiple jobs with the same score
849
+ # find the one with the right JID and push it
850
+ matched, nonmatched = results.partition { |message|
851
+ if message.index(jid)
852
+ msg = Sidekiq.load_json(message)
853
+ msg["jid"] == jid
854
+ else
855
+ false
856
+ end
857
+ }
858
+
859
+ msg = matched.first
860
+ if msg
861
+ yield msg
862
+ @_size -= 1
863
+ end
864
+
865
+ # push the rest back onto the sorted set
866
+ conn.multi do |transaction|
867
+ nonmatched.each do |message|
868
+ transaction.zadd(name, score.to_f.to_s, message)
869
+ end
870
+ end
871
+ end
872
+ end
873
+ end
874
+
770
875
  # :nodoc:
771
876
  # @api private
772
877
  def delete_by_value(name, value)
@@ -940,19 +1045,20 @@ module Sidekiq
940
1045
  # you'll be happier this way
941
1046
  conn.pipelined do |pipeline|
942
1047
  procs.each do |key|
943
- pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
1048
+ pipeline.hmget(key, "info", "concurrency", "busy", "beat", "quiet", "rss", "rtt_us")
944
1049
  end
945
1050
  end
946
1051
  }
947
1052
 
948
- result.each do |info, busy, beat, quiet, rss, rtt_us|
1053
+ result.each do |info, concurrency, busy, beat, quiet, rss, rtt_us|
949
1054
  # If a process is stopped between when we query Redis for `procs` and
950
1055
  # when we query for `result`, we will have an item in `result` that is
951
1056
  # composed of `nil` values.
952
1057
  next if info.nil?
953
1058
 
954
1059
  hash = Sidekiq.load_json(info)
955
- yield Process.new(hash.merge("busy" => busy.to_i,
1060
+ yield Process.new(hash.merge("concurrency" => concurrency.to_i,
1061
+ "busy" => busy.to_i,
956
1062
  "beat" => beat.to_f,
957
1063
  "quiet" => quiet,
958
1064
  "rss" => rss.to_i,
@@ -1007,9 +1113,9 @@ module Sidekiq
1007
1113
  # 'started_at' => <process start time>,
1008
1114
  # 'pid' => 12345,
1009
1115
  # 'tag' => 'myapp'
1010
- # 'concurrency' => 25,
1011
- # 'queues' => ['default', 'low'],
1012
- # 'busy' => 10,
1116
+ # 'concurrency' => 5,
1117
+ # 'capsules' => {"default" => {"mode" => "weighted", "concurrency" => 5, "weights" => {"default" => 2, "low" => 1}}},
1118
+ # 'busy' => 3,
1013
1119
  # 'beat' => <last heartbeat>,
1014
1120
  # 'identity' => <unique string identifying the process>,
1015
1121
  # 'embedded' => true,
@@ -1036,13 +1142,37 @@ module Sidekiq
1036
1142
  def identity
1037
1143
  self["identity"]
1038
1144
  end
1145
+ alias_method :id, :identity
1039
1146
 
1147
+ # deprecated, use capsules below
1040
1148
  def queues
1041
- self["queues"]
1149
+ # Backwards compatibility with <8.0.8
1150
+ if !self["capsules"]
1151
+ self["queues"]
1152
+ else
1153
+ capsules.values.flat_map { |x| x["weights"].keys }.uniq
1154
+ end
1042
1155
  end
1043
1156
 
1157
+ # deprecated, use capsules below
1044
1158
  def weights
1045
- self["weights"]
1159
+ # Backwards compatibility with <8.0.8
1160
+ if !self["capsules"]
1161
+ self["weights"]
1162
+ else
1163
+ hash = {}
1164
+ capsules.values.each do |cap|
1165
+ # Note: will lose data if two capsules are processing the same named queue
1166
+ cap["weights"].each_pair do |queue, weight|
1167
+ hash[queue] = weight
1168
+ end
1169
+ end
1170
+ hash
1171
+ end
1172
+ end
1173
+
1174
+ def capsules
1175
+ self["capsules"]
1046
1176
  end
1047
1177
 
1048
1178
  def version
@@ -1086,6 +1216,10 @@ module Sidekiq
1086
1216
  self["quiet"] == "true"
1087
1217
  end
1088
1218
 
1219
+ def leader?
1220
+ Sidekiq.redis { |c| c.get("dear-leader") == identity }
1221
+ end
1222
+
1089
1223
  private
1090
1224
 
1091
1225
  def signal(sig)
@@ -1114,9 +1248,8 @@ module Sidekiq
1114
1248
  # works.each do |process_id, thread_id, work|
1115
1249
  # # process_id is a unique identifier per Sidekiq process
1116
1250
  # # thread_id is a unique identifier per thread
1117
- # # work is a Hash which looks like:
1118
- # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
1119
- # # run_at is an epoch Integer.
1251
+ # # work is a `Sidekiq::Work` instance that has the following accessor methods.
1252
+ # # [work.queue, work.run_at, work.payload]
1120
1253
  # end
1121
1254
  #
1122
1255
  class WorkSet
@@ -1142,7 +1275,7 @@ module Sidekiq
1142
1275
  end
1143
1276
  end
1144
1277
 
1145
- results.sort_by { |(_, _, hsh)| hsh.raw("run_at") }.each(&block)
1278
+ results.sort_by { |(_, _, work)| work.run_at }.each(&block)
1146
1279
  end
1147
1280
 
1148
1281
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -1172,13 +1305,14 @@ module Sidekiq
1172
1305
  #
1173
1306
  # @param jid [String] the job identifier
1174
1307
  # @return [Sidekiq::Work] the work or nil
1175
- def find_work_by_jid(jid)
1308
+ def find_work(jid)
1176
1309
  each do |_process_id, _thread_id, work|
1177
1310
  job = work.job
1178
1311
  return work if job.jid == jid
1179
1312
  end
1180
1313
  nil
1181
1314
  end
1315
+ alias_method :find_work_by_jid, :find_work
1182
1316
  end
1183
1317
 
1184
1318
  # Sidekiq::Work represents a job which is currently executing.
@@ -1208,33 +1342,126 @@ module Sidekiq
1208
1342
  def payload
1209
1343
  @hsh["payload"]
1210
1344
  end
1345
+ end
1211
1346
 
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)
1347
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1348
+ # Is "worker" a process, a type of job, a thread? Undefined!
1349
+ # WorkSet better describes the data.
1350
+ Workers = WorkSet
1217
1351
 
1218
- @hsh[key]
1352
+ class ProfileSet
1353
+ include Enumerable
1354
+
1355
+ # This is a point in time/snapshot API, you'll need to instantiate a new instance
1356
+ # if you want to fetch newer records.
1357
+ def initialize
1358
+ @records = Sidekiq.redis do |c|
1359
+ # This throws away expired profiles
1360
+ c.zremrangebyscore("profiles", "-inf", Time.now.to_f.to_s)
1361
+ # retreive records, newest to oldest
1362
+ c.zrange("profiles", "+inf", 0, "byscore", "rev")
1363
+ end
1219
1364
  end
1220
1365
 
1221
- # :nodoc:
1222
- # @api private
1223
- def raw(name)
1224
- @hsh[name]
1366
+ def size
1367
+ @records.size
1225
1368
  end
1226
1369
 
1227
- def method_missing(*all)
1228
- @hsh.send(*all)
1370
+ def each(&block)
1371
+ fetch_keys = %w[started_at jid type token size elapsed].freeze
1372
+ arrays = Sidekiq.redis do |c|
1373
+ c.pipelined do |p|
1374
+ @records.each do |key|
1375
+ p.hmget(key, *fetch_keys)
1376
+ end
1377
+ end
1378
+ end
1379
+
1380
+ arrays.compact.map { |arr| ProfileRecord.new(arr) }.each(&block)
1381
+ end
1382
+ end
1383
+
1384
+ class ProfileRecord
1385
+ attr_reader :started_at, :jid, :type, :token, :size, :elapsed
1386
+
1387
+ def initialize(arr)
1388
+ # Must be same order as fetch_keys above
1389
+ @started_at = Time.at(Integer(arr[0]))
1390
+ @jid = arr[1]
1391
+ @type = arr[2]
1392
+ @token = arr[3]
1393
+ @size = Integer(arr[4])
1394
+ @elapsed = Float(arr[5])
1229
1395
  end
1230
1396
 
1231
- def respond_to_missing?(name, *args)
1232
- @hsh.respond_to?(name)
1397
+ def key
1398
+ "#{token}-#{jid}"
1399
+ end
1400
+
1401
+ def data
1402
+ Sidekiq.redis { |c| c.hget(key, "data") }
1233
1403
  end
1234
1404
  end
1235
1405
 
1236
- # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1237
- # Is "worker" a process, a type of job, a thread? Undefined!
1238
- # WorkSet better describes the data.
1239
- Workers = WorkSet
1406
+ # Persisted iteration state from Redis for jobs using Sidekiq::IterableJob.
1407
+ class IterableJobQuery
1408
+ def initialize(jids)
1409
+ @cache = bulk_fetch(jids)
1410
+ end
1411
+
1412
+ def [](jid)
1413
+ @cache[jid]
1414
+ end
1415
+
1416
+ private
1417
+
1418
+ # Bulk-fetch iteration state for multiple JIDs in a single Redis pipeline.
1419
+ # Returns a Hash of { jid => State } for JIDs that have iteration state.
1420
+ def bulk_fetch(jids)
1421
+ raise ArgumentError unless jids
1422
+ jids_to_fetch = Array(jids).compact.uniq
1423
+ return {} if jids_to_fetch.empty?
1424
+
1425
+ results = Sidekiq.redis do |conn|
1426
+ conn.pipelined do |pipe|
1427
+ jids_to_fetch.each { |jid| pipe.hgetall("it-#{jid}") }
1428
+ end
1429
+ end
1430
+
1431
+ # TODO Requires Ruby 4
1432
+ # states = ::Hash.new(capacity: jids_to_fetch.size)
1433
+ states = {}
1434
+ jids_to_fetch.each_with_index do |jid, i|
1435
+ raw = results[i]
1436
+ next if raw.nil? || raw.empty?
1437
+
1438
+ states[jid] = State.new(jid, raw)
1439
+ end
1440
+ states
1441
+ end
1442
+
1443
+ State = Struct.new(:jid, :raw) do
1444
+ def executions
1445
+ raw["ex"].to_i
1446
+ end
1447
+
1448
+ def runtime
1449
+ raw["rt"].to_f
1450
+ end
1451
+
1452
+ def cursor
1453
+ @cursor ||= begin
1454
+ Sidekiq.load_json(raw["c"])
1455
+ rescue JSON::ParserError
1456
+ raw["c"]
1457
+ end
1458
+ end
1459
+
1460
+ def cancelled
1461
+ raw["cancelled"]&.to_i
1462
+ end
1463
+ end
1464
+ end
1240
1465
  end
1466
+
1467
+ Sidekiq.loader.run_load_hooks(:api)
@@ -11,12 +11,12 @@ module Sidekiq
11
11
  # This capsule will pull jobs from the "single" queue and process
12
12
  # the jobs with one thread, meaning the jobs will be processed serially.
13
13
  #
14
- # Sidekiq.configure_server do |config|
15
- # config.capsule("single-threaded") do |cap|
16
- # cap.concurrency = 1
17
- # cap.queues = %w(single)
14
+ # Sidekiq.configure_server do |config|
15
+ # config.capsule("single-threaded") do |cap|
16
+ # cap.concurrency = 1
17
+ # cap.queues = %w(single)
18
+ # end
18
19
  # end
19
- # end
20
20
  class Capsule
21
21
  include Sidekiq::Component
22
22
  extend Forwardable
@@ -27,7 +27,7 @@ module Sidekiq
27
27
  attr_reader :mode
28
28
  attr_reader :weights
29
29
 
30
- def_delegators :@config, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
30
+ def_delegators :@config, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig, :thread_priority
31
31
 
32
32
  def initialize(name, config)
33
33
  @name = name
@@ -38,6 +38,10 @@ module Sidekiq
38
38
  @mode = :strict
39
39
  end
40
40
 
41
+ def to_h
42
+ {concurrency: concurrency, mode: mode, weights: weights}
43
+ end
44
+
41
45
  def fetcher
42
46
  @fetcher ||= begin
43
47
  instance = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
@@ -47,7 +51,6 @@ module Sidekiq
47
51
  end
48
52
 
49
53
  def stop
50
- fetcher&.bulk_requeue([])
51
54
  end
52
55
 
53
56
  # Sidekiq checks queues in three modes: