sidekiq 6.5.10 → 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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +568 -18
  3. data/README.md +66 -38
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/bin/multi_queue_bench +271 -0
  7. data/bin/sidekiq +3 -8
  8. data/bin/sidekiqload +215 -118
  9. data/bin/sidekiqmon +3 -0
  10. data/bin/webload +69 -0
  11. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  12. data/lib/generators/sidekiq/job_generator.rb +17 -3
  13. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  14. data/lib/sidekiq/api.rb +526 -218
  15. data/lib/sidekiq/capsule.rb +135 -0
  16. data/lib/sidekiq/cli.rb +88 -96
  17. data/lib/sidekiq/client.rb +114 -47
  18. data/lib/sidekiq/component.rb +71 -4
  19. data/lib/sidekiq/config.rb +316 -0
  20. data/lib/sidekiq/deploy.rb +64 -0
  21. data/lib/sidekiq/embedded.rb +64 -0
  22. data/lib/sidekiq/fetch.rb +12 -14
  23. data/lib/sidekiq/iterable_job.rb +56 -0
  24. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  25. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  26. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  27. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  28. data/lib/sidekiq/job/iterable.rb +322 -0
  29. data/lib/sidekiq/job.rb +397 -10
  30. data/lib/sidekiq/job_logger.rb +14 -11
  31. data/lib/sidekiq/job_retry.rb +78 -28
  32. data/lib/sidekiq/job_util.rb +58 -16
  33. data/lib/sidekiq/launcher.rb +86 -70
  34. data/lib/sidekiq/loader.rb +57 -0
  35. data/lib/sidekiq/logger.rb +25 -94
  36. data/lib/sidekiq/manager.rb +11 -13
  37. data/lib/sidekiq/metrics/query.rb +84 -48
  38. data/lib/sidekiq/metrics/shared.rb +29 -14
  39. data/lib/sidekiq/metrics/tracking.rb +49 -30
  40. data/lib/sidekiq/middleware/chain.rb +19 -18
  41. data/lib/sidekiq/middleware/current_attributes.rb +78 -21
  42. data/lib/sidekiq/middleware/i18n.rb +2 -0
  43. data/lib/sidekiq/middleware/modules.rb +2 -0
  44. data/lib/sidekiq/monitor.rb +15 -5
  45. data/lib/sidekiq/paginator.rb +26 -4
  46. data/lib/sidekiq/processor.rb +78 -65
  47. data/lib/sidekiq/profiler.rb +73 -0
  48. data/lib/sidekiq/rails.rb +45 -60
  49. data/lib/sidekiq/redis_client_adapter.rb +35 -72
  50. data/lib/sidekiq/redis_connection.rb +54 -114
  51. data/lib/sidekiq/ring_buffer.rb +3 -0
  52. data/lib/sidekiq/scheduled.rb +28 -30
  53. data/lib/sidekiq/systemd.rb +2 -0
  54. data/lib/sidekiq/test_api.rb +331 -0
  55. data/lib/sidekiq/testing/inline.rb +2 -30
  56. data/lib/sidekiq/testing.rb +2 -342
  57. data/lib/sidekiq/transaction_aware_client.rb +23 -9
  58. data/lib/sidekiq/tui/controls.rb +53 -0
  59. data/lib/sidekiq/tui/filtering.rb +53 -0
  60. data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
  61. data/lib/sidekiq/tui/tabs/busy.rb +104 -0
  62. data/lib/sidekiq/tui/tabs/dead.rb +14 -0
  63. data/lib/sidekiq/tui/tabs/home.rb +115 -0
  64. data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
  65. data/lib/sidekiq/tui/tabs/queues.rb +83 -0
  66. data/lib/sidekiq/tui/tabs/retries.rb +14 -0
  67. data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
  68. data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
  69. data/lib/sidekiq/tui/tabs.rb +15 -0
  70. data/lib/sidekiq/tui.rb +390 -0
  71. data/lib/sidekiq/version.rb +6 -1
  72. data/lib/sidekiq/web/action.rb +149 -64
  73. data/lib/sidekiq/web/application.rb +379 -281
  74. data/lib/sidekiq/web/config.rb +117 -0
  75. data/lib/sidekiq/web/helpers.rb +190 -71
  76. data/lib/sidekiq/web/router.rb +62 -75
  77. data/lib/sidekiq/web.rb +68 -101
  78. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  79. data/lib/sidekiq.rb +85 -269
  80. data/sidekiq.gemspec +15 -11
  81. data/web/assets/images/logo.png +0 -0
  82. data/web/assets/images/status.png +0 -0
  83. data/web/assets/javascripts/application.js +84 -24
  84. data/web/assets/javascripts/base-charts.js +120 -0
  85. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  86. data/web/assets/javascripts/dashboard-charts.js +194 -0
  87. data/web/assets/javascripts/dashboard.js +18 -234
  88. data/web/assets/javascripts/metrics.js +138 -120
  89. data/web/assets/stylesheets/style.css +776 -0
  90. data/web/locales/ar.yml +72 -70
  91. data/web/locales/cs.yml +64 -62
  92. data/web/locales/da.yml +62 -53
  93. data/web/locales/de.yml +67 -65
  94. data/web/locales/el.yml +4 -7
  95. data/web/locales/en.yml +89 -71
  96. data/web/locales/es.yml +91 -68
  97. data/web/locales/fa.yml +67 -65
  98. data/web/locales/fr.yml +82 -67
  99. data/web/locales/gd.yml +110 -0
  100. data/web/locales/he.yml +67 -64
  101. data/web/locales/hi.yml +95 -59
  102. data/web/locales/it.yml +94 -54
  103. data/web/locales/ja.yml +69 -70
  104. data/web/locales/ko.yml +54 -52
  105. data/web/locales/lt.yml +68 -66
  106. data/web/locales/nb.yml +63 -61
  107. data/web/locales/nl.yml +54 -52
  108. data/web/locales/pl.yml +47 -45
  109. data/web/locales/{pt-br.yml → pt-BR.yml} +81 -70
  110. data/web/locales/pt.yml +53 -51
  111. data/web/locales/ru.yml +69 -66
  112. data/web/locales/sv.yml +55 -53
  113. data/web/locales/ta.yml +100 -64
  114. data/web/locales/tr.yml +102 -0
  115. data/web/locales/uk.yml +87 -61
  116. data/web/locales/ur.yml +66 -64
  117. data/web/locales/vi.yml +69 -67
  118. data/web/locales/zh-CN.yml +107 -0
  119. data/web/locales/{zh-tw.yml → zh-TW.yml} +13 -3
  120. data/web/views/_footer.html.erb +32 -0
  121. data/web/views/_job_info.html.erb +115 -0
  122. data/web/views/_metrics_period_select.html.erb +15 -0
  123. data/web/views/_nav.html.erb +45 -0
  124. data/web/views/_paging.html.erb +26 -0
  125. data/web/views/_poll_link.html.erb +4 -0
  126. data/web/views/_summary.html.erb +40 -0
  127. data/web/views/busy.html.erb +155 -0
  128. data/web/views/dashboard.html.erb +104 -0
  129. data/web/views/dead.html.erb +38 -0
  130. data/web/views/filtering.html.erb +6 -0
  131. data/web/views/layout.html.erb +26 -0
  132. data/web/views/metrics.html.erb +85 -0
  133. data/web/views/metrics_for_job.html.erb +58 -0
  134. data/web/views/morgue.html.erb +69 -0
  135. data/web/views/profiles.html.erb +43 -0
  136. data/web/views/queue.html.erb +57 -0
  137. data/web/views/queues.html.erb +46 -0
  138. data/web/views/retries.html.erb +77 -0
  139. data/web/views/retry.html.erb +39 -0
  140. data/web/views/scheduled.html.erb +64 -0
  141. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  142. metadata +113 -70
  143. data/lib/sidekiq/delay.rb +0 -43
  144. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  145. data/lib/sidekiq/extensions/active_record.rb +0 -43
  146. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  147. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  148. data/lib/sidekiq/metrics/deploy.rb +0 -47
  149. data/lib/sidekiq/web/csrf_protection.rb +0 -180
  150. data/lib/sidekiq/worker.rb +0 -370
  151. data/web/assets/javascripts/graph.js +0 -16
  152. data/web/assets/stylesheets/application-dark.css +0 -143
  153. data/web/assets/stylesheets/application-rtl.css +0 -242
  154. data/web/assets/stylesheets/application.css +0 -999
  155. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  156. data/web/assets/stylesheets/bootstrap.css +0 -5
  157. data/web/locales/zh-cn.yml +0 -93
  158. data/web/views/_footer.erb +0 -20
  159. data/web/views/_job_info.erb +0 -89
  160. data/web/views/_nav.erb +0 -52
  161. data/web/views/_paging.erb +0 -23
  162. data/web/views/_poll_link.erb +0 -4
  163. data/web/views/_status.erb +0 -4
  164. data/web/views/_summary.erb +0 -40
  165. data/web/views/busy.erb +0 -137
  166. data/web/views/dashboard.erb +0 -84
  167. data/web/views/dead.erb +0 -34
  168. data/web/views/layout.erb +0 -42
  169. data/web/views/metrics.erb +0 -69
  170. data/web/views/metrics_for_job.erb +0 -87
  171. data/web/views/morgue.erb +0 -78
  172. data/web/views/queue.erb +0 -59
  173. data/web/views/queues.erb +0 -38
  174. data/web/views/retries.erb +0 -83
  175. data/web/views/retry.erb +0 -34
  176. data/web/views/scheduled.erb +0 -57
  177. /data/{LICENSE → LICENSE.txt} +0 -0
data/lib/sidekiq/api.rb CHANGED
@@ -1,15 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq"
4
-
5
3
  require "zlib"
6
- require "set"
7
- require "base64"
8
4
 
9
- if ENV["SIDEKIQ_METRICS_BETA"]
10
- require "sidekiq/metrics/deploy"
11
- require "sidekiq/metrics/query"
12
- end
5
+ require "sidekiq"
6
+ require "sidekiq/metrics/query"
13
7
 
14
8
  #
15
9
  # Sidekiq's Data API provides a Ruby object model on top
@@ -23,12 +17,37 @@ end
23
17
  #
24
18
 
25
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
+
26
39
  # Retrieve runtime statistics from Redis regarding
27
40
  # this Sidekiq cluster.
28
41
  #
29
42
  # stat = Sidekiq::Stats.new
30
43
  # stat.processed
31
44
  class Stats
45
+ QueueSummary = Data.define(:name, :size, :latency, :paused) do
46
+ alias_method :paused?, :paused
47
+ end
48
+
49
+ include ApiUtils
50
+
32
51
  def initialize
33
52
  fetch_stats_fast!
34
53
  end
@@ -69,8 +88,55 @@ module Sidekiq
69
88
  stat :default_queue_latency
70
89
  end
71
90
 
91
+ # @return [Hash{String => Integer}] a hash of queue names to their lengths
72
92
  def queues
73
- Sidekiq::Stats::Queues.new.lengths
93
+ Sidekiq.redis do |conn|
94
+ queues = conn.sscan("queues").to_a
95
+
96
+ lengths = conn.pipelined { |pipeline|
97
+ queues.each do |queue|
98
+ pipeline.llen("queue:#{queue}")
99
+ end
100
+ }
101
+
102
+ array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
103
+ array_of_arrays.to_h
104
+ end
105
+ end
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
74
140
  end
75
141
 
76
142
  # O(1) redis calls
@@ -84,21 +150,20 @@ module Sidekiq
84
150
  pipeline.zcard("retry")
85
151
  pipeline.zcard("dead")
86
152
  pipeline.scard("processes")
87
- pipeline.lrange("queue:default", -1, -1)
153
+ pipeline.lindex("queue:default", -1)
88
154
  end
89
155
  }
90
156
 
91
- default_queue_latency = if (entry = pipe1_res[6].first)
157
+ default_queue_latency = if (entry = pipe1_res[6])
92
158
  job = begin
93
159
  Sidekiq.load_json(entry)
94
160
  rescue
95
161
  {}
96
162
  end
97
- now = Time.now.to_f
98
- thence = job["enqueued_at"] || now
99
- now - thence
163
+
164
+ calculate_latency(job)
100
165
  else
101
- 0
166
+ 0.0
102
167
  end
103
168
 
104
169
  @stats = {
@@ -117,11 +182,11 @@ module Sidekiq
117
182
  # @api private
118
183
  def fetch_stats_slow!
119
184
  processes = Sidekiq.redis { |conn|
120
- conn.sscan_each("processes").to_a
185
+ conn.sscan("processes").to_a
121
186
  }
122
187
 
123
188
  queues = Sidekiq.redis { |conn|
124
- conn.sscan_each("queues").to_a
189
+ conn.sscan("queues").to_a
125
190
  }
126
191
 
127
192
  pipe2_res = Sidekiq.redis { |conn|
@@ -133,7 +198,7 @@ module Sidekiq
133
198
 
134
199
  s = processes.size
135
200
  workers_size = pipe2_res[0...s].sum(&:to_i)
136
- enqueued = pipe2_res[s..-1].sum(&:to_i)
201
+ enqueued = pipe2_res[s..].sum(&:to_i)
137
202
 
138
203
  @stats[:workers_size] = workers_size
139
204
  @stats[:enqueued] = enqueued
@@ -168,23 +233,6 @@ module Sidekiq
168
233
  @stats[s] || raise(ArgumentError, "Unknown stat #{s}")
169
234
  end
170
235
 
171
- class Queues
172
- def lengths
173
- Sidekiq.redis do |conn|
174
- queues = conn.sscan_each("queues").to_a
175
-
176
- lengths = conn.pipelined { |pipeline|
177
- queues.each do |queue|
178
- pipeline.llen("queue:#{queue}")
179
- end
180
- }
181
-
182
- array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
183
- array_of_arrays.to_h
184
- end
185
- end
186
- end
187
-
188
236
  class History
189
237
  def initialize(days_previous, start_date = nil)
190
238
  # we only store five years of data in Redis
@@ -211,15 +259,10 @@ module Sidekiq
211
259
 
212
260
  keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
213
261
 
214
- begin
215
- Sidekiq.redis do |conn|
216
- conn.mget(keys).each_with_index do |value, idx|
217
- stat_hash[dates[idx]] = value ? value.to_i : 0
218
- end
262
+ Sidekiq.redis do |conn|
263
+ conn.mget(keys).each_with_index do |value, idx|
264
+ stat_hash[dates[idx]] = value ? value.to_i : 0
219
265
  end
220
- rescue RedisConnection.adapter::CommandError
221
- # mget will trigger a CROSSSLOT error when run against a Cluster
222
- # TODO Someone want to add Cluster support?
223
266
  end
224
267
 
225
268
  stat_hash
@@ -241,16 +284,18 @@ module Sidekiq
241
284
  # end
242
285
  class Queue
243
286
  include Enumerable
287
+ include ApiUtils
244
288
 
245
289
  ##
246
290
  # Fetch all known queues within Redis.
247
291
  #
248
292
  # @return [Array<Sidekiq::Queue>]
249
293
  def self.all
250
- Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
294
+ Sidekiq.redis { |c| c.sscan("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
251
295
  end
252
296
 
253
297
  attr_reader :name
298
+ alias_method :id, :name
254
299
 
255
300
  # @param name [String] the name of the queue
256
301
  def initialize(name = "default")
@@ -278,13 +323,12 @@ module Sidekiq
278
323
  # @return [Float] in seconds
279
324
  def latency
280
325
  entry = Sidekiq.redis { |conn|
281
- conn.lrange(@rname, -1, -1)
282
- }.first
283
- return 0 unless entry
326
+ conn.lindex(@rname, -1)
327
+ }
328
+ return 0.0 unless entry
329
+
284
330
  job = Sidekiq.load_json(entry)
285
- now = Time.now.to_f
286
- thence = job["enqueued_at"] || now
287
- now - thence
331
+ calculate_latency(job)
288
332
  end
289
333
 
290
334
  def each
@@ -347,6 +391,8 @@ module Sidekiq
347
391
  # The job should be considered immutable but may be
348
392
  # removed from the queue via JobRecord#delete.
349
393
  class JobRecord
394
+ include ApiUtils
395
+
350
396
  # the parsed Hash of job data
351
397
  # @!attribute [r] Item
352
398
  attr_reader :item
@@ -388,12 +434,7 @@ module Sidekiq
388
434
  def display_class
389
435
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
390
436
  @klass ||= self["display_class"] || begin
391
- case klass
392
- when /\ASidekiq::Extensions::Delayed/
393
- safe_load(args[0], klass) do |target, method, _|
394
- "#{target}.#{method}"
395
- end
396
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
437
+ if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
397
438
  job_class = @item["wrapped"] || args[0]
398
439
  if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
399
440
  # MailerClass#mailer_method
@@ -409,23 +450,14 @@ module Sidekiq
409
450
 
410
451
  def display_args
411
452
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
412
- @display_args ||= case klass
413
- when /\ASidekiq::Extensions::Delayed/
414
- safe_load(args[0], args) do |_, _, arg, kwarg|
415
- if !kwarg || kwarg.empty?
416
- arg
417
- else
418
- [arg, kwarg]
419
- end
420
- end
421
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
422
- job_args = self["wrapped"] ? args[0]["arguments"] : []
453
+ @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
454
+ job_args = self["wrapped"] ? deserialize_argument(args[0]["arguments"]) : []
423
455
  if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
424
456
  # remove MailerClass, mailer_method and 'deliver_now'
425
457
  job_args.drop(3)
426
458
  elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
427
459
  # remove MailerClass, mailer_method and 'deliver_now'
428
- job_args.drop(3).first["args"]
460
+ job_args.drop(3).first.values_at("params", "args")
429
461
  else
430
462
  job_args
431
463
  end
@@ -446,12 +478,34 @@ module Sidekiq
446
478
  self["jid"]
447
479
  end
448
480
 
481
+ def iterable_state
482
+ @iterable_state ||= Sidekiq::IterableJobQuery.new(jid)[jid]
483
+ end
484
+
485
+ def bid
486
+ self["bid"]
487
+ end
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
+
449
501
  def enqueued_at
450
- self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
502
+ if self["enqueued_at"]
503
+ time_from_timestamp(self["enqueued_at"])
504
+ end
451
505
  end
452
506
 
453
507
  def created_at
454
- Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
508
+ time_from_timestamp(self["created_at"] || self["enqueued_at"] || 0)
455
509
  end
456
510
 
457
511
  def tags
@@ -469,8 +523,7 @@ module Sidekiq
469
523
  end
470
524
 
471
525
  def latency
472
- now = Time.now.to_f
473
- now - (@item["enqueued_at"] || @item["created_at"] || now)
526
+ calculate_latency(@item)
474
527
  end
475
528
 
476
529
  # Remove this job from the queue
@@ -491,30 +544,41 @@ module Sidekiq
491
544
 
492
545
  private
493
546
 
494
- def safe_load(content, default)
495
- yield(*YAML.load(content))
496
- rescue => ex
497
- # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
498
- # memory yet so the YAML can't be loaded.
499
- # TODO is this still necessary? Zeitwerk reloader should handle?
500
- Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
501
- default
547
+ ACTIVE_JOB_PREFIX = "_aj_"
548
+ GLOBALID_KEY = "_aj_globalid"
549
+
550
+ def deserialize_argument(argument)
551
+ case argument
552
+ when Array
553
+ argument.map { |arg| deserialize_argument(arg) }
554
+ when Hash
555
+ if serialized_global_id?(argument)
556
+ argument[GLOBALID_KEY]
557
+ else
558
+ argument.transform_values { |v| deserialize_argument(v) }
559
+ .reject { |k, _| k.start_with?(ACTIVE_JOB_PREFIX) }
560
+ end
561
+ else
562
+ argument
563
+ end
564
+ end
565
+
566
+ def serialized_global_id?(hash)
567
+ hash.size == 1 && hash.include?(GLOBALID_KEY)
502
568
  end
503
569
 
504
570
  def uncompress_backtrace(backtrace)
505
- if backtrace.is_a?(Array)
506
- # Handle old jobs with raw Array backtrace format
507
- backtrace
571
+ strict_base64_decoded = backtrace.unpack1("m")
572
+ uncompressed = Zlib::Inflate.inflate(strict_base64_decoded)
573
+ Sidekiq.load_json(uncompressed)
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
508
580
  else
509
- decoded = Base64.decode64(backtrace)
510
- uncompressed = Zlib::Inflate.inflate(decoded)
511
- begin
512
- Sidekiq.load_json(uncompressed)
513
- rescue
514
- # Handle old jobs with marshalled backtrace format
515
- # TODO Remove in 7.x
516
- Marshal.load(uncompressed)
517
- end
581
+ Time.at(timestamp / 1000, timestamp % 1000, :millisecond)
518
582
  end
519
583
  end
520
584
  end
@@ -524,17 +588,24 @@ module Sidekiq
524
588
  # could be the scheduled time for it to run (e.g. scheduled set),
525
589
  # or the expiration date after which the entry should be deleted (e.g. dead set).
526
590
  class SortedEntry < JobRecord
527
- attr_reader :score
528
591
  attr_reader :parent
529
592
 
530
593
  # :nodoc:
531
594
  # @api private
532
595
  def initialize(parent, score, item)
533
596
  super(item)
534
- @score = Float(score)
597
+ @score = score
535
598
  @parent = parent
536
599
  end
537
600
 
601
+ def score
602
+ Float(@score)
603
+ end
604
+
605
+ def id
606
+ "#{@score}|#{item["jid"]}"
607
+ end
608
+
538
609
  # The timestamp associated with this entry
539
610
  def at
540
611
  Time.at(score).utc
@@ -545,7 +616,7 @@ module Sidekiq
545
616
  if @value
546
617
  @parent.delete_by_value(@parent.name, @value)
547
618
  else
548
- @parent.delete_by_jid(score, jid)
619
+ @parent.delete_by_jid(@score, jid)
549
620
  end
550
621
  end
551
622
 
@@ -554,8 +625,9 @@ module Sidekiq
554
625
  # @param at [Time] the new timestamp for this job
555
626
  def reschedule(at)
556
627
  Sidekiq.redis do |conn|
557
- 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))
558
629
  end
630
+ @score = at.to_f
559
631
  end
560
632
 
561
633
  # Enqueue this job from the scheduled or dead set so it will
@@ -590,38 +662,8 @@ module Sidekiq
590
662
 
591
663
  private
592
664
 
593
- def remove_job
594
- Sidekiq.redis do |conn|
595
- results = conn.multi { |transaction|
596
- transaction.zrangebyscore(parent.name, score, score)
597
- transaction.zremrangebyscore(parent.name, score, score)
598
- }.first
599
-
600
- if results.size == 1
601
- yield results.first
602
- else
603
- # multiple jobs with the same score
604
- # find the one with the right JID and push it
605
- matched, nonmatched = results.partition { |message|
606
- if message.index(jid)
607
- msg = Sidekiq.load_json(message)
608
- msg["jid"] == jid
609
- else
610
- false
611
- end
612
- }
613
-
614
- msg = matched.first
615
- yield msg if msg
616
-
617
- # push the rest back onto the sorted set
618
- conn.multi do |transaction|
619
- nonmatched.each do |message|
620
- transaction.zadd(parent.name, score.to_f.to_s, message)
621
- end
622
- end
623
- end
624
- end
665
+ def remove_job(&)
666
+ parent.remove_job(self, &)
625
667
  end
626
668
  end
627
669
 
@@ -656,7 +698,7 @@ module Sidekiq
656
698
 
657
699
  match = "*#{match}*" unless match.include?("*")
658
700
  Sidekiq.redis do |conn|
659
- conn.zscan_each(name, match: match, count: count) do |entry, score|
701
+ conn.zscan(name, match: match, count: count) do |entry, score|
660
702
  yield SortedEntry.new(self, score, entry)
661
703
  end
662
704
  end
@@ -691,6 +733,41 @@ module Sidekiq
691
733
  end
692
734
  end
693
735
 
736
+ def pop_each
737
+ Sidekiq.redis do |c|
738
+ size.times do
739
+ data, score = c.zpopmin(name, 1)&.first
740
+ break unless data
741
+ yield data, score
742
+ end
743
+ end
744
+ end
745
+
746
+ def retry_all
747
+ c = Sidekiq::Client.new
748
+ pop_each do |msg, _|
749
+ job = Sidekiq.load_json(msg)
750
+ # Manual retries should not count against the retry limit.
751
+ job["retry_count"] -= 1 if job["retry_count"]
752
+ c.push(job)
753
+ end
754
+ end
755
+
756
+ # Move all jobs from this Set to the Dead Set.
757
+ # See DeadSet#kill
758
+ def kill_all(notify_failure: false, ex: nil)
759
+ ds = DeadSet.new
760
+ opts = {notify_failure: notify_failure, ex: ex, trim: false}
761
+
762
+ begin
763
+ pop_each do |msg, _|
764
+ ds.kill(msg, opts)
765
+ end
766
+ ensure
767
+ ds.trim
768
+ end
769
+ end
770
+
694
771
  def each
695
772
  initial_size = @_size
696
773
  offset_size = 0
@@ -701,7 +778,7 @@ module Sidekiq
701
778
  range_start = page * page_size + offset_size
702
779
  range_end = range_start + page_size - 1
703
780
  elements = Sidekiq.redis { |conn|
704
- conn.zrange name, range_start, range_end, withscores: true
781
+ conn.zrange name, range_start, range_end, "withscores"
705
782
  }
706
783
  break if elements.empty?
707
784
  page -= 1
@@ -728,7 +805,7 @@ module Sidekiq
728
805
  end
729
806
 
730
807
  elements = Sidekiq.redis { |conn|
731
- conn.zrangebyscore(name, begin_score, end_score, withscores: true)
808
+ conn.zrange(name, begin_score, end_score, "BYSCORE", "withscores")
732
809
  }
733
810
 
734
811
  elements.each_with_object([]) do |element, result|
@@ -746,8 +823,8 @@ module Sidekiq
746
823
  # @return [SortedEntry] the record or nil
747
824
  def find_job(jid)
748
825
  Sidekiq.redis do |conn|
749
- conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
750
- job = JSON.parse(entry)
826
+ conn.zscan(name, match: "*#{jid}*", count: 100) do |entry, score|
827
+ job = Sidekiq.load_json(entry)
751
828
  matched = job["jid"] == jid
752
829
  return SortedEntry.new(self, score, entry) if matched
753
830
  end
@@ -755,6 +832,46 @@ module Sidekiq
755
832
  nil
756
833
  end
757
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
+
758
875
  # :nodoc:
759
876
  # @api private
760
877
  def delete_by_value(name, value)
@@ -769,7 +886,7 @@ module Sidekiq
769
886
  # @api private
770
887
  def delete_by_jid(score, jid)
771
888
  Sidekiq.redis do |conn|
772
- elements = conn.zrangebyscore(name, score, score)
889
+ elements = conn.zrange(name, score, score, "BYSCORE")
773
890
  elements.each do |element|
774
891
  if element.index(jid)
775
892
  message = Sidekiq.load_json(element)
@@ -788,47 +905,21 @@ module Sidekiq
788
905
 
789
906
  ##
790
907
  # The set of scheduled jobs within Sidekiq.
791
- # Based on this, you can search/filter for jobs. Here's an
792
- # example where I'm selecting jobs based on some complex logic
793
- # and deleting them from the scheduled set.
908
+ # See the API wiki page for usage notes and examples.
794
909
  #
795
- # r = Sidekiq::ScheduledSet.new
796
- # r.select do |scheduled|
797
- # scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
798
- # scheduled.args[0] == 'User' &&
799
- # scheduled.args[1] == 'setup_new_subscriber'
800
- # end.map(&:delete)
801
910
  class ScheduledSet < JobSet
802
911
  def initialize
803
- super "schedule"
912
+ super("schedule")
804
913
  end
805
914
  end
806
915
 
807
916
  ##
808
917
  # The set of retries within Sidekiq.
809
- # Based on this, you can search/filter for jobs. Here's an
810
- # example where I'm selecting all jobs of a certain type
811
- # and deleting them from the retry queue.
918
+ # See the API wiki page for usage notes and examples.
812
919
  #
813
- # r = Sidekiq::RetrySet.new
814
- # r.select do |retri|
815
- # retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
816
- # retri.args[0] == 'User' &&
817
- # retri.args[1] == 'setup_new_subscriber'
818
- # end.map(&:delete)
819
920
  class RetrySet < JobSet
820
921
  def initialize
821
- super "retry"
822
- end
823
-
824
- # Enqueues all jobs pending within the retry set.
825
- def retry_all
826
- each(&:retry) while size > 0
827
- end
828
-
829
- # Kills all jobs pending within the retry set.
830
- def kill_all
831
- each(&:kill) while size > 0
922
+ super("retry")
832
923
  end
833
924
  end
834
925
 
@@ -839,48 +930,48 @@ module Sidekiq
839
930
  #
840
931
  class DeadSet < JobSet
841
932
  def initialize
842
- super "dead"
933
+ super("dead")
934
+ end
935
+
936
+ # Trim dead jobs which are over our storage limits
937
+ def trim
938
+ hash = Sidekiq.default_configuration
939
+ now = Time.now.to_f
940
+ Sidekiq.redis do |conn|
941
+ conn.multi do |transaction|
942
+ transaction.zremrangebyscore(name, "-inf", now - hash[:dead_timeout_in_seconds])
943
+ transaction.zremrangebyrank(name, 0, - hash[:dead_max_jobs])
944
+ end
945
+ end
843
946
  end
844
947
 
845
948
  # Add the given job to the Dead set.
846
949
  # @param message [String] the job data as JSON
950
+ # @option opts [Boolean] :notify_failure (true) Whether death handlers should be called
951
+ # @option opts [Boolean] :trim (true) Whether Sidekiq should trim the structure to keep it within configuration
952
+ # @option opts [Exception] :ex (RuntimeError) An exception to pass to the death handlers
847
953
  def kill(message, opts = {})
848
954
  now = Time.now.to_f
849
955
  Sidekiq.redis do |conn|
850
- conn.multi do |transaction|
851
- transaction.zadd(name, now.to_s, message)
852
- transaction.zremrangebyscore(name, "-inf", now - self.class.timeout)
853
- transaction.zremrangebyrank(name, 0, - self.class.max_jobs)
854
- end
956
+ conn.zadd(name, now.to_s, message)
855
957
  end
856
958
 
959
+ trim if opts[:trim] != false
960
+
857
961
  if opts[:notify_failure] != false
858
962
  job = Sidekiq.load_json(message)
859
- r = RuntimeError.new("Job killed by API")
860
- r.set_backtrace(caller)
861
- Sidekiq.death_handlers.each do |handle|
862
- handle.call(job, r)
963
+ if opts[:ex]
964
+ ex = opts[:ex]
965
+ else
966
+ ex = RuntimeError.new("Job killed by API")
967
+ ex.set_backtrace(caller)
968
+ end
969
+ Sidekiq.default_configuration.death_handlers.each do |handle|
970
+ handle.call(job, ex)
863
971
  end
864
972
  end
865
973
  true
866
974
  end
867
-
868
- # Enqueue all dead jobs
869
- def retry_all
870
- each(&:retry) while size > 0
871
- end
872
-
873
- # The maximum size of the Dead set. Older entries will be trimmed
874
- # to stay within this limit. Default value is 10,000.
875
- def self.max_jobs
876
- Sidekiq[:dead_max_jobs]
877
- end
878
-
879
- # The time limit for entries within the Dead set. Older entries will be thrown away.
880
- # Default value is six months.
881
- def self.timeout
882
- Sidekiq[:dead_timeout_in_seconds]
883
- end
884
975
  end
885
976
 
886
977
  ##
@@ -893,6 +984,24 @@ module Sidekiq
893
984
  class ProcessSet
894
985
  include Enumerable
895
986
 
987
+ def self.[](identity)
988
+ exists, (info, busy, beat, quiet, rss, rtt_us) = Sidekiq.redis { |conn|
989
+ conn.multi { |transaction|
990
+ transaction.sismember("processes", identity)
991
+ transaction.hmget(identity, "info", "busy", "beat", "quiet", "rss", "rtt_us")
992
+ }
993
+ }
994
+
995
+ return nil if exists == 0 || info.nil?
996
+
997
+ hash = Sidekiq.load_json(info)
998
+ Process.new(hash.merge("busy" => busy.to_i,
999
+ "beat" => beat.to_f,
1000
+ "quiet" => quiet,
1001
+ "rss" => rss.to_i,
1002
+ "rtt_us" => rtt_us.to_i))
1003
+ end
1004
+
896
1005
  # :nodoc:
897
1006
  # @api private
898
1007
  def initialize(clean_plz = true)
@@ -905,11 +1014,11 @@ module Sidekiq
905
1014
  # @api private
906
1015
  def cleanup
907
1016
  # dont run cleanup more than once per minute
908
- return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
1017
+ return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", "NX", "EX", "60") }
909
1018
 
910
1019
  count = 0
911
1020
  Sidekiq.redis do |conn|
912
- procs = conn.sscan_each("processes").to_a
1021
+ procs = conn.sscan("processes").to_a
913
1022
  heartbeats = conn.pipelined { |pipeline|
914
1023
  procs.each do |key|
915
1024
  pipeline.hget(key, "info")
@@ -929,30 +1038,31 @@ module Sidekiq
929
1038
 
930
1039
  def each
931
1040
  result = Sidekiq.redis { |conn|
932
- procs = conn.sscan_each("processes").to_a.sort
1041
+ procs = conn.sscan("processes").to_a.sort
933
1042
 
934
1043
  # We're making a tradeoff here between consuming more memory instead of
935
1044
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
936
1045
  # you'll be happier this way
937
1046
  conn.pipelined do |pipeline|
938
1047
  procs.each do |key|
939
- pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
1048
+ pipeline.hmget(key, "info", "concurrency", "busy", "beat", "quiet", "rss", "rtt_us")
940
1049
  end
941
1050
  end
942
1051
  }
943
1052
 
944
- result.each do |info, busy, at_s, quiet, rss, rtt|
1053
+ result.each do |info, concurrency, busy, beat, quiet, rss, rtt_us|
945
1054
  # If a process is stopped between when we query Redis for `procs` and
946
1055
  # when we query for `result`, we will have an item in `result` that is
947
1056
  # composed of `nil` values.
948
1057
  next if info.nil?
949
1058
 
950
1059
  hash = Sidekiq.load_json(info)
951
- yield Process.new(hash.merge("busy" => busy.to_i,
952
- "beat" => at_s.to_f,
1060
+ yield Process.new(hash.merge("concurrency" => concurrency.to_i,
1061
+ "busy" => busy.to_i,
1062
+ "beat" => beat.to_f,
953
1063
  "quiet" => quiet,
954
1064
  "rss" => rss.to_i,
955
- "rtt_us" => rtt.to_i))
1065
+ "rtt_us" => rtt_us.to_i))
956
1066
  end
957
1067
  end
958
1068
 
@@ -1003,11 +1113,12 @@ module Sidekiq
1003
1113
  # 'started_at' => <process start time>,
1004
1114
  # 'pid' => 12345,
1005
1115
  # 'tag' => 'myapp'
1006
- # 'concurrency' => 25,
1007
- # 'queues' => ['default', 'low'],
1008
- # 'busy' => 10,
1116
+ # 'concurrency' => 5,
1117
+ # 'capsules' => {"default" => {"mode" => "weighted", "concurrency" => 5, "weights" => {"default" => 2, "low" => 1}}},
1118
+ # 'busy' => 3,
1009
1119
  # 'beat' => <last heartbeat>,
1010
1120
  # 'identity' => <unique string identifying the process>,
1121
+ # 'embedded' => true,
1011
1122
  # }
1012
1123
  class Process
1013
1124
  # :nodoc:
@@ -1021,7 +1132,7 @@ module Sidekiq
1021
1132
  end
1022
1133
 
1023
1134
  def labels
1024
- Array(self["labels"])
1135
+ self["labels"].to_a
1025
1136
  end
1026
1137
 
1027
1138
  def [](key)
@@ -1031,9 +1142,45 @@ module Sidekiq
1031
1142
  def identity
1032
1143
  self["identity"]
1033
1144
  end
1145
+ alias_method :id, :identity
1034
1146
 
1147
+ # deprecated, use capsules below
1035
1148
  def queues
1036
- 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
1155
+ end
1156
+
1157
+ # deprecated, use capsules below
1158
+ def 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"]
1176
+ end
1177
+
1178
+ def version
1179
+ self["version"]
1180
+ end
1181
+
1182
+ def embedded?
1183
+ self["embedded"]
1037
1184
  end
1038
1185
 
1039
1186
  # Signal this process to stop processing new jobs.
@@ -1041,6 +1188,8 @@ module Sidekiq
1041
1188
  # This method is *asynchronous* and it can take 5-10
1042
1189
  # seconds for the process to quiet.
1043
1190
  def quiet!
1191
+ raise "Can't quiet an embedded process" if embedded?
1192
+
1044
1193
  signal("TSTP")
1045
1194
  end
1046
1195
 
@@ -1049,6 +1198,8 @@ module Sidekiq
1049
1198
  # This method is *asynchronous* and it can take 5-10
1050
1199
  # seconds for the process to start shutting down.
1051
1200
  def stop!
1201
+ raise "Can't stop an embedded process" if embedded?
1202
+
1052
1203
  signal("TERM")
1053
1204
  end
1054
1205
 
@@ -1065,6 +1216,10 @@ module Sidekiq
1065
1216
  self["quiet"] == "true"
1066
1217
  end
1067
1218
 
1219
+ def leader?
1220
+ Sidekiq.redis { |c| c.get("dear-leader") == identity }
1221
+ end
1222
+
1068
1223
  private
1069
1224
 
1070
1225
  def signal(sig)
@@ -1093,9 +1248,8 @@ module Sidekiq
1093
1248
  # works.each do |process_id, thread_id, work|
1094
1249
  # # process_id is a unique identifier per Sidekiq process
1095
1250
  # # thread_id is a unique identifier per thread
1096
- # # work is a Hash which looks like:
1097
- # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
1098
- # # 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]
1099
1253
  # end
1100
1254
  #
1101
1255
  class WorkSet
@@ -1107,8 +1261,7 @@ module Sidekiq
1107
1261
  all_works = nil
1108
1262
 
1109
1263
  Sidekiq.redis do |conn|
1110
- procs = conn.sscan_each("processes").to_a.sort
1111
-
1264
+ procs = conn.sscan("processes").to_a.sort
1112
1265
  all_works = conn.pipelined do |pipeline|
1113
1266
  procs.each do |key|
1114
1267
  pipeline.hgetall("#{key}:work")
@@ -1118,17 +1271,11 @@ module Sidekiq
1118
1271
 
1119
1272
  procs.zip(all_works).each do |key, workers|
1120
1273
  workers.each_pair do |tid, json|
1121
- next if json.empty?
1122
-
1123
- hsh = Sidekiq.load_json(json)
1124
- p = hsh["payload"]
1125
- # avoid breaking API, this is a side effect of the JSON optimization in #4316
1126
- hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
1127
- results << [key, tid, hsh]
1274
+ results << [key, tid, Sidekiq::Work.new(key, tid, Sidekiq.load_json(json))] unless json.empty?
1128
1275
  end
1129
1276
  end
1130
1277
 
1131
- results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
1278
+ results.sort_by { |(_, _, work)| work.run_at }.each(&block)
1132
1279
  end
1133
1280
 
1134
1281
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -1139,7 +1286,7 @@ module Sidekiq
1139
1286
  # which can easily get out of sync with crashy processes.
1140
1287
  def size
1141
1288
  Sidekiq.redis do |conn|
1142
- procs = conn.sscan_each("processes").to_a
1289
+ procs = conn.sscan("processes").to_a
1143
1290
  if procs.empty?
1144
1291
  0
1145
1292
  else
@@ -1151,9 +1298,170 @@ module Sidekiq
1151
1298
  end
1152
1299
  end
1153
1300
  end
1301
+
1302
+ ##
1303
+ # Find the work which represents a job with the given JID.
1304
+ # *This is a slow O(n) operation*. Do not use for app logic.
1305
+ #
1306
+ # @param jid [String] the job identifier
1307
+ # @return [Sidekiq::Work] the work or nil
1308
+ def find_work(jid)
1309
+ each do |_process_id, _thread_id, work|
1310
+ job = work.job
1311
+ return work if job.jid == jid
1312
+ end
1313
+ nil
1314
+ end
1315
+ alias_method :find_work_by_jid, :find_work
1154
1316
  end
1317
+
1318
+ # Sidekiq::Work represents a job which is currently executing.
1319
+ class Work
1320
+ attr_reader :process_id
1321
+ attr_reader :thread_id
1322
+
1323
+ def initialize(pid, tid, hsh)
1324
+ @process_id = pid
1325
+ @thread_id = tid
1326
+ @hsh = hsh
1327
+ @job = nil
1328
+ end
1329
+
1330
+ def queue
1331
+ @hsh["queue"]
1332
+ end
1333
+
1334
+ def run_at
1335
+ Time.at(@hsh["run_at"])
1336
+ end
1337
+
1338
+ def job
1339
+ @job ||= Sidekiq::JobRecord.new(@hsh["payload"])
1340
+ end
1341
+
1342
+ def payload
1343
+ @hsh["payload"]
1344
+ end
1345
+ end
1346
+
1155
1347
  # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1156
1348
  # Is "worker" a process, a type of job, a thread? Undefined!
1157
1349
  # WorkSet better describes the data.
1158
1350
  Workers = WorkSet
1351
+
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
1364
+ end
1365
+
1366
+ def size
1367
+ @records.size
1368
+ end
1369
+
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])
1395
+ end
1396
+
1397
+ def key
1398
+ "#{token}-#{jid}"
1399
+ end
1400
+
1401
+ def data
1402
+ Sidekiq.redis { |c| c.hget(key, "data") }
1403
+ end
1404
+ end
1405
+
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
1159
1465
  end
1466
+
1467
+ Sidekiq.loader.run_load_hooks(:api)