sidekiq 6.5.1 → 7.0.9

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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +142 -12
  3. data/README.md +40 -32
  4. data/bin/sidekiq +3 -8
  5. data/bin/sidekiqload +186 -118
  6. data/bin/sidekiqmon +3 -0
  7. data/lib/sidekiq/api.rb +226 -139
  8. data/lib/sidekiq/capsule.rb +127 -0
  9. data/lib/sidekiq/cli.rb +55 -61
  10. data/lib/sidekiq/client.rb +31 -18
  11. data/lib/sidekiq/component.rb +5 -1
  12. data/lib/sidekiq/config.rb +270 -0
  13. data/lib/sidekiq/deploy.rb +62 -0
  14. data/lib/sidekiq/embedded.rb +61 -0
  15. data/lib/sidekiq/fetch.rb +11 -14
  16. data/lib/sidekiq/job.rb +375 -10
  17. data/lib/sidekiq/job_logger.rb +2 -2
  18. data/lib/sidekiq/job_retry.rb +62 -41
  19. data/lib/sidekiq/job_util.rb +48 -14
  20. data/lib/sidekiq/launcher.rb +71 -65
  21. data/lib/sidekiq/logger.rb +1 -26
  22. data/lib/sidekiq/manager.rb +9 -11
  23. data/lib/sidekiq/metrics/query.rb +153 -0
  24. data/lib/sidekiq/metrics/shared.rb +95 -0
  25. data/lib/sidekiq/metrics/tracking.rb +136 -0
  26. data/lib/sidekiq/middleware/chain.rb +84 -48
  27. data/lib/sidekiq/middleware/current_attributes.rb +12 -17
  28. data/lib/sidekiq/monitor.rb +17 -4
  29. data/lib/sidekiq/paginator.rb +9 -1
  30. data/lib/sidekiq/processor.rb +27 -27
  31. data/lib/sidekiq/rails.rb +4 -9
  32. data/lib/sidekiq/redis_client_adapter.rb +8 -47
  33. data/lib/sidekiq/redis_connection.rb +11 -113
  34. data/lib/sidekiq/scheduled.rb +60 -33
  35. data/lib/sidekiq/testing.rb +5 -33
  36. data/lib/sidekiq/transaction_aware_client.rb +4 -5
  37. data/lib/sidekiq/version.rb +2 -1
  38. data/lib/sidekiq/web/action.rb +3 -3
  39. data/lib/sidekiq/web/application.rb +40 -9
  40. data/lib/sidekiq/web/csrf_protection.rb +1 -1
  41. data/lib/sidekiq/web/helpers.rb +32 -18
  42. data/lib/sidekiq/web.rb +7 -14
  43. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  44. data/lib/sidekiq.rb +76 -266
  45. data/sidekiq.gemspec +21 -10
  46. data/web/assets/javascripts/application.js +19 -1
  47. data/web/assets/javascripts/base-charts.js +106 -0
  48. data/web/assets/javascripts/chart.min.js +13 -0
  49. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  50. data/web/assets/javascripts/dashboard-charts.js +166 -0
  51. data/web/assets/javascripts/dashboard.js +3 -240
  52. data/web/assets/javascripts/metrics.js +264 -0
  53. data/web/assets/stylesheets/application-dark.css +4 -0
  54. data/web/assets/stylesheets/application-rtl.css +2 -91
  55. data/web/assets/stylesheets/application.css +65 -297
  56. data/web/locales/ar.yml +70 -70
  57. data/web/locales/cs.yml +62 -62
  58. data/web/locales/da.yml +60 -53
  59. data/web/locales/de.yml +65 -65
  60. data/web/locales/el.yml +43 -24
  61. data/web/locales/en.yml +82 -69
  62. data/web/locales/es.yml +68 -68
  63. data/web/locales/fa.yml +65 -65
  64. data/web/locales/fr.yml +67 -67
  65. data/web/locales/gd.yml +99 -0
  66. data/web/locales/he.yml +65 -64
  67. data/web/locales/hi.yml +59 -59
  68. data/web/locales/it.yml +53 -53
  69. data/web/locales/ja.yml +73 -68
  70. data/web/locales/ko.yml +52 -52
  71. data/web/locales/lt.yml +66 -66
  72. data/web/locales/nb.yml +61 -61
  73. data/web/locales/nl.yml +52 -52
  74. data/web/locales/pl.yml +45 -45
  75. data/web/locales/pt-br.yml +59 -69
  76. data/web/locales/pt.yml +51 -51
  77. data/web/locales/ru.yml +67 -66
  78. data/web/locales/sv.yml +53 -53
  79. data/web/locales/ta.yml +60 -60
  80. data/web/locales/uk.yml +62 -61
  81. data/web/locales/ur.yml +64 -64
  82. data/web/locales/vi.yml +67 -67
  83. data/web/locales/zh-cn.yml +43 -16
  84. data/web/locales/zh-tw.yml +42 -8
  85. data/web/views/_footer.erb +5 -2
  86. data/web/views/_job_info.erb +18 -2
  87. data/web/views/_metrics_period_select.erb +12 -0
  88. data/web/views/_nav.erb +1 -1
  89. data/web/views/_paging.erb +2 -0
  90. data/web/views/_poll_link.erb +1 -1
  91. data/web/views/busy.erb +43 -27
  92. data/web/views/dashboard.erb +36 -4
  93. data/web/views/metrics.erb +82 -0
  94. data/web/views/metrics_for_job.erb +68 -0
  95. data/web/views/morgue.erb +5 -9
  96. data/web/views/queue.erb +15 -15
  97. data/web/views/queues.erb +3 -1
  98. data/web/views/retries.erb +5 -9
  99. data/web/views/scheduled.erb +12 -13
  100. metadata +60 -27
  101. data/lib/sidekiq/.DS_Store +0 -0
  102. data/lib/sidekiq/delay.rb +0 -43
  103. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  104. data/lib/sidekiq/extensions/active_record.rb +0 -43
  105. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  106. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  107. data/lib/sidekiq/worker.rb +0 -367
  108. /data/{LICENSE → LICENSE.txt} +0 -0
data/lib/sidekiq/api.rb CHANGED
@@ -3,9 +3,28 @@
3
3
  require "sidekiq"
4
4
 
5
5
  require "zlib"
6
+ require "set"
6
7
  require "base64"
7
8
 
9
+ require "sidekiq/metrics/query"
10
+
11
+ #
12
+ # Sidekiq's Data API provides a Ruby object model on top
13
+ # of Sidekiq's runtime data in Redis. This API should never
14
+ # be used within application code for business logic.
15
+ #
16
+ # The Sidekiq server process never uses this API: all data
17
+ # manipulation is done directly for performance reasons to
18
+ # ensure we are using Redis as efficiently as possible at
19
+ # every callsite.
20
+ #
21
+
8
22
  module Sidekiq
23
+ # Retrieve runtime statistics from Redis regarding
24
+ # this Sidekiq cluster.
25
+ #
26
+ # stat = Sidekiq::Stats.new
27
+ # stat.processed
9
28
  class Stats
10
29
  def initialize
11
30
  fetch_stats_fast!
@@ -48,10 +67,22 @@ module Sidekiq
48
67
  end
49
68
 
50
69
  def queues
51
- Sidekiq::Stats::Queues.new.lengths
70
+ Sidekiq.redis do |conn|
71
+ queues = conn.sscan("queues").to_a
72
+
73
+ lengths = conn.pipelined { |pipeline|
74
+ queues.each do |queue|
75
+ pipeline.llen("queue:#{queue}")
76
+ end
77
+ }
78
+
79
+ array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
80
+ array_of_arrays.to_h
81
+ end
52
82
  end
53
83
 
54
84
  # O(1) redis calls
85
+ # @api private
55
86
  def fetch_stats_fast!
56
87
  pipe1_res = Sidekiq.redis { |conn|
57
88
  conn.pipelined do |pipeline|
@@ -91,13 +122,14 @@ module Sidekiq
91
122
  end
92
123
 
93
124
  # O(number of processes + number of queues) redis calls
125
+ # @api private
94
126
  def fetch_stats_slow!
95
127
  processes = Sidekiq.redis { |conn|
96
- conn.sscan_each("processes").to_a
128
+ conn.sscan("processes").to_a
97
129
  }
98
130
 
99
131
  queues = Sidekiq.redis { |conn|
100
- conn.sscan_each("queues").to_a
132
+ conn.sscan("queues").to_a
101
133
  }
102
134
 
103
135
  pipe2_res = Sidekiq.redis { |conn|
@@ -109,18 +141,20 @@ module Sidekiq
109
141
 
110
142
  s = processes.size
111
143
  workers_size = pipe2_res[0...s].sum(&:to_i)
112
- enqueued = pipe2_res[s..-1].sum(&:to_i)
144
+ enqueued = pipe2_res[s..].sum(&:to_i)
113
145
 
114
146
  @stats[:workers_size] = workers_size
115
147
  @stats[:enqueued] = enqueued
116
148
  @stats
117
149
  end
118
150
 
151
+ # @api private
119
152
  def fetch_stats!
120
153
  fetch_stats_fast!
121
154
  fetch_stats_slow!
122
155
  end
123
156
 
157
+ # @api private
124
158
  def reset(*stats)
125
159
  all = %w[failed processed]
126
160
  stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
@@ -142,25 +176,8 @@ module Sidekiq
142
176
  @stats[s] || raise(ArgumentError, "Unknown stat #{s}")
143
177
  end
144
178
 
145
- class Queues
146
- def lengths
147
- Sidekiq.redis do |conn|
148
- queues = conn.sscan_each("queues").to_a
149
-
150
- lengths = conn.pipelined { |pipeline|
151
- queues.each do |queue|
152
- pipeline.llen("queue:#{queue}")
153
- end
154
- }
155
-
156
- array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
157
- array_of_arrays.to_h
158
- end
159
- end
160
- end
161
-
162
179
  class History
163
- def initialize(days_previous, start_date = nil)
180
+ def initialize(days_previous, start_date = nil, pool: nil)
164
181
  # we only store five years of data in Redis
165
182
  raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
166
183
  @days_previous = days_previous
@@ -185,15 +202,10 @@ module Sidekiq
185
202
 
186
203
  keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
187
204
 
188
- begin
189
- Sidekiq.redis do |conn|
190
- conn.mget(keys).each_with_index do |value, idx|
191
- stat_hash[dates[idx]] = value ? value.to_i : 0
192
- end
205
+ Sidekiq.redis do |conn|
206
+ conn.mget(keys).each_with_index do |value, idx|
207
+ stat_hash[dates[idx]] = value ? value.to_i : 0
193
208
  end
194
- rescue RedisConnection.adapter::CommandError
195
- # mget will trigger a CROSSSLOT error when run against a Cluster
196
- # TODO Someone want to add Cluster support?
197
209
  end
198
210
 
199
211
  stat_hash
@@ -202,9 +214,10 @@ module Sidekiq
202
214
  end
203
215
 
204
216
  ##
205
- # Encapsulates a queue within Sidekiq.
217
+ # Represents a queue within Sidekiq.
206
218
  # Allows enumeration of all jobs within the queue
207
- # and deletion of jobs.
219
+ # and deletion of jobs. NB: this queue data is real-time
220
+ # and is changing within Redis moment by moment.
208
221
  #
209
222
  # queue = Sidekiq::Queue.new("mailer")
210
223
  # queue.each do |job|
@@ -212,7 +225,6 @@ module Sidekiq
212
225
  # job.args # => [1, 2, 3]
213
226
  # job.delete if job.jid == 'abcdef1234567890'
214
227
  # end
215
- #
216
228
  class Queue
217
229
  include Enumerable
218
230
 
@@ -221,7 +233,7 @@ module Sidekiq
221
233
  #
222
234
  # @return [Array<Sidekiq::Queue>]
223
235
  def self.all
224
- Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
236
+ Sidekiq.redis { |c| c.sscan("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
225
237
  end
226
238
 
227
239
  attr_reader :name
@@ -296,41 +308,53 @@ module Sidekiq
296
308
  end
297
309
 
298
310
  # delete all jobs within this queue
311
+ # @return [Boolean] true
299
312
  def clear
300
313
  Sidekiq.redis do |conn|
301
314
  conn.multi do |transaction|
302
315
  transaction.unlink(@rname)
303
- transaction.srem("queues", name)
316
+ transaction.srem("queues", [name])
304
317
  end
305
318
  end
319
+ true
306
320
  end
307
321
  alias_method :💣, :clear
308
322
 
309
- def as_json(options = nil) # :nodoc:
323
+ # :nodoc:
324
+ # @api private
325
+ def as_json(options = nil)
310
326
  {name: name} # 5336
311
327
  end
312
328
  end
313
329
 
314
330
  ##
315
- # Encapsulates a pending job within a Sidekiq queue or
316
- # sorted set.
331
+ # Represents a pending job within a Sidekiq queue.
317
332
  #
318
333
  # The job should be considered immutable but may be
319
334
  # removed from the queue via JobRecord#delete.
320
- #
321
335
  class JobRecord
336
+ # the parsed Hash of job data
337
+ # @!attribute [r] Item
322
338
  attr_reader :item
339
+ # the underlying String in Redis
340
+ # @!attribute [r] Value
323
341
  attr_reader :value
342
+ # the queue associated with this job
343
+ # @!attribute [r] Queue
324
344
  attr_reader :queue
325
345
 
326
- def initialize(item, queue_name = nil) # :nodoc:
346
+ # :nodoc:
347
+ # @api private
348
+ def initialize(item, queue_name = nil)
327
349
  @args = nil
328
350
  @value = item
329
351
  @item = item.is_a?(Hash) ? item : parse(item)
330
352
  @queue = queue_name || @item["queue"]
331
353
  end
332
354
 
333
- def parse(item) # :nodoc:
355
+ # :nodoc:
356
+ # @api private
357
+ def parse(item)
334
358
  Sidekiq.load_json(item)
335
359
  rescue JSON::ParserError
336
360
  # If the job payload in Redis is invalid JSON, we'll load
@@ -341,6 +365,8 @@ module Sidekiq
341
365
  {}
342
366
  end
343
367
 
368
+ # This is the job class which Sidekiq will execute. If using ActiveJob,
369
+ # this class will be the ActiveJob adapter class rather than a specific job.
344
370
  def klass
345
371
  self["class"]
346
372
  end
@@ -348,12 +374,7 @@ module Sidekiq
348
374
  def display_class
349
375
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
350
376
  @klass ||= self["display_class"] || begin
351
- case klass
352
- when /\ASidekiq::Extensions::Delayed/
353
- safe_load(args[0], klass) do |target, method, _|
354
- "#{target}.#{method}"
355
- end
356
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
377
+ if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
357
378
  job_class = @item["wrapped"] || args[0]
358
379
  if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
359
380
  # MailerClass#mailer_method
@@ -369,16 +390,7 @@ module Sidekiq
369
390
 
370
391
  def display_args
371
392
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
372
- @display_args ||= case klass
373
- when /\ASidekiq::Extensions::Delayed/
374
- safe_load(args[0], args) do |_, _, arg, kwarg|
375
- if !kwarg || kwarg.empty?
376
- arg
377
- else
378
- [arg, kwarg]
379
- end
380
- end
381
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
393
+ @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
382
394
  job_args = self["wrapped"] ? args[0]["arguments"] : []
383
395
  if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
384
396
  # remove MailerClass, mailer_method and 'deliver_now'
@@ -406,6 +418,10 @@ module Sidekiq
406
418
  self["jid"]
407
419
  end
408
420
 
421
+ def bid
422
+ self["bid"]
423
+ end
424
+
409
425
  def enqueued_at
410
426
  self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
411
427
  end
@@ -451,50 +467,35 @@ module Sidekiq
451
467
 
452
468
  private
453
469
 
454
- def safe_load(content, default)
455
- yield(*YAML.load(content))
456
- rescue => ex
457
- # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
458
- # memory yet so the YAML can't be loaded.
459
- # TODO is this still necessary? Zeitwerk reloader should handle?
460
- Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.config[:environment] == "development"
461
- default
462
- end
463
-
464
470
  def uncompress_backtrace(backtrace)
465
- if backtrace.is_a?(Array)
466
- # Handle old jobs with raw Array backtrace format
467
- backtrace
468
- else
469
- decoded = Base64.decode64(backtrace)
470
- uncompressed = Zlib::Inflate.inflate(decoded)
471
- begin
472
- Sidekiq.load_json(uncompressed)
473
- rescue
474
- # Handle old jobs with marshalled backtrace format
475
- # TODO Remove in 7.x
476
- Marshal.load(uncompressed)
477
- end
478
- end
471
+ decoded = Base64.decode64(backtrace)
472
+ uncompressed = Zlib::Inflate.inflate(decoded)
473
+ Sidekiq.load_json(uncompressed)
479
474
  end
480
475
  end
481
476
 
482
477
  # Represents a job within a Redis sorted set where the score
483
- # represents a timestamp for the job.
478
+ # represents a timestamp associated with the job. This timestamp
479
+ # could be the scheduled time for it to run (e.g. scheduled set),
480
+ # or the expiration date after which the entry should be deleted (e.g. dead set).
484
481
  class SortedEntry < JobRecord
485
482
  attr_reader :score
486
483
  attr_reader :parent
487
484
 
488
- def initialize(parent, score, item) # :nodoc:
485
+ # :nodoc:
486
+ # @api private
487
+ def initialize(parent, score, item)
489
488
  super(item)
490
489
  @score = Float(score)
491
490
  @parent = parent
492
491
  end
493
492
 
493
+ # The timestamp associated with this entry
494
494
  def at
495
495
  Time.at(score).utc
496
496
  end
497
497
 
498
+ # remove this entry from the sorted set
498
499
  def delete
499
500
  if @value
500
501
  @parent.delete_by_value(@parent.name, @value)
@@ -505,7 +506,7 @@ module Sidekiq
505
506
 
506
507
  # Change the scheduled time for this job.
507
508
  #
508
- # @param [Time] the new timestamp when this job will be enqueued.
509
+ # @param at [Time] the new timestamp for this job
509
510
  def reschedule(at)
510
511
  Sidekiq.redis do |conn|
511
512
  conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
@@ -579,47 +580,69 @@ module Sidekiq
579
580
  end
580
581
  end
581
582
 
583
+ # Base class for all sorted sets within Sidekiq.
582
584
  class SortedSet
583
585
  include Enumerable
584
586
 
587
+ # Redis key of the set
588
+ # @!attribute [r] Name
585
589
  attr_reader :name
586
590
 
591
+ # :nodoc:
592
+ # @api private
587
593
  def initialize(name)
588
594
  @name = name
589
595
  @_size = size
590
596
  end
591
597
 
598
+ # real-time size of the set, will change
592
599
  def size
593
600
  Sidekiq.redis { |c| c.zcard(name) }
594
601
  end
595
602
 
603
+ # Scan through each element of the sorted set, yielding each to the supplied block.
604
+ # Please see Redis's <a href="https://redis.io/commands/scan/">SCAN documentation</a> for implementation details.
605
+ #
606
+ # @param match [String] a snippet or regexp to filter matches.
607
+ # @param count [Integer] number of elements to retrieve at a time, default 100
608
+ # @yieldparam [Sidekiq::SortedEntry] each entry
596
609
  def scan(match, count = 100)
597
610
  return to_enum(:scan, match, count) unless block_given?
598
611
 
599
612
  match = "*#{match}*" unless match.include?("*")
600
613
  Sidekiq.redis do |conn|
601
- conn.zscan_each(name, match: match, count: count) do |entry, score|
614
+ conn.zscan(name, match: match, count: count) do |entry, score|
602
615
  yield SortedEntry.new(self, score, entry)
603
616
  end
604
617
  end
605
618
  end
606
619
 
620
+ # @return [Boolean] always true
607
621
  def clear
608
622
  Sidekiq.redis do |conn|
609
623
  conn.unlink(name)
610
624
  end
625
+ true
611
626
  end
612
627
  alias_method :💣, :clear
613
628
 
614
- def as_json(options = nil) # :nodoc:
629
+ # :nodoc:
630
+ # @api private
631
+ def as_json(options = nil)
615
632
  {name: name} # 5336
616
633
  end
617
634
  end
618
635
 
636
+ # Base class for all sorted sets which contain jobs, e.g. scheduled, retry and dead.
637
+ # Sidekiq Pro and Enterprise add additional sorted sets which do not contain job data,
638
+ # e.g. Batches.
619
639
  class JobSet < SortedSet
620
- def schedule(timestamp, message)
640
+ # Add a job with the associated timestamp to this set.
641
+ # @param timestamp [Time] the score for the job
642
+ # @param job [Hash] the job data
643
+ def schedule(timestamp, job)
621
644
  Sidekiq.redis do |conn|
622
- conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
645
+ conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(job))
623
646
  end
624
647
  end
625
648
 
@@ -647,6 +670,10 @@ module Sidekiq
647
670
  ##
648
671
  # Fetch jobs that match a given time or Range. Job ID is an
649
672
  # optional second argument.
673
+ #
674
+ # @param score [Time,Range] a specific timestamp or range
675
+ # @param jid [String, optional] find a specific JID within the score
676
+ # @return [Array<SortedEntry>] any results found, can be empty
650
677
  def fetch(score, jid = nil)
651
678
  begin_score, end_score =
652
679
  if score.is_a?(Range)
@@ -668,11 +695,14 @@ module Sidekiq
668
695
 
669
696
  ##
670
697
  # Find the job with the given JID within this sorted set.
671
- # This is a slower O(n) operation. Do not use for app logic.
698
+ # *This is a slow O(n) operation*. Do not use for app logic.
699
+ #
700
+ # @param jid [String] the job identifier
701
+ # @return [SortedEntry] the record or nil
672
702
  def find_job(jid)
673
703
  Sidekiq.redis do |conn|
674
- conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
675
- job = JSON.parse(entry)
704
+ conn.zscan(name, match: "*#{jid}*", count: 100) do |entry, score|
705
+ job = Sidekiq.load_json(entry)
676
706
  matched = job["jid"] == jid
677
707
  return SortedEntry.new(self, score, entry) if matched
678
708
  end
@@ -680,6 +710,8 @@ module Sidekiq
680
710
  nil
681
711
  end
682
712
 
713
+ # :nodoc:
714
+ # @api private
683
715
  def delete_by_value(name, value)
684
716
  Sidekiq.redis do |conn|
685
717
  ret = conn.zrem(name, value)
@@ -688,6 +720,8 @@ module Sidekiq
688
720
  end
689
721
  end
690
722
 
723
+ # :nodoc:
724
+ # @api private
691
725
  def delete_by_jid(score, jid)
692
726
  Sidekiq.redis do |conn|
693
727
  elements = conn.zrangebyscore(name, score, score)
@@ -708,17 +742,13 @@ module Sidekiq
708
742
  end
709
743
 
710
744
  ##
711
- # Allows enumeration of scheduled jobs within Sidekiq.
745
+ # The set of scheduled jobs within Sidekiq.
712
746
  # Based on this, you can search/filter for jobs. Here's an
713
- # example where I'm selecting all jobs of a certain type
714
- # and deleting them from the schedule queue.
747
+ # example where I'm selecting jobs based on some complex logic
748
+ # and deleting them from the scheduled set.
749
+ #
750
+ # See the API wiki page for usage notes and examples.
715
751
  #
716
- # r = Sidekiq::ScheduledSet.new
717
- # r.select do |scheduled|
718
- # scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
719
- # scheduled.args[0] == 'User' &&
720
- # scheduled.args[1] == 'setup_new_subscriber'
721
- # end.map(&:delete)
722
752
  class ScheduledSet < JobSet
723
753
  def initialize
724
754
  super "schedule"
@@ -726,46 +756,48 @@ module Sidekiq
726
756
  end
727
757
 
728
758
  ##
729
- # Allows enumeration of retries within Sidekiq.
759
+ # The set of retries within Sidekiq.
730
760
  # Based on this, you can search/filter for jobs. Here's an
731
761
  # example where I'm selecting all jobs of a certain type
732
762
  # and deleting them from the retry queue.
733
763
  #
734
- # r = Sidekiq::RetrySet.new
735
- # r.select do |retri|
736
- # retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
737
- # retri.args[0] == 'User' &&
738
- # retri.args[1] == 'setup_new_subscriber'
739
- # end.map(&:delete)
764
+ # See the API wiki page for usage notes and examples.
765
+ #
740
766
  class RetrySet < JobSet
741
767
  def initialize
742
768
  super "retry"
743
769
  end
744
770
 
771
+ # Enqueues all jobs pending within the retry set.
745
772
  def retry_all
746
773
  each(&:retry) while size > 0
747
774
  end
748
775
 
776
+ # Kills all jobs pending within the retry set.
749
777
  def kill_all
750
778
  each(&:kill) while size > 0
751
779
  end
752
780
  end
753
781
 
754
782
  ##
755
- # Allows enumeration of dead jobs within Sidekiq.
783
+ # The set of dead jobs within Sidekiq. Dead jobs have failed all of
784
+ # their retries and are helding in this set pending some sort of manual
785
+ # fix. They will be removed after 6 months (dead_timeout) if not.
756
786
  #
757
787
  class DeadSet < JobSet
758
788
  def initialize
759
789
  super "dead"
760
790
  end
761
791
 
792
+ # Add the given job to the Dead set.
793
+ # @param message [String] the job data as JSON
762
794
  def kill(message, opts = {})
763
795
  now = Time.now.to_f
764
796
  Sidekiq.redis do |conn|
765
797
  conn.multi do |transaction|
766
798
  transaction.zadd(name, now.to_s, message)
767
- transaction.zremrangebyscore(name, "-inf", now - self.class.timeout)
768
- transaction.zremrangebyrank(name, 0, - self.class.max_jobs)
799
+ transaction.zremrangebyscore(name, "-inf", now - Sidekiq::Config::DEFAULTS[:dead_timeout_in_seconds])
800
+ transaction.zremrangebyrank(name, 0, - Sidekiq::Config::DEFAULTS[:dead_max_jobs])
769
801
  end
770
802
  end
771
803
 
@@ -773,24 +805,17 @@ module Sidekiq
773
805
  job = Sidekiq.load_json(message)
774
806
  r = RuntimeError.new("Job killed by API")
775
807
  r.set_backtrace(caller)
776
- Sidekiq.death_handlers.each do |handle|
808
+ Sidekiq.default_configuration.death_handlers.each do |handle|
777
809
  handle.call(job, r)
778
810
  end
779
811
  end
780
812
  true
781
813
  end
782
814
 
815
+ # Enqueue all dead jobs
783
816
  def retry_all
784
817
  each(&:retry) while size > 0
785
818
  end
786
-
787
- def self.max_jobs
788
- Sidekiq[:dead_max_jobs]
789
- end
790
-
791
- def self.timeout
792
- Sidekiq[:dead_timeout_in_seconds]
793
- end
794
819
  end
795
820
 
796
821
  ##
@@ -798,21 +823,46 @@ module Sidekiq
798
823
  # right now. Each process sends a heartbeat to Redis every 5 seconds
799
824
  # so this set should be relatively accurate, barring network partitions.
800
825
  #
801
- # Yields a Sidekiq::Process.
826
+ # @yieldparam [Sidekiq::Process]
802
827
  #
803
828
  class ProcessSet
804
829
  include Enumerable
805
830
 
831
+ def self.[](identity)
832
+ exists, (info, busy, beat, quiet, rss, rtt_us) = Sidekiq.redis { |conn|
833
+ conn.multi { |transaction|
834
+ transaction.sismember("processes", identity)
835
+ transaction.hmget(identity, "info", "busy", "beat", "quiet", "rss", "rtt_us")
836
+ }
837
+ }
838
+
839
+ return nil if exists == 0 || info.nil?
840
+
841
+ hash = Sidekiq.load_json(info)
842
+ Process.new(hash.merge("busy" => busy.to_i,
843
+ "beat" => beat.to_f,
844
+ "quiet" => quiet,
845
+ "rss" => rss.to_i,
846
+ "rtt_us" => rtt_us.to_i))
847
+ end
848
+
849
+ # :nodoc:
850
+ # @api private
806
851
  def initialize(clean_plz = true)
807
852
  cleanup if clean_plz
808
853
  end
809
854
 
810
855
  # Cleans up dead processes recorded in Redis.
811
856
  # Returns the number of processes cleaned.
857
+ # :nodoc:
858
+ # @api private
812
859
  def cleanup
860
+ # dont run cleanup more than once per minute
861
+ return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
862
+
813
863
  count = 0
814
864
  Sidekiq.redis do |conn|
815
- procs = conn.sscan_each("processes").to_a.sort
865
+ procs = conn.sscan("processes").to_a
816
866
  heartbeats = conn.pipelined { |pipeline|
817
867
  procs.each do |key|
818
868
  pipeline.hget(key, "info")
@@ -832,7 +882,7 @@ module Sidekiq
832
882
 
833
883
  def each
834
884
  result = Sidekiq.redis { |conn|
835
- procs = conn.sscan_each("processes").to_a.sort
885
+ procs = conn.sscan("processes").to_a.sort
836
886
 
837
887
  # We're making a tradeoff here between consuming more memory instead of
838
888
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
@@ -844,7 +894,7 @@ module Sidekiq
844
894
  end
845
895
  }
846
896
 
847
- result.each do |info, busy, at_s, quiet, rss, rtt|
897
+ result.each do |info, busy, beat, quiet, rss, rtt_us|
848
898
  # If a process is stopped between when we query Redis for `procs` and
849
899
  # when we query for `result`, we will have an item in `result` that is
850
900
  # composed of `nil` values.
@@ -852,10 +902,10 @@ module Sidekiq
852
902
 
853
903
  hash = Sidekiq.load_json(info)
854
904
  yield Process.new(hash.merge("busy" => busy.to_i,
855
- "beat" => at_s.to_f,
905
+ "beat" => beat.to_f,
856
906
  "quiet" => quiet,
857
907
  "rss" => rss.to_i,
858
- "rtt_us" => rtt.to_i))
908
+ "rtt_us" => rtt_us.to_i))
859
909
  end
860
910
  end
861
911
 
@@ -863,6 +913,7 @@ module Sidekiq
863
913
  # based on current heartbeat. #each does that and ensures the set only
864
914
  # contains Sidekiq processes which have sent a heartbeat within the last
865
915
  # 60 seconds.
916
+ # @return [Integer] current number of registered Sidekiq processes
866
917
  def size
867
918
  Sidekiq.redis { |conn| conn.scard("processes") }
868
919
  end
@@ -870,10 +921,12 @@ module Sidekiq
870
921
  # Total number of threads available to execute jobs.
871
922
  # For Sidekiq Enterprise customers this number (in production) must be
872
923
  # less than or equal to your licensed concurrency.
924
+ # @return [Integer] the sum of process concurrency
873
925
  def total_concurrency
874
926
  sum { |x| x["concurrency"].to_i }
875
927
  end
876
928
 
929
+ # @return [Integer] total amount of RSS memory consumed by Sidekiq processes
877
930
  def total_rss_in_kb
878
931
  sum { |x| x["rss"].to_i }
879
932
  end
@@ -882,6 +935,8 @@ module Sidekiq
882
935
  # Returns the identity of the current cluster leader or "" if no leader.
883
936
  # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
884
937
  # or Sidekiq Pro.
938
+ # @return [String] Identity of cluster leader
939
+ # @return [String] empty string if no leader
885
940
  def leader
886
941
  @leader ||= begin
887
942
  x = Sidekiq.redis { |c| c.get("dear-leader") }
@@ -906,8 +961,11 @@ module Sidekiq
906
961
  # 'busy' => 10,
907
962
  # 'beat' => <last heartbeat>,
908
963
  # 'identity' => <unique string identifying the process>,
964
+ # 'embedded' => true,
909
965
  # }
910
966
  class Process
967
+ # :nodoc:
968
+ # @api private
911
969
  def initialize(hash)
912
970
  @attribs = hash
913
971
  end
@@ -917,7 +975,7 @@ module Sidekiq
917
975
  end
918
976
 
919
977
  def labels
920
- Array(self["labels"])
978
+ self["labels"].to_a
921
979
  end
922
980
 
923
981
  def [](key)
@@ -932,18 +990,47 @@ module Sidekiq
932
990
  self["queues"]
933
991
  end
934
992
 
993
+ def weights
994
+ self["weights"]
995
+ end
996
+
997
+ def version
998
+ self["version"]
999
+ end
1000
+
1001
+ def embedded?
1002
+ self["embedded"]
1003
+ end
1004
+
1005
+ # Signal this process to stop processing new jobs.
1006
+ # It will continue to execute jobs it has already fetched.
1007
+ # This method is *asynchronous* and it can take 5-10
1008
+ # seconds for the process to quiet.
935
1009
  def quiet!
1010
+ raise "Can't quiet an embedded process" if embedded?
1011
+
936
1012
  signal("TSTP")
937
1013
  end
938
1014
 
1015
+ # Signal this process to shutdown.
1016
+ # It will shutdown within its configured :timeout value, default 25 seconds.
1017
+ # This method is *asynchronous* and it can take 5-10
1018
+ # seconds for the process to start shutting down.
939
1019
  def stop!
1020
+ raise "Can't stop an embedded process" if embedded?
1021
+
940
1022
  signal("TERM")
941
1023
  end
942
1024
 
1025
+ # Signal this process to log backtraces for all threads.
1026
+ # Useful if you have a frozen or deadlocked process which is
1027
+ # still sending a heartbeat.
1028
+ # This method is *asynchronous* and it can take 5-10 seconds.
943
1029
  def dump_threads
944
1030
  signal("TTIN")
945
1031
  end
946
1032
 
1033
+ # @return [Boolean] true if this process is quiet or shutting down
947
1034
  def stopping?
948
1035
  self["quiet"] == "true"
949
1036
  end
@@ -986,24 +1073,24 @@ module Sidekiq
986
1073
 
987
1074
  def each(&block)
988
1075
  results = []
1076
+ procs = nil
1077
+ all_works = nil
1078
+
989
1079
  Sidekiq.redis do |conn|
990
- procs = conn.sscan_each("processes").to_a
991
- procs.sort.each do |key|
992
- valid, workers = conn.pipelined { |pipeline|
993
- pipeline.exists?(key)
1080
+ procs = conn.sscan("processes").to_a.sort
1081
+ all_works = conn.pipelined do |pipeline|
1082
+ procs.each do |key|
994
1083
  pipeline.hgetall("#{key}:work")
995
- }
996
- next unless valid
997
- workers.each_pair do |tid, json|
998
- hsh = Sidekiq.load_json(json)
999
- p = hsh["payload"]
1000
- # avoid breaking API, this is a side effect of the JSON optimization in #4316
1001
- hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
1002
- results << [key, tid, hsh]
1003
1084
  end
1004
1085
  end
1005
1086
  end
1006
1087
 
1088
+ procs.zip(all_works).each do |key, workers|
1089
+ workers.each_pair do |tid, json|
1090
+ results << [key, tid, Sidekiq.load_json(json)] unless json.empty?
1091
+ end
1092
+ end
1093
+
1007
1094
  results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
1008
1095
  end
1009
1096
 
@@ -1015,7 +1102,7 @@ module Sidekiq
1015
1102
  # which can easily get out of sync with crashy processes.
1016
1103
  def size
1017
1104
  Sidekiq.redis do |conn|
1018
- procs = conn.sscan_each("processes").to_a
1105
+ procs = conn.sscan("processes").to_a
1019
1106
  if procs.empty?
1020
1107
  0
1021
1108
  else