sidekiq 6.5.1 → 7.3.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Changes.md +376 -12
- data/README.md +43 -35
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiq +3 -8
- data/bin/sidekiqload +213 -118
- data/bin/sidekiqmon +3 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +88 -0
- data/lib/generators/sidekiq/job_generator.rb +2 -0
- data/lib/sidekiq/api.rb +378 -173
- data/lib/sidekiq/capsule.rb +132 -0
- data/lib/sidekiq/cli.rb +61 -63
- data/lib/sidekiq/client.rb +89 -40
- data/lib/sidekiq/component.rb +6 -2
- data/lib/sidekiq/config.rb +305 -0
- data/lib/sidekiq/deploy.rb +64 -0
- data/lib/sidekiq/embedded.rb +63 -0
- data/lib/sidekiq/fetch.rb +11 -14
- data/lib/sidekiq/iterable_job.rb +55 -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 +294 -0
- data/lib/sidekiq/job.rb +382 -10
- data/lib/sidekiq/job_logger.rb +8 -7
- data/lib/sidekiq/job_retry.rb +89 -46
- data/lib/sidekiq/job_util.rb +53 -15
- data/lib/sidekiq/launcher.rb +77 -69
- data/lib/sidekiq/logger.rb +2 -27
- data/lib/sidekiq/manager.rb +9 -11
- data/lib/sidekiq/metrics/query.rb +158 -0
- data/lib/sidekiq/metrics/shared.rb +106 -0
- data/lib/sidekiq/metrics/tracking.rb +148 -0
- data/lib/sidekiq/middleware/chain.rb +84 -48
- data/lib/sidekiq/middleware/current_attributes.rb +87 -20
- data/lib/sidekiq/middleware/modules.rb +2 -0
- data/lib/sidekiq/monitor.rb +19 -5
- data/lib/sidekiq/paginator.rb +11 -3
- data/lib/sidekiq/processor.rb +67 -56
- data/lib/sidekiq/rails.rb +22 -16
- data/lib/sidekiq/redis_client_adapter.rb +31 -71
- data/lib/sidekiq/redis_connection.rb +44 -117
- data/lib/sidekiq/ring_buffer.rb +2 -0
- data/lib/sidekiq/scheduled.rb +62 -35
- data/lib/sidekiq/systemd.rb +2 -0
- data/lib/sidekiq/testing.rb +37 -46
- data/lib/sidekiq/transaction_aware_client.rb +11 -5
- data/lib/sidekiq/version.rb +6 -1
- data/lib/sidekiq/web/action.rb +15 -5
- data/lib/sidekiq/web/application.rb +94 -24
- data/lib/sidekiq/web/csrf_protection.rb +10 -7
- data/lib/sidekiq/web/helpers.rb +118 -45
- data/lib/sidekiq/web/router.rb +5 -2
- data/lib/sidekiq/web.rb +67 -15
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +78 -266
- data/sidekiq.gemspec +12 -10
- data/web/assets/javascripts/application.js +46 -1
- data/web/assets/javascripts/base-charts.js +106 -0
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/web/assets/javascripts/dashboard-charts.js +192 -0
- data/web/assets/javascripts/dashboard.js +11 -250
- data/web/assets/javascripts/metrics.js +298 -0
- data/web/assets/stylesheets/application-dark.css +4 -0
- data/web/assets/stylesheets/application-rtl.css +10 -89
- data/web/assets/stylesheets/application.css +98 -295
- data/web/locales/ar.yml +70 -70
- data/web/locales/cs.yml +62 -62
- data/web/locales/da.yml +60 -53
- data/web/locales/de.yml +65 -65
- data/web/locales/el.yml +43 -24
- data/web/locales/en.yml +83 -69
- data/web/locales/es.yml +68 -68
- data/web/locales/fa.yml +65 -65
- data/web/locales/fr.yml +80 -67
- data/web/locales/gd.yml +98 -0
- data/web/locales/he.yml +65 -64
- data/web/locales/hi.yml +59 -59
- data/web/locales/it.yml +85 -54
- data/web/locales/ja.yml +72 -68
- data/web/locales/ko.yml +52 -52
- data/web/locales/lt.yml +66 -66
- data/web/locales/nb.yml +61 -61
- data/web/locales/nl.yml +52 -52
- data/web/locales/pl.yml +45 -45
- data/web/locales/pt-br.yml +78 -69
- data/web/locales/pt.yml +51 -51
- data/web/locales/ru.yml +67 -66
- data/web/locales/sv.yml +53 -53
- data/web/locales/ta.yml +60 -60
- data/web/locales/tr.yml +100 -0
- data/web/locales/uk.yml +85 -61
- data/web/locales/ur.yml +64 -64
- data/web/locales/vi.yml +67 -67
- data/web/locales/zh-cn.yml +42 -16
- data/web/locales/zh-tw.yml +41 -8
- data/web/views/_footer.erb +17 -2
- data/web/views/_job_info.erb +18 -2
- data/web/views/_metrics_period_select.erb +12 -0
- data/web/views/_nav.erb +1 -1
- data/web/views/_paging.erb +2 -0
- data/web/views/_poll_link.erb +1 -1
- data/web/views/_summary.erb +7 -7
- data/web/views/busy.erb +49 -33
- data/web/views/dashboard.erb +28 -6
- data/web/views/filtering.erb +6 -0
- data/web/views/layout.erb +6 -6
- data/web/views/metrics.erb +90 -0
- data/web/views/metrics_for_job.erb +59 -0
- data/web/views/morgue.erb +5 -9
- data/web/views/queue.erb +15 -15
- data/web/views/queues.erb +9 -3
- data/web/views/retries.erb +5 -9
- data/web/views/scheduled.erb +12 -13
- metadata +61 -26
- data/lib/sidekiq/.DS_Store +0 -0
- data/lib/sidekiq/delay.rb +0 -43
- data/lib/sidekiq/extensions/action_mailer.rb +0 -48
- data/lib/sidekiq/extensions/active_record.rb +0 -43
- data/lib/sidekiq/extensions/class_methods.rb +0 -43
- data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
- data/lib/sidekiq/worker.rb +0 -367
- /data/{LICENSE → LICENSE.txt} +0 -0
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
|
|
@@ -9,26 +11,32 @@ module Sidekiq
|
|
9
11
|
|
10
12
|
def validate(item)
|
11
13
|
raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: `#{item}`") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
|
12
|
-
raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array)
|
14
|
+
raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array) || item["args"].is_a?(Enumerator::Lazy)
|
13
15
|
raise(ArgumentError, "Job class must be either a Class or String representation of the class name: `#{item}`") unless item["class"].is_a?(Class) || item["class"].is_a?(String)
|
14
16
|
raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric)
|
15
17
|
raise(ArgumentError, "Job tags must be an Array: `#{item}`") if item["tags"] && !item["tags"].is_a?(Array)
|
18
|
+
raise(ArgumentError, "retry_for must be a relative amount of time, e.g. 48.hours `#{item}`") if item["retry_for"] && item["retry_for"] > 1_000_000_000
|
16
19
|
end
|
17
20
|
|
18
21
|
def verify_json(item)
|
19
22
|
job_class = item["wrapped"] || item["class"]
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
23
|
+
args = item["args"]
|
24
|
+
mode = Sidekiq::Config::DEFAULTS[:on_complex_arguments]
|
25
|
+
|
26
|
+
if mode == :raise || mode == :warn
|
27
|
+
if (unsafe_item = json_unsafe?(args))
|
28
|
+
msg = <<~EOM
|
29
|
+
Job arguments to #{job_class} must be native JSON types, but #{unsafe_item.inspect} is a #{unsafe_item.class}.
|
30
|
+
See https://github.com/sidekiq/sidekiq/wiki/Best-Practices
|
31
|
+
To disable this error, add `Sidekiq.strict_args!(false)` to your initializer.
|
32
|
+
EOM
|
33
|
+
|
34
|
+
if mode == :raise
|
35
|
+
raise(ArgumentError, msg)
|
36
|
+
else
|
37
|
+
warn(msg)
|
38
|
+
end
|
39
|
+
end
|
32
40
|
end
|
33
41
|
end
|
34
42
|
|
@@ -49,6 +57,7 @@ module Sidekiq
|
|
49
57
|
item["jid"] ||= SecureRandom.hex(12)
|
50
58
|
item["class"] = item["class"].to_s
|
51
59
|
item["queue"] = item["queue"].to_s
|
60
|
+
item["retry_for"] = item["retry_for"].to_i if item["retry_for"]
|
52
61
|
item["created_at"] ||= Time.now.to_f
|
53
62
|
item
|
54
63
|
end
|
@@ -64,8 +73,37 @@ module Sidekiq
|
|
64
73
|
|
65
74
|
private
|
66
75
|
|
67
|
-
|
68
|
-
|
76
|
+
RECURSIVE_JSON_UNSAFE = {
|
77
|
+
Integer => ->(val) {},
|
78
|
+
Float => ->(val) {},
|
79
|
+
TrueClass => ->(val) {},
|
80
|
+
FalseClass => ->(val) {},
|
81
|
+
NilClass => ->(val) {},
|
82
|
+
String => ->(val) {},
|
83
|
+
Array => ->(val) {
|
84
|
+
val.each do |e|
|
85
|
+
unsafe_item = RECURSIVE_JSON_UNSAFE[e.class].call(e)
|
86
|
+
return unsafe_item unless unsafe_item.nil?
|
87
|
+
end
|
88
|
+
nil
|
89
|
+
},
|
90
|
+
Hash => ->(val) {
|
91
|
+
val.each do |k, v|
|
92
|
+
return k unless String === k
|
93
|
+
|
94
|
+
unsafe_item = RECURSIVE_JSON_UNSAFE[v.class].call(v)
|
95
|
+
return unsafe_item unless unsafe_item.nil?
|
96
|
+
end
|
97
|
+
nil
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
RECURSIVE_JSON_UNSAFE.default = ->(val) { val }
|
102
|
+
RECURSIVE_JSON_UNSAFE.compare_by_identity
|
103
|
+
private_constant :RECURSIVE_JSON_UNSAFE
|
104
|
+
|
105
|
+
def json_unsafe?(item)
|
106
|
+
RECURSIVE_JSON_UNSAFE[item.class].call(item)
|
69
107
|
end
|
70
108
|
end
|
71
109
|
end
|
data/lib/sidekiq/launcher.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "sidekiq/manager"
|
4
|
-
require "sidekiq/
|
4
|
+
require "sidekiq/capsule"
|
5
5
|
require "sidekiq/scheduled"
|
6
6
|
require "sidekiq/ring_buffer"
|
7
7
|
|
8
8
|
module Sidekiq
|
9
|
-
# The Launcher starts the
|
9
|
+
# The Launcher starts the Capsule Managers, the Poller thread and provides the process heartbeat.
|
10
10
|
class Launcher
|
11
11
|
include Sidekiq::Component
|
12
12
|
|
@@ -16,48 +16,56 @@ module Sidekiq
|
|
16
16
|
proc { "sidekiq" },
|
17
17
|
proc { Sidekiq::VERSION },
|
18
18
|
proc { |me, data| data["tag"] },
|
19
|
-
proc { |me, data| "[#{Processor::WORK_STATE.size} of #{
|
19
|
+
proc { |me, data| "[#{Processor::WORK_STATE.size} of #{me.config.total_concurrency} busy]" },
|
20
20
|
proc { |me, data| "stopping" if me.stopping? }
|
21
21
|
]
|
22
22
|
|
23
|
-
attr_accessor :
|
23
|
+
attr_accessor :managers, :poller
|
24
24
|
|
25
|
-
def initialize(
|
26
|
-
@config =
|
27
|
-
|
28
|
-
@
|
29
|
-
|
25
|
+
def initialize(config, embedded: false)
|
26
|
+
@config = config
|
27
|
+
@embedded = embedded
|
28
|
+
@managers = config.capsules.values.map do |cap|
|
29
|
+
Sidekiq::Manager.new(cap)
|
30
|
+
end
|
31
|
+
@poller = Sidekiq::Scheduled::Poller.new(@config)
|
30
32
|
@done = false
|
31
33
|
end
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
+
# Start this Sidekiq instance. If an embedding process already
|
36
|
+
# has a heartbeat thread, caller can use `async_beat: false`
|
37
|
+
# and instead have thread call Launcher#heartbeat every N seconds.
|
38
|
+
def run(async_beat: true)
|
39
|
+
logger.debug { @config.merge!({}) }
|
40
|
+
Sidekiq.freeze!
|
41
|
+
@thread = safe_thread("heartbeat", &method(:start_heartbeat)) if async_beat
|
35
42
|
@poller.start
|
36
|
-
@
|
43
|
+
@managers.each(&:start)
|
37
44
|
end
|
38
45
|
|
39
46
|
# Stops this instance from processing any more jobs,
|
40
|
-
#
|
41
47
|
def quiet
|
48
|
+
return if @done
|
49
|
+
|
42
50
|
@done = true
|
43
|
-
@
|
51
|
+
@managers.each(&:quiet)
|
44
52
|
@poller.terminate
|
53
|
+
fire_event(:quiet, reverse: true)
|
45
54
|
end
|
46
55
|
|
47
56
|
# Shuts down this Sidekiq instance. Waits up to the deadline for all jobs to complete.
|
48
57
|
def stop
|
49
58
|
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @config[:timeout]
|
50
59
|
|
51
|
-
|
52
|
-
@
|
53
|
-
|
54
|
-
|
55
|
-
|
60
|
+
quiet
|
61
|
+
stoppers = @managers.map do |mgr|
|
62
|
+
Thread.new do
|
63
|
+
mgr.stop(deadline)
|
64
|
+
end
|
65
|
+
end
|
56
66
|
|
57
|
-
|
58
|
-
|
59
|
-
strategy = @config[:fetch]
|
60
|
-
strategy.bulk_requeue([], @config)
|
67
|
+
fire_event(:shutdown, reverse: true)
|
68
|
+
stoppers.each(&:join)
|
61
69
|
|
62
70
|
clear_heartbeat
|
63
71
|
end
|
@@ -66,25 +74,39 @@ module Sidekiq
|
|
66
74
|
@done
|
67
75
|
end
|
68
76
|
|
77
|
+
# If embedding Sidekiq, you can have the process heartbeat
|
78
|
+
# call this method to regularly heartbeat rather than creating
|
79
|
+
# a separate thread.
|
80
|
+
def heartbeat
|
81
|
+
❤
|
82
|
+
end
|
83
|
+
|
69
84
|
private unless $TESTING
|
70
85
|
|
71
|
-
BEAT_PAUSE =
|
86
|
+
BEAT_PAUSE = 10
|
72
87
|
|
73
88
|
def start_heartbeat
|
74
89
|
loop do
|
75
|
-
|
90
|
+
beat
|
76
91
|
sleep BEAT_PAUSE
|
77
92
|
end
|
78
93
|
logger.info("Heartbeat stopping...")
|
79
94
|
end
|
80
95
|
|
96
|
+
def beat
|
97
|
+
$0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ") unless @embedded
|
98
|
+
❤
|
99
|
+
end
|
100
|
+
|
81
101
|
def clear_heartbeat
|
102
|
+
flush_stats
|
103
|
+
|
82
104
|
# Remove record from Redis since we are shutting down.
|
83
105
|
# Note we don't stop the heartbeat thread; if the process
|
84
106
|
# doesn't actually exit, it'll reappear in the Web UI.
|
85
107
|
redis do |conn|
|
86
108
|
conn.pipelined do |pipeline|
|
87
|
-
pipeline.srem("processes", identity)
|
109
|
+
pipeline.srem("processes", [identity])
|
88
110
|
pipeline.unlink("#{identity}:work")
|
89
111
|
end
|
90
112
|
end
|
@@ -92,20 +114,14 @@ module Sidekiq
|
|
92
114
|
# best effort, ignore network errors
|
93
115
|
end
|
94
116
|
|
95
|
-
def
|
96
|
-
$0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
|
97
|
-
|
98
|
-
❤
|
99
|
-
end
|
100
|
-
|
101
|
-
def self.flush_stats
|
117
|
+
def flush_stats
|
102
118
|
fails = Processor::FAILURE.reset
|
103
119
|
procd = Processor::PROCESSED.reset
|
104
120
|
return if fails + procd == 0
|
105
121
|
|
106
122
|
nowdate = Time.now.utc.strftime("%Y-%m-%d")
|
107
123
|
begin
|
108
|
-
|
124
|
+
redis do |conn|
|
109
125
|
conn.pipelined do |pipeline|
|
110
126
|
pipeline.incrby("stat:processed", procd)
|
111
127
|
pipeline.incrby("stat:processed:#{nowdate}", procd)
|
@@ -117,43 +133,29 @@ module Sidekiq
|
|
117
133
|
end
|
118
134
|
end
|
119
135
|
rescue => ex
|
120
|
-
|
121
|
-
# try to handle the exception
|
122
|
-
Sidekiq.logger.warn("Unable to flush stats: #{ex}")
|
136
|
+
logger.warn("Unable to flush stats: #{ex}")
|
123
137
|
end
|
124
138
|
end
|
125
|
-
at_exit(&method(:flush_stats))
|
126
139
|
|
127
140
|
def ❤
|
128
141
|
key = identity
|
129
142
|
fails = procd = 0
|
130
143
|
|
131
144
|
begin
|
132
|
-
|
133
|
-
procd = Processor::PROCESSED.reset
|
134
|
-
curstate = Processor::WORK_STATE.dup
|
145
|
+
flush_stats
|
135
146
|
|
136
|
-
|
147
|
+
curstate = Processor::WORK_STATE.dup
|
148
|
+
curstate.transform_values! { |val| Sidekiq.dump_json(val) }
|
137
149
|
|
138
150
|
redis do |conn|
|
139
|
-
conn.multi do |transaction|
|
140
|
-
transaction.incrby("stat:processed", procd)
|
141
|
-
transaction.incrby("stat:processed:#{nowdate}", procd)
|
142
|
-
transaction.expire("stat:processed:#{nowdate}", STATS_TTL)
|
143
|
-
|
144
|
-
transaction.incrby("stat:failed", fails)
|
145
|
-
transaction.incrby("stat:failed:#{nowdate}", fails)
|
146
|
-
transaction.expire("stat:failed:#{nowdate}", STATS_TTL)
|
147
|
-
end
|
148
|
-
|
149
151
|
# work is the current set of executing jobs
|
150
152
|
work_key = "#{key}:work"
|
151
|
-
conn.
|
153
|
+
conn.multi do |transaction|
|
152
154
|
transaction.unlink(work_key)
|
153
|
-
curstate.
|
154
|
-
transaction.hset(work_key,
|
155
|
+
if curstate.size > 0
|
156
|
+
transaction.hset(work_key, curstate)
|
157
|
+
transaction.expire(work_key, 60)
|
155
158
|
end
|
156
|
-
transaction.expire(work_key, 60)
|
157
159
|
end
|
158
160
|
end
|
159
161
|
|
@@ -162,11 +164,11 @@ module Sidekiq
|
|
162
164
|
fails = procd = 0
|
163
165
|
kb = memory_usage(::Process.pid)
|
164
166
|
|
165
|
-
_, exists, _, _,
|
167
|
+
_, exists, _, _, signal = redis { |conn|
|
166
168
|
conn.multi { |transaction|
|
167
|
-
transaction.sadd("processes", key)
|
168
|
-
transaction.exists
|
169
|
-
transaction.
|
169
|
+
transaction.sadd("processes", [key])
|
170
|
+
transaction.exists(key)
|
171
|
+
transaction.hset(key, "info", to_json,
|
170
172
|
"busy", curstate.size,
|
171
173
|
"beat", Time.now.to_f,
|
172
174
|
"rtt_us", rtt,
|
@@ -178,11 +180,10 @@ module Sidekiq
|
|
178
180
|
}
|
179
181
|
|
180
182
|
# first heartbeat or recovering from an outage and need to reestablish our heartbeat
|
181
|
-
fire_event(:heartbeat) unless exists
|
183
|
+
fire_event(:heartbeat) unless exists > 0
|
184
|
+
fire_event(:beat, oneshot: false)
|
182
185
|
|
183
|
-
|
184
|
-
|
185
|
-
::Process.kill(msg, ::Process.pid)
|
186
|
+
::Process.kill(signal, ::Process.pid) if signal && !@embedded
|
186
187
|
rescue => e
|
187
188
|
# ignore all redis/network issues
|
188
189
|
logger.error("heartbeat: #{e}")
|
@@ -216,7 +217,7 @@ module Sidekiq
|
|
216
217
|
Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
|
217
218
|
Ensure Redis is running in the same AZ or datacenter as Sidekiq.
|
218
219
|
If these values are close to 100,000, that means your Sidekiq process may be
|
219
|
-
CPU-saturated; reduce your concurrency and/or see https://github.com/
|
220
|
+
CPU-saturated; reduce your concurrency and/or see https://github.com/sidekiq/sidekiq/discussions/5039
|
220
221
|
EOM
|
221
222
|
RTT_READINGS.reset
|
222
223
|
end
|
@@ -249,13 +250,20 @@ module Sidekiq
|
|
249
250
|
"started_at" => Time.now.to_f,
|
250
251
|
"pid" => ::Process.pid,
|
251
252
|
"tag" => @config[:tag] || "",
|
252
|
-
"concurrency" => @config
|
253
|
-
"queues" => @config
|
254
|
-
"
|
255
|
-
"
|
253
|
+
"concurrency" => @config.total_concurrency,
|
254
|
+
"queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
|
255
|
+
"weights" => to_weights,
|
256
|
+
"labels" => @config[:labels].to_a,
|
257
|
+
"identity" => identity,
|
258
|
+
"version" => Sidekiq::VERSION,
|
259
|
+
"embedded" => @embedded
|
256
260
|
}
|
257
261
|
end
|
258
262
|
|
263
|
+
def to_weights
|
264
|
+
@config.capsules.values.map(&:weights)
|
265
|
+
end
|
266
|
+
|
259
267
|
def to_json
|
260
268
|
# this data changes infrequently so dump it to a string
|
261
269
|
# now so we don't need to dump it every heartbeat.
|
data/lib/sidekiq/logger.rb
CHANGED
@@ -31,12 +31,12 @@ module Sidekiq
|
|
31
31
|
"fatal" => 4
|
32
32
|
}
|
33
33
|
LEVELS.default_proc = proc do |_, level|
|
34
|
-
|
34
|
+
puts("Invalid log level: #{level.inspect}")
|
35
35
|
nil
|
36
36
|
end
|
37
37
|
|
38
38
|
LEVELS.each do |level, numeric_level|
|
39
|
-
define_method("#{level}?") do
|
39
|
+
define_method(:"#{level}?") do
|
40
40
|
local_level.nil? ? super() : local_level <= numeric_level
|
41
41
|
end
|
42
42
|
end
|
@@ -70,36 +70,11 @@ module Sidekiq
|
|
70
70
|
ensure
|
71
71
|
self.local_level = old_local_level
|
72
72
|
end
|
73
|
-
|
74
|
-
# Redefined to check severity against #level, and thus the thread-local level, rather than +@level+.
|
75
|
-
# FIXME: Remove when the minimum Ruby version supports overriding Logger#level.
|
76
|
-
def add(severity, message = nil, progname = nil, &block)
|
77
|
-
severity ||= ::Logger::UNKNOWN
|
78
|
-
progname ||= @progname
|
79
|
-
|
80
|
-
return true if @logdev.nil? || severity < level
|
81
|
-
|
82
|
-
if message.nil?
|
83
|
-
if block
|
84
|
-
message = yield
|
85
|
-
else
|
86
|
-
message = progname
|
87
|
-
progname = @progname
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
@logdev.write format_message(format_severity(severity), Time.now, progname, message)
|
92
|
-
end
|
93
73
|
end
|
94
74
|
|
95
75
|
class Logger < ::Logger
|
96
76
|
include LoggingUtils
|
97
77
|
|
98
|
-
def initialize(*args, **kwargs)
|
99
|
-
super
|
100
|
-
self.formatter = Sidekiq.log_formatter
|
101
|
-
end
|
102
|
-
|
103
78
|
module Formatters
|
104
79
|
class Base < ::Logger::Formatter
|
105
80
|
def tid
|
data/lib/sidekiq/manager.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "sidekiq/processor"
|
4
|
-
require "sidekiq/fetch"
|
5
4
|
require "set"
|
6
5
|
|
7
6
|
module Sidekiq
|
@@ -23,19 +22,19 @@ module Sidekiq
|
|
23
22
|
include Sidekiq::Component
|
24
23
|
|
25
24
|
attr_reader :workers
|
25
|
+
attr_reader :capsule
|
26
26
|
|
27
|
-
def initialize(
|
28
|
-
@config =
|
29
|
-
|
30
|
-
@count = options[:concurrency] || 10
|
27
|
+
def initialize(capsule)
|
28
|
+
@config = @capsule = capsule
|
29
|
+
@count = capsule.concurrency
|
31
30
|
raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
|
32
31
|
|
33
32
|
@done = false
|
34
33
|
@workers = Set.new
|
34
|
+
@plock = Mutex.new
|
35
35
|
@count.times do
|
36
36
|
@workers << Processor.new(@config, &method(:processor_result))
|
37
37
|
end
|
38
|
-
@plock = Mutex.new
|
39
38
|
end
|
40
39
|
|
41
40
|
def start
|
@@ -46,14 +45,12 @@ module Sidekiq
|
|
46
45
|
return if @done
|
47
46
|
@done = true
|
48
47
|
|
49
|
-
logger.info { "Terminating quiet threads" }
|
48
|
+
logger.info { "Terminating quiet threads for #{capsule.name} capsule" }
|
50
49
|
@workers.each(&:terminate)
|
51
|
-
fire_event(:quiet, reverse: true)
|
52
50
|
end
|
53
51
|
|
54
52
|
def stop(deadline)
|
55
53
|
quiet
|
56
|
-
fire_event(:shutdown, reverse: true)
|
57
54
|
|
58
55
|
# some of the shutdown events can be async,
|
59
56
|
# we don't have any way to know when they're done but
|
@@ -66,6 +63,8 @@ module Sidekiq
|
|
66
63
|
return if @workers.empty?
|
67
64
|
|
68
65
|
hard_shutdown
|
66
|
+
ensure
|
67
|
+
capsule.stop
|
69
68
|
end
|
70
69
|
|
71
70
|
def processor_result(processor, reason = nil)
|
@@ -105,8 +104,7 @@ module Sidekiq
|
|
105
104
|
# contract says that jobs are run AT LEAST once. Process termination
|
106
105
|
# is delayed until we're certain the jobs are back in Redis because
|
107
106
|
# it is worse to lose a job than to run it twice.
|
108
|
-
|
109
|
-
strategy.bulk_requeue(jobs, @config)
|
107
|
+
capsule.fetcher.bulk_requeue(jobs)
|
110
108
|
end
|
111
109
|
|
112
110
|
cleanup.each do |processor|
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sidekiq"
|
4
|
+
require "date"
|
5
|
+
require "set"
|
6
|
+
|
7
|
+
require "sidekiq/metrics/shared"
|
8
|
+
|
9
|
+
module Sidekiq
|
10
|
+
module Metrics
|
11
|
+
# Allows caller to query for Sidekiq execution metrics within Redis.
|
12
|
+
# Caller sets a set of attributes to act as filters. {#fetch} will call
|
13
|
+
# Redis and return a Hash of results.
|
14
|
+
#
|
15
|
+
# NB: all metrics and times/dates are UTC only. We specifically do not
|
16
|
+
# support timezones.
|
17
|
+
class Query
|
18
|
+
def initialize(pool: nil, now: Time.now)
|
19
|
+
@time = now.utc
|
20
|
+
@pool = pool || Sidekiq.default_configuration.redis_pool
|
21
|
+
@klass = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Get metric data for all jobs from the last hour
|
25
|
+
# +class_filter+: return only results for classes matching filter
|
26
|
+
def top_jobs(class_filter: nil, minutes: 60)
|
27
|
+
result = Result.new
|
28
|
+
|
29
|
+
time = @time
|
30
|
+
redis_results = @pool.with do |conn|
|
31
|
+
conn.pipelined do |pipe|
|
32
|
+
minutes.times do |idx|
|
33
|
+
key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
|
34
|
+
pipe.hgetall key
|
35
|
+
result.prepend_bucket time
|
36
|
+
time -= 60
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
time = @time
|
42
|
+
redis_results.each do |hash|
|
43
|
+
hash.each do |k, v|
|
44
|
+
kls, metric = k.split("|")
|
45
|
+
next if class_filter && !class_filter.match?(kls)
|
46
|
+
result.job_results[kls].add_metric metric, time, v.to_i
|
47
|
+
end
|
48
|
+
time -= 60
|
49
|
+
end
|
50
|
+
|
51
|
+
result.marks = fetch_marks(result.starts_at..result.ends_at)
|
52
|
+
|
53
|
+
result
|
54
|
+
end
|
55
|
+
|
56
|
+
def for_job(klass, minutes: 60)
|
57
|
+
result = Result.new
|
58
|
+
|
59
|
+
time = @time
|
60
|
+
redis_results = @pool.with do |conn|
|
61
|
+
conn.pipelined do |pipe|
|
62
|
+
minutes.times do |idx|
|
63
|
+
key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
|
64
|
+
pipe.hmget key, "#{klass}|ms", "#{klass}|p", "#{klass}|f"
|
65
|
+
result.prepend_bucket time
|
66
|
+
time -= 60
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
time = @time
|
72
|
+
@pool.with do |conn|
|
73
|
+
redis_results.each do |(ms, p, f)|
|
74
|
+
result.job_results[klass].add_metric "ms", time, ms.to_i if ms
|
75
|
+
result.job_results[klass].add_metric "p", time, p.to_i if p
|
76
|
+
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 -= 60
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
result.marks = fetch_marks(result.starts_at..result.ends_at)
|
83
|
+
|
84
|
+
result
|
85
|
+
end
|
86
|
+
|
87
|
+
class Result < Struct.new(:starts_at, :ends_at, :size, :buckets, :job_results, :marks)
|
88
|
+
def initialize
|
89
|
+
super
|
90
|
+
self.buckets = []
|
91
|
+
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
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class JobResult < Struct.new(:series, :hist, :totals)
|
103
|
+
def initialize
|
104
|
+
super
|
105
|
+
self.series = Hash.new { |h, k| h[k] = Hash.new(0) }
|
106
|
+
self.hist = Hash.new { |h, k| h[k] = [] }
|
107
|
+
self.totals = Hash.new(0)
|
108
|
+
end
|
109
|
+
|
110
|
+
def add_metric(metric, time, value)
|
111
|
+
totals[metric] += value
|
112
|
+
series[metric][time.strftime("%H:%M")] += value
|
113
|
+
|
114
|
+
# Include timing measurements in seconds for convenience
|
115
|
+
add_metric("s", time, value / 1000.0) if metric == "ms"
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_hist(time, hist_result)
|
119
|
+
hist[time.strftime("%H:%M")] = hist_result
|
120
|
+
end
|
121
|
+
|
122
|
+
def total_avg(metric = "ms")
|
123
|
+
completed = totals["p"] - totals["f"]
|
124
|
+
return 0 if completed.zero?
|
125
|
+
totals[metric].to_f / completed
|
126
|
+
end
|
127
|
+
|
128
|
+
def series_avg(metric = "ms")
|
129
|
+
series[metric].each_with_object(Hash.new(0)) do |(bucket, value), result|
|
130
|
+
completed = series.dig("p", bucket) - series.dig("f", bucket)
|
131
|
+
result[bucket] = (completed == 0) ? 0 : value.to_f / completed
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class MarkResult < Struct.new(:time, :label)
|
137
|
+
def bucket
|
138
|
+
time.strftime("%H:%M")
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def fetch_marks(time_range)
|
145
|
+
[].tap do |result|
|
146
|
+
marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
|
147
|
+
|
148
|
+
marks.each do |timestamp, label|
|
149
|
+
time = Time.parse(timestamp)
|
150
|
+
if time_range.cover? time
|
151
|
+
result << MarkResult.new(time, label)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|