sidekiq 6.5.10 → 8.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Changes.md +568 -18
- data/README.md +66 -38
- data/bin/kiq +17 -0
- data/bin/lint-herb +13 -0
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiq +3 -8
- data/bin/sidekiqload +215 -118
- data/bin/sidekiqmon +3 -0
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
- data/lib/generators/sidekiq/job_generator.rb +17 -3
- data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
- data/lib/sidekiq/api.rb +526 -218
- data/lib/sidekiq/capsule.rb +135 -0
- data/lib/sidekiq/cli.rb +88 -96
- data/lib/sidekiq/client.rb +114 -47
- data/lib/sidekiq/component.rb +71 -4
- data/lib/sidekiq/config.rb +316 -0
- data/lib/sidekiq/deploy.rb +64 -0
- data/lib/sidekiq/embedded.rb +64 -0
- data/lib/sidekiq/fetch.rb +12 -14
- data/lib/sidekiq/iterable_job.rb +56 -0
- data/lib/sidekiq/job/interrupt_handler.rb +24 -0
- data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
- data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
- data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
- data/lib/sidekiq/job/iterable.rb +322 -0
- data/lib/sidekiq/job.rb +397 -10
- data/lib/sidekiq/job_logger.rb +14 -11
- data/lib/sidekiq/job_retry.rb +78 -28
- data/lib/sidekiq/job_util.rb +58 -16
- data/lib/sidekiq/launcher.rb +86 -70
- data/lib/sidekiq/loader.rb +57 -0
- data/lib/sidekiq/logger.rb +25 -94
- data/lib/sidekiq/manager.rb +11 -13
- data/lib/sidekiq/metrics/query.rb +84 -48
- data/lib/sidekiq/metrics/shared.rb +29 -14
- data/lib/sidekiq/metrics/tracking.rb +49 -30
- data/lib/sidekiq/middleware/chain.rb +19 -18
- data/lib/sidekiq/middleware/current_attributes.rb +78 -21
- data/lib/sidekiq/middleware/i18n.rb +2 -0
- data/lib/sidekiq/middleware/modules.rb +2 -0
- data/lib/sidekiq/monitor.rb +15 -5
- data/lib/sidekiq/paginator.rb +26 -4
- data/lib/sidekiq/processor.rb +78 -65
- data/lib/sidekiq/profiler.rb +73 -0
- data/lib/sidekiq/rails.rb +45 -60
- data/lib/sidekiq/redis_client_adapter.rb +35 -72
- data/lib/sidekiq/redis_connection.rb +54 -114
- data/lib/sidekiq/ring_buffer.rb +3 -0
- data/lib/sidekiq/scheduled.rb +28 -30
- data/lib/sidekiq/systemd.rb +2 -0
- data/lib/sidekiq/test_api.rb +331 -0
- data/lib/sidekiq/testing/inline.rb +2 -30
- data/lib/sidekiq/testing.rb +2 -342
- data/lib/sidekiq/transaction_aware_client.rb +23 -9
- data/lib/sidekiq/tui/controls.rb +53 -0
- data/lib/sidekiq/tui/filtering.rb +53 -0
- data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
- data/lib/sidekiq/tui/tabs/busy.rb +104 -0
- data/lib/sidekiq/tui/tabs/dead.rb +14 -0
- data/lib/sidekiq/tui/tabs/home.rb +115 -0
- data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
- data/lib/sidekiq/tui/tabs/queues.rb +83 -0
- data/lib/sidekiq/tui/tabs/retries.rb +14 -0
- data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
- data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
- data/lib/sidekiq/tui/tabs.rb +15 -0
- data/lib/sidekiq/tui.rb +390 -0
- data/lib/sidekiq/version.rb +6 -1
- data/lib/sidekiq/web/action.rb +149 -64
- data/lib/sidekiq/web/application.rb +379 -281
- data/lib/sidekiq/web/config.rb +117 -0
- data/lib/sidekiq/web/helpers.rb +190 -71
- data/lib/sidekiq/web/router.rb +62 -75
- data/lib/sidekiq/web.rb +68 -101
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +85 -269
- data/sidekiq.gemspec +15 -11
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status.png +0 -0
- data/web/assets/javascripts/application.js +84 -24
- data/web/assets/javascripts/base-charts.js +120 -0
- data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/web/assets/javascripts/dashboard-charts.js +194 -0
- data/web/assets/javascripts/dashboard.js +18 -234
- data/web/assets/javascripts/metrics.js +138 -120
- data/web/assets/stylesheets/style.css +776 -0
- data/web/locales/ar.yml +72 -70
- data/web/locales/cs.yml +64 -62
- data/web/locales/da.yml +62 -53
- data/web/locales/de.yml +67 -65
- data/web/locales/el.yml +4 -7
- data/web/locales/en.yml +89 -71
- data/web/locales/es.yml +91 -68
- data/web/locales/fa.yml +67 -65
- data/web/locales/fr.yml +82 -67
- data/web/locales/gd.yml +110 -0
- data/web/locales/he.yml +67 -64
- data/web/locales/hi.yml +95 -59
- data/web/locales/it.yml +94 -54
- data/web/locales/ja.yml +69 -70
- data/web/locales/ko.yml +54 -52
- data/web/locales/lt.yml +68 -66
- data/web/locales/nb.yml +63 -61
- data/web/locales/nl.yml +54 -52
- data/web/locales/pl.yml +47 -45
- data/web/locales/{pt-br.yml → pt-BR.yml} +81 -70
- data/web/locales/pt.yml +53 -51
- data/web/locales/ru.yml +69 -66
- data/web/locales/sv.yml +55 -53
- data/web/locales/ta.yml +100 -64
- data/web/locales/tr.yml +102 -0
- data/web/locales/uk.yml +87 -61
- data/web/locales/ur.yml +66 -64
- data/web/locales/vi.yml +69 -67
- data/web/locales/zh-CN.yml +107 -0
- data/web/locales/{zh-tw.yml → zh-TW.yml} +13 -3
- data/web/views/_footer.html.erb +32 -0
- data/web/views/_job_info.html.erb +115 -0
- data/web/views/_metrics_period_select.html.erb +15 -0
- data/web/views/_nav.html.erb +45 -0
- data/web/views/_paging.html.erb +26 -0
- data/web/views/_poll_link.html.erb +4 -0
- data/web/views/_summary.html.erb +40 -0
- data/web/views/busy.html.erb +155 -0
- data/web/views/dashboard.html.erb +104 -0
- data/web/views/dead.html.erb +38 -0
- data/web/views/filtering.html.erb +6 -0
- data/web/views/layout.html.erb +26 -0
- data/web/views/metrics.html.erb +85 -0
- data/web/views/metrics_for_job.html.erb +58 -0
- data/web/views/morgue.html.erb +69 -0
- data/web/views/profiles.html.erb +43 -0
- data/web/views/queue.html.erb +57 -0
- data/web/views/queues.html.erb +46 -0
- data/web/views/retries.html.erb +77 -0
- data/web/views/retry.html.erb +39 -0
- data/web/views/scheduled.html.erb +64 -0
- data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
- metadata +113 -70
- data/lib/sidekiq/delay.rb +0 -43
- data/lib/sidekiq/extensions/action_mailer.rb +0 -48
- data/lib/sidekiq/extensions/active_record.rb +0 -43
- data/lib/sidekiq/extensions/class_methods.rb +0 -43
- data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
- data/lib/sidekiq/metrics/deploy.rb +0 -47
- data/lib/sidekiq/web/csrf_protection.rb +0 -180
- data/lib/sidekiq/worker.rb +0 -370
- data/web/assets/javascripts/graph.js +0 -16
- data/web/assets/stylesheets/application-dark.css +0 -143
- data/web/assets/stylesheets/application-rtl.css +0 -242
- data/web/assets/stylesheets/application.css +0 -999
- data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
- data/web/assets/stylesheets/bootstrap.css +0 -5
- data/web/locales/zh-cn.yml +0 -93
- data/web/views/_footer.erb +0 -20
- data/web/views/_job_info.erb +0 -89
- data/web/views/_nav.erb +0 -52
- data/web/views/_paging.erb +0 -23
- data/web/views/_poll_link.erb +0 -4
- data/web/views/_status.erb +0 -4
- data/web/views/_summary.erb +0 -40
- data/web/views/busy.erb +0 -137
- data/web/views/dashboard.erb +0 -84
- data/web/views/dead.erb +0 -34
- data/web/views/layout.erb +0 -42
- data/web/views/metrics.erb +0 -69
- data/web/views/metrics_for_job.erb +0 -87
- data/web/views/morgue.erb +0 -78
- data/web/views/queue.erb +0 -59
- data/web/views/queues.erb +0 -38
- data/web/views/retries.erb +0 -83
- data/web/views/retry.erb +0 -34
- data/web/views/scheduled.erb +0 -57
- /data/{LICENSE → LICENSE.txt} +0 -0
data/lib/sidekiq/api.rb
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "sidekiq"
|
|
4
|
-
|
|
5
3
|
require "zlib"
|
|
6
|
-
require "set"
|
|
7
|
-
require "base64"
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
require "sidekiq/metrics/query"
|
|
12
|
-
end
|
|
5
|
+
require "sidekiq"
|
|
6
|
+
require "sidekiq/metrics/query"
|
|
13
7
|
|
|
14
8
|
#
|
|
15
9
|
# Sidekiq's Data API provides a Ruby object model on top
|
|
@@ -23,12 +17,37 @@ end
|
|
|
23
17
|
#
|
|
24
18
|
|
|
25
19
|
module Sidekiq
|
|
20
|
+
module ApiUtils
|
|
21
|
+
# @api private
|
|
22
|
+
# Calculate the latency in seconds for a job based on its enqueued timestamp
|
|
23
|
+
# @param job [Hash] the job hash
|
|
24
|
+
# @return [Float] latency in seconds
|
|
25
|
+
def calculate_latency(job)
|
|
26
|
+
timestamp = job["enqueued_at"] || job["created_at"]
|
|
27
|
+
return 0.0 unless timestamp
|
|
28
|
+
|
|
29
|
+
if timestamp.is_a?(Float)
|
|
30
|
+
# old format
|
|
31
|
+
Time.now.to_f - timestamp
|
|
32
|
+
else
|
|
33
|
+
now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
|
|
34
|
+
(now - timestamp) / 1000.0
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
26
39
|
# Retrieve runtime statistics from Redis regarding
|
|
27
40
|
# this Sidekiq cluster.
|
|
28
41
|
#
|
|
29
42
|
# stat = Sidekiq::Stats.new
|
|
30
43
|
# stat.processed
|
|
31
44
|
class Stats
|
|
45
|
+
QueueSummary = Data.define(:name, :size, :latency, :paused) do
|
|
46
|
+
alias_method :paused?, :paused
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
include ApiUtils
|
|
50
|
+
|
|
32
51
|
def initialize
|
|
33
52
|
fetch_stats_fast!
|
|
34
53
|
end
|
|
@@ -69,8 +88,55 @@ module Sidekiq
|
|
|
69
88
|
stat :default_queue_latency
|
|
70
89
|
end
|
|
71
90
|
|
|
91
|
+
# @return [Hash{String => Integer}] a hash of queue names to their lengths
|
|
72
92
|
def queues
|
|
73
|
-
Sidekiq
|
|
93
|
+
Sidekiq.redis do |conn|
|
|
94
|
+
queues = conn.sscan("queues").to_a
|
|
95
|
+
|
|
96
|
+
lengths = conn.pipelined { |pipeline|
|
|
97
|
+
queues.each do |queue|
|
|
98
|
+
pipeline.llen("queue:#{queue}")
|
|
99
|
+
end
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
|
|
103
|
+
array_of_arrays.to_h
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# More detailed information about each queue: name, size, latency, paused status
|
|
108
|
+
# @return [Array<QueueSummary>]
|
|
109
|
+
def queue_summaries
|
|
110
|
+
Sidekiq.redis do |conn|
|
|
111
|
+
queues = conn.sscan("queues").to_a
|
|
112
|
+
return [] if queues.empty?
|
|
113
|
+
|
|
114
|
+
results = conn.pipelined { |pipeline|
|
|
115
|
+
queues.each do |queue|
|
|
116
|
+
pipeline.llen("queue:#{queue}")
|
|
117
|
+
pipeline.lindex("queue:#{queue}", -1)
|
|
118
|
+
pipeline.sismember("paused", queue)
|
|
119
|
+
end
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
queue_summaries = []
|
|
123
|
+
queues.each_with_index do |name, idx|
|
|
124
|
+
size = results[idx * 3]
|
|
125
|
+
last_item = results[idx * 3 + 1]
|
|
126
|
+
paused = results[idx * 3 + 2] > 0
|
|
127
|
+
|
|
128
|
+
latency = if last_item
|
|
129
|
+
job = Sidekiq.load_json(last_item)
|
|
130
|
+
calculate_latency(job)
|
|
131
|
+
else
|
|
132
|
+
0.0
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
queue_summaries << QueueSummary.new(name:, size:, latency:, paused:)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
queue_summaries.sort_by { |qd| -qd.size }
|
|
139
|
+
end
|
|
74
140
|
end
|
|
75
141
|
|
|
76
142
|
# O(1) redis calls
|
|
@@ -84,21 +150,20 @@ module Sidekiq
|
|
|
84
150
|
pipeline.zcard("retry")
|
|
85
151
|
pipeline.zcard("dead")
|
|
86
152
|
pipeline.scard("processes")
|
|
87
|
-
pipeline.
|
|
153
|
+
pipeline.lindex("queue:default", -1)
|
|
88
154
|
end
|
|
89
155
|
}
|
|
90
156
|
|
|
91
|
-
default_queue_latency = if (entry = pipe1_res[6]
|
|
157
|
+
default_queue_latency = if (entry = pipe1_res[6])
|
|
92
158
|
job = begin
|
|
93
159
|
Sidekiq.load_json(entry)
|
|
94
160
|
rescue
|
|
95
161
|
{}
|
|
96
162
|
end
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
now - thence
|
|
163
|
+
|
|
164
|
+
calculate_latency(job)
|
|
100
165
|
else
|
|
101
|
-
0
|
|
166
|
+
0.0
|
|
102
167
|
end
|
|
103
168
|
|
|
104
169
|
@stats = {
|
|
@@ -117,11 +182,11 @@ module Sidekiq
|
|
|
117
182
|
# @api private
|
|
118
183
|
def fetch_stats_slow!
|
|
119
184
|
processes = Sidekiq.redis { |conn|
|
|
120
|
-
conn.
|
|
185
|
+
conn.sscan("processes").to_a
|
|
121
186
|
}
|
|
122
187
|
|
|
123
188
|
queues = Sidekiq.redis { |conn|
|
|
124
|
-
conn.
|
|
189
|
+
conn.sscan("queues").to_a
|
|
125
190
|
}
|
|
126
191
|
|
|
127
192
|
pipe2_res = Sidekiq.redis { |conn|
|
|
@@ -133,7 +198,7 @@ module Sidekiq
|
|
|
133
198
|
|
|
134
199
|
s = processes.size
|
|
135
200
|
workers_size = pipe2_res[0...s].sum(&:to_i)
|
|
136
|
-
enqueued = pipe2_res[s
|
|
201
|
+
enqueued = pipe2_res[s..].sum(&:to_i)
|
|
137
202
|
|
|
138
203
|
@stats[:workers_size] = workers_size
|
|
139
204
|
@stats[:enqueued] = enqueued
|
|
@@ -168,23 +233,6 @@ module Sidekiq
|
|
|
168
233
|
@stats[s] || raise(ArgumentError, "Unknown stat #{s}")
|
|
169
234
|
end
|
|
170
235
|
|
|
171
|
-
class Queues
|
|
172
|
-
def lengths
|
|
173
|
-
Sidekiq.redis do |conn|
|
|
174
|
-
queues = conn.sscan_each("queues").to_a
|
|
175
|
-
|
|
176
|
-
lengths = conn.pipelined { |pipeline|
|
|
177
|
-
queues.each do |queue|
|
|
178
|
-
pipeline.llen("queue:#{queue}")
|
|
179
|
-
end
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
|
|
183
|
-
array_of_arrays.to_h
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
236
|
class History
|
|
189
237
|
def initialize(days_previous, start_date = nil)
|
|
190
238
|
# we only store five years of data in Redis
|
|
@@ -211,15 +259,10 @@ module Sidekiq
|
|
|
211
259
|
|
|
212
260
|
keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
|
|
213
261
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
stat_hash[dates[idx]] = value ? value.to_i : 0
|
|
218
|
-
end
|
|
262
|
+
Sidekiq.redis do |conn|
|
|
263
|
+
conn.mget(keys).each_with_index do |value, idx|
|
|
264
|
+
stat_hash[dates[idx]] = value ? value.to_i : 0
|
|
219
265
|
end
|
|
220
|
-
rescue RedisConnection.adapter::CommandError
|
|
221
|
-
# mget will trigger a CROSSSLOT error when run against a Cluster
|
|
222
|
-
# TODO Someone want to add Cluster support?
|
|
223
266
|
end
|
|
224
267
|
|
|
225
268
|
stat_hash
|
|
@@ -241,16 +284,18 @@ module Sidekiq
|
|
|
241
284
|
# end
|
|
242
285
|
class Queue
|
|
243
286
|
include Enumerable
|
|
287
|
+
include ApiUtils
|
|
244
288
|
|
|
245
289
|
##
|
|
246
290
|
# Fetch all known queues within Redis.
|
|
247
291
|
#
|
|
248
292
|
# @return [Array<Sidekiq::Queue>]
|
|
249
293
|
def self.all
|
|
250
|
-
Sidekiq.redis { |c| c.
|
|
294
|
+
Sidekiq.redis { |c| c.sscan("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
|
|
251
295
|
end
|
|
252
296
|
|
|
253
297
|
attr_reader :name
|
|
298
|
+
alias_method :id, :name
|
|
254
299
|
|
|
255
300
|
# @param name [String] the name of the queue
|
|
256
301
|
def initialize(name = "default")
|
|
@@ -278,13 +323,12 @@ module Sidekiq
|
|
|
278
323
|
# @return [Float] in seconds
|
|
279
324
|
def latency
|
|
280
325
|
entry = Sidekiq.redis { |conn|
|
|
281
|
-
conn.
|
|
282
|
-
}
|
|
283
|
-
return 0 unless entry
|
|
326
|
+
conn.lindex(@rname, -1)
|
|
327
|
+
}
|
|
328
|
+
return 0.0 unless entry
|
|
329
|
+
|
|
284
330
|
job = Sidekiq.load_json(entry)
|
|
285
|
-
|
|
286
|
-
thence = job["enqueued_at"] || now
|
|
287
|
-
now - thence
|
|
331
|
+
calculate_latency(job)
|
|
288
332
|
end
|
|
289
333
|
|
|
290
334
|
def each
|
|
@@ -347,6 +391,8 @@ module Sidekiq
|
|
|
347
391
|
# The job should be considered immutable but may be
|
|
348
392
|
# removed from the queue via JobRecord#delete.
|
|
349
393
|
class JobRecord
|
|
394
|
+
include ApiUtils
|
|
395
|
+
|
|
350
396
|
# the parsed Hash of job data
|
|
351
397
|
# @!attribute [r] Item
|
|
352
398
|
attr_reader :item
|
|
@@ -388,12 +434,7 @@ module Sidekiq
|
|
|
388
434
|
def display_class
|
|
389
435
|
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
|
390
436
|
@klass ||= self["display_class"] || begin
|
|
391
|
-
|
|
392
|
-
when /\ASidekiq::Extensions::Delayed/
|
|
393
|
-
safe_load(args[0], klass) do |target, method, _|
|
|
394
|
-
"#{target}.#{method}"
|
|
395
|
-
end
|
|
396
|
-
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
|
437
|
+
if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
|
|
397
438
|
job_class = @item["wrapped"] || args[0]
|
|
398
439
|
if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
|
|
399
440
|
# MailerClass#mailer_method
|
|
@@ -409,23 +450,14 @@ module Sidekiq
|
|
|
409
450
|
|
|
410
451
|
def display_args
|
|
411
452
|
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
|
412
|
-
@display_args ||=
|
|
413
|
-
|
|
414
|
-
safe_load(args[0], args) do |_, _, arg, kwarg|
|
|
415
|
-
if !kwarg || kwarg.empty?
|
|
416
|
-
arg
|
|
417
|
-
else
|
|
418
|
-
[arg, kwarg]
|
|
419
|
-
end
|
|
420
|
-
end
|
|
421
|
-
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
|
422
|
-
job_args = self["wrapped"] ? args[0]["arguments"] : []
|
|
453
|
+
@display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
|
|
454
|
+
job_args = self["wrapped"] ? deserialize_argument(args[0]["arguments"]) : []
|
|
423
455
|
if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
|
|
424
456
|
# remove MailerClass, mailer_method and 'deliver_now'
|
|
425
457
|
job_args.drop(3)
|
|
426
458
|
elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
|
|
427
459
|
# remove MailerClass, mailer_method and 'deliver_now'
|
|
428
|
-
job_args.drop(3).first
|
|
460
|
+
job_args.drop(3).first.values_at("params", "args")
|
|
429
461
|
else
|
|
430
462
|
job_args
|
|
431
463
|
end
|
|
@@ -446,12 +478,34 @@ module Sidekiq
|
|
|
446
478
|
self["jid"]
|
|
447
479
|
end
|
|
448
480
|
|
|
481
|
+
def iterable_state
|
|
482
|
+
@iterable_state ||= Sidekiq::IterableJobQuery.new(jid)[jid]
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def bid
|
|
486
|
+
self["bid"]
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def failed_at
|
|
490
|
+
if self["failed_at"]
|
|
491
|
+
time_from_timestamp(self["failed_at"])
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def retried_at
|
|
496
|
+
if self["retried_at"]
|
|
497
|
+
time_from_timestamp(self["retried_at"])
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
449
501
|
def enqueued_at
|
|
450
|
-
|
|
502
|
+
if self["enqueued_at"]
|
|
503
|
+
time_from_timestamp(self["enqueued_at"])
|
|
504
|
+
end
|
|
451
505
|
end
|
|
452
506
|
|
|
453
507
|
def created_at
|
|
454
|
-
|
|
508
|
+
time_from_timestamp(self["created_at"] || self["enqueued_at"] || 0)
|
|
455
509
|
end
|
|
456
510
|
|
|
457
511
|
def tags
|
|
@@ -469,8 +523,7 @@ module Sidekiq
|
|
|
469
523
|
end
|
|
470
524
|
|
|
471
525
|
def latency
|
|
472
|
-
|
|
473
|
-
now - (@item["enqueued_at"] || @item["created_at"] || now)
|
|
526
|
+
calculate_latency(@item)
|
|
474
527
|
end
|
|
475
528
|
|
|
476
529
|
# Remove this job from the queue
|
|
@@ -491,30 +544,41 @@ module Sidekiq
|
|
|
491
544
|
|
|
492
545
|
private
|
|
493
546
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
547
|
+
ACTIVE_JOB_PREFIX = "_aj_"
|
|
548
|
+
GLOBALID_KEY = "_aj_globalid"
|
|
549
|
+
|
|
550
|
+
def deserialize_argument(argument)
|
|
551
|
+
case argument
|
|
552
|
+
when Array
|
|
553
|
+
argument.map { |arg| deserialize_argument(arg) }
|
|
554
|
+
when Hash
|
|
555
|
+
if serialized_global_id?(argument)
|
|
556
|
+
argument[GLOBALID_KEY]
|
|
557
|
+
else
|
|
558
|
+
argument.transform_values { |v| deserialize_argument(v) }
|
|
559
|
+
.reject { |k, _| k.start_with?(ACTIVE_JOB_PREFIX) }
|
|
560
|
+
end
|
|
561
|
+
else
|
|
562
|
+
argument
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def serialized_global_id?(hash)
|
|
567
|
+
hash.size == 1 && hash.include?(GLOBALID_KEY)
|
|
502
568
|
end
|
|
503
569
|
|
|
504
570
|
def uncompress_backtrace(backtrace)
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
571
|
+
strict_base64_decoded = backtrace.unpack1("m")
|
|
572
|
+
uncompressed = Zlib::Inflate.inflate(strict_base64_decoded)
|
|
573
|
+
Sidekiq.load_json(uncompressed)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def time_from_timestamp(timestamp)
|
|
577
|
+
if timestamp.is_a?(Float)
|
|
578
|
+
# old format, timestamps were stored as fractional seconds since the epoch
|
|
579
|
+
Time.at(timestamp).utc
|
|
508
580
|
else
|
|
509
|
-
|
|
510
|
-
uncompressed = Zlib::Inflate.inflate(decoded)
|
|
511
|
-
begin
|
|
512
|
-
Sidekiq.load_json(uncompressed)
|
|
513
|
-
rescue
|
|
514
|
-
# Handle old jobs with marshalled backtrace format
|
|
515
|
-
# TODO Remove in 7.x
|
|
516
|
-
Marshal.load(uncompressed)
|
|
517
|
-
end
|
|
581
|
+
Time.at(timestamp / 1000, timestamp % 1000, :millisecond)
|
|
518
582
|
end
|
|
519
583
|
end
|
|
520
584
|
end
|
|
@@ -524,17 +588,24 @@ module Sidekiq
|
|
|
524
588
|
# could be the scheduled time for it to run (e.g. scheduled set),
|
|
525
589
|
# or the expiration date after which the entry should be deleted (e.g. dead set).
|
|
526
590
|
class SortedEntry < JobRecord
|
|
527
|
-
attr_reader :score
|
|
528
591
|
attr_reader :parent
|
|
529
592
|
|
|
530
593
|
# :nodoc:
|
|
531
594
|
# @api private
|
|
532
595
|
def initialize(parent, score, item)
|
|
533
596
|
super(item)
|
|
534
|
-
@score =
|
|
597
|
+
@score = score
|
|
535
598
|
@parent = parent
|
|
536
599
|
end
|
|
537
600
|
|
|
601
|
+
def score
|
|
602
|
+
Float(@score)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def id
|
|
606
|
+
"#{@score}|#{item["jid"]}"
|
|
607
|
+
end
|
|
608
|
+
|
|
538
609
|
# The timestamp associated with this entry
|
|
539
610
|
def at
|
|
540
611
|
Time.at(score).utc
|
|
@@ -545,7 +616,7 @@ module Sidekiq
|
|
|
545
616
|
if @value
|
|
546
617
|
@parent.delete_by_value(@parent.name, @value)
|
|
547
618
|
else
|
|
548
|
-
@parent.delete_by_jid(score, jid)
|
|
619
|
+
@parent.delete_by_jid(@score, jid)
|
|
549
620
|
end
|
|
550
621
|
end
|
|
551
622
|
|
|
@@ -554,8 +625,9 @@ module Sidekiq
|
|
|
554
625
|
# @param at [Time] the new timestamp for this job
|
|
555
626
|
def reschedule(at)
|
|
556
627
|
Sidekiq.redis do |conn|
|
|
557
|
-
conn.zincrby(@parent.name, at.to_f -
|
|
628
|
+
conn.zincrby(@parent.name, at.to_f - score, Sidekiq.dump_json(@item))
|
|
558
629
|
end
|
|
630
|
+
@score = at.to_f
|
|
559
631
|
end
|
|
560
632
|
|
|
561
633
|
# Enqueue this job from the scheduled or dead set so it will
|
|
@@ -590,38 +662,8 @@ module Sidekiq
|
|
|
590
662
|
|
|
591
663
|
private
|
|
592
664
|
|
|
593
|
-
def remove_job
|
|
594
|
-
|
|
595
|
-
results = conn.multi { |transaction|
|
|
596
|
-
transaction.zrangebyscore(parent.name, score, score)
|
|
597
|
-
transaction.zremrangebyscore(parent.name, score, score)
|
|
598
|
-
}.first
|
|
599
|
-
|
|
600
|
-
if results.size == 1
|
|
601
|
-
yield results.first
|
|
602
|
-
else
|
|
603
|
-
# multiple jobs with the same score
|
|
604
|
-
# find the one with the right JID and push it
|
|
605
|
-
matched, nonmatched = results.partition { |message|
|
|
606
|
-
if message.index(jid)
|
|
607
|
-
msg = Sidekiq.load_json(message)
|
|
608
|
-
msg["jid"] == jid
|
|
609
|
-
else
|
|
610
|
-
false
|
|
611
|
-
end
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
msg = matched.first
|
|
615
|
-
yield msg if msg
|
|
616
|
-
|
|
617
|
-
# push the rest back onto the sorted set
|
|
618
|
-
conn.multi do |transaction|
|
|
619
|
-
nonmatched.each do |message|
|
|
620
|
-
transaction.zadd(parent.name, score.to_f.to_s, message)
|
|
621
|
-
end
|
|
622
|
-
end
|
|
623
|
-
end
|
|
624
|
-
end
|
|
665
|
+
def remove_job(&)
|
|
666
|
+
parent.remove_job(self, &)
|
|
625
667
|
end
|
|
626
668
|
end
|
|
627
669
|
|
|
@@ -656,7 +698,7 @@ module Sidekiq
|
|
|
656
698
|
|
|
657
699
|
match = "*#{match}*" unless match.include?("*")
|
|
658
700
|
Sidekiq.redis do |conn|
|
|
659
|
-
conn.
|
|
701
|
+
conn.zscan(name, match: match, count: count) do |entry, score|
|
|
660
702
|
yield SortedEntry.new(self, score, entry)
|
|
661
703
|
end
|
|
662
704
|
end
|
|
@@ -691,6 +733,41 @@ module Sidekiq
|
|
|
691
733
|
end
|
|
692
734
|
end
|
|
693
735
|
|
|
736
|
+
def pop_each
|
|
737
|
+
Sidekiq.redis do |c|
|
|
738
|
+
size.times do
|
|
739
|
+
data, score = c.zpopmin(name, 1)&.first
|
|
740
|
+
break unless data
|
|
741
|
+
yield data, score
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def retry_all
|
|
747
|
+
c = Sidekiq::Client.new
|
|
748
|
+
pop_each do |msg, _|
|
|
749
|
+
job = Sidekiq.load_json(msg)
|
|
750
|
+
# Manual retries should not count against the retry limit.
|
|
751
|
+
job["retry_count"] -= 1 if job["retry_count"]
|
|
752
|
+
c.push(job)
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# Move all jobs from this Set to the Dead Set.
|
|
757
|
+
# See DeadSet#kill
|
|
758
|
+
def kill_all(notify_failure: false, ex: nil)
|
|
759
|
+
ds = DeadSet.new
|
|
760
|
+
opts = {notify_failure: notify_failure, ex: ex, trim: false}
|
|
761
|
+
|
|
762
|
+
begin
|
|
763
|
+
pop_each do |msg, _|
|
|
764
|
+
ds.kill(msg, opts)
|
|
765
|
+
end
|
|
766
|
+
ensure
|
|
767
|
+
ds.trim
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
|
|
694
771
|
def each
|
|
695
772
|
initial_size = @_size
|
|
696
773
|
offset_size = 0
|
|
@@ -701,7 +778,7 @@ module Sidekiq
|
|
|
701
778
|
range_start = page * page_size + offset_size
|
|
702
779
|
range_end = range_start + page_size - 1
|
|
703
780
|
elements = Sidekiq.redis { |conn|
|
|
704
|
-
conn.zrange name, range_start, range_end, withscores
|
|
781
|
+
conn.zrange name, range_start, range_end, "withscores"
|
|
705
782
|
}
|
|
706
783
|
break if elements.empty?
|
|
707
784
|
page -= 1
|
|
@@ -728,7 +805,7 @@ module Sidekiq
|
|
|
728
805
|
end
|
|
729
806
|
|
|
730
807
|
elements = Sidekiq.redis { |conn|
|
|
731
|
-
conn.
|
|
808
|
+
conn.zrange(name, begin_score, end_score, "BYSCORE", "withscores")
|
|
732
809
|
}
|
|
733
810
|
|
|
734
811
|
elements.each_with_object([]) do |element, result|
|
|
@@ -746,8 +823,8 @@ module Sidekiq
|
|
|
746
823
|
# @return [SortedEntry] the record or nil
|
|
747
824
|
def find_job(jid)
|
|
748
825
|
Sidekiq.redis do |conn|
|
|
749
|
-
conn.
|
|
750
|
-
job =
|
|
826
|
+
conn.zscan(name, match: "*#{jid}*", count: 100) do |entry, score|
|
|
827
|
+
job = Sidekiq.load_json(entry)
|
|
751
828
|
matched = job["jid"] == jid
|
|
752
829
|
return SortedEntry.new(self, score, entry) if matched
|
|
753
830
|
end
|
|
@@ -755,6 +832,46 @@ module Sidekiq
|
|
|
755
832
|
nil
|
|
756
833
|
end
|
|
757
834
|
|
|
835
|
+
def remove_job(entry)
|
|
836
|
+
score = entry.score
|
|
837
|
+
jid = entry.jid
|
|
838
|
+
Sidekiq.redis do |conn|
|
|
839
|
+
results = conn.multi { |transaction|
|
|
840
|
+
transaction.zrange(name, score, score, "BYSCORE")
|
|
841
|
+
transaction.zremrangebyscore(name, score, score)
|
|
842
|
+
}.first
|
|
843
|
+
|
|
844
|
+
if results.size == 1
|
|
845
|
+
yield results.first
|
|
846
|
+
@_size -= 1
|
|
847
|
+
else
|
|
848
|
+
# multiple jobs with the same score
|
|
849
|
+
# find the one with the right JID and push it
|
|
850
|
+
matched, nonmatched = results.partition { |message|
|
|
851
|
+
if message.index(jid)
|
|
852
|
+
msg = Sidekiq.load_json(message)
|
|
853
|
+
msg["jid"] == jid
|
|
854
|
+
else
|
|
855
|
+
false
|
|
856
|
+
end
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
msg = matched.first
|
|
860
|
+
if msg
|
|
861
|
+
yield msg
|
|
862
|
+
@_size -= 1
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
# push the rest back onto the sorted set
|
|
866
|
+
conn.multi do |transaction|
|
|
867
|
+
nonmatched.each do |message|
|
|
868
|
+
transaction.zadd(name, score.to_f.to_s, message)
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
end
|
|
874
|
+
|
|
758
875
|
# :nodoc:
|
|
759
876
|
# @api private
|
|
760
877
|
def delete_by_value(name, value)
|
|
@@ -769,7 +886,7 @@ module Sidekiq
|
|
|
769
886
|
# @api private
|
|
770
887
|
def delete_by_jid(score, jid)
|
|
771
888
|
Sidekiq.redis do |conn|
|
|
772
|
-
elements = conn.
|
|
889
|
+
elements = conn.zrange(name, score, score, "BYSCORE")
|
|
773
890
|
elements.each do |element|
|
|
774
891
|
if element.index(jid)
|
|
775
892
|
message = Sidekiq.load_json(element)
|
|
@@ -788,47 +905,21 @@ module Sidekiq
|
|
|
788
905
|
|
|
789
906
|
##
|
|
790
907
|
# The set of scheduled jobs within Sidekiq.
|
|
791
|
-
#
|
|
792
|
-
# example where I'm selecting jobs based on some complex logic
|
|
793
|
-
# and deleting them from the scheduled set.
|
|
908
|
+
# See the API wiki page for usage notes and examples.
|
|
794
909
|
#
|
|
795
|
-
# r = Sidekiq::ScheduledSet.new
|
|
796
|
-
# r.select do |scheduled|
|
|
797
|
-
# scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
|
|
798
|
-
# scheduled.args[0] == 'User' &&
|
|
799
|
-
# scheduled.args[1] == 'setup_new_subscriber'
|
|
800
|
-
# end.map(&:delete)
|
|
801
910
|
class ScheduledSet < JobSet
|
|
802
911
|
def initialize
|
|
803
|
-
super
|
|
912
|
+
super("schedule")
|
|
804
913
|
end
|
|
805
914
|
end
|
|
806
915
|
|
|
807
916
|
##
|
|
808
917
|
# The set of retries within Sidekiq.
|
|
809
|
-
#
|
|
810
|
-
# example where I'm selecting all jobs of a certain type
|
|
811
|
-
# and deleting them from the retry queue.
|
|
918
|
+
# See the API wiki page for usage notes and examples.
|
|
812
919
|
#
|
|
813
|
-
# r = Sidekiq::RetrySet.new
|
|
814
|
-
# r.select do |retri|
|
|
815
|
-
# retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
|
|
816
|
-
# retri.args[0] == 'User' &&
|
|
817
|
-
# retri.args[1] == 'setup_new_subscriber'
|
|
818
|
-
# end.map(&:delete)
|
|
819
920
|
class RetrySet < JobSet
|
|
820
921
|
def initialize
|
|
821
|
-
super
|
|
822
|
-
end
|
|
823
|
-
|
|
824
|
-
# Enqueues all jobs pending within the retry set.
|
|
825
|
-
def retry_all
|
|
826
|
-
each(&:retry) while size > 0
|
|
827
|
-
end
|
|
828
|
-
|
|
829
|
-
# Kills all jobs pending within the retry set.
|
|
830
|
-
def kill_all
|
|
831
|
-
each(&:kill) while size > 0
|
|
922
|
+
super("retry")
|
|
832
923
|
end
|
|
833
924
|
end
|
|
834
925
|
|
|
@@ -839,48 +930,48 @@ module Sidekiq
|
|
|
839
930
|
#
|
|
840
931
|
class DeadSet < JobSet
|
|
841
932
|
def initialize
|
|
842
|
-
super
|
|
933
|
+
super("dead")
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
# Trim dead jobs which are over our storage limits
|
|
937
|
+
def trim
|
|
938
|
+
hash = Sidekiq.default_configuration
|
|
939
|
+
now = Time.now.to_f
|
|
940
|
+
Sidekiq.redis do |conn|
|
|
941
|
+
conn.multi do |transaction|
|
|
942
|
+
transaction.zremrangebyscore(name, "-inf", now - hash[:dead_timeout_in_seconds])
|
|
943
|
+
transaction.zremrangebyrank(name, 0, - hash[:dead_max_jobs])
|
|
944
|
+
end
|
|
945
|
+
end
|
|
843
946
|
end
|
|
844
947
|
|
|
845
948
|
# Add the given job to the Dead set.
|
|
846
949
|
# @param message [String] the job data as JSON
|
|
950
|
+
# @option opts [Boolean] :notify_failure (true) Whether death handlers should be called
|
|
951
|
+
# @option opts [Boolean] :trim (true) Whether Sidekiq should trim the structure to keep it within configuration
|
|
952
|
+
# @option opts [Exception] :ex (RuntimeError) An exception to pass to the death handlers
|
|
847
953
|
def kill(message, opts = {})
|
|
848
954
|
now = Time.now.to_f
|
|
849
955
|
Sidekiq.redis do |conn|
|
|
850
|
-
conn.
|
|
851
|
-
transaction.zadd(name, now.to_s, message)
|
|
852
|
-
transaction.zremrangebyscore(name, "-inf", now - self.class.timeout)
|
|
853
|
-
transaction.zremrangebyrank(name, 0, - self.class.max_jobs)
|
|
854
|
-
end
|
|
956
|
+
conn.zadd(name, now.to_s, message)
|
|
855
957
|
end
|
|
856
958
|
|
|
959
|
+
trim if opts[:trim] != false
|
|
960
|
+
|
|
857
961
|
if opts[:notify_failure] != false
|
|
858
962
|
job = Sidekiq.load_json(message)
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
963
|
+
if opts[:ex]
|
|
964
|
+
ex = opts[:ex]
|
|
965
|
+
else
|
|
966
|
+
ex = RuntimeError.new("Job killed by API")
|
|
967
|
+
ex.set_backtrace(caller)
|
|
968
|
+
end
|
|
969
|
+
Sidekiq.default_configuration.death_handlers.each do |handle|
|
|
970
|
+
handle.call(job, ex)
|
|
863
971
|
end
|
|
864
972
|
end
|
|
865
973
|
true
|
|
866
974
|
end
|
|
867
|
-
|
|
868
|
-
# Enqueue all dead jobs
|
|
869
|
-
def retry_all
|
|
870
|
-
each(&:retry) while size > 0
|
|
871
|
-
end
|
|
872
|
-
|
|
873
|
-
# The maximum size of the Dead set. Older entries will be trimmed
|
|
874
|
-
# to stay within this limit. Default value is 10,000.
|
|
875
|
-
def self.max_jobs
|
|
876
|
-
Sidekiq[:dead_max_jobs]
|
|
877
|
-
end
|
|
878
|
-
|
|
879
|
-
# The time limit for entries within the Dead set. Older entries will be thrown away.
|
|
880
|
-
# Default value is six months.
|
|
881
|
-
def self.timeout
|
|
882
|
-
Sidekiq[:dead_timeout_in_seconds]
|
|
883
|
-
end
|
|
884
975
|
end
|
|
885
976
|
|
|
886
977
|
##
|
|
@@ -893,6 +984,24 @@ module Sidekiq
|
|
|
893
984
|
class ProcessSet
|
|
894
985
|
include Enumerable
|
|
895
986
|
|
|
987
|
+
def self.[](identity)
|
|
988
|
+
exists, (info, busy, beat, quiet, rss, rtt_us) = Sidekiq.redis { |conn|
|
|
989
|
+
conn.multi { |transaction|
|
|
990
|
+
transaction.sismember("processes", identity)
|
|
991
|
+
transaction.hmget(identity, "info", "busy", "beat", "quiet", "rss", "rtt_us")
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return nil if exists == 0 || info.nil?
|
|
996
|
+
|
|
997
|
+
hash = Sidekiq.load_json(info)
|
|
998
|
+
Process.new(hash.merge("busy" => busy.to_i,
|
|
999
|
+
"beat" => beat.to_f,
|
|
1000
|
+
"quiet" => quiet,
|
|
1001
|
+
"rss" => rss.to_i,
|
|
1002
|
+
"rtt_us" => rtt_us.to_i))
|
|
1003
|
+
end
|
|
1004
|
+
|
|
896
1005
|
# :nodoc:
|
|
897
1006
|
# @api private
|
|
898
1007
|
def initialize(clean_plz = true)
|
|
@@ -905,11 +1014,11 @@ module Sidekiq
|
|
|
905
1014
|
# @api private
|
|
906
1015
|
def cleanup
|
|
907
1016
|
# dont run cleanup more than once per minute
|
|
908
|
-
return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1",
|
|
1017
|
+
return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", "NX", "EX", "60") }
|
|
909
1018
|
|
|
910
1019
|
count = 0
|
|
911
1020
|
Sidekiq.redis do |conn|
|
|
912
|
-
procs = conn.
|
|
1021
|
+
procs = conn.sscan("processes").to_a
|
|
913
1022
|
heartbeats = conn.pipelined { |pipeline|
|
|
914
1023
|
procs.each do |key|
|
|
915
1024
|
pipeline.hget(key, "info")
|
|
@@ -929,30 +1038,31 @@ module Sidekiq
|
|
|
929
1038
|
|
|
930
1039
|
def each
|
|
931
1040
|
result = Sidekiq.redis { |conn|
|
|
932
|
-
procs = conn.
|
|
1041
|
+
procs = conn.sscan("processes").to_a.sort
|
|
933
1042
|
|
|
934
1043
|
# We're making a tradeoff here between consuming more memory instead of
|
|
935
1044
|
# making more roundtrips to Redis, but if you have hundreds or thousands of workers,
|
|
936
1045
|
# you'll be happier this way
|
|
937
1046
|
conn.pipelined do |pipeline|
|
|
938
1047
|
procs.each do |key|
|
|
939
|
-
pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
|
|
1048
|
+
pipeline.hmget(key, "info", "concurrency", "busy", "beat", "quiet", "rss", "rtt_us")
|
|
940
1049
|
end
|
|
941
1050
|
end
|
|
942
1051
|
}
|
|
943
1052
|
|
|
944
|
-
result.each do |info, busy,
|
|
1053
|
+
result.each do |info, concurrency, busy, beat, quiet, rss, rtt_us|
|
|
945
1054
|
# If a process is stopped between when we query Redis for `procs` and
|
|
946
1055
|
# when we query for `result`, we will have an item in `result` that is
|
|
947
1056
|
# composed of `nil` values.
|
|
948
1057
|
next if info.nil?
|
|
949
1058
|
|
|
950
1059
|
hash = Sidekiq.load_json(info)
|
|
951
|
-
yield Process.new(hash.merge("
|
|
952
|
-
"
|
|
1060
|
+
yield Process.new(hash.merge("concurrency" => concurrency.to_i,
|
|
1061
|
+
"busy" => busy.to_i,
|
|
1062
|
+
"beat" => beat.to_f,
|
|
953
1063
|
"quiet" => quiet,
|
|
954
1064
|
"rss" => rss.to_i,
|
|
955
|
-
"rtt_us" =>
|
|
1065
|
+
"rtt_us" => rtt_us.to_i))
|
|
956
1066
|
end
|
|
957
1067
|
end
|
|
958
1068
|
|
|
@@ -1003,11 +1113,12 @@ module Sidekiq
|
|
|
1003
1113
|
# 'started_at' => <process start time>,
|
|
1004
1114
|
# 'pid' => 12345,
|
|
1005
1115
|
# 'tag' => 'myapp'
|
|
1006
|
-
# 'concurrency' =>
|
|
1007
|
-
# '
|
|
1008
|
-
# 'busy' =>
|
|
1116
|
+
# 'concurrency' => 5,
|
|
1117
|
+
# 'capsules' => {"default" => {"mode" => "weighted", "concurrency" => 5, "weights" => {"default" => 2, "low" => 1}}},
|
|
1118
|
+
# 'busy' => 3,
|
|
1009
1119
|
# 'beat' => <last heartbeat>,
|
|
1010
1120
|
# 'identity' => <unique string identifying the process>,
|
|
1121
|
+
# 'embedded' => true,
|
|
1011
1122
|
# }
|
|
1012
1123
|
class Process
|
|
1013
1124
|
# :nodoc:
|
|
@@ -1021,7 +1132,7 @@ module Sidekiq
|
|
|
1021
1132
|
end
|
|
1022
1133
|
|
|
1023
1134
|
def labels
|
|
1024
|
-
|
|
1135
|
+
self["labels"].to_a
|
|
1025
1136
|
end
|
|
1026
1137
|
|
|
1027
1138
|
def [](key)
|
|
@@ -1031,9 +1142,45 @@ module Sidekiq
|
|
|
1031
1142
|
def identity
|
|
1032
1143
|
self["identity"]
|
|
1033
1144
|
end
|
|
1145
|
+
alias_method :id, :identity
|
|
1034
1146
|
|
|
1147
|
+
# deprecated, use capsules below
|
|
1035
1148
|
def queues
|
|
1036
|
-
|
|
1149
|
+
# Backwards compatibility with <8.0.8
|
|
1150
|
+
if !self["capsules"]
|
|
1151
|
+
self["queues"]
|
|
1152
|
+
else
|
|
1153
|
+
capsules.values.flat_map { |x| x["weights"].keys }.uniq
|
|
1154
|
+
end
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
# deprecated, use capsules below
|
|
1158
|
+
def weights
|
|
1159
|
+
# Backwards compatibility with <8.0.8
|
|
1160
|
+
if !self["capsules"]
|
|
1161
|
+
self["weights"]
|
|
1162
|
+
else
|
|
1163
|
+
hash = {}
|
|
1164
|
+
capsules.values.each do |cap|
|
|
1165
|
+
# Note: will lose data if two capsules are processing the same named queue
|
|
1166
|
+
cap["weights"].each_pair do |queue, weight|
|
|
1167
|
+
hash[queue] = weight
|
|
1168
|
+
end
|
|
1169
|
+
end
|
|
1170
|
+
hash
|
|
1171
|
+
end
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
def capsules
|
|
1175
|
+
self["capsules"]
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
def version
|
|
1179
|
+
self["version"]
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
def embedded?
|
|
1183
|
+
self["embedded"]
|
|
1037
1184
|
end
|
|
1038
1185
|
|
|
1039
1186
|
# Signal this process to stop processing new jobs.
|
|
@@ -1041,6 +1188,8 @@ module Sidekiq
|
|
|
1041
1188
|
# This method is *asynchronous* and it can take 5-10
|
|
1042
1189
|
# seconds for the process to quiet.
|
|
1043
1190
|
def quiet!
|
|
1191
|
+
raise "Can't quiet an embedded process" if embedded?
|
|
1192
|
+
|
|
1044
1193
|
signal("TSTP")
|
|
1045
1194
|
end
|
|
1046
1195
|
|
|
@@ -1049,6 +1198,8 @@ module Sidekiq
|
|
|
1049
1198
|
# This method is *asynchronous* and it can take 5-10
|
|
1050
1199
|
# seconds for the process to start shutting down.
|
|
1051
1200
|
def stop!
|
|
1201
|
+
raise "Can't stop an embedded process" if embedded?
|
|
1202
|
+
|
|
1052
1203
|
signal("TERM")
|
|
1053
1204
|
end
|
|
1054
1205
|
|
|
@@ -1065,6 +1216,10 @@ module Sidekiq
|
|
|
1065
1216
|
self["quiet"] == "true"
|
|
1066
1217
|
end
|
|
1067
1218
|
|
|
1219
|
+
def leader?
|
|
1220
|
+
Sidekiq.redis { |c| c.get("dear-leader") == identity }
|
|
1221
|
+
end
|
|
1222
|
+
|
|
1068
1223
|
private
|
|
1069
1224
|
|
|
1070
1225
|
def signal(sig)
|
|
@@ -1093,9 +1248,8 @@ module Sidekiq
|
|
|
1093
1248
|
# works.each do |process_id, thread_id, work|
|
|
1094
1249
|
# # process_id is a unique identifier per Sidekiq process
|
|
1095
1250
|
# # thread_id is a unique identifier per thread
|
|
1096
|
-
# # work is a
|
|
1097
|
-
# #
|
|
1098
|
-
# # run_at is an epoch Integer.
|
|
1251
|
+
# # work is a `Sidekiq::Work` instance that has the following accessor methods.
|
|
1252
|
+
# # [work.queue, work.run_at, work.payload]
|
|
1099
1253
|
# end
|
|
1100
1254
|
#
|
|
1101
1255
|
class WorkSet
|
|
@@ -1107,8 +1261,7 @@ module Sidekiq
|
|
|
1107
1261
|
all_works = nil
|
|
1108
1262
|
|
|
1109
1263
|
Sidekiq.redis do |conn|
|
|
1110
|
-
procs = conn.
|
|
1111
|
-
|
|
1264
|
+
procs = conn.sscan("processes").to_a.sort
|
|
1112
1265
|
all_works = conn.pipelined do |pipeline|
|
|
1113
1266
|
procs.each do |key|
|
|
1114
1267
|
pipeline.hgetall("#{key}:work")
|
|
@@ -1118,17 +1271,11 @@ module Sidekiq
|
|
|
1118
1271
|
|
|
1119
1272
|
procs.zip(all_works).each do |key, workers|
|
|
1120
1273
|
workers.each_pair do |tid, json|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
hsh = Sidekiq.load_json(json)
|
|
1124
|
-
p = hsh["payload"]
|
|
1125
|
-
# avoid breaking API, this is a side effect of the JSON optimization in #4316
|
|
1126
|
-
hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
|
|
1127
|
-
results << [key, tid, hsh]
|
|
1274
|
+
results << [key, tid, Sidekiq::Work.new(key, tid, Sidekiq.load_json(json))] unless json.empty?
|
|
1128
1275
|
end
|
|
1129
1276
|
end
|
|
1130
1277
|
|
|
1131
|
-
results.sort_by { |(_, _,
|
|
1278
|
+
results.sort_by { |(_, _, work)| work.run_at }.each(&block)
|
|
1132
1279
|
end
|
|
1133
1280
|
|
|
1134
1281
|
# Note that #size is only as accurate as Sidekiq's heartbeat,
|
|
@@ -1139,7 +1286,7 @@ module Sidekiq
|
|
|
1139
1286
|
# which can easily get out of sync with crashy processes.
|
|
1140
1287
|
def size
|
|
1141
1288
|
Sidekiq.redis do |conn|
|
|
1142
|
-
procs = conn.
|
|
1289
|
+
procs = conn.sscan("processes").to_a
|
|
1143
1290
|
if procs.empty?
|
|
1144
1291
|
0
|
|
1145
1292
|
else
|
|
@@ -1151,9 +1298,170 @@ module Sidekiq
|
|
|
1151
1298
|
end
|
|
1152
1299
|
end
|
|
1153
1300
|
end
|
|
1301
|
+
|
|
1302
|
+
##
|
|
1303
|
+
# Find the work which represents a job with the given JID.
|
|
1304
|
+
# *This is a slow O(n) operation*. Do not use for app logic.
|
|
1305
|
+
#
|
|
1306
|
+
# @param jid [String] the job identifier
|
|
1307
|
+
# @return [Sidekiq::Work] the work or nil
|
|
1308
|
+
def find_work(jid)
|
|
1309
|
+
each do |_process_id, _thread_id, work|
|
|
1310
|
+
job = work.job
|
|
1311
|
+
return work if job.jid == jid
|
|
1312
|
+
end
|
|
1313
|
+
nil
|
|
1314
|
+
end
|
|
1315
|
+
alias_method :find_work_by_jid, :find_work
|
|
1154
1316
|
end
|
|
1317
|
+
|
|
1318
|
+
# Sidekiq::Work represents a job which is currently executing.
|
|
1319
|
+
class Work
|
|
1320
|
+
attr_reader :process_id
|
|
1321
|
+
attr_reader :thread_id
|
|
1322
|
+
|
|
1323
|
+
def initialize(pid, tid, hsh)
|
|
1324
|
+
@process_id = pid
|
|
1325
|
+
@thread_id = tid
|
|
1326
|
+
@hsh = hsh
|
|
1327
|
+
@job = nil
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
def queue
|
|
1331
|
+
@hsh["queue"]
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
def run_at
|
|
1335
|
+
Time.at(@hsh["run_at"])
|
|
1336
|
+
end
|
|
1337
|
+
|
|
1338
|
+
def job
|
|
1339
|
+
@job ||= Sidekiq::JobRecord.new(@hsh["payload"])
|
|
1340
|
+
end
|
|
1341
|
+
|
|
1342
|
+
def payload
|
|
1343
|
+
@hsh["payload"]
|
|
1344
|
+
end
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1155
1347
|
# Since "worker" is a nebulous term, we've deprecated the use of this class name.
|
|
1156
1348
|
# Is "worker" a process, a type of job, a thread? Undefined!
|
|
1157
1349
|
# WorkSet better describes the data.
|
|
1158
1350
|
Workers = WorkSet
|
|
1351
|
+
|
|
1352
|
+
class ProfileSet
|
|
1353
|
+
include Enumerable
|
|
1354
|
+
|
|
1355
|
+
# This is a point in time/snapshot API, you'll need to instantiate a new instance
|
|
1356
|
+
# if you want to fetch newer records.
|
|
1357
|
+
def initialize
|
|
1358
|
+
@records = Sidekiq.redis do |c|
|
|
1359
|
+
# This throws away expired profiles
|
|
1360
|
+
c.zremrangebyscore("profiles", "-inf", Time.now.to_f.to_s)
|
|
1361
|
+
# retreive records, newest to oldest
|
|
1362
|
+
c.zrange("profiles", "+inf", 0, "byscore", "rev")
|
|
1363
|
+
end
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1366
|
+
def size
|
|
1367
|
+
@records.size
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
def each(&block)
|
|
1371
|
+
fetch_keys = %w[started_at jid type token size elapsed].freeze
|
|
1372
|
+
arrays = Sidekiq.redis do |c|
|
|
1373
|
+
c.pipelined do |p|
|
|
1374
|
+
@records.each do |key|
|
|
1375
|
+
p.hmget(key, *fetch_keys)
|
|
1376
|
+
end
|
|
1377
|
+
end
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
arrays.compact.map { |arr| ProfileRecord.new(arr) }.each(&block)
|
|
1381
|
+
end
|
|
1382
|
+
end
|
|
1383
|
+
|
|
1384
|
+
class ProfileRecord
|
|
1385
|
+
attr_reader :started_at, :jid, :type, :token, :size, :elapsed
|
|
1386
|
+
|
|
1387
|
+
def initialize(arr)
|
|
1388
|
+
# Must be same order as fetch_keys above
|
|
1389
|
+
@started_at = Time.at(Integer(arr[0]))
|
|
1390
|
+
@jid = arr[1]
|
|
1391
|
+
@type = arr[2]
|
|
1392
|
+
@token = arr[3]
|
|
1393
|
+
@size = Integer(arr[4])
|
|
1394
|
+
@elapsed = Float(arr[5])
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
def key
|
|
1398
|
+
"#{token}-#{jid}"
|
|
1399
|
+
end
|
|
1400
|
+
|
|
1401
|
+
def data
|
|
1402
|
+
Sidekiq.redis { |c| c.hget(key, "data") }
|
|
1403
|
+
end
|
|
1404
|
+
end
|
|
1405
|
+
|
|
1406
|
+
# Persisted iteration state from Redis for jobs using Sidekiq::IterableJob.
|
|
1407
|
+
class IterableJobQuery
|
|
1408
|
+
def initialize(jids)
|
|
1409
|
+
@cache = bulk_fetch(jids)
|
|
1410
|
+
end
|
|
1411
|
+
|
|
1412
|
+
def [](jid)
|
|
1413
|
+
@cache[jid]
|
|
1414
|
+
end
|
|
1415
|
+
|
|
1416
|
+
private
|
|
1417
|
+
|
|
1418
|
+
# Bulk-fetch iteration state for multiple JIDs in a single Redis pipeline.
|
|
1419
|
+
# Returns a Hash of { jid => State } for JIDs that have iteration state.
|
|
1420
|
+
def bulk_fetch(jids)
|
|
1421
|
+
raise ArgumentError unless jids
|
|
1422
|
+
jids_to_fetch = Array(jids).compact.uniq
|
|
1423
|
+
return {} if jids_to_fetch.empty?
|
|
1424
|
+
|
|
1425
|
+
results = Sidekiq.redis do |conn|
|
|
1426
|
+
conn.pipelined do |pipe|
|
|
1427
|
+
jids_to_fetch.each { |jid| pipe.hgetall("it-#{jid}") }
|
|
1428
|
+
end
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
# TODO Requires Ruby 4
|
|
1432
|
+
# states = ::Hash.new(capacity: jids_to_fetch.size)
|
|
1433
|
+
states = {}
|
|
1434
|
+
jids_to_fetch.each_with_index do |jid, i|
|
|
1435
|
+
raw = results[i]
|
|
1436
|
+
next if raw.nil? || raw.empty?
|
|
1437
|
+
|
|
1438
|
+
states[jid] = State.new(jid, raw)
|
|
1439
|
+
end
|
|
1440
|
+
states
|
|
1441
|
+
end
|
|
1442
|
+
|
|
1443
|
+
State = Struct.new(:jid, :raw) do
|
|
1444
|
+
def executions
|
|
1445
|
+
raw["ex"].to_i
|
|
1446
|
+
end
|
|
1447
|
+
|
|
1448
|
+
def runtime
|
|
1449
|
+
raw["rt"].to_f
|
|
1450
|
+
end
|
|
1451
|
+
|
|
1452
|
+
def cursor
|
|
1453
|
+
@cursor ||= begin
|
|
1454
|
+
Sidekiq.load_json(raw["c"])
|
|
1455
|
+
rescue JSON::ParserError
|
|
1456
|
+
raw["c"]
|
|
1457
|
+
end
|
|
1458
|
+
end
|
|
1459
|
+
|
|
1460
|
+
def cancelled
|
|
1461
|
+
raw["cancelled"]&.to_i
|
|
1462
|
+
end
|
|
1463
|
+
end
|
|
1464
|
+
end
|
|
1159
1465
|
end
|
|
1466
|
+
|
|
1467
|
+
Sidekiq.loader.run_load_hooks(:api)
|