sidekiq 7.1.4 → 8.0.9
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 +333 -0
- data/README.md +16 -13
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiqload +31 -22
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +121 -0
- data/lib/generators/sidekiq/job_generator.rb +2 -0
- data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
- data/lib/sidekiq/api.rb +260 -67
- data/lib/sidekiq/capsule.rb +17 -8
- data/lib/sidekiq/cli.rb +19 -20
- data/lib/sidekiq/client.rb +48 -15
- data/lib/sidekiq/component.rb +64 -3
- data/lib/sidekiq/config.rb +60 -18
- data/lib/sidekiq/deploy.rb +4 -2
- data/lib/sidekiq/embedded.rb +4 -1
- data/lib/sidekiq/fetch.rb +2 -1
- 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 +16 -5
- data/lib/sidekiq/job_logger.rb +15 -12
- data/lib/sidekiq/job_retry.rb +41 -13
- data/lib/sidekiq/job_util.rb +7 -1
- data/lib/sidekiq/launcher.rb +23 -11
- data/lib/sidekiq/loader.rb +57 -0
- data/lib/sidekiq/logger.rb +25 -69
- data/lib/sidekiq/manager.rb +0 -1
- data/lib/sidekiq/metrics/query.rb +76 -45
- data/lib/sidekiq/metrics/shared.rb +23 -9
- data/lib/sidekiq/metrics/tracking.rb +32 -15
- data/lib/sidekiq/middleware/current_attributes.rb +39 -14
- data/lib/sidekiq/middleware/i18n.rb +2 -0
- data/lib/sidekiq/middleware/modules.rb +2 -0
- data/lib/sidekiq/monitor.rb +6 -9
- data/lib/sidekiq/paginator.rb +16 -3
- data/lib/sidekiq/processor.rb +37 -20
- data/lib/sidekiq/profiler.rb +73 -0
- data/lib/sidekiq/rails.rb +47 -57
- data/lib/sidekiq/redis_client_adapter.rb +25 -8
- data/lib/sidekiq/redis_connection.rb +49 -9
- data/lib/sidekiq/ring_buffer.rb +3 -0
- data/lib/sidekiq/scheduled.rb +2 -2
- data/lib/sidekiq/systemd.rb +2 -0
- data/lib/sidekiq/testing.rb +34 -15
- data/lib/sidekiq/transaction_aware_client.rb +20 -5
- data/lib/sidekiq/version.rb +6 -2
- data/lib/sidekiq/web/action.rb +149 -64
- data/lib/sidekiq/web/application.rb +367 -297
- data/lib/sidekiq/web/config.rb +120 -0
- data/lib/sidekiq/web/csrf_protection.rb +8 -5
- data/lib/sidekiq/web/helpers.rb +146 -64
- data/lib/sidekiq/web/router.rb +61 -74
- data/lib/sidekiq/web.rb +53 -106
- data/lib/sidekiq.rb +11 -4
- data/sidekiq.gemspec +6 -5
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status.png +0 -0
- data/web/assets/javascripts/application.js +66 -24
- 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/dashboard-charts.js +37 -11
- data/web/assets/javascripts/dashboard.js +15 -11
- data/web/assets/javascripts/metrics.js +50 -34
- data/web/assets/stylesheets/style.css +776 -0
- data/web/locales/ar.yml +2 -0
- data/web/locales/cs.yml +2 -0
- data/web/locales/da.yml +2 -0
- data/web/locales/de.yml +2 -0
- data/web/locales/el.yml +2 -0
- data/web/locales/en.yml +12 -1
- data/web/locales/es.yml +25 -2
- data/web/locales/fa.yml +2 -0
- data/web/locales/fr.yml +2 -1
- data/web/locales/gd.yml +2 -1
- data/web/locales/he.yml +2 -0
- data/web/locales/hi.yml +2 -0
- data/web/locales/it.yml +41 -1
- data/web/locales/ja.yml +2 -1
- data/web/locales/ko.yml +2 -0
- data/web/locales/lt.yml +2 -0
- data/web/locales/nb.yml +2 -0
- data/web/locales/nl.yml +2 -0
- data/web/locales/pl.yml +2 -0
- data/web/locales/{pt-br.yml → pt-BR.yml} +4 -3
- data/web/locales/pt.yml +2 -0
- data/web/locales/ru.yml +2 -0
- data/web/locales/sv.yml +2 -0
- data/web/locales/ta.yml +2 -0
- data/web/locales/tr.yml +102 -0
- data/web/locales/uk.yml +29 -4
- data/web/locales/ur.yml +2 -0
- data/web/locales/vi.yml +2 -0
- data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -74
- data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -2
- data/web/views/_footer.erb +31 -22
- 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 +22 -21
- data/web/views/_poll_link.erb +2 -2
- data/web/views/_summary.erb +23 -23
- data/web/views/busy.erb +123 -125
- data/web/views/dashboard.erb +71 -82
- data/web/views/dead.erb +31 -27
- data/web/views/filtering.erb +6 -0
- data/web/views/layout.erb +13 -29
- data/web/views/metrics.erb +70 -68
- data/web/views/metrics_for_job.erb +30 -40
- data/web/views/morgue.erb +65 -70
- data/web/views/profiles.erb +43 -0
- data/web/views/queue.erb +54 -52
- data/web/views/queues.erb +43 -37
- data/web/views/retries.erb +70 -75
- data/web/views/retry.erb +32 -27
- data/web/views/scheduled.erb +63 -55
- data/web/views/scheduled_job_info.erb +3 -3
- metadata +49 -27
- data/web/assets/stylesheets/application-dark.css +0 -147
- data/web/assets/stylesheets/application-rtl.css +0 -153
- data/web/assets/stylesheets/application.css +0 -724
- 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/job_retry.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "zlib"
|
|
4
|
-
require "base64"
|
|
5
4
|
require "sidekiq/component"
|
|
6
5
|
|
|
7
6
|
module Sidekiq
|
|
@@ -60,8 +59,13 @@ module Sidekiq
|
|
|
60
59
|
# end
|
|
61
60
|
#
|
|
62
61
|
class JobRetry
|
|
62
|
+
# Handled means the job failed but has been dealt with
|
|
63
|
+
# (by creating a retry, rescheduling it, etc). It still
|
|
64
|
+
# needs to be logged and dispatched to error_handlers.
|
|
63
65
|
class Handled < ::RuntimeError; end
|
|
64
66
|
|
|
67
|
+
# Skip means the job failed but Sidekiq does not need to
|
|
68
|
+
# create a retry, log it or send to error_handlers.
|
|
65
69
|
class Skip < Handled; end
|
|
66
70
|
|
|
67
71
|
include Sidekiq::Component
|
|
@@ -130,11 +134,15 @@ module Sidekiq
|
|
|
130
134
|
process_retry(jobinst, msg, queue, e)
|
|
131
135
|
# We've handled this error associated with this job, don't
|
|
132
136
|
# need to handle it at the global level
|
|
133
|
-
raise
|
|
137
|
+
raise Handled
|
|
134
138
|
end
|
|
135
139
|
|
|
136
140
|
private
|
|
137
141
|
|
|
142
|
+
def now_ms
|
|
143
|
+
::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
|
|
144
|
+
end
|
|
145
|
+
|
|
138
146
|
# Note that +jobinst+ can be nil here if an error is raised before we can
|
|
139
147
|
# instantiate the job instance. All access must be guarded and
|
|
140
148
|
# best effort.
|
|
@@ -145,17 +153,17 @@ module Sidekiq
|
|
|
145
153
|
|
|
146
154
|
m = exception_message(exception)
|
|
147
155
|
if m.respond_to?(:scrub!)
|
|
148
|
-
m.force_encoding(
|
|
156
|
+
m.force_encoding(Encoding::UTF_8)
|
|
149
157
|
m.scrub!
|
|
150
158
|
end
|
|
151
159
|
|
|
152
160
|
msg["error_message"] = m
|
|
153
161
|
msg["error_class"] = exception.class.name
|
|
154
162
|
count = if msg["retry_count"]
|
|
155
|
-
msg["retried_at"] =
|
|
163
|
+
msg["retried_at"] = now_ms
|
|
156
164
|
msg["retry_count"] += 1
|
|
157
165
|
else
|
|
158
|
-
msg["failed_at"] =
|
|
166
|
+
msg["failed_at"] = now_ms
|
|
159
167
|
msg["retry_count"] = 0
|
|
160
168
|
end
|
|
161
169
|
|
|
@@ -173,19 +181,21 @@ module Sidekiq
|
|
|
173
181
|
return retries_exhausted(jobinst, msg, exception) if count >= max_retry_attempts
|
|
174
182
|
|
|
175
183
|
rf = msg["retry_for"]
|
|
176
|
-
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
|
|
177
185
|
|
|
178
186
|
strategy, delay = delay_for(jobinst, count, exception, msg)
|
|
179
187
|
case strategy
|
|
180
188
|
when :discard
|
|
181
|
-
|
|
189
|
+
msg["discarded_at"] = now_ms
|
|
190
|
+
|
|
191
|
+
return run_death_handlers(msg, exception)
|
|
182
192
|
when :kill
|
|
183
193
|
return retries_exhausted(jobinst, msg, exception)
|
|
184
194
|
end
|
|
185
195
|
|
|
186
196
|
# Logging here can break retries if the logging device raises ENOSPC #3979
|
|
187
197
|
# logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
|
|
188
|
-
jitter = rand(10
|
|
198
|
+
jitter = rand(10 * (count + 1))
|
|
189
199
|
retry_at = Time.now.to_f + delay + jitter
|
|
190
200
|
payload = Sidekiq.dump_json(msg)
|
|
191
201
|
redis do |conn|
|
|
@@ -193,6 +203,14 @@ module Sidekiq
|
|
|
193
203
|
end
|
|
194
204
|
end
|
|
195
205
|
|
|
206
|
+
def time_for(item)
|
|
207
|
+
if item.is_a?(Float)
|
|
208
|
+
Time.at(item)
|
|
209
|
+
else
|
|
210
|
+
Time.at(item / 1000, item % 1000)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
196
214
|
# returns (strategy, seconds)
|
|
197
215
|
def delay_for(jobinst, count, exception, msg)
|
|
198
216
|
rv = begin
|
|
@@ -226,7 +244,7 @@ module Sidekiq
|
|
|
226
244
|
end
|
|
227
245
|
|
|
228
246
|
def retries_exhausted(jobinst, msg, exception)
|
|
229
|
-
begin
|
|
247
|
+
rv = begin
|
|
230
248
|
block = jobinst&.sidekiq_retries_exhausted_block
|
|
231
249
|
|
|
232
250
|
# the sidekiq_retries_exhausted_block can be defined in a wrapped class (ActiveJob for instance)
|
|
@@ -239,12 +257,22 @@ module Sidekiq
|
|
|
239
257
|
handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
|
|
240
258
|
end
|
|
241
259
|
|
|
242
|
-
|
|
260
|
+
discarded = msg["dead"] == false || rv == :discard
|
|
261
|
+
|
|
262
|
+
if discarded
|
|
263
|
+
msg["discarded_at"] = now_ms
|
|
264
|
+
else
|
|
265
|
+
send_to_morgue(msg)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
run_death_handlers(msg, exception)
|
|
269
|
+
end
|
|
243
270
|
|
|
271
|
+
def run_death_handlers(job, exception)
|
|
244
272
|
@capsule.config.death_handlers.each do |handler|
|
|
245
|
-
handler.call(
|
|
273
|
+
handler.call(job, exception)
|
|
246
274
|
rescue => e
|
|
247
|
-
handle_exception(e, {context: "Error calling death handler", job:
|
|
275
|
+
handle_exception(e, {context: "Error calling death handler", job: job})
|
|
248
276
|
end
|
|
249
277
|
end
|
|
250
278
|
|
|
@@ -294,7 +322,7 @@ module Sidekiq
|
|
|
294
322
|
def compress_backtrace(backtrace)
|
|
295
323
|
serialized = Sidekiq.dump_json(backtrace)
|
|
296
324
|
compressed = Zlib::Deflate.deflate(serialized)
|
|
297
|
-
|
|
325
|
+
[compressed].pack("m0") # Base64.strict_encode64
|
|
298
326
|
end
|
|
299
327
|
end
|
|
300
328
|
end
|
data/lib/sidekiq/job_util.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "securerandom"
|
|
2
4
|
require "time"
|
|
3
5
|
|
|
@@ -56,10 +58,14 @@ module Sidekiq
|
|
|
56
58
|
item["class"] = item["class"].to_s
|
|
57
59
|
item["queue"] = item["queue"].to_s
|
|
58
60
|
item["retry_for"] = item["retry_for"].to_i if item["retry_for"]
|
|
59
|
-
item["created_at"] ||=
|
|
61
|
+
item["created_at"] ||= now_in_millis
|
|
60
62
|
item
|
|
61
63
|
end
|
|
62
64
|
|
|
65
|
+
def now_in_millis
|
|
66
|
+
::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
|
|
67
|
+
end
|
|
68
|
+
|
|
63
69
|
def normalized_hash(item_class)
|
|
64
70
|
if item_class.is_a?(Class)
|
|
65
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
|
@@ -36,8 +36,8 @@ module Sidekiq
|
|
|
36
36
|
# has a heartbeat thread, caller can use `async_beat: false`
|
|
37
37
|
# and instead have thread call Launcher#heartbeat every N seconds.
|
|
38
38
|
def run(async_beat: true)
|
|
39
|
-
Sidekiq.freeze!
|
|
40
39
|
logger.debug { @config.merge!({}) }
|
|
40
|
+
Sidekiq.freeze!
|
|
41
41
|
@thread = safe_thread("heartbeat", &method(:start_heartbeat)) if async_beat
|
|
42
42
|
@poller.start
|
|
43
43
|
@managers.each(&:start)
|
|
@@ -68,6 +68,7 @@ module Sidekiq
|
|
|
68
68
|
stoppers.each(&:join)
|
|
69
69
|
|
|
70
70
|
clear_heartbeat
|
|
71
|
+
fire_event(:exit, reverse: true)
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
def stopping?
|
|
@@ -81,7 +82,7 @@ module Sidekiq
|
|
|
81
82
|
❤
|
|
82
83
|
end
|
|
83
84
|
|
|
84
|
-
private
|
|
85
|
+
private
|
|
85
86
|
|
|
86
87
|
BEAT_PAUSE = 10
|
|
87
88
|
|
|
@@ -141,19 +142,27 @@ module Sidekiq
|
|
|
141
142
|
key = identity
|
|
142
143
|
fails = procd = 0
|
|
143
144
|
|
|
145
|
+
idle = config[:reap_connections]
|
|
146
|
+
if idle
|
|
147
|
+
config.capsules.each_value { |cap| cap.local_redis_pool.reap(idle, &:close) }
|
|
148
|
+
config.local_redis_pool.reap(idle, &:close)
|
|
149
|
+
end
|
|
150
|
+
|
|
144
151
|
begin
|
|
145
152
|
flush_stats
|
|
146
153
|
|
|
147
154
|
curstate = Processor::WORK_STATE.dup
|
|
155
|
+
curstate.transform_values! { |val| Sidekiq.dump_json(val) }
|
|
156
|
+
|
|
148
157
|
redis do |conn|
|
|
149
158
|
# work is the current set of executing jobs
|
|
150
159
|
work_key = "#{key}:work"
|
|
151
|
-
conn.
|
|
160
|
+
conn.multi do |transaction|
|
|
152
161
|
transaction.unlink(work_key)
|
|
153
|
-
curstate.
|
|
154
|
-
transaction.hset(work_key,
|
|
162
|
+
if curstate.size > 0
|
|
163
|
+
transaction.hset(work_key, curstate)
|
|
164
|
+
transaction.expire(work_key, 60)
|
|
155
165
|
end
|
|
156
|
-
transaction.expire(work_key, 60)
|
|
157
166
|
end
|
|
158
167
|
end
|
|
159
168
|
|
|
@@ -249,8 +258,15 @@ module Sidekiq
|
|
|
249
258
|
"pid" => ::Process.pid,
|
|
250
259
|
"tag" => @config[:tag] || "",
|
|
251
260
|
"concurrency" => @config.total_concurrency,
|
|
261
|
+
"capsules" => @config.capsules.each_with_object({}) { |(name, cap), memo|
|
|
262
|
+
memo[name] = cap.to_h
|
|
263
|
+
},
|
|
264
|
+
#####
|
|
265
|
+
# TODO deprecated, remove in 9.0
|
|
266
|
+
# This data is now found in the `capsules` element above
|
|
252
267
|
"queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
|
|
253
|
-
"weights" =>
|
|
268
|
+
"weights" => @config.capsules.values.map(&:weights),
|
|
269
|
+
#####
|
|
254
270
|
"labels" => @config[:labels].to_a,
|
|
255
271
|
"identity" => identity,
|
|
256
272
|
"version" => Sidekiq::VERSION,
|
|
@@ -258,10 +274,6 @@ module Sidekiq
|
|
|
258
274
|
}
|
|
259
275
|
end
|
|
260
276
|
|
|
261
|
-
def to_weights
|
|
262
|
-
@config.capsules.values.map(&:weights)
|
|
263
|
-
end
|
|
264
|
-
|
|
265
277
|
def to_json
|
|
266
278
|
# this data changes infrequently so dump it to a string
|
|
267
279
|
# now so we don't need to dump it every heartbeat.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
require "sidekiq/component"
|
|
3
|
+
|
|
4
|
+
class Loader
|
|
5
|
+
include Sidekiq::Component
|
|
6
|
+
|
|
7
|
+
def initialize(cfg = Sidekiq.default_configuration)
|
|
8
|
+
@config = cfg
|
|
9
|
+
@load_hooks = Hash.new { |h, k| h[k] = [] }
|
|
10
|
+
@loaded = Set.new
|
|
11
|
+
@lock = Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Declares a block that will be executed when a Sidekiq component is fully
|
|
15
|
+
# loaded. If the component has already loaded, the block is executed
|
|
16
|
+
# immediately.
|
|
17
|
+
#
|
|
18
|
+
# Sidekiq.loader.on_load(:api) do
|
|
19
|
+
# # extend the sidekiq API
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
def on_load(name, &block)
|
|
23
|
+
# we don't want to hold the lock while calling the block
|
|
24
|
+
to_run = nil
|
|
25
|
+
|
|
26
|
+
@lock.synchronize do
|
|
27
|
+
if @loaded.include?(name)
|
|
28
|
+
to_run = block
|
|
29
|
+
else
|
|
30
|
+
@load_hooks[name] << block
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
to_run&.call
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Executes all blocks registered to +name+ via on_load.
|
|
39
|
+
#
|
|
40
|
+
# Sidekiq.loader.run_load_hooks(:api)
|
|
41
|
+
#
|
|
42
|
+
# In the case of the above example, it will execute all hooks registered for +:api+.
|
|
43
|
+
#
|
|
44
|
+
def run_load_hooks(name)
|
|
45
|
+
hks = @lock.synchronize do
|
|
46
|
+
@loaded << name
|
|
47
|
+
@load_hooks.delete(name)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
hks&.each do |blk|
|
|
51
|
+
blk.call
|
|
52
|
+
rescue => ex
|
|
53
|
+
handle_exception(ex, hook: name)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/sidekiq/logger.rb
CHANGED
|
@@ -22,92 +22,48 @@ module Sidekiq
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
module LoggingUtils
|
|
26
|
-
LEVELS = {
|
|
27
|
-
"debug" => 0,
|
|
28
|
-
"info" => 1,
|
|
29
|
-
"warn" => 2,
|
|
30
|
-
"error" => 3,
|
|
31
|
-
"fatal" => 4
|
|
32
|
-
}
|
|
33
|
-
LEVELS.default_proc = proc do |_, level|
|
|
34
|
-
puts("Invalid log level: #{level.inspect}")
|
|
35
|
-
nil
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
LEVELS.each do |level, numeric_level|
|
|
39
|
-
define_method("#{level}?") do
|
|
40
|
-
local_level.nil? ? super() : local_level <= numeric_level
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def local_level
|
|
45
|
-
Thread.current[:sidekiq_log_level]
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def local_level=(level)
|
|
49
|
-
case level
|
|
50
|
-
when Integer
|
|
51
|
-
Thread.current[:sidekiq_log_level] = level
|
|
52
|
-
when Symbol, String
|
|
53
|
-
Thread.current[:sidekiq_log_level] = LEVELS[level.to_s]
|
|
54
|
-
when nil
|
|
55
|
-
Thread.current[:sidekiq_log_level] = nil
|
|
56
|
-
else
|
|
57
|
-
raise ArgumentError, "Invalid log level: #{level.inspect}"
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def level
|
|
62
|
-
local_level || super
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Change the thread-local level for the duration of the given block.
|
|
66
|
-
def log_at(level)
|
|
67
|
-
old_local_level = local_level
|
|
68
|
-
self.local_level = level
|
|
69
|
-
yield
|
|
70
|
-
ensure
|
|
71
|
-
self.local_level = old_local_level
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
25
|
class Logger < ::Logger
|
|
76
|
-
include LoggingUtils
|
|
77
|
-
|
|
78
26
|
module Formatters
|
|
79
27
|
class Base < ::Logger::Formatter
|
|
28
|
+
COLORS = {
|
|
29
|
+
"DEBUG" => "\e[1;32mDEBUG\e[0m", # green
|
|
30
|
+
"INFO" => "\e[1;34mINFO \e[0m", # blue
|
|
31
|
+
"WARN" => "\e[1;33mWARN \e[0m", # yellow
|
|
32
|
+
"ERROR" => "\e[1;31mERROR\e[0m", # red
|
|
33
|
+
"FATAL" => "\e[1;35mFATAL\e[0m" # pink
|
|
34
|
+
}
|
|
35
|
+
|
|
80
36
|
def tid
|
|
81
37
|
Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
|
|
82
38
|
end
|
|
83
39
|
|
|
84
|
-
def
|
|
85
|
-
|
|
40
|
+
def format_context(ctxt = Sidekiq::Context.current)
|
|
41
|
+
(ctxt.size == 0) ? "" : " #{ctxt.map { |k, v|
|
|
42
|
+
case v
|
|
43
|
+
when Array
|
|
44
|
+
"#{k}=#{v.join(",")}"
|
|
45
|
+
else
|
|
46
|
+
"#{k}=#{v}"
|
|
47
|
+
end
|
|
48
|
+
}.join(" ")}"
|
|
86
49
|
end
|
|
50
|
+
end
|
|
87
51
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
case v
|
|
92
|
-
when Array
|
|
93
|
-
"#{k}=#{v.join(",")}"
|
|
94
|
-
else
|
|
95
|
-
"#{k}=#{v}"
|
|
96
|
-
end
|
|
97
|
-
}.join(" ")
|
|
98
|
-
end
|
|
52
|
+
class Pretty < Base
|
|
53
|
+
def call(severity, time, program_name, message)
|
|
54
|
+
"#{COLORS[severity]} #{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context}: #{message}\n"
|
|
99
55
|
end
|
|
100
56
|
end
|
|
101
57
|
|
|
102
|
-
class
|
|
58
|
+
class Plain < Base
|
|
103
59
|
def call(severity, time, program_name, message)
|
|
104
|
-
"#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context}
|
|
60
|
+
"#{severity} #{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context}: #{message}\n"
|
|
105
61
|
end
|
|
106
62
|
end
|
|
107
63
|
|
|
108
64
|
class WithoutTimestamp < Pretty
|
|
109
65
|
def call(severity, time, program_name, message)
|
|
110
|
-
"pid=#{::Process.pid} tid=#{tid}#{format_context}
|
|
66
|
+
"#{COLORS[severity]} pid=#{::Process.pid} tid=#{tid}#{format_context}: #{message}\n"
|
|
111
67
|
end
|
|
112
68
|
end
|
|
113
69
|
|
|
@@ -120,7 +76,7 @@ module Sidekiq
|
|
|
120
76
|
lvl: severity,
|
|
121
77
|
msg: message
|
|
122
78
|
}
|
|
123
|
-
c =
|
|
79
|
+
c = Sidekiq::Context.current
|
|
124
80
|
hash["ctx"] = c unless c.empty?
|
|
125
81
|
|
|
126
82
|
Sidekiq.dump_json(hash) << "\n"
|
data/lib/sidekiq/manager.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
require "date"
|
|
3
|
-
require "set"
|
|
1
|
+
# frozen_string_literal: true
|
|
4
2
|
|
|
3
|
+
require "date"
|
|
4
|
+
require "sidekiq"
|
|
5
5
|
require "sidekiq/metrics/shared"
|
|
6
6
|
|
|
7
7
|
module Sidekiq
|
|
@@ -10,7 +10,7 @@ module Sidekiq
|
|
|
10
10
|
# Caller sets a set of attributes to act as filters. {#fetch} will call
|
|
11
11
|
# Redis and return a Hash of results.
|
|
12
12
|
#
|
|
13
|
-
# NB: all metrics and times/dates are UTC only. We
|
|
13
|
+
# NB: all metrics and times/dates are UTC only. We explicitly do not
|
|
14
14
|
# support timezones.
|
|
15
15
|
class Query
|
|
16
16
|
def initialize(pool: nil, now: Time.now)
|
|
@@ -19,85 +19,113 @@ module Sidekiq
|
|
|
19
19
|
@klass = nil
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
}
|
|
25
32
|
|
|
33
|
+
# Get metric data for all jobs from the last hour
|
|
34
|
+
# +class_filter+: return only results for classes matching filter
|
|
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)
|
|
26
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
|
+
|
|
27
51
|
redis_results = @pool.with do |conn|
|
|
28
52
|
conn.pipelined do |pipe|
|
|
29
|
-
|
|
30
|
-
key =
|
|
53
|
+
count.times do |idx|
|
|
54
|
+
key = keyproc.call(time)
|
|
31
55
|
pipe.hgetall key
|
|
32
|
-
|
|
33
|
-
time -= 60
|
|
56
|
+
time -= stride
|
|
34
57
|
end
|
|
35
58
|
end
|
|
36
59
|
end
|
|
37
60
|
|
|
61
|
+
result.starts_at = time
|
|
38
62
|
time = @time
|
|
39
63
|
redis_results.each do |hash|
|
|
40
64
|
hash.each do |k, v|
|
|
41
65
|
kls, metric = k.split("|")
|
|
66
|
+
next if class_filter && !class_filter.match?(kls)
|
|
42
67
|
result.job_results[kls].add_metric metric, time, v.to_i
|
|
43
68
|
end
|
|
44
|
-
time -=
|
|
69
|
+
time -= stride
|
|
45
70
|
end
|
|
46
71
|
|
|
47
|
-
result.marks = fetch_marks(result.starts_at..result.ends_at)
|
|
48
|
-
|
|
72
|
+
result.marks = fetch_marks(result.starts_at..result.ends_at, granularity)
|
|
49
73
|
result
|
|
50
74
|
end
|
|
51
75
|
|
|
52
|
-
def for_job(klass, minutes:
|
|
53
|
-
result = Result.new
|
|
54
|
-
|
|
76
|
+
def for_job(klass, minutes: nil, hours: nil)
|
|
55
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
|
+
|
|
56
90
|
redis_results = @pool.with do |conn|
|
|
57
91
|
conn.pipelined do |pipe|
|
|
58
|
-
|
|
59
|
-
key =
|
|
92
|
+
count.times do |idx|
|
|
93
|
+
key = keyproc.call(time)
|
|
60
94
|
pipe.hmget key, "#{klass}|ms", "#{klass}|p", "#{klass}|f"
|
|
61
|
-
|
|
62
|
-
time -= 60
|
|
95
|
+
time -= stride
|
|
63
96
|
end
|
|
64
97
|
end
|
|
65
98
|
end
|
|
66
99
|
|
|
100
|
+
result.starts_at = time
|
|
67
101
|
time = @time
|
|
68
102
|
@pool.with do |conn|
|
|
69
103
|
redis_results.each do |(ms, p, f)|
|
|
70
104
|
result.job_results[klass].add_metric "ms", time, ms.to_i if ms
|
|
71
105
|
result.job_results[klass].add_metric "p", time, p.to_i if p
|
|
72
106
|
result.job_results[klass].add_metric "f", time, f.to_i if f
|
|
73
|
-
result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time).reverse
|
|
74
|
-
time -=
|
|
107
|
+
result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time).reverse if minutes
|
|
108
|
+
time -= stride
|
|
75
109
|
end
|
|
76
110
|
end
|
|
77
111
|
|
|
78
|
-
result.marks = fetch_marks(result.starts_at..result.ends_at)
|
|
79
|
-
|
|
112
|
+
result.marks = fetch_marks(result.starts_at..result.ends_at, granularity)
|
|
80
113
|
result
|
|
81
114
|
end
|
|
82
115
|
|
|
83
|
-
class Result < Struct.new(:starts_at, :ends_at, :size, :
|
|
84
|
-
def initialize
|
|
116
|
+
class Result < Struct.new(:granularity, :starts_at, :ends_at, :size, :job_results, :marks)
|
|
117
|
+
def initialize(granularity = :minutely)
|
|
85
118
|
super
|
|
86
|
-
self.
|
|
119
|
+
self.granularity = granularity
|
|
87
120
|
self.marks = []
|
|
88
|
-
self.job_results = Hash.new { |h, k| h[k] = JobResult.new }
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def prepend_bucket(time)
|
|
92
|
-
buckets.unshift time.strftime("%H:%M")
|
|
93
|
-
self.ends_at ||= time
|
|
94
|
-
self.starts_at = time
|
|
121
|
+
self.job_results = Hash.new { |h, k| h[k] = JobResult.new(granularity) }
|
|
95
122
|
end
|
|
96
123
|
end
|
|
97
124
|
|
|
98
|
-
class JobResult < Struct.new(:series, :hist, :totals)
|
|
99
|
-
def initialize
|
|
125
|
+
class JobResult < Struct.new(:granularity, :series, :hist, :totals)
|
|
126
|
+
def initialize(granularity = :minutely)
|
|
100
127
|
super
|
|
128
|
+
self.granularity = granularity
|
|
101
129
|
self.series = Hash.new { |h, k| h[k] = Hash.new(0) }
|
|
102
130
|
self.hist = Hash.new { |h, k| h[k] = [] }
|
|
103
131
|
self.totals = Hash.new(0)
|
|
@@ -105,18 +133,19 @@ module Sidekiq
|
|
|
105
133
|
|
|
106
134
|
def add_metric(metric, time, value)
|
|
107
135
|
totals[metric] += value
|
|
108
|
-
series[metric][
|
|
136
|
+
series[metric][Query.bkt_time_s(time, granularity)] += value
|
|
109
137
|
|
|
110
138
|
# Include timing measurements in seconds for convenience
|
|
111
139
|
add_metric("s", time, value / 1000.0) if metric == "ms"
|
|
112
140
|
end
|
|
113
141
|
|
|
114
142
|
def add_hist(time, hist_result)
|
|
115
|
-
hist[
|
|
143
|
+
hist[Query.bkt_time_s(time, granularity)] = hist_result
|
|
116
144
|
end
|
|
117
145
|
|
|
118
146
|
def total_avg(metric = "ms")
|
|
119
147
|
completed = totals["p"] - totals["f"]
|
|
148
|
+
return 0 if completed.zero?
|
|
120
149
|
totals[metric].to_f / completed
|
|
121
150
|
end
|
|
122
151
|
|
|
@@ -128,22 +157,24 @@ module Sidekiq
|
|
|
128
157
|
end
|
|
129
158
|
end
|
|
130
159
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
135
166
|
end
|
|
136
167
|
|
|
137
168
|
private
|
|
138
169
|
|
|
139
|
-
def fetch_marks(time_range)
|
|
170
|
+
def fetch_marks(time_range, granularity)
|
|
140
171
|
[].tap do |result|
|
|
141
172
|
marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
|
|
142
173
|
|
|
143
174
|
marks.each do |timestamp, label|
|
|
144
175
|
time = Time.parse(timestamp)
|
|
145
176
|
if time_range.cover? time
|
|
146
|
-
result << MarkResult.new(time, label)
|
|
177
|
+
result << MarkResult.new(time, label, Query.bkt_time_s(time, granularity))
|
|
147
178
|
end
|
|
148
179
|
end
|
|
149
180
|
end
|