sidekiq 5.2.1 → 6.4.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 +5 -5
- data/Changes.md +368 -1
- data/LICENSE +3 -3
- data/README.md +21 -37
- data/bin/sidekiq +26 -2
- data/bin/sidekiqload +33 -25
- data/bin/sidekiqmon +8 -0
- 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 +316 -246
- data/lib/sidekiq/cli.rb +195 -221
- data/lib/sidekiq/client.rb +42 -60
- data/lib/sidekiq/delay.rb +7 -6
- data/lib/sidekiq/exception_handler.rb +10 -12
- data/lib/sidekiq/extensions/action_mailer.rb +15 -24
- data/lib/sidekiq/extensions/active_record.rb +15 -12
- data/lib/sidekiq/extensions/class_methods.rb +16 -13
- data/lib/sidekiq/extensions/generic_proxy.rb +8 -6
- data/lib/sidekiq/fetch.rb +39 -31
- data/lib/sidekiq/job.rb +13 -0
- data/lib/sidekiq/job_logger.rb +47 -9
- data/lib/sidekiq/job_retry.rb +88 -68
- data/lib/sidekiq/job_util.rb +65 -0
- data/lib/sidekiq/launcher.rb +151 -61
- data/lib/sidekiq/logger.rb +166 -0
- data/lib/sidekiq/manager.rb +18 -22
- data/lib/sidekiq/middleware/chain.rb +20 -8
- data/lib/sidekiq/middleware/current_attributes.rb +57 -0
- data/lib/sidekiq/middleware/i18n.rb +5 -7
- data/lib/sidekiq/monitor.rb +133 -0
- data/lib/sidekiq/paginator.rb +18 -14
- data/lib/sidekiq/processor.rb +116 -82
- data/lib/sidekiq/rails.rb +42 -38
- data/lib/sidekiq/redis_connection.rb +49 -30
- data/lib/sidekiq/scheduled.rb +62 -28
- data/lib/sidekiq/sd_notify.rb +149 -0
- data/lib/sidekiq/systemd.rb +24 -0
- data/lib/sidekiq/testing/inline.rb +2 -1
- data/lib/sidekiq/testing.rb +36 -27
- data/lib/sidekiq/util.rb +57 -15
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web/action.rb +15 -11
- data/lib/sidekiq/web/application.rb +95 -76
- data/lib/sidekiq/web/csrf_protection.rb +180 -0
- data/lib/sidekiq/web/helpers.rb +115 -91
- data/lib/sidekiq/web/router.rb +23 -19
- data/lib/sidekiq/web.rb +61 -105
- data/lib/sidekiq/worker.rb +259 -99
- data/lib/sidekiq.rb +79 -45
- data/sidekiq.gemspec +23 -18
- data/web/assets/images/apple-touch-icon.png +0 -0
- data/web/assets/javascripts/application.js +83 -64
- data/web/assets/javascripts/dashboard.js +66 -75
- data/web/assets/stylesheets/application-dark.css +143 -0
- data/web/assets/stylesheets/application-rtl.css +0 -4
- data/web/assets/stylesheets/application.css +75 -231
- data/web/assets/stylesheets/bootstrap.css +1 -1
- data/web/locales/ar.yml +9 -2
- data/web/locales/de.yml +14 -2
- data/web/locales/en.yml +7 -1
- data/web/locales/es.yml +18 -2
- data/web/locales/fr.yml +10 -3
- data/web/locales/ja.yml +7 -1
- data/web/locales/lt.yml +83 -0
- data/web/locales/pl.yml +4 -4
- data/web/locales/ru.yml +4 -0
- data/web/locales/vi.yml +83 -0
- data/web/views/_footer.erb +1 -1
- data/web/views/_job_info.erb +3 -2
- data/web/views/_nav.erb +3 -17
- data/web/views/_poll_link.erb +2 -5
- data/web/views/_summary.erb +7 -7
- data/web/views/busy.erb +54 -20
- data/web/views/dashboard.erb +22 -14
- data/web/views/dead.erb +3 -3
- data/web/views/layout.erb +3 -1
- data/web/views/morgue.erb +9 -6
- data/web/views/queue.erb +20 -10
- data/web/views/queues.erb +11 -3
- data/web/views/retries.erb +14 -7
- data/web/views/retry.erb +3 -3
- data/web/views/scheduled.erb +5 -2
- metadata +39 -54
- data/.github/contributing.md +0 -32
- data/.github/issue_template.md +0 -11
- data/.gitignore +0 -13
- data/.travis.yml +0 -14
- data/3.0-Upgrade.md +0 -70
- data/4.0-Upgrade.md +0 -53
- data/5.0-Upgrade.md +0 -56
- data/COMM-LICENSE +0 -95
- data/Ent-Changes.md +0 -221
- data/Gemfile +0 -14
- data/Pro-2.0-Upgrade.md +0 -138
- data/Pro-3.0-Upgrade.md +0 -44
- data/Pro-4.0-Upgrade.md +0 -35
- data/Pro-Changes.md +0 -739
- data/Rakefile +0 -8
- data/bin/sidekiqctl +0 -99
- data/code_of_conduct.md +0 -50
- data/lib/generators/sidekiq/worker_generator.rb +0 -49
- data/lib/sidekiq/core_ext.rb +0 -1
- data/lib/sidekiq/logging.rb +0 -122
- data/lib/sidekiq/middleware/server/active_record.rb +0 -23
data/lib/sidekiq/api.rb
CHANGED
@@ -1,26 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require 'sidekiq'
|
3
2
|
|
4
|
-
|
3
|
+
require "sidekiq"
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
cursor = '0'
|
9
|
-
result = []
|
10
|
-
loop do
|
11
|
-
cursor, values = conn.sscan(key, cursor)
|
12
|
-
result.push(*values)
|
13
|
-
break if cursor == '0'
|
14
|
-
end
|
15
|
-
result
|
16
|
-
end
|
17
|
-
end
|
5
|
+
require "zlib"
|
6
|
+
require "base64"
|
18
7
|
|
8
|
+
module Sidekiq
|
19
9
|
class Stats
|
20
|
-
include RedisScanner
|
21
|
-
|
22
10
|
def initialize
|
23
|
-
|
11
|
+
fetch_stats_fast!
|
24
12
|
end
|
25
13
|
|
26
14
|
def processed
|
@@ -63,62 +51,78 @@ module Sidekiq
|
|
63
51
|
Sidekiq::Stats::Queues.new.lengths
|
64
52
|
end
|
65
53
|
|
66
|
-
|
67
|
-
|
54
|
+
# O(1) redis calls
|
55
|
+
def fetch_stats_fast!
|
56
|
+
pipe1_res = Sidekiq.redis { |conn|
|
68
57
|
conn.pipelined do
|
69
|
-
conn.get(
|
70
|
-
conn.get(
|
71
|
-
conn.zcard(
|
72
|
-
conn.zcard(
|
73
|
-
conn.zcard(
|
74
|
-
conn.scard(
|
75
|
-
conn.lrange(
|
58
|
+
conn.get("stat:processed")
|
59
|
+
conn.get("stat:failed")
|
60
|
+
conn.zcard("schedule")
|
61
|
+
conn.zcard("retry")
|
62
|
+
conn.zcard("dead")
|
63
|
+
conn.scard("processes")
|
64
|
+
conn.lrange("queue:default", -1, -1)
|
76
65
|
end
|
77
|
-
|
66
|
+
}
|
78
67
|
|
79
|
-
|
80
|
-
|
68
|
+
default_queue_latency = if (entry = pipe1_res[6].first)
|
69
|
+
job = begin
|
70
|
+
Sidekiq.load_json(entry)
|
71
|
+
rescue
|
72
|
+
{}
|
73
|
+
end
|
74
|
+
now = Time.now.to_f
|
75
|
+
thence = job["enqueued_at"] || now
|
76
|
+
now - thence
|
77
|
+
else
|
78
|
+
0
|
81
79
|
end
|
82
80
|
|
83
|
-
|
84
|
-
|
85
|
-
|
81
|
+
@stats = {
|
82
|
+
processed: pipe1_res[0].to_i,
|
83
|
+
failed: pipe1_res[1].to_i,
|
84
|
+
scheduled_size: pipe1_res[2],
|
85
|
+
retry_size: pipe1_res[3],
|
86
|
+
dead_size: pipe1_res[4],
|
87
|
+
processes_size: pipe1_res[5],
|
88
|
+
|
89
|
+
default_queue_latency: default_queue_latency
|
90
|
+
}
|
91
|
+
end
|
86
92
|
|
87
|
-
|
93
|
+
# O(number of processes + number of queues) redis calls
|
94
|
+
def fetch_stats_slow!
|
95
|
+
processes = Sidekiq.redis { |conn|
|
96
|
+
conn.sscan_each("processes").to_a
|
97
|
+
}
|
98
|
+
|
99
|
+
queues = Sidekiq.redis { |conn|
|
100
|
+
conn.sscan_each("queues").to_a
|
101
|
+
}
|
102
|
+
|
103
|
+
pipe2_res = Sidekiq.redis { |conn|
|
88
104
|
conn.pipelined do
|
89
|
-
processes.each {|key| conn.hget(key,
|
90
|
-
queues.each {|queue| conn.llen("queue:#{queue}") }
|
105
|
+
processes.each { |key| conn.hget(key, "busy") }
|
106
|
+
queues.each { |queue| conn.llen("queue:#{queue}") }
|
91
107
|
end
|
92
|
-
|
108
|
+
}
|
93
109
|
|
94
110
|
s = processes.size
|
95
|
-
workers_size = pipe2_res[0...s].
|
96
|
-
enqueued
|
111
|
+
workers_size = pipe2_res[0...s].sum(&:to_i)
|
112
|
+
enqueued = pipe2_res[s..-1].sum(&:to_i)
|
97
113
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
@stats = {
|
107
|
-
processed: pipe1_res[0].to_i,
|
108
|
-
failed: pipe1_res[1].to_i,
|
109
|
-
scheduled_size: pipe1_res[2],
|
110
|
-
retry_size: pipe1_res[3],
|
111
|
-
dead_size: pipe1_res[4],
|
112
|
-
processes_size: pipe1_res[5],
|
113
|
-
|
114
|
-
default_queue_latency: default_queue_latency,
|
115
|
-
workers_size: workers_size,
|
116
|
-
enqueued: enqueued
|
117
|
-
}
|
114
|
+
@stats[:workers_size] = workers_size
|
115
|
+
@stats[:enqueued] = enqueued
|
116
|
+
@stats
|
117
|
+
end
|
118
|
+
|
119
|
+
def fetch_stats!
|
120
|
+
fetch_stats_fast!
|
121
|
+
fetch_stats_slow!
|
118
122
|
end
|
119
123
|
|
120
124
|
def reset(*stats)
|
121
|
-
all
|
125
|
+
all = %w[failed processed]
|
122
126
|
stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
|
123
127
|
|
124
128
|
mset_args = []
|
@@ -134,36 +138,31 @@ module Sidekiq
|
|
134
138
|
private
|
135
139
|
|
136
140
|
def stat(s)
|
137
|
-
@stats[s]
|
141
|
+
fetch_stats_slow! if @stats[s].nil?
|
142
|
+
@stats[s] || raise(ArgumentError, "Unknown stat #{s}")
|
138
143
|
end
|
139
144
|
|
140
145
|
class Queues
|
141
|
-
include RedisScanner
|
142
|
-
|
143
146
|
def lengths
|
144
147
|
Sidekiq.redis do |conn|
|
145
|
-
queues =
|
148
|
+
queues = conn.sscan_each("queues").to_a
|
146
149
|
|
147
|
-
lengths = conn.pipelined
|
150
|
+
lengths = conn.pipelined {
|
148
151
|
queues.each do |queue|
|
149
152
|
conn.llen("queue:#{queue}")
|
150
153
|
end
|
151
|
-
|
152
|
-
|
153
|
-
i = 0
|
154
|
-
array_of_arrays = queues.inject({}) do |memo, queue|
|
155
|
-
memo[queue] = lengths[i]
|
156
|
-
i += 1
|
157
|
-
memo
|
158
|
-
end.sort_by { |_, size| size }
|
154
|
+
}
|
159
155
|
|
160
|
-
|
156
|
+
array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
|
157
|
+
array_of_arrays.to_h
|
161
158
|
end
|
162
159
|
end
|
163
160
|
end
|
164
161
|
|
165
162
|
class History
|
166
163
|
def initialize(days_previous, start_date = nil)
|
164
|
+
# we only store five years of data in Redis
|
165
|
+
raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
|
167
166
|
@days_previous = days_previous
|
168
167
|
@start_date = start_date || Time.now.utc.to_date
|
169
168
|
end
|
@@ -179,18 +178,12 @@ module Sidekiq
|
|
179
178
|
private
|
180
179
|
|
181
180
|
def date_stat_hash(stat)
|
182
|
-
i = 0
|
183
181
|
stat_hash = {}
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
datestr = date.strftime("%Y-%m-%d")
|
190
|
-
keys << "stat:#{stat}:#{datestr}"
|
191
|
-
dates << datestr
|
192
|
-
i += 1
|
193
|
-
end
|
182
|
+
dates = @start_date.downto(@start_date - @days_previous + 1).map { |date|
|
183
|
+
date.strftime("%Y-%m-%d")
|
184
|
+
}
|
185
|
+
|
186
|
+
keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
|
194
187
|
|
195
188
|
begin
|
196
189
|
Sidekiq.redis do |conn|
|
@@ -222,18 +215,17 @@ module Sidekiq
|
|
222
215
|
#
|
223
216
|
class Queue
|
224
217
|
include Enumerable
|
225
|
-
extend RedisScanner
|
226
218
|
|
227
219
|
##
|
228
220
|
# Return all known queues within Redis.
|
229
221
|
#
|
230
222
|
def self.all
|
231
|
-
Sidekiq.redis { |c|
|
223
|
+
Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
|
232
224
|
end
|
233
225
|
|
234
226
|
attr_reader :name
|
235
227
|
|
236
|
-
def initialize(name="default")
|
228
|
+
def initialize(name = "default")
|
237
229
|
@name = name.to_s
|
238
230
|
@rname = "queue:#{name}"
|
239
231
|
end
|
@@ -253,13 +245,13 @@ module Sidekiq
|
|
253
245
|
#
|
254
246
|
# @return Float
|
255
247
|
def latency
|
256
|
-
entry = Sidekiq.redis
|
248
|
+
entry = Sidekiq.redis { |conn|
|
257
249
|
conn.lrange(@rname, -1, -1)
|
258
|
-
|
250
|
+
}.first
|
259
251
|
return 0 unless entry
|
260
252
|
job = Sidekiq.load_json(entry)
|
261
253
|
now = Time.now.to_f
|
262
|
-
thence = job[
|
254
|
+
thence = job["enqueued_at"] || now
|
263
255
|
now - thence
|
264
256
|
end
|
265
257
|
|
@@ -269,16 +261,16 @@ module Sidekiq
|
|
269
261
|
page = 0
|
270
262
|
page_size = 50
|
271
263
|
|
272
|
-
|
264
|
+
loop do
|
273
265
|
range_start = page * page_size - deleted_size
|
274
|
-
range_end
|
275
|
-
entries = Sidekiq.redis
|
266
|
+
range_end = range_start + page_size - 1
|
267
|
+
entries = Sidekiq.redis { |conn|
|
276
268
|
conn.lrange @rname, range_start, range_end
|
277
|
-
|
269
|
+
}
|
278
270
|
break if entries.empty?
|
279
271
|
page += 1
|
280
272
|
entries.each do |entry|
|
281
|
-
yield
|
273
|
+
yield JobRecord.new(entry, @name)
|
282
274
|
end
|
283
275
|
deleted_size = initial_size - size
|
284
276
|
end
|
@@ -288,7 +280,7 @@ module Sidekiq
|
|
288
280
|
# Find the job with the given JID within this queue.
|
289
281
|
#
|
290
282
|
# This is a slow, inefficient operation. Do not use under
|
291
|
-
# normal conditions.
|
283
|
+
# normal conditions.
|
292
284
|
def find_job(jid)
|
293
285
|
detect { |j| j.jid == jid }
|
294
286
|
end
|
@@ -296,7 +288,7 @@ module Sidekiq
|
|
296
288
|
def clear
|
297
289
|
Sidekiq.redis do |conn|
|
298
290
|
conn.multi do
|
299
|
-
conn.
|
291
|
+
conn.unlink(@rname)
|
300
292
|
conn.srem("queues", name)
|
301
293
|
end
|
302
294
|
end
|
@@ -309,17 +301,17 @@ module Sidekiq
|
|
309
301
|
# sorted set.
|
310
302
|
#
|
311
303
|
# The job should be considered immutable but may be
|
312
|
-
# removed from the queue via
|
304
|
+
# removed from the queue via JobRecord#delete.
|
313
305
|
#
|
314
|
-
class
|
306
|
+
class JobRecord
|
315
307
|
attr_reader :item
|
316
308
|
attr_reader :value
|
317
309
|
|
318
|
-
def initialize(item, queue_name=nil)
|
310
|
+
def initialize(item, queue_name = nil)
|
319
311
|
@args = nil
|
320
312
|
@value = item
|
321
313
|
@item = item.is_a?(Hash) ? item : parse(item)
|
322
|
-
@queue = queue_name || @item[
|
314
|
+
@queue = queue_name || @item["queue"]
|
323
315
|
end
|
324
316
|
|
325
317
|
def parse(item)
|
@@ -334,27 +326,29 @@ module Sidekiq
|
|
334
326
|
end
|
335
327
|
|
336
328
|
def klass
|
337
|
-
self[
|
329
|
+
self["class"]
|
338
330
|
end
|
339
331
|
|
340
332
|
def display_class
|
341
333
|
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
342
|
-
@klass ||=
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
334
|
+
@klass ||= self["display_class"] || begin
|
335
|
+
case klass
|
336
|
+
when /\ASidekiq::Extensions::Delayed/
|
337
|
+
safe_load(args[0], klass) do |target, method, _|
|
338
|
+
"#{target}.#{method}"
|
339
|
+
end
|
340
|
+
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
341
|
+
job_class = @item["wrapped"] || args[0]
|
342
|
+
if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
|
343
|
+
# MailerClass#mailer_method
|
344
|
+
args[0]["arguments"][0..1].join("#")
|
345
|
+
else
|
346
|
+
job_class
|
347
|
+
end
|
348
|
+
else
|
349
|
+
klass
|
350
|
+
end
|
351
|
+
end
|
358
352
|
end
|
359
353
|
|
360
354
|
def display_args
|
@@ -365,53 +359,68 @@ module Sidekiq
|
|
365
359
|
arg
|
366
360
|
end
|
367
361
|
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
368
|
-
job_args = self[
|
369
|
-
if
|
362
|
+
job_args = self["wrapped"] ? args[0]["arguments"] : []
|
363
|
+
if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
|
370
364
|
# remove MailerClass, mailer_method and 'deliver_now'
|
371
365
|
job_args.drop(3)
|
366
|
+
elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
|
367
|
+
# remove MailerClass, mailer_method and 'deliver_now'
|
368
|
+
job_args.drop(3).first["args"]
|
372
369
|
else
|
373
370
|
job_args
|
374
371
|
end
|
375
372
|
else
|
376
|
-
if self[
|
373
|
+
if self["encrypt"]
|
377
374
|
# no point in showing 150+ bytes of random garbage
|
378
|
-
args[-1] =
|
375
|
+
args[-1] = "[encrypted data]"
|
379
376
|
end
|
380
377
|
args
|
381
|
-
|
378
|
+
end
|
382
379
|
end
|
383
380
|
|
384
381
|
def args
|
385
|
-
@args || @item[
|
382
|
+
@args || @item["args"]
|
386
383
|
end
|
387
384
|
|
388
385
|
def jid
|
389
|
-
self[
|
386
|
+
self["jid"]
|
390
387
|
end
|
391
388
|
|
392
389
|
def enqueued_at
|
393
|
-
self[
|
390
|
+
self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
|
394
391
|
end
|
395
392
|
|
396
393
|
def created_at
|
397
|
-
Time.at(self[
|
394
|
+
Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
|
398
395
|
end
|
399
396
|
|
400
|
-
def
|
401
|
-
|
397
|
+
def tags
|
398
|
+
self["tags"] || []
|
402
399
|
end
|
403
400
|
|
401
|
+
def error_backtrace
|
402
|
+
# Cache nil values
|
403
|
+
if defined?(@error_backtrace)
|
404
|
+
@error_backtrace
|
405
|
+
else
|
406
|
+
value = self["error_backtrace"]
|
407
|
+
@error_backtrace = value && uncompress_backtrace(value)
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
attr_reader :queue
|
412
|
+
|
404
413
|
def latency
|
405
414
|
now = Time.now.to_f
|
406
|
-
now - (@item[
|
415
|
+
now - (@item["enqueued_at"] || @item["created_at"] || now)
|
407
416
|
end
|
408
417
|
|
409
418
|
##
|
410
419
|
# Remove this job from the queue.
|
411
420
|
def delete
|
412
|
-
count = Sidekiq.redis
|
421
|
+
count = Sidekiq.redis { |conn|
|
413
422
|
conn.lrem("queue:#{@queue}", 1, @value)
|
414
|
-
|
423
|
+
}
|
415
424
|
count != 0
|
416
425
|
end
|
417
426
|
|
@@ -425,18 +434,33 @@ module Sidekiq
|
|
425
434
|
private
|
426
435
|
|
427
436
|
def safe_load(content, default)
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
437
|
+
yield(*YAML.load(content))
|
438
|
+
rescue => ex
|
439
|
+
# #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
|
440
|
+
# memory yet so the YAML can't be loaded.
|
441
|
+
Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
|
442
|
+
default
|
443
|
+
end
|
444
|
+
|
445
|
+
def uncompress_backtrace(backtrace)
|
446
|
+
if backtrace.is_a?(Array)
|
447
|
+
# Handle old jobs with raw Array backtrace format
|
448
|
+
backtrace
|
449
|
+
else
|
450
|
+
decoded = Base64.decode64(backtrace)
|
451
|
+
uncompressed = Zlib::Inflate.inflate(decoded)
|
452
|
+
begin
|
453
|
+
Sidekiq.load_json(uncompressed)
|
454
|
+
rescue
|
455
|
+
# Handle old jobs with marshalled backtrace format
|
456
|
+
# TODO Remove in 7.x
|
457
|
+
Marshal.load(uncompressed)
|
458
|
+
end
|
435
459
|
end
|
436
460
|
end
|
437
461
|
end
|
438
462
|
|
439
|
-
class SortedEntry <
|
463
|
+
class SortedEntry < JobRecord
|
440
464
|
attr_reader :score
|
441
465
|
attr_reader :parent
|
442
466
|
|
@@ -459,8 +483,9 @@ module Sidekiq
|
|
459
483
|
end
|
460
484
|
|
461
485
|
def reschedule(at)
|
462
|
-
|
463
|
-
|
486
|
+
Sidekiq.redis do |conn|
|
487
|
+
conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
|
488
|
+
end
|
464
489
|
end
|
465
490
|
|
466
491
|
def add_to_queue
|
@@ -473,7 +498,7 @@ module Sidekiq
|
|
473
498
|
def retry
|
474
499
|
remove_job do |message|
|
475
500
|
msg = Sidekiq.load_json(message)
|
476
|
-
msg[
|
501
|
+
msg["retry_count"] -= 1 if msg["retry_count"]
|
477
502
|
Sidekiq::Client.push(msg)
|
478
503
|
end
|
479
504
|
end
|
@@ -487,45 +512,44 @@ module Sidekiq
|
|
487
512
|
end
|
488
513
|
|
489
514
|
def error?
|
490
|
-
!!item[
|
515
|
+
!!item["error_class"]
|
491
516
|
end
|
492
517
|
|
493
518
|
private
|
494
519
|
|
495
520
|
def remove_job
|
496
521
|
Sidekiq.redis do |conn|
|
497
|
-
results = conn.multi
|
522
|
+
results = conn.multi {
|
498
523
|
conn.zrangebyscore(parent.name, score, score)
|
499
524
|
conn.zremrangebyscore(parent.name, score, score)
|
500
|
-
|
525
|
+
}.first
|
501
526
|
|
502
527
|
if results.size == 1
|
503
528
|
yield results.first
|
504
529
|
else
|
505
530
|
# multiple jobs with the same score
|
506
531
|
# find the one with the right JID and push it
|
507
|
-
|
532
|
+
matched, nonmatched = results.partition { |message|
|
508
533
|
if message.index(jid)
|
509
534
|
msg = Sidekiq.load_json(message)
|
510
|
-
msg[
|
535
|
+
msg["jid"] == jid
|
511
536
|
else
|
512
537
|
false
|
513
538
|
end
|
514
|
-
|
539
|
+
}
|
515
540
|
|
516
|
-
msg =
|
541
|
+
msg = matched.first
|
517
542
|
yield msg if msg
|
518
543
|
|
519
544
|
# push the rest back onto the sorted set
|
520
545
|
conn.multi do
|
521
|
-
|
546
|
+
nonmatched.each do |message|
|
522
547
|
conn.zadd(parent.name, score.to_f.to_s, message)
|
523
548
|
end
|
524
549
|
end
|
525
550
|
end
|
526
551
|
end
|
527
552
|
end
|
528
|
-
|
529
553
|
end
|
530
554
|
|
531
555
|
class SortedSet
|
@@ -542,16 +566,26 @@ module Sidekiq
|
|
542
566
|
Sidekiq.redis { |c| c.zcard(name) }
|
543
567
|
end
|
544
568
|
|
569
|
+
def scan(match, count = 100)
|
570
|
+
return to_enum(:scan, match, count) unless block_given?
|
571
|
+
|
572
|
+
match = "*#{match}*" unless match.include?("*")
|
573
|
+
Sidekiq.redis do |conn|
|
574
|
+
conn.zscan_each(name, match: match, count: count) do |entry, score|
|
575
|
+
yield SortedEntry.new(self, score, entry)
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
545
580
|
def clear
|
546
581
|
Sidekiq.redis do |conn|
|
547
|
-
conn.
|
582
|
+
conn.unlink(name)
|
548
583
|
end
|
549
584
|
end
|
550
585
|
alias_method :💣, :clear
|
551
586
|
end
|
552
587
|
|
553
588
|
class JobSet < SortedSet
|
554
|
-
|
555
589
|
def schedule(timestamp, message)
|
556
590
|
Sidekiq.redis do |conn|
|
557
591
|
conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
|
@@ -564,44 +598,55 @@ module Sidekiq
|
|
564
598
|
page = -1
|
565
599
|
page_size = 50
|
566
600
|
|
567
|
-
|
601
|
+
loop do
|
568
602
|
range_start = page * page_size + offset_size
|
569
|
-
range_end
|
570
|
-
elements = Sidekiq.redis
|
603
|
+
range_end = range_start + page_size - 1
|
604
|
+
elements = Sidekiq.redis { |conn|
|
571
605
|
conn.zrange name, range_start, range_end, with_scores: true
|
572
|
-
|
606
|
+
}
|
573
607
|
break if elements.empty?
|
574
608
|
page -= 1
|
575
|
-
elements.
|
609
|
+
elements.reverse_each do |element, score|
|
576
610
|
yield SortedEntry.new(self, score, element)
|
577
611
|
end
|
578
612
|
offset_size = initial_size - @_size
|
579
613
|
end
|
580
614
|
end
|
581
615
|
|
616
|
+
##
|
617
|
+
# Fetch jobs that match a given time or Range. Job ID is an
|
618
|
+
# optional second argument.
|
582
619
|
def fetch(score, jid = nil)
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
elements.inject([]) do |result, element|
|
588
|
-
entry = SortedEntry.new(self, score, element)
|
589
|
-
if jid
|
590
|
-
result << entry if entry.jid == jid
|
620
|
+
begin_score, end_score =
|
621
|
+
if score.is_a?(Range)
|
622
|
+
[score.first, score.last]
|
591
623
|
else
|
592
|
-
|
624
|
+
[score, score]
|
593
625
|
end
|
594
|
-
|
626
|
+
|
627
|
+
elements = Sidekiq.redis { |conn|
|
628
|
+
conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
|
629
|
+
}
|
630
|
+
|
631
|
+
elements.each_with_object([]) do |element, result|
|
632
|
+
data, job_score = element
|
633
|
+
entry = SortedEntry.new(self, job_score, data)
|
634
|
+
result << entry if jid.nil? || entry.jid == jid
|
595
635
|
end
|
596
636
|
end
|
597
637
|
|
598
638
|
##
|
599
639
|
# Find the job with the given JID within this sorted set.
|
600
|
-
#
|
601
|
-
# This is a slow, inefficient operation. Do not use under
|
602
|
-
# normal conditions. Sidekiq Pro contains a faster version.
|
640
|
+
# This is a slower O(n) operation. Do not use for app logic.
|
603
641
|
def find_job(jid)
|
604
|
-
|
642
|
+
Sidekiq.redis do |conn|
|
643
|
+
conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
|
644
|
+
job = JSON.parse(entry)
|
645
|
+
matched = job["jid"] == jid
|
646
|
+
return SortedEntry.new(self, score, entry) if matched
|
647
|
+
end
|
648
|
+
end
|
649
|
+
nil
|
605
650
|
end
|
606
651
|
|
607
652
|
def delete_by_value(name, value)
|
@@ -616,13 +661,14 @@ module Sidekiq
|
|
616
661
|
Sidekiq.redis do |conn|
|
617
662
|
elements = conn.zrangebyscore(name, score, score)
|
618
663
|
elements.each do |element|
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
664
|
+
if element.index(jid)
|
665
|
+
message = Sidekiq.load_json(element)
|
666
|
+
if message["jid"] == jid
|
667
|
+
ret = conn.zrem(name, element)
|
668
|
+
@_size -= 1 if ret
|
669
|
+
break ret
|
670
|
+
end
|
624
671
|
end
|
625
|
-
false
|
626
672
|
end
|
627
673
|
end
|
628
674
|
end
|
@@ -644,7 +690,7 @@ module Sidekiq
|
|
644
690
|
# end.map(&:delete)
|
645
691
|
class ScheduledSet < JobSet
|
646
692
|
def initialize
|
647
|
-
super
|
693
|
+
super "schedule"
|
648
694
|
end
|
649
695
|
end
|
650
696
|
|
@@ -662,13 +708,15 @@ module Sidekiq
|
|
662
708
|
# end.map(&:delete)
|
663
709
|
class RetrySet < JobSet
|
664
710
|
def initialize
|
665
|
-
super
|
711
|
+
super "retry"
|
666
712
|
end
|
667
713
|
|
668
714
|
def retry_all
|
669
|
-
while size > 0
|
670
|
-
|
671
|
-
|
715
|
+
each(&:retry) while size > 0
|
716
|
+
end
|
717
|
+
|
718
|
+
def kill_all
|
719
|
+
each(&:kill) while size > 0
|
672
720
|
end
|
673
721
|
end
|
674
722
|
|
@@ -677,15 +725,15 @@ module Sidekiq
|
|
677
725
|
#
|
678
726
|
class DeadSet < JobSet
|
679
727
|
def initialize
|
680
|
-
super
|
728
|
+
super "dead"
|
681
729
|
end
|
682
730
|
|
683
|
-
def kill(message, opts={})
|
731
|
+
def kill(message, opts = {})
|
684
732
|
now = Time.now.to_f
|
685
733
|
Sidekiq.redis do |conn|
|
686
734
|
conn.multi do
|
687
735
|
conn.zadd(name, now.to_s, message)
|
688
|
-
conn.zremrangebyscore(name,
|
736
|
+
conn.zremrangebyscore(name, "-inf", now - self.class.timeout)
|
689
737
|
conn.zremrangebyrank(name, 0, - self.class.max_jobs)
|
690
738
|
end
|
691
739
|
end
|
@@ -702,9 +750,7 @@ module Sidekiq
|
|
702
750
|
end
|
703
751
|
|
704
752
|
def retry_all
|
705
|
-
while size > 0
|
706
|
-
each(&:retry)
|
707
|
-
end
|
753
|
+
each(&:retry) while size > 0
|
708
754
|
end
|
709
755
|
|
710
756
|
def self.max_jobs
|
@@ -718,16 +764,15 @@ module Sidekiq
|
|
718
764
|
|
719
765
|
##
|
720
766
|
# Enumerates the set of Sidekiq processes which are actively working
|
721
|
-
# right now. Each process
|
767
|
+
# right now. Each process sends a heartbeat to Redis every 5 seconds
|
722
768
|
# so this set should be relatively accurate, barring network partitions.
|
723
769
|
#
|
724
770
|
# Yields a Sidekiq::Process.
|
725
771
|
#
|
726
772
|
class ProcessSet
|
727
773
|
include Enumerable
|
728
|
-
include RedisScanner
|
729
774
|
|
730
|
-
def initialize(clean_plz=true)
|
775
|
+
def initialize(clean_plz = true)
|
731
776
|
cleanup if clean_plz
|
732
777
|
end
|
733
778
|
|
@@ -736,50 +781,51 @@ module Sidekiq
|
|
736
781
|
def cleanup
|
737
782
|
count = 0
|
738
783
|
Sidekiq.redis do |conn|
|
739
|
-
procs =
|
740
|
-
heartbeats = conn.pipelined
|
784
|
+
procs = conn.sscan_each("processes").to_a.sort
|
785
|
+
heartbeats = conn.pipelined {
|
741
786
|
procs.each do |key|
|
742
|
-
conn.hget(key,
|
787
|
+
conn.hget(key, "info")
|
743
788
|
end
|
744
|
-
|
789
|
+
}
|
745
790
|
|
746
791
|
# the hash named key has an expiry of 60 seconds.
|
747
792
|
# if it's not found, that means the process has not reported
|
748
793
|
# in to Redis and probably died.
|
749
|
-
to_prune =
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
count = conn.srem('processes', to_prune) unless to_prune.empty?
|
794
|
+
to_prune = procs.select.with_index { |proc, i|
|
795
|
+
heartbeats[i].nil?
|
796
|
+
}
|
797
|
+
count = conn.srem("processes", to_prune) unless to_prune.empty?
|
754
798
|
end
|
755
799
|
count
|
756
800
|
end
|
757
801
|
|
758
802
|
def each
|
759
|
-
|
803
|
+
result = Sidekiq.redis { |conn|
|
804
|
+
procs = conn.sscan_each("processes").to_a.sort
|
760
805
|
|
761
|
-
Sidekiq.redis do |conn|
|
762
806
|
# We're making a tradeoff here between consuming more memory instead of
|
763
807
|
# making more roundtrips to Redis, but if you have hundreds or thousands of workers,
|
764
808
|
# you'll be happier this way
|
765
|
-
|
809
|
+
conn.pipelined do
|
766
810
|
procs.each do |key|
|
767
|
-
conn.hmget(key,
|
811
|
+
conn.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
|
768
812
|
end
|
769
813
|
end
|
814
|
+
}
|
770
815
|
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
816
|
+
result.each do |info, busy, at_s, quiet, rss, rtt|
|
817
|
+
# If a process is stopped between when we query Redis for `procs` and
|
818
|
+
# when we query for `result`, we will have an item in `result` that is
|
819
|
+
# composed of `nil` values.
|
820
|
+
next if info.nil?
|
776
821
|
|
777
|
-
|
778
|
-
|
779
|
-
|
822
|
+
hash = Sidekiq.load_json(info)
|
823
|
+
yield Process.new(hash.merge("busy" => busy.to_i,
|
824
|
+
"beat" => at_s.to_f,
|
825
|
+
"quiet" => quiet,
|
826
|
+
"rss" => rss.to_i,
|
827
|
+
"rtt_us" => rtt.to_i))
|
780
828
|
end
|
781
|
-
|
782
|
-
nil
|
783
829
|
end
|
784
830
|
|
785
831
|
# This method is not guaranteed accurate since it does not prune the set
|
@@ -787,17 +833,29 @@ module Sidekiq
|
|
787
833
|
# contains Sidekiq processes which have sent a heartbeat within the last
|
788
834
|
# 60 seconds.
|
789
835
|
def size
|
790
|
-
Sidekiq.redis { |conn| conn.scard(
|
836
|
+
Sidekiq.redis { |conn| conn.scard("processes") }
|
791
837
|
end
|
792
838
|
|
839
|
+
# Total number of threads available to execute jobs.
|
840
|
+
# For Sidekiq Enterprise customers this number (in production) must be
|
841
|
+
# less than or equal to your licensed concurrency.
|
842
|
+
def total_concurrency
|
843
|
+
sum { |x| x["concurrency"].to_i }
|
844
|
+
end
|
845
|
+
|
846
|
+
def total_rss_in_kb
|
847
|
+
sum { |x| x["rss"].to_i }
|
848
|
+
end
|
849
|
+
alias_method :total_rss, :total_rss_in_kb
|
850
|
+
|
793
851
|
# Returns the identity of the current cluster leader or "" if no leader.
|
794
852
|
# This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
|
795
853
|
# or Sidekiq Pro.
|
796
854
|
def leader
|
797
855
|
@leader ||= begin
|
798
|
-
x = Sidekiq.redis {|c| c.get("dear-leader") }
|
856
|
+
x = Sidekiq.redis { |c| c.get("dear-leader") }
|
799
857
|
# need a non-falsy value so we can memoize
|
800
|
-
x
|
858
|
+
x ||= ""
|
801
859
|
x
|
802
860
|
end
|
803
861
|
end
|
@@ -824,11 +882,11 @@ module Sidekiq
|
|
824
882
|
end
|
825
883
|
|
826
884
|
def tag
|
827
|
-
self[
|
885
|
+
self["tag"]
|
828
886
|
end
|
829
887
|
|
830
888
|
def labels
|
831
|
-
Array(self[
|
889
|
+
Array(self["labels"])
|
832
890
|
end
|
833
891
|
|
834
892
|
def [](key)
|
@@ -836,23 +894,27 @@ module Sidekiq
|
|
836
894
|
end
|
837
895
|
|
838
896
|
def identity
|
839
|
-
self[
|
897
|
+
self["identity"]
|
898
|
+
end
|
899
|
+
|
900
|
+
def queues
|
901
|
+
self["queues"]
|
840
902
|
end
|
841
903
|
|
842
904
|
def quiet!
|
843
|
-
signal(
|
905
|
+
signal("TSTP")
|
844
906
|
end
|
845
907
|
|
846
908
|
def stop!
|
847
|
-
signal(
|
909
|
+
signal("TERM")
|
848
910
|
end
|
849
911
|
|
850
912
|
def dump_threads
|
851
|
-
signal(
|
913
|
+
signal("TTIN")
|
852
914
|
end
|
853
915
|
|
854
916
|
def stopping?
|
855
|
-
self[
|
917
|
+
self["quiet"] == "true"
|
856
918
|
end
|
857
919
|
|
858
920
|
private
|
@@ -866,12 +928,11 @@ module Sidekiq
|
|
866
928
|
end
|
867
929
|
end
|
868
930
|
end
|
869
|
-
|
870
931
|
end
|
871
932
|
|
872
933
|
##
|
873
|
-
#
|
874
|
-
#
|
934
|
+
# The WorkSet stores the work being done by this Sidekiq cluster.
|
935
|
+
# It tracks the process and thread working on each job.
|
875
936
|
#
|
876
937
|
# WARNING WARNING WARNING
|
877
938
|
#
|
@@ -879,34 +940,40 @@ module Sidekiq
|
|
879
940
|
# If you call #size => 5 and then expect #each to be
|
880
941
|
# called 5 times, you're going to have a bad time.
|
881
942
|
#
|
882
|
-
#
|
883
|
-
#
|
884
|
-
#
|
943
|
+
# works = Sidekiq::WorkSet.new
|
944
|
+
# works.size => 2
|
945
|
+
# works.each do |process_id, thread_id, work|
|
885
946
|
# # process_id is a unique identifier per Sidekiq process
|
886
947
|
# # thread_id is a unique identifier per thread
|
887
948
|
# # work is a Hash which looks like:
|
888
|
-
# # { 'queue' => name, 'run_at' => timestamp, 'payload' =>
|
949
|
+
# # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
|
889
950
|
# # run_at is an epoch Integer.
|
890
951
|
# end
|
891
952
|
#
|
892
|
-
class
|
953
|
+
class WorkSet
|
893
954
|
include Enumerable
|
894
|
-
include RedisScanner
|
895
955
|
|
896
|
-
def each
|
956
|
+
def each(&block)
|
957
|
+
results = []
|
897
958
|
Sidekiq.redis do |conn|
|
898
|
-
procs =
|
959
|
+
procs = conn.sscan_each("processes").to_a
|
899
960
|
procs.sort.each do |key|
|
900
|
-
valid, workers = conn.pipelined
|
901
|
-
conn.exists(key)
|
961
|
+
valid, workers = conn.pipelined {
|
962
|
+
conn.exists?(key)
|
902
963
|
conn.hgetall("#{key}:workers")
|
903
|
-
|
964
|
+
}
|
904
965
|
next unless valid
|
905
966
|
workers.each_pair do |tid, json|
|
906
|
-
|
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]
|
907
972
|
end
|
908
973
|
end
|
909
974
|
end
|
975
|
+
|
976
|
+
results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
|
910
977
|
end
|
911
978
|
|
912
979
|
# Note that #size is only as accurate as Sidekiq's heartbeat,
|
@@ -917,18 +984,21 @@ module Sidekiq
|
|
917
984
|
# which can easily get out of sync with crashy processes.
|
918
985
|
def size
|
919
986
|
Sidekiq.redis do |conn|
|
920
|
-
procs =
|
987
|
+
procs = conn.sscan_each("processes").to_a
|
921
988
|
if procs.empty?
|
922
989
|
0
|
923
990
|
else
|
924
|
-
conn.pipelined
|
991
|
+
conn.pipelined {
|
925
992
|
procs.each do |key|
|
926
|
-
conn.hget(key,
|
993
|
+
conn.hget(key, "busy")
|
927
994
|
end
|
928
|
-
|
995
|
+
}.sum(&:to_i)
|
929
996
|
end
|
930
997
|
end
|
931
998
|
end
|
932
999
|
end
|
933
|
-
|
1000
|
+
# Since "worker" is a nebulous term, we've deprecated the use of this class name.
|
1001
|
+
# Is "worker" a process, a type of job, a thread? Undefined!
|
1002
|
+
# WorkSet better describes the data.
|
1003
|
+
Workers = WorkSet
|
934
1004
|
end
|