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