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