sidekiq 6.2.2 → 8.1.5
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 +726 -11
- data/LICENSE.txt +9 -0
- data/README.md +70 -39
- data/bin/kiq +17 -0
- data/bin/lint-herb +13 -0
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiq +4 -9
- data/bin/sidekiqload +214 -115
- data/bin/sidekiqmon +4 -1
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
- data/lib/generators/sidekiq/job_generator.rb +71 -0
- data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +3 -3
- 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 +729 -264
- data/lib/sidekiq/capsule.rb +135 -0
- data/lib/sidekiq/cli.rb +124 -100
- data/lib/sidekiq/client.rb +153 -106
- data/lib/sidekiq/component.rb +132 -0
- data/lib/sidekiq/config.rb +320 -0
- data/lib/sidekiq/deploy.rb +64 -0
- data/lib/sidekiq/embedded.rb +64 -0
- data/lib/sidekiq/fetch.rb +27 -26
- 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 -5
- data/lib/sidekiq/job_logger.rb +23 -32
- data/lib/sidekiq/job_retry.rb +141 -68
- data/lib/sidekiq/job_util.rb +113 -0
- data/lib/sidekiq/launcher.rb +122 -98
- data/lib/sidekiq/loader.rb +57 -0
- data/lib/sidekiq/logger.rb +27 -106
- data/lib/sidekiq/manager.rb +41 -43
- data/lib/sidekiq/metrics/query.rb +184 -0
- data/lib/sidekiq/metrics/shared.rb +109 -0
- data/lib/sidekiq/metrics/tracking.rb +153 -0
- data/lib/sidekiq/middleware/chain.rb +96 -51
- data/lib/sidekiq/middleware/current_attributes.rb +120 -0
- data/lib/sidekiq/middleware/i18n.rb +8 -4
- data/lib/sidekiq/middleware/modules.rb +23 -0
- data/lib/sidekiq/monitor.rb +16 -6
- data/lib/sidekiq/paginator.rb +37 -10
- data/lib/sidekiq/processor.rb +105 -87
- data/lib/sidekiq/profiler.rb +73 -0
- data/lib/sidekiq/rails.rb +49 -36
- data/lib/sidekiq/redis_client_adapter.rb +117 -0
- data/lib/sidekiq/redis_connection.rb +55 -86
- data/lib/sidekiq/ring_buffer.rb +32 -0
- data/lib/sidekiq/scheduled.rb +106 -50
- 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 +59 -0
- data/lib/sidekiq/tui/controls.rb +53 -0
- data/lib/sidekiq/tui/filtering.rb +53 -0
- data/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
- data/lib/sidekiq/tui/tabs/busy.rb +118 -0
- data/lib/sidekiq/tui/tabs/dead.rb +19 -0
- data/lib/sidekiq/tui/tabs/home.rb +144 -0
- data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
- data/lib/sidekiq/tui/tabs/queues.rb +95 -0
- data/lib/sidekiq/tui/tabs/retries.rb +19 -0
- data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
- data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
- data/lib/sidekiq/tui/tabs.rb +15 -0
- data/lib/sidekiq/tui.rb +382 -0
- data/lib/sidekiq/version.rb +6 -1
- data/lib/sidekiq/web/action.rb +149 -64
- data/lib/sidekiq/web/application.rb +376 -268
- data/lib/sidekiq/web/config.rb +117 -0
- data/lib/sidekiq/web/helpers.rb +213 -87
- data/lib/sidekiq/web/router.rb +61 -74
- data/lib/sidekiq/web.rb +71 -100
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +95 -196
- data/sidekiq.gemspec +14 -11
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status.png +0 -0
- data/web/assets/javascripts/application.js +171 -57
- data/web/assets/javascripts/base-charts.js +120 -0
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/web/assets/javascripts/dashboard-charts.js +194 -0
- data/web/assets/javascripts/dashboard.js +41 -274
- data/web/assets/javascripts/metrics.js +280 -0
- 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 +45 -24
- data/web/locales/en.yml +93 -69
- 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 +61 -59
- data/web/locales/it.yml +94 -54
- data/web/locales/ja.yml +74 -68
- 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} +85 -56
- 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 +62 -60
- 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} +44 -9
- 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 +151 -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 +130 -61
- data/LICENSE +0 -9
- data/lib/generators/sidekiq/worker_generator.rb +0 -57
- data/lib/sidekiq/delay.rb +0 -41
- data/lib/sidekiq/exception_handler.rb +0 -27
- 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/util.rb +0 -95
- data/lib/sidekiq/web/csrf_protection.rb +0 -180
- data/lib/sidekiq/worker.rb +0 -244
- data/web/assets/stylesheets/application-dark.css +0 -147
- data/web/assets/stylesheets/application-rtl.css +0 -246
- data/web/assets/stylesheets/application.css +0 -1053
- 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 -68
- 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 -7
- data/web/views/_status.erb +0 -4
- data/web/views/_summary.erb +0 -40
- data/web/views/busy.erb +0 -132
- data/web/views/dashboard.erb +0 -83
- data/web/views/dead.erb +0 -34
- data/web/views/layout.erb +0 -42
- data/web/views/morgue.erb +0 -78
- data/web/views/queue.erb +0 -55
- 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/lib/sidekiq/api.rb
CHANGED
|
@@ -1,12 +1,53 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "sidekiq"
|
|
4
|
-
|
|
5
3
|
require "zlib"
|
|
6
|
-
|
|
4
|
+
|
|
5
|
+
require "sidekiq"
|
|
6
|
+
require "sidekiq/metrics/query"
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# Sidekiq's Data API provides a Ruby object model on top
|
|
10
|
+
# of Sidekiq's runtime data in Redis. This API should never
|
|
11
|
+
# be used within application code for business logic.
|
|
12
|
+
#
|
|
13
|
+
# The Sidekiq server process never uses this API: all data
|
|
14
|
+
# manipulation is done directly for performance reasons to
|
|
15
|
+
# ensure we are using Redis as efficiently as possible at
|
|
16
|
+
# every callsite.
|
|
17
|
+
#
|
|
7
18
|
|
|
8
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
|
+
|
|
39
|
+
# Retrieve runtime statistics from Redis regarding
|
|
40
|
+
# this Sidekiq cluster.
|
|
41
|
+
#
|
|
42
|
+
# stat = Sidekiq::Stats.new
|
|
43
|
+
# stat.processed
|
|
9
44
|
class Stats
|
|
45
|
+
QueueSummary = Data.define(:name, :size, :latency, :paused) do
|
|
46
|
+
alias_method :paused?, :paused
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
include ApiUtils
|
|
50
|
+
|
|
10
51
|
def initialize
|
|
11
52
|
fetch_stats_fast!
|
|
12
53
|
end
|
|
@@ -47,35 +88,82 @@ module Sidekiq
|
|
|
47
88
|
stat :default_queue_latency
|
|
48
89
|
end
|
|
49
90
|
|
|
91
|
+
# @return [Hash{String => Integer}] a hash of queue names to their lengths
|
|
50
92
|
def queues
|
|
51
|
-
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
|
|
52
140
|
end
|
|
53
141
|
|
|
54
142
|
# O(1) redis calls
|
|
143
|
+
# @api private
|
|
55
144
|
def fetch_stats_fast!
|
|
56
145
|
pipe1_res = Sidekiq.redis { |conn|
|
|
57
|
-
conn.pipelined do
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
146
|
+
conn.pipelined do |pipeline|
|
|
147
|
+
pipeline.get("stat:processed")
|
|
148
|
+
pipeline.get("stat:failed")
|
|
149
|
+
pipeline.zcard("schedule")
|
|
150
|
+
pipeline.zcard("retry")
|
|
151
|
+
pipeline.zcard("dead")
|
|
152
|
+
pipeline.scard("processes")
|
|
153
|
+
pipeline.lindex("queue:default", -1)
|
|
65
154
|
end
|
|
66
155
|
}
|
|
67
156
|
|
|
68
|
-
default_queue_latency = if (entry = pipe1_res[6]
|
|
157
|
+
default_queue_latency = if (entry = pipe1_res[6])
|
|
69
158
|
job = begin
|
|
70
159
|
Sidekiq.load_json(entry)
|
|
71
160
|
rescue
|
|
72
161
|
{}
|
|
73
162
|
end
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
now - thence
|
|
163
|
+
|
|
164
|
+
calculate_latency(job)
|
|
77
165
|
else
|
|
78
|
-
0
|
|
166
|
+
0.0
|
|
79
167
|
end
|
|
80
168
|
|
|
81
169
|
@stats = {
|
|
@@ -91,35 +179,39 @@ module Sidekiq
|
|
|
91
179
|
end
|
|
92
180
|
|
|
93
181
|
# O(number of processes + number of queues) redis calls
|
|
182
|
+
# @api private
|
|
94
183
|
def fetch_stats_slow!
|
|
95
184
|
processes = Sidekiq.redis { |conn|
|
|
96
|
-
conn.
|
|
185
|
+
conn.sscan("processes").to_a
|
|
97
186
|
}
|
|
98
187
|
|
|
99
188
|
queues = Sidekiq.redis { |conn|
|
|
100
|
-
conn.
|
|
189
|
+
conn.sscan("queues").to_a
|
|
101
190
|
}
|
|
102
191
|
|
|
103
192
|
pipe2_res = Sidekiq.redis { |conn|
|
|
104
|
-
conn.pipelined do
|
|
105
|
-
processes.each { |key|
|
|
106
|
-
queues.each { |queue|
|
|
193
|
+
conn.pipelined do |pipeline|
|
|
194
|
+
processes.each { |key| pipeline.hget(key, "busy") }
|
|
195
|
+
queues.each { |queue| pipeline.llen("queue:#{queue}") }
|
|
107
196
|
end
|
|
108
197
|
}
|
|
109
198
|
|
|
110
199
|
s = processes.size
|
|
111
200
|
workers_size = pipe2_res[0...s].sum(&:to_i)
|
|
112
|
-
enqueued = pipe2_res[s
|
|
201
|
+
enqueued = pipe2_res[s..].sum(&:to_i)
|
|
113
202
|
|
|
114
203
|
@stats[:workers_size] = workers_size
|
|
115
204
|
@stats[:enqueued] = enqueued
|
|
205
|
+
@stats
|
|
116
206
|
end
|
|
117
207
|
|
|
208
|
+
# @api private
|
|
118
209
|
def fetch_stats!
|
|
119
210
|
fetch_stats_fast!
|
|
120
211
|
fetch_stats_slow!
|
|
121
212
|
end
|
|
122
213
|
|
|
214
|
+
# @api private
|
|
123
215
|
def reset(*stats)
|
|
124
216
|
all = %w[failed processed]
|
|
125
217
|
stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
|
|
@@ -141,25 +233,10 @@ module Sidekiq
|
|
|
141
233
|
@stats[s] || raise(ArgumentError, "Unknown stat #{s}")
|
|
142
234
|
end
|
|
143
235
|
|
|
144
|
-
class Queues
|
|
145
|
-
def lengths
|
|
146
|
-
Sidekiq.redis do |conn|
|
|
147
|
-
queues = conn.sscan_each("queues").to_a
|
|
148
|
-
|
|
149
|
-
lengths = conn.pipelined {
|
|
150
|
-
queues.each do |queue|
|
|
151
|
-
conn.llen("queue:#{queue}")
|
|
152
|
-
end
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
|
|
156
|
-
array_of_arrays.to_h
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
236
|
class History
|
|
162
|
-
def initialize(days_previous, start_date = nil)
|
|
237
|
+
def initialize(days_previous, start_date = nil, pool: nil)
|
|
238
|
+
# we only store five years of data in Redis
|
|
239
|
+
raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
|
|
163
240
|
@days_previous = days_previous
|
|
164
241
|
@start_date = start_date || Time.now.utc.to_date
|
|
165
242
|
end
|
|
@@ -182,15 +259,10 @@ module Sidekiq
|
|
|
182
259
|
|
|
183
260
|
keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
|
|
184
261
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
stat_hash[dates[idx]] = value ? value.to_i : 0
|
|
189
|
-
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
|
|
190
265
|
end
|
|
191
|
-
rescue Redis::CommandError
|
|
192
|
-
# mget will trigger a CROSSSLOT error when run against a Cluster
|
|
193
|
-
# TODO Someone want to add Cluster support?
|
|
194
266
|
end
|
|
195
267
|
|
|
196
268
|
stat_hash
|
|
@@ -199,9 +271,10 @@ module Sidekiq
|
|
|
199
271
|
end
|
|
200
272
|
|
|
201
273
|
##
|
|
202
|
-
#
|
|
274
|
+
# Represents a queue within Sidekiq.
|
|
203
275
|
# Allows enumeration of all jobs within the queue
|
|
204
|
-
# and deletion of jobs.
|
|
276
|
+
# and deletion of jobs. NB: this queue data is real-time
|
|
277
|
+
# and is changing within Redis moment by moment.
|
|
205
278
|
#
|
|
206
279
|
# queue = Sidekiq::Queue.new("mailer")
|
|
207
280
|
# queue.each do |job|
|
|
@@ -209,29 +282,36 @@ module Sidekiq
|
|
|
209
282
|
# job.args # => [1, 2, 3]
|
|
210
283
|
# job.delete if job.jid == 'abcdef1234567890'
|
|
211
284
|
# end
|
|
212
|
-
#
|
|
213
285
|
class Queue
|
|
214
286
|
include Enumerable
|
|
287
|
+
include ApiUtils
|
|
215
288
|
|
|
216
289
|
##
|
|
217
|
-
#
|
|
290
|
+
# Fetch all known queues within Redis.
|
|
218
291
|
#
|
|
292
|
+
# @return [Array<Sidekiq::Queue>]
|
|
219
293
|
def self.all
|
|
220
|
-
Sidekiq.redis { |c| c.
|
|
294
|
+
Sidekiq.redis { |c| c.sscan("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
|
|
221
295
|
end
|
|
222
296
|
|
|
223
297
|
attr_reader :name
|
|
298
|
+
alias_method :id, :name
|
|
224
299
|
|
|
300
|
+
# @param name [String] the name of the queue
|
|
225
301
|
def initialize(name = "default")
|
|
226
302
|
@name = name.to_s
|
|
227
303
|
@rname = "queue:#{name}"
|
|
228
304
|
end
|
|
229
305
|
|
|
306
|
+
# The current size of the queue within Redis.
|
|
307
|
+
# This value is real-time and can change between calls.
|
|
308
|
+
#
|
|
309
|
+
# @return [Integer] the size
|
|
230
310
|
def size
|
|
231
311
|
Sidekiq.redis { |con| con.llen(@rname) }
|
|
232
312
|
end
|
|
233
313
|
|
|
234
|
-
#
|
|
314
|
+
# @return [Boolean] if the queue is currently paused
|
|
235
315
|
def paused?
|
|
236
316
|
false
|
|
237
317
|
end
|
|
@@ -240,16 +320,15 @@ module Sidekiq
|
|
|
240
320
|
# Calculates this queue's latency, the difference in seconds since the oldest
|
|
241
321
|
# job in the queue was enqueued.
|
|
242
322
|
#
|
|
243
|
-
# @return Float
|
|
323
|
+
# @return [Float] in seconds
|
|
244
324
|
def latency
|
|
245
325
|
entry = Sidekiq.redis { |conn|
|
|
246
|
-
conn.
|
|
247
|
-
}
|
|
248
|
-
return 0 unless entry
|
|
326
|
+
conn.lindex(@rname, -1)
|
|
327
|
+
}
|
|
328
|
+
return 0.0 unless entry
|
|
329
|
+
|
|
249
330
|
job = Sidekiq.load_json(entry)
|
|
250
|
-
|
|
251
|
-
thence = job["enqueued_at"] || now
|
|
252
|
-
now - thence
|
|
331
|
+
calculate_latency(job)
|
|
253
332
|
end
|
|
254
333
|
|
|
255
334
|
def each
|
|
@@ -276,34 +355,56 @@ module Sidekiq
|
|
|
276
355
|
##
|
|
277
356
|
# Find the job with the given JID within this queue.
|
|
278
357
|
#
|
|
279
|
-
# This is a slow, inefficient operation. Do not use under
|
|
358
|
+
# This is a *slow, inefficient* operation. Do not use under
|
|
280
359
|
# normal conditions.
|
|
360
|
+
#
|
|
361
|
+
# @param jid [String] the job_id to look for
|
|
362
|
+
# @return [Sidekiq::JobRecord]
|
|
363
|
+
# @return [nil] if not found
|
|
281
364
|
def find_job(jid)
|
|
282
365
|
detect { |j| j.jid == jid }
|
|
283
366
|
end
|
|
284
367
|
|
|
368
|
+
# delete all jobs within this queue
|
|
369
|
+
# @return [Boolean] true
|
|
285
370
|
def clear
|
|
286
371
|
Sidekiq.redis do |conn|
|
|
287
|
-
conn.multi do
|
|
288
|
-
|
|
289
|
-
|
|
372
|
+
conn.multi do |transaction|
|
|
373
|
+
transaction.unlink(@rname)
|
|
374
|
+
transaction.srem("queues", [name])
|
|
290
375
|
end
|
|
291
376
|
end
|
|
377
|
+
true
|
|
292
378
|
end
|
|
293
379
|
alias_method :💣, :clear
|
|
380
|
+
|
|
381
|
+
# :nodoc:
|
|
382
|
+
# @api private
|
|
383
|
+
def as_json(options = nil)
|
|
384
|
+
{name: name} # 5336
|
|
385
|
+
end
|
|
294
386
|
end
|
|
295
387
|
|
|
296
388
|
##
|
|
297
|
-
#
|
|
298
|
-
# sorted set.
|
|
389
|
+
# Represents a pending job within a Sidekiq queue.
|
|
299
390
|
#
|
|
300
391
|
# The job should be considered immutable but may be
|
|
301
392
|
# removed from the queue via JobRecord#delete.
|
|
302
|
-
#
|
|
303
393
|
class JobRecord
|
|
394
|
+
include ApiUtils
|
|
395
|
+
|
|
396
|
+
# the parsed Hash of job data
|
|
397
|
+
# @!attribute [r] Item
|
|
304
398
|
attr_reader :item
|
|
399
|
+
# the underlying String in Redis
|
|
400
|
+
# @!attribute [r] Value
|
|
305
401
|
attr_reader :value
|
|
402
|
+
# the queue associated with this job
|
|
403
|
+
# @!attribute [r] Queue
|
|
404
|
+
attr_reader :queue
|
|
306
405
|
|
|
406
|
+
# :nodoc:
|
|
407
|
+
# @api private
|
|
307
408
|
def initialize(item, queue_name = nil)
|
|
308
409
|
@args = nil
|
|
309
410
|
@value = item
|
|
@@ -311,6 +412,8 @@ module Sidekiq
|
|
|
311
412
|
@queue = queue_name || @item["queue"]
|
|
312
413
|
end
|
|
313
414
|
|
|
415
|
+
# :nodoc:
|
|
416
|
+
# @api private
|
|
314
417
|
def parse(item)
|
|
315
418
|
Sidekiq.load_json(item)
|
|
316
419
|
rescue JSON::ParserError
|
|
@@ -322,6 +425,8 @@ module Sidekiq
|
|
|
322
425
|
{}
|
|
323
426
|
end
|
|
324
427
|
|
|
428
|
+
# This is the job class which Sidekiq will execute. If using ActiveJob,
|
|
429
|
+
# this class will be the ActiveJob adapter class rather than a specific job.
|
|
325
430
|
def klass
|
|
326
431
|
self["class"]
|
|
327
432
|
end
|
|
@@ -329,12 +434,7 @@ module Sidekiq
|
|
|
329
434
|
def display_class
|
|
330
435
|
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
|
331
436
|
@klass ||= self["display_class"] || begin
|
|
332
|
-
|
|
333
|
-
when /\ASidekiq::Extensions::Delayed/
|
|
334
|
-
safe_load(args[0], klass) do |target, method, _|
|
|
335
|
-
"#{target}.#{method}"
|
|
336
|
-
end
|
|
337
|
-
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
|
437
|
+
if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
|
|
338
438
|
job_class = @item["wrapped"] || args[0]
|
|
339
439
|
if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
|
|
340
440
|
# MailerClass#mailer_method
|
|
@@ -350,28 +450,23 @@ module Sidekiq
|
|
|
350
450
|
|
|
351
451
|
def display_args
|
|
352
452
|
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
|
353
|
-
@display_args ||=
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if self["encrypt"]
|
|
371
|
-
# no point in showing 150+ bytes of random garbage
|
|
372
|
-
args[-1] = "[encrypted data]"
|
|
373
|
-
end
|
|
374
|
-
args
|
|
453
|
+
@display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" || klass == "Sidekiq::ActiveJob::Wrapper"
|
|
454
|
+
job_args = self["wrapped"] ? deserialize_argument(args[0]["arguments"]) : []
|
|
455
|
+
if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
|
|
456
|
+
# remove MailerClass, mailer_method and 'deliver_now'
|
|
457
|
+
job_args.drop(3)
|
|
458
|
+
elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
|
|
459
|
+
# remove MailerClass, mailer_method and 'deliver_now'
|
|
460
|
+
job_args.drop(3).first.values_at("params", "args")
|
|
461
|
+
else
|
|
462
|
+
job_args
|
|
463
|
+
end
|
|
464
|
+
else
|
|
465
|
+
if self["encrypt"]
|
|
466
|
+
# no point in showing 150+ bytes of random garbage
|
|
467
|
+
args[-1] = "[encrypted data]"
|
|
468
|
+
end
|
|
469
|
+
args
|
|
375
470
|
end
|
|
376
471
|
end
|
|
377
472
|
|
|
@@ -383,12 +478,34 @@ module Sidekiq
|
|
|
383
478
|
self["jid"]
|
|
384
479
|
end
|
|
385
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
|
+
|
|
386
501
|
def enqueued_at
|
|
387
|
-
|
|
502
|
+
if self["enqueued_at"]
|
|
503
|
+
time_from_timestamp(self["enqueued_at"])
|
|
504
|
+
end
|
|
388
505
|
end
|
|
389
506
|
|
|
390
507
|
def created_at
|
|
391
|
-
|
|
508
|
+
time_from_timestamp(self["created_at"] || self["enqueued_at"] || 0)
|
|
392
509
|
end
|
|
393
510
|
|
|
394
511
|
def tags
|
|
@@ -405,15 +522,11 @@ module Sidekiq
|
|
|
405
522
|
end
|
|
406
523
|
end
|
|
407
524
|
|
|
408
|
-
attr_reader :queue
|
|
409
|
-
|
|
410
525
|
def latency
|
|
411
|
-
|
|
412
|
-
now - (@item["enqueued_at"] || @item["created_at"] || now)
|
|
526
|
+
calculate_latency(@item)
|
|
413
527
|
end
|
|
414
528
|
|
|
415
|
-
|
|
416
|
-
# Remove this job from the queue.
|
|
529
|
+
# Remove this job from the queue
|
|
417
530
|
def delete
|
|
418
531
|
count = Sidekiq.redis { |conn|
|
|
419
532
|
conn.lrem("queue:#{@queue}", 1, @value)
|
|
@@ -421,6 +534,7 @@ module Sidekiq
|
|
|
421
534
|
count != 0
|
|
422
535
|
end
|
|
423
536
|
|
|
537
|
+
# Access arbitrary attributes within the job hash
|
|
424
538
|
def [](name)
|
|
425
539
|
# nil will happen if the JSON fails to parse.
|
|
426
540
|
# We don't guarantee Sidekiq will work with bad job JSON but we should
|
|
@@ -430,61 +544,93 @@ module Sidekiq
|
|
|
430
544
|
|
|
431
545
|
private
|
|
432
546
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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)
|
|
440
568
|
end
|
|
441
569
|
|
|
442
570
|
def uncompress_backtrace(backtrace)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
446
580
|
else
|
|
447
|
-
|
|
448
|
-
uncompressed = Zlib::Inflate.inflate(decoded)
|
|
449
|
-
begin
|
|
450
|
-
Sidekiq.load_json(uncompressed)
|
|
451
|
-
rescue
|
|
452
|
-
# Handle old jobs with marshalled backtrace format
|
|
453
|
-
# TODO Remove in 7.x
|
|
454
|
-
Marshal.load(uncompressed)
|
|
455
|
-
end
|
|
581
|
+
Time.at(timestamp / 1000, timestamp % 1000, :millisecond)
|
|
456
582
|
end
|
|
457
583
|
end
|
|
458
584
|
end
|
|
459
585
|
|
|
586
|
+
# Represents a job within a Redis sorted set where the score
|
|
587
|
+
# represents a timestamp associated with the job. This timestamp
|
|
588
|
+
# could be the scheduled time for it to run (e.g. scheduled set),
|
|
589
|
+
# or the expiration date after which the entry should be deleted (e.g. dead set).
|
|
460
590
|
class SortedEntry < JobRecord
|
|
461
|
-
attr_reader :score
|
|
462
591
|
attr_reader :parent
|
|
463
592
|
|
|
593
|
+
# :nodoc:
|
|
594
|
+
# @api private
|
|
464
595
|
def initialize(parent, score, item)
|
|
465
596
|
super(item)
|
|
466
597
|
@score = score
|
|
467
598
|
@parent = parent
|
|
468
599
|
end
|
|
469
600
|
|
|
601
|
+
def score
|
|
602
|
+
Float(@score)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def id
|
|
606
|
+
"#{@score}|#{item["jid"]}"
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# The timestamp associated with this entry
|
|
470
610
|
def at
|
|
471
611
|
Time.at(score).utc
|
|
472
612
|
end
|
|
473
613
|
|
|
614
|
+
# remove this entry from the sorted set
|
|
474
615
|
def delete
|
|
475
616
|
if @value
|
|
476
617
|
@parent.delete_by_value(@parent.name, @value)
|
|
477
618
|
else
|
|
478
|
-
@parent.delete_by_jid(score, jid)
|
|
619
|
+
@parent.delete_by_jid(@score, jid)
|
|
479
620
|
end
|
|
480
621
|
end
|
|
481
622
|
|
|
623
|
+
# Change the scheduled time for this job.
|
|
624
|
+
#
|
|
625
|
+
# @param at [Time] the new timestamp for this job
|
|
482
626
|
def reschedule(at)
|
|
483
627
|
Sidekiq.redis do |conn|
|
|
484
|
-
conn.zincrby(@parent.name, at.to_f -
|
|
628
|
+
conn.zincrby(@parent.name, at.to_f - score, Sidekiq.dump_json(@item))
|
|
485
629
|
end
|
|
486
630
|
end
|
|
487
631
|
|
|
632
|
+
# Enqueue this job from the scheduled or dead set so it will
|
|
633
|
+
# be executed at some point in the near future.
|
|
488
634
|
def add_to_queue
|
|
489
635
|
remove_job do |message|
|
|
490
636
|
msg = Sidekiq.load_json(message)
|
|
@@ -492,6 +638,8 @@ module Sidekiq
|
|
|
492
638
|
end
|
|
493
639
|
end
|
|
494
640
|
|
|
641
|
+
# enqueue this job from the retry set so it will be executed
|
|
642
|
+
# at some point in the near future.
|
|
495
643
|
def retry
|
|
496
644
|
remove_job do |message|
|
|
497
645
|
msg = Sidekiq.load_json(message)
|
|
@@ -500,8 +648,7 @@ module Sidekiq
|
|
|
500
648
|
end
|
|
501
649
|
end
|
|
502
650
|
|
|
503
|
-
|
|
504
|
-
# Place job in the dead set
|
|
651
|
+
# Move this job from its current set into the Dead set.
|
|
505
652
|
def kill
|
|
506
653
|
remove_job do |message|
|
|
507
654
|
DeadSet.new.kill(message)
|
|
@@ -514,78 +661,109 @@ module Sidekiq
|
|
|
514
661
|
|
|
515
662
|
private
|
|
516
663
|
|
|
517
|
-
def remove_job
|
|
518
|
-
|
|
519
|
-
results = conn.multi {
|
|
520
|
-
conn.zrangebyscore(parent.name, score, score)
|
|
521
|
-
conn.zremrangebyscore(parent.name, score, score)
|
|
522
|
-
}.first
|
|
523
|
-
|
|
524
|
-
if results.size == 1
|
|
525
|
-
yield results.first
|
|
526
|
-
else
|
|
527
|
-
# multiple jobs with the same score
|
|
528
|
-
# find the one with the right JID and push it
|
|
529
|
-
matched, nonmatched = results.partition { |message|
|
|
530
|
-
if message.index(jid)
|
|
531
|
-
msg = Sidekiq.load_json(message)
|
|
532
|
-
msg["jid"] == jid
|
|
533
|
-
else
|
|
534
|
-
false
|
|
535
|
-
end
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
msg = matched.first
|
|
539
|
-
yield msg if msg
|
|
540
|
-
|
|
541
|
-
# push the rest back onto the sorted set
|
|
542
|
-
conn.multi do
|
|
543
|
-
nonmatched.each do |message|
|
|
544
|
-
conn.zadd(parent.name, score.to_f.to_s, message)
|
|
545
|
-
end
|
|
546
|
-
end
|
|
547
|
-
end
|
|
548
|
-
end
|
|
664
|
+
def remove_job(&)
|
|
665
|
+
parent.remove_job(self, &)
|
|
549
666
|
end
|
|
550
667
|
end
|
|
551
668
|
|
|
669
|
+
# Base class for all sorted sets within Sidekiq.
|
|
552
670
|
class SortedSet
|
|
553
671
|
include Enumerable
|
|
554
672
|
|
|
673
|
+
# Redis key of the set
|
|
674
|
+
# @!attribute [r] Name
|
|
555
675
|
attr_reader :name
|
|
556
676
|
|
|
677
|
+
# :nodoc:
|
|
678
|
+
# @api private
|
|
557
679
|
def initialize(name)
|
|
558
680
|
@name = name
|
|
559
681
|
@_size = size
|
|
560
682
|
end
|
|
561
683
|
|
|
684
|
+
# real-time size of the set, will change
|
|
562
685
|
def size
|
|
563
686
|
Sidekiq.redis { |c| c.zcard(name) }
|
|
564
687
|
end
|
|
565
688
|
|
|
689
|
+
# Scan through each element of the sorted set, yielding each to the supplied block.
|
|
690
|
+
# Please see Redis's <a href="https://redis.io/commands/scan/">SCAN documentation</a> for implementation details.
|
|
691
|
+
#
|
|
692
|
+
# @param match [String] a snippet or regexp to filter matches.
|
|
693
|
+
# @param count [Integer] number of elements to retrieve at a time, default 100
|
|
694
|
+
# @yieldparam [Sidekiq::SortedEntry] each entry
|
|
566
695
|
def scan(match, count = 100)
|
|
567
696
|
return to_enum(:scan, match, count) unless block_given?
|
|
568
697
|
|
|
569
698
|
match = "*#{match}*" unless match.include?("*")
|
|
570
699
|
Sidekiq.redis do |conn|
|
|
571
|
-
conn.
|
|
700
|
+
conn.zscan(name, match: match, count: count) do |entry, score|
|
|
572
701
|
yield SortedEntry.new(self, score, entry)
|
|
573
702
|
end
|
|
574
703
|
end
|
|
575
704
|
end
|
|
576
705
|
|
|
706
|
+
# @return [Boolean] always true
|
|
577
707
|
def clear
|
|
578
708
|
Sidekiq.redis do |conn|
|
|
579
709
|
conn.unlink(name)
|
|
580
710
|
end
|
|
711
|
+
true
|
|
581
712
|
end
|
|
582
713
|
alias_method :💣, :clear
|
|
714
|
+
|
|
715
|
+
# :nodoc:
|
|
716
|
+
# @api private
|
|
717
|
+
def as_json(options = nil)
|
|
718
|
+
{name: name} # 5336
|
|
719
|
+
end
|
|
583
720
|
end
|
|
584
721
|
|
|
722
|
+
# Base class for all sorted sets which contain jobs, e.g. scheduled, retry and dead.
|
|
723
|
+
# Sidekiq Pro and Enterprise add additional sorted sets which do not contain job data,
|
|
724
|
+
# e.g. Batches.
|
|
585
725
|
class JobSet < SortedSet
|
|
586
|
-
|
|
726
|
+
# Add a job with the associated timestamp to this set.
|
|
727
|
+
# @param timestamp [Time] the score for the job
|
|
728
|
+
# @param job [Hash] the job data
|
|
729
|
+
def schedule(timestamp, job)
|
|
587
730
|
Sidekiq.redis do |conn|
|
|
588
|
-
conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(
|
|
731
|
+
conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(job))
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def pop_each
|
|
736
|
+
Sidekiq.redis do |c|
|
|
737
|
+
size.times do
|
|
738
|
+
data, score = c.zpopmin(name, 1)&.first
|
|
739
|
+
break unless data
|
|
740
|
+
yield data, score
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def retry_all
|
|
746
|
+
c = Sidekiq::Client.new
|
|
747
|
+
pop_each do |msg, _|
|
|
748
|
+
job = Sidekiq.load_json(msg)
|
|
749
|
+
# Manual retries should not count against the retry limit.
|
|
750
|
+
job["retry_count"] -= 1 if job["retry_count"]
|
|
751
|
+
c.push(job)
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# Move all jobs from this Set to the Dead Set.
|
|
756
|
+
# See DeadSet#kill
|
|
757
|
+
def kill_all(notify_failure: false, ex: nil)
|
|
758
|
+
ds = DeadSet.new
|
|
759
|
+
opts = {notify_failure: notify_failure, ex: ex, trim: false}
|
|
760
|
+
|
|
761
|
+
begin
|
|
762
|
+
pop_each do |msg, _|
|
|
763
|
+
ds.kill(msg, opts)
|
|
764
|
+
end
|
|
765
|
+
ensure
|
|
766
|
+
ds.trim
|
|
589
767
|
end
|
|
590
768
|
end
|
|
591
769
|
|
|
@@ -599,7 +777,7 @@ module Sidekiq
|
|
|
599
777
|
range_start = page * page_size + offset_size
|
|
600
778
|
range_end = range_start + page_size - 1
|
|
601
779
|
elements = Sidekiq.redis { |conn|
|
|
602
|
-
conn.zrange name, range_start, range_end,
|
|
780
|
+
conn.zrange name, range_start, range_end, "withscores"
|
|
603
781
|
}
|
|
604
782
|
break if elements.empty?
|
|
605
783
|
page -= 1
|
|
@@ -613,6 +791,10 @@ module Sidekiq
|
|
|
613
791
|
##
|
|
614
792
|
# Fetch jobs that match a given time or Range. Job ID is an
|
|
615
793
|
# optional second argument.
|
|
794
|
+
#
|
|
795
|
+
# @param score [Time,Range] a specific timestamp or range
|
|
796
|
+
# @param jid [String, optional] find a specific JID within the score
|
|
797
|
+
# @return [Array<SortedEntry>] any results found, can be empty
|
|
616
798
|
def fetch(score, jid = nil)
|
|
617
799
|
begin_score, end_score =
|
|
618
800
|
if score.is_a?(Range)
|
|
@@ -622,7 +804,7 @@ module Sidekiq
|
|
|
622
804
|
end
|
|
623
805
|
|
|
624
806
|
elements = Sidekiq.redis { |conn|
|
|
625
|
-
conn.
|
|
807
|
+
conn.zrange(name, begin_score, end_score, "BYSCORE", "withscores")
|
|
626
808
|
}
|
|
627
809
|
|
|
628
810
|
elements.each_with_object([]) do |element, result|
|
|
@@ -634,11 +816,14 @@ module Sidekiq
|
|
|
634
816
|
|
|
635
817
|
##
|
|
636
818
|
# Find the job with the given JID within this sorted set.
|
|
637
|
-
# This is a
|
|
819
|
+
# *This is a slow O(n) operation*. Do not use for app logic.
|
|
820
|
+
#
|
|
821
|
+
# @param jid [String] the job identifier
|
|
822
|
+
# @return [SortedEntry] the record or nil
|
|
638
823
|
def find_job(jid)
|
|
639
824
|
Sidekiq.redis do |conn|
|
|
640
|
-
conn.
|
|
641
|
-
job =
|
|
825
|
+
conn.zscan(name, match: "*#{jid}*", count: 100) do |entry, score|
|
|
826
|
+
job = Sidekiq.load_json(entry)
|
|
642
827
|
matched = job["jid"] == jid
|
|
643
828
|
return SortedEntry.new(self, score, entry) if matched
|
|
644
829
|
end
|
|
@@ -646,6 +831,48 @@ module Sidekiq
|
|
|
646
831
|
nil
|
|
647
832
|
end
|
|
648
833
|
|
|
834
|
+
def remove_job(entry)
|
|
835
|
+
score = entry.score
|
|
836
|
+
jid = entry.jid
|
|
837
|
+
Sidekiq.redis do |conn|
|
|
838
|
+
results = conn.multi { |transaction|
|
|
839
|
+
transaction.zrange(name, score, score, "BYSCORE")
|
|
840
|
+
transaction.zremrangebyscore(name, score, score)
|
|
841
|
+
}.first
|
|
842
|
+
|
|
843
|
+
if results.size == 1
|
|
844
|
+
yield results.first
|
|
845
|
+
@_size -= 1
|
|
846
|
+
else
|
|
847
|
+
# multiple jobs with the same score
|
|
848
|
+
# find the one with the right JID and push it
|
|
849
|
+
matched, nonmatched = results.partition { |message|
|
|
850
|
+
if message.index(jid)
|
|
851
|
+
msg = Sidekiq.load_json(message)
|
|
852
|
+
msg["jid"] == jid
|
|
853
|
+
else
|
|
854
|
+
false
|
|
855
|
+
end
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
msg = matched.first
|
|
859
|
+
if msg
|
|
860
|
+
yield msg
|
|
861
|
+
@_size -= 1
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
# push the rest back onto the sorted set
|
|
865
|
+
conn.multi do |transaction|
|
|
866
|
+
nonmatched.each do |message|
|
|
867
|
+
transaction.zadd(name, score.to_f.to_s, message)
|
|
868
|
+
end
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# :nodoc:
|
|
875
|
+
# @api private
|
|
649
876
|
def delete_by_value(name, value)
|
|
650
877
|
Sidekiq.redis do |conn|
|
|
651
878
|
ret = conn.zrem(name, value)
|
|
@@ -654,9 +881,11 @@ module Sidekiq
|
|
|
654
881
|
end
|
|
655
882
|
end
|
|
656
883
|
|
|
884
|
+
# :nodoc:
|
|
885
|
+
# @api private
|
|
657
886
|
def delete_by_jid(score, jid)
|
|
658
887
|
Sidekiq.redis do |conn|
|
|
659
|
-
elements = conn.
|
|
888
|
+
elements = conn.zrange(name, score, score, "BYSCORE")
|
|
660
889
|
elements.each do |element|
|
|
661
890
|
if element.index(jid)
|
|
662
891
|
message = Sidekiq.load_json(element)
|
|
@@ -674,89 +903,74 @@ module Sidekiq
|
|
|
674
903
|
end
|
|
675
904
|
|
|
676
905
|
##
|
|
677
|
-
#
|
|
678
|
-
#
|
|
679
|
-
# example where I'm selecting all jobs of a certain type
|
|
680
|
-
# and deleting them from the schedule queue.
|
|
906
|
+
# The set of scheduled jobs within Sidekiq.
|
|
907
|
+
# See the API wiki page for usage notes and examples.
|
|
681
908
|
#
|
|
682
|
-
# r = Sidekiq::ScheduledSet.new
|
|
683
|
-
# r.select do |scheduled|
|
|
684
|
-
# scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
|
|
685
|
-
# scheduled.args[0] == 'User' &&
|
|
686
|
-
# scheduled.args[1] == 'setup_new_subscriber'
|
|
687
|
-
# end.map(&:delete)
|
|
688
909
|
class ScheduledSet < JobSet
|
|
689
910
|
def initialize
|
|
690
|
-
super
|
|
911
|
+
super("schedule")
|
|
691
912
|
end
|
|
692
913
|
end
|
|
693
914
|
|
|
694
915
|
##
|
|
695
|
-
#
|
|
696
|
-
#
|
|
697
|
-
# example where I'm selecting all jobs of a certain type
|
|
698
|
-
# and deleting them from the retry queue.
|
|
916
|
+
# The set of retries within Sidekiq.
|
|
917
|
+
# See the API wiki page for usage notes and examples.
|
|
699
918
|
#
|
|
700
|
-
# r = Sidekiq::RetrySet.new
|
|
701
|
-
# r.select do |retri|
|
|
702
|
-
# retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
|
|
703
|
-
# retri.args[0] == 'User' &&
|
|
704
|
-
# retri.args[1] == 'setup_new_subscriber'
|
|
705
|
-
# end.map(&:delete)
|
|
706
919
|
class RetrySet < JobSet
|
|
707
920
|
def initialize
|
|
708
|
-
super
|
|
709
|
-
end
|
|
710
|
-
|
|
711
|
-
def retry_all
|
|
712
|
-
each(&:retry) while size > 0
|
|
713
|
-
end
|
|
714
|
-
|
|
715
|
-
def kill_all
|
|
716
|
-
each(&:kill) while size > 0
|
|
921
|
+
super("retry")
|
|
717
922
|
end
|
|
718
923
|
end
|
|
719
924
|
|
|
720
925
|
##
|
|
721
|
-
#
|
|
926
|
+
# The set of dead jobs within Sidekiq. Dead jobs have failed all of
|
|
927
|
+
# their retries and are helding in this set pending some sort of manual
|
|
928
|
+
# fix. They will be removed after 6 months (dead_timeout) if not.
|
|
722
929
|
#
|
|
723
930
|
class DeadSet < JobSet
|
|
724
931
|
def initialize
|
|
725
|
-
super
|
|
932
|
+
super("dead")
|
|
726
933
|
end
|
|
727
934
|
|
|
728
|
-
|
|
935
|
+
# Trim dead jobs which are over our storage limits
|
|
936
|
+
def trim
|
|
937
|
+
hash = Sidekiq.default_configuration
|
|
729
938
|
now = Time.now.to_f
|
|
730
939
|
Sidekiq.redis do |conn|
|
|
731
|
-
conn.multi do
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
conn.zremrangebyrank(name, 0, - self.class.max_jobs)
|
|
940
|
+
conn.multi do |transaction|
|
|
941
|
+
transaction.zremrangebyscore(name, "-inf", now - hash[:dead_timeout_in_seconds])
|
|
942
|
+
transaction.zremrangebyrank(name, 0, - hash[:dead_max_jobs])
|
|
735
943
|
end
|
|
736
944
|
end
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
# Add the given job to the Dead set.
|
|
948
|
+
# @param message [String] the job data as JSON
|
|
949
|
+
# @option opts [Boolean] :notify_failure (true) Whether death handlers should be called
|
|
950
|
+
# @option opts [Boolean] :trim (true) Whether Sidekiq should trim the structure to keep it within configuration
|
|
951
|
+
# @option opts [Exception] :ex (RuntimeError) An exception to pass to the death handlers
|
|
952
|
+
def kill(message, opts = {})
|
|
953
|
+
now = Time.now.to_f
|
|
954
|
+
Sidekiq.redis do |conn|
|
|
955
|
+
conn.zadd(name, now.to_s, message)
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
trim if opts[:trim] != false
|
|
737
959
|
|
|
738
960
|
if opts[:notify_failure] != false
|
|
739
961
|
job = Sidekiq.load_json(message)
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
962
|
+
if opts[:ex]
|
|
963
|
+
ex = opts[:ex]
|
|
964
|
+
else
|
|
965
|
+
ex = RuntimeError.new("Job killed by API")
|
|
966
|
+
ex.set_backtrace(caller)
|
|
967
|
+
end
|
|
968
|
+
Sidekiq.default_configuration.death_handlers.each do |handle|
|
|
969
|
+
handle.call(job, ex)
|
|
744
970
|
end
|
|
745
971
|
end
|
|
746
972
|
true
|
|
747
973
|
end
|
|
748
|
-
|
|
749
|
-
def retry_all
|
|
750
|
-
each(&:retry) while size > 0
|
|
751
|
-
end
|
|
752
|
-
|
|
753
|
-
def self.max_jobs
|
|
754
|
-
Sidekiq.options[:dead_max_jobs]
|
|
755
|
-
end
|
|
756
|
-
|
|
757
|
-
def self.timeout
|
|
758
|
-
Sidekiq.options[:dead_timeout_in_seconds]
|
|
759
|
-
end
|
|
760
974
|
end
|
|
761
975
|
|
|
762
976
|
##
|
|
@@ -764,24 +978,49 @@ module Sidekiq
|
|
|
764
978
|
# right now. Each process sends a heartbeat to Redis every 5 seconds
|
|
765
979
|
# so this set should be relatively accurate, barring network partitions.
|
|
766
980
|
#
|
|
767
|
-
#
|
|
981
|
+
# @yieldparam [Sidekiq::Process]
|
|
768
982
|
#
|
|
769
983
|
class ProcessSet
|
|
770
984
|
include Enumerable
|
|
771
985
|
|
|
986
|
+
def self.[](identity)
|
|
987
|
+
exists, (info, busy, beat, quiet, rss, rtt_us) = Sidekiq.redis { |conn|
|
|
988
|
+
conn.multi { |transaction|
|
|
989
|
+
transaction.sismember("processes", identity)
|
|
990
|
+
transaction.hmget(identity, "info", "busy", "beat", "quiet", "rss", "rtt_us")
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return nil if exists == 0 || info.nil?
|
|
995
|
+
|
|
996
|
+
hash = Sidekiq.load_json(info)
|
|
997
|
+
Process.new(hash.merge("busy" => busy.to_i,
|
|
998
|
+
"beat" => beat.to_f,
|
|
999
|
+
"quiet" => quiet,
|
|
1000
|
+
"rss" => rss.to_i,
|
|
1001
|
+
"rtt_us" => rtt_us.to_i))
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
# :nodoc:
|
|
1005
|
+
# @api private
|
|
772
1006
|
def initialize(clean_plz = true)
|
|
773
1007
|
cleanup if clean_plz
|
|
774
1008
|
end
|
|
775
1009
|
|
|
776
1010
|
# Cleans up dead processes recorded in Redis.
|
|
777
1011
|
# Returns the number of processes cleaned.
|
|
1012
|
+
# :nodoc:
|
|
1013
|
+
# @api private
|
|
778
1014
|
def cleanup
|
|
1015
|
+
# dont run cleanup more than once per minute
|
|
1016
|
+
return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", "NX", "EX", "60") }
|
|
1017
|
+
|
|
779
1018
|
count = 0
|
|
780
1019
|
Sidekiq.redis do |conn|
|
|
781
|
-
procs = conn.
|
|
782
|
-
heartbeats = conn.pipelined {
|
|
1020
|
+
procs = conn.sscan("processes").to_a
|
|
1021
|
+
heartbeats = conn.pipelined { |pipeline|
|
|
783
1022
|
procs.each do |key|
|
|
784
|
-
|
|
1023
|
+
pipeline.hget(key, "info")
|
|
785
1024
|
end
|
|
786
1025
|
}
|
|
787
1026
|
|
|
@@ -798,30 +1037,31 @@ module Sidekiq
|
|
|
798
1037
|
|
|
799
1038
|
def each
|
|
800
1039
|
result = Sidekiq.redis { |conn|
|
|
801
|
-
procs = conn.
|
|
1040
|
+
procs = conn.sscan("processes").to_a.sort
|
|
802
1041
|
|
|
803
1042
|
# We're making a tradeoff here between consuming more memory instead of
|
|
804
1043
|
# making more roundtrips to Redis, but if you have hundreds or thousands of workers,
|
|
805
1044
|
# you'll be happier this way
|
|
806
|
-
conn.pipelined do
|
|
1045
|
+
conn.pipelined do |pipeline|
|
|
807
1046
|
procs.each do |key|
|
|
808
|
-
|
|
1047
|
+
pipeline.hmget(key, "info", "concurrency", "busy", "beat", "quiet", "rss", "rtt_us")
|
|
809
1048
|
end
|
|
810
1049
|
end
|
|
811
1050
|
}
|
|
812
1051
|
|
|
813
|
-
result.each do |info, busy,
|
|
1052
|
+
result.each do |info, concurrency, busy, beat, quiet, rss, rtt_us|
|
|
814
1053
|
# If a process is stopped between when we query Redis for `procs` and
|
|
815
1054
|
# when we query for `result`, we will have an item in `result` that is
|
|
816
1055
|
# composed of `nil` values.
|
|
817
1056
|
next if info.nil?
|
|
818
1057
|
|
|
819
1058
|
hash = Sidekiq.load_json(info)
|
|
820
|
-
yield Process.new(hash.merge("
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1059
|
+
yield Process.new(hash.merge("concurrency" => concurrency.to_i,
|
|
1060
|
+
"busy" => busy.to_i,
|
|
1061
|
+
"beat" => beat.to_f,
|
|
1062
|
+
"quiet" => quiet,
|
|
1063
|
+
"rss" => rss.to_i,
|
|
1064
|
+
"rtt_us" => rtt_us.to_i))
|
|
825
1065
|
end
|
|
826
1066
|
end
|
|
827
1067
|
|
|
@@ -829,6 +1069,7 @@ module Sidekiq
|
|
|
829
1069
|
# based on current heartbeat. #each does that and ensures the set only
|
|
830
1070
|
# contains Sidekiq processes which have sent a heartbeat within the last
|
|
831
1071
|
# 60 seconds.
|
|
1072
|
+
# @return [Integer] current number of registered Sidekiq processes
|
|
832
1073
|
def size
|
|
833
1074
|
Sidekiq.redis { |conn| conn.scard("processes") }
|
|
834
1075
|
end
|
|
@@ -836,10 +1077,12 @@ module Sidekiq
|
|
|
836
1077
|
# Total number of threads available to execute jobs.
|
|
837
1078
|
# For Sidekiq Enterprise customers this number (in production) must be
|
|
838
1079
|
# less than or equal to your licensed concurrency.
|
|
1080
|
+
# @return [Integer] the sum of process concurrency
|
|
839
1081
|
def total_concurrency
|
|
840
1082
|
sum { |x| x["concurrency"].to_i }
|
|
841
1083
|
end
|
|
842
1084
|
|
|
1085
|
+
# @return [Integer] total amount of RSS memory consumed by Sidekiq processes
|
|
843
1086
|
def total_rss_in_kb
|
|
844
1087
|
sum { |x| x["rss"].to_i }
|
|
845
1088
|
end
|
|
@@ -848,6 +1091,8 @@ module Sidekiq
|
|
|
848
1091
|
# Returns the identity of the current cluster leader or "" if no leader.
|
|
849
1092
|
# This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
|
|
850
1093
|
# or Sidekiq Pro.
|
|
1094
|
+
# @return [String] Identity of cluster leader
|
|
1095
|
+
# @return [String] empty string if no leader
|
|
851
1096
|
def leader
|
|
852
1097
|
@leader ||= begin
|
|
853
1098
|
x = Sidekiq.redis { |c| c.get("dear-leader") }
|
|
@@ -867,13 +1112,16 @@ module Sidekiq
|
|
|
867
1112
|
# 'started_at' => <process start time>,
|
|
868
1113
|
# 'pid' => 12345,
|
|
869
1114
|
# 'tag' => 'myapp'
|
|
870
|
-
# 'concurrency' =>
|
|
871
|
-
# '
|
|
872
|
-
# 'busy' =>
|
|
1115
|
+
# 'concurrency' => 5,
|
|
1116
|
+
# 'capsules' => {"default" => {"mode" => "weighted", "concurrency" => 5, "weights" => {"default" => 2, "low" => 1}}},
|
|
1117
|
+
# 'busy' => 3,
|
|
873
1118
|
# 'beat' => <last heartbeat>,
|
|
874
1119
|
# 'identity' => <unique string identifying the process>,
|
|
1120
|
+
# 'embedded' => true,
|
|
875
1121
|
# }
|
|
876
1122
|
class Process
|
|
1123
|
+
# :nodoc:
|
|
1124
|
+
# @api private
|
|
877
1125
|
def initialize(hash)
|
|
878
1126
|
@attribs = hash
|
|
879
1127
|
end
|
|
@@ -883,7 +1131,7 @@ module Sidekiq
|
|
|
883
1131
|
end
|
|
884
1132
|
|
|
885
1133
|
def labels
|
|
886
|
-
|
|
1134
|
+
self["labels"].to_a
|
|
887
1135
|
end
|
|
888
1136
|
|
|
889
1137
|
def [](key)
|
|
@@ -893,35 +1141,92 @@ module Sidekiq
|
|
|
893
1141
|
def identity
|
|
894
1142
|
self["identity"]
|
|
895
1143
|
end
|
|
1144
|
+
alias_method :id, :identity
|
|
896
1145
|
|
|
1146
|
+
# deprecated, use capsules below
|
|
897
1147
|
def queues
|
|
898
|
-
|
|
1148
|
+
# Backwards compatibility with <8.0.8
|
|
1149
|
+
if !self["capsules"]
|
|
1150
|
+
self["queues"]
|
|
1151
|
+
else
|
|
1152
|
+
capsules.values.flat_map { |x| x["weights"].keys }.uniq
|
|
1153
|
+
end
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
# deprecated, use capsules below
|
|
1157
|
+
def weights
|
|
1158
|
+
# Backwards compatibility with <8.0.8
|
|
1159
|
+
if !self["capsules"]
|
|
1160
|
+
self["weights"]
|
|
1161
|
+
else
|
|
1162
|
+
hash = {}
|
|
1163
|
+
capsules.values.each do |cap|
|
|
1164
|
+
# Note: will lose data if two capsules are processing the same named queue
|
|
1165
|
+
cap["weights"].each_pair do |queue, weight|
|
|
1166
|
+
hash[queue] = weight
|
|
1167
|
+
end
|
|
1168
|
+
end
|
|
1169
|
+
hash
|
|
1170
|
+
end
|
|
1171
|
+
end
|
|
1172
|
+
|
|
1173
|
+
def capsules
|
|
1174
|
+
self["capsules"]
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
def version
|
|
1178
|
+
self["version"]
|
|
1179
|
+
end
|
|
1180
|
+
|
|
1181
|
+
def embedded?
|
|
1182
|
+
self["embedded"]
|
|
899
1183
|
end
|
|
900
1184
|
|
|
1185
|
+
# Signal this process to stop processing new jobs.
|
|
1186
|
+
# It will continue to execute jobs it has already fetched.
|
|
1187
|
+
# This method is *asynchronous* and it can take 5-10
|
|
1188
|
+
# seconds for the process to quiet.
|
|
901
1189
|
def quiet!
|
|
1190
|
+
raise "Can't quiet an embedded process" if embedded?
|
|
1191
|
+
|
|
902
1192
|
signal("TSTP")
|
|
903
1193
|
end
|
|
904
1194
|
|
|
1195
|
+
# Signal this process to shutdown.
|
|
1196
|
+
# It will shutdown within its configured :timeout value, default 25 seconds.
|
|
1197
|
+
# This method is *asynchronous* and it can take 5-10
|
|
1198
|
+
# seconds for the process to start shutting down.
|
|
905
1199
|
def stop!
|
|
1200
|
+
raise "Can't stop an embedded process" if embedded?
|
|
1201
|
+
|
|
906
1202
|
signal("TERM")
|
|
907
1203
|
end
|
|
908
1204
|
|
|
1205
|
+
# Signal this process to log backtraces for all threads.
|
|
1206
|
+
# Useful if you have a frozen or deadlocked process which is
|
|
1207
|
+
# still sending a heartbeat.
|
|
1208
|
+
# This method is *asynchronous* and it can take 5-10 seconds.
|
|
909
1209
|
def dump_threads
|
|
910
1210
|
signal("TTIN")
|
|
911
1211
|
end
|
|
912
1212
|
|
|
1213
|
+
# @return [Boolean] true if this process is quiet or shutting down
|
|
913
1214
|
def stopping?
|
|
914
1215
|
self["quiet"] == "true"
|
|
915
1216
|
end
|
|
916
1217
|
|
|
1218
|
+
def leader?
|
|
1219
|
+
Sidekiq.redis { |c| c.get("dear-leader") == identity }
|
|
1220
|
+
end
|
|
1221
|
+
|
|
917
1222
|
private
|
|
918
1223
|
|
|
919
1224
|
def signal(sig)
|
|
920
1225
|
key = "#{identity}-signals"
|
|
921
1226
|
Sidekiq.redis do |c|
|
|
922
|
-
c.multi do
|
|
923
|
-
|
|
924
|
-
|
|
1227
|
+
c.multi do |transaction|
|
|
1228
|
+
transaction.lpush(key, sig)
|
|
1229
|
+
transaction.expire(key, 60)
|
|
925
1230
|
end
|
|
926
1231
|
end
|
|
927
1232
|
end
|
|
@@ -942,9 +1247,8 @@ module Sidekiq
|
|
|
942
1247
|
# works.each do |process_id, thread_id, work|
|
|
943
1248
|
# # process_id is a unique identifier per Sidekiq process
|
|
944
1249
|
# # thread_id is a unique identifier per thread
|
|
945
|
-
# # work is a
|
|
946
|
-
# #
|
|
947
|
-
# # run_at is an epoch Integer.
|
|
1250
|
+
# # work is a `Sidekiq::Work` instance that has the following accessor methods.
|
|
1251
|
+
# # [work.queue, work.run_at, work.payload]
|
|
948
1252
|
# end
|
|
949
1253
|
#
|
|
950
1254
|
class WorkSet
|
|
@@ -952,25 +1256,25 @@ module Sidekiq
|
|
|
952
1256
|
|
|
953
1257
|
def each(&block)
|
|
954
1258
|
results = []
|
|
1259
|
+
procs = nil
|
|
1260
|
+
all_works = nil
|
|
1261
|
+
|
|
955
1262
|
Sidekiq.redis do |conn|
|
|
956
|
-
procs = conn.
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
conn.hgetall("#{key}:workers")
|
|
961
|
-
}
|
|
962
|
-
next unless valid
|
|
963
|
-
workers.each_pair do |tid, json|
|
|
964
|
-
hsh = Sidekiq.load_json(json)
|
|
965
|
-
p = hsh["payload"]
|
|
966
|
-
# avoid breaking API, this is a side effect of the JSON optimization in #4316
|
|
967
|
-
hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
|
|
968
|
-
results << [key, tid, hsh]
|
|
1263
|
+
procs = conn.sscan("processes").to_a.sort
|
|
1264
|
+
all_works = conn.pipelined do |pipeline|
|
|
1265
|
+
procs.each do |key|
|
|
1266
|
+
pipeline.hgetall("#{key}:work")
|
|
969
1267
|
end
|
|
970
1268
|
end
|
|
971
1269
|
end
|
|
972
1270
|
|
|
973
|
-
|
|
1271
|
+
procs.zip(all_works).each do |key, workers|
|
|
1272
|
+
workers.each_pair do |tid, json|
|
|
1273
|
+
results << [key, tid, Sidekiq::Work.new(key, tid, Sidekiq.load_json(json))] unless json.empty?
|
|
1274
|
+
end
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
results.sort_by { |(_, _, work)| work.run_at }.each(&block)
|
|
974
1278
|
end
|
|
975
1279
|
|
|
976
1280
|
# Note that #size is only as accurate as Sidekiq's heartbeat,
|
|
@@ -981,21 +1285,182 @@ module Sidekiq
|
|
|
981
1285
|
# which can easily get out of sync with crashy processes.
|
|
982
1286
|
def size
|
|
983
1287
|
Sidekiq.redis do |conn|
|
|
984
|
-
procs = conn.
|
|
1288
|
+
procs = conn.sscan("processes").to_a
|
|
985
1289
|
if procs.empty?
|
|
986
1290
|
0
|
|
987
1291
|
else
|
|
988
|
-
conn.pipelined {
|
|
1292
|
+
conn.pipelined { |pipeline|
|
|
989
1293
|
procs.each do |key|
|
|
990
|
-
|
|
1294
|
+
pipeline.hget(key, "busy")
|
|
991
1295
|
end
|
|
992
1296
|
}.sum(&:to_i)
|
|
993
1297
|
end
|
|
994
1298
|
end
|
|
995
1299
|
end
|
|
1300
|
+
|
|
1301
|
+
##
|
|
1302
|
+
# Find the work which represents a job with the given JID.
|
|
1303
|
+
# *This is a slow O(n) operation*. Do not use for app logic.
|
|
1304
|
+
#
|
|
1305
|
+
# @param jid [String] the job identifier
|
|
1306
|
+
# @return [Sidekiq::Work] the work or nil
|
|
1307
|
+
def find_work(jid)
|
|
1308
|
+
each do |_process_id, _thread_id, work|
|
|
1309
|
+
job = work.job
|
|
1310
|
+
return work if job.jid == jid
|
|
1311
|
+
end
|
|
1312
|
+
nil
|
|
1313
|
+
end
|
|
1314
|
+
alias_method :find_work_by_jid, :find_work
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1317
|
+
# Sidekiq::Work represents a job which is currently executing.
|
|
1318
|
+
class Work
|
|
1319
|
+
attr_reader :process_id
|
|
1320
|
+
attr_reader :thread_id
|
|
1321
|
+
|
|
1322
|
+
def initialize(pid, tid, hsh)
|
|
1323
|
+
@process_id = pid
|
|
1324
|
+
@thread_id = tid
|
|
1325
|
+
@hsh = hsh
|
|
1326
|
+
@job = nil
|
|
1327
|
+
end
|
|
1328
|
+
|
|
1329
|
+
def queue
|
|
1330
|
+
@hsh["queue"]
|
|
1331
|
+
end
|
|
1332
|
+
|
|
1333
|
+
def run_at
|
|
1334
|
+
Time.at(@hsh["run_at"])
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
def job
|
|
1338
|
+
@job ||= Sidekiq::JobRecord.new(@hsh["payload"])
|
|
1339
|
+
end
|
|
1340
|
+
|
|
1341
|
+
def payload
|
|
1342
|
+
@hsh["payload"]
|
|
1343
|
+
end
|
|
996
1344
|
end
|
|
1345
|
+
|
|
997
1346
|
# Since "worker" is a nebulous term, we've deprecated the use of this class name.
|
|
998
1347
|
# Is "worker" a process, a type of job, a thread? Undefined!
|
|
999
1348
|
# WorkSet better describes the data.
|
|
1000
1349
|
Workers = WorkSet
|
|
1350
|
+
|
|
1351
|
+
class ProfileSet
|
|
1352
|
+
include Enumerable
|
|
1353
|
+
|
|
1354
|
+
# This is a point in time/snapshot API, you'll need to instantiate a new instance
|
|
1355
|
+
# if you want to fetch newer records.
|
|
1356
|
+
def initialize
|
|
1357
|
+
@records = Sidekiq.redis do |c|
|
|
1358
|
+
# This throws away expired profiles
|
|
1359
|
+
c.zremrangebyscore("profiles", "-inf", Time.now.to_f.to_s)
|
|
1360
|
+
# retreive records, newest to oldest
|
|
1361
|
+
c.zrange("profiles", "+inf", 0, "byscore", "rev")
|
|
1362
|
+
end
|
|
1363
|
+
end
|
|
1364
|
+
|
|
1365
|
+
def size
|
|
1366
|
+
@records.size
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1369
|
+
def each(&block)
|
|
1370
|
+
fetch_keys = %w[started_at jid type token size elapsed].freeze
|
|
1371
|
+
arrays = Sidekiq.redis do |c|
|
|
1372
|
+
c.pipelined do |p|
|
|
1373
|
+
@records.each do |key|
|
|
1374
|
+
p.hmget(key, *fetch_keys)
|
|
1375
|
+
end
|
|
1376
|
+
end
|
|
1377
|
+
end
|
|
1378
|
+
|
|
1379
|
+
arrays.compact.map { |arr| ProfileRecord.new(arr) }.each(&block)
|
|
1380
|
+
end
|
|
1381
|
+
end
|
|
1382
|
+
|
|
1383
|
+
class ProfileRecord
|
|
1384
|
+
attr_reader :started_at, :jid, :type, :token, :size, :elapsed
|
|
1385
|
+
|
|
1386
|
+
def initialize(arr)
|
|
1387
|
+
# Must be same order as fetch_keys above
|
|
1388
|
+
@started_at = Time.at(Integer(arr[0]))
|
|
1389
|
+
@jid = arr[1]
|
|
1390
|
+
@type = arr[2]
|
|
1391
|
+
@token = arr[3]
|
|
1392
|
+
@size = Integer(arr[4])
|
|
1393
|
+
@elapsed = Float(arr[5])
|
|
1394
|
+
end
|
|
1395
|
+
|
|
1396
|
+
def key
|
|
1397
|
+
"#{token}-#{jid}"
|
|
1398
|
+
end
|
|
1399
|
+
|
|
1400
|
+
def data
|
|
1401
|
+
Sidekiq.redis { |c| c.hget(key, "data") }
|
|
1402
|
+
end
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1405
|
+
# Persisted iteration state from Redis for jobs using Sidekiq::IterableJob.
|
|
1406
|
+
class IterableJobQuery
|
|
1407
|
+
def initialize(jids)
|
|
1408
|
+
@cache = bulk_fetch(jids)
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
def [](jid)
|
|
1412
|
+
@cache[jid]
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1415
|
+
private
|
|
1416
|
+
|
|
1417
|
+
# Bulk-fetch iteration state for multiple JIDs in a single Redis pipeline.
|
|
1418
|
+
# Returns a Hash of { jid => IterableJobState } for JIDs that have iteration state.
|
|
1419
|
+
def bulk_fetch(jids)
|
|
1420
|
+
raise ArgumentError unless jids
|
|
1421
|
+
jids_to_fetch = Array(jids).compact.uniq
|
|
1422
|
+
return {} if jids_to_fetch.empty?
|
|
1423
|
+
|
|
1424
|
+
results = Sidekiq.redis do |conn|
|
|
1425
|
+
conn.pipelined do |pipe|
|
|
1426
|
+
jids_to_fetch.each { |jid| pipe.hgetall("it-#{jid}") }
|
|
1427
|
+
end
|
|
1428
|
+
end
|
|
1429
|
+
|
|
1430
|
+
# TODO Requires Ruby 4
|
|
1431
|
+
# states = ::Hash.new(capacity: jids_to_fetch.size)
|
|
1432
|
+
states = {}
|
|
1433
|
+
jids_to_fetch.each_with_index do |jid, i|
|
|
1434
|
+
raw = results[i]
|
|
1435
|
+
next if raw.nil? || raw.empty?
|
|
1436
|
+
|
|
1437
|
+
states[jid] = State.new(jid, raw)
|
|
1438
|
+
end
|
|
1439
|
+
states
|
|
1440
|
+
end
|
|
1441
|
+
|
|
1442
|
+
State = Struct.new(:jid, :raw) do
|
|
1443
|
+
def executions
|
|
1444
|
+
raw["ex"].to_i
|
|
1445
|
+
end
|
|
1446
|
+
|
|
1447
|
+
def runtime
|
|
1448
|
+
raw["rt"].to_f
|
|
1449
|
+
end
|
|
1450
|
+
|
|
1451
|
+
def cursor
|
|
1452
|
+
@cursor ||= begin
|
|
1453
|
+
Sidekiq.load_json(raw["c"])
|
|
1454
|
+
rescue JSON::ParserError
|
|
1455
|
+
@raw["c"]
|
|
1456
|
+
end
|
|
1457
|
+
end
|
|
1458
|
+
|
|
1459
|
+
def cancelled
|
|
1460
|
+
raw["cancelled"]&.to_i
|
|
1461
|
+
end
|
|
1462
|
+
end
|
|
1463
|
+
end
|
|
1001
1464
|
end
|
|
1465
|
+
|
|
1466
|
+
Sidekiq.loader.run_load_hooks(:api)
|