sidekiq 7.3.9 → 8.0.0.beta2
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 +28 -0
- data/README.md +16 -13
- data/bin/sidekiqload +10 -10
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +5 -5
- data/lib/sidekiq/api.rb +120 -36
- data/lib/sidekiq/capsule.rb +6 -6
- data/lib/sidekiq/cli.rb +15 -19
- data/lib/sidekiq/client.rb +13 -16
- data/lib/sidekiq/component.rb +40 -2
- data/lib/sidekiq/config.rb +18 -15
- data/lib/sidekiq/embedded.rb +1 -0
- data/lib/sidekiq/iterable_job.rb +1 -0
- data/lib/sidekiq/job/iterable.rb +13 -4
- data/lib/sidekiq/job_retry.rb +17 -5
- data/lib/sidekiq/job_util.rb +5 -1
- data/lib/sidekiq/launcher.rb +1 -1
- data/lib/sidekiq/logger.rb +6 -10
- data/lib/sidekiq/manager.rb +0 -1
- data/lib/sidekiq/metrics/query.rb +71 -45
- data/lib/sidekiq/metrics/shared.rb +4 -1
- data/lib/sidekiq/metrics/tracking.rb +9 -7
- data/lib/sidekiq/middleware/current_attributes.rb +5 -17
- data/lib/sidekiq/paginator.rb +8 -1
- data/lib/sidekiq/processor.rb +21 -14
- data/lib/sidekiq/profiler.rb +59 -0
- data/lib/sidekiq/redis_client_adapter.rb +0 -1
- data/lib/sidekiq/testing.rb +2 -2
- data/lib/sidekiq/version.rb +2 -2
- data/lib/sidekiq/web/action.rb +104 -84
- data/lib/sidekiq/web/application.rb +347 -332
- data/lib/sidekiq/web/config.rb +116 -0
- data/lib/sidekiq/web/helpers.rb +41 -16
- data/lib/sidekiq/web/router.rb +60 -76
- data/lib/sidekiq/web.rb +51 -156
- data/lib/sidekiq.rb +1 -1
- data/sidekiq.gemspec +5 -4
- data/web/assets/javascripts/application.js +6 -13
- data/web/assets/javascripts/base-charts.js +30 -16
- data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/web/assets/javascripts/metrics.js +16 -34
- data/web/assets/stylesheets/style.css +750 -0
- data/web/locales/ar.yml +1 -0
- data/web/locales/cs.yml +1 -0
- data/web/locales/da.yml +1 -0
- data/web/locales/de.yml +1 -0
- data/web/locales/el.yml +1 -0
- data/web/locales/en.yml +6 -0
- data/web/locales/es.yml +24 -2
- data/web/locales/fa.yml +1 -0
- data/web/locales/fr.yml +1 -0
- data/web/locales/gd.yml +1 -0
- data/web/locales/he.yml +1 -0
- data/web/locales/hi.yml +1 -0
- data/web/locales/it.yml +1 -0
- data/web/locales/ja.yml +1 -0
- data/web/locales/ko.yml +1 -0
- data/web/locales/lt.yml +1 -0
- data/web/locales/nb.yml +1 -0
- data/web/locales/nl.yml +1 -0
- data/web/locales/pl.yml +1 -0
- data/web/locales/{pt-br.yml → pt-BR.yml} +2 -1
- data/web/locales/pt.yml +1 -0
- data/web/locales/ru.yml +1 -0
- data/web/locales/sv.yml +1 -0
- data/web/locales/ta.yml +1 -0
- data/web/locales/tr.yml +1 -0
- data/web/locales/uk.yml +1 -0
- data/web/locales/ur.yml +1 -0
- data/web/locales/vi.yml +1 -0
- data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -73
- data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -1
- data/web/views/_footer.erb +31 -33
- data/web/views/_job_info.erb +91 -89
- data/web/views/_metrics_period_select.erb +13 -10
- data/web/views/_nav.erb +14 -21
- data/web/views/_paging.erb +23 -21
- data/web/views/_poll_link.erb +2 -2
- data/web/views/_summary.erb +16 -16
- data/web/views/busy.erb +124 -122
- data/web/views/dashboard.erb +62 -66
- data/web/views/dead.erb +31 -27
- data/web/views/filtering.erb +3 -3
- data/web/views/layout.erb +6 -22
- data/web/views/metrics.erb +75 -81
- data/web/views/metrics_for_job.erb +45 -46
- data/web/views/morgue.erb +61 -70
- data/web/views/profiles.erb +43 -0
- data/web/views/queue.erb +54 -52
- data/web/views/queues.erb +43 -41
- data/web/views/retries.erb +66 -75
- data/web/views/retry.erb +32 -27
- data/web/views/scheduled.erb +58 -54
- data/web/views/scheduled_job_info.erb +1 -1
- metadata +32 -18
- data/web/assets/stylesheets/application-dark.css +0 -147
- data/web/assets/stylesheets/application-rtl.css +0 -163
- data/web/assets/stylesheets/application.css +0 -759
- data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
- data/web/assets/stylesheets/bootstrap.css +0 -5
- data/web/views/_status.erb +0 -4
data/lib/sidekiq/client.rb
CHANGED
@@ -67,9 +67,7 @@ module Sidekiq
|
|
67
67
|
c.pipelined do |p|
|
68
68
|
p.hsetnx(key, "cancelled", Time.now.to_i)
|
69
69
|
p.hget(key, "cancelled")
|
70
|
-
p.expire(key, Sidekiq::Job::Iterable::STATE_TTL)
|
71
|
-
# TODO When Redis 7.2 is required
|
72
|
-
# p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
|
70
|
+
p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
|
73
71
|
end
|
74
72
|
end
|
75
73
|
result.to_i
|
@@ -266,22 +264,21 @@ module Sidekiq
|
|
266
264
|
if payloads.first.key?("at")
|
267
265
|
conn.zadd("schedule", payloads.flat_map { |hash|
|
268
266
|
at = hash["at"].to_s
|
269
|
-
# ActiveJob sets
|
270
|
-
hash.
|
271
|
-
# TODO: Use hash.except("at") when support for Ruby 2.7 is dropped
|
272
|
-
hash = hash.dup
|
273
|
-
hash.delete("at")
|
267
|
+
# ActiveJob sets enqueued_at but the job has not been enqueued yet
|
268
|
+
hash = hash.except("enqueued_at", "at")
|
274
269
|
[at, Sidekiq.dump_json(hash)]
|
275
270
|
})
|
276
271
|
else
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
272
|
+
now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) # milliseconds since the epoch
|
273
|
+
grouped_queues = payloads.group_by { |job| job["queue"] }
|
274
|
+
conn.sadd("queues", grouped_queues.keys)
|
275
|
+
grouped_queues.each do |queue, grouped_payloads|
|
276
|
+
to_push = grouped_payloads.map { |entry|
|
277
|
+
entry["enqueued_at"] = now
|
278
|
+
Sidekiq.dump_json(entry)
|
279
|
+
}
|
280
|
+
conn.lpush("queue:#{queue}", to_push)
|
281
|
+
end
|
285
282
|
end
|
286
283
|
end
|
287
284
|
end
|
data/lib/sidekiq/component.rb
CHANGED
@@ -1,11 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Sidekiq
|
4
|
+
# Ruby's default thread priority is 0, which uses 100ms time slices.
|
5
|
+
# This can lead to some surprising thread starvation; if using a lot of
|
6
|
+
# CPU-heavy concurrency, it may take several seconds before a Thread gets
|
7
|
+
# on the CPU.
|
8
|
+
#
|
9
|
+
# Negative priorities lower the timeslice by half, so -1 = 50ms, -2 = 25ms, etc.
|
10
|
+
# With more frequent timeslices, we reduce the risk of unintentional timeouts
|
11
|
+
# and starvation.
|
12
|
+
#
|
13
|
+
# Customize like so:
|
14
|
+
#
|
15
|
+
# Sidekiq.configure_server do |cfg|
|
16
|
+
# cfg.thread_priority = 0
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
DEFAULT_THREAD_PRIORITY = -1
|
20
|
+
|
4
21
|
##
|
5
22
|
# Sidekiq::Component assumes a config instance is available at @config
|
6
23
|
module Component # :nodoc:
|
7
24
|
attr_reader :config
|
8
25
|
|
26
|
+
# This is epoch milliseconds, appropriate for persistence
|
27
|
+
def real_ms
|
28
|
+
::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
|
29
|
+
end
|
30
|
+
|
31
|
+
# used for time difference and relative comparisons, not persistence.
|
32
|
+
def mono_ms
|
33
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
|
34
|
+
end
|
35
|
+
|
9
36
|
def watchdog(last_words)
|
10
37
|
yield
|
11
38
|
rescue Exception => ex
|
@@ -13,11 +40,11 @@ module Sidekiq
|
|
13
40
|
raise ex
|
14
41
|
end
|
15
42
|
|
16
|
-
def safe_thread(name, &block)
|
43
|
+
def safe_thread(name, priority: nil, &block)
|
17
44
|
Thread.new do
|
18
45
|
Thread.current.name = "sidekiq.#{name}"
|
19
46
|
watchdog(name, &block)
|
20
|
-
end
|
47
|
+
end.tap { |t| t.priority = (priority || config.thread_priority || DEFAULT_THREAD_PRIORITY) }
|
21
48
|
end
|
22
49
|
|
23
50
|
def logger
|
@@ -86,5 +113,16 @@ module Sidekiq
|
|
86
113
|
end.join(", ")
|
87
114
|
}>"
|
88
115
|
end
|
116
|
+
|
117
|
+
def default_tag(dir = Dir.pwd)
|
118
|
+
name = File.basename(dir)
|
119
|
+
prevdir = File.dirname(dir) # Capistrano release directory?
|
120
|
+
if name.to_i != 0 && prevdir
|
121
|
+
if File.basename(prevdir) == "releases"
|
122
|
+
return File.basename(File.dirname(prevdir))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
name
|
126
|
+
end
|
89
127
|
end
|
90
128
|
end
|
data/lib/sidekiq/config.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "forwardable"
|
4
|
-
|
5
|
-
require "set"
|
6
4
|
require "sidekiq/redis_connection"
|
7
5
|
|
8
6
|
module Sidekiq
|
@@ -41,12 +39,22 @@ module Sidekiq
|
|
41
39
|
}
|
42
40
|
|
43
41
|
ERROR_HANDLER = ->(ex, ctx, cfg = Sidekiq.default_configuration) {
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
42
|
+
Sidekiq::Context.with(ctx) do
|
43
|
+
dev = cfg[:environment] == "development"
|
44
|
+
fancy = dev && $stdout.tty? # 🎩
|
45
|
+
# Weird logic here but we want to show the backtrace in local
|
46
|
+
# development or if verbose logging is enabled.
|
47
|
+
#
|
48
|
+
# `full_message` contains the error class, message and backtrace
|
49
|
+
# `detailed_message` contains the error class and message
|
50
|
+
#
|
51
|
+
# Absolutely terrible API names. Not useful at all to have two
|
52
|
+
# methods with similar but obscure names.
|
53
|
+
if dev || cfg.logger.debug?
|
54
|
+
cfg.logger.info { ex.full_message(highlight: fancy) }
|
55
|
+
else
|
56
|
+
cfg.logger.info { ex.detailed_message(highlight: fancy) }
|
57
|
+
end
|
50
58
|
end
|
51
59
|
}
|
52
60
|
|
@@ -60,6 +68,7 @@ module Sidekiq
|
|
60
68
|
|
61
69
|
def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
|
62
70
|
attr_reader :capsules
|
71
|
+
attr_accessor :thread_priority
|
63
72
|
|
64
73
|
def inspect
|
65
74
|
"#<#{self.class.name} @options=#{
|
@@ -293,13 +302,7 @@ module Sidekiq
|
|
293
302
|
p ["!!!!!", ex]
|
294
303
|
end
|
295
304
|
@options[:error_handlers].each do |handler|
|
296
|
-
|
297
|
-
# TODO Remove in 8.0
|
298
|
-
logger.info { "DEPRECATION: Sidekiq exception handlers now take three arguments, see #{handler}" }
|
299
|
-
handler.call(ex, {_config: self}.merge(ctx))
|
300
|
-
else
|
301
|
-
handler.call(ex, ctx, self)
|
302
|
-
end
|
305
|
+
handler.call(ex, ctx, self)
|
303
306
|
rescue Exception => e
|
304
307
|
l = logger
|
305
308
|
l.error "!!! ERROR HANDLER THREW AN ERROR !!!"
|
data/lib/sidekiq/embedded.rb
CHANGED
@@ -34,6 +34,7 @@ module Sidekiq
|
|
34
34
|
private
|
35
35
|
|
36
36
|
def housekeeping
|
37
|
+
@config[:tag] ||= default_tag
|
37
38
|
logger.info "Running in #{RUBY_DESCRIPTION}"
|
38
39
|
logger.info Sidekiq::LICENSE
|
39
40
|
logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
|
data/lib/sidekiq/iterable_job.rb
CHANGED
data/lib/sidekiq/job/iterable.rb
CHANGED
@@ -54,9 +54,7 @@ module Sidekiq
|
|
54
54
|
c.pipelined do |p|
|
55
55
|
p.hsetnx(key, "cancelled", Time.now.to_i)
|
56
56
|
p.hget(key, "cancelled")
|
57
|
-
|
58
|
-
# p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
|
59
|
-
p.expire(key, Sidekiq::Job::Iterable::STATE_TTL)
|
57
|
+
p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
|
60
58
|
end
|
61
59
|
end
|
62
60
|
@_cancelled = result.to_i
|
@@ -66,6 +64,10 @@ module Sidekiq
|
|
66
64
|
@_cancelled
|
67
65
|
end
|
68
66
|
|
67
|
+
def cursor
|
68
|
+
@_cursor.freeze
|
69
|
+
end
|
70
|
+
|
69
71
|
# A hook to override that will be called when the job starts iterating.
|
70
72
|
#
|
71
73
|
# It is called only once, for the first time.
|
@@ -93,6 +95,11 @@ module Sidekiq
|
|
93
95
|
def on_stop
|
94
96
|
end
|
95
97
|
|
98
|
+
# A hook to override that will be called when the job is cancelled.
|
99
|
+
#
|
100
|
+
def on_cancel
|
101
|
+
end
|
102
|
+
|
96
103
|
# A hook to override that will be called when the job finished iterating.
|
97
104
|
#
|
98
105
|
def on_complete
|
@@ -184,6 +191,7 @@ module Sidekiq
|
|
184
191
|
|
185
192
|
def iterate_with_enumerator(enumerator, arguments)
|
186
193
|
if is_cancelled?
|
194
|
+
on_cancel
|
187
195
|
logger.info { "Job cancelled" }
|
188
196
|
return true
|
189
197
|
end
|
@@ -202,6 +210,7 @@ module Sidekiq
|
|
202
210
|
state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
203
211
|
if cancelled
|
204
212
|
@_cancelled = true
|
213
|
+
on_cancel
|
205
214
|
logger.info { "Job cancelled" }
|
206
215
|
return true
|
207
216
|
end
|
@@ -265,7 +274,7 @@ module Sidekiq
|
|
265
274
|
Sidekiq.redis do |conn|
|
266
275
|
conn.multi do |pipe|
|
267
276
|
pipe.hset(key, state)
|
268
|
-
pipe.expire(key, STATE_TTL)
|
277
|
+
pipe.expire(key, STATE_TTL, "nx")
|
269
278
|
pipe.hget(key, "cancelled")
|
270
279
|
end
|
271
280
|
end
|
data/lib/sidekiq/job_retry.rb
CHANGED
@@ -139,6 +139,10 @@ module Sidekiq
|
|
139
139
|
|
140
140
|
private
|
141
141
|
|
142
|
+
def now_ms
|
143
|
+
::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
|
144
|
+
end
|
145
|
+
|
142
146
|
# Note that +jobinst+ can be nil here if an error is raised before we can
|
143
147
|
# instantiate the job instance. All access must be guarded and
|
144
148
|
# best effort.
|
@@ -149,17 +153,17 @@ module Sidekiq
|
|
149
153
|
|
150
154
|
m = exception_message(exception)
|
151
155
|
if m.respond_to?(:scrub!)
|
152
|
-
m.force_encoding(
|
156
|
+
m.force_encoding(Encoding::UTF_8)
|
153
157
|
m.scrub!
|
154
158
|
end
|
155
159
|
|
156
160
|
msg["error_message"] = m
|
157
161
|
msg["error_class"] = exception.class.name
|
158
162
|
count = if msg["retry_count"]
|
159
|
-
msg["retried_at"] =
|
163
|
+
msg["retried_at"] = now_ms
|
160
164
|
msg["retry_count"] += 1
|
161
165
|
else
|
162
|
-
msg["failed_at"] =
|
166
|
+
msg["failed_at"] = now_ms
|
163
167
|
msg["retry_count"] = 0
|
164
168
|
end
|
165
169
|
|
@@ -177,7 +181,7 @@ module Sidekiq
|
|
177
181
|
return retries_exhausted(jobinst, msg, exception) if count >= max_retry_attempts
|
178
182
|
|
179
183
|
rf = msg["retry_for"]
|
180
|
-
return retries_exhausted(jobinst, msg, exception) if rf && ((msg["failed_at"] + rf) < Time.now
|
184
|
+
return retries_exhausted(jobinst, msg, exception) if rf && (time_for(msg["failed_at"]) + rf) < Time.now
|
181
185
|
|
182
186
|
strategy, delay = delay_for(jobinst, count, exception, msg)
|
183
187
|
case strategy
|
@@ -189,7 +193,7 @@ module Sidekiq
|
|
189
193
|
|
190
194
|
# Logging here can break retries if the logging device raises ENOSPC #3979
|
191
195
|
# logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
|
192
|
-
jitter = rand(10
|
196
|
+
jitter = rand(10 * (count + 1))
|
193
197
|
retry_at = Time.now.to_f + delay + jitter
|
194
198
|
payload = Sidekiq.dump_json(msg)
|
195
199
|
redis do |conn|
|
@@ -197,6 +201,14 @@ module Sidekiq
|
|
197
201
|
end
|
198
202
|
end
|
199
203
|
|
204
|
+
def time_for(item)
|
205
|
+
if item.is_a?(Float)
|
206
|
+
Time.at(item)
|
207
|
+
else
|
208
|
+
Time.at(item / 1000, item % 1000)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
200
212
|
# returns (strategy, seconds)
|
201
213
|
def delay_for(jobinst, count, exception, msg)
|
202
214
|
rv = begin
|
data/lib/sidekiq/job_util.rb
CHANGED
@@ -58,10 +58,14 @@ module Sidekiq
|
|
58
58
|
item["class"] = item["class"].to_s
|
59
59
|
item["queue"] = item["queue"].to_s
|
60
60
|
item["retry_for"] = item["retry_for"].to_i if item["retry_for"]
|
61
|
-
item["created_at"] ||=
|
61
|
+
item["created_at"] ||= now_in_millis
|
62
62
|
item
|
63
63
|
end
|
64
64
|
|
65
|
+
def now_in_millis
|
66
|
+
::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
|
67
|
+
end
|
68
|
+
|
65
69
|
def normalized_hash(item_class)
|
66
70
|
if item_class.is_a?(Class)
|
67
71
|
raise(ArgumentError, "Message must include a Sidekiq::Job class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options)
|
data/lib/sidekiq/launcher.rb
CHANGED
data/lib/sidekiq/logger.rb
CHANGED
@@ -81,13 +81,9 @@ module Sidekiq
|
|
81
81
|
Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
|
82
82
|
end
|
83
83
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
def format_context
|
89
|
-
if ctx.any?
|
90
|
-
" " + ctx.compact.map { |k, v|
|
84
|
+
def format_context(ctxt = Sidekiq::Context.current)
|
85
|
+
if ctxt.size > 0
|
86
|
+
ctxt.map { |k, v|
|
91
87
|
case v
|
92
88
|
when Array
|
93
89
|
"#{k}=#{v.join(",")}"
|
@@ -101,13 +97,13 @@ module Sidekiq
|
|
101
97
|
|
102
98
|
class Pretty < Base
|
103
99
|
def call(severity, time, program_name, message)
|
104
|
-
"#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
|
100
|
+
"#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid} #{format_context} #{severity}: #{message}\n"
|
105
101
|
end
|
106
102
|
end
|
107
103
|
|
108
104
|
class WithoutTimestamp < Pretty
|
109
105
|
def call(severity, time, program_name, message)
|
110
|
-
"pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
|
106
|
+
"pid=#{::Process.pid} tid=#{tid} #{format_context} #{severity}: #{message}\n"
|
111
107
|
end
|
112
108
|
end
|
113
109
|
|
@@ -120,7 +116,7 @@ module Sidekiq
|
|
120
116
|
lvl: severity,
|
121
117
|
msg: message
|
122
118
|
}
|
123
|
-
c =
|
119
|
+
c = Sidekiq::Context.current
|
124
120
|
hash["ctx"] = c unless c.empty?
|
125
121
|
|
126
122
|
Sidekiq.dump_json(hash) << "\n"
|
data/lib/sidekiq/manager.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "sidekiq"
|
4
3
|
require "date"
|
5
|
-
require "
|
6
|
-
|
4
|
+
require "sidekiq"
|
7
5
|
require "sidekiq/metrics/shared"
|
8
6
|
|
9
7
|
module Sidekiq
|
@@ -12,7 +10,7 @@ module Sidekiq
|
|
12
10
|
# Caller sets a set of attributes to act as filters. {#fetch} will call
|
13
11
|
# Redis and return a Hash of results.
|
14
12
|
#
|
15
|
-
# NB: all metrics and times/dates are UTC only. We
|
13
|
+
# NB: all metrics and times/dates are UTC only. We explicitly do not
|
16
14
|
# support timezones.
|
17
15
|
class Query
|
18
16
|
def initialize(pool: nil, now: Time.now)
|
@@ -21,23 +19,46 @@ module Sidekiq
|
|
21
19
|
@klass = nil
|
22
20
|
end
|
23
21
|
|
22
|
+
ROLLUPS = {
|
23
|
+
# minutely aggregates per minute
|
24
|
+
minutely: [60, ->(time) { time.strftime("j|%y%m%d|%-H:%M") }],
|
25
|
+
# hourly aggregates every 10 minutes so we'll have six data points per hour
|
26
|
+
hourly: [600, ->(time) {
|
27
|
+
m = time.min
|
28
|
+
mins = (m < 10) ? "0" : m.to_s[0]
|
29
|
+
time.strftime("j|%y%m%d|%-H:#{mins}")
|
30
|
+
}]
|
31
|
+
}
|
32
|
+
|
24
33
|
# Get metric data for all jobs from the last hour
|
25
34
|
# +class_filter+: return only results for classes matching filter
|
26
|
-
|
27
|
-
|
28
|
-
|
35
|
+
# +minutes+: the number of fine-grained minute buckets to retrieve
|
36
|
+
# +hours+: the number of coarser-grained 10-minute buckets to retrieve, in hours
|
37
|
+
def top_jobs(class_filter: nil, minutes: nil, hours: nil)
|
29
38
|
time = @time
|
39
|
+
minutes = 60 unless minutes || hours
|
40
|
+
|
41
|
+
# DoS protection, sanity check
|
42
|
+
minutes = 60 if minutes && minutes > 480
|
43
|
+
hours = 72 if hours && hours > 72
|
44
|
+
|
45
|
+
granularity = hours ? :hourly : :minutely
|
46
|
+
result = Result.new(granularity)
|
47
|
+
result.ends_at = time
|
48
|
+
count = hours ? hours * 6 : minutes
|
49
|
+
stride, keyproc = ROLLUPS[granularity]
|
50
|
+
|
30
51
|
redis_results = @pool.with do |conn|
|
31
52
|
conn.pipelined do |pipe|
|
32
|
-
|
33
|
-
key =
|
53
|
+
count.times do |idx|
|
54
|
+
key = keyproc.call(time)
|
34
55
|
pipe.hgetall key
|
35
|
-
|
36
|
-
time -= 60
|
56
|
+
time -= stride
|
37
57
|
end
|
38
58
|
end
|
39
59
|
end
|
40
60
|
|
61
|
+
result.starts_at = time
|
41
62
|
time = @time
|
42
63
|
redis_results.each do |hash|
|
43
64
|
hash.each do |k, v|
|
@@ -45,63 +66,66 @@ module Sidekiq
|
|
45
66
|
next if class_filter && !class_filter.match?(kls)
|
46
67
|
result.job_results[kls].add_metric metric, time, v.to_i
|
47
68
|
end
|
48
|
-
time -=
|
69
|
+
time -= stride
|
49
70
|
end
|
50
71
|
|
51
|
-
result.marks = fetch_marks(result.starts_at..result.ends_at)
|
52
|
-
|
72
|
+
result.marks = fetch_marks(result.starts_at..result.ends_at, granularity)
|
53
73
|
result
|
54
74
|
end
|
55
75
|
|
56
|
-
def for_job(klass, minutes:
|
57
|
-
result = Result.new
|
58
|
-
|
76
|
+
def for_job(klass, minutes: nil, hours: nil)
|
59
77
|
time = @time
|
78
|
+
minutes = 60 unless minutes || hours
|
79
|
+
|
80
|
+
# DoS protection, sanity check
|
81
|
+
minutes = 60 if minutes && minutes > 480
|
82
|
+
hours = 72 if hours && hours > 72
|
83
|
+
|
84
|
+
granularity = hours ? :hourly : :minutely
|
85
|
+
result = Result.new(granularity)
|
86
|
+
result.ends_at = time
|
87
|
+
count = hours ? hours * 6 : minutes
|
88
|
+
stride, keyproc = ROLLUPS[granularity]
|
89
|
+
|
60
90
|
redis_results = @pool.with do |conn|
|
61
91
|
conn.pipelined do |pipe|
|
62
|
-
|
63
|
-
key =
|
92
|
+
count.times do |idx|
|
93
|
+
key = keyproc.call(time)
|
64
94
|
pipe.hmget key, "#{klass}|ms", "#{klass}|p", "#{klass}|f"
|
65
|
-
|
66
|
-
time -= 60
|
95
|
+
time -= stride
|
67
96
|
end
|
68
97
|
end
|
69
98
|
end
|
70
99
|
|
100
|
+
result.starts_at = time
|
71
101
|
time = @time
|
72
102
|
@pool.with do |conn|
|
73
103
|
redis_results.each do |(ms, p, f)|
|
74
104
|
result.job_results[klass].add_metric "ms", time, ms.to_i if ms
|
75
105
|
result.job_results[klass].add_metric "p", time, p.to_i if p
|
76
106
|
result.job_results[klass].add_metric "f", time, f.to_i if f
|
77
|
-
result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time).reverse
|
78
|
-
time -=
|
107
|
+
result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time).reverse if minutes
|
108
|
+
time -= stride
|
79
109
|
end
|
80
110
|
end
|
81
111
|
|
82
|
-
result.marks = fetch_marks(result.starts_at..result.ends_at)
|
83
|
-
|
112
|
+
result.marks = fetch_marks(result.starts_at..result.ends_at, granularity)
|
84
113
|
result
|
85
114
|
end
|
86
115
|
|
87
|
-
class Result < Struct.new(:starts_at, :ends_at, :size, :
|
88
|
-
def initialize
|
116
|
+
class Result < Struct.new(:granularity, :starts_at, :ends_at, :size, :job_results, :marks)
|
117
|
+
def initialize(granularity = :minutely)
|
89
118
|
super
|
90
|
-
self.
|
119
|
+
self.granularity = granularity
|
91
120
|
self.marks = []
|
92
|
-
self.job_results = Hash.new { |h, k| h[k] = JobResult.new }
|
93
|
-
end
|
94
|
-
|
95
|
-
def prepend_bucket(time)
|
96
|
-
buckets.unshift time.strftime("%H:%M")
|
97
|
-
self.ends_at ||= time
|
98
|
-
self.starts_at = time
|
121
|
+
self.job_results = Hash.new { |h, k| h[k] = JobResult.new(granularity) }
|
99
122
|
end
|
100
123
|
end
|
101
124
|
|
102
|
-
class JobResult < Struct.new(:series, :hist, :totals)
|
103
|
-
def initialize
|
125
|
+
class JobResult < Struct.new(:granularity, :series, :hist, :totals)
|
126
|
+
def initialize(granularity = :minutely)
|
104
127
|
super
|
128
|
+
self.granularity = granularity
|
105
129
|
self.series = Hash.new { |h, k| h[k] = Hash.new(0) }
|
106
130
|
self.hist = Hash.new { |h, k| h[k] = [] }
|
107
131
|
self.totals = Hash.new(0)
|
@@ -109,14 +133,14 @@ module Sidekiq
|
|
109
133
|
|
110
134
|
def add_metric(metric, time, value)
|
111
135
|
totals[metric] += value
|
112
|
-
series[metric][
|
136
|
+
series[metric][Query.bkt_time_s(time, granularity)] += value
|
113
137
|
|
114
138
|
# Include timing measurements in seconds for convenience
|
115
139
|
add_metric("s", time, value / 1000.0) if metric == "ms"
|
116
140
|
end
|
117
141
|
|
118
142
|
def add_hist(time, hist_result)
|
119
|
-
hist[
|
143
|
+
hist[Query.bkt_time_s(time, granularity)] = hist_result
|
120
144
|
end
|
121
145
|
|
122
146
|
def total_avg(metric = "ms")
|
@@ -133,22 +157,24 @@ module Sidekiq
|
|
133
157
|
end
|
134
158
|
end
|
135
159
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
160
|
+
MarkResult = Struct.new(:time, :label, :bucket)
|
161
|
+
|
162
|
+
def self.bkt_time_s(time, granularity)
|
163
|
+
# truncate time to ten minutes ("8:40", not "8:43") or one minute
|
164
|
+
truncation = (granularity == :hourly) ? 600 : 60
|
165
|
+
Time.at(time.to_i - time.to_i % truncation).utc.iso8601
|
140
166
|
end
|
141
167
|
|
142
168
|
private
|
143
169
|
|
144
|
-
def fetch_marks(time_range)
|
170
|
+
def fetch_marks(time_range, granularity)
|
145
171
|
[].tap do |result|
|
146
172
|
marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
|
147
173
|
|
148
174
|
marks.each do |timestamp, label|
|
149
175
|
time = Time.parse(timestamp)
|
150
176
|
if time_range.cover? time
|
151
|
-
result << MarkResult.new(time, label)
|
177
|
+
result << MarkResult.new(time, label, Query.bkt_time_s(time, granularity))
|
152
178
|
end
|
153
179
|
end
|
154
180
|
end
|
@@ -25,7 +25,10 @@ module Sidekiq
|
|
25
25
|
#
|
26
26
|
# To store this data, we use Redis' BITFIELD command to store unsigned 16-bit counters
|
27
27
|
# per bucket per klass per minute. It's unlikely that most people will be executing more
|
28
|
-
# than 1000 job/sec for a full minute of a specific type.
|
28
|
+
# than 1000 job/sec for a full minute of a specific type (i.e. overflow 65,536).
|
29
|
+
#
|
30
|
+
# Histograms are only stored at the fine-grained level, they are not rolled up
|
31
|
+
# for longer-term buckets.
|
29
32
|
class Histogram
|
30
33
|
include Enumerable
|
31
34
|
|
@@ -19,13 +19,13 @@ module Sidekiq
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def track(queue, klass)
|
22
|
-
start =
|
22
|
+
start = mono_ms
|
23
23
|
time_ms = 0
|
24
24
|
begin
|
25
25
|
begin
|
26
26
|
yield
|
27
27
|
ensure
|
28
|
-
finish =
|
28
|
+
finish = mono_ms
|
29
29
|
time_ms = finish - start
|
30
30
|
end
|
31
31
|
# We don't track time for failed jobs as they can have very unpredictable
|
@@ -51,7 +51,7 @@ module Sidekiq
|
|
51
51
|
end
|
52
52
|
|
53
53
|
# LONG_TERM = 90 * 24 * 60 * 60
|
54
|
-
|
54
|
+
MID_TERM = 3 * 24 * 60 * 60
|
55
55
|
SHORT_TERM = 8 * 60 * 60
|
56
56
|
|
57
57
|
def flush(time = Time.now)
|
@@ -62,8 +62,10 @@ module Sidekiq
|
|
62
62
|
|
63
63
|
now = time.utc
|
64
64
|
# nowdate = now.strftime("%Y%m%d")
|
65
|
-
#
|
66
|
-
|
65
|
+
# "250214|8:4" is the 10 minute bucket for Feb 14 2025, 08:43
|
66
|
+
nowmid = now.strftime("%y%m%d|%-H:%M")[0..-2]
|
67
|
+
# "250214|8:43" is the 1 minute bucket for Feb 14 2025, 08:43
|
68
|
+
nowshort = now.strftime("%y%m%d|%-H:%M")
|
67
69
|
count = 0
|
68
70
|
|
69
71
|
redis do |conn|
|
@@ -81,8 +83,8 @@ module Sidekiq
|
|
81
83
|
# daily or hourly rollups.
|
82
84
|
[
|
83
85
|
# ["j", jobs, nowdate, LONG_TERM],
|
84
|
-
|
85
|
-
["j", jobs,
|
86
|
+
["j", jobs, nowmid, MID_TERM],
|
87
|
+
["j", jobs, nowshort, SHORT_TERM]
|
86
88
|
].each do |prefix, data, bucket, ttl|
|
87
89
|
conn.pipelined do |xa|
|
88
90
|
stats = "#{prefix}|#{bucket}"
|