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