sidekiq 6.1.1 → 6.5.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +250 -3
  3. data/LICENSE +3 -3
  4. data/README.md +10 -6
  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 +352 -156
  13. data/lib/sidekiq/cli.rb +86 -41
  14. data/lib/sidekiq/client.rb +49 -73
  15. data/lib/sidekiq/{util.rb → component.rb} +12 -14
  16. data/lib/sidekiq/delay.rb +3 -1
  17. data/lib/sidekiq/extensions/action_mailer.rb +3 -2
  18. data/lib/sidekiq/extensions/active_record.rb +1 -1
  19. data/lib/sidekiq/extensions/generic_proxy.rb +4 -2
  20. data/lib/sidekiq/fetch.rb +31 -20
  21. data/lib/sidekiq/job.rb +13 -0
  22. data/lib/sidekiq/job_logger.rb +16 -28
  23. data/lib/sidekiq/job_retry.rb +79 -59
  24. data/lib/sidekiq/job_util.rb +71 -0
  25. data/lib/sidekiq/launcher.rb +126 -65
  26. data/lib/sidekiq/logger.rb +11 -20
  27. data/lib/sidekiq/manager.rb +35 -34
  28. data/lib/sidekiq/metrics/deploy.rb +47 -0
  29. data/lib/sidekiq/metrics/query.rb +153 -0
  30. data/lib/sidekiq/metrics/shared.rb +94 -0
  31. data/lib/sidekiq/metrics/tracking.rb +134 -0
  32. data/lib/sidekiq/middleware/chain.rb +88 -42
  33. data/lib/sidekiq/middleware/current_attributes.rb +63 -0
  34. data/lib/sidekiq/middleware/i18n.rb +6 -4
  35. data/lib/sidekiq/middleware/modules.rb +21 -0
  36. data/lib/sidekiq/monitor.rb +2 -2
  37. data/lib/sidekiq/paginator.rb +17 -9
  38. data/lib/sidekiq/processor.rb +47 -41
  39. data/lib/sidekiq/rails.rb +32 -4
  40. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  41. data/lib/sidekiq/redis_connection.rb +84 -55
  42. data/lib/sidekiq/ring_buffer.rb +29 -0
  43. data/lib/sidekiq/scheduled.rb +96 -32
  44. data/lib/sidekiq/testing/inline.rb +4 -4
  45. data/lib/sidekiq/testing.rb +38 -39
  46. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  47. data/lib/sidekiq/version.rb +1 -1
  48. data/lib/sidekiq/web/action.rb +3 -3
  49. data/lib/sidekiq/web/application.rb +41 -16
  50. data/lib/sidekiq/web/csrf_protection.rb +32 -5
  51. data/lib/sidekiq/web/helpers.rb +52 -30
  52. data/lib/sidekiq/web/router.rb +4 -1
  53. data/lib/sidekiq/web.rb +38 -78
  54. data/lib/sidekiq/worker.rb +142 -16
  55. data/lib/sidekiq.rb +114 -31
  56. data/sidekiq.gemspec +12 -4
  57. data/web/assets/images/apple-touch-icon.png +0 -0
  58. data/web/assets/javascripts/application.js +114 -60
  59. data/web/assets/javascripts/chart.min.js +13 -0
  60. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  61. data/web/assets/javascripts/dashboard.js +50 -67
  62. data/web/assets/javascripts/graph.js +16 -0
  63. data/web/assets/javascripts/metrics.js +262 -0
  64. data/web/assets/stylesheets/application-dark.css +61 -51
  65. data/web/assets/stylesheets/application-rtl.css +0 -4
  66. data/web/assets/stylesheets/application.css +84 -243
  67. data/web/locales/ar.yml +8 -2
  68. data/web/locales/el.yml +43 -19
  69. data/web/locales/en.yml +11 -1
  70. data/web/locales/es.yml +18 -2
  71. data/web/locales/fr.yml +8 -1
  72. data/web/locales/ja.yml +10 -0
  73. data/web/locales/lt.yml +1 -1
  74. data/web/locales/pt-br.yml +27 -9
  75. data/web/locales/ru.yml +4 -0
  76. data/web/locales/zh-cn.yml +36 -11
  77. data/web/locales/zh-tw.yml +32 -7
  78. data/web/views/_footer.erb +1 -1
  79. data/web/views/_job_info.erb +1 -1
  80. data/web/views/_nav.erb +1 -1
  81. data/web/views/_poll_link.erb +2 -5
  82. data/web/views/_summary.erb +7 -7
  83. data/web/views/busy.erb +57 -21
  84. data/web/views/dashboard.erb +23 -14
  85. data/web/views/dead.erb +1 -1
  86. data/web/views/layout.erb +2 -1
  87. data/web/views/metrics.erb +69 -0
  88. data/web/views/metrics_for_job.erb +87 -0
  89. data/web/views/morgue.erb +6 -6
  90. data/web/views/queue.erb +15 -11
  91. data/web/views/queues.erb +4 -4
  92. data/web/views/retries.erb +7 -7
  93. data/web/views/retry.erb +1 -1
  94. data/web/views/scheduled.erb +1 -1
  95. metadata +52 -39
  96. data/.circleci/config.yml +0 -71
  97. data/.github/contributing.md +0 -32
  98. data/.github/issue_template.md +0 -11
  99. data/.gitignore +0 -13
  100. data/.standard.yml +0 -20
  101. data/3.0-Upgrade.md +0 -70
  102. data/4.0-Upgrade.md +0 -53
  103. data/5.0-Upgrade.md +0 -56
  104. data/6.0-Upgrade.md +0 -72
  105. data/COMM-LICENSE +0 -97
  106. data/Ent-2.0-Upgrade.md +0 -37
  107. data/Ent-Changes.md +0 -275
  108. data/Gemfile +0 -24
  109. data/Gemfile.lock +0 -208
  110. data/Pro-2.0-Upgrade.md +0 -138
  111. data/Pro-3.0-Upgrade.md +0 -44
  112. data/Pro-4.0-Upgrade.md +0 -35
  113. data/Pro-5.0-Upgrade.md +0 -25
  114. data/Pro-Changes.md +0 -795
  115. data/Rakefile +0 -10
  116. data/code_of_conduct.md +0 -50
  117. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  118. data/lib/sidekiq/exception_handler.rb +0 -27
data/lib/sidekiq/api.rb CHANGED
@@ -3,12 +3,34 @@
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
+
14
+ #
15
+ # Sidekiq's Data API provides a Ruby object model on top
16
+ # of Sidekiq's runtime data in Redis. This API should never
17
+ # be used within application code for business logic.
18
+ #
19
+ # The Sidekiq server process never uses this API: all data
20
+ # manipulation is done directly for performance reasons to
21
+ # ensure we are using Redis as efficiently as possible at
22
+ # every callsite.
23
+ #
24
+
8
25
  module Sidekiq
26
+ # Retrieve runtime statistics from Redis regarding
27
+ # this Sidekiq cluster.
28
+ #
29
+ # stat = Sidekiq::Stats.new
30
+ # stat.processed
9
31
  class Stats
10
32
  def initialize
11
- fetch_stats!
33
+ fetch_stats_fast!
12
34
  end
13
35
 
14
36
  def processed
@@ -51,50 +73,34 @@ module Sidekiq
51
73
  Sidekiq::Stats::Queues.new.lengths
52
74
  end
53
75
 
54
- def fetch_stats!
76
+ # O(1) redis calls
77
+ # @api private
78
+ def fetch_stats_fast!
55
79
  pipe1_res = Sidekiq.redis { |conn|
56
- conn.pipelined do
57
- conn.get("stat:processed")
58
- conn.get("stat:failed")
59
- conn.zcard("schedule")
60
- conn.zcard("retry")
61
- conn.zcard("dead")
62
- conn.scard("processes")
63
- conn.lrange("queue:default", -1, -1)
64
- end
65
- }
66
-
67
- processes = Sidekiq.redis { |conn|
68
- conn.sscan_each("processes").to_a
69
- }
70
-
71
- queues = Sidekiq.redis { |conn|
72
- conn.sscan_each("queues").to_a
73
- }
74
-
75
- pipe2_res = Sidekiq.redis { |conn|
76
- conn.pipelined do
77
- processes.each { |key| conn.hget(key, "busy") }
78
- queues.each { |queue| conn.llen("queue:#{queue}") }
80
+ conn.pipelined do |pipeline|
81
+ pipeline.get("stat:processed")
82
+ pipeline.get("stat:failed")
83
+ pipeline.zcard("schedule")
84
+ pipeline.zcard("retry")
85
+ pipeline.zcard("dead")
86
+ pipeline.scard("processes")
87
+ pipeline.lrange("queue:default", -1, -1)
79
88
  end
80
89
  }
81
90
 
82
- s = processes.size
83
- workers_size = pipe2_res[0...s].sum(&:to_i)
84
- enqueued = pipe2_res[s..-1].sum(&:to_i)
85
-
86
91
  default_queue_latency = if (entry = pipe1_res[6].first)
87
92
  job = begin
88
- Sidekiq.load_json(entry)
89
- rescue
90
- {}
91
- end
93
+ Sidekiq.load_json(entry)
94
+ rescue
95
+ {}
96
+ end
92
97
  now = Time.now.to_f
93
98
  thence = job["enqueued_at"] || now
94
99
  now - thence
95
100
  else
96
101
  0
97
102
  end
103
+
98
104
  @stats = {
99
105
  processed: pipe1_res[0].to_i,
100
106
  failed: pipe1_res[1].to_i,
@@ -103,12 +109,44 @@ module Sidekiq
103
109
  dead_size: pipe1_res[4],
104
110
  processes_size: pipe1_res[5],
105
111
 
106
- default_queue_latency: default_queue_latency,
107
- workers_size: workers_size,
108
- enqueued: enqueued
112
+ default_queue_latency: default_queue_latency
109
113
  }
110
114
  end
111
115
 
116
+ # O(number of processes + number of queues) redis calls
117
+ # @api private
118
+ def fetch_stats_slow!
119
+ processes = Sidekiq.redis { |conn|
120
+ conn.sscan_each("processes").to_a
121
+ }
122
+
123
+ queues = Sidekiq.redis { |conn|
124
+ conn.sscan_each("queues").to_a
125
+ }
126
+
127
+ pipe2_res = Sidekiq.redis { |conn|
128
+ conn.pipelined do |pipeline|
129
+ processes.each { |key| pipeline.hget(key, "busy") }
130
+ queues.each { |queue| pipeline.llen("queue:#{queue}") }
131
+ end
132
+ }
133
+
134
+ s = processes.size
135
+ workers_size = pipe2_res[0...s].sum(&:to_i)
136
+ enqueued = pipe2_res[s..-1].sum(&:to_i)
137
+
138
+ @stats[:workers_size] = workers_size
139
+ @stats[:enqueued] = enqueued
140
+ @stats
141
+ end
142
+
143
+ # @api private
144
+ def fetch_stats!
145
+ fetch_stats_fast!
146
+ fetch_stats_slow!
147
+ end
148
+
149
+ # @api private
112
150
  def reset(*stats)
113
151
  all = %w[failed processed]
114
152
  stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
@@ -126,7 +164,8 @@ module Sidekiq
126
164
  private
127
165
 
128
166
  def stat(s)
129
- @stats[s]
167
+ fetch_stats_slow! if @stats[s].nil?
168
+ @stats[s] || raise(ArgumentError, "Unknown stat #{s}")
130
169
  end
131
170
 
132
171
  class Queues
@@ -134,20 +173,22 @@ module Sidekiq
134
173
  Sidekiq.redis do |conn|
135
174
  queues = conn.sscan_each("queues").to_a
136
175
 
137
- lengths = conn.pipelined {
176
+ lengths = conn.pipelined { |pipeline|
138
177
  queues.each do |queue|
139
- conn.llen("queue:#{queue}")
178
+ pipeline.llen("queue:#{queue}")
140
179
  end
141
180
  }
142
181
 
143
182
  array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
144
- Hash[array_of_arrays]
183
+ array_of_arrays.to_h
145
184
  end
146
185
  end
147
186
  end
148
187
 
149
188
  class History
150
189
  def initialize(days_previous, start_date = nil)
190
+ # we only store five years of data in Redis
191
+ raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
151
192
  @days_previous = days_previous
152
193
  @start_date = start_date || Time.now.utc.to_date
153
194
  end
@@ -176,7 +217,7 @@ module Sidekiq
176
217
  stat_hash[dates[idx]] = value ? value.to_i : 0
177
218
  end
178
219
  end
179
- rescue Redis::CommandError
220
+ rescue RedisConnection.adapter::CommandError
180
221
  # mget will trigger a CROSSSLOT error when run against a Cluster
181
222
  # TODO Someone want to add Cluster support?
182
223
  end
@@ -187,9 +228,10 @@ module Sidekiq
187
228
  end
188
229
 
189
230
  ##
190
- # Encapsulates a queue within Sidekiq.
231
+ # Represents a queue within Sidekiq.
191
232
  # Allows enumeration of all jobs within the queue
192
- # and deletion of jobs.
233
+ # and deletion of jobs. NB: this queue data is real-time
234
+ # and is changing within Redis moment by moment.
193
235
  #
194
236
  # queue = Sidekiq::Queue.new("mailer")
195
237
  # queue.each do |job|
@@ -197,29 +239,34 @@ module Sidekiq
197
239
  # job.args # => [1, 2, 3]
198
240
  # job.delete if job.jid == 'abcdef1234567890'
199
241
  # end
200
- #
201
242
  class Queue
202
243
  include Enumerable
203
244
 
204
245
  ##
205
- # Return all known queues within Redis.
246
+ # Fetch all known queues within Redis.
206
247
  #
248
+ # @return [Array<Sidekiq::Queue>]
207
249
  def self.all
208
250
  Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
209
251
  end
210
252
 
211
253
  attr_reader :name
212
254
 
255
+ # @param name [String] the name of the queue
213
256
  def initialize(name = "default")
214
257
  @name = name.to_s
215
258
  @rname = "queue:#{name}"
216
259
  end
217
260
 
261
+ # The current size of the queue within Redis.
262
+ # This value is real-time and can change between calls.
263
+ #
264
+ # @return [Integer] the size
218
265
  def size
219
266
  Sidekiq.redis { |con| con.llen(@rname) }
220
267
  end
221
268
 
222
- # Sidekiq Pro overrides this
269
+ # @return [Boolean] if the queue is currently paused
223
270
  def paused?
224
271
  false
225
272
  end
@@ -228,7 +275,7 @@ module Sidekiq
228
275
  # Calculates this queue's latency, the difference in seconds since the oldest
229
276
  # job in the queue was enqueued.
230
277
  #
231
- # @return Float
278
+ # @return [Float] in seconds
232
279
  def latency
233
280
  entry = Sidekiq.redis { |conn|
234
281
  conn.lrange(@rname, -1, -1)
@@ -255,7 +302,7 @@ module Sidekiq
255
302
  break if entries.empty?
256
303
  page += 1
257
304
  entries.each do |entry|
258
- yield Job.new(entry, @name)
305
+ yield JobRecord.new(entry, @name)
259
306
  end
260
307
  deleted_size = initial_size - size
261
308
  end
@@ -264,34 +311,54 @@ module Sidekiq
264
311
  ##
265
312
  # Find the job with the given JID within this queue.
266
313
  #
267
- # This is a slow, inefficient operation. Do not use under
268
- # normal conditions. Sidekiq Pro contains a faster version.
314
+ # This is a *slow, inefficient* operation. Do not use under
315
+ # normal conditions.
316
+ #
317
+ # @param jid [String] the job_id to look for
318
+ # @return [Sidekiq::JobRecord]
319
+ # @return [nil] if not found
269
320
  def find_job(jid)
270
321
  detect { |j| j.jid == jid }
271
322
  end
272
323
 
324
+ # delete all jobs within this queue
325
+ # @return [Boolean] true
273
326
  def clear
274
327
  Sidekiq.redis do |conn|
275
- conn.multi do
276
- conn.unlink(@rname)
277
- conn.srem("queues", name)
328
+ conn.multi do |transaction|
329
+ transaction.unlink(@rname)
330
+ transaction.srem("queues", [name])
278
331
  end
279
332
  end
333
+ true
280
334
  end
281
335
  alias_method :💣, :clear
336
+
337
+ # :nodoc:
338
+ # @api private
339
+ def as_json(options = nil)
340
+ {name: name} # 5336
341
+ end
282
342
  end
283
343
 
284
344
  ##
285
- # Encapsulates a pending job within a Sidekiq queue or
286
- # sorted set.
345
+ # Represents a pending job within a Sidekiq queue.
287
346
  #
288
347
  # The job should be considered immutable but may be
289
- # removed from the queue via Job#delete.
290
- #
291
- class Job
348
+ # removed from the queue via JobRecord#delete.
349
+ class JobRecord
350
+ # the parsed Hash of job data
351
+ # @!attribute [r] Item
292
352
  attr_reader :item
353
+ # the underlying String in Redis
354
+ # @!attribute [r] Value
293
355
  attr_reader :value
356
+ # the queue associated with this job
357
+ # @!attribute [r] Queue
358
+ attr_reader :queue
294
359
 
360
+ # :nodoc:
361
+ # @api private
295
362
  def initialize(item, queue_name = nil)
296
363
  @args = nil
297
364
  @value = item
@@ -299,6 +366,8 @@ module Sidekiq
299
366
  @queue = queue_name || @item["queue"]
300
367
  end
301
368
 
369
+ # :nodoc:
370
+ # @api private
302
371
  def parse(item)
303
372
  Sidekiq.load_json(item)
304
373
  rescue JSON::ParserError
@@ -310,54 +379,62 @@ module Sidekiq
310
379
  {}
311
380
  end
312
381
 
382
+ # This is the job class which Sidekiq will execute. If using ActiveJob,
383
+ # this class will be the ActiveJob adapter class rather than a specific job.
313
384
  def klass
314
385
  self["class"]
315
386
  end
316
387
 
317
388
  def display_class
318
389
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
319
- @klass ||= case klass
320
- when /\ASidekiq::Extensions::Delayed/
321
- safe_load(args[0], klass) do |target, method, _|
322
- "#{target}.#{method}"
323
- end
324
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
325
- job_class = @item["wrapped"] || args[0]
326
- if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
327
- # MailerClass#mailer_method
328
- args[0]["arguments"][0..1].join("#")
329
- else
330
- job_class
331
- end
332
- else
333
- klass
390
+ @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"
397
+ job_class = @item["wrapped"] || args[0]
398
+ if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
399
+ # MailerClass#mailer_method
400
+ args[0]["arguments"][0..1].join("#")
401
+ else
402
+ job_class
403
+ end
404
+ else
405
+ klass
406
+ end
334
407
  end
335
408
  end
336
409
 
337
410
  def display_args
338
411
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
339
412
  @display_args ||= case klass
340
- when /\ASidekiq::Extensions::Delayed/
341
- safe_load(args[0], args) do |_, _, arg|
342
- arg
343
- end
344
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
345
- job_args = self["wrapped"] ? args[0]["arguments"] : []
346
- if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
347
- # remove MailerClass, mailer_method and 'deliver_now'
348
- job_args.drop(3)
349
- elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
350
- # remove MailerClass, mailer_method and 'deliver_now'
351
- job_args.drop(3).first["args"]
352
- else
353
- job_args
354
- end
355
- else
356
- if self["encrypt"]
357
- # no point in showing 150+ bytes of random garbage
358
- args[-1] = "[encrypted data]"
359
- end
360
- args
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"] : []
423
+ if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
424
+ # remove MailerClass, mailer_method and 'deliver_now'
425
+ job_args.drop(3)
426
+ elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
427
+ # remove MailerClass, mailer_method and 'deliver_now'
428
+ job_args.drop(3).first["args"]
429
+ else
430
+ job_args
431
+ end
432
+ else
433
+ if self["encrypt"]
434
+ # no point in showing 150+ bytes of random garbage
435
+ args[-1] = "[encrypted data]"
436
+ end
437
+ args
361
438
  end
362
439
  end
363
440
 
@@ -391,15 +468,12 @@ module Sidekiq
391
468
  end
392
469
  end
393
470
 
394
- attr_reader :queue
395
-
396
471
  def latency
397
472
  now = Time.now.to_f
398
473
  now - (@item["enqueued_at"] || @item["created_at"] || now)
399
474
  end
400
475
 
401
- ##
402
- # Remove this job from the queue.
476
+ # Remove this job from the queue
403
477
  def delete
404
478
  count = Sidekiq.redis { |conn|
405
479
  conn.lrem("queue:#{@queue}", 1, @value)
@@ -407,6 +481,7 @@ module Sidekiq
407
481
  count != 0
408
482
  end
409
483
 
484
+ # Access arbitrary attributes within the job hash
410
485
  def [](name)
411
486
  # nil will happen if the JSON fails to parse.
412
487
  # We don't guarantee Sidekiq will work with bad job JSON but we should
@@ -421,6 +496,7 @@ module Sidekiq
421
496
  rescue => ex
422
497
  # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
423
498
  # memory yet so the YAML can't be loaded.
499
+ # TODO is this still necessary? Zeitwerk reloader should handle?
424
500
  Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
425
501
  default
426
502
  end
@@ -443,20 +519,28 @@ module Sidekiq
443
519
  end
444
520
  end
445
521
 
446
- class SortedEntry < Job
522
+ # Represents a job within a Redis sorted set where the score
523
+ # represents a timestamp associated with the job. This timestamp
524
+ # could be the scheduled time for it to run (e.g. scheduled set),
525
+ # or the expiration date after which the entry should be deleted (e.g. dead set).
526
+ class SortedEntry < JobRecord
447
527
  attr_reader :score
448
528
  attr_reader :parent
449
529
 
530
+ # :nodoc:
531
+ # @api private
450
532
  def initialize(parent, score, item)
451
533
  super(item)
452
- @score = score
534
+ @score = Float(score)
453
535
  @parent = parent
454
536
  end
455
537
 
538
+ # The timestamp associated with this entry
456
539
  def at
457
540
  Time.at(score).utc
458
541
  end
459
542
 
543
+ # remove this entry from the sorted set
460
544
  def delete
461
545
  if @value
462
546
  @parent.delete_by_value(@parent.name, @value)
@@ -465,12 +549,17 @@ module Sidekiq
465
549
  end
466
550
  end
467
551
 
552
+ # Change the scheduled time for this job.
553
+ #
554
+ # @param at [Time] the new timestamp for this job
468
555
  def reschedule(at)
469
556
  Sidekiq.redis do |conn|
470
557
  conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
471
558
  end
472
559
  end
473
560
 
561
+ # Enqueue this job from the scheduled or dead set so it will
562
+ # be executed at some point in the near future.
474
563
  def add_to_queue
475
564
  remove_job do |message|
476
565
  msg = Sidekiq.load_json(message)
@@ -478,6 +567,8 @@ module Sidekiq
478
567
  end
479
568
  end
480
569
 
570
+ # enqueue this job from the retry set so it will be executed
571
+ # at some point in the near future.
481
572
  def retry
482
573
  remove_job do |message|
483
574
  msg = Sidekiq.load_json(message)
@@ -486,8 +577,7 @@ module Sidekiq
486
577
  end
487
578
  end
488
579
 
489
- ##
490
- # Place job in the dead set
580
+ # Move this job from its current set into the Dead set.
491
581
  def kill
492
582
  remove_job do |message|
493
583
  DeadSet.new.kill(message)
@@ -502,9 +592,9 @@ module Sidekiq
502
592
 
503
593
  def remove_job
504
594
  Sidekiq.redis do |conn|
505
- results = conn.multi {
506
- conn.zrangebyscore(parent.name, score, score)
507
- conn.zremrangebyscore(parent.name, score, score)
595
+ results = conn.multi { |transaction|
596
+ transaction.zrangebyscore(parent.name, score, score)
597
+ transaction.zremrangebyscore(parent.name, score, score)
508
598
  }.first
509
599
 
510
600
  if results.size == 1
@@ -525,9 +615,9 @@ module Sidekiq
525
615
  yield msg if msg
526
616
 
527
617
  # push the rest back onto the sorted set
528
- conn.multi do
618
+ conn.multi do |transaction|
529
619
  nonmatched.each do |message|
530
- conn.zadd(parent.name, score.to_f.to_s, message)
620
+ transaction.zadd(parent.name, score.to_f.to_s, message)
531
621
  end
532
622
  end
533
623
  end
@@ -535,20 +625,32 @@ module Sidekiq
535
625
  end
536
626
  end
537
627
 
628
+ # Base class for all sorted sets within Sidekiq.
538
629
  class SortedSet
539
630
  include Enumerable
540
631
 
632
+ # Redis key of the set
633
+ # @!attribute [r] Name
541
634
  attr_reader :name
542
635
 
636
+ # :nodoc:
637
+ # @api private
543
638
  def initialize(name)
544
639
  @name = name
545
640
  @_size = size
546
641
  end
547
642
 
643
+ # real-time size of the set, will change
548
644
  def size
549
645
  Sidekiq.redis { |c| c.zcard(name) }
550
646
  end
551
647
 
648
+ # Scan through each element of the sorted set, yielding each to the supplied block.
649
+ # Please see Redis's <a href="https://redis.io/commands/scan/">SCAN documentation</a> for implementation details.
650
+ #
651
+ # @param match [String] a snippet or regexp to filter matches.
652
+ # @param count [Integer] number of elements to retrieve at a time, default 100
653
+ # @yieldparam [Sidekiq::SortedEntry] each entry
552
654
  def scan(match, count = 100)
553
655
  return to_enum(:scan, match, count) unless block_given?
554
656
 
@@ -560,18 +662,32 @@ module Sidekiq
560
662
  end
561
663
  end
562
664
 
665
+ # @return [Boolean] always true
563
666
  def clear
564
667
  Sidekiq.redis do |conn|
565
668
  conn.unlink(name)
566
669
  end
670
+ true
567
671
  end
568
672
  alias_method :💣, :clear
673
+
674
+ # :nodoc:
675
+ # @api private
676
+ def as_json(options = nil)
677
+ {name: name} # 5336
678
+ end
569
679
  end
570
680
 
681
+ # Base class for all sorted sets which contain jobs, e.g. scheduled, retry and dead.
682
+ # Sidekiq Pro and Enterprise add additional sorted sets which do not contain job data,
683
+ # e.g. Batches.
571
684
  class JobSet < SortedSet
572
- def schedule(timestamp, message)
685
+ # Add a job with the associated timestamp to this set.
686
+ # @param timestamp [Time] the score for the job
687
+ # @param job [Hash] the job data
688
+ def schedule(timestamp, job)
573
689
  Sidekiq.redis do |conn|
574
- conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
690
+ conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(job))
575
691
  end
576
692
  end
577
693
 
@@ -585,7 +701,7 @@ module Sidekiq
585
701
  range_start = page * page_size + offset_size
586
702
  range_end = range_start + page_size - 1
587
703
  elements = Sidekiq.redis { |conn|
588
- conn.zrange name, range_start, range_end, with_scores: true
704
+ conn.zrange name, range_start, range_end, withscores: true
589
705
  }
590
706
  break if elements.empty?
591
707
  page -= 1
@@ -599,6 +715,10 @@ module Sidekiq
599
715
  ##
600
716
  # Fetch jobs that match a given time or Range. Job ID is an
601
717
  # optional second argument.
718
+ #
719
+ # @param score [Time,Range] a specific timestamp or range
720
+ # @param jid [String, optional] find a specific JID within the score
721
+ # @return [Array<SortedEntry>] any results found, can be empty
602
722
  def fetch(score, jid = nil)
603
723
  begin_score, end_score =
604
724
  if score.is_a?(Range)
@@ -608,7 +728,7 @@ module Sidekiq
608
728
  end
609
729
 
610
730
  elements = Sidekiq.redis { |conn|
611
- conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
731
+ conn.zrangebyscore(name, begin_score, end_score, withscores: true)
612
732
  }
613
733
 
614
734
  elements.each_with_object([]) do |element, result|
@@ -620,7 +740,10 @@ module Sidekiq
620
740
 
621
741
  ##
622
742
  # Find the job with the given JID within this sorted set.
623
- # This is a slower O(n) operation. Do not use for app logic.
743
+ # *This is a slow O(n) operation*. Do not use for app logic.
744
+ #
745
+ # @param jid [String] the job identifier
746
+ # @return [SortedEntry] the record or nil
624
747
  def find_job(jid)
625
748
  Sidekiq.redis do |conn|
626
749
  conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
@@ -632,6 +755,8 @@ module Sidekiq
632
755
  nil
633
756
  end
634
757
 
758
+ # :nodoc:
759
+ # @api private
635
760
  def delete_by_value(name, value)
636
761
  Sidekiq.redis do |conn|
637
762
  ret = conn.zrem(name, value)
@@ -640,6 +765,8 @@ module Sidekiq
640
765
  end
641
766
  end
642
767
 
768
+ # :nodoc:
769
+ # @api private
643
770
  def delete_by_jid(score, jid)
644
771
  Sidekiq.redis do |conn|
645
772
  elements = conn.zrangebyscore(name, score, score)
@@ -660,10 +787,10 @@ module Sidekiq
660
787
  end
661
788
 
662
789
  ##
663
- # Allows enumeration of scheduled jobs within Sidekiq.
790
+ # The set of scheduled jobs within Sidekiq.
664
791
  # Based on this, you can search/filter for jobs. Here's an
665
- # example where I'm selecting all jobs of a certain type
666
- # and deleting them from the schedule queue.
792
+ # example where I'm selecting jobs based on some complex logic
793
+ # and deleting them from the scheduled set.
667
794
  #
668
795
  # r = Sidekiq::ScheduledSet.new
669
796
  # r.select do |scheduled|
@@ -678,7 +805,7 @@ module Sidekiq
678
805
  end
679
806
 
680
807
  ##
681
- # Allows enumeration of retries within Sidekiq.
808
+ # The set of retries within Sidekiq.
682
809
  # Based on this, you can search/filter for jobs. Here's an
683
810
  # example where I'm selecting all jobs of a certain type
684
811
  # and deleting them from the retry queue.
@@ -694,30 +821,36 @@ module Sidekiq
694
821
  super "retry"
695
822
  end
696
823
 
824
+ # Enqueues all jobs pending within the retry set.
697
825
  def retry_all
698
826
  each(&:retry) while size > 0
699
827
  end
700
828
 
829
+ # Kills all jobs pending within the retry set.
701
830
  def kill_all
702
831
  each(&:kill) while size > 0
703
832
  end
704
833
  end
705
834
 
706
835
  ##
707
- # Allows enumeration of dead jobs within Sidekiq.
836
+ # The set of dead jobs within Sidekiq. Dead jobs have failed all of
837
+ # their retries and are helding in this set pending some sort of manual
838
+ # fix. They will be removed after 6 months (dead_timeout) if not.
708
839
  #
709
840
  class DeadSet < JobSet
710
841
  def initialize
711
842
  super "dead"
712
843
  end
713
844
 
845
+ # Add the given job to the Dead set.
846
+ # @param message [String] the job data as JSON
714
847
  def kill(message, opts = {})
715
848
  now = Time.now.to_f
716
849
  Sidekiq.redis do |conn|
717
- conn.multi do
718
- conn.zadd(name, now.to_s, message)
719
- conn.zremrangebyscore(name, "-inf", now - self.class.timeout)
720
- conn.zremrangebyrank(name, 0, - self.class.max_jobs)
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)
721
854
  end
722
855
  end
723
856
 
@@ -732,16 +865,21 @@ module Sidekiq
732
865
  true
733
866
  end
734
867
 
868
+ # Enqueue all dead jobs
735
869
  def retry_all
736
870
  each(&:retry) while size > 0
737
871
  end
738
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.
739
875
  def self.max_jobs
740
- Sidekiq.options[:dead_max_jobs]
876
+ Sidekiq[:dead_max_jobs]
741
877
  end
742
878
 
879
+ # The time limit for entries within the Dead set. Older entries will be thrown away.
880
+ # Default value is six months.
743
881
  def self.timeout
744
- Sidekiq.options[:dead_timeout_in_seconds]
882
+ Sidekiq[:dead_timeout_in_seconds]
745
883
  end
746
884
  end
747
885
 
@@ -750,24 +888,31 @@ module Sidekiq
750
888
  # right now. Each process sends a heartbeat to Redis every 5 seconds
751
889
  # so this set should be relatively accurate, barring network partitions.
752
890
  #
753
- # Yields a Sidekiq::Process.
891
+ # @yieldparam [Sidekiq::Process]
754
892
  #
755
893
  class ProcessSet
756
894
  include Enumerable
757
895
 
896
+ # :nodoc:
897
+ # @api private
758
898
  def initialize(clean_plz = true)
759
899
  cleanup if clean_plz
760
900
  end
761
901
 
762
902
  # Cleans up dead processes recorded in Redis.
763
903
  # Returns the number of processes cleaned.
904
+ # :nodoc:
905
+ # @api private
764
906
  def cleanup
907
+ # dont run cleanup more than once per minute
908
+ return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
909
+
765
910
  count = 0
766
911
  Sidekiq.redis do |conn|
767
- procs = conn.sscan_each("processes").to_a.sort
768
- heartbeats = conn.pipelined {
912
+ procs = conn.sscan_each("processes").to_a
913
+ heartbeats = conn.pipelined { |pipeline|
769
914
  procs.each do |key|
770
- conn.hget(key, "info")
915
+ pipeline.hget(key, "info")
771
916
  end
772
917
  }
773
918
 
@@ -789,21 +934,25 @@ module Sidekiq
789
934
  # We're making a tradeoff here between consuming more memory instead of
790
935
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
791
936
  # you'll be happier this way
792
- conn.pipelined do
937
+ conn.pipelined do |pipeline|
793
938
  procs.each do |key|
794
- conn.hmget(key, "info", "busy", "beat", "quiet")
939
+ pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
795
940
  end
796
941
  end
797
942
  }
798
943
 
799
- result.each do |info, busy, at_s, quiet|
944
+ result.each do |info, busy, at_s, quiet, rss, rtt|
800
945
  # If a process is stopped between when we query Redis for `procs` and
801
946
  # when we query for `result`, we will have an item in `result` that is
802
947
  # composed of `nil` values.
803
948
  next if info.nil?
804
949
 
805
950
  hash = Sidekiq.load_json(info)
806
- yield Process.new(hash.merge("busy" => busy.to_i, "beat" => at_s.to_f, "quiet" => quiet))
951
+ yield Process.new(hash.merge("busy" => busy.to_i,
952
+ "beat" => at_s.to_f,
953
+ "quiet" => quiet,
954
+ "rss" => rss.to_i,
955
+ "rtt_us" => rtt.to_i))
807
956
  end
808
957
  end
809
958
 
@@ -811,13 +960,30 @@ module Sidekiq
811
960
  # based on current heartbeat. #each does that and ensures the set only
812
961
  # contains Sidekiq processes which have sent a heartbeat within the last
813
962
  # 60 seconds.
963
+ # @return [Integer] current number of registered Sidekiq processes
814
964
  def size
815
965
  Sidekiq.redis { |conn| conn.scard("processes") }
816
966
  end
817
967
 
968
+ # Total number of threads available to execute jobs.
969
+ # For Sidekiq Enterprise customers this number (in production) must be
970
+ # less than or equal to your licensed concurrency.
971
+ # @return [Integer] the sum of process concurrency
972
+ def total_concurrency
973
+ sum { |x| x["concurrency"].to_i }
974
+ end
975
+
976
+ # @return [Integer] total amount of RSS memory consumed by Sidekiq processes
977
+ def total_rss_in_kb
978
+ sum { |x| x["rss"].to_i }
979
+ end
980
+ alias_method :total_rss, :total_rss_in_kb
981
+
818
982
  # Returns the identity of the current cluster leader or "" if no leader.
819
983
  # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
820
984
  # or Sidekiq Pro.
985
+ # @return [String] Identity of cluster leader
986
+ # @return [String] empty string if no leader
821
987
  def leader
822
988
  @leader ||= begin
823
989
  x = Sidekiq.redis { |c| c.get("dear-leader") }
@@ -844,6 +1010,8 @@ module Sidekiq
844
1010
  # 'identity' => <unique string identifying the process>,
845
1011
  # }
846
1012
  class Process
1013
+ # :nodoc:
1014
+ # @api private
847
1015
  def initialize(hash)
848
1016
  @attribs = hash
849
1017
  end
@@ -864,18 +1032,35 @@ module Sidekiq
864
1032
  self["identity"]
865
1033
  end
866
1034
 
1035
+ def queues
1036
+ self["queues"]
1037
+ end
1038
+
1039
+ # Signal this process to stop processing new jobs.
1040
+ # It will continue to execute jobs it has already fetched.
1041
+ # This method is *asynchronous* and it can take 5-10
1042
+ # seconds for the process to quiet.
867
1043
  def quiet!
868
1044
  signal("TSTP")
869
1045
  end
870
1046
 
1047
+ # Signal this process to shutdown.
1048
+ # It will shutdown within its configured :timeout value, default 25 seconds.
1049
+ # This method is *asynchronous* and it can take 5-10
1050
+ # seconds for the process to start shutting down.
871
1051
  def stop!
872
1052
  signal("TERM")
873
1053
  end
874
1054
 
1055
+ # Signal this process to log backtraces for all threads.
1056
+ # Useful if you have a frozen or deadlocked process which is
1057
+ # still sending a heartbeat.
1058
+ # This method is *asynchronous* and it can take 5-10 seconds.
875
1059
  def dump_threads
876
1060
  signal("TTIN")
877
1061
  end
878
1062
 
1063
+ # @return [Boolean] true if this process is quiet or shutting down
879
1064
  def stopping?
880
1065
  self["quiet"] == "true"
881
1066
  end
@@ -885,17 +1070,17 @@ module Sidekiq
885
1070
  def signal(sig)
886
1071
  key = "#{identity}-signals"
887
1072
  Sidekiq.redis do |c|
888
- c.multi do
889
- c.lpush(key, sig)
890
- c.expire(key, 60)
1073
+ c.multi do |transaction|
1074
+ transaction.lpush(key, sig)
1075
+ transaction.expire(key, 60)
891
1076
  end
892
1077
  end
893
1078
  end
894
1079
  end
895
1080
 
896
1081
  ##
897
- # A worker is a thread that is currently processing a job.
898
- # Programmatic access to the current active worker set.
1082
+ # The WorkSet stores the work being done by this Sidekiq cluster.
1083
+ # It tracks the process and thread working on each job.
899
1084
  #
900
1085
  # WARNING WARNING WARNING
901
1086
  #
@@ -903,39 +1088,46 @@ module Sidekiq
903
1088
  # If you call #size => 5 and then expect #each to be
904
1089
  # called 5 times, you're going to have a bad time.
905
1090
  #
906
- # workers = Sidekiq::Workers.new
907
- # workers.size => 2
908
- # workers.each do |process_id, thread_id, work|
1091
+ # works = Sidekiq::WorkSet.new
1092
+ # works.size => 2
1093
+ # works.each do |process_id, thread_id, work|
909
1094
  # # process_id is a unique identifier per Sidekiq process
910
1095
  # # thread_id is a unique identifier per thread
911
1096
  # # work is a Hash which looks like:
912
- # # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
1097
+ # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
913
1098
  # # run_at is an epoch Integer.
914
1099
  # end
915
1100
  #
916
- class Workers
1101
+ class WorkSet
917
1102
  include Enumerable
918
1103
 
919
1104
  def each(&block)
920
1105
  results = []
1106
+ procs = nil
1107
+ all_works = nil
1108
+
921
1109
  Sidekiq.redis do |conn|
922
- procs = conn.sscan_each("processes").to_a
923
- procs.sort.each do |key|
924
- valid, workers = conn.pipelined {
925
- conn.exists?(key)
926
- conn.hgetall("#{key}:workers")
927
- }
928
- next unless valid
929
- workers.each_pair do |tid, json|
930
- hsh = Sidekiq.load_json(json)
931
- p = hsh["payload"]
932
- # avoid breaking API, this is a side effect of the JSON optimization in #4316
933
- hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
934
- results << [key, tid, hsh]
1110
+ procs = conn.sscan_each("processes").to_a.sort
1111
+
1112
+ all_works = conn.pipelined do |pipeline|
1113
+ procs.each do |key|
1114
+ pipeline.hgetall("#{key}:work")
935
1115
  end
936
1116
  end
937
1117
  end
938
1118
 
1119
+ procs.zip(all_works).each do |key, workers|
1120
+ 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]
1128
+ end
1129
+ end
1130
+
939
1131
  results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
940
1132
  end
941
1133
 
@@ -951,13 +1143,17 @@ module Sidekiq
951
1143
  if procs.empty?
952
1144
  0
953
1145
  else
954
- conn.pipelined {
1146
+ conn.pipelined { |pipeline|
955
1147
  procs.each do |key|
956
- conn.hget(key, "busy")
1148
+ pipeline.hget(key, "busy")
957
1149
  end
958
1150
  }.sum(&:to_i)
959
1151
  end
960
1152
  end
961
1153
  end
962
1154
  end
1155
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1156
+ # Is "worker" a process, a type of job, a thread? Undefined!
1157
+ # WorkSet better describes the data.
1158
+ Workers = WorkSet
963
1159
  end