sidekiq 6.2.2 → 7.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sidekiq might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Changes.md +279 -11
- data/LICENSE.txt +9 -0
- data/README.md +45 -32
- data/bin/sidekiq +4 -9
- data/bin/sidekiqload +207 -117
- data/bin/sidekiqmon +4 -1
- data/lib/generators/sidekiq/job_generator.rb +57 -0
- data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
- data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
- data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
- data/lib/sidekiq/api.rb +331 -187
- data/lib/sidekiq/capsule.rb +127 -0
- data/lib/sidekiq/cli.rb +94 -81
- data/lib/sidekiq/client.rb +100 -96
- data/lib/sidekiq/{util.rb → component.rb} +14 -41
- data/lib/sidekiq/config.rb +274 -0
- data/lib/sidekiq/deploy.rb +62 -0
- data/lib/sidekiq/embedded.rb +61 -0
- data/lib/sidekiq/fetch.rb +26 -26
- data/lib/sidekiq/job.rb +371 -5
- data/lib/sidekiq/job_logger.rb +16 -28
- data/lib/sidekiq/job_retry.rb +85 -59
- data/lib/sidekiq/job_util.rb +105 -0
- data/lib/sidekiq/launcher.rb +106 -94
- data/lib/sidekiq/logger.rb +9 -44
- data/lib/sidekiq/manager.rb +40 -41
- 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 +96 -51
- data/lib/sidekiq/middleware/current_attributes.rb +56 -0
- data/lib/sidekiq/middleware/i18n.rb +6 -4
- data/lib/sidekiq/middleware/modules.rb +21 -0
- data/lib/sidekiq/monitor.rb +17 -4
- data/lib/sidekiq/paginator.rb +17 -9
- data/lib/sidekiq/processor.rb +60 -60
- data/lib/sidekiq/rails.rb +25 -6
- data/lib/sidekiq/redis_client_adapter.rb +96 -0
- data/lib/sidekiq/redis_connection.rb +17 -88
- data/lib/sidekiq/ring_buffer.rb +29 -0
- data/lib/sidekiq/scheduled.rb +101 -44
- data/lib/sidekiq/testing/inline.rb +4 -4
- data/lib/sidekiq/testing.rb +41 -68
- data/lib/sidekiq/transaction_aware_client.rb +44 -0
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web/action.rb +3 -3
- data/lib/sidekiq/web/application.rb +47 -13
- data/lib/sidekiq/web/csrf_protection.rb +3 -3
- data/lib/sidekiq/web/helpers.rb +36 -33
- data/lib/sidekiq/web.rb +10 -17
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +86 -201
- data/sidekiq.gemspec +12 -10
- data/web/assets/javascripts/application.js +131 -60
- 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 +36 -273
- data/web/assets/javascripts/metrics.js +264 -0
- data/web/assets/stylesheets/application-dark.css +23 -23
- data/web/assets/stylesheets/application-rtl.css +2 -95
- data/web/assets/stylesheets/application.css +73 -402
- 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 +81 -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 +63 -55
- 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 +6 -3
- 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 +3 -6
- data/web/views/_summary.erb +7 -7
- data/web/views/busy.erb +44 -28
- data/web/views/dashboard.erb +44 -12
- data/web/views/layout.erb +1 -1
- 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 +24 -24
- data/web/views/queues.erb +4 -2
- data/web/views/retries.erb +5 -9
- data/web/views/scheduled.erb +12 -13
- metadata +62 -31
- data/LICENSE +0 -9
- data/lib/generators/sidekiq/worker_generator.rb +0 -57
- data/lib/sidekiq/delay.rb +0 -41
- data/lib/sidekiq/exception_handler.rb +0 -27
- 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 -244
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,20 +67,32 @@ 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
|
-
conn.pipelined do
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
88
|
+
conn.pipelined do |pipeline|
|
89
|
+
pipeline.get("stat:processed")
|
90
|
+
pipeline.get("stat:failed")
|
91
|
+
pipeline.zcard("schedule")
|
92
|
+
pipeline.zcard("retry")
|
93
|
+
pipeline.zcard("dead")
|
94
|
+
pipeline.scard("processes")
|
95
|
+
pipeline.lrange("queue:default", -1, -1)
|
65
96
|
end
|
66
97
|
}
|
67
98
|
|
@@ -91,35 +122,39 @@ 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|
|
104
|
-
conn.pipelined do
|
105
|
-
processes.each { |key|
|
106
|
-
queues.each { |queue|
|
136
|
+
conn.pipelined do |pipeline|
|
137
|
+
processes.each { |key| pipeline.hget(key, "busy") }
|
138
|
+
queues.each { |queue| pipeline.llen("queue:#{queue}") }
|
107
139
|
end
|
108
140
|
}
|
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
|
148
|
+
@stats
|
116
149
|
end
|
117
150
|
|
151
|
+
# @api private
|
118
152
|
def fetch_stats!
|
119
153
|
fetch_stats_fast!
|
120
154
|
fetch_stats_slow!
|
121
155
|
end
|
122
156
|
|
157
|
+
# @api private
|
123
158
|
def reset(*stats)
|
124
159
|
all = %w[failed processed]
|
125
160
|
stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
|
@@ -141,25 +176,10 @@ module Sidekiq
|
|
141
176
|
@stats[s] || raise(ArgumentError, "Unknown stat #{s}")
|
142
177
|
end
|
143
178
|
|
144
|
-
class Queues
|
145
|
-
def lengths
|
146
|
-
Sidekiq.redis do |conn|
|
147
|
-
queues = conn.sscan_each("queues").to_a
|
148
|
-
|
149
|
-
lengths = conn.pipelined {
|
150
|
-
queues.each do |queue|
|
151
|
-
conn.llen("queue:#{queue}")
|
152
|
-
end
|
153
|
-
}
|
154
|
-
|
155
|
-
array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
|
156
|
-
array_of_arrays.to_h
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
179
|
class History
|
162
|
-
def initialize(days_previous, start_date = nil)
|
180
|
+
def initialize(days_previous, start_date = nil, pool: nil)
|
181
|
+
# we only store five years of data in Redis
|
182
|
+
raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
|
163
183
|
@days_previous = days_previous
|
164
184
|
@start_date = start_date || Time.now.utc.to_date
|
165
185
|
end
|
@@ -182,15 +202,10 @@ module Sidekiq
|
|
182
202
|
|
183
203
|
keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
|
184
204
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
stat_hash[dates[idx]] = value ? value.to_i : 0
|
189
|
-
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
|
190
208
|
end
|
191
|
-
rescue Redis::CommandError
|
192
|
-
# mget will trigger a CROSSSLOT error when run against a Cluster
|
193
|
-
# TODO Someone want to add Cluster support?
|
194
209
|
end
|
195
210
|
|
196
211
|
stat_hash
|
@@ -199,9 +214,10 @@ module Sidekiq
|
|
199
214
|
end
|
200
215
|
|
201
216
|
##
|
202
|
-
#
|
217
|
+
# Represents a queue within Sidekiq.
|
203
218
|
# Allows enumeration of all jobs within the queue
|
204
|
-
# 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.
|
205
221
|
#
|
206
222
|
# queue = Sidekiq::Queue.new("mailer")
|
207
223
|
# queue.each do |job|
|
@@ -209,29 +225,34 @@ module Sidekiq
|
|
209
225
|
# job.args # => [1, 2, 3]
|
210
226
|
# job.delete if job.jid == 'abcdef1234567890'
|
211
227
|
# end
|
212
|
-
#
|
213
228
|
class Queue
|
214
229
|
include Enumerable
|
215
230
|
|
216
231
|
##
|
217
|
-
#
|
232
|
+
# Fetch all known queues within Redis.
|
218
233
|
#
|
234
|
+
# @return [Array<Sidekiq::Queue>]
|
219
235
|
def self.all
|
220
|
-
Sidekiq.redis { |c| c.
|
236
|
+
Sidekiq.redis { |c| c.sscan("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
|
221
237
|
end
|
222
238
|
|
223
239
|
attr_reader :name
|
224
240
|
|
241
|
+
# @param name [String] the name of the queue
|
225
242
|
def initialize(name = "default")
|
226
243
|
@name = name.to_s
|
227
244
|
@rname = "queue:#{name}"
|
228
245
|
end
|
229
246
|
|
247
|
+
# The current size of the queue within Redis.
|
248
|
+
# This value is real-time and can change between calls.
|
249
|
+
#
|
250
|
+
# @return [Integer] the size
|
230
251
|
def size
|
231
252
|
Sidekiq.redis { |con| con.llen(@rname) }
|
232
253
|
end
|
233
254
|
|
234
|
-
#
|
255
|
+
# @return [Boolean] if the queue is currently paused
|
235
256
|
def paused?
|
236
257
|
false
|
237
258
|
end
|
@@ -240,7 +261,7 @@ module Sidekiq
|
|
240
261
|
# Calculates this queue's latency, the difference in seconds since the oldest
|
241
262
|
# job in the queue was enqueued.
|
242
263
|
#
|
243
|
-
# @return Float
|
264
|
+
# @return [Float] in seconds
|
244
265
|
def latency
|
245
266
|
entry = Sidekiq.redis { |conn|
|
246
267
|
conn.lrange(@rname, -1, -1)
|
@@ -276,34 +297,54 @@ module Sidekiq
|
|
276
297
|
##
|
277
298
|
# Find the job with the given JID within this queue.
|
278
299
|
#
|
279
|
-
# This is a slow, inefficient operation. Do not use under
|
300
|
+
# This is a *slow, inefficient* operation. Do not use under
|
280
301
|
# normal conditions.
|
302
|
+
#
|
303
|
+
# @param jid [String] the job_id to look for
|
304
|
+
# @return [Sidekiq::JobRecord]
|
305
|
+
# @return [nil] if not found
|
281
306
|
def find_job(jid)
|
282
307
|
detect { |j| j.jid == jid }
|
283
308
|
end
|
284
309
|
|
310
|
+
# delete all jobs within this queue
|
311
|
+
# @return [Boolean] true
|
285
312
|
def clear
|
286
313
|
Sidekiq.redis do |conn|
|
287
|
-
conn.multi do
|
288
|
-
|
289
|
-
|
314
|
+
conn.multi do |transaction|
|
315
|
+
transaction.unlink(@rname)
|
316
|
+
transaction.srem("queues", [name])
|
290
317
|
end
|
291
318
|
end
|
319
|
+
true
|
292
320
|
end
|
293
321
|
alias_method :💣, :clear
|
322
|
+
|
323
|
+
# :nodoc:
|
324
|
+
# @api private
|
325
|
+
def as_json(options = nil)
|
326
|
+
{name: name} # 5336
|
327
|
+
end
|
294
328
|
end
|
295
329
|
|
296
330
|
##
|
297
|
-
#
|
298
|
-
# sorted set.
|
331
|
+
# Represents a pending job within a Sidekiq queue.
|
299
332
|
#
|
300
333
|
# The job should be considered immutable but may be
|
301
334
|
# removed from the queue via JobRecord#delete.
|
302
|
-
#
|
303
335
|
class JobRecord
|
336
|
+
# the parsed Hash of job data
|
337
|
+
# @!attribute [r] Item
|
304
338
|
attr_reader :item
|
339
|
+
# the underlying String in Redis
|
340
|
+
# @!attribute [r] Value
|
305
341
|
attr_reader :value
|
342
|
+
# the queue associated with this job
|
343
|
+
# @!attribute [r] Queue
|
344
|
+
attr_reader :queue
|
306
345
|
|
346
|
+
# :nodoc:
|
347
|
+
# @api private
|
307
348
|
def initialize(item, queue_name = nil)
|
308
349
|
@args = nil
|
309
350
|
@value = item
|
@@ -311,6 +352,8 @@ module Sidekiq
|
|
311
352
|
@queue = queue_name || @item["queue"]
|
312
353
|
end
|
313
354
|
|
355
|
+
# :nodoc:
|
356
|
+
# @api private
|
314
357
|
def parse(item)
|
315
358
|
Sidekiq.load_json(item)
|
316
359
|
rescue JSON::ParserError
|
@@ -322,6 +365,8 @@ module Sidekiq
|
|
322
365
|
{}
|
323
366
|
end
|
324
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.
|
325
370
|
def klass
|
326
371
|
self["class"]
|
327
372
|
end
|
@@ -329,12 +374,7 @@ module Sidekiq
|
|
329
374
|
def display_class
|
330
375
|
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
331
376
|
@klass ||= self["display_class"] || begin
|
332
|
-
|
333
|
-
when /\ASidekiq::Extensions::Delayed/
|
334
|
-
safe_load(args[0], klass) do |target, method, _|
|
335
|
-
"#{target}.#{method}"
|
336
|
-
end
|
337
|
-
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
377
|
+
if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
338
378
|
job_class = @item["wrapped"] || args[0]
|
339
379
|
if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
|
340
380
|
# MailerClass#mailer_method
|
@@ -350,28 +390,23 @@ module Sidekiq
|
|
350
390
|
|
351
391
|
def display_args
|
352
392
|
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
353
|
-
@display_args ||=
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
if self["encrypt"]
|
371
|
-
# no point in showing 150+ bytes of random garbage
|
372
|
-
args[-1] = "[encrypted data]"
|
373
|
-
end
|
374
|
-
args
|
393
|
+
@display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
394
|
+
job_args = self["wrapped"] ? deserialize_argument(args[0]["arguments"]) : []
|
395
|
+
if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
|
396
|
+
# remove MailerClass, mailer_method and 'deliver_now'
|
397
|
+
job_args.drop(3)
|
398
|
+
elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
|
399
|
+
# remove MailerClass, mailer_method and 'deliver_now'
|
400
|
+
job_args.drop(3).first.values_at("params", "args")
|
401
|
+
else
|
402
|
+
job_args
|
403
|
+
end
|
404
|
+
else
|
405
|
+
if self["encrypt"]
|
406
|
+
# no point in showing 150+ bytes of random garbage
|
407
|
+
args[-1] = "[encrypted data]"
|
408
|
+
end
|
409
|
+
args
|
375
410
|
end
|
376
411
|
end
|
377
412
|
|
@@ -383,6 +418,10 @@ module Sidekiq
|
|
383
418
|
self["jid"]
|
384
419
|
end
|
385
420
|
|
421
|
+
def bid
|
422
|
+
self["bid"]
|
423
|
+
end
|
424
|
+
|
386
425
|
def enqueued_at
|
387
426
|
self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
|
388
427
|
end
|
@@ -405,15 +444,12 @@ module Sidekiq
|
|
405
444
|
end
|
406
445
|
end
|
407
446
|
|
408
|
-
attr_reader :queue
|
409
|
-
|
410
447
|
def latency
|
411
448
|
now = Time.now.to_f
|
412
449
|
now - (@item["enqueued_at"] || @item["created_at"] || now)
|
413
450
|
end
|
414
451
|
|
415
|
-
|
416
|
-
# Remove this job from the queue.
|
452
|
+
# Remove this job from the queue
|
417
453
|
def delete
|
418
454
|
count = Sidekiq.redis { |conn|
|
419
455
|
conn.lrem("queue:#{@queue}", 1, @value)
|
@@ -421,6 +457,7 @@ module Sidekiq
|
|
421
457
|
count != 0
|
422
458
|
end
|
423
459
|
|
460
|
+
# Access arbitrary attributes within the job hash
|
424
461
|
def [](name)
|
425
462
|
# nil will happen if the JSON fails to parse.
|
426
463
|
# We don't guarantee Sidekiq will work with bad job JSON but we should
|
@@ -430,47 +467,58 @@ module Sidekiq
|
|
430
467
|
|
431
468
|
private
|
432
469
|
|
433
|
-
|
434
|
-
|
435
|
-
rescue => ex
|
436
|
-
# #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
|
437
|
-
# memory yet so the YAML can't be loaded.
|
438
|
-
Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
|
439
|
-
default
|
440
|
-
end
|
470
|
+
ACTIVE_JOB_PREFIX = "_aj_"
|
471
|
+
GLOBALID_KEY = "_aj_globalid"
|
441
472
|
|
442
|
-
def
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
# Handle old jobs with marshalled backtrace format
|
453
|
-
# TODO Remove in 7.x
|
454
|
-
Marshal.load(uncompressed)
|
473
|
+
def deserialize_argument(argument)
|
474
|
+
case argument
|
475
|
+
when Array
|
476
|
+
argument.map { |arg| deserialize_argument(arg) }
|
477
|
+
when Hash
|
478
|
+
if serialized_global_id?(argument)
|
479
|
+
argument[GLOBALID_KEY]
|
480
|
+
else
|
481
|
+
argument.transform_values { |v| deserialize_argument(v) }
|
482
|
+
.reject { |k, _| k.start_with?(ACTIVE_JOB_PREFIX) }
|
455
483
|
end
|
484
|
+
else
|
485
|
+
argument
|
456
486
|
end
|
457
487
|
end
|
488
|
+
|
489
|
+
def serialized_global_id?(hash)
|
490
|
+
hash.size == 1 && hash.include?(GLOBALID_KEY)
|
491
|
+
end
|
492
|
+
|
493
|
+
def uncompress_backtrace(backtrace)
|
494
|
+
decoded = Base64.decode64(backtrace)
|
495
|
+
uncompressed = Zlib::Inflate.inflate(decoded)
|
496
|
+
Sidekiq.load_json(uncompressed)
|
497
|
+
end
|
458
498
|
end
|
459
499
|
|
500
|
+
# Represents a job within a Redis sorted set where the score
|
501
|
+
# represents a timestamp associated with the job. This timestamp
|
502
|
+
# could be the scheduled time for it to run (e.g. scheduled set),
|
503
|
+
# or the expiration date after which the entry should be deleted (e.g. dead set).
|
460
504
|
class SortedEntry < JobRecord
|
461
505
|
attr_reader :score
|
462
506
|
attr_reader :parent
|
463
507
|
|
508
|
+
# :nodoc:
|
509
|
+
# @api private
|
464
510
|
def initialize(parent, score, item)
|
465
511
|
super(item)
|
466
|
-
@score = score
|
512
|
+
@score = Float(score)
|
467
513
|
@parent = parent
|
468
514
|
end
|
469
515
|
|
516
|
+
# The timestamp associated with this entry
|
470
517
|
def at
|
471
518
|
Time.at(score).utc
|
472
519
|
end
|
473
520
|
|
521
|
+
# remove this entry from the sorted set
|
474
522
|
def delete
|
475
523
|
if @value
|
476
524
|
@parent.delete_by_value(@parent.name, @value)
|
@@ -479,12 +527,17 @@ module Sidekiq
|
|
479
527
|
end
|
480
528
|
end
|
481
529
|
|
530
|
+
# Change the scheduled time for this job.
|
531
|
+
#
|
532
|
+
# @param at [Time] the new timestamp for this job
|
482
533
|
def reschedule(at)
|
483
534
|
Sidekiq.redis do |conn|
|
484
535
|
conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
|
485
536
|
end
|
486
537
|
end
|
487
538
|
|
539
|
+
# Enqueue this job from the scheduled or dead set so it will
|
540
|
+
# be executed at some point in the near future.
|
488
541
|
def add_to_queue
|
489
542
|
remove_job do |message|
|
490
543
|
msg = Sidekiq.load_json(message)
|
@@ -492,6 +545,8 @@ module Sidekiq
|
|
492
545
|
end
|
493
546
|
end
|
494
547
|
|
548
|
+
# enqueue this job from the retry set so it will be executed
|
549
|
+
# at some point in the near future.
|
495
550
|
def retry
|
496
551
|
remove_job do |message|
|
497
552
|
msg = Sidekiq.load_json(message)
|
@@ -500,8 +555,7 @@ module Sidekiq
|
|
500
555
|
end
|
501
556
|
end
|
502
557
|
|
503
|
-
|
504
|
-
# Place job in the dead set
|
558
|
+
# Move this job from its current set into the Dead set.
|
505
559
|
def kill
|
506
560
|
remove_job do |message|
|
507
561
|
DeadSet.new.kill(message)
|
@@ -516,9 +570,9 @@ module Sidekiq
|
|
516
570
|
|
517
571
|
def remove_job
|
518
572
|
Sidekiq.redis do |conn|
|
519
|
-
results = conn.multi {
|
520
|
-
|
521
|
-
|
573
|
+
results = conn.multi { |transaction|
|
574
|
+
transaction.zrange(parent.name, score, score, "BYSCORE")
|
575
|
+
transaction.zremrangebyscore(parent.name, score, score)
|
522
576
|
}.first
|
523
577
|
|
524
578
|
if results.size == 1
|
@@ -539,9 +593,9 @@ module Sidekiq
|
|
539
593
|
yield msg if msg
|
540
594
|
|
541
595
|
# push the rest back onto the sorted set
|
542
|
-
conn.multi do
|
596
|
+
conn.multi do |transaction|
|
543
597
|
nonmatched.each do |message|
|
544
|
-
|
598
|
+
transaction.zadd(parent.name, score.to_f.to_s, message)
|
545
599
|
end
|
546
600
|
end
|
547
601
|
end
|
@@ -549,43 +603,69 @@ module Sidekiq
|
|
549
603
|
end
|
550
604
|
end
|
551
605
|
|
606
|
+
# Base class for all sorted sets within Sidekiq.
|
552
607
|
class SortedSet
|
553
608
|
include Enumerable
|
554
609
|
|
610
|
+
# Redis key of the set
|
611
|
+
# @!attribute [r] Name
|
555
612
|
attr_reader :name
|
556
613
|
|
614
|
+
# :nodoc:
|
615
|
+
# @api private
|
557
616
|
def initialize(name)
|
558
617
|
@name = name
|
559
618
|
@_size = size
|
560
619
|
end
|
561
620
|
|
621
|
+
# real-time size of the set, will change
|
562
622
|
def size
|
563
623
|
Sidekiq.redis { |c| c.zcard(name) }
|
564
624
|
end
|
565
625
|
|
626
|
+
# Scan through each element of the sorted set, yielding each to the supplied block.
|
627
|
+
# Please see Redis's <a href="https://redis.io/commands/scan/">SCAN documentation</a> for implementation details.
|
628
|
+
#
|
629
|
+
# @param match [String] a snippet or regexp to filter matches.
|
630
|
+
# @param count [Integer] number of elements to retrieve at a time, default 100
|
631
|
+
# @yieldparam [Sidekiq::SortedEntry] each entry
|
566
632
|
def scan(match, count = 100)
|
567
633
|
return to_enum(:scan, match, count) unless block_given?
|
568
634
|
|
569
635
|
match = "*#{match}*" unless match.include?("*")
|
570
636
|
Sidekiq.redis do |conn|
|
571
|
-
conn.
|
637
|
+
conn.zscan(name, match: match, count: count) do |entry, score|
|
572
638
|
yield SortedEntry.new(self, score, entry)
|
573
639
|
end
|
574
640
|
end
|
575
641
|
end
|
576
642
|
|
643
|
+
# @return [Boolean] always true
|
577
644
|
def clear
|
578
645
|
Sidekiq.redis do |conn|
|
579
646
|
conn.unlink(name)
|
580
647
|
end
|
648
|
+
true
|
581
649
|
end
|
582
650
|
alias_method :💣, :clear
|
651
|
+
|
652
|
+
# :nodoc:
|
653
|
+
# @api private
|
654
|
+
def as_json(options = nil)
|
655
|
+
{name: name} # 5336
|
656
|
+
end
|
583
657
|
end
|
584
658
|
|
659
|
+
# Base class for all sorted sets which contain jobs, e.g. scheduled, retry and dead.
|
660
|
+
# Sidekiq Pro and Enterprise add additional sorted sets which do not contain job data,
|
661
|
+
# e.g. Batches.
|
585
662
|
class JobSet < SortedSet
|
586
|
-
|
663
|
+
# Add a job with the associated timestamp to this set.
|
664
|
+
# @param timestamp [Time] the score for the job
|
665
|
+
# @param job [Hash] the job data
|
666
|
+
def schedule(timestamp, job)
|
587
667
|
Sidekiq.redis do |conn|
|
588
|
-
conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(
|
668
|
+
conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(job))
|
589
669
|
end
|
590
670
|
end
|
591
671
|
|
@@ -599,7 +679,7 @@ module Sidekiq
|
|
599
679
|
range_start = page * page_size + offset_size
|
600
680
|
range_end = range_start + page_size - 1
|
601
681
|
elements = Sidekiq.redis { |conn|
|
602
|
-
conn.zrange name, range_start, range_end,
|
682
|
+
conn.zrange name, range_start, range_end, withscores: true
|
603
683
|
}
|
604
684
|
break if elements.empty?
|
605
685
|
page -= 1
|
@@ -613,6 +693,10 @@ module Sidekiq
|
|
613
693
|
##
|
614
694
|
# Fetch jobs that match a given time or Range. Job ID is an
|
615
695
|
# optional second argument.
|
696
|
+
#
|
697
|
+
# @param score [Time,Range] a specific timestamp or range
|
698
|
+
# @param jid [String, optional] find a specific JID within the score
|
699
|
+
# @return [Array<SortedEntry>] any results found, can be empty
|
616
700
|
def fetch(score, jid = nil)
|
617
701
|
begin_score, end_score =
|
618
702
|
if score.is_a?(Range)
|
@@ -622,7 +706,7 @@ module Sidekiq
|
|
622
706
|
end
|
623
707
|
|
624
708
|
elements = Sidekiq.redis { |conn|
|
625
|
-
conn.
|
709
|
+
conn.zrange(name, begin_score, end_score, "BYSCORE", withscores: true)
|
626
710
|
}
|
627
711
|
|
628
712
|
elements.each_with_object([]) do |element, result|
|
@@ -634,11 +718,14 @@ module Sidekiq
|
|
634
718
|
|
635
719
|
##
|
636
720
|
# Find the job with the given JID within this sorted set.
|
637
|
-
# This is a
|
721
|
+
# *This is a slow O(n) operation*. Do not use for app logic.
|
722
|
+
#
|
723
|
+
# @param jid [String] the job identifier
|
724
|
+
# @return [SortedEntry] the record or nil
|
638
725
|
def find_job(jid)
|
639
726
|
Sidekiq.redis do |conn|
|
640
|
-
conn.
|
641
|
-
job =
|
727
|
+
conn.zscan(name, match: "*#{jid}*", count: 100) do |entry, score|
|
728
|
+
job = Sidekiq.load_json(entry)
|
642
729
|
matched = job["jid"] == jid
|
643
730
|
return SortedEntry.new(self, score, entry) if matched
|
644
731
|
end
|
@@ -646,6 +733,8 @@ module Sidekiq
|
|
646
733
|
nil
|
647
734
|
end
|
648
735
|
|
736
|
+
# :nodoc:
|
737
|
+
# @api private
|
649
738
|
def delete_by_value(name, value)
|
650
739
|
Sidekiq.redis do |conn|
|
651
740
|
ret = conn.zrem(name, value)
|
@@ -654,9 +743,11 @@ module Sidekiq
|
|
654
743
|
end
|
655
744
|
end
|
656
745
|
|
746
|
+
# :nodoc:
|
747
|
+
# @api private
|
657
748
|
def delete_by_jid(score, jid)
|
658
749
|
Sidekiq.redis do |conn|
|
659
|
-
elements = conn.
|
750
|
+
elements = conn.zrange(name, score, score, "BYSCORE")
|
660
751
|
elements.each do |element|
|
661
752
|
if element.index(jid)
|
662
753
|
message = Sidekiq.load_json(element)
|
@@ -674,17 +765,13 @@ module Sidekiq
|
|
674
765
|
end
|
675
766
|
|
676
767
|
##
|
677
|
-
#
|
768
|
+
# The set of scheduled jobs within Sidekiq.
|
678
769
|
# Based on this, you can search/filter for jobs. Here's an
|
679
|
-
# example where I'm selecting
|
680
|
-
# and deleting them from the
|
770
|
+
# example where I'm selecting jobs based on some complex logic
|
771
|
+
# and deleting them from the scheduled set.
|
772
|
+
#
|
773
|
+
# See the API wiki page for usage notes and examples.
|
681
774
|
#
|
682
|
-
# r = Sidekiq::ScheduledSet.new
|
683
|
-
# r.select do |scheduled|
|
684
|
-
# scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
|
685
|
-
# scheduled.args[0] == 'User' &&
|
686
|
-
# scheduled.args[1] == 'setup_new_subscriber'
|
687
|
-
# end.map(&:delete)
|
688
775
|
class ScheduledSet < JobSet
|
689
776
|
def initialize
|
690
777
|
super "schedule"
|
@@ -692,46 +779,48 @@ module Sidekiq
|
|
692
779
|
end
|
693
780
|
|
694
781
|
##
|
695
|
-
#
|
782
|
+
# The set of retries within Sidekiq.
|
696
783
|
# Based on this, you can search/filter for jobs. Here's an
|
697
784
|
# example where I'm selecting all jobs of a certain type
|
698
785
|
# and deleting them from the retry queue.
|
699
786
|
#
|
700
|
-
#
|
701
|
-
#
|
702
|
-
# retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
|
703
|
-
# retri.args[0] == 'User' &&
|
704
|
-
# retri.args[1] == 'setup_new_subscriber'
|
705
|
-
# end.map(&:delete)
|
787
|
+
# See the API wiki page for usage notes and examples.
|
788
|
+
#
|
706
789
|
class RetrySet < JobSet
|
707
790
|
def initialize
|
708
791
|
super "retry"
|
709
792
|
end
|
710
793
|
|
794
|
+
# Enqueues all jobs pending within the retry set.
|
711
795
|
def retry_all
|
712
796
|
each(&:retry) while size > 0
|
713
797
|
end
|
714
798
|
|
799
|
+
# Kills all jobs pending within the retry set.
|
715
800
|
def kill_all
|
716
801
|
each(&:kill) while size > 0
|
717
802
|
end
|
718
803
|
end
|
719
804
|
|
720
805
|
##
|
721
|
-
#
|
806
|
+
# The set of dead jobs within Sidekiq. Dead jobs have failed all of
|
807
|
+
# their retries and are helding in this set pending some sort of manual
|
808
|
+
# fix. They will be removed after 6 months (dead_timeout) if not.
|
722
809
|
#
|
723
810
|
class DeadSet < JobSet
|
724
811
|
def initialize
|
725
812
|
super "dead"
|
726
813
|
end
|
727
814
|
|
815
|
+
# Add the given job to the Dead set.
|
816
|
+
# @param message [String] the job data as JSON
|
728
817
|
def kill(message, opts = {})
|
729
818
|
now = Time.now.to_f
|
730
819
|
Sidekiq.redis do |conn|
|
731
|
-
conn.multi do
|
732
|
-
|
733
|
-
|
734
|
-
|
820
|
+
conn.multi do |transaction|
|
821
|
+
transaction.zadd(name, now.to_s, message)
|
822
|
+
transaction.zremrangebyscore(name, "-inf", now - Sidekiq::Config::DEFAULTS[:dead_timeout_in_seconds])
|
823
|
+
transaction.zremrangebyrank(name, 0, - Sidekiq::Config::DEFAULTS[:dead_max_jobs])
|
735
824
|
end
|
736
825
|
end
|
737
826
|
|
@@ -739,24 +828,17 @@ module Sidekiq
|
|
739
828
|
job = Sidekiq.load_json(message)
|
740
829
|
r = RuntimeError.new("Job killed by API")
|
741
830
|
r.set_backtrace(caller)
|
742
|
-
Sidekiq.death_handlers.each do |handle|
|
831
|
+
Sidekiq.default_configuration.death_handlers.each do |handle|
|
743
832
|
handle.call(job, r)
|
744
833
|
end
|
745
834
|
end
|
746
835
|
true
|
747
836
|
end
|
748
837
|
|
838
|
+
# Enqueue all dead jobs
|
749
839
|
def retry_all
|
750
840
|
each(&:retry) while size > 0
|
751
841
|
end
|
752
|
-
|
753
|
-
def self.max_jobs
|
754
|
-
Sidekiq.options[:dead_max_jobs]
|
755
|
-
end
|
756
|
-
|
757
|
-
def self.timeout
|
758
|
-
Sidekiq.options[:dead_timeout_in_seconds]
|
759
|
-
end
|
760
842
|
end
|
761
843
|
|
762
844
|
##
|
@@ -764,24 +846,49 @@ module Sidekiq
|
|
764
846
|
# right now. Each process sends a heartbeat to Redis every 5 seconds
|
765
847
|
# so this set should be relatively accurate, barring network partitions.
|
766
848
|
#
|
767
|
-
#
|
849
|
+
# @yieldparam [Sidekiq::Process]
|
768
850
|
#
|
769
851
|
class ProcessSet
|
770
852
|
include Enumerable
|
771
853
|
|
854
|
+
def self.[](identity)
|
855
|
+
exists, (info, busy, beat, quiet, rss, rtt_us) = Sidekiq.redis { |conn|
|
856
|
+
conn.multi { |transaction|
|
857
|
+
transaction.sismember("processes", identity)
|
858
|
+
transaction.hmget(identity, "info", "busy", "beat", "quiet", "rss", "rtt_us")
|
859
|
+
}
|
860
|
+
}
|
861
|
+
|
862
|
+
return nil if exists == 0 || info.nil?
|
863
|
+
|
864
|
+
hash = Sidekiq.load_json(info)
|
865
|
+
Process.new(hash.merge("busy" => busy.to_i,
|
866
|
+
"beat" => beat.to_f,
|
867
|
+
"quiet" => quiet,
|
868
|
+
"rss" => rss.to_i,
|
869
|
+
"rtt_us" => rtt_us.to_i))
|
870
|
+
end
|
871
|
+
|
872
|
+
# :nodoc:
|
873
|
+
# @api private
|
772
874
|
def initialize(clean_plz = true)
|
773
875
|
cleanup if clean_plz
|
774
876
|
end
|
775
877
|
|
776
878
|
# Cleans up dead processes recorded in Redis.
|
777
879
|
# Returns the number of processes cleaned.
|
880
|
+
# :nodoc:
|
881
|
+
# @api private
|
778
882
|
def cleanup
|
883
|
+
# dont run cleanup more than once per minute
|
884
|
+
return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
|
885
|
+
|
779
886
|
count = 0
|
780
887
|
Sidekiq.redis do |conn|
|
781
|
-
procs = conn.
|
782
|
-
heartbeats = conn.pipelined {
|
888
|
+
procs = conn.sscan("processes").to_a
|
889
|
+
heartbeats = conn.pipelined { |pipeline|
|
783
890
|
procs.each do |key|
|
784
|
-
|
891
|
+
pipeline.hget(key, "info")
|
785
892
|
end
|
786
893
|
}
|
787
894
|
|
@@ -798,19 +905,19 @@ module Sidekiq
|
|
798
905
|
|
799
906
|
def each
|
800
907
|
result = Sidekiq.redis { |conn|
|
801
|
-
procs = conn.
|
908
|
+
procs = conn.sscan("processes").to_a.sort
|
802
909
|
|
803
910
|
# We're making a tradeoff here between consuming more memory instead of
|
804
911
|
# making more roundtrips to Redis, but if you have hundreds or thousands of workers,
|
805
912
|
# you'll be happier this way
|
806
|
-
conn.pipelined do
|
913
|
+
conn.pipelined do |pipeline|
|
807
914
|
procs.each do |key|
|
808
|
-
|
915
|
+
pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
|
809
916
|
end
|
810
917
|
end
|
811
918
|
}
|
812
919
|
|
813
|
-
result.each do |info, busy,
|
920
|
+
result.each do |info, busy, beat, quiet, rss, rtt_us|
|
814
921
|
# If a process is stopped between when we query Redis for `procs` and
|
815
922
|
# when we query for `result`, we will have an item in `result` that is
|
816
923
|
# composed of `nil` values.
|
@@ -818,10 +925,10 @@ module Sidekiq
|
|
818
925
|
|
819
926
|
hash = Sidekiq.load_json(info)
|
820
927
|
yield Process.new(hash.merge("busy" => busy.to_i,
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
928
|
+
"beat" => beat.to_f,
|
929
|
+
"quiet" => quiet,
|
930
|
+
"rss" => rss.to_i,
|
931
|
+
"rtt_us" => rtt_us.to_i))
|
825
932
|
end
|
826
933
|
end
|
827
934
|
|
@@ -829,6 +936,7 @@ module Sidekiq
|
|
829
936
|
# based on current heartbeat. #each does that and ensures the set only
|
830
937
|
# contains Sidekiq processes which have sent a heartbeat within the last
|
831
938
|
# 60 seconds.
|
939
|
+
# @return [Integer] current number of registered Sidekiq processes
|
832
940
|
def size
|
833
941
|
Sidekiq.redis { |conn| conn.scard("processes") }
|
834
942
|
end
|
@@ -836,10 +944,12 @@ module Sidekiq
|
|
836
944
|
# Total number of threads available to execute jobs.
|
837
945
|
# For Sidekiq Enterprise customers this number (in production) must be
|
838
946
|
# less than or equal to your licensed concurrency.
|
947
|
+
# @return [Integer] the sum of process concurrency
|
839
948
|
def total_concurrency
|
840
949
|
sum { |x| x["concurrency"].to_i }
|
841
950
|
end
|
842
951
|
|
952
|
+
# @return [Integer] total amount of RSS memory consumed by Sidekiq processes
|
843
953
|
def total_rss_in_kb
|
844
954
|
sum { |x| x["rss"].to_i }
|
845
955
|
end
|
@@ -848,6 +958,8 @@ module Sidekiq
|
|
848
958
|
# Returns the identity of the current cluster leader or "" if no leader.
|
849
959
|
# This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
|
850
960
|
# or Sidekiq Pro.
|
961
|
+
# @return [String] Identity of cluster leader
|
962
|
+
# @return [String] empty string if no leader
|
851
963
|
def leader
|
852
964
|
@leader ||= begin
|
853
965
|
x = Sidekiq.redis { |c| c.get("dear-leader") }
|
@@ -872,8 +984,11 @@ module Sidekiq
|
|
872
984
|
# 'busy' => 10,
|
873
985
|
# 'beat' => <last heartbeat>,
|
874
986
|
# 'identity' => <unique string identifying the process>,
|
987
|
+
# 'embedded' => true,
|
875
988
|
# }
|
876
989
|
class Process
|
990
|
+
# :nodoc:
|
991
|
+
# @api private
|
877
992
|
def initialize(hash)
|
878
993
|
@attribs = hash
|
879
994
|
end
|
@@ -883,7 +998,7 @@ module Sidekiq
|
|
883
998
|
end
|
884
999
|
|
885
1000
|
def labels
|
886
|
-
|
1001
|
+
self["labels"].to_a
|
887
1002
|
end
|
888
1003
|
|
889
1004
|
def [](key)
|
@@ -898,18 +1013,47 @@ module Sidekiq
|
|
898
1013
|
self["queues"]
|
899
1014
|
end
|
900
1015
|
|
1016
|
+
def weights
|
1017
|
+
self["weights"]
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
def version
|
1021
|
+
self["version"]
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
def embedded?
|
1025
|
+
self["embedded"]
|
1026
|
+
end
|
1027
|
+
|
1028
|
+
# Signal this process to stop processing new jobs.
|
1029
|
+
# It will continue to execute jobs it has already fetched.
|
1030
|
+
# This method is *asynchronous* and it can take 5-10
|
1031
|
+
# seconds for the process to quiet.
|
901
1032
|
def quiet!
|
1033
|
+
raise "Can't quiet an embedded process" if embedded?
|
1034
|
+
|
902
1035
|
signal("TSTP")
|
903
1036
|
end
|
904
1037
|
|
1038
|
+
# Signal this process to shutdown.
|
1039
|
+
# It will shutdown within its configured :timeout value, default 25 seconds.
|
1040
|
+
# This method is *asynchronous* and it can take 5-10
|
1041
|
+
# seconds for the process to start shutting down.
|
905
1042
|
def stop!
|
1043
|
+
raise "Can't stop an embedded process" if embedded?
|
1044
|
+
|
906
1045
|
signal("TERM")
|
907
1046
|
end
|
908
1047
|
|
1048
|
+
# Signal this process to log backtraces for all threads.
|
1049
|
+
# Useful if you have a frozen or deadlocked process which is
|
1050
|
+
# still sending a heartbeat.
|
1051
|
+
# This method is *asynchronous* and it can take 5-10 seconds.
|
909
1052
|
def dump_threads
|
910
1053
|
signal("TTIN")
|
911
1054
|
end
|
912
1055
|
|
1056
|
+
# @return [Boolean] true if this process is quiet or shutting down
|
913
1057
|
def stopping?
|
914
1058
|
self["quiet"] == "true"
|
915
1059
|
end
|
@@ -919,9 +1063,9 @@ module Sidekiq
|
|
919
1063
|
def signal(sig)
|
920
1064
|
key = "#{identity}-signals"
|
921
1065
|
Sidekiq.redis do |c|
|
922
|
-
c.multi do
|
923
|
-
|
924
|
-
|
1066
|
+
c.multi do |transaction|
|
1067
|
+
transaction.lpush(key, sig)
|
1068
|
+
transaction.expire(key, 60)
|
925
1069
|
end
|
926
1070
|
end
|
927
1071
|
end
|
@@ -952,24 +1096,24 @@ module Sidekiq
|
|
952
1096
|
|
953
1097
|
def each(&block)
|
954
1098
|
results = []
|
1099
|
+
procs = nil
|
1100
|
+
all_works = nil
|
1101
|
+
|
955
1102
|
Sidekiq.redis do |conn|
|
956
|
-
procs = conn.
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
conn.hgetall("#{key}:workers")
|
961
|
-
}
|
962
|
-
next unless valid
|
963
|
-
workers.each_pair do |tid, json|
|
964
|
-
hsh = Sidekiq.load_json(json)
|
965
|
-
p = hsh["payload"]
|
966
|
-
# avoid breaking API, this is a side effect of the JSON optimization in #4316
|
967
|
-
hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
|
968
|
-
results << [key, tid, hsh]
|
1103
|
+
procs = conn.sscan("processes").to_a.sort
|
1104
|
+
all_works = conn.pipelined do |pipeline|
|
1105
|
+
procs.each do |key|
|
1106
|
+
pipeline.hgetall("#{key}:work")
|
969
1107
|
end
|
970
1108
|
end
|
971
1109
|
end
|
972
1110
|
|
1111
|
+
procs.zip(all_works).each do |key, workers|
|
1112
|
+
workers.each_pair do |tid, json|
|
1113
|
+
results << [key, tid, Sidekiq.load_json(json)] unless json.empty?
|
1114
|
+
end
|
1115
|
+
end
|
1116
|
+
|
973
1117
|
results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
|
974
1118
|
end
|
975
1119
|
|
@@ -981,13 +1125,13 @@ module Sidekiq
|
|
981
1125
|
# which can easily get out of sync with crashy processes.
|
982
1126
|
def size
|
983
1127
|
Sidekiq.redis do |conn|
|
984
|
-
procs = conn.
|
1128
|
+
procs = conn.sscan("processes").to_a
|
985
1129
|
if procs.empty?
|
986
1130
|
0
|
987
1131
|
else
|
988
|
-
conn.pipelined {
|
1132
|
+
conn.pipelined { |pipeline|
|
989
1133
|
procs.each do |key|
|
990
|
-
|
1134
|
+
pipeline.hget(key, "busy")
|
991
1135
|
end
|
992
1136
|
}.sum(&:to_i)
|
993
1137
|
end
|