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
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Sidekiq
|
|
4
4
|
module Metrics
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
class Counter
|
|
6
|
+
def initialize
|
|
7
|
+
@value = 0
|
|
8
|
+
@lock = Mutex.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def increment
|
|
12
|
+
@lock.synchronize { @value += 1 }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def value
|
|
16
|
+
@lock.synchronize { @value }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
8
19
|
|
|
9
20
|
# Implements space-efficient but statistically useful histogram storage.
|
|
10
21
|
# A precise time histogram stores every time. Instead we break times into a set of
|
|
@@ -14,7 +25,10 @@ module Sidekiq
|
|
|
14
25
|
#
|
|
15
26
|
# To store this data, we use Redis' BITFIELD command to store unsigned 16-bit counters
|
|
16
27
|
# per bucket per klass per minute. It's unlikely that most people will be executing more
|
|
17
|
-
# 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.
|
|
18
32
|
class Histogram
|
|
19
33
|
include Enumerable
|
|
20
34
|
|
|
@@ -71,15 +85,15 @@ module Sidekiq
|
|
|
71
85
|
end
|
|
72
86
|
|
|
73
87
|
def fetch(conn, now = Time.now)
|
|
74
|
-
window = now.utc.strftime("
|
|
75
|
-
key = "
|
|
88
|
+
window = now.utc.strftime("%-d-%-H:%-M")
|
|
89
|
+
key = "h|#{@klass}-#{window}"
|
|
76
90
|
conn.bitfield_ro(key, *FETCH)
|
|
77
91
|
end
|
|
78
92
|
|
|
79
93
|
def persist(conn, now = Time.now)
|
|
80
94
|
buckets, @buckets = @buckets, []
|
|
81
|
-
window = now.utc.strftime("
|
|
82
|
-
key = "
|
|
95
|
+
window = now.utc.strftime("%-d-%-H:%-M")
|
|
96
|
+
key = "h|#{@klass}-#{window}"
|
|
83
97
|
cmd = [key, "OVERFLOW", "SAT"]
|
|
84
98
|
buckets.each_with_index do |counter, idx|
|
|
85
99
|
val = counter.value
|
|
@@ -19,23 +19,23 @@ 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
|
|
32
32
|
# execution times. more important to know average time for successful jobs so we
|
|
33
33
|
# can better recognize when a perf regression is introduced.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
track_time(klass, time_ms)
|
|
35
|
+
rescue JobRetry::Skip
|
|
36
|
+
# This is raised when iterable job is interrupted.
|
|
37
|
+
track_time(klass, time_ms)
|
|
38
|
+
raise
|
|
39
39
|
rescue Exception
|
|
40
40
|
@lock.synchronize {
|
|
41
41
|
@jobs["#{klass}|f"] += 1
|
|
@@ -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}"
|
|
@@ -100,15 +102,27 @@ module Sidekiq
|
|
|
100
102
|
|
|
101
103
|
private
|
|
102
104
|
|
|
105
|
+
def track_time(klass, time_ms)
|
|
106
|
+
@lock.synchronize {
|
|
107
|
+
@grams[klass].record_time(time_ms)
|
|
108
|
+
@jobs["#{klass}|ms"] += time_ms
|
|
109
|
+
@totals["ms"] += time_ms
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
103
113
|
def reset
|
|
104
114
|
@lock.synchronize {
|
|
105
115
|
array = [@totals, @jobs, @grams]
|
|
106
|
-
|
|
107
|
-
@jobs = Hash.new(0)
|
|
108
|
-
@grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
|
|
116
|
+
reset_instance_variables
|
|
109
117
|
array
|
|
110
118
|
}
|
|
111
119
|
end
|
|
120
|
+
|
|
121
|
+
def reset_instance_variables
|
|
122
|
+
@totals = Hash.new(0)
|
|
123
|
+
@jobs = Hash.new(0)
|
|
124
|
+
@grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
|
|
125
|
+
end
|
|
112
126
|
end
|
|
113
127
|
|
|
114
128
|
class Middleware
|
|
@@ -133,4 +147,7 @@ Sidekiq.configure_server do |config|
|
|
|
133
147
|
config.on(:beat) do
|
|
134
148
|
exec.flush
|
|
135
149
|
end
|
|
150
|
+
config.on(:exit) do
|
|
151
|
+
exec.flush
|
|
152
|
+
end
|
|
136
153
|
end
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job/arguments"
|
|
1
4
|
require "active_support/current_attributes"
|
|
2
5
|
|
|
3
6
|
module Sidekiq
|
|
@@ -18,6 +21,8 @@ module Sidekiq
|
|
|
18
21
|
# Sidekiq::CurrentAttributes.persist(["Myapp::Current", "Myapp::OtherCurrent"])
|
|
19
22
|
#
|
|
20
23
|
module CurrentAttributes
|
|
24
|
+
Serializer = ::ActiveJob::Arguments
|
|
25
|
+
|
|
21
26
|
class Save
|
|
22
27
|
include Sidekiq::ClientMiddleware
|
|
23
28
|
|
|
@@ -31,7 +36,7 @@ module Sidekiq
|
|
|
31
36
|
attrs = strklass.constantize.attributes
|
|
32
37
|
# Retries can push the job N times, we don't
|
|
33
38
|
# want retries to reset cattr. #5692, #5090
|
|
34
|
-
job[key] = attrs if attrs.any?
|
|
39
|
+
job[key] = Serializer.serialize(attrs) if attrs.any?
|
|
35
40
|
end
|
|
36
41
|
end
|
|
37
42
|
yield
|
|
@@ -45,23 +50,42 @@ module Sidekiq
|
|
|
45
50
|
@cattrs = cattrs
|
|
46
51
|
end
|
|
47
52
|
|
|
48
|
-
def call(_, job,
|
|
49
|
-
|
|
53
|
+
def call(_, job, *, &block)
|
|
54
|
+
klass_attrs = {}
|
|
50
55
|
|
|
51
56
|
@cattrs.each do |(key, strklass)|
|
|
52
|
-
|
|
53
|
-
constklass = strklass.constantize
|
|
54
|
-
cattrs_to_reset << constklass
|
|
57
|
+
next unless job.has_key?(key)
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
constklass.public_send("#{attribute}=", value)
|
|
58
|
-
end
|
|
59
|
-
end
|
|
59
|
+
klass_attrs[strklass.constantize] = Serializer.deserialize(job[key]).to_h
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
wrap(klass_attrs.to_a, &block)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def wrap(klass_attrs, &block)
|
|
68
|
+
klass, attrs = klass_attrs.shift
|
|
69
|
+
return block.call unless klass
|
|
70
|
+
|
|
71
|
+
retried = false
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
set_succeeded = false
|
|
75
|
+
klass.set(attrs) do
|
|
76
|
+
set_succeeded = true
|
|
77
|
+
wrap(klass_attrs, &block)
|
|
78
|
+
end
|
|
79
|
+
rescue NoMethodError
|
|
80
|
+
# Don't retry if the no method error didn't come from current attributes
|
|
81
|
+
raise if retried || set_succeeded
|
|
82
|
+
|
|
83
|
+
# It is possible that the `CurrentAttributes` definition
|
|
84
|
+
# was changed before the job started processing.
|
|
85
|
+
attrs = attrs.select { |attr| klass.respond_to?(attr) }
|
|
86
|
+
retried = true
|
|
87
|
+
retry
|
|
88
|
+
end
|
|
65
89
|
end
|
|
66
90
|
end
|
|
67
91
|
|
|
@@ -69,8 +93,9 @@ module Sidekiq
|
|
|
69
93
|
def persist(klass_or_array, config = Sidekiq.default_configuration)
|
|
70
94
|
cattrs = build_cattrs_hash(klass_or_array)
|
|
71
95
|
|
|
96
|
+
config.client_middleware.prepend Load, cattrs
|
|
72
97
|
config.client_middleware.add Save, cattrs
|
|
73
|
-
config.server_middleware.
|
|
98
|
+
config.server_middleware.prepend Load, cattrs
|
|
74
99
|
end
|
|
75
100
|
|
|
76
101
|
private
|
|
@@ -11,6 +11,7 @@ module Sidekiq::Middleware::I18n
|
|
|
11
11
|
# to be sent to Sidekiq.
|
|
12
12
|
class Client
|
|
13
13
|
include Sidekiq::ClientMiddleware
|
|
14
|
+
|
|
14
15
|
def call(_jobclass, job, _queue, _redis)
|
|
15
16
|
job["locale"] ||= I18n.locale
|
|
16
17
|
yield
|
|
@@ -20,6 +21,7 @@ module Sidekiq::Middleware::I18n
|
|
|
20
21
|
# Pull the msg locale out and set the current thread to use it.
|
|
21
22
|
class Server
|
|
22
23
|
include Sidekiq::ServerMiddleware
|
|
24
|
+
|
|
23
25
|
def call(_jobclass, job, _queue, &block)
|
|
24
26
|
I18n.with_locale(job.fetch("locale", I18n.default_locale), &block)
|
|
25
27
|
end
|
data/lib/sidekiq/monitor.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require "fileutils"
|
|
4
5
|
require "sidekiq/api"
|
|
@@ -48,14 +49,10 @@ class Sidekiq::Monitor
|
|
|
48
49
|
puts "---- Processes (#{process_set.size}) ----"
|
|
49
50
|
process_set.each_with_index do |process, index|
|
|
50
51
|
# Keep compatibility with legacy versions since we don't want to break sidekiqmon during rolling upgrades or downgrades.
|
|
51
|
-
#
|
|
52
|
-
# Before:
|
|
53
|
-
# ["default", "critical"]
|
|
54
|
-
#
|
|
55
|
-
# After:
|
|
56
|
-
# {"default" => 1, "critical" => 10}
|
|
57
52
|
queues =
|
|
58
|
-
if process["
|
|
53
|
+
if process["capsules"] # 8.0.6+
|
|
54
|
+
process["capsules"].values.map { |x| x["weights"].keys.join(", ") }
|
|
55
|
+
elsif process["weights"]
|
|
59
56
|
process["weights"].sort_by { |queue| queue[0] }.map { |capsule| capsule.map { |name, weight| (weight > 0) ? "#{name}: #{weight}" : name }.join(", ") }
|
|
60
57
|
else
|
|
61
58
|
process["queues"].sort
|
|
@@ -98,13 +95,13 @@ class Sidekiq::Monitor
|
|
|
98
95
|
pad = opts[:pad] || 0
|
|
99
96
|
max_length = opts[:max_length] || (80 - pad)
|
|
100
97
|
out = []
|
|
101
|
-
line = ""
|
|
98
|
+
line = +""
|
|
102
99
|
values.each do |value|
|
|
103
100
|
if (line.length + value.length) > max_length
|
|
104
101
|
out << line
|
|
105
102
|
line = " " * pad
|
|
106
103
|
end
|
|
107
|
-
line << value + "
|
|
104
|
+
line << value + "; "
|
|
108
105
|
end
|
|
109
106
|
out << line[0..-3]
|
|
110
107
|
out.join("\n")
|
data/lib/sidekiq/paginator.rb
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Sidekiq
|
|
4
4
|
module Paginator
|
|
5
|
+
TYPE_CACHE = {
|
|
6
|
+
"dead" => "zset",
|
|
7
|
+
"retry" => "zset",
|
|
8
|
+
"schedule" => "zset"
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
def page(key, pageidx = 1, page_size = 25, opts = nil)
|
|
6
12
|
current_page = (pageidx.to_i < 1) ? 1 : pageidx.to_i
|
|
7
13
|
pageidx = current_page - 1
|
|
@@ -11,7 +17,14 @@ module Sidekiq
|
|
|
11
17
|
ending = starting + page_size - 1
|
|
12
18
|
|
|
13
19
|
Sidekiq.redis do |conn|
|
|
14
|
-
|
|
20
|
+
# horrible, think you can make this cleaner?
|
|
21
|
+
type = TYPE_CACHE[key]
|
|
22
|
+
if type
|
|
23
|
+
elsif key.start_with?("queue:")
|
|
24
|
+
type = TYPE_CACHE[key] = "list"
|
|
25
|
+
else
|
|
26
|
+
type = TYPE_CACHE[key] = conn.type(key)
|
|
27
|
+
end
|
|
15
28
|
rev = opts && opts[:reverse]
|
|
16
29
|
|
|
17
30
|
case type
|
|
@@ -19,9 +32,9 @@ module Sidekiq
|
|
|
19
32
|
total_size, items = conn.multi { |transaction|
|
|
20
33
|
transaction.zcard(key)
|
|
21
34
|
if rev
|
|
22
|
-
transaction.zrange(key, starting, ending, "REV", withscores
|
|
35
|
+
transaction.zrange(key, starting, ending, "REV", "withscores")
|
|
23
36
|
else
|
|
24
|
-
transaction.zrange(key, starting, ending, withscores
|
|
37
|
+
transaction.zrange(key, starting, ending, "withscores")
|
|
25
38
|
end
|
|
26
39
|
}
|
|
27
40
|
[current_page, total_size, items]
|
data/lib/sidekiq/processor.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "sidekiq/fetch"
|
|
4
4
|
require "sidekiq/job_logger"
|
|
5
5
|
require "sidekiq/job_retry"
|
|
6
|
+
require "sidekiq/profiler"
|
|
6
7
|
|
|
7
8
|
module Sidekiq
|
|
8
9
|
##
|
|
@@ -36,7 +37,7 @@ module Sidekiq
|
|
|
36
37
|
@job = nil
|
|
37
38
|
@thread = nil
|
|
38
39
|
@reloader = Sidekiq.default_configuration[:reloader]
|
|
39
|
-
@job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(
|
|
40
|
+
@job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(capsule.config)
|
|
40
41
|
@retrier = Sidekiq::JobRetry.new(capsule)
|
|
41
42
|
end
|
|
42
43
|
|
|
@@ -58,11 +59,15 @@ module Sidekiq
|
|
|
58
59
|
@thread.value if wait
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
def stopping?
|
|
63
|
+
@done
|
|
64
|
+
end
|
|
65
|
+
|
|
61
66
|
def start
|
|
62
67
|
@thread ||= safe_thread("#{config.name}/processor", &method(:run))
|
|
63
68
|
end
|
|
64
69
|
|
|
65
|
-
private
|
|
70
|
+
private
|
|
66
71
|
|
|
67
72
|
def run
|
|
68
73
|
# By setting this thread-local, Sidekiq.redis will access +Sidekiq::Capsule#redis_pool+
|
|
@@ -108,13 +113,17 @@ module Sidekiq
|
|
|
108
113
|
def handle_fetch_exception(ex)
|
|
109
114
|
unless @down
|
|
110
115
|
@down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
111
|
-
logger.error("Error fetching job: #{ex}")
|
|
112
116
|
handle_exception(ex)
|
|
113
117
|
end
|
|
114
118
|
sleep(1)
|
|
115
119
|
nil
|
|
116
120
|
end
|
|
117
121
|
|
|
122
|
+
def profile(job, &block)
|
|
123
|
+
return yield unless job["profile"]
|
|
124
|
+
Sidekiq::Profiler.new(config).call(job, &block)
|
|
125
|
+
end
|
|
126
|
+
|
|
118
127
|
def dispatch(job_hash, queue, jobstr)
|
|
119
128
|
# since middleware can mutate the job hash
|
|
120
129
|
# we need to clone it to report the original
|
|
@@ -128,16 +137,19 @@ module Sidekiq
|
|
|
128
137
|
@retrier.global(jobstr, queue) do
|
|
129
138
|
@job_logger.call(job_hash, queue) do
|
|
130
139
|
stats(jobstr, queue) do
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
profile(job_hash) do
|
|
141
|
+
# Rails 5 requires a Reloader to wrap code execution. In order to
|
|
142
|
+
# constantize the worker and instantiate an instance, we have to call
|
|
143
|
+
# the Reloader. It handles code loading, db connection management, etc.
|
|
144
|
+
# Effectively this block denotes a "unit of work" to Rails.
|
|
145
|
+
@reloader.call do
|
|
146
|
+
klass = Object.const_get(job_hash["class"])
|
|
147
|
+
instance = klass.new
|
|
148
|
+
instance.jid = job_hash["jid"]
|
|
149
|
+
instance._context = self
|
|
150
|
+
@retrier.local(instance, jobstr, queue) do
|
|
151
|
+
yield instance
|
|
152
|
+
end
|
|
141
153
|
end
|
|
142
154
|
end
|
|
143
155
|
end
|
|
@@ -160,7 +172,6 @@ module Sidekiq
|
|
|
160
172
|
begin
|
|
161
173
|
job_hash = Sidekiq.load_json(jobstr)
|
|
162
174
|
rescue => ex
|
|
163
|
-
handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
|
|
164
175
|
now = Time.now.to_f
|
|
165
176
|
redis do |conn|
|
|
166
177
|
conn.multi do |xa|
|
|
@@ -169,15 +180,16 @@ module Sidekiq
|
|
|
169
180
|
xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
|
|
170
181
|
end
|
|
171
182
|
end
|
|
183
|
+
handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
|
|
172
184
|
return uow.acknowledge
|
|
173
185
|
end
|
|
174
186
|
|
|
175
187
|
ack = false
|
|
176
188
|
Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
|
|
177
189
|
Thread.handle_interrupt(ALLOW_SHUTDOWN_INTERRUPTS) do
|
|
178
|
-
dispatch(job_hash, queue, jobstr) do |
|
|
179
|
-
config.server_middleware.invoke(
|
|
180
|
-
execute_job(
|
|
190
|
+
dispatch(job_hash, queue, jobstr) do |instance|
|
|
191
|
+
config.server_middleware.invoke(instance, job_hash, queue) do
|
|
192
|
+
execute_job(instance, job_hash["args"])
|
|
181
193
|
end
|
|
182
194
|
end
|
|
183
195
|
ack = true
|
|
@@ -185,9 +197,14 @@ module Sidekiq
|
|
|
185
197
|
# Had to force kill this job because it didn't finish
|
|
186
198
|
# within the timeout. Don't acknowledge the work since
|
|
187
199
|
# we didn't properly finish it.
|
|
200
|
+
rescue Sidekiq::JobRetry::Skip => s
|
|
201
|
+
# Skip means we handled this error elsewhere. We don't
|
|
202
|
+
# need to log or report the error.
|
|
203
|
+
ack = true
|
|
204
|
+
raise s
|
|
188
205
|
rescue Sidekiq::JobRetry::Handled => h
|
|
189
206
|
# this is the common case: job raised error and Sidekiq::JobRetry::Handled
|
|
190
|
-
# signals that we created a retry successfully. We can
|
|
207
|
+
# signals that we created a retry successfully. We can acknowledge the job.
|
|
191
208
|
ack = true
|
|
192
209
|
e = h.cause || h
|
|
193
210
|
handle_exception(e, {context: "Job raised exception", job: job_hash})
|
|
@@ -206,8 +223,8 @@ module Sidekiq
|
|
|
206
223
|
end
|
|
207
224
|
end
|
|
208
225
|
|
|
209
|
-
def execute_job(
|
|
210
|
-
|
|
226
|
+
def execute_job(instance, cloned_args)
|
|
227
|
+
instance.perform(*cloned_args)
|
|
211
228
|
end
|
|
212
229
|
|
|
213
230
|
# Ruby doesn't provide atomic counters out of the box so we'll
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "sidekiq/component"
|
|
3
|
+
|
|
4
|
+
module Sidekiq
|
|
5
|
+
# Allows the user to profile jobs running in production.
|
|
6
|
+
# See details in the Profiling wiki page.
|
|
7
|
+
class Profiler
|
|
8
|
+
EXPIRY = 86400 # 1 day
|
|
9
|
+
DEFAULT_OPTIONS = {
|
|
10
|
+
mode: :wall
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
include Sidekiq::Component
|
|
14
|
+
|
|
15
|
+
def initialize(config)
|
|
16
|
+
@config = config
|
|
17
|
+
@vernier_output_dir = ENV.fetch("VERNIER_OUTPUT_DIR") { Dir.tmpdir }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(job, &block)
|
|
21
|
+
return yield unless job["profile"]
|
|
22
|
+
|
|
23
|
+
token = job["profile"]
|
|
24
|
+
type = job["class"]
|
|
25
|
+
jid = job["jid"]
|
|
26
|
+
started_at = Time.now
|
|
27
|
+
|
|
28
|
+
rundata = {
|
|
29
|
+
started_at: started_at.to_i,
|
|
30
|
+
token: token,
|
|
31
|
+
type: type,
|
|
32
|
+
jid: jid,
|
|
33
|
+
# .gz extension tells Vernier to compress the data
|
|
34
|
+
filename: File.join(
|
|
35
|
+
@vernier_output_dir,
|
|
36
|
+
"#{token}-#{type}-#{jid}-#{started_at.strftime("%Y%m%d-%H%M%S")}.json.gz"
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
profiler_options = profiler_options(job, rundata)
|
|
40
|
+
|
|
41
|
+
require "vernier"
|
|
42
|
+
begin
|
|
43
|
+
a = Time.now
|
|
44
|
+
rc = Vernier.profile(**profiler_options, &block)
|
|
45
|
+
b = Time.now
|
|
46
|
+
|
|
47
|
+
# Failed jobs will raise an exception on previous line and skip this
|
|
48
|
+
# block. Only successful jobs will persist profile data to Redis.
|
|
49
|
+
key = "#{token}-#{jid}"
|
|
50
|
+
data = File.read(rundata[:filename])
|
|
51
|
+
redis do |conn|
|
|
52
|
+
conn.multi do |m|
|
|
53
|
+
m.zadd("profiles", Time.now.to_f + EXPIRY, key)
|
|
54
|
+
m.hset(key, rundata.merge(elapsed: (b - a), data: data, size: data.bytesize))
|
|
55
|
+
m.expire(key, EXPIRY)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
rc
|
|
59
|
+
ensure
|
|
60
|
+
FileUtils.rm_f(rundata[:filename])
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def profiler_options(job, rundata)
|
|
67
|
+
profiler_options = (job["profiler_options"] || {}).transform_keys(&:to_sym)
|
|
68
|
+
profiler_options[:mode] = profiler_options[:mode].to_sym if profiler_options[:mode]
|
|
69
|
+
|
|
70
|
+
DEFAULT_OPTIONS.merge(profiler_options, {out: rundata[:filename]})
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|