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