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