sidekiq 6.2.2 → 6.5.5

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +150 -1
  3. data/LICENSE +3 -3
  4. data/README.md +9 -4
  5. data/bin/sidekiq +3 -3
  6. data/bin/sidekiqload +70 -66
  7. data/bin/sidekiqmon +1 -1
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +232 -94
  13. data/lib/sidekiq/cli.rb +60 -40
  14. data/lib/sidekiq/client.rb +46 -66
  15. data/lib/sidekiq/{util.rb → component.rb} +12 -42
  16. data/lib/sidekiq/delay.rb +3 -1
  17. data/lib/sidekiq/extensions/generic_proxy.rb +1 -1
  18. data/lib/sidekiq/fetch.rb +22 -19
  19. data/lib/sidekiq/job.rb +8 -3
  20. data/lib/sidekiq/job_logger.rb +15 -27
  21. data/lib/sidekiq/job_retry.rb +78 -55
  22. data/lib/sidekiq/job_util.rb +71 -0
  23. data/lib/sidekiq/launcher.rb +62 -54
  24. data/lib/sidekiq/logger.rb +8 -18
  25. data/lib/sidekiq/manager.rb +35 -34
  26. data/lib/sidekiq/metrics/deploy.rb +47 -0
  27. data/lib/sidekiq/metrics/query.rb +153 -0
  28. data/lib/sidekiq/metrics/shared.rb +94 -0
  29. data/lib/sidekiq/metrics/tracking.rb +134 -0
  30. data/lib/sidekiq/middleware/chain.rb +82 -38
  31. data/lib/sidekiq/middleware/current_attributes.rb +63 -0
  32. data/lib/sidekiq/middleware/i18n.rb +6 -4
  33. data/lib/sidekiq/middleware/modules.rb +21 -0
  34. data/lib/sidekiq/monitor.rb +1 -1
  35. data/lib/sidekiq/paginator.rb +8 -8
  36. data/lib/sidekiq/processor.rb +47 -41
  37. data/lib/sidekiq/rails.rb +22 -4
  38. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  39. data/lib/sidekiq/redis_connection.rb +85 -54
  40. data/lib/sidekiq/ring_buffer.rb +29 -0
  41. data/lib/sidekiq/scheduled.rb +54 -30
  42. data/lib/sidekiq/testing/inline.rb +4 -4
  43. data/lib/sidekiq/testing.rb +37 -36
  44. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  45. data/lib/sidekiq/version.rb +1 -1
  46. data/lib/sidekiq/web/action.rb +3 -3
  47. data/lib/sidekiq/web/application.rb +25 -9
  48. data/lib/sidekiq/web/csrf_protection.rb +2 -2
  49. data/lib/sidekiq/web/helpers.rb +30 -18
  50. data/lib/sidekiq/web.rb +8 -4
  51. data/lib/sidekiq/worker.rb +136 -13
  52. data/lib/sidekiq.rb +114 -31
  53. data/sidekiq.gemspec +2 -2
  54. data/web/assets/javascripts/application.js +113 -60
  55. data/web/assets/javascripts/chart.min.js +13 -0
  56. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  57. data/web/assets/javascripts/dashboard.js +50 -67
  58. data/web/assets/javascripts/graph.js +16 -0
  59. data/web/assets/javascripts/metrics.js +262 -0
  60. data/web/assets/stylesheets/application-dark.css +19 -23
  61. data/web/assets/stylesheets/application-rtl.css +0 -4
  62. data/web/assets/stylesheets/application.css +55 -109
  63. data/web/locales/el.yml +43 -19
  64. data/web/locales/en.yml +8 -1
  65. data/web/locales/pt-br.yml +27 -9
  66. data/web/views/_footer.erb +1 -1
  67. data/web/views/_nav.erb +1 -1
  68. data/web/views/_poll_link.erb +2 -5
  69. data/web/views/_summary.erb +7 -7
  70. data/web/views/busy.erb +4 -4
  71. data/web/views/dashboard.erb +9 -8
  72. data/web/views/layout.erb +1 -1
  73. data/web/views/metrics.erb +69 -0
  74. data/web/views/metrics_for_job.erb +87 -0
  75. data/web/views/queue.erb +14 -10
  76. data/web/views/queues.erb +1 -1
  77. metadata +27 -12
  78. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  79. data/lib/sidekiq/exception_handler.rb +0 -27
data/lib/sidekiq/api.rb CHANGED
@@ -3,9 +3,20 @@
3
3
  require "sidekiq"
4
4
 
5
5
  require "zlib"
6
+ require "set"
6
7
  require "base64"
7
8
 
9
+ if ENV["SIDEKIQ_METRICS_BETA"]
10
+ require "sidekiq/metrics/deploy"
11
+ require "sidekiq/metrics/query"
12
+ end
13
+
8
14
  module Sidekiq
15
+ # Retrieve runtime statistics from Redis regarding
16
+ # this Sidekiq cluster.
17
+ #
18
+ # stat = Sidekiq::Stats.new
19
+ # stat.processed
9
20
  class Stats
10
21
  def initialize
11
22
  fetch_stats_fast!
@@ -52,16 +63,17 @@ module Sidekiq
52
63
  end
53
64
 
54
65
  # O(1) redis calls
66
+ # @api private
55
67
  def fetch_stats_fast!
56
68
  pipe1_res = Sidekiq.redis { |conn|
57
- conn.pipelined do
58
- conn.get("stat:processed")
59
- conn.get("stat:failed")
60
- conn.zcard("schedule")
61
- conn.zcard("retry")
62
- conn.zcard("dead")
63
- conn.scard("processes")
64
- conn.lrange("queue:default", -1, -1)
69
+ conn.pipelined do |pipeline|
70
+ pipeline.get("stat:processed")
71
+ pipeline.get("stat:failed")
72
+ pipeline.zcard("schedule")
73
+ pipeline.zcard("retry")
74
+ pipeline.zcard("dead")
75
+ pipeline.scard("processes")
76
+ pipeline.lrange("queue:default", -1, -1)
65
77
  end
66
78
  }
67
79
 
@@ -91,6 +103,7 @@ module Sidekiq
91
103
  end
92
104
 
93
105
  # O(number of processes + number of queues) redis calls
106
+ # @api private
94
107
  def fetch_stats_slow!
95
108
  processes = Sidekiq.redis { |conn|
96
109
  conn.sscan_each("processes").to_a
@@ -101,9 +114,9 @@ module Sidekiq
101
114
  }
102
115
 
103
116
  pipe2_res = Sidekiq.redis { |conn|
104
- conn.pipelined do
105
- processes.each { |key| conn.hget(key, "busy") }
106
- queues.each { |queue| conn.llen("queue:#{queue}") }
117
+ conn.pipelined do |pipeline|
118
+ processes.each { |key| pipeline.hget(key, "busy") }
119
+ queues.each { |queue| pipeline.llen("queue:#{queue}") }
107
120
  end
108
121
  }
109
122
 
@@ -113,13 +126,16 @@ module Sidekiq
113
126
 
114
127
  @stats[:workers_size] = workers_size
115
128
  @stats[:enqueued] = enqueued
129
+ @stats
116
130
  end
117
131
 
132
+ # @api private
118
133
  def fetch_stats!
119
134
  fetch_stats_fast!
120
135
  fetch_stats_slow!
121
136
  end
122
137
 
138
+ # @api private
123
139
  def reset(*stats)
124
140
  all = %w[failed processed]
125
141
  stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
@@ -146,9 +162,9 @@ module Sidekiq
146
162
  Sidekiq.redis do |conn|
147
163
  queues = conn.sscan_each("queues").to_a
148
164
 
149
- lengths = conn.pipelined {
165
+ lengths = conn.pipelined { |pipeline|
150
166
  queues.each do |queue|
151
- conn.llen("queue:#{queue}")
167
+ pipeline.llen("queue:#{queue}")
152
168
  end
153
169
  }
154
170
 
@@ -160,6 +176,8 @@ module Sidekiq
160
176
 
161
177
  class History
162
178
  def initialize(days_previous, start_date = nil)
179
+ # we only store five years of data in Redis
180
+ raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
163
181
  @days_previous = days_previous
164
182
  @start_date = start_date || Time.now.utc.to_date
165
183
  end
@@ -188,7 +206,7 @@ module Sidekiq
188
206
  stat_hash[dates[idx]] = value ? value.to_i : 0
189
207
  end
190
208
  end
191
- rescue Redis::CommandError
209
+ rescue RedisConnection.adapter::CommandError
192
210
  # mget will trigger a CROSSSLOT error when run against a Cluster
193
211
  # TODO Someone want to add Cluster support?
194
212
  end
@@ -199,9 +217,10 @@ module Sidekiq
199
217
  end
200
218
 
201
219
  ##
202
- # Encapsulates a queue within Sidekiq.
220
+ # Represents a queue within Sidekiq.
203
221
  # Allows enumeration of all jobs within the queue
204
- # and deletion of jobs.
222
+ # and deletion of jobs. NB: this queue data is real-time
223
+ # and is changing within Redis moment by moment.
205
224
  #
206
225
  # queue = Sidekiq::Queue.new("mailer")
207
226
  # queue.each do |job|
@@ -209,29 +228,34 @@ module Sidekiq
209
228
  # job.args # => [1, 2, 3]
210
229
  # job.delete if job.jid == 'abcdef1234567890'
211
230
  # end
212
- #
213
231
  class Queue
214
232
  include Enumerable
215
233
 
216
234
  ##
217
- # Return all known queues within Redis.
235
+ # Fetch all known queues within Redis.
218
236
  #
237
+ # @return [Array<Sidekiq::Queue>]
219
238
  def self.all
220
239
  Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
221
240
  end
222
241
 
223
242
  attr_reader :name
224
243
 
244
+ # @param name [String] the name of the queue
225
245
  def initialize(name = "default")
226
246
  @name = name.to_s
227
247
  @rname = "queue:#{name}"
228
248
  end
229
249
 
250
+ # The current size of the queue within Redis.
251
+ # This value is real-time and can change between calls.
252
+ #
253
+ # @return [Integer] the size
230
254
  def size
231
255
  Sidekiq.redis { |con| con.llen(@rname) }
232
256
  end
233
257
 
234
- # Sidekiq Pro overrides this
258
+ # @return [Boolean] if the queue is currently paused
235
259
  def paused?
236
260
  false
237
261
  end
@@ -240,7 +264,7 @@ module Sidekiq
240
264
  # Calculates this queue's latency, the difference in seconds since the oldest
241
265
  # job in the queue was enqueued.
242
266
  #
243
- # @return Float
267
+ # @return [Float] in seconds
244
268
  def latency
245
269
  entry = Sidekiq.redis { |conn|
246
270
  conn.lrange(@rname, -1, -1)
@@ -276,34 +300,54 @@ module Sidekiq
276
300
  ##
277
301
  # Find the job with the given JID within this queue.
278
302
  #
279
- # This is a slow, inefficient operation. Do not use under
303
+ # This is a *slow, inefficient* operation. Do not use under
280
304
  # normal conditions.
305
+ #
306
+ # @param jid [String] the job_id to look for
307
+ # @return [Sidekiq::JobRecord]
308
+ # @return [nil] if not found
281
309
  def find_job(jid)
282
310
  detect { |j| j.jid == jid }
283
311
  end
284
312
 
313
+ # delete all jobs within this queue
314
+ # @return [Boolean] true
285
315
  def clear
286
316
  Sidekiq.redis do |conn|
287
- conn.multi do
288
- conn.unlink(@rname)
289
- conn.srem("queues", name)
317
+ conn.multi do |transaction|
318
+ transaction.unlink(@rname)
319
+ transaction.srem("queues", name)
290
320
  end
291
321
  end
322
+ true
292
323
  end
293
324
  alias_method :💣, :clear
325
+
326
+ # :nodoc:
327
+ # @api private
328
+ def as_json(options = nil)
329
+ {name: name} # 5336
330
+ end
294
331
  end
295
332
 
296
333
  ##
297
- # Encapsulates a pending job within a Sidekiq queue or
298
- # sorted set.
334
+ # Represents a pending job within a Sidekiq queue.
299
335
  #
300
336
  # The job should be considered immutable but may be
301
337
  # removed from the queue via JobRecord#delete.
302
- #
303
338
  class JobRecord
339
+ # the parsed Hash of job data
340
+ # @!attribute [r] Item
304
341
  attr_reader :item
342
+ # the underlying String in Redis
343
+ # @!attribute [r] Value
305
344
  attr_reader :value
345
+ # the queue associated with this job
346
+ # @!attribute [r] Queue
347
+ attr_reader :queue
306
348
 
349
+ # :nodoc:
350
+ # @api private
307
351
  def initialize(item, queue_name = nil)
308
352
  @args = nil
309
353
  @value = item
@@ -311,6 +355,8 @@ module Sidekiq
311
355
  @queue = queue_name || @item["queue"]
312
356
  end
313
357
 
358
+ # :nodoc:
359
+ # @api private
314
360
  def parse(item)
315
361
  Sidekiq.load_json(item)
316
362
  rescue JSON::ParserError
@@ -322,6 +368,8 @@ module Sidekiq
322
368
  {}
323
369
  end
324
370
 
371
+ # This is the job class which Sidekiq will execute. If using ActiveJob,
372
+ # this class will be the ActiveJob adapter class rather than a specific job.
325
373
  def klass
326
374
  self["class"]
327
375
  end
@@ -351,27 +399,31 @@ module Sidekiq
351
399
  def display_args
352
400
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
353
401
  @display_args ||= case klass
354
- when /\ASidekiq::Extensions::Delayed/
355
- safe_load(args[0], args) do |_, _, arg|
356
- arg
357
- end
358
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
359
- job_args = self["wrapped"] ? args[0]["arguments"] : []
360
- if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
361
- # remove MailerClass, mailer_method and 'deliver_now'
362
- job_args.drop(3)
363
- elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
364
- # remove MailerClass, mailer_method and 'deliver_now'
365
- job_args.drop(3).first["args"]
366
- else
367
- job_args
368
- end
369
- else
370
- if self["encrypt"]
371
- # no point in showing 150+ bytes of random garbage
372
- args[-1] = "[encrypted data]"
373
- end
374
- args
402
+ when /\ASidekiq::Extensions::Delayed/
403
+ safe_load(args[0], args) do |_, _, arg, kwarg|
404
+ if !kwarg || kwarg.empty?
405
+ arg
406
+ else
407
+ [arg, kwarg]
408
+ end
409
+ end
410
+ when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
411
+ job_args = self["wrapped"] ? args[0]["arguments"] : []
412
+ if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
413
+ # remove MailerClass, mailer_method and 'deliver_now'
414
+ job_args.drop(3)
415
+ elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
416
+ # remove MailerClass, mailer_method and 'deliver_now'
417
+ job_args.drop(3).first["args"]
418
+ else
419
+ job_args
420
+ end
421
+ else
422
+ if self["encrypt"]
423
+ # no point in showing 150+ bytes of random garbage
424
+ args[-1] = "[encrypted data]"
425
+ end
426
+ args
375
427
  end
376
428
  end
377
429
 
@@ -405,15 +457,12 @@ module Sidekiq
405
457
  end
406
458
  end
407
459
 
408
- attr_reader :queue
409
-
410
460
  def latency
411
461
  now = Time.now.to_f
412
462
  now - (@item["enqueued_at"] || @item["created_at"] || now)
413
463
  end
414
464
 
415
- ##
416
- # Remove this job from the queue.
465
+ # Remove this job from the queue
417
466
  def delete
418
467
  count = Sidekiq.redis { |conn|
419
468
  conn.lrem("queue:#{@queue}", 1, @value)
@@ -421,6 +470,7 @@ module Sidekiq
421
470
  count != 0
422
471
  end
423
472
 
473
+ # Access arbitrary attributes within the job hash
424
474
  def [](name)
425
475
  # nil will happen if the JSON fails to parse.
426
476
  # We don't guarantee Sidekiq will work with bad job JSON but we should
@@ -435,7 +485,8 @@ module Sidekiq
435
485
  rescue => ex
436
486
  # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
437
487
  # memory yet so the YAML can't be loaded.
438
- Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
488
+ # TODO is this still necessary? Zeitwerk reloader should handle?
489
+ Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.config[:environment] == "development"
439
490
  default
440
491
  end
441
492
 
@@ -457,20 +508,28 @@ module Sidekiq
457
508
  end
458
509
  end
459
510
 
511
+ # Represents a job within a Redis sorted set where the score
512
+ # represents a timestamp associated with the job. This timestamp
513
+ # could be the scheduled time for it to run (e.g. scheduled set),
514
+ # or the expiration date after which the entry should be deleted (e.g. dead set).
460
515
  class SortedEntry < JobRecord
461
516
  attr_reader :score
462
517
  attr_reader :parent
463
518
 
519
+ # :nodoc:
520
+ # @api private
464
521
  def initialize(parent, score, item)
465
522
  super(item)
466
- @score = score
523
+ @score = Float(score)
467
524
  @parent = parent
468
525
  end
469
526
 
527
+ # The timestamp associated with this entry
470
528
  def at
471
529
  Time.at(score).utc
472
530
  end
473
531
 
532
+ # remove this entry from the sorted set
474
533
  def delete
475
534
  if @value
476
535
  @parent.delete_by_value(@parent.name, @value)
@@ -479,12 +538,17 @@ module Sidekiq
479
538
  end
480
539
  end
481
540
 
541
+ # Change the scheduled time for this job.
542
+ #
543
+ # @param at [Time] the new timestamp for this job
482
544
  def reschedule(at)
483
545
  Sidekiq.redis do |conn|
484
546
  conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
485
547
  end
486
548
  end
487
549
 
550
+ # Enqueue this job from the scheduled or dead set so it will
551
+ # be executed at some point in the near future.
488
552
  def add_to_queue
489
553
  remove_job do |message|
490
554
  msg = Sidekiq.load_json(message)
@@ -492,6 +556,8 @@ module Sidekiq
492
556
  end
493
557
  end
494
558
 
559
+ # enqueue this job from the retry set so it will be executed
560
+ # at some point in the near future.
495
561
  def retry
496
562
  remove_job do |message|
497
563
  msg = Sidekiq.load_json(message)
@@ -500,8 +566,7 @@ module Sidekiq
500
566
  end
501
567
  end
502
568
 
503
- ##
504
- # Place job in the dead set
569
+ # Move this job from its current set into the Dead set.
505
570
  def kill
506
571
  remove_job do |message|
507
572
  DeadSet.new.kill(message)
@@ -516,9 +581,9 @@ module Sidekiq
516
581
 
517
582
  def remove_job
518
583
  Sidekiq.redis do |conn|
519
- results = conn.multi {
520
- conn.zrangebyscore(parent.name, score, score)
521
- conn.zremrangebyscore(parent.name, score, score)
584
+ results = conn.multi { |transaction|
585
+ transaction.zrangebyscore(parent.name, score, score)
586
+ transaction.zremrangebyscore(parent.name, score, score)
522
587
  }.first
523
588
 
524
589
  if results.size == 1
@@ -539,9 +604,9 @@ module Sidekiq
539
604
  yield msg if msg
540
605
 
541
606
  # push the rest back onto the sorted set
542
- conn.multi do
607
+ conn.multi do |transaction|
543
608
  nonmatched.each do |message|
544
- conn.zadd(parent.name, score.to_f.to_s, message)
609
+ transaction.zadd(parent.name, score.to_f.to_s, message)
545
610
  end
546
611
  end
547
612
  end
@@ -549,20 +614,32 @@ module Sidekiq
549
614
  end
550
615
  end
551
616
 
617
+ # Base class for all sorted sets within Sidekiq.
552
618
  class SortedSet
553
619
  include Enumerable
554
620
 
621
+ # Redis key of the set
622
+ # @!attribute [r] Name
555
623
  attr_reader :name
556
624
 
625
+ # :nodoc:
626
+ # @api private
557
627
  def initialize(name)
558
628
  @name = name
559
629
  @_size = size
560
630
  end
561
631
 
632
+ # real-time size of the set, will change
562
633
  def size
563
634
  Sidekiq.redis { |c| c.zcard(name) }
564
635
  end
565
636
 
637
+ # Scan through each element of the sorted set, yielding each to the supplied block.
638
+ # Please see Redis's <a href="https://redis.io/commands/scan/">SCAN documentation</a> for implementation details.
639
+ #
640
+ # @param match [String] a snippet or regexp to filter matches.
641
+ # @param count [Integer] number of elements to retrieve at a time, default 100
642
+ # @yieldparam [Sidekiq::SortedEntry] each entry
566
643
  def scan(match, count = 100)
567
644
  return to_enum(:scan, match, count) unless block_given?
568
645
 
@@ -574,18 +651,32 @@ module Sidekiq
574
651
  end
575
652
  end
576
653
 
654
+ # @return [Boolean] always true
577
655
  def clear
578
656
  Sidekiq.redis do |conn|
579
657
  conn.unlink(name)
580
658
  end
659
+ true
581
660
  end
582
661
  alias_method :💣, :clear
662
+
663
+ # :nodoc:
664
+ # @api private
665
+ def as_json(options = nil)
666
+ {name: name} # 5336
667
+ end
583
668
  end
584
669
 
670
+ # Base class for all sorted sets which contain jobs, e.g. scheduled, retry and dead.
671
+ # Sidekiq Pro and Enterprise add additional sorted sets which do not contain job data,
672
+ # e.g. Batches.
585
673
  class JobSet < SortedSet
586
- def schedule(timestamp, message)
674
+ # Add a job with the associated timestamp to this set.
675
+ # @param timestamp [Time] the score for the job
676
+ # @param job [Hash] the job data
677
+ def schedule(timestamp, job)
587
678
  Sidekiq.redis do |conn|
588
- conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
679
+ conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(job))
589
680
  end
590
681
  end
591
682
 
@@ -599,7 +690,7 @@ module Sidekiq
599
690
  range_start = page * page_size + offset_size
600
691
  range_end = range_start + page_size - 1
601
692
  elements = Sidekiq.redis { |conn|
602
- conn.zrange name, range_start, range_end, with_scores: true
693
+ conn.zrange name, range_start, range_end, withscores: true
603
694
  }
604
695
  break if elements.empty?
605
696
  page -= 1
@@ -613,6 +704,10 @@ module Sidekiq
613
704
  ##
614
705
  # Fetch jobs that match a given time or Range. Job ID is an
615
706
  # optional second argument.
707
+ #
708
+ # @param score [Time,Range] a specific timestamp or range
709
+ # @param jid [String, optional] find a specific JID within the score
710
+ # @return [Array<SortedEntry>] any results found, can be empty
616
711
  def fetch(score, jid = nil)
617
712
  begin_score, end_score =
618
713
  if score.is_a?(Range)
@@ -622,7 +717,7 @@ module Sidekiq
622
717
  end
623
718
 
624
719
  elements = Sidekiq.redis { |conn|
625
- conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
720
+ conn.zrangebyscore(name, begin_score, end_score, withscores: true)
626
721
  }
627
722
 
628
723
  elements.each_with_object([]) do |element, result|
@@ -634,7 +729,10 @@ module Sidekiq
634
729
 
635
730
  ##
636
731
  # Find the job with the given JID within this sorted set.
637
- # This is a slower O(n) operation. Do not use for app logic.
732
+ # *This is a slow O(n) operation*. Do not use for app logic.
733
+ #
734
+ # @param jid [String] the job identifier
735
+ # @return [SortedEntry] the record or nil
638
736
  def find_job(jid)
639
737
  Sidekiq.redis do |conn|
640
738
  conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
@@ -646,6 +744,8 @@ module Sidekiq
646
744
  nil
647
745
  end
648
746
 
747
+ # :nodoc:
748
+ # @api private
649
749
  def delete_by_value(name, value)
650
750
  Sidekiq.redis do |conn|
651
751
  ret = conn.zrem(name, value)
@@ -654,6 +754,8 @@ module Sidekiq
654
754
  end
655
755
  end
656
756
 
757
+ # :nodoc:
758
+ # @api private
657
759
  def delete_by_jid(score, jid)
658
760
  Sidekiq.redis do |conn|
659
761
  elements = conn.zrangebyscore(name, score, score)
@@ -674,10 +776,10 @@ module Sidekiq
674
776
  end
675
777
 
676
778
  ##
677
- # Allows enumeration of scheduled jobs within Sidekiq.
779
+ # The set of scheduled jobs within Sidekiq.
678
780
  # Based on this, you can search/filter for jobs. Here's an
679
- # example where I'm selecting all jobs of a certain type
680
- # and deleting them from the schedule queue.
781
+ # example where I'm selecting jobs based on some complex logic
782
+ # and deleting them from the scheduled set.
681
783
  #
682
784
  # r = Sidekiq::ScheduledSet.new
683
785
  # r.select do |scheduled|
@@ -692,7 +794,7 @@ module Sidekiq
692
794
  end
693
795
 
694
796
  ##
695
- # Allows enumeration of retries within Sidekiq.
797
+ # The set of retries within Sidekiq.
696
798
  # Based on this, you can search/filter for jobs. Here's an
697
799
  # example where I'm selecting all jobs of a certain type
698
800
  # and deleting them from the retry queue.
@@ -708,30 +810,36 @@ module Sidekiq
708
810
  super "retry"
709
811
  end
710
812
 
813
+ # Enqueues all jobs pending within the retry set.
711
814
  def retry_all
712
815
  each(&:retry) while size > 0
713
816
  end
714
817
 
818
+ # Kills all jobs pending within the retry set.
715
819
  def kill_all
716
820
  each(&:kill) while size > 0
717
821
  end
718
822
  end
719
823
 
720
824
  ##
721
- # Allows enumeration of dead jobs within Sidekiq.
825
+ # The set of dead jobs within Sidekiq. Dead jobs have failed all of
826
+ # their retries and are helding in this set pending some sort of manual
827
+ # fix. They will be removed after 6 months (dead_timeout) if not.
722
828
  #
723
829
  class DeadSet < JobSet
724
830
  def initialize
725
831
  super "dead"
726
832
  end
727
833
 
834
+ # Add the given job to the Dead set.
835
+ # @param message [String] the job data as JSON
728
836
  def kill(message, opts = {})
729
837
  now = Time.now.to_f
730
838
  Sidekiq.redis do |conn|
731
- conn.multi do
732
- conn.zadd(name, now.to_s, message)
733
- conn.zremrangebyscore(name, "-inf", now - self.class.timeout)
734
- conn.zremrangebyrank(name, 0, - self.class.max_jobs)
839
+ conn.multi do |transaction|
840
+ transaction.zadd(name, now.to_s, message)
841
+ transaction.zremrangebyscore(name, "-inf", now - self.class.timeout)
842
+ transaction.zremrangebyrank(name, 0, - self.class.max_jobs)
735
843
  end
736
844
  end
737
845
 
@@ -746,16 +854,21 @@ module Sidekiq
746
854
  true
747
855
  end
748
856
 
857
+ # Enqueue all dead jobs
749
858
  def retry_all
750
859
  each(&:retry) while size > 0
751
860
  end
752
861
 
862
+ # The maximum size of the Dead set. Older entries will be trimmed
863
+ # to stay within this limit. Default value is 10,000.
753
864
  def self.max_jobs
754
- Sidekiq.options[:dead_max_jobs]
865
+ Sidekiq[:dead_max_jobs]
755
866
  end
756
867
 
868
+ # The time limit for entries within the Dead set. Older entries will be thrown away.
869
+ # Default value is six months.
757
870
  def self.timeout
758
- Sidekiq.options[:dead_timeout_in_seconds]
871
+ Sidekiq[:dead_timeout_in_seconds]
759
872
  end
760
873
  end
761
874
 
@@ -764,24 +877,29 @@ module Sidekiq
764
877
  # right now. Each process sends a heartbeat to Redis every 5 seconds
765
878
  # so this set should be relatively accurate, barring network partitions.
766
879
  #
767
- # Yields a Sidekiq::Process.
880
+ # @yieldparam [Sidekiq::Process]
768
881
  #
769
882
  class ProcessSet
770
883
  include Enumerable
771
884
 
885
+ # :nodoc:
886
+ # @api private
772
887
  def initialize(clean_plz = true)
773
888
  cleanup if clean_plz
774
889
  end
775
890
 
776
891
  # Cleans up dead processes recorded in Redis.
777
892
  # Returns the number of processes cleaned.
893
+ # :nodoc:
894
+ # @api private
778
895
  def cleanup
896
+ return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
779
897
  count = 0
780
898
  Sidekiq.redis do |conn|
781
899
  procs = conn.sscan_each("processes").to_a.sort
782
- heartbeats = conn.pipelined {
900
+ heartbeats = conn.pipelined { |pipeline|
783
901
  procs.each do |key|
784
- conn.hget(key, "info")
902
+ pipeline.hget(key, "info")
785
903
  end
786
904
  }
787
905
 
@@ -803,9 +921,9 @@ module Sidekiq
803
921
  # We're making a tradeoff here between consuming more memory instead of
804
922
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
805
923
  # you'll be happier this way
806
- conn.pipelined do
924
+ conn.pipelined do |pipeline|
807
925
  procs.each do |key|
808
- conn.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
926
+ pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
809
927
  end
810
928
  end
811
929
  }
@@ -818,10 +936,10 @@ module Sidekiq
818
936
 
819
937
  hash = Sidekiq.load_json(info)
820
938
  yield Process.new(hash.merge("busy" => busy.to_i,
821
- "beat" => at_s.to_f,
822
- "quiet" => quiet,
823
- "rss" => rss.to_i,
824
- "rtt_us" => rtt.to_i))
939
+ "beat" => at_s.to_f,
940
+ "quiet" => quiet,
941
+ "rss" => rss.to_i,
942
+ "rtt_us" => rtt.to_i))
825
943
  end
826
944
  end
827
945
 
@@ -829,6 +947,7 @@ module Sidekiq
829
947
  # based on current heartbeat. #each does that and ensures the set only
830
948
  # contains Sidekiq processes which have sent a heartbeat within the last
831
949
  # 60 seconds.
950
+ # @return [Integer] current number of registered Sidekiq processes
832
951
  def size
833
952
  Sidekiq.redis { |conn| conn.scard("processes") }
834
953
  end
@@ -836,10 +955,12 @@ module Sidekiq
836
955
  # Total number of threads available to execute jobs.
837
956
  # For Sidekiq Enterprise customers this number (in production) must be
838
957
  # less than or equal to your licensed concurrency.
958
+ # @return [Integer] the sum of process concurrency
839
959
  def total_concurrency
840
960
  sum { |x| x["concurrency"].to_i }
841
961
  end
842
962
 
963
+ # @return [Integer] total amount of RSS memory consumed by Sidekiq processes
843
964
  def total_rss_in_kb
844
965
  sum { |x| x["rss"].to_i }
845
966
  end
@@ -848,6 +969,8 @@ module Sidekiq
848
969
  # Returns the identity of the current cluster leader or "" if no leader.
849
970
  # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
850
971
  # or Sidekiq Pro.
972
+ # @return [String] Identity of cluster leader
973
+ # @return [String] empty string if no leader
851
974
  def leader
852
975
  @leader ||= begin
853
976
  x = Sidekiq.redis { |c| c.get("dear-leader") }
@@ -874,6 +997,8 @@ module Sidekiq
874
997
  # 'identity' => <unique string identifying the process>,
875
998
  # }
876
999
  class Process
1000
+ # :nodoc:
1001
+ # @api private
877
1002
  def initialize(hash)
878
1003
  @attribs = hash
879
1004
  end
@@ -898,18 +1023,31 @@ module Sidekiq
898
1023
  self["queues"]
899
1024
  end
900
1025
 
1026
+ # Signal this process to stop processing new jobs.
1027
+ # It will continue to execute jobs it has already fetched.
1028
+ # This method is *asynchronous* and it can take 5-10
1029
+ # seconds for the process to quiet.
901
1030
  def quiet!
902
1031
  signal("TSTP")
903
1032
  end
904
1033
 
1034
+ # Signal this process to shutdown.
1035
+ # It will shutdown within its configured :timeout value, default 25 seconds.
1036
+ # This method is *asynchronous* and it can take 5-10
1037
+ # seconds for the process to start shutting down.
905
1038
  def stop!
906
1039
  signal("TERM")
907
1040
  end
908
1041
 
1042
+ # Signal this process to log backtraces for all threads.
1043
+ # Useful if you have a frozen or deadlocked process which is
1044
+ # still sending a heartbeat.
1045
+ # This method is *asynchronous* and it can take 5-10 seconds.
909
1046
  def dump_threads
910
1047
  signal("TTIN")
911
1048
  end
912
1049
 
1050
+ # @return [Boolean] true if this process is quiet or shutting down
913
1051
  def stopping?
914
1052
  self["quiet"] == "true"
915
1053
  end
@@ -919,9 +1057,9 @@ module Sidekiq
919
1057
  def signal(sig)
920
1058
  key = "#{identity}-signals"
921
1059
  Sidekiq.redis do |c|
922
- c.multi do
923
- c.lpush(key, sig)
924
- c.expire(key, 60)
1060
+ c.multi do |transaction|
1061
+ transaction.lpush(key, sig)
1062
+ transaction.expire(key, 60)
925
1063
  end
926
1064
  end
927
1065
  end
@@ -955,9 +1093,9 @@ module Sidekiq
955
1093
  Sidekiq.redis do |conn|
956
1094
  procs = conn.sscan_each("processes").to_a
957
1095
  procs.sort.each do |key|
958
- valid, workers = conn.pipelined {
959
- conn.exists?(key)
960
- conn.hgetall("#{key}:workers")
1096
+ valid, workers = conn.pipelined { |pipeline|
1097
+ pipeline.exists?(key)
1098
+ pipeline.hgetall("#{key}:work")
961
1099
  }
962
1100
  next unless valid
963
1101
  workers.each_pair do |tid, json|
@@ -985,9 +1123,9 @@ module Sidekiq
985
1123
  if procs.empty?
986
1124
  0
987
1125
  else
988
- conn.pipelined {
1126
+ conn.pipelined { |pipeline|
989
1127
  procs.each do |key|
990
- conn.hget(key, "busy")
1128
+ pipeline.hget(key, "busy")
991
1129
  end
992
1130
  }.sum(&:to_i)
993
1131
  end