sidekiq 6.4.0 → 6.5.12
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.
- checksums.yaml +4 -4
- data/Changes.md +119 -1
- data/README.md +6 -1
- data/bin/sidekiq +3 -3
- data/bin/sidekiqload +70 -66
- data/bin/sidekiqmon +1 -1
- data/lib/sidekiq/api.rb +255 -100
- data/lib/sidekiq/cli.rb +60 -38
- data/lib/sidekiq/client.rb +44 -30
- data/lib/sidekiq/component.rb +65 -0
- data/lib/sidekiq/delay.rb +2 -2
- data/lib/sidekiq/extensions/action_mailer.rb +2 -2
- data/lib/sidekiq/extensions/active_record.rb +2 -2
- data/lib/sidekiq/extensions/class_methods.rb +2 -2
- data/lib/sidekiq/extensions/generic_proxy.rb +3 -3
- data/lib/sidekiq/fetch.rb +20 -18
- data/lib/sidekiq/job_logger.rb +15 -27
- data/lib/sidekiq/job_retry.rb +73 -52
- data/lib/sidekiq/job_util.rb +15 -9
- data/lib/sidekiq/launcher.rb +58 -54
- data/lib/sidekiq/logger.rb +8 -18
- data/lib/sidekiq/manager.rb +28 -25
- data/lib/sidekiq/metrics/deploy.rb +47 -0
- data/lib/sidekiq/metrics/query.rb +153 -0
- data/lib/sidekiq/metrics/shared.rb +94 -0
- data/lib/sidekiq/metrics/tracking.rb +134 -0
- data/lib/sidekiq/middleware/chain.rb +82 -38
- data/lib/sidekiq/middleware/current_attributes.rb +18 -12
- data/lib/sidekiq/middleware/i18n.rb +6 -4
- data/lib/sidekiq/middleware/modules.rb +21 -0
- data/lib/sidekiq/monitor.rb +2 -2
- data/lib/sidekiq/paginator.rb +17 -9
- data/lib/sidekiq/processor.rb +47 -41
- data/lib/sidekiq/rails.rb +19 -13
- data/lib/sidekiq/redis_client_adapter.rb +154 -0
- data/lib/sidekiq/redis_connection.rb +80 -49
- data/lib/sidekiq/ring_buffer.rb +29 -0
- data/lib/sidekiq/scheduled.rb +53 -24
- data/lib/sidekiq/testing/inline.rb +4 -4
- data/lib/sidekiq/testing.rb +37 -36
- data/lib/sidekiq/transaction_aware_client.rb +45 -0
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/action.rb +3 -3
- data/lib/sidekiq/web/application.rb +21 -5
- data/lib/sidekiq/web/csrf_protection.rb +2 -2
- data/lib/sidekiq/web/helpers.rb +21 -8
- data/lib/sidekiq/web.rb +8 -4
- data/lib/sidekiq/worker.rb +26 -20
- data/lib/sidekiq.rb +107 -31
- data/sidekiq.gemspec +2 -2
- data/web/assets/javascripts/application.js +59 -26
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/web/assets/javascripts/dashboard.js +0 -17
- data/web/assets/javascripts/graph.js +16 -0
- data/web/assets/javascripts/metrics.js +262 -0
- data/web/assets/stylesheets/application.css +45 -1
- data/web/locales/el.yml +43 -19
- data/web/locales/en.yml +7 -0
- data/web/locales/ja.yml +7 -0
- data/web/locales/pt-br.yml +27 -9
- data/web/locales/zh-cn.yml +36 -11
- data/web/locales/zh-tw.yml +32 -7
- data/web/views/_nav.erb +1 -1
- data/web/views/_summary.erb +1 -1
- data/web/views/busy.erb +9 -4
- data/web/views/dashboard.erb +1 -0
- data/web/views/metrics.erb +69 -0
- data/web/views/metrics_for_job.erb +87 -0
- data/web/views/queue.erb +5 -1
- metadata +34 -9
- data/lib/sidekiq/exception_handler.rb +0 -27
- data/lib/sidekiq/util.rb +0 -108
data/lib/sidekiq/api.rb
CHANGED
@@ -3,9 +3,31 @@
|
|
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
33
|
fetch_stats_fast!
|
@@ -52,16 +74,17 @@ module Sidekiq
|
|
52
74
|
end
|
53
75
|
|
54
76
|
# O(1) redis calls
|
77
|
+
# @api private
|
55
78
|
def fetch_stats_fast!
|
56
79
|
pipe1_res = Sidekiq.redis { |conn|
|
57
|
-
conn.pipelined do
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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)
|
65
88
|
end
|
66
89
|
}
|
67
90
|
|
@@ -91,6 +114,7 @@ module Sidekiq
|
|
91
114
|
end
|
92
115
|
|
93
116
|
# O(number of processes + number of queues) redis calls
|
117
|
+
# @api private
|
94
118
|
def fetch_stats_slow!
|
95
119
|
processes = Sidekiq.redis { |conn|
|
96
120
|
conn.sscan_each("processes").to_a
|
@@ -101,9 +125,9 @@ module Sidekiq
|
|
101
125
|
}
|
102
126
|
|
103
127
|
pipe2_res = Sidekiq.redis { |conn|
|
104
|
-
conn.pipelined do
|
105
|
-
processes.each { |key|
|
106
|
-
queues.each { |queue|
|
128
|
+
conn.pipelined do |pipeline|
|
129
|
+
processes.each { |key| pipeline.hget(key, "busy") }
|
130
|
+
queues.each { |queue| pipeline.llen("queue:#{queue}") }
|
107
131
|
end
|
108
132
|
}
|
109
133
|
|
@@ -116,11 +140,13 @@ module Sidekiq
|
|
116
140
|
@stats
|
117
141
|
end
|
118
142
|
|
143
|
+
# @api private
|
119
144
|
def fetch_stats!
|
120
145
|
fetch_stats_fast!
|
121
146
|
fetch_stats_slow!
|
122
147
|
end
|
123
148
|
|
149
|
+
# @api private
|
124
150
|
def reset(*stats)
|
125
151
|
all = %w[failed processed]
|
126
152
|
stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
|
@@ -147,9 +173,9 @@ module Sidekiq
|
|
147
173
|
Sidekiq.redis do |conn|
|
148
174
|
queues = conn.sscan_each("queues").to_a
|
149
175
|
|
150
|
-
lengths = conn.pipelined {
|
176
|
+
lengths = conn.pipelined { |pipeline|
|
151
177
|
queues.each do |queue|
|
152
|
-
|
178
|
+
pipeline.llen("queue:#{queue}")
|
153
179
|
end
|
154
180
|
}
|
155
181
|
|
@@ -191,7 +217,7 @@ module Sidekiq
|
|
191
217
|
stat_hash[dates[idx]] = value ? value.to_i : 0
|
192
218
|
end
|
193
219
|
end
|
194
|
-
rescue
|
220
|
+
rescue RedisConnection.adapter::CommandError
|
195
221
|
# mget will trigger a CROSSSLOT error when run against a Cluster
|
196
222
|
# TODO Someone want to add Cluster support?
|
197
223
|
end
|
@@ -202,9 +228,10 @@ module Sidekiq
|
|
202
228
|
end
|
203
229
|
|
204
230
|
##
|
205
|
-
#
|
231
|
+
# Represents a queue within Sidekiq.
|
206
232
|
# Allows enumeration of all jobs within the queue
|
207
|
-
# 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.
|
208
235
|
#
|
209
236
|
# queue = Sidekiq::Queue.new("mailer")
|
210
237
|
# queue.each do |job|
|
@@ -212,29 +239,34 @@ module Sidekiq
|
|
212
239
|
# job.args # => [1, 2, 3]
|
213
240
|
# job.delete if job.jid == 'abcdef1234567890'
|
214
241
|
# end
|
215
|
-
#
|
216
242
|
class Queue
|
217
243
|
include Enumerable
|
218
244
|
|
219
245
|
##
|
220
|
-
#
|
246
|
+
# Fetch all known queues within Redis.
|
221
247
|
#
|
248
|
+
# @return [Array<Sidekiq::Queue>]
|
222
249
|
def self.all
|
223
250
|
Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
|
224
251
|
end
|
225
252
|
|
226
253
|
attr_reader :name
|
227
254
|
|
255
|
+
# @param name [String] the name of the queue
|
228
256
|
def initialize(name = "default")
|
229
257
|
@name = name.to_s
|
230
258
|
@rname = "queue:#{name}"
|
231
259
|
end
|
232
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
|
233
265
|
def size
|
234
266
|
Sidekiq.redis { |con| con.llen(@rname) }
|
235
267
|
end
|
236
268
|
|
237
|
-
#
|
269
|
+
# @return [Boolean] if the queue is currently paused
|
238
270
|
def paused?
|
239
271
|
false
|
240
272
|
end
|
@@ -243,7 +275,7 @@ module Sidekiq
|
|
243
275
|
# Calculates this queue's latency, the difference in seconds since the oldest
|
244
276
|
# job in the queue was enqueued.
|
245
277
|
#
|
246
|
-
# @return Float
|
278
|
+
# @return [Float] in seconds
|
247
279
|
def latency
|
248
280
|
entry = Sidekiq.redis { |conn|
|
249
281
|
conn.lrange(@rname, -1, -1)
|
@@ -279,34 +311,54 @@ module Sidekiq
|
|
279
311
|
##
|
280
312
|
# Find the job with the given JID within this queue.
|
281
313
|
#
|
282
|
-
# This is a slow, inefficient operation. Do not use under
|
314
|
+
# This is a *slow, inefficient* operation. Do not use under
|
283
315
|
# normal conditions.
|
316
|
+
#
|
317
|
+
# @param jid [String] the job_id to look for
|
318
|
+
# @return [Sidekiq::JobRecord]
|
319
|
+
# @return [nil] if not found
|
284
320
|
def find_job(jid)
|
285
321
|
detect { |j| j.jid == jid }
|
286
322
|
end
|
287
323
|
|
324
|
+
# delete all jobs within this queue
|
325
|
+
# @return [Boolean] true
|
288
326
|
def clear
|
289
327
|
Sidekiq.redis do |conn|
|
290
|
-
conn.multi do
|
291
|
-
|
292
|
-
|
328
|
+
conn.multi do |transaction|
|
329
|
+
transaction.unlink(@rname)
|
330
|
+
transaction.srem("queues", [name])
|
293
331
|
end
|
294
332
|
end
|
333
|
+
true
|
295
334
|
end
|
296
335
|
alias_method :💣, :clear
|
336
|
+
|
337
|
+
# :nodoc:
|
338
|
+
# @api private
|
339
|
+
def as_json(options = nil)
|
340
|
+
{name: name} # 5336
|
341
|
+
end
|
297
342
|
end
|
298
343
|
|
299
344
|
##
|
300
|
-
#
|
301
|
-
# sorted set.
|
345
|
+
# Represents a pending job within a Sidekiq queue.
|
302
346
|
#
|
303
347
|
# The job should be considered immutable but may be
|
304
348
|
# removed from the queue via JobRecord#delete.
|
305
|
-
#
|
306
349
|
class JobRecord
|
350
|
+
# the parsed Hash of job data
|
351
|
+
# @!attribute [r] Item
|
307
352
|
attr_reader :item
|
353
|
+
# the underlying String in Redis
|
354
|
+
# @!attribute [r] Value
|
308
355
|
attr_reader :value
|
356
|
+
# the queue associated with this job
|
357
|
+
# @!attribute [r] Queue
|
358
|
+
attr_reader :queue
|
309
359
|
|
360
|
+
# :nodoc:
|
361
|
+
# @api private
|
310
362
|
def initialize(item, queue_name = nil)
|
311
363
|
@args = nil
|
312
364
|
@value = item
|
@@ -314,6 +366,8 @@ module Sidekiq
|
|
314
366
|
@queue = queue_name || @item["queue"]
|
315
367
|
end
|
316
368
|
|
369
|
+
# :nodoc:
|
370
|
+
# @api private
|
317
371
|
def parse(item)
|
318
372
|
Sidekiq.load_json(item)
|
319
373
|
rescue JSON::ParserError
|
@@ -325,6 +379,8 @@ module Sidekiq
|
|
325
379
|
{}
|
326
380
|
end
|
327
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.
|
328
384
|
def klass
|
329
385
|
self["class"]
|
330
386
|
end
|
@@ -354,27 +410,31 @@ module Sidekiq
|
|
354
410
|
def display_args
|
355
411
|
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
356
412
|
@display_args ||= case klass
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
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
|
378
438
|
end
|
379
439
|
end
|
380
440
|
|
@@ -408,15 +468,12 @@ module Sidekiq
|
|
408
468
|
end
|
409
469
|
end
|
410
470
|
|
411
|
-
attr_reader :queue
|
412
|
-
|
413
471
|
def latency
|
414
472
|
now = Time.now.to_f
|
415
473
|
now - (@item["enqueued_at"] || @item["created_at"] || now)
|
416
474
|
end
|
417
475
|
|
418
|
-
|
419
|
-
# Remove this job from the queue.
|
476
|
+
# Remove this job from the queue
|
420
477
|
def delete
|
421
478
|
count = Sidekiq.redis { |conn|
|
422
479
|
conn.lrem("queue:#{@queue}", 1, @value)
|
@@ -424,6 +481,7 @@ module Sidekiq
|
|
424
481
|
count != 0
|
425
482
|
end
|
426
483
|
|
484
|
+
# Access arbitrary attributes within the job hash
|
427
485
|
def [](name)
|
428
486
|
# nil will happen if the JSON fails to parse.
|
429
487
|
# We don't guarantee Sidekiq will work with bad job JSON but we should
|
@@ -438,6 +496,7 @@ module Sidekiq
|
|
438
496
|
rescue => ex
|
439
497
|
# #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
|
440
498
|
# memory yet so the YAML can't be loaded.
|
499
|
+
# TODO is this still necessary? Zeitwerk reloader should handle?
|
441
500
|
Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
|
442
501
|
default
|
443
502
|
end
|
@@ -460,20 +519,28 @@ module Sidekiq
|
|
460
519
|
end
|
461
520
|
end
|
462
521
|
|
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).
|
463
526
|
class SortedEntry < JobRecord
|
464
527
|
attr_reader :score
|
465
528
|
attr_reader :parent
|
466
529
|
|
530
|
+
# :nodoc:
|
531
|
+
# @api private
|
467
532
|
def initialize(parent, score, item)
|
468
533
|
super(item)
|
469
|
-
@score = score
|
534
|
+
@score = Float(score)
|
470
535
|
@parent = parent
|
471
536
|
end
|
472
537
|
|
538
|
+
# The timestamp associated with this entry
|
473
539
|
def at
|
474
540
|
Time.at(score).utc
|
475
541
|
end
|
476
542
|
|
543
|
+
# remove this entry from the sorted set
|
477
544
|
def delete
|
478
545
|
if @value
|
479
546
|
@parent.delete_by_value(@parent.name, @value)
|
@@ -482,12 +549,17 @@ module Sidekiq
|
|
482
549
|
end
|
483
550
|
end
|
484
551
|
|
552
|
+
# Change the scheduled time for this job.
|
553
|
+
#
|
554
|
+
# @param at [Time] the new timestamp for this job
|
485
555
|
def reschedule(at)
|
486
556
|
Sidekiq.redis do |conn|
|
487
557
|
conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
|
488
558
|
end
|
489
559
|
end
|
490
560
|
|
561
|
+
# Enqueue this job from the scheduled or dead set so it will
|
562
|
+
# be executed at some point in the near future.
|
491
563
|
def add_to_queue
|
492
564
|
remove_job do |message|
|
493
565
|
msg = Sidekiq.load_json(message)
|
@@ -495,6 +567,8 @@ module Sidekiq
|
|
495
567
|
end
|
496
568
|
end
|
497
569
|
|
570
|
+
# enqueue this job from the retry set so it will be executed
|
571
|
+
# at some point in the near future.
|
498
572
|
def retry
|
499
573
|
remove_job do |message|
|
500
574
|
msg = Sidekiq.load_json(message)
|
@@ -503,8 +577,7 @@ module Sidekiq
|
|
503
577
|
end
|
504
578
|
end
|
505
579
|
|
506
|
-
|
507
|
-
# Place job in the dead set
|
580
|
+
# Move this job from its current set into the Dead set.
|
508
581
|
def kill
|
509
582
|
remove_job do |message|
|
510
583
|
DeadSet.new.kill(message)
|
@@ -519,9 +592,9 @@ module Sidekiq
|
|
519
592
|
|
520
593
|
def remove_job
|
521
594
|
Sidekiq.redis do |conn|
|
522
|
-
results = conn.multi {
|
523
|
-
|
524
|
-
|
595
|
+
results = conn.multi { |transaction|
|
596
|
+
transaction.zrangebyscore(parent.name, score, score)
|
597
|
+
transaction.zremrangebyscore(parent.name, score, score)
|
525
598
|
}.first
|
526
599
|
|
527
600
|
if results.size == 1
|
@@ -542,9 +615,9 @@ module Sidekiq
|
|
542
615
|
yield msg if msg
|
543
616
|
|
544
617
|
# push the rest back onto the sorted set
|
545
|
-
conn.multi do
|
618
|
+
conn.multi do |transaction|
|
546
619
|
nonmatched.each do |message|
|
547
|
-
|
620
|
+
transaction.zadd(parent.name, score.to_f.to_s, message)
|
548
621
|
end
|
549
622
|
end
|
550
623
|
end
|
@@ -552,20 +625,32 @@ module Sidekiq
|
|
552
625
|
end
|
553
626
|
end
|
554
627
|
|
628
|
+
# Base class for all sorted sets within Sidekiq.
|
555
629
|
class SortedSet
|
556
630
|
include Enumerable
|
557
631
|
|
632
|
+
# Redis key of the set
|
633
|
+
# @!attribute [r] Name
|
558
634
|
attr_reader :name
|
559
635
|
|
636
|
+
# :nodoc:
|
637
|
+
# @api private
|
560
638
|
def initialize(name)
|
561
639
|
@name = name
|
562
640
|
@_size = size
|
563
641
|
end
|
564
642
|
|
643
|
+
# real-time size of the set, will change
|
565
644
|
def size
|
566
645
|
Sidekiq.redis { |c| c.zcard(name) }
|
567
646
|
end
|
568
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
|
569
654
|
def scan(match, count = 100)
|
570
655
|
return to_enum(:scan, match, count) unless block_given?
|
571
656
|
|
@@ -577,18 +662,32 @@ module Sidekiq
|
|
577
662
|
end
|
578
663
|
end
|
579
664
|
|
665
|
+
# @return [Boolean] always true
|
580
666
|
def clear
|
581
667
|
Sidekiq.redis do |conn|
|
582
668
|
conn.unlink(name)
|
583
669
|
end
|
670
|
+
true
|
584
671
|
end
|
585
672
|
alias_method :💣, :clear
|
673
|
+
|
674
|
+
# :nodoc:
|
675
|
+
# @api private
|
676
|
+
def as_json(options = nil)
|
677
|
+
{name: name} # 5336
|
678
|
+
end
|
586
679
|
end
|
587
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.
|
588
684
|
class JobSet < SortedSet
|
589
|
-
|
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)
|
590
689
|
Sidekiq.redis do |conn|
|
591
|
-
conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(
|
690
|
+
conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(job))
|
592
691
|
end
|
593
692
|
end
|
594
693
|
|
@@ -602,7 +701,7 @@ module Sidekiq
|
|
602
701
|
range_start = page * page_size + offset_size
|
603
702
|
range_end = range_start + page_size - 1
|
604
703
|
elements = Sidekiq.redis { |conn|
|
605
|
-
conn.zrange name, range_start, range_end,
|
704
|
+
conn.zrange name, range_start, range_end, withscores: true
|
606
705
|
}
|
607
706
|
break if elements.empty?
|
608
707
|
page -= 1
|
@@ -616,6 +715,10 @@ module Sidekiq
|
|
616
715
|
##
|
617
716
|
# Fetch jobs that match a given time or Range. Job ID is an
|
618
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
|
619
722
|
def fetch(score, jid = nil)
|
620
723
|
begin_score, end_score =
|
621
724
|
if score.is_a?(Range)
|
@@ -625,7 +728,7 @@ module Sidekiq
|
|
625
728
|
end
|
626
729
|
|
627
730
|
elements = Sidekiq.redis { |conn|
|
628
|
-
conn.zrangebyscore(name, begin_score, end_score,
|
731
|
+
conn.zrangebyscore(name, begin_score, end_score, withscores: true)
|
629
732
|
}
|
630
733
|
|
631
734
|
elements.each_with_object([]) do |element, result|
|
@@ -637,7 +740,10 @@ module Sidekiq
|
|
637
740
|
|
638
741
|
##
|
639
742
|
# Find the job with the given JID within this sorted set.
|
640
|
-
# This is a
|
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
|
641
747
|
def find_job(jid)
|
642
748
|
Sidekiq.redis do |conn|
|
643
749
|
conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
|
@@ -649,6 +755,8 @@ module Sidekiq
|
|
649
755
|
nil
|
650
756
|
end
|
651
757
|
|
758
|
+
# :nodoc:
|
759
|
+
# @api private
|
652
760
|
def delete_by_value(name, value)
|
653
761
|
Sidekiq.redis do |conn|
|
654
762
|
ret = conn.zrem(name, value)
|
@@ -657,6 +765,8 @@ module Sidekiq
|
|
657
765
|
end
|
658
766
|
end
|
659
767
|
|
768
|
+
# :nodoc:
|
769
|
+
# @api private
|
660
770
|
def delete_by_jid(score, jid)
|
661
771
|
Sidekiq.redis do |conn|
|
662
772
|
elements = conn.zrangebyscore(name, score, score)
|
@@ -677,10 +787,10 @@ module Sidekiq
|
|
677
787
|
end
|
678
788
|
|
679
789
|
##
|
680
|
-
#
|
790
|
+
# The set of scheduled jobs within Sidekiq.
|
681
791
|
# Based on this, you can search/filter for jobs. Here's an
|
682
|
-
# example where I'm selecting
|
683
|
-
# and deleting them from the
|
792
|
+
# example where I'm selecting jobs based on some complex logic
|
793
|
+
# and deleting them from the scheduled set.
|
684
794
|
#
|
685
795
|
# r = Sidekiq::ScheduledSet.new
|
686
796
|
# r.select do |scheduled|
|
@@ -695,7 +805,7 @@ module Sidekiq
|
|
695
805
|
end
|
696
806
|
|
697
807
|
##
|
698
|
-
#
|
808
|
+
# The set of retries within Sidekiq.
|
699
809
|
# Based on this, you can search/filter for jobs. Here's an
|
700
810
|
# example where I'm selecting all jobs of a certain type
|
701
811
|
# and deleting them from the retry queue.
|
@@ -711,30 +821,36 @@ module Sidekiq
|
|
711
821
|
super "retry"
|
712
822
|
end
|
713
823
|
|
824
|
+
# Enqueues all jobs pending within the retry set.
|
714
825
|
def retry_all
|
715
826
|
each(&:retry) while size > 0
|
716
827
|
end
|
717
828
|
|
829
|
+
# Kills all jobs pending within the retry set.
|
718
830
|
def kill_all
|
719
831
|
each(&:kill) while size > 0
|
720
832
|
end
|
721
833
|
end
|
722
834
|
|
723
835
|
##
|
724
|
-
#
|
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.
|
725
839
|
#
|
726
840
|
class DeadSet < JobSet
|
727
841
|
def initialize
|
728
842
|
super "dead"
|
729
843
|
end
|
730
844
|
|
845
|
+
# Add the given job to the Dead set.
|
846
|
+
# @param message [String] the job data as JSON
|
731
847
|
def kill(message, opts = {})
|
732
848
|
now = Time.now.to_f
|
733
849
|
Sidekiq.redis do |conn|
|
734
|
-
conn.multi do
|
735
|
-
|
736
|
-
|
737
|
-
|
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)
|
738
854
|
end
|
739
855
|
end
|
740
856
|
|
@@ -749,16 +865,21 @@ module Sidekiq
|
|
749
865
|
true
|
750
866
|
end
|
751
867
|
|
868
|
+
# Enqueue all dead jobs
|
752
869
|
def retry_all
|
753
870
|
each(&:retry) while size > 0
|
754
871
|
end
|
755
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.
|
756
875
|
def self.max_jobs
|
757
|
-
Sidekiq
|
876
|
+
Sidekiq[:dead_max_jobs]
|
758
877
|
end
|
759
878
|
|
879
|
+
# The time limit for entries within the Dead set. Older entries will be thrown away.
|
880
|
+
# Default value is six months.
|
760
881
|
def self.timeout
|
761
|
-
Sidekiq
|
882
|
+
Sidekiq[:dead_timeout_in_seconds]
|
762
883
|
end
|
763
884
|
end
|
764
885
|
|
@@ -767,24 +888,31 @@ module Sidekiq
|
|
767
888
|
# right now. Each process sends a heartbeat to Redis every 5 seconds
|
768
889
|
# so this set should be relatively accurate, barring network partitions.
|
769
890
|
#
|
770
|
-
#
|
891
|
+
# @yieldparam [Sidekiq::Process]
|
771
892
|
#
|
772
893
|
class ProcessSet
|
773
894
|
include Enumerable
|
774
895
|
|
896
|
+
# :nodoc:
|
897
|
+
# @api private
|
775
898
|
def initialize(clean_plz = true)
|
776
899
|
cleanup if clean_plz
|
777
900
|
end
|
778
901
|
|
779
902
|
# Cleans up dead processes recorded in Redis.
|
780
903
|
# Returns the number of processes cleaned.
|
904
|
+
# :nodoc:
|
905
|
+
# @api private
|
781
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
|
+
|
782
910
|
count = 0
|
783
911
|
Sidekiq.redis do |conn|
|
784
|
-
procs = conn.sscan_each("processes").to_a
|
785
|
-
heartbeats = conn.pipelined {
|
912
|
+
procs = conn.sscan_each("processes").to_a
|
913
|
+
heartbeats = conn.pipelined { |pipeline|
|
786
914
|
procs.each do |key|
|
787
|
-
|
915
|
+
pipeline.hget(key, "info")
|
788
916
|
end
|
789
917
|
}
|
790
918
|
|
@@ -806,9 +934,9 @@ module Sidekiq
|
|
806
934
|
# We're making a tradeoff here between consuming more memory instead of
|
807
935
|
# making more roundtrips to Redis, but if you have hundreds or thousands of workers,
|
808
936
|
# you'll be happier this way
|
809
|
-
conn.pipelined do
|
937
|
+
conn.pipelined do |pipeline|
|
810
938
|
procs.each do |key|
|
811
|
-
|
939
|
+
pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
|
812
940
|
end
|
813
941
|
end
|
814
942
|
}
|
@@ -832,6 +960,7 @@ module Sidekiq
|
|
832
960
|
# based on current heartbeat. #each does that and ensures the set only
|
833
961
|
# contains Sidekiq processes which have sent a heartbeat within the last
|
834
962
|
# 60 seconds.
|
963
|
+
# @return [Integer] current number of registered Sidekiq processes
|
835
964
|
def size
|
836
965
|
Sidekiq.redis { |conn| conn.scard("processes") }
|
837
966
|
end
|
@@ -839,10 +968,12 @@ module Sidekiq
|
|
839
968
|
# Total number of threads available to execute jobs.
|
840
969
|
# For Sidekiq Enterprise customers this number (in production) must be
|
841
970
|
# less than or equal to your licensed concurrency.
|
971
|
+
# @return [Integer] the sum of process concurrency
|
842
972
|
def total_concurrency
|
843
973
|
sum { |x| x["concurrency"].to_i }
|
844
974
|
end
|
845
975
|
|
976
|
+
# @return [Integer] total amount of RSS memory consumed by Sidekiq processes
|
846
977
|
def total_rss_in_kb
|
847
978
|
sum { |x| x["rss"].to_i }
|
848
979
|
end
|
@@ -851,6 +982,8 @@ module Sidekiq
|
|
851
982
|
# Returns the identity of the current cluster leader or "" if no leader.
|
852
983
|
# This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
|
853
984
|
# or Sidekiq Pro.
|
985
|
+
# @return [String] Identity of cluster leader
|
986
|
+
# @return [String] empty string if no leader
|
854
987
|
def leader
|
855
988
|
@leader ||= begin
|
856
989
|
x = Sidekiq.redis { |c| c.get("dear-leader") }
|
@@ -877,6 +1010,8 @@ module Sidekiq
|
|
877
1010
|
# 'identity' => <unique string identifying the process>,
|
878
1011
|
# }
|
879
1012
|
class Process
|
1013
|
+
# :nodoc:
|
1014
|
+
# @api private
|
880
1015
|
def initialize(hash)
|
881
1016
|
@attribs = hash
|
882
1017
|
end
|
@@ -901,18 +1036,31 @@ module Sidekiq
|
|
901
1036
|
self["queues"]
|
902
1037
|
end
|
903
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.
|
904
1043
|
def quiet!
|
905
1044
|
signal("TSTP")
|
906
1045
|
end
|
907
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.
|
908
1051
|
def stop!
|
909
1052
|
signal("TERM")
|
910
1053
|
end
|
911
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.
|
912
1059
|
def dump_threads
|
913
1060
|
signal("TTIN")
|
914
1061
|
end
|
915
1062
|
|
1063
|
+
# @return [Boolean] true if this process is quiet or shutting down
|
916
1064
|
def stopping?
|
917
1065
|
self["quiet"] == "true"
|
918
1066
|
end
|
@@ -922,9 +1070,9 @@ module Sidekiq
|
|
922
1070
|
def signal(sig)
|
923
1071
|
key = "#{identity}-signals"
|
924
1072
|
Sidekiq.redis do |c|
|
925
|
-
c.multi do
|
926
|
-
|
927
|
-
|
1073
|
+
c.multi do |transaction|
|
1074
|
+
transaction.lpush(key, sig)
|
1075
|
+
transaction.expire(key, 60)
|
928
1076
|
end
|
929
1077
|
end
|
930
1078
|
end
|
@@ -955,24 +1103,31 @@ module Sidekiq
|
|
955
1103
|
|
956
1104
|
def each(&block)
|
957
1105
|
results = []
|
1106
|
+
procs = nil
|
1107
|
+
all_works = nil
|
1108
|
+
|
958
1109
|
Sidekiq.redis do |conn|
|
959
|
-
procs = conn.sscan_each("processes").to_a
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
}
|
965
|
-
next unless valid
|
966
|
-
workers.each_pair do |tid, json|
|
967
|
-
hsh = Sidekiq.load_json(json)
|
968
|
-
p = hsh["payload"]
|
969
|
-
# avoid breaking API, this is a side effect of the JSON optimization in #4316
|
970
|
-
hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
|
971
|
-
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")
|
972
1115
|
end
|
973
1116
|
end
|
974
1117
|
end
|
975
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
|
+
|
976
1131
|
results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
|
977
1132
|
end
|
978
1133
|
|
@@ -988,9 +1143,9 @@ module Sidekiq
|
|
988
1143
|
if procs.empty?
|
989
1144
|
0
|
990
1145
|
else
|
991
|
-
conn.pipelined {
|
1146
|
+
conn.pipelined { |pipeline|
|
992
1147
|
procs.each do |key|
|
993
|
-
|
1148
|
+
pipeline.hget(key, "busy")
|
994
1149
|
end
|
995
1150
|
}.sum(&:to_i)
|
996
1151
|
end
|