sidekiq 5.2.9 → 6.4.1
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 +318 -1
- data/LICENSE +3 -3
- data/README.md +23 -34
- data/bin/sidekiq +27 -3
- data/bin/sidekiqload +67 -61
- 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 +335 -267
- data/lib/sidekiq/cli.rb +164 -182
- data/lib/sidekiq/client.rb +58 -61
- data/lib/sidekiq/delay.rb +7 -6
- data/lib/sidekiq/exception_handler.rb +10 -12
- data/lib/sidekiq/extensions/action_mailer.rb +13 -22
- data/lib/sidekiq/extensions/active_record.rb +13 -10
- data/lib/sidekiq/extensions/class_methods.rb +14 -11
- data/lib/sidekiq/extensions/generic_proxy.rb +6 -4
- data/lib/sidekiq/fetch.rb +40 -32
- data/lib/sidekiq/job.rb +13 -0
- data/lib/sidekiq/job_logger.rb +33 -7
- data/lib/sidekiq/job_retry.rb +70 -71
- data/lib/sidekiq/job_util.rb +65 -0
- data/lib/sidekiq/launcher.rb +161 -71
- data/lib/sidekiq/logger.rb +170 -0
- data/lib/sidekiq/manager.rb +17 -21
- 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 +20 -16
- data/lib/sidekiq/processor.rb +71 -70
- data/lib/sidekiq/rails.rb +40 -37
- data/lib/sidekiq/redis_connection.rb +48 -48
- 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 +88 -75
- data/lib/sidekiq/web/csrf_protection.rb +180 -0
- data/lib/sidekiq/web/helpers.rb +109 -92
- data/lib/sidekiq/web/router.rb +23 -19
- data/lib/sidekiq/web.rb +61 -105
- data/lib/sidekiq/worker.rb +247 -105
- data/lib/sidekiq.rb +77 -44
- data/sidekiq.gemspec +23 -16
- data/web/assets/images/apple-touch-icon.png +0 -0
- data/web/assets/javascripts/application.js +83 -64
- data/web/assets/javascripts/dashboard.js +54 -73
- data/web/assets/stylesheets/application-dark.css +143 -0
- data/web/assets/stylesheets/application-rtl.css +0 -4
- data/web/assets/stylesheets/application.css +45 -232
- data/web/locales/ar.yml +8 -2
- data/web/locales/de.yml +14 -2
- data/web/locales/en.yml +6 -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/_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 +19 -10
- data/web/views/queues.erb +10 -2
- data/web/views/retries.erb +11 -8
- data/web/views/retry.erb +3 -3
- data/web/views/scheduled.erb +5 -2
- metadata +34 -64
- data/.circleci/config.yml +0 -61
- data/.github/contributing.md +0 -32
- data/.github/issue_template.md +0 -11
- data/.gitignore +0 -15
- data/.travis.yml +0 -11
- 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 -97
- data/Ent-Changes.md +0 -238
- data/Gemfile +0 -23
- 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 -759
- data/Rakefile +0 -9
- data/bin/sidekiqctl +0 -20
- 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/ctl.rb +0 -221
- 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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
54
|
+
# O(1) redis calls
|
55
|
+
def fetch_stats_fast!
|
56
|
+
pipe1_res = Sidekiq.redis { |conn|
|
57
|
+
conn.pipelined do |pipeline|
|
58
|
+
pipeline.get("stat:processed")
|
59
|
+
pipeline.get("stat:failed")
|
60
|
+
pipeline.zcard("schedule")
|
61
|
+
pipeline.zcard("retry")
|
62
|
+
pipeline.zcard("dead")
|
63
|
+
pipeline.scard("processes")
|
64
|
+
pipeline.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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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|
|
104
|
+
conn.pipelined do |pipeline|
|
105
|
+
processes.each { |key| pipeline.hget(key, "busy") }
|
106
|
+
queues.each { |queue| pipeline.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 { |pipeline|
|
148
151
|
queues.each do |queue|
|
149
|
-
|
152
|
+
pipeline.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,16 +280,16 @@ 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
|
295
287
|
|
296
288
|
def clear
|
297
289
|
Sidekiq.redis do |conn|
|
298
|
-
conn.multi do
|
299
|
-
|
300
|
-
|
290
|
+
conn.multi do |transaction|
|
291
|
+
transaction.unlink(@rname)
|
292
|
+
transaction.srem("queues", name)
|
301
293
|
end
|
302
294
|
end
|
303
295
|
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,84 +326,105 @@ 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
|
361
355
|
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
362
356
|
@display_args ||= case klass
|
363
357
|
when /\ASidekiq::Extensions::Delayed/
|
364
|
-
safe_load(args[0], args) do |_, _, arg|
|
365
|
-
|
358
|
+
safe_load(args[0], args) do |_, _, arg, kwarg|
|
359
|
+
if !kwarg || kwarg.empty?
|
360
|
+
arg
|
361
|
+
else
|
362
|
+
[arg, kwarg]
|
363
|
+
end
|
366
364
|
end
|
367
365
|
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
368
|
-
job_args = self[
|
369
|
-
if
|
366
|
+
job_args = self["wrapped"] ? args[0]["arguments"] : []
|
367
|
+
if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
|
370
368
|
# remove MailerClass, mailer_method and 'deliver_now'
|
371
369
|
job_args.drop(3)
|
370
|
+
elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
|
371
|
+
# remove MailerClass, mailer_method and 'deliver_now'
|
372
|
+
job_args.drop(3).first["args"]
|
372
373
|
else
|
373
374
|
job_args
|
374
375
|
end
|
375
376
|
else
|
376
|
-
if self[
|
377
|
+
if self["encrypt"]
|
377
378
|
# no point in showing 150+ bytes of random garbage
|
378
|
-
args[-1] =
|
379
|
+
args[-1] = "[encrypted data]"
|
379
380
|
end
|
380
381
|
args
|
381
|
-
|
382
|
+
end
|
382
383
|
end
|
383
384
|
|
384
385
|
def args
|
385
|
-
@args || @item[
|
386
|
+
@args || @item["args"]
|
386
387
|
end
|
387
388
|
|
388
389
|
def jid
|
389
|
-
self[
|
390
|
+
self["jid"]
|
390
391
|
end
|
391
392
|
|
392
393
|
def enqueued_at
|
393
|
-
self[
|
394
|
+
self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
|
394
395
|
end
|
395
396
|
|
396
397
|
def created_at
|
397
|
-
Time.at(self[
|
398
|
+
Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
|
399
|
+
end
|
400
|
+
|
401
|
+
def tags
|
402
|
+
self["tags"] || []
|
398
403
|
end
|
399
404
|
|
400
|
-
def
|
401
|
-
|
405
|
+
def error_backtrace
|
406
|
+
# Cache nil values
|
407
|
+
if defined?(@error_backtrace)
|
408
|
+
@error_backtrace
|
409
|
+
else
|
410
|
+
value = self["error_backtrace"]
|
411
|
+
@error_backtrace = value && uncompress_backtrace(value)
|
412
|
+
end
|
402
413
|
end
|
403
414
|
|
415
|
+
attr_reader :queue
|
416
|
+
|
404
417
|
def latency
|
405
418
|
now = Time.now.to_f
|
406
|
-
now - (@item[
|
419
|
+
now - (@item["enqueued_at"] || @item["created_at"] || now)
|
407
420
|
end
|
408
421
|
|
409
422
|
##
|
410
423
|
# Remove this job from the queue.
|
411
424
|
def delete
|
412
|
-
count = Sidekiq.redis
|
425
|
+
count = Sidekiq.redis { |conn|
|
413
426
|
conn.lrem("queue:#{@queue}", 1, @value)
|
414
|
-
|
427
|
+
}
|
415
428
|
count != 0
|
416
429
|
end
|
417
430
|
|
@@ -425,18 +438,33 @@ module Sidekiq
|
|
425
438
|
private
|
426
439
|
|
427
440
|
def safe_load(content, default)
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
441
|
+
yield(*YAML.load(content))
|
442
|
+
rescue => ex
|
443
|
+
# #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
|
444
|
+
# memory yet so the YAML can't be loaded.
|
445
|
+
Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
|
446
|
+
default
|
447
|
+
end
|
448
|
+
|
449
|
+
def uncompress_backtrace(backtrace)
|
450
|
+
if backtrace.is_a?(Array)
|
451
|
+
# Handle old jobs with raw Array backtrace format
|
452
|
+
backtrace
|
453
|
+
else
|
454
|
+
decoded = Base64.decode64(backtrace)
|
455
|
+
uncompressed = Zlib::Inflate.inflate(decoded)
|
456
|
+
begin
|
457
|
+
Sidekiq.load_json(uncompressed)
|
458
|
+
rescue
|
459
|
+
# Handle old jobs with marshalled backtrace format
|
460
|
+
# TODO Remove in 7.x
|
461
|
+
Marshal.load(uncompressed)
|
462
|
+
end
|
435
463
|
end
|
436
464
|
end
|
437
465
|
end
|
438
466
|
|
439
|
-
class SortedEntry <
|
467
|
+
class SortedEntry < JobRecord
|
440
468
|
attr_reader :score
|
441
469
|
attr_reader :parent
|
442
470
|
|
@@ -459,8 +487,9 @@ module Sidekiq
|
|
459
487
|
end
|
460
488
|
|
461
489
|
def reschedule(at)
|
462
|
-
|
463
|
-
|
490
|
+
Sidekiq.redis do |conn|
|
491
|
+
conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
|
492
|
+
end
|
464
493
|
end
|
465
494
|
|
466
495
|
def add_to_queue
|
@@ -473,7 +502,7 @@ module Sidekiq
|
|
473
502
|
def retry
|
474
503
|
remove_job do |message|
|
475
504
|
msg = Sidekiq.load_json(message)
|
476
|
-
msg[
|
505
|
+
msg["retry_count"] -= 1 if msg["retry_count"]
|
477
506
|
Sidekiq::Client.push(msg)
|
478
507
|
end
|
479
508
|
end
|
@@ -487,45 +516,44 @@ module Sidekiq
|
|
487
516
|
end
|
488
517
|
|
489
518
|
def error?
|
490
|
-
!!item[
|
519
|
+
!!item["error_class"]
|
491
520
|
end
|
492
521
|
|
493
522
|
private
|
494
523
|
|
495
524
|
def remove_job
|
496
525
|
Sidekiq.redis do |conn|
|
497
|
-
results = conn.multi
|
498
|
-
|
499
|
-
|
500
|
-
|
526
|
+
results = conn.multi { |transaction|
|
527
|
+
transaction.zrangebyscore(parent.name, score, score)
|
528
|
+
transaction.zremrangebyscore(parent.name, score, score)
|
529
|
+
}.first
|
501
530
|
|
502
531
|
if results.size == 1
|
503
532
|
yield results.first
|
504
533
|
else
|
505
534
|
# multiple jobs with the same score
|
506
535
|
# find the one with the right JID and push it
|
507
|
-
|
536
|
+
matched, nonmatched = results.partition { |message|
|
508
537
|
if message.index(jid)
|
509
538
|
msg = Sidekiq.load_json(message)
|
510
|
-
msg[
|
539
|
+
msg["jid"] == jid
|
511
540
|
else
|
512
541
|
false
|
513
542
|
end
|
514
|
-
|
543
|
+
}
|
515
544
|
|
516
|
-
msg =
|
545
|
+
msg = matched.first
|
517
546
|
yield msg if msg
|
518
547
|
|
519
548
|
# push the rest back onto the sorted set
|
520
|
-
conn.multi do
|
521
|
-
|
522
|
-
|
549
|
+
conn.multi do |transaction|
|
550
|
+
nonmatched.each do |message|
|
551
|
+
transaction.zadd(parent.name, score.to_f.to_s, message)
|
523
552
|
end
|
524
553
|
end
|
525
554
|
end
|
526
555
|
end
|
527
556
|
end
|
528
|
-
|
529
557
|
end
|
530
558
|
|
531
559
|
class SortedSet
|
@@ -542,16 +570,26 @@ module Sidekiq
|
|
542
570
|
Sidekiq.redis { |c| c.zcard(name) }
|
543
571
|
end
|
544
572
|
|
573
|
+
def scan(match, count = 100)
|
574
|
+
return to_enum(:scan, match, count) unless block_given?
|
575
|
+
|
576
|
+
match = "*#{match}*" unless match.include?("*")
|
577
|
+
Sidekiq.redis do |conn|
|
578
|
+
conn.zscan_each(name, match: match, count: count) do |entry, score|
|
579
|
+
yield SortedEntry.new(self, score, entry)
|
580
|
+
end
|
581
|
+
end
|
582
|
+
end
|
583
|
+
|
545
584
|
def clear
|
546
585
|
Sidekiq.redis do |conn|
|
547
|
-
conn.
|
586
|
+
conn.unlink(name)
|
548
587
|
end
|
549
588
|
end
|
550
589
|
alias_method :💣, :clear
|
551
590
|
end
|
552
591
|
|
553
592
|
class JobSet < SortedSet
|
554
|
-
|
555
593
|
def schedule(timestamp, message)
|
556
594
|
Sidekiq.redis do |conn|
|
557
595
|
conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
|
@@ -564,44 +602,55 @@ module Sidekiq
|
|
564
602
|
page = -1
|
565
603
|
page_size = 50
|
566
604
|
|
567
|
-
|
605
|
+
loop do
|
568
606
|
range_start = page * page_size + offset_size
|
569
|
-
range_end
|
570
|
-
elements = Sidekiq.redis
|
607
|
+
range_end = range_start + page_size - 1
|
608
|
+
elements = Sidekiq.redis { |conn|
|
571
609
|
conn.zrange name, range_start, range_end, with_scores: true
|
572
|
-
|
610
|
+
}
|
573
611
|
break if elements.empty?
|
574
612
|
page -= 1
|
575
|
-
elements.
|
613
|
+
elements.reverse_each do |element, score|
|
576
614
|
yield SortedEntry.new(self, score, element)
|
577
615
|
end
|
578
616
|
offset_size = initial_size - @_size
|
579
617
|
end
|
580
618
|
end
|
581
619
|
|
620
|
+
##
|
621
|
+
# Fetch jobs that match a given time or Range. Job ID is an
|
622
|
+
# optional second argument.
|
582
623
|
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
|
624
|
+
begin_score, end_score =
|
625
|
+
if score.is_a?(Range)
|
626
|
+
[score.first, score.last]
|
591
627
|
else
|
592
|
-
|
628
|
+
[score, score]
|
593
629
|
end
|
594
|
-
|
630
|
+
|
631
|
+
elements = Sidekiq.redis { |conn|
|
632
|
+
conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
|
633
|
+
}
|
634
|
+
|
635
|
+
elements.each_with_object([]) do |element, result|
|
636
|
+
data, job_score = element
|
637
|
+
entry = SortedEntry.new(self, job_score, data)
|
638
|
+
result << entry if jid.nil? || entry.jid == jid
|
595
639
|
end
|
596
640
|
end
|
597
641
|
|
598
642
|
##
|
599
643
|
# 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.
|
644
|
+
# This is a slower O(n) operation. Do not use for app logic.
|
603
645
|
def find_job(jid)
|
604
|
-
|
646
|
+
Sidekiq.redis do |conn|
|
647
|
+
conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
|
648
|
+
job = JSON.parse(entry)
|
649
|
+
matched = job["jid"] == jid
|
650
|
+
return SortedEntry.new(self, score, entry) if matched
|
651
|
+
end
|
652
|
+
end
|
653
|
+
nil
|
605
654
|
end
|
606
655
|
|
607
656
|
def delete_by_value(name, value)
|
@@ -616,13 +665,14 @@ module Sidekiq
|
|
616
665
|
Sidekiq.redis do |conn|
|
617
666
|
elements = conn.zrangebyscore(name, score, score)
|
618
667
|
elements.each do |element|
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
668
|
+
if element.index(jid)
|
669
|
+
message = Sidekiq.load_json(element)
|
670
|
+
if message["jid"] == jid
|
671
|
+
ret = conn.zrem(name, element)
|
672
|
+
@_size -= 1 if ret
|
673
|
+
break ret
|
674
|
+
end
|
624
675
|
end
|
625
|
-
false
|
626
676
|
end
|
627
677
|
end
|
628
678
|
end
|
@@ -644,7 +694,7 @@ module Sidekiq
|
|
644
694
|
# end.map(&:delete)
|
645
695
|
class ScheduledSet < JobSet
|
646
696
|
def initialize
|
647
|
-
super
|
697
|
+
super "schedule"
|
648
698
|
end
|
649
699
|
end
|
650
700
|
|
@@ -662,19 +712,15 @@ module Sidekiq
|
|
662
712
|
# end.map(&:delete)
|
663
713
|
class RetrySet < JobSet
|
664
714
|
def initialize
|
665
|
-
super
|
715
|
+
super "retry"
|
666
716
|
end
|
667
717
|
|
668
718
|
def retry_all
|
669
|
-
while size > 0
|
670
|
-
each(&:retry)
|
671
|
-
end
|
719
|
+
each(&:retry) while size > 0
|
672
720
|
end
|
673
721
|
|
674
722
|
def kill_all
|
675
|
-
while size > 0
|
676
|
-
each(&:kill)
|
677
|
-
end
|
723
|
+
each(&:kill) while size > 0
|
678
724
|
end
|
679
725
|
end
|
680
726
|
|
@@ -683,16 +729,16 @@ module Sidekiq
|
|
683
729
|
#
|
684
730
|
class DeadSet < JobSet
|
685
731
|
def initialize
|
686
|
-
super
|
732
|
+
super "dead"
|
687
733
|
end
|
688
734
|
|
689
|
-
def kill(message, opts={})
|
735
|
+
def kill(message, opts = {})
|
690
736
|
now = Time.now.to_f
|
691
737
|
Sidekiq.redis do |conn|
|
692
|
-
conn.multi do
|
693
|
-
|
694
|
-
|
695
|
-
|
738
|
+
conn.multi do |transaction|
|
739
|
+
transaction.zadd(name, now.to_s, message)
|
740
|
+
transaction.zremrangebyscore(name, "-inf", now - self.class.timeout)
|
741
|
+
transaction.zremrangebyrank(name, 0, - self.class.max_jobs)
|
696
742
|
end
|
697
743
|
end
|
698
744
|
|
@@ -708,9 +754,7 @@ module Sidekiq
|
|
708
754
|
end
|
709
755
|
|
710
756
|
def retry_all
|
711
|
-
while size > 0
|
712
|
-
each(&:retry)
|
713
|
-
end
|
757
|
+
each(&:retry) while size > 0
|
714
758
|
end
|
715
759
|
|
716
760
|
def self.max_jobs
|
@@ -724,16 +768,15 @@ module Sidekiq
|
|
724
768
|
|
725
769
|
##
|
726
770
|
# Enumerates the set of Sidekiq processes which are actively working
|
727
|
-
# right now. Each process
|
771
|
+
# right now. Each process sends a heartbeat to Redis every 5 seconds
|
728
772
|
# so this set should be relatively accurate, barring network partitions.
|
729
773
|
#
|
730
774
|
# Yields a Sidekiq::Process.
|
731
775
|
#
|
732
776
|
class ProcessSet
|
733
777
|
include Enumerable
|
734
|
-
include RedisScanner
|
735
778
|
|
736
|
-
def initialize(clean_plz=true)
|
779
|
+
def initialize(clean_plz = true)
|
737
780
|
cleanup if clean_plz
|
738
781
|
end
|
739
782
|
|
@@ -742,50 +785,51 @@ module Sidekiq
|
|
742
785
|
def cleanup
|
743
786
|
count = 0
|
744
787
|
Sidekiq.redis do |conn|
|
745
|
-
procs =
|
746
|
-
heartbeats = conn.pipelined
|
788
|
+
procs = conn.sscan_each("processes").to_a.sort
|
789
|
+
heartbeats = conn.pipelined { |pipeline|
|
747
790
|
procs.each do |key|
|
748
|
-
|
791
|
+
pipeline.hget(key, "info")
|
749
792
|
end
|
750
|
-
|
793
|
+
}
|
751
794
|
|
752
795
|
# the hash named key has an expiry of 60 seconds.
|
753
796
|
# if it's not found, that means the process has not reported
|
754
797
|
# in to Redis and probably died.
|
755
|
-
to_prune =
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
count = conn.srem('processes', to_prune) unless to_prune.empty?
|
798
|
+
to_prune = procs.select.with_index { |proc, i|
|
799
|
+
heartbeats[i].nil?
|
800
|
+
}
|
801
|
+
count = conn.srem("processes", to_prune) unless to_prune.empty?
|
760
802
|
end
|
761
803
|
count
|
762
804
|
end
|
763
805
|
|
764
806
|
def each
|
765
|
-
|
807
|
+
result = Sidekiq.redis { |conn|
|
808
|
+
procs = conn.sscan_each("processes").to_a.sort
|
766
809
|
|
767
|
-
Sidekiq.redis do |conn|
|
768
810
|
# We're making a tradeoff here between consuming more memory instead of
|
769
811
|
# making more roundtrips to Redis, but if you have hundreds or thousands of workers,
|
770
812
|
# you'll be happier this way
|
771
|
-
|
813
|
+
conn.pipelined do |pipeline|
|
772
814
|
procs.each do |key|
|
773
|
-
|
815
|
+
pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
|
774
816
|
end
|
775
817
|
end
|
818
|
+
}
|
776
819
|
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
820
|
+
result.each do |info, busy, at_s, quiet, rss, rtt|
|
821
|
+
# If a process is stopped between when we query Redis for `procs` and
|
822
|
+
# when we query for `result`, we will have an item in `result` that is
|
823
|
+
# composed of `nil` values.
|
824
|
+
next if info.nil?
|
782
825
|
|
783
|
-
|
784
|
-
|
785
|
-
|
826
|
+
hash = Sidekiq.load_json(info)
|
827
|
+
yield Process.new(hash.merge("busy" => busy.to_i,
|
828
|
+
"beat" => at_s.to_f,
|
829
|
+
"quiet" => quiet,
|
830
|
+
"rss" => rss.to_i,
|
831
|
+
"rtt_us" => rtt.to_i))
|
786
832
|
end
|
787
|
-
|
788
|
-
nil
|
789
833
|
end
|
790
834
|
|
791
835
|
# This method is not guaranteed accurate since it does not prune the set
|
@@ -793,17 +837,29 @@ module Sidekiq
|
|
793
837
|
# contains Sidekiq processes which have sent a heartbeat within the last
|
794
838
|
# 60 seconds.
|
795
839
|
def size
|
796
|
-
Sidekiq.redis { |conn| conn.scard(
|
840
|
+
Sidekiq.redis { |conn| conn.scard("processes") }
|
797
841
|
end
|
798
842
|
|
843
|
+
# Total number of threads available to execute jobs.
|
844
|
+
# For Sidekiq Enterprise customers this number (in production) must be
|
845
|
+
# less than or equal to your licensed concurrency.
|
846
|
+
def total_concurrency
|
847
|
+
sum { |x| x["concurrency"].to_i }
|
848
|
+
end
|
849
|
+
|
850
|
+
def total_rss_in_kb
|
851
|
+
sum { |x| x["rss"].to_i }
|
852
|
+
end
|
853
|
+
alias_method :total_rss, :total_rss_in_kb
|
854
|
+
|
799
855
|
# Returns the identity of the current cluster leader or "" if no leader.
|
800
856
|
# This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
|
801
857
|
# or Sidekiq Pro.
|
802
858
|
def leader
|
803
859
|
@leader ||= begin
|
804
|
-
x = Sidekiq.redis {|c| c.get("dear-leader") }
|
860
|
+
x = Sidekiq.redis { |c| c.get("dear-leader") }
|
805
861
|
# need a non-falsy value so we can memoize
|
806
|
-
x
|
862
|
+
x ||= ""
|
807
863
|
x
|
808
864
|
end
|
809
865
|
end
|
@@ -830,11 +886,11 @@ module Sidekiq
|
|
830
886
|
end
|
831
887
|
|
832
888
|
def tag
|
833
|
-
self[
|
889
|
+
self["tag"]
|
834
890
|
end
|
835
891
|
|
836
892
|
def labels
|
837
|
-
Array(self[
|
893
|
+
Array(self["labels"])
|
838
894
|
end
|
839
895
|
|
840
896
|
def [](key)
|
@@ -842,23 +898,27 @@ module Sidekiq
|
|
842
898
|
end
|
843
899
|
|
844
900
|
def identity
|
845
|
-
self[
|
901
|
+
self["identity"]
|
902
|
+
end
|
903
|
+
|
904
|
+
def queues
|
905
|
+
self["queues"]
|
846
906
|
end
|
847
907
|
|
848
908
|
def quiet!
|
849
|
-
signal(
|
909
|
+
signal("TSTP")
|
850
910
|
end
|
851
911
|
|
852
912
|
def stop!
|
853
|
-
signal(
|
913
|
+
signal("TERM")
|
854
914
|
end
|
855
915
|
|
856
916
|
def dump_threads
|
857
|
-
signal(
|
917
|
+
signal("TTIN")
|
858
918
|
end
|
859
919
|
|
860
920
|
def stopping?
|
861
|
-
self[
|
921
|
+
self["quiet"] == "true"
|
862
922
|
end
|
863
923
|
|
864
924
|
private
|
@@ -866,18 +926,17 @@ module Sidekiq
|
|
866
926
|
def signal(sig)
|
867
927
|
key = "#{identity}-signals"
|
868
928
|
Sidekiq.redis do |c|
|
869
|
-
c.multi do
|
870
|
-
|
871
|
-
|
929
|
+
c.multi do |transaction|
|
930
|
+
transaction.lpush(key, sig)
|
931
|
+
transaction.expire(key, 60)
|
872
932
|
end
|
873
933
|
end
|
874
934
|
end
|
875
|
-
|
876
935
|
end
|
877
936
|
|
878
937
|
##
|
879
|
-
#
|
880
|
-
#
|
938
|
+
# The WorkSet stores the work being done by this Sidekiq cluster.
|
939
|
+
# It tracks the process and thread working on each job.
|
881
940
|
#
|
882
941
|
# WARNING WARNING WARNING
|
883
942
|
#
|
@@ -885,34 +944,40 @@ module Sidekiq
|
|
885
944
|
# If you call #size => 5 and then expect #each to be
|
886
945
|
# called 5 times, you're going to have a bad time.
|
887
946
|
#
|
888
|
-
#
|
889
|
-
#
|
890
|
-
#
|
947
|
+
# works = Sidekiq::WorkSet.new
|
948
|
+
# works.size => 2
|
949
|
+
# works.each do |process_id, thread_id, work|
|
891
950
|
# # process_id is a unique identifier per Sidekiq process
|
892
951
|
# # thread_id is a unique identifier per thread
|
893
952
|
# # work is a Hash which looks like:
|
894
|
-
# # { 'queue' => name, 'run_at' => timestamp, 'payload' =>
|
953
|
+
# # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
|
895
954
|
# # run_at is an epoch Integer.
|
896
955
|
# end
|
897
956
|
#
|
898
|
-
class
|
957
|
+
class WorkSet
|
899
958
|
include Enumerable
|
900
|
-
include RedisScanner
|
901
959
|
|
902
|
-
def each
|
960
|
+
def each(&block)
|
961
|
+
results = []
|
903
962
|
Sidekiq.redis do |conn|
|
904
|
-
procs =
|
963
|
+
procs = conn.sscan_each("processes").to_a
|
905
964
|
procs.sort.each do |key|
|
906
|
-
valid, workers = conn.pipelined
|
907
|
-
|
908
|
-
|
909
|
-
|
965
|
+
valid, workers = conn.pipelined { |pipeline|
|
966
|
+
pipeline.exists?(key)
|
967
|
+
pipeline.hgetall("#{key}:workers")
|
968
|
+
}
|
910
969
|
next unless valid
|
911
970
|
workers.each_pair do |tid, json|
|
912
|
-
|
971
|
+
hsh = Sidekiq.load_json(json)
|
972
|
+
p = hsh["payload"]
|
973
|
+
# avoid breaking API, this is a side effect of the JSON optimization in #4316
|
974
|
+
hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
|
975
|
+
results << [key, tid, hsh]
|
913
976
|
end
|
914
977
|
end
|
915
978
|
end
|
979
|
+
|
980
|
+
results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
|
916
981
|
end
|
917
982
|
|
918
983
|
# Note that #size is only as accurate as Sidekiq's heartbeat,
|
@@ -923,18 +988,21 @@ module Sidekiq
|
|
923
988
|
# which can easily get out of sync with crashy processes.
|
924
989
|
def size
|
925
990
|
Sidekiq.redis do |conn|
|
926
|
-
procs =
|
991
|
+
procs = conn.sscan_each("processes").to_a
|
927
992
|
if procs.empty?
|
928
993
|
0
|
929
994
|
else
|
930
|
-
conn.pipelined
|
995
|
+
conn.pipelined { |pipeline|
|
931
996
|
procs.each do |key|
|
932
|
-
|
997
|
+
pipeline.hget(key, "busy")
|
933
998
|
end
|
934
|
-
|
999
|
+
}.sum(&:to_i)
|
935
1000
|
end
|
936
1001
|
end
|
937
1002
|
end
|
938
1003
|
end
|
939
|
-
|
1004
|
+
# Since "worker" is a nebulous term, we've deprecated the use of this class name.
|
1005
|
+
# Is "worker" a process, a type of job, a thread? Undefined!
|
1006
|
+
# WorkSet better describes the data.
|
1007
|
+
Workers = WorkSet
|
940
1008
|
end
|